并查集
顾名思义,并查集是支持“合并”和“查询”操作的集合。
合并操作针对的是两个区域,是将在图论中称为非连通块的区域合并为一个连通块。
查询操作则是查找两个节点是否连通。
而对于无序性的集合来说,需要特别说明,并查集能够操作的是无向图,并不适用于有向图强连通性。
原理和结构
为了方便表示,我们用树这种数据结构来实现并查集,每个节点都有一个父节点,我们定义一个pre[i]数组用于存放第i个节点的父节点。初始化如下:
for(int i = 1;i<=n;i++) pre[i]=i;
这样我们就得到了n个指向自己的节点。
合并:
并查集是怎么实现合并操作的呢?最简单地,我们构造一条新的边,将被操作的两个节点连在一起,就能够实现合并操作。但是并不能直接合并,因为每个节点只有一个父节点。有这样一种情况:
节点1——节点2,此时我们认为节点1的父节点为节点2,也就是pre[1]=2,如果需要再将节点1和节点3合并。按照错误的思路,我们会写到pre[1]=3这样一个代码。注意到,此时节点1与节点2便不在连通,因为节点1指向了节点3。那么有人说,将节点2的父节点指向节点3不就好了。是的,这样问题就解决了,但是问题解决的原因是什么。节点1和节点2有什么不同?他们的关系是怎样的?
节点2是节点1的根。没错,之所以用节点2操作可以实现,就是因为节点2是根的这一特性。同样,我们也可以认为节点3是根,在这里将节点3指向节点2也是正确的。所以合并的关键就是对根进行操作。实现代码如下:
void merge(int x,int y){x=root(x),y=root(y);//先找到根节点if(x==y) return;//如果根节点相同,则已连通,直接returnpre[x]=y;//pre[y]=x也可以
}int root(int x){//方法一:递归return pre[x]==x?x:root(x);
}
int root(int x){//方法二:循环while(pre[x]!=x) x=pre[x];
}
查询:
我们已经知道了并查集的原理和结构,也实现了合并操作,那么查询也就不难了。如何查询两个节点是否连通?考虑到我们树的数据结构,越往根节点走,节点之间的关系应该是越密切,因此我们判断是否的连通的条件就是根节点是否相同,同根即连通。实现代码如下:
if(root(x)==root(y)) return true;
return false;
算法优化
上面是基础的模板,这里进一步思考有没有更加高效的算法。我们注意到,无论是合并还是查询的操作,关键都是在找根节点来进行操作,操作的过程并不复杂,往往就是一行代码,而主要的时间都花在了找根节点上,那么如何减少浪费在找根节点的时间呢?一方面,我们可以优化我们的具体找根的过程,比如通过记忆化,如果节点1的根节点是10,那么在1-10之间的所有节0点,其根节点也都应该是10,那么我们就可以采用路径压缩。另一方面,我们可以考虑合并时花点心思,将树树形结构合并的又矮又胖,也能减少查询的次数。
路径压缩
时间复杂度:O(1)
原理刚刚已经讲过了,就是那么回事,直接上代码:
int root(int x){//方法一:递归return pre[x]=(pre[x]==x?x:root(x));//找到根后返回的过程中,给路径上的节点的pre都赋上根节点的编号
}
int root(int x){//方法二:循环int res = x;while(pre[res]!=res) res=pre[res];//res为根 while(pre[x]!=x){int y = x;x=pre[x];pre[y]=res;}//类似于链表的插入操作,只不过这里是将链表中要指向的新的节点改成根节点
}
路径压缩有一个弊端:那就是改变了原来的树形结构
按秩合并
时间复杂度:O(logn)
秩可以理解为树高,我们尽量保证得到一个每个树支上的秩都比较平均的树,而非一个很深很深,每次找根都要从头找到尾的树,因此合并时,可以按秩进行合并。将秩小的节点合并到秩大的节点上,此时秩不会改变,而当两部分秩相同,则根节点的秩+1
int rnk[N];
void merge(int x,int y){x=root(X),y=root(y);if(x==y)return;if(rnk[x]>rnk[y]) swap(x,y)//保证rnk[x]<=rnk[y]pre[x]=y;//秩小的指向秩大的if(rnk[x]==rnk[y]) rnk[y]++;//相等时需要更新根节点的秩
}
启发式合并(按大小合并)
时间复杂度:O(logn)
大小和秩的本质类似,都是为了避免又长又深的树,代码也类似
int sz[N];
void merge(int x,int y){x=root(X),y=root(y);if(x==y)return;if(sz[x]>sz[y]) swap(x,y)//保证sz[x]<=sz[y]pre[x]=y;//小的指向大的sz[y]+=sz[x]//根节点更新大小
}int main(){
for(int i = 1;i<=n;i++)sz[i]=1;//不要忘记初始化,否则一直为0
}
总结大概就到此为止啦~