【数据结构】加权并查集

209 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第7天,点击查看活动详情

之前已经简单介绍过了并查集,如果并查集维护了集合内元素之间关系的有无,在我们给边附加了权值之后,加权并查集就可以维护“关系是什么”了。下面我们还是从一个经典例题入手对加权并查集进行简单说明。

问题定义

让我们直接来看一道题目。

题目链接

P2024 [NOI2001] 食物链 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

题目大意

image.png

思路

没错就是超级经典的食物链qwq~

虽然可以用扩展域并查集来实现,但是我们在这里用它为载体来讲解加权并查集。我们刚才说了:

加权并查集可以维护“关系是什么”

在本题中需要维护的关系三种,我们开一个数组 ww 进行记录:

  • w[x]=1w[x]=1,表示 xxf[x]f[x]
  • w[x]=2w[x]=2,表示 xxf[x]f[x] 吃。
  • w[x]=0w[x]=0,表示 xxf[x]f[x] 是同类。

查询

那么我们应该怎样通过 xxf[x]f[x] 之间的关系得到 xx 和根节点之间的关系呢?

image.png

对于上图,我们可以列出如下关系表:

e1e_1e2e_2e(A,C)e_{(A,C)}解释
000A 和 B 是同类,B 和 C 是同类,所以 A 和 C 是同类。
011A 和 B 是同类,B 吃 C,所以 A 吃 C。
022A 和 B 是同类,B 被 C 吃,所以 A 被 C 吃。
101A 吃 B,B 和 C 是同类,所以 A 吃 C。
112A 吃 B,B 吃 C,所以 A 被 C 吃。
120A 吃 B,B 被 C 吃,所以 A 和 C 是同类。
202A 被 B 吃,B 和 C 是同类,所以 A 被 C 吃。
210A 被 B 吃,B 吃 C,所以 A 和 C 是同类。
221A 被 B 吃,B 被 C 吃,所以 A 吃 C。

容易发现 e(A,C)=(e1+e2)mod3e_{(A,C)}=(e_1+e_2)\mod 3,我们找根的递归过程中应每次进行转移,所以我们寻找代表元素时路径压缩的代码可以实现为:

int find(int x)
{
	if (x==f[x]) return x;
	int t=find(f[x]);
	w[x]=(w[x]+w[f[x]])%3;
	return f[x]=t;
}

判断与合并

由于我们转移 ww 时进行了路径压缩,所以我们执行完 find(x)find(x) 之后,f[x]f[x] 就是 xx 所在集合的代表元素,w[x]w[x] 就是 xx 与其所在集合代表元素之间的关系值。

如果 xxyy 在同一个集合中,我们需要判断他们两个之间的关系是否和输入的一致。 如果 xxyy 不在同一个集合中,我们需要将两个集合合并。具体判断方式也可以利用列表格或简单推理求出,详见代码。

代码

#include <iostream>
using namespace std;
int f[50001],w[50001],n,k,x,y,z,ans,i;
int find(int x)
{
	if (x==f[x]) return x;
	int t=find(f[x]);
	w[x]=(w[x]+w[f[x]])%3;	//执行完find后,x的父亲指向代表元素,w[t]得到更新,更新w[x] 
	return f[x]=t;
}
int main()
{
	cin>>n>>k;
	for (i=1;i<=n;++i) f[i]=i;
	for (i=1;i<=k;++i)
	{
		cin>>z>>x>>y;
		if (x>n||y>n) ans++;
		else 
		{
			if (find(x)!=find(y))//若x和y不在一个并查集中,将其所在并查集合并 
	    	{ 
		    	if (w[x]==0) w[f[x]]=z-1;//若x和它的祖先是同类,则x和y的关系即为x的祖先与y的关系
		    	else if (w[x]==z-1) w[f[x]]=0;//若x和它的祖先的关系和x和y的关系相同,则x的祖先和y是同类 
		    	    else if (z==1) //若x和y是同类 
		    	        {//若x吃x的祖先,则x的祖先被y吃   
						//若x被x的祖先吃,则x的祖先吃y 
		    	    	    if (w[x]==1) w[f[x]]=2;
		    	    	    if (w[x]==2) w[f[x]]=1;
					    }
					    else w[f[x]]=2;//此时w[x]!=0,z!=1,w[x]!=z-1
						 //所以z=2,即z-1=1,w[x]=2,
						 //即x吃y,x被x的祖先吃,所以x的祖先被y吃 
				f[f[x]]=y;//令x的祖先的父亲为y
    		}
	    	else //若x和y在一个并查集中,查询新关系是否与其旧关系冲突 
		    {
			    if (z==2&&w[x]-w[y]!=1&&w[x]-w[y]!=-2) ans++;//若x与其祖先的关系和y与其祖先的关系相同,则说明x和y是同类 
    			if (z==1&&w[x]!=w[y]) ans++;//当。。。被满足时,x吃y 
	    	}
	    }
    }
    cout<<ans;
    return 0;
}  

Other

Codeforces Round #836 (Div. 2) E. Tick, Tock 可以用加权并查集做,后续更新题解。