POJ 1182 食物链(带权并查集)

时间:2021-11-19 19:37:30

传送门

食物链 
Time Limit: 1000MS   Memory Limit: 10000K
Total Submissions: 65579   Accepted: 19336

Description

动物王国中有三类动物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),输出假话的总数。

Input

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

Output

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

Sample Input

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

Sample Output

3

思路

解法一:来自《挑战程序设计竞赛》

由于N和K很大,所以必须高效地维护动物之间的关系,并快速判断是否产生了矛盾。并查集是维护 “属于同一组” 的数据结构,但是在本题中,并不只有属于同一类的信息,还有捕食关系的存在。因此需要开动脑筋维护这些关系。

对于每只动物i创建3个元素i-A, i-B, i-C, 并用这3*N个元素建立并查集。这个并查集维护如下信息:

  • i-x 表示 “i属于种类x”。
  • 并查集里的每一个组表示组内所有元素代表的情况都同时发生或不发生。

例如,如果i-Aj-B在同一个组里,就表示如果i属于种类A那么j一定属于种类B,如果j属于种类B那么i一定属于种类A。因此,对于每一条信息,只需要按照下面进行操作就可以了。

  • 第一种,x和y属于同一种类———合并x-A和y-A、x-B和y-B、x-C和y-C。
  • 第二种,x吃y—————————合并x-A和y-B、x-B和y-C、x-C和y-A。

不过在合并之前需要先判断合并是否会产生矛盾。例如在第一种信息的情况下,需要检查比如x-Ay-B或者y-C是否在同一组等信息。

 解法二:加权并查集

思路来源于:click here

Part I - 权值(relation)的确定。

    注:每个节点对应的 relation[]值记录他与根节点的关系:

      我们根据题意,森林中有3种动物。A吃B,B吃C,C吃A。
    我们还要使用并查集,那么,我们就以动物之间的关系来作为并查集每个节点的权值。
    注意,我们不知道所给的动物(题目说了,输入只给编号)所属的种类。

    所以,我们可以用动物之间“相对”的关系来确定一个并查集。

    • 0 - 这个节点与它的父节点是同类
    • 1 - 这个节点被它的父节点吃
    • 2 - 这个节点吃它的父节点

    注意,这个0,1,2所代表的意义不是随便制定的,我们看题目中的要求。
    输入的时候,第一个数字(下文中,设为d)指定了后面两种动物的关系:

    • 1 - X与Y同类
    • 2 - X吃Y

    我们注意到:

    • 如果d == 1 则 x 和 y 是同类 ,那么 y 对 x 的关系是 0
    • 如果d == 2 则 x 吃了 y, 那么 y 对 x 的关系是 1, x 对 y 的关系是 2.
    • 综上所述 ,无论 d为1 或者是为 2,  y 对 x 的关系都是 d-1

    所以,这个0,1,2不是随便选的

Part II - 路径压缩,以及节点间关系确定
    确定了权值之后,我们要确定有关的操作。
    我们把所有的动物全初始化 relation[i]=0,f[i]=i

    (1)路径压缩时的节点算法

    通过穷举我们可以发现

      f[now]=f[f[now]]

      relation[now]=(relation[now]+relation[f[now]]) % 3

    这个路径压缩算法是正确的
    关于这个路径压缩算法,还有一点需要注意的地方,我们一会再谈
    注意,根据当前节点的relation和当前节点父节点的relation推出
    当前节点与其父节点的父节点的relation这个公式十分重要!!
    它推不出来下面都理解不了!!自己用穷举法推一下:
    好吧,为了方便伸手党,我给出穷举过程
                 i      j
        爷爷  父亲 儿子      儿子与爷爷
           0      0      (i + j)%3 = 0
           0      1      (i + j)%3 = 1
           0      2      (i + j)%3 = 2
           1      0      (i + j)%3 = 1
           1      1      (i + j)%3 = 2
           1      2      (i + j)%3 = 0
           2      0      (i + j)%3 = 2
           2      1      (i + j)%3 = 0
           2      2      (i + j)%3 = 1
    这样可以看到,( 儿子relation + 父亲relation ) % 3 = 儿子对爷爷的relation

    所以有relation[now]=(relation[now]+relation[f[now]]) % 3
    这就是路径压缩的节点算法

    (2) 集合间关系的确定

        当确定D X Y 的关系的正确性后,要把二者的集合合并为一个集合。就是把Y的集合的根节点其父亲设置为X的集合的根节点。

        y的根节点与x的根节点间的关系(即fy与fx的关系):
        relation[find(y)]=(3-relation[y]+(d-1)+relation[x]) % 3;
        这个公式,是分三部分,这么推出来的:
        ( d - 1 ) :这是X和Y之间的relation,X是Y的父节点时,Y的relation就是这个
        3 - relation[y] = 根据Y与根节点的关系,逆推根节点与Y的关系
        这部分也是穷举法推出来的,我们举例:
              0(父子同类)( 3 - 0 ) % 3 = 0
              1(父吃子) ( 3 - 1 ) % 3 = 2  //父吃子
              2(子吃父) ( 3 - 2 ) % 3 = 1  //子吃父

        所以有y的父结点与x的父结点关系relation[find(y)]=(3-relation[y]+(d-1)+relation[x]) % 3;
        注意,这个当所有集合都是初始化状态的时候也适用

POJ 1182 食物链(带权并查集)

Part III - 判断   

      每次输入一组数据 d, x, y判断是否超过 N 后,先通过find()函数找他们的根节点从而判断他们是否在同一棵树中。(也就是是否有确定的关系)

      1.如果在同一棵树中find(x) == find(y):直接判断是否说谎。

      1)如果 d ==1,那么 x 与 y 应该是同类,他们的r[]应该相等,如果不相等,则说谎数 +1

      2)如果 d==2,那么 x 应该吃了 y,也就是 (r[x]+1)%3 == r[y]如果不满足,则说谎数 +1

         如何判断 x 吃了 y 是  (r[x]+1)%3 == r[y],请看下图:(PS:箭头方向指向被吃方)

POJ 1182 食物链(带权并查集)

      2.如果不在同一棵树中:那么合并 x 与 y 分别所在的树。

        合并树时要注意顺序,我这里是把 x 树的根当做主根,否则会WA的很惨

        再次提醒:找父亲节点时,要不断更新 r[]的值。这里有一个关系:如果 x 和y 为关系 r1, y 和 z 为关系 r2,那么 x 和z的关系就是 (r1+r2)%3,证明过程上面已经论证过了。

#include<stdio.h>
#include<string.h>
const int maxn = 50005;
int N,fa[maxn*3];

int find(int x)
{
	int r = x;
	while (r != fa[r])	r = fa[r];
	int i = x,j;
	while (i != r)
	{
		j = fa[i];
		fa[i] = r;
		i = j;
	}
	return r;
}

void unite(int x,int y)
{
	x = find(x),y = find(y);
	if (x != y)	fa[x] = y;
}

bool same(int x,int y)
{
	return find(x) == find(y);
}

int main()
{
	int K,D,X,Y,res = 0;
	scanf("%d%d",&N,&K);
	for (int i = 1;i <= 3*N;i++)	fa[i] = i;
	while (K--)
	{
		scanf("%d%d%d",&D,&X,&Y);
		if (X < 1 || X > N || Y < 1 || Y > N)
		{
			res++;
			continue;
		}
		if (D == 1)
		{
			if (same(X,Y+N) || same(X,Y+2*N))	res++;
			else	unite(X,Y),unite(X+N,Y+N),unite(X+2*N,Y+2*N);
		}
		else if (D == 2)
		{
			if (same(X,Y) || same(X,Y+2*N))	res++;
			else	unite(X,Y+N),unite(X+N,Y+2*N),unite(X+2*N,Y);
		}
	}
	printf("%d\n",res);
	return 0;
} 
 
#include<stdio.h>
const int maxn =  50005;
int r[maxn],fa[maxn];

int find(int x)
{
	if (x == fa[x])	return fa[x];
	int tmp = fa[x];
	fa[x] = find(fa[x]);
	r[x] = (r[x] + r[tmp])%3;
	return fa[x];
}

void unite(int x,int y,int d)
{
	int fx = find(x),fy = find(y);
	fa[fy] = fx;             //y被x吃,所以以x作为y的根节点
	r[fy] = (r[x] - r[y] + 3 + (d - 1))%3;
}

int main()
{
	int N,K,X,Y,D,res = 0;
	scanf("%d%d",&N,&K);
	for (int i = 1;i <= N;i++)	r[i] = 0,fa[i] = i;
	while (K--)
	{
		scanf("%d%d%d",&D,&X,&Y);
		if (X > N || Y > N ||(X == Y && D == 2))	res++;
		else if (find(X) == find(Y))
		{
			if (D == 1 && r[X] != r[Y])	res++;
			if (D == 2 && (r[X] + 1) % 3 != r[Y])	res++;
		}
		else unite(X,Y,D);
	}
	printf("%d\n",res);
	return 0;
}