模板并查集
Posted maoyiting
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了模板并查集相关的知识,希望对你有一定的参考价值。
引入
在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。
这一类问题近几年来反复出现在信息学的国际国内竞赛题中,其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往超过了空间的限制,计算机无法承受;即使在空间上能勉强通过,运行的时间复杂度也极高,根本不可能在比赛规定的运行时间内计算出试题需要的结果,只能采用一种特殊数据结构——并查集来描述。
1、什么叫并查集
并查集(union-find set)是一种用于分离集合操作的抽象数据类型。**它所处理的是“集合”之间的关系,即动态地维护和处理集合元素之间复杂的关系,当给出两个元素的一个无序对(a,b)时,需要快速“合并”a和b分别所在的集合,这其间需要反复“查找”某元素所在的集合。**
“并”、“查”和“集”三字由此而来。在这种数据类型中,n个不同的元素被分为若干组。每组是一个集合,这种集合叫做分离集合(disjoint set)。并查集支持查找一个元素所属的集合以及两个元素各自所属的集合的合并。
例如,有这样的问题:初始时n个元素分属不同的n个集合,通过不断的给出元素间的联系,要求实时的统计元素间的关系(是否存在直接或间接的联系)。
这时就有了并查集的用武之地了。元素间是否有联系,只要判断两个元素是否属于同一个集合;而给出元素间的联系,建立这种联系,则只需合并两个元素各自所属的集合。这些操作都是并查集所提供的。
并查集本身不具有结构,必须借助一定的数据结构以得到支持和实现。
数据结构的选择是一个重要的环节,选择不同的数据结构可能会在查找和合并的操作效率上有很大的差别,但操作实现都比较简单高效。并查集的数据结构实现方法很多,数组实现、链表实现和树实现。一般用的比较多的是数组实现。
2、并查集支持的操作
并查集的数据结构记录了一组分离的动态集合S={S1,S2,…,Sk}。每个集合通过一个代表加以识别,代表即该元素中的某个元素,哪一个成员被选做代表是无所谓的,重要的是:如果求某一动态集合的代表两次,且在两次请求间不修改集合,则两次得到的答案应该是相同的。
动态集合中的每一元素是由一个对象来表示的,设x表示一个对象,并查集的实现需要支持如下操作:
MAKE(x):建立一个新的集合,其仅有的成员(同时就是代表)是x。由于各集合是分离的,要求x没有在其它集合中出现过。
UNIONN(x,y):将包含x和y的动态集合(例如Sx和Sy)合并为一个新的集合,假定在此操作前这两个集合是分离的。结果的集合代表是Sx∪Sy的某个成员。一般来说,在不同的实现中通常都以Sx或者Sy的代表作为新集合的代表。此后,由新的集合S代替了原来的Sx和Sy。
FIND(x):返回一个指向包含x的集合的代表。(某蒟蒻myt一般会用一个get函数来实现)
并查集的基本思想
对于引例的问题,我们可以运用并查集简单地进行如下做法:
⑴元素的合并图示:(用“画图”画的,将就一下看吧)
① 合并1和2
② 合并1和3
③ 合并5和4
④ 合并3和5
用father[i]表示元素i的父亲结点,进行不断并到不同的集合中
⑵再对输入的数据进行判断:是否在同一集合。
具体程序如下:
1 #include<bits/stdc++.h> 2 #define int long long 3 using namespace std; 4 const int N=10010; 5 int n,m,z,x,y,f[N]; 6 int get(int u){ 7 return f[u]==u?u:get(f[u]); 8 } 9 signed main() 10 { 11 //freopen(".in","r",stdin); 12 //freopen(".out","w",stdout); 13 scanf("%lld%lld",&n,&m); 14 for(int i=1;i<=n;i++) f[i]=i;//建立新的集合,其仅有的成员是i 15 for(int i=1;i<=m;i++){ 16 scanf("%lld%lld%lld",&z,&x,&y); 17 if(z==1) f[get(x)]=get(y);//将x与y所在的集合合并 18 if(z==2) get(x)==get(y)?printf("Y "):printf("N ");//判断x与y是否在同一集合内 19 } 20 return 0; 21 }
以上做法当数据比较特殊的时候,比如一条单链老长,数据这种“并”与“查”的方式肯定会超时
⑶下面有一种优化的方法:
并查集的路径压缩
此种做法就是将元素的父亲结点指来指去地指,当这棵树是链的时候,可见判断两个元素是否属于同一集合需要O(n)的时间,于是路径压缩产生了作用。
路径压缩实际上是在找完根结点之后,在递归回来的时候顺便把路径上元素的父亲指针都指向根结点。
这就是说,我们在“合并5和3”的时候,不是简单地将5的父亲指向3,而是直接指向根节点1,如图:(蒟蒻用“画图”画的图QAQ)
由此我们得到了一个复杂度几乎为常数的算法。
【程序清单】
(1)初始化:
for(int i=1;i<=n;i++) f[i]=i;
因为每个元素属于单独的一个集合,所以每个元素以自己作为根结点。
(2)寻找根结点编号并压缩路径:
int get(int x){ if(f[x]!=x) f[x]=get(f[x]); return f[x]; }
压完行以后……
int get(int u){ return f[u]==u?u:get(f[u]); }
(3)合并两个集合:
void unionn(int x,int y){ x=get(x),y=get(y); f[y]=x; }
(4)判断元素是否属于同一集合:
bool judge(int x,int y){ x=get(x),y=get(y); if(x==y) return 1; return 0; }
这个的引题已经完全阐述了并查集的基本操作和作用。
【模板】参考代码
1 #include<bits/stdc++.h> 2 #define int long long 3 using namespace std; 4 const int N=10010; 5 int n,m,z,x,y,f[N]; 6 int get(int u){ 7 return f[u]==u?u:f[u]=get(f[u]); 8 } 9 signed main() 10 { 11 //freopen(".in","r",stdin); 12 //freopen(".out","w",stdout); 13 scanf("%lld%lld",&n,&m); 14 for(int i=1;i<=n;i++) f[i]=i;//建立新的集合,其仅有的成员是i 15 for(int i=1;i<=m;i++){ 16 scanf("%lld%lld%lld",&z,&x,&y); 17 if(z==1) f[get(x)]=get(y);//将x与y所在的集合合并 18 if(z==2) get(x)==get(y)?printf("Y "):printf("N ");//判断x与y是否在同一集合内 19 } 20 return 0; 21 }
这种做法就可能不会超时了
[点击自主测评](https://www.luogu.org/problem/P3367)
【并查集的延伸】
1、求无向图的连通分量
求无向图连通分量是个非常常用的算法。通过并查集可以使得空间上省去对边的保存,同时时间效率又是很高的。
需要特别指出的是,如果用链表来实现的话,最后任何在同一个集合(即连通块)中的元素,其代表指针的值都是相等的。而采用有根树来实现的话,算法结束后,留下的依然是树的关系,因此如果希望每个元素都指向它的根的话,还需要对每个节点进行一次find(即get)操作,这样每个节点的父节点都是代表此集合的节点。在某些统计问题中,往往需要这样做。
2、Kruskal最小生成树算法
此经典算法的思想是将树上的边按照边权排序,然后从小到大分析每一条边,如果选到一条边e=(v1,v2),且v1和v2不在一个连通块中,就将e作为最小生成树的一条边,否则忽略e。这其中明显就包含了并查集的算法。Kruskal算法也只有在结合了并查集后才能说是个高效的算法。
3、蒟蒻的小结
某蒟蒻认为,在解决某些特定的问题时,并查集往往能够发挥出重要的作用……(然后就没了)
【推荐例题】
https://www.luogu.org/problem/P1551 应该能秒切吧QAQ
https://www.luogu.org/problem/P1111
https://www.luogu.org/problem/P3958
[HAOI2006]聪明的猴子 注:此题可用并查集的延伸Kruskal最小生成树解决
[SCOI2005]繁忙的都市 注:此题也可用并查集的延伸Kruskal最小生成树解决
https://www.luogu.org/problem/P1195
感谢巨佬来看蒟蒻的博客啊QAQ
以上是关于模板并查集的主要内容,如果未能解决你的问题,请参考以下文章
带权并查集(含种类并查集)经典模板 例题:①POJ 1182 食物链(经典)②HDU - 1829 A bug's life(简单) ③hihoCoder 1515 : 分数调查(示例代码(代