并查集
概念
并查集是一种维护集合的数据结构。并------合并(合并俩个集合),查------查找(判断两个元素是否存在一个集合),集------集合。
怎么实现?
- 数组模拟树:
father[i]=x;- 上述表示元素 i 的父亲节点为 x ,其中,根节点为元素本身,对同一个集合来说只存在一个根结点,且将其作为所属集合的标识。
father[1]=2; //元素1的父亲节点为2
father[1]=1; //元素1为该集合的根节点
注意 :
处理并查集时始终用一个函数 来合并集合,避免直接修改父节点 数组(或者修改完父节点数组后再次重新查找父节点),因为这会破坏路径压缩,导致循环引用或造成错误的集合关系,破坏了并查集结构。
操作
初始化
- 每个元素都是一个单独的集合,所有元素的根节点都等于本身
cpp
for(int i=1;i<=n;i++){
father[i]=i;
}+-
查找
根据某节点找到所在集合的根节点,可用递推或递归
递推:
cpp
int find(int i){
while(i!=father[i]){
i=father[i]; //得到父亲节点继续寻呼那循环
}
return i; //返回根节点
}
递归:
cpp
int find(int i){
if(i==father[i]){
return i;
}else{
return find(father[i]);
}
}
合并
思路:
- 判断两个元素是否属于同一个集合,只有当两个元素属于不同集合时才合并
- 合并是把其中一个集合的根结点的父亲指向另一个集合的根结点。
cpp
void union(int i,int j){
int a=find(i);
int b=find(j);
if(a!=b){
father[a]=b;
}
}
路径压缩
在题目给出的元素数量很多并且形成一条链,那么这个查找函数的效率就会非常低。因为每次都要回溯相当于遍历了一遍,计算量极大
对于根节点为1树:
正常查找:
father[1]=1;
father[2]=1;
father[3]=2;
father[4]=3;
如果只是为了查找根结点,那么完全可以想办法把操作等价地变成:
father[1]=1;
father[2]=1;
father[3]=1;
father[4]=1;
这样相当于把当前查询结点的路径上的所有结点的父亲都指向根结点,查找的时候就不需要一直回溯去找父亲了,查询的复杂度可以降为O(1)。
cpp
int find(int i){
int a=i; //i在下面的while会变成根节点,保存一下
while(i!=father[i]){ //找根节点
i=father[i];
}
while(a!=father[a]){ //把路径上的所有节点改为根节点i
int b=a; //a要被其父节点覆盖,保存用来改变根节点
a=father[a];
father[b]=i; //将原先的结点a的父亲改为根结点i
}
return i; //返回根节点
}
递归:
cpp
//写法一
int find(int i){
if(i==father[i]){ //找根结点
return i;
}else{
int root=find(father[i]); //递出寻找根节点
father[i]=root; //归回时赋予根节点root
return root;
}
}
//写法二
int find(int i){
if(father[i]==i){
return i;
}
return father[i]=find(father[i]);
//朝着父节点一直递,直到父节点等于自己时归,归的时候将路径上的所有节点改为父节点
}
种类并查集(扩展域并查集)
并查集能维护连通性、传递性,通俗地说,亲戚的亲戚是亲戚 。
然而当我们需要维护一些对立关系 ,比如 敌人的敌人是朋友时,正常的并查集就很难满足我们的需求。
这时,种类并查集 (扩展域并查集)就诞生了。
常见的做法是将原并查集扩大一倍规模 ,并划分为两个种类:其中[1,n]表示处于一个种类,[n+1,2n]表示处于另一个种类。
在同个种类 的并查集中合并,和原始的并查集没什么区别,仍然表达他们是朋友 这个含义。
考虑在不同种类 的并查集中合并的意义,其实就表达他们是敌人 这个含义了
按照并查集美妙的传递性,我们就能具体知道某两个元素到底是敌人还是朋友了。
有数据[1,n],种类并查集下假设[n+1,2n],现在处理数据x,y:
-
维护朋友关系:
- 合并x,y所在的树
-
维护敌人关系:
- x与y的假想敌合并(x与y的敌人是朋友)
- y与x的假想敌合并(y与x的敌人是朋友)
题目
- 洛谷P3367
模板并查集
思路:
- 纯纯板子题
- 洛谷P1525
关押罪犯
思路:
-
我们要尽可能让矛盾中值的罪犯在两个监狱里---矛盾值排序。敌人的敌人就是朋友:两个人a,b有仇,那么把他们放在一起显然会打起来,那么我们把a与b的其他敌人放在一起。
-
首先需要并查集初始化
-
先把所有的矛盾关系按照矛盾值从大到小排一遍序
-
接下来每次操作取出一个关系,看矛盾的两个人x和y是否已经分配到同一个集合中(并查集找父亲即可)
- 如果在一起那么显然会打起来(会出现矛盾),那么直接输出当前的边权(矛盾值)即可(此时可以保证是最小矛盾值,因为已经排序了)
- 如果不在同一组,则按照"敌人的敌人就是朋友"的原则,把x与y的其他敌人分在同一组,y与x的其他敌人分在同一组
-
不断进行以上操作最终可以得到答案