食物链

动物王国中有三类动物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


代码我粘别人的,我来用我自己的话我解释一下这代码的意思

这道题让我认识到了并查集的作用如此之大。

原来,并查集是关系的汇总!

我之前做的都是并查集的裸题,但是这道题开拓了我的思路。

刚刚说了,并查集其实就是关系的汇总,如果有关系,我们就把他们放到一起,但是!

放到一起仅仅是放到了一起,一个节点链接一个节点,并没有体现其中的关系。于是我们用类似的方法。就是建立一个类似数组,记录与头节点之间的关系。

只要关系记录好了,那么这个并查集所表示的关系就是完整的。

此题,就是定义了这样的一个数组。

我们首先来思考一下,如果两个节点有关系,那么会是什么样的关系呢?

1.儿子节点可以吃父亲节点(2)

2.父亲节点可以吃儿子节点(1)

3.儿子接地与父亲节点是一样的物种(0)

知道了这三种关系,我们就有三种状态,分别定义这三种状态为 2 1 0

1.首先初始化,每个节点初始化状态都为0(表示自己与自己是一个物种)

2.如何找到自己的boss节点,并且更新关系+压缩路径:

为了提高效率与优化题目,我们这里并不是每拿到一个节点就去从叶节点经过一个很深的路径遍历到头结点,而是通过压缩路径来让叶节点直接连接到根节点,但是这样做的话

一定会打破原来的连接关系,连接关系的打破就需要更新一下他们的关系。如果压缩了路径,叶节点都链接头结点,更新的关系自然也就是更新头结点与叶节点之间的关系,

那么如何更细呢?有如下方法

首先从路径底部遍历到头结点,在遍历过程中的每一层记录下当前节点的上一个节点。通过上一个节点就能确定当前节点与(上一个节点的上一个节点)的关系。也就是隔着的节点的关系。

int find(int x) //查找根结点  
{  
    int temp;  
    if(x == p[x].pre)  
        return x;  
    temp = p[x].pre; //路径压缩  
    p[x].pre = find(temp);  
    p[x].relation = (p[x].relation + p[temp].relation) % 3; //关系域更新  
    return p[x].pre; //根结点  
}  
这样的话,压缩路径+更细节点任务已经完成

3.如何合并

如果两个节点的boss节点不一样的也就意味着他们之间还没有确定关系,所以你需要合并

合并我们只需要直接把一个并查集的头节点挂到另一个并查集的头结点的上面或者下面就可以了

关键是,你改变了一个节点的位置,你就需要更新你的关系

比如说你进行如下操作

p[root2].pre = root1; 

这个意思就是把root2挂到root1的名下

因为root2之前没有上一个元素而现在有了,那么就要更新root2的关系

那么又是如何更新的呢。我们知道了这两个节点的关系,那我们也知道了他们与各自头结点之间的关系(压缩路径的时候),这时候能够唯一确定两个头结点之间的关系。

这里关系直接给出

        if(root1 != root2) // 合并  
        {  
            p[root2].pre = root1;  
            p[root2].relation = (3 + (ope - 1) +p[a].relation - p[b].relation) % 3;  
        }
注意:当你把节点合并的时候,root2点的关系虽然更新了,但是与root2链接的节点的关系还是没有更新的。这里来解释一下,虽然这些节点还是没有更新,但是如果你有机会
用到这些节点的时候他们是不会让你失望的,当你下一次使用这里面的节点的时候,由于路径压缩,他们又会从新顺出新的关系来
4.判断

什么时候他们的关系就是错的呢,唯一的可能就是他们在同一个关系之中,然后你输入的关系与之前的关系不太一样,就错了

如果在一个关系中,并且你输入了1(a与b是一个相等关系),那么此时如果对应他们的关系节点的值不相同,则肯定是错误的

如果你输入了2(a,b捕猎关系),这时候也可以根据一定的数学规律判断出来。说实话这个规律比较难搞,这里就说下这个题大体上的思路

下面给出判断关系

 if(ope == 1 && p[a].relation != p[b].relation)  
            {  
                sum++;  
                continue;  
            }  
            if(ope == 2 && ((3 - p[a].relation + p[b].relation) % 3 != ope - 1))  
            {  
                sum++;  
                continue;}  

5.完整代码

#include
#include
#include
#include
using namespace std;
#define N 50010

struct node
{
	int pre;
	int relation;
};
node p[N];

int find(int x) //查找根结点
{
	int temp;
	if(x == p[x].pre)
		return x;
	temp = p[x].pre; //路径压缩
	p[x].pre = find(temp);
	p[x].relation = (p[x].relation + p[temp].relation) % 3; //关系域更新
	return p[x].pre; //根结点
}

int main()
{
	int n, k;
	int ope, a, b;
	int root1, root2;
	int sum = 0; //假话数量
	scanf("%d%d", &n, &k);
	for(int i = 1; i <= n; ++i) //初始化
	{
		p[i].pre = i;
		p[i].relation = 0;
	}
	for(int i = 1; i <= k; ++i)
	{
		scanf("%d%d%d", &ope, &a, &b);
		if(a > n || b > n) //条件2
		{
			sum++;
			continue;
		}
		if(ope == 2 && a == b) //条件3
		{
			sum++;
			continue;
		}
		root1 = find(a);
		root2 = find(b);
		if(root1 != root2) // 合并
		{
			p[root2].pre = root1;
			p[root2].relation = (3 + (ope - 1) +p[a].relation - p[b].relation) % 3;
		}
		else
		{
			if(ope == 1 && p[a].relation != p[b].relation)
			{
				sum++;
				continue;
			}
			if(ope == 2 && ((3 - p[a].relation + p[b].relation) % 3 != ope - 1))
			{
				sum++;
				continue;}
		}
	}
	printf("%d\n", sum);
	return 0;
}




你可能感兴趣的:(并查集)