基础并查集
并查集可以进行集合归并操作,并能方便的查询两个元素是否在同一集合,原理是查询每个元素的根节点,如果两个元素根节点相同,那么两个元素处于同一个集合。关键是代码量少,这么简单实用的数据结构当然要好好研究下。
1.初始化。初始化使每个元素的根节点指向自己,自己是一个独立的集合。一般通过数组存储每个元素的父节点。
constructor(n){
this.fa = new Array(n+1);
for(let i=0;i<=n;i++) this.fa[i] = i;
}
2.查找元素根节点操作。该操作是不断递归查询节点的父节点,直到集合的根节点。怎么判断根节点呢,当元素的父节点和下标相同就是根节点了。比如我们进行了合并1和3节点,3和5节点,结果如图:
我们查询1的节点的父节点,指向3,发现3也不是根节点,接着找到了5,5的父节点和下标相同就是集合的根节点。可得知3和5的根节点相同,也是同一个集合的。
find(x){
//this.fa[x] = this.find(this.fa[x])主要是优化树的高度,比如优化后1的根节点就直接是5了,后面查询就更快。该操作的复杂度是逆阿克曼函数,只比O(1)慢一点。
return (this.fa[x]===x)?x:(this.fa[x] = this.find(this.fa[x]));
}
3.集合合并,集合合并是通过改变该点指向的父节点,从而改变该点指向的集合。
合并节点1和2,3和4后如图:
一般合并的话都是找到根节点合并的,比如上图1和2已经处于同一个集合,假如1还要和3合并那么2和3也一定是同一个集合的。所以需要找到根节点合并两个集合。
join(a,b){//合并两个元素
a = this.find(a);
b = this.find(b);
this.fa[a] = b;
}
完整的基础版代码:
class DisjointSet{
constructor(n){//初始化
this.fa = new Array(n+1);
for(let i=0;i<=n;i++) this.fa[i] = i;
this.count = n;//集合的个数
}
find(x){//查询x的根节点
return (this.fa[x]===x)?x:(this.fa[x] = this.find(this.fa[x]));
}
join(a,b){//合并两个元素
a = this.find(a);
b = this.find(b);
if(a!==b){//不在同一集合
this.count--;
this.fa[a] = b;
}
}
samecollect(a,b){//两个元素是否同一个集合
return this.find(a)===this.find(b);
}
}
可以尝试一下:547. 省份数量
带权并查集
可能遇到这样的场景,元素之间有倍数关系,且倍数关系可以传递。我们最后要查询任意两个元素间的关系。这时候就可以使用带权重的并查集,我们加上一个表示元素间权重的数组,该数组存放和父节点的倍数关系,父节点是谁?当然是和fa数组同步的。最后查询两个节点后也可以方便计算出两个节点的倍数关系。
class DisJointUnit{
constructor(n){
this.fa = new Array(n);
this.weight = new Array(n);
for(let i=0;i<n;i++){
this.fa[i] = i;
this.weight[i] = 1;//父节点是自身为1倍关系
}
this.count = n;
}
union(x,y,val){//节点合并
let a = this.find(x);
let b = this.find(y);
if(a!=b){
this.count--;
this.fa[a] = b;
this.weight[a] = val*this.weight[y]/this.weight[x];//更新节点的倍数关系
}
}
find(x){//寻找某节点的根
if(x===this.fa[x]) return x;
else{
let next = this.find(this.fa[x]);
this.weight[x] = this.weight[x]*this.weight[this.fa[x]];
this.fa[x] = next;//对fa数组操作也要更新weight数组的倍数关系
}
return this.fa[x];
}
}
可以尝试一下: 399. 除法求值
带查询集合大小并查集
有时候我们不但需要知道元素间的关系,还要知道元素所在集合的大小,里面有多少元素。同样需要一个数组表示元素所在集合的大小,而集合大小只有集合合并才会改变,比带权的要简单。
class DisjointSet{
constructor(n){
this.fa = new Array(n);
this.size = new Array(n).fill(1);//保存各个集合的大小
this.maxSize = 1;
for(let i=0;i<n;i++) this.fa[i] = i;
}
union(a,b){
let ax = this.find(a);
let bx = this.find(b);
if(ax!==bx){
this.fa[bx] = ax;
this.size[ax]+=this.size[bx];
this.maxSize = Math.max(this.size[ax]);
}
}
getSize(x){//获取某个集合的大小
return this.size[this.find(x)];
}
find(x){
return this.fa[x] === x?x:(this.fa[x] = this.find(this.fa[x]));
}
isConnect(a,b){
return this.find(a)===this.find(b);
}
}
可以尝试一下: 827. 最大人工岛
反向点并查集
有时候我们不知道两个点是否同一个集合,但是知道两个点不在同一个集合,那么这个时候可以用并查集吗。当然也可以,我们需要要构造反向的点将互斥的两个元素分别在反向点连接。反向点只要把并查集的范围扩大一倍,n+x就是x的反向点了。
如题:886. 可能的二分法
比如有一组互斥点:[1,2] [2,3] [1,3]
怎么判断能不能分为两组,用并查集的方法,处理前两组后如图:
这个时候我们发现1和3在同一个集合,不能互斥也就无法分为两组。
这些就是本人做题过程中遇到的一些并查集比较方便解决的问题,如果有更多的适用场景欢迎指教。