POJ1182 食物链 并查集 带权或者扩展域 详细分析 适合新手

描述

动物王国中有三类动物A,B,C,这三类动物的食物链构成了有趣的环形。A吃B, B吃C,C吃A。
现有N个动物,以1-N编号。每个动物都是A,B,C中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这N个动物所构成的食物链关系进行描述:
第一种说法是"1 X Y",表示X和Y是同类。
第二种说法是"2 X Y",表示X吃Y。
此人对N个动物,用上述两种说法,一句接一句地说出K句话,这K句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
1) 当前的话与前面的某些真的话冲突,就是假话;
2) 当前的话中X或Y比N大,就是假话;
3) 当前的话表示X吃X,就是假话。
你的任务是根据给定的N(1 <= N <= 50,000)和K句话(0 <= K <= 100,000),输出假话的总数。

输入

第一行是两个整数N和K,以一个空格分隔。
以下K行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中D表示说法的种类。
若D=1,则表示X和Y是同类。
若D=2,则表示X吃Y。

输出

只有一个整数,表示假话的数目。

样例输入

100 7
1 101 1 
2 1 2
2 2 3 
2 3 3 
1 1 3 
2 3 1 
1 5 5

样例输出

3

两种搞法。

第一种是扩展域并查集,这种并查集适用于变量间关系不止一种,比如除了等于,还有不等于的关系需要并起来。

或者是奇偶性,同性,或者异性,连续2次异性等于同性。

第二种是带权并查集,这种适用于可以赋值权值的并查集。合并集合的时候利用矢量和进行链接2个集合的边权关系。

这一题有3中关系,同类,吃,被吃。

(一条链上)连续吃3次(相当于连续集合并三次)等于同类,连续被吃3次等于同类,连续吃两次等于天敌,连续被吃两次等于猎物。上面的关系有的绕  用A->B->C->D来说,链上连续吃,那么D一定与A 是同类。连续被吃A,D也一定是同类。

如果有一个信息,A,B同类,那么如果A吃B 或者B吃A,这个信息都是错的。

如果有一个信息,A吃B,那么如果A.B同类 或者B吃A,这个信息都是错的。

以上判断条件,不重不漏。那我们如何去做呢?

第一种。

扩展域并查集,我们为每个动物都开3个域,同类域self,捕食域eat,天敌域enemy

先说合并集合

如果A,B同类,那不用说,三个域肯定都相同。

如果A吃B,那么A的捕食域与B的同类域并起来,A的捕食域与B的天敌域并起来,A的天敌域与B的捕食域并起来。

那么如果2个动物A,B  A的捕食域与B的同类域在同一集合,代表A吃B,

这样搞我们就能进行之前说的判断了。

还有一个问题,为啥保证链能连上。

你想啊,如果A吃B,B吃C,C吃D,(如果两个物种同类只是把三个域平行连不交叉,不影响结果)

A同类域连B天敌域(代表了   B的天敌是与A是一类),  B天敌域连C捕食域(代表了 B的天敌和C的捕食是一类 ),C捕食域连D同类域(代表了 C的捕食与D的同类,是一类)链式连的都是一类动物,说明A,B的天敌,C的捕食,D是一类,正好满足了捕食成环的关系,是不是很巧妙 多想想自己敲一遍就明白了。

#include<bits/stdc++.h>
using namespace std;
const int M = 500000+100;
int fa[M],eat[M];
int get(int x)
{
	if(fa[x]==x)return x;
	return fa[x]=get(fa[x]);
}
int main()
{
	int n,k;
	cin>>n>>k;
	for(int i=1;i<=n+n+n;i++)
	fa[i]=i;
	int cnt=0;
	int ec=0;
	while(k--)
	{
		int d,x,y;
		scanf("%d %d %d",&d,&x,&y);
		if(x>n||y>n||(d==2&&x==y))
		{
			cnt++;
			continue;
		}
		int gx_self=get(x),gx_eat=get(x+n),gx_enemy=get(x+n+n);
		int gy_self=get(y),gy_eat=get(y+n),gy_enemy=get(y+n+n);
		if(d==1)
		{
			if(gx_self==gy_eat||gx_eat==gy_self)
			{
				cnt++;
				continue;
			}
			fa[gx_self]=gy_self,fa[gx_eat]=gy_eat,fa[gx_enemy]=gy_enemy;
		}
		else
		{
			if(gx_self==gy_self||gy_eat==gx_self)
			{
				cnt++;
				continue;
				
			}
			fa[gx_self]=gy_enemy,fa[gx_eat]=gy_self,fa[gx_enemy]=gy_eat;
		}
	}
	printf("%d\n",cnt);
	return 0;
}

另一种就是带权并查集 本质是一样,不过在合并集合和判断的适合有些区别。

基本套路是,边权维护与父亲节点的关系,然后合并集合的时候(假设让x的祖先的父节点变成y的祖先)用矢量和计算法则 根据x,y的边权  计算x的祖先应该被赋予的值。(每个集合祖先的边权都是0,给祖先赋值是为了把2个集合之前差的关系传递(通过路径压缩)给子孙)。然后如果x,y都在一个集合里就可以直接根据x,y的边权来进行判断了。

这一题我们可以让边权为与父亲吃或被吃的 关系。

当边权==0,与父亲同类;

当边权==1,吃父亲;

当边权==2,被父亲吃;

每个集合的祖先默认边权为0;

那么子孙与父亲的关系,通过路径压缩时,从上往下,的边权等于自身加上父亲的边权,d[x]=(d[fa[x]]+d[x])%3;

因为,

当d[fa[x]]=1,d[x]=0,时,x的父亲吃x的爷爷,x与父亲同类,那x就吃父亲。d[x]=1;

……1,……1,x的父亲吃x的爷爷,x吃父亲,那x就被爷爷吃。d[x]=2;(别忘了,只有三个物种环状吃与被吃)

……0,……1,x的父亲与x的爷爷同类,x吃父亲,那x就吃爷爷。d[x]=1;

……1,……2,x的父亲吃x的爷爷,x被父亲吃,那x与x的爷爷同类。(都被x的父亲吃)d[x]=0;

……2,……1,x的父亲被x爷爷吃,x吃父亲,那x与爷爷同类(都吃x的父亲)d[x]=0;

……2,……2,x的父亲被x的爷爷吃,x被父亲吃,那x吃x的爷爷(相当于倒着环吃)d[x]=1;

就上面6种情况,搞明白一次以后再做带权并查集就好理解了。

#include<bits/stdc++.h>
using namespace std;
const int M = 100000+100;
int fa[M],d[M];
//d[]==1吃父亲,d[]==2被父亲吃,d[]==0和父亲同类 
int get(int x)
{
	if(fa[x]==x)return x;
	int root = get(fa[x]);
	d[x]=(d[fa[x]]+d[x])%3;//路径压缩从祖先开始向下传递
	return fa[x]=root;
}
int merge(int D,int x,int y)
{
	int gx=get(x),gy=get(y);
	if(gx==gy)
	{
	//	printf("%d----%d    %d  %d\n",d[x],d[y],gx,gy);
		if((d[x]-d[y]+3)%3!=D-1)
			return 1;
		return 0;
	}
	fa[gx]=gy;
	d[gx]=(D-1+d[y]-d[x]+3)%3;//为了让x所在集合的点的边权都加上x,y集合的边权矢量差。
//	puts("oo");
	return 0;
}
int main()
{
	int n,k;
	cin>>n>>k;
	for(int i=1;i<=n;i++)
	fa[i]=i,d[i]=0;
	int cnt=0;
	while(k--)
	{
		int D,x,y;
		scanf("%d%d%d",&D,&x,&y);
		int gx=get(x),gy=get(y);
		if(D==2&&x==y||x>n||y>n||merge(D,x,y))
		{
		//	puts("1111");
			cnt++;
			continue;
		}
	}
	printf("%d\n",cnt);
	return 0;
 } 

猜你喜欢

转载自blog.csdn.net/bjfu170203101/article/details/89035824