并查集运用

本文探讨了并查集在图论中的应用,包括解决连通性问题、扩展域并查集(敌人的敌人是朋友)、带权并查集的构建和操作,以及在P1892团伙、P1525关押罪犯等实际问题中的实例。通过路径压缩、集合合并和权值计算,展示了如何利用并查集技术处理复杂逻辑关系。
摘要由CSDN通过智能技术生成

并查集运用

常见的要解决的问题

  • 解决图的连通性问题:连通块大小,点与点之间是否连通,Kruskal 最小生成树算法
  • 维护数据间的集合关系或逻辑关系

扩展域并查集(种类并查集)

考虑“敌人的敌人是朋友”这一原则,令一个并查集用于查询元素之间的关系,称之为原域,为了记录敌人,再开一个并查集,称之为扩展域。
在这里插入图片描述
现在假设 i i i j j j 敌对,那么根据上述定义,令原域的 i i i 与扩展域的 j j j 连边,再令原域的 j j j 与扩展域的 i i i 连边,表达敌对关系。若此时有另一敌对关系 ( j , k ) (j,k) (j,k) ,那么完成连边后,可以得到下图:
在这里插入图片描述
这时我们可以发现,如果我们在原域查询 i , k i,k i,k ,可以得到友好关系,完成了“敌人的敌人是朋友”的表达。
由此可知:种类并查集可以维护对立关系,而且可以维护多对等价双向的对立关系,可以维护“敌人的敌人是朋友”这一原则。

  • 查询祖先:与普通并查集一样,同样可以用路径压缩

  • 合并集合:

    void join(int x,int y)
    {
        //...与普通并查集一样
    }
    ...
        join(x,y + n);//连两次
    	join(y,x + n);
    ...
    
  • 询问是否在同一集合中,如果是,则友好关系,如果不是,则敌对关系:与普通并查集一样

带权并查集

对于“敌人的敌人是朋友”这一原则,我们考虑用模 2 运算表达,那么每次连边合并,我们就要赋予边权。
现在考虑实现以下要求:

  • 敌人之间的路径权值模 2 为 1
  • 朋友之间的路径权值模 2 为 0

容易想到以下构造:
在这里插入图片描述
黑色标号为加边的顺序,红色标号为边的权值,那么这个关系是 r t rt rt f x f_x fx 敌对, f x f_x fx x x x 敌对,而 r t rt rt x x x 友好,同时它们之间的路径权值和模 2 为 0.
现在我们定义 v a l ( x ) val(x) val(x) 表示 x x x 到父亲结点的路径上的权值和。

考虑能否在带权并查集上实现路径压缩

可以实现路径压缩,但是路径压缩的过程中,被压缩掉的边的权值和要记录下来,并与结点到父结点的边权一同组成新边权,这条新边从这个结点到根结点。
v a l ′ ( x ) = v a l ( x ) + S val'(x) = val(x) + S val(x)=val(x)+S
其中 S S S 可以在递归的过程中计算出,并最后赋值在 v a l ′ ( f x ) val'(f_x) val(fx)
代码如下:

int findx(int x)
{
	int tmp = fa[x];
	if(tmp != x)
	{
		fa[x] = findx(fa[x]);
		val[x] = (val[x] + val[tmp])%2'
		return fa[x];
	}
	else return tmp;
}
考虑如何合并两点所在的两个不同的集合

合并两点所在的两个不同集合,关键在于修改这两个集合的根之间的边的权值,即知道 x x x f x f_x fx 之间的关系,知道 y y y f y f_y fy 之间的关系,知道 x x x y y y 的关系,求 f x f_x fx f y f_y fy 之间的关系。
现在我们压缩路径之后,得到 v a l ( x ) val(x) val(x) 表示 x x x 到根结点的边权和。
在这里插入图片描述
所以在合并两个结合的时候,要修改 x x x 的根结点 f x f_x fx f y f_y fy 的权值,令权值 v a l ( f x ) = v a l ( y ) − v a l ( x ) + r val(f_x) = val(y) - val(x) + r val(fx)=val(y)val(x)+r

void join(int x,int y)
{
	int rx = findx(x);int ry = findx(y);
	fa[rx] = ry;
	val[rx] = (((val[y] - val[x])%2 + 2)%2 + 1) % 2;
	return ;
}
考虑如何计算两点间的权值

一般来说,只有当两点在同一连通块中才可以计算两点间的权值,那么令 x x x 到根结点的权值和为 v a l ( x ) val(x) val(x) ,令 y y y 到根结点的权值和为 v a l ( y ) val(y) val(y),同样观察上图的情况,可以得到 r = v a l ( x ) − v a l ( y ) r = val(x) - val(y) r=val(x)val(y)

综上,我们得到了带权并查集的基本操作,于是就可以运用它来解题。

小结

带权并查集和扩展域并查集均可以表达“敌人的敌人是朋友这一原则”,事实上,它们都可以表达两种及以上的关系,可以参考[NOI2001]食物链,这题维护了三对对立关系。通过稍加改编,我们可以利用这些新手段解决更为灵活的题目。

集合关系与逻辑关系问题

比较常见的划分集合思想是:

  • 与我的朋友是朋友的人是我的朋友
  • 与我敌对的人有敌对关系的人是我的朋友

逻辑关系

传递性:如果某种逻辑存在传递性,那么满足这种逻辑的元素应该被划分为同一个集合

对立性:如果某种逻辑存在对立性,那么满足这种逻辑的元素不应该被划分为同一个集合(注意:是不应该被划分为一个集合,而不是应该被划分为另一集合)

P1892 [BOI2003]团伙

直接运用上述划分集合的思想即可

P1525 [NOIP2010 提高组] 关押罪犯

题解,涉及一题多解

P1955 [NOI2015] 程序自动分析

= = = 具有传递性, ≠ \neq = 使元素间对立。

x i = x j x_i=x_j xi=xj ,则将 i , j i,j i,j 划入同一集合中。

x i ≠ x j x_i\neq x_j xi=xj ,则判断 i , j i,j i,j 是否在同一集合中,如果在,那么就矛盾,如果不在,就不矛盾。

P4047 [JSOI2010]部落划分

本题划分集合的标准是居住点之间的距离,看到求最大最近距离,与规定划分 k k k 个部落,考虑二分答案。

思路:二分最大最近距离,检查两两居住点之间是否小于这个距离,如果小于这个距离,这两个居住点就要划分为同一个部落,划分完毕后检查划分了多少个部落,如果划分多了,就意味着二分的答案小了,如果划分少了,就意味着二分的答案大了。

出现的问题:

如果直接令 l = min ⁡ d i s ( i , j ) , r = max ⁡ d i s ( i , j ) l = \min dis(i,j),r = \max dis(i,j) l=mindis(i,j),r=maxdis(i,j) 会导致无法使 m i d mid mid 取得精确答案值,二分进入死循环,我们发现答案一定是某两个居住点之间的距离,于是预处理出所有居住点间的距离,排序后在这些距离中寻找答案。

居住点间距离与二分的答案相等,这时考虑求出如果距离与二分答案相等,那么全部不划分为同一部落所得到的部落个数 n 1 n_1 n1 ,再记录如果距离与二分答案相等,全部划分为同一部落所得到的部落个数 n 2 n_2 n2 ,查看是否满足 n 2 ⩽ k ⩽ n 1 n_2\leqslant k \leqslant n_1 n2kn1 即可。

时间复杂度 O ( n 2 ) O(n^2) O(n2) 可以通过本题。

for(int i = 1;i <= n;i ++)
	for(int j = 1;j < i;j ++)
	{
		D[i][j] = D[j][i] = dis(i,j);
		tot ++;dd[tot] = D[i][j];//计算全部距离
	}
sort(dd + 1,dd + tot + 1);//排序
for(int i = 1;i <= tot;i ++)
{
	if(dd[i] != dd[i - 1])
	{
		ntot ++;
		ndd[ntot] = dd[i];
	}
}
for(int i = ntot + 1;i <= tot;i ++) dd[i] = 0.000;
for(int i = 1;i <= ntot;i ++) dd[i] = ndd[i];
tot = ntot;
l = 1;r = tot;
for(;l <= r;)//二分答案
{
	mid = (l + r)/2;
	cnt1 = 0;cnt2 = 0;
	for(int i = 1;i <= n;i ++) fa[i] = i;
	for(int i = 1;i <= n;i ++)
		for(int j = i + 1;j <= n;j ++)
			if(D[i][j] < dd[mid] && judge(i,j) == 0) join(i,j);//距离等于答案的一律不划分为同一部落
	for(int i = 1;i <= n;i ++) vis[i] = 0,fa[i] = findx(i);
	for(int i = 1;i <= n;i ++) if(!vis[fa[i]]) {vis[fa[i]] = 1,cnt1 ++;}
	for(int i = 1;i <= n;i ++) fa[i] = i;
	for(int i = 1;i <= n;i ++)
		for(int j = i + 1;j <= n;j ++)
			if(D[i][j] <= dd[mid] && judge(i,j) == 0) join(i,j);//距离等于答案的一律划分为同一部落
	for(int i = 1;i <= n;i ++) vis[i] = 0,fa[i] = findx(i);
	for(int i = 1;i <= n;i ++) if(!vis[fa[i]]) {vis[fa[i]] = 1,cnt2 ++;}
	if(cnt1 >= k && k >= cnt2)
	{
		l = mid + 1;
		ans = max(ans,dd[mid]);
	}
	else
	{
		if(cnt2 > k) l = mid + 1;
		else r = mid - 1;
	}
}
printf("%.2lf",ans);

图论问题

P4185 [USACO18JAN]MooTube G

抓住题目中视频间相关性的定义

将任意一对视频的相关性定义为沿此路径的任何连接的最小相关性

也就相当于给出一个 K K K ,从 v i v_i vi 开始遍历,一旦碰到 r < K r < K r<K 的边就停止遍历并回溯,问 v i v_i vi 最多可以到达多少个不同结点。

关注到碰到 r < K r <K r<K 的边就停止遍历,又关注到有多次询问,那么就可以将每次询问视为对一棵无根树,删去 r < K r<K r<K 的所有边,问 v i v_i vi 所在连通块的大小,然后我们将目光放到连通块上来。

考虑离线操作,将所有询问记录下来,按 K K K 从大到小排序,又将边按 r r r 从大到小排序,于是我们就将删边操作视为为加边操作,对于每次询问,我们往图中新增 r ⩾ K i r \geqslant K_i rKi 的边,然后回答这些询问,时间复杂度 O ( Q α ( n ) ) O(Q\alpha(n)) O(Qα(n))

...
sort(e + 1,e + 1 + cnt,cmp2);//给边排序
for(int i = 1;i <= N;i ++) fa[i] = i,siz[i] = 1;
for(int qq = 1;qq <= Q;qq ++)
	scanf("%d%d",&b[qq].K,&b[qq].p),b[qq].id = qq;//记录所有询问
sort(b + 1,b + 1 + Q,cmp);openi = 1;//按 K 排序
for(int t = 1;t <= Q;t ++)//离线回答
{
	if(b[t].K != openK)
	{
		openK = b[t].K;
		for(;e[openi].val >= openK;openi ++)//删边操作转为加边操作
		{
			if(!judge(e[openi].from,e[openi].to))
				join(e[openi].from,e[openi].to);
			if(e[openi + 1].val < openK) break;
		}
	}
	b[t].ans = siz[findx(b[t].p)] - 1;//O(α(n))回答询问
}		
sort(b + 1,b + 1 + Q,cmp3);
for(int i = 1;i <= Q;i ++)
	printf("%d\n",b[i].ans);//输出答案
...
P1197 [JSOI2008]星球大战

问题问按一定顺序删除一些点和与这个点相连的边后,还有多少连通块。

同上一题一样,朴素并查集不支持删边操作,那么我们考虑离线操作,将删边操作改为加边操作,删点操作改为加点操作。

然后再知道一个结论:两个连通块合并后,连通块数目减 1。

然后这道题就做完了。

for(int i = 1;i <= k;i ++) scanf("%d",&a[i]),a[i] ++,ban[a[i]] = 1;
for(int i = 1;i <= n;i ++) fa[i] = i;
for(int i = 1;i <= n;i ++)
{
	if(ban[i] == 0)
	{
		tot ++;
		for(int j = head[i];j;j = e[j].nxt)
			if(ban[e[j].to] == 0 && (!judge(i,e[j].to)))
			{
				join(e[j].to,i);
				tot --;//两个连通块合并,数目减1
			}
	}
}
acnt ++;ans[acnt] = tot;
for(int i = k;i >= 1;i --)
{
	tot ++;ban[a[i]] = 0;
	for(int j = head[a[i]];j;j = e[j].nxt)
		if(ban[e[j].to] == 0 && (!judge(a[i],e[j].to)))
		{
			join(e[j].to,a[i]);
			tot --;
		}
	acnt ++;
	ans[acnt] = tot;
}
for(int i = acnt;i >= 1;i --) printf("%d\n",ans[i]);

更多应用

OI Wiki

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值