【笔记】并查集学习总结

#并查集

并查集在baidu上的定义:

并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来出现频率不低,其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。

简而言之,就是有“并”&“查”两种操作的集
:将两个集合合并
:查询两个元素是否在同一个节点中

举一个小栗子,有一张城市铁路网,中间有n个城市,起始时全部不相通,有俩操作,一是修建俩城市A和B间的铁路,使所有与A相连的城市能与所有与B相连的城市互通;第二是询问某俩城市是否相通;
这就是较经典的并查集问题(简言之,若A与B、B与C有关系,则A、C有关系)

###就如baidu所说,模拟算法在空间或时间上十分地不给力,使得在将来把并查集当成工具使用时会因为成本太高而放弃;

第一次接触并查集是在初中升高中的信息特长生考试中,当时做完了其他题,就反过来想这一题(著名的亲戚查询问题,题意如上),结果发现了一种树状结构,心情那个激动啊,出了考场才发现别人都知道这是个并查集。

功利一点,就不介绍没啥用的线性方法了,直接讲
###树状并查集
我们设一个数组名曰dad[maxn],再定义一个函数叫father(int x)(个人习惯)
dad[x]的值是x的直接爹爹,father(x)返回的值是x的祖先;


合并A与B
将B(或B的祖先)设为 A的祖先的爹爹
(由于不用知道谁是爹爹谁是儿子,就先别管谁辈分大些了)


查询A与B是否在同意集合中
看看他们的祖先是否相同

看看代码应该更好:

inline int father(int x){//用前dad memset成 -1
	if(-1==dad[x])
	return father(dad[x]);
}

void merge(int A,int B){dad[father(A)]=father(B);}

//还有一个更高效的

#define merge(A,B) dad[father(A)]=father(B);

####A子树下的节点在father函数中顺着dad的值往上找祖先就会进入B子树,祖先就一样了

####附一个优化的方法:路径压缩

众所周知,并查集在数据特别恶心或出题人RP - - 时会退化成链,时间变为恶心的O(n)

所以,聪明的大佬们想到了一个聪明的办法,名曰“状态压缩”

思想是将一棵深度很深的树转化为一棵多叉两层树

具体做法是将自己的父亲直接指向自己的祖先

具体代码:

inline int father(int x){//用前dad memset成 -1
	if(-1==dad[x])    return x;
	dad[x]=father(dad[x]);  //千万别写成dad[x]=father(x);会陷入死循环的,上次调了好久才调出来
	return dad[x];
}

看到这里想到那次特长生考试时就是因为没有路径压缩,所以70分 /(ㄒoㄒ)/~~

看到这里,我们会发现并查集的代码真是贼短,时间也只是常数,即便强大的路径压缩也只耗费常数时间,所以考试时并查集就大胆地用吧

##并查集的用法
###一:任何结构都不乏模板题
亲戚问题:题意同引言里的城市铁路网
###二:在图中判定两点的联通问题

这种用法一般是作为正解的一个小小小工具
如:最小生成树中的kruscarl算法在确认一条边能否使用时就只要看看边两边的点是否联通
再如:Bzoj1050 旅行(Ahoi2014) 中虽说用贪心,但并查集也是其中一个“较为重要的”工具

###三:并查集中不是那么裸的题

Bzoj1370 团伙

有 n 个人,m 条关系,一条关系表示两个人是朋友或敌人。现有规定,“我朋友的朋友是我的朋友,我敌人的敌人是我的朋友”,所有是朋友的人会连成一个团伙,问共有多少个团伙

这题一看就是并查集,但会发现细节不好实现,因为当A与B是朋友,C与D是朋友时,不知道该放哪个祖先

其实这题有仨解法,这里先说俩种,还有一种在“五”里面讲;

①用奇偶性判断关系,在一个关系缔造时若缔造关系的双方还未确定自己的位置,则先闲置,若已有一方有位置,则将其接到一个根据位置信息和关系确定奇偶性的位置,比如说,若1和2是敌人,且1的位置是5,则2的子树连到偶数位去(如:2,4,6,……)

②楼上其实有一点麻烦,直接用并查集可能会更好,编程复杂度也更低,直接上规则:

若A,B两人是朋友,则将A,B合并,A+n,B+n合并;
若A,B两人是敌人,则将A,B+n合并,A+n,B合并;

这样的话,

当朋友遇上朋友仍是朋友 A = B , B = C —> A = C

当朋友遇上敌人变为敌人 A = B + n , B = C —> A = C + n ≠ C

当敌人遇上敌人还是朋友 A = B + n , B + n = C —> A = C

###四:构造

构造是并查集真正有点难度的地方,所謂构造就是将一个看起来奇怪的题转化为并查集的裸题

给出几道个人认为较好的题

染色问题

给定一个长度为n的数字序列,有m次对[Li,Ri]的涂色(或其他修改),求最后的序列

其实这种题最突出的特征是覆盖,即后面的操作会覆盖前面的操作,所以若一段区间被修改多次,取最后一次修改即可;

这种题用线段树还是有一丢丢浪费,即便用zkw,O(nlogn)的复杂度还是在那里滴,有一个神奇的线性做法,其实感性地想一想(我已经分不清理性还是感性了) 若是从后往前时光把m个操作反过来,先操作m,m-1,……,1然后已经染色过的点就不操作了。
理想很远大,但操作就不简单了
操作不简单?
操作很简单
既然操作过这个区间就不必再操作了,那么为了达到线性时间,在下一次询问时就必须直接跳过其。

那么可不可以直接将区间左端点直接指向右端点呢?
当然不可以。举个小栗子:若先修改[3,5]再操作[1,6],那在最后输出路径的时候就会忽略[3,5]。

有一个解决方法,就是将该区间所有节点都指向右边的节点,并将区间左端点的左边的点飞跃整个区间,指向右端点的右边的点,这样在下一次访问到区间后就会直接飞过整个区间。
再举个栗子:当修改了[3,5]后
2->6->空白
3->4->5->颜色
再修改[1,6]后
1->2->6->颜色
3->4->5->颜色
我们十分惊奇的发现,前面拜访过的区间之后不会拜访了,所以无论有多少次修改,时间永远都是线性的(包括最后的输出),最后输出使用棒棒的路径压缩,又是线性的 (o)/~ 庶民的胜利

区间奇偶性问题

有一个01序列,给出m段区间,Li到Ri区间的1的数量的奇偶性,求出有多少个给定序列

乍一看,搜索,再一看,有点像树状数组,耐着性子看最后一遍,诶,有了

由于区间奇偶性知道了,所以sum[i-1]与sum[j]的(sum为前缀和)同奇或同偶性便已经知道了,所以它俩就是一根绳上的蚂蚱,她变他也变,他们只有俩情况,最后看看有多少根绳的蚂蚱(多少个联通分量),总数n,则有pow(2,n)的情况数

矩阵奇偶性问题

其实与楼上相像,就是一个01矩阵,已知该矩阵每一个2×2的方格内异或和为1,已知一些格的01情况,求共有多少个矩阵符合要求

其实这题可以巧妙地推出由於 a[i][j]a[i][j+1]a[i+1][j]^a[i+1][j+1]=1 所以當i與j同爲偶數時 a[1][1]a[1][j]a[i][1]^a[i][j]=1 否則爲0 所以呢,當我們將a[1][1]的值枚舉一下下(也只有兩個而已啦)再根據已經確認的01值(a[i][j]),便可知a[i][1]與a[1][j]的奇偶性相當,則兩個數是被綁定的,所以呢,將其合並,最後有幾個聯通量,n則答案爲pow(2,n-1),因爲a[1][1]是可知的,但其實乘2也沒關系,因爲大循環是2呀 哦,對了,由於可以聯通的量是兩條邊界上的數a[1][j]和a[i][1],所以在統計聯通量的時候不要把矩陣拉進去了(以前用繁体写的,懒得改简体了,凑合着看吧)

###五:带权并查集问题

其实团伙那题可以用带权并查集来做,先看看带权并查集的模板:

inline int father(int x){
	if(!dad[x])
		return x;
	int dont_look_at_me=father(dad[x]);
	c[x]+=c[dad[x]];
	dad[x]=dont_look_at_me;
	return dad[x];
}

然后就可以愉快地解决团伙问题了:

Bzoj1370 团伙

有 n 个人,m 条关系,一条关系表示两个人是朋友或敌人。现有规定,“我朋友的朋友是我的朋友,我敌人的敌人是我的朋友”,所有是朋友的人会连成一个团伙,问共有多少个团伙

A、B是朋友时,合并时的边权值为0;是敌人则为1;最后求两点的祖先权值差的奇偶性就行

NOI2002银河英雄传说

*公元五八〇一年,地球居民迁移至金牛座α第二行星,在那里发表银河联邦创立宣言,同年改元为宇宙历元年,并开始向银河系深处拓展。 *
*宇宙历七九九年,银河系的两大军事集团在巴米利恩星域爆发战争。泰山压顶集团派宇宙舰队司令莱因哈特率领十万余艘战舰出征,气吞山河集团点名将杨威利组织麾下三万艘战舰迎敌。 *
*杨威利擅长排兵布阵,巧妙运用各种战术屡次以少胜多,难免恣生骄气。在这次决战中,他将巴米利恩星域战场划分成30000列,每列依次编号为1, 2, …, 30000。之后,他把自己的战舰也依次编号为1, 2, …, 30000,让第i号战舰处于第i列(i = 1, 2, …,30000),形成”一字长蛇阵”,诱敌深入。这是初始阵形。当进犯之敌到达时,杨威利会多次发布合并指令,将大部分战舰集中在某几列上,实施密集攻击。合并指令为M i j,含义为让第i号战舰所在的整个战舰队列,作为一个整体(头在前尾在后)接至第j号战舰所在的战舰队列的尾部。显然战舰队列是由处于同一列的一个或多个战舰组成的。合并指令的执行结果会使队列增大。 *
*然而,老谋深算的莱因哈特早已在战略上取得了主动。在交战中,他可以通过庞大的情报网络随时监听杨威利的舰队调动指令。 *
在杨威利发布指令调动舰队的同时,莱因哈特为了及时了解当前杨威利的战舰分布情况,也会发出一些询问指令:C i j。该指令意思是,询问电脑,杨威利的第i号战舰与第j号战舰当前是否在同一列中,如果在同一列中,那么它们之间布置有多少战舰。
*作为一个资深的高级程序设计员,你被要求编写程序分析杨威利的指令,以及回答莱因哈特的询问。 *
*最终的决战已经展开,银河的历史又翻过了一页…… *

看完累赘的题目就会发现

这难道不就是一道带权并查集的裸题吗 \(≧▽≦)/啦啦啦

♪(*)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值