首先理解并查集的含义
并:可以合并
查:可以查询
集:由集合组成
为什么我们需要并查集
在解决“连通块”,“城市修路”,“江湖门派”等问题时,使用并查集可以高效地解决问题。非常,非常高效。
并查集长啥样(同权并查集)
并查集是一种单边有向图,边代表的含义仅仅是属于关系,一般可以用数组实现。举个例子,我们现在有5个点(1 ~ 5),分别随机摆放。
我们初始化一个pre[]数组,指向某点的前导点(父节点),父节点默认为自身。
void init()
{
for(int i = 1; i <= n; ++i)pre[i] = i;
}
此时的每个点都指向自身,即pre[1] = 1,pre[2] = 2,pre[3] = 3,pre[4] = 4,pre[5] = 5;
如何合并
在合并之前,我们需要写一个函数来“找到根节点”,注意是根节点而不是父节点,根节点是父的父的父的父的父......最终的父亲才是根。由于单个点过于随机和不好处理,之后的合并,查询等操作都会在“根节点”上执行,无一例外。
int root(int x)//找到点 x 的根节点
{
while(pre[x] != x)x = pre[x];
//只要x不是根节点,就一直回溯
return x;
}
是不是非常简单?(这不是最终版的查找根节点的函数,后续还有路径压缩优化)
比如我们现在有如下的图(先别管怎么连起来的):
很明显pre[2] = 3,pre[3] = 4,pre[4] = 4。
那么 root( 2 ) 会返回4,root( 3 ) 也会返回4,这是显而易见的。
接下来,我们试着把 节点5 合并到 2 - 3 - 4 中,前面说过所有操作都在根上完成,那么我们就这样写合并函数(为了防止函数名冲突,特意没用merge这个名字)。
void mege(int u,int v)//将u v 节点合并
{
pre[root(u)] = root(v);//u的根节点指向v的根节点
//pre[root(v)] = root(u);反之也行
}
这样操作之后,图会有两种可能。下面参数中的的2 3 4可以随意替换,但是5必须有。
如果我们执行 mege(5,2)
若执行mege(3,5)
霸特!无伤大雅,我们只要他们几个连起来就行,根是谁不重要,根相同才重要。
如何查询
比如我们想知道 2 与 4 是否连通,那么只需要判断是否同根。
bool iscon(int u,int v)
{
return root(u) == root(v);
}
甚至连注释都不想写了,这个道理也是显而易见的。
假设此时图为第二种。
我们想知道 1 和 3 是否连通,那么函数会返回:1 == 5 显然是 false。
如果要查 2 和 3是否连通,那么函数返回:5 == 5 显然是 true。
并查集的核心优化 - 路径压缩
如果图比较小,或者路径比较短,合并与查询速度都还行,但是如果有一条长度为10000甚至更长的路径,如下图:
如果此时我们查询 1 与 2是否连通,或者 合并 1 和 5 这类操作,时间复杂度会达到惊人的O(n),而且是每一次操作都这么慢。
有没有办法优化呢?有,而且很简单,只需要稍微修改一下root函数就行了,你怎么不早说啊喂!
int root(int x)//找到点 x 的根节点
{
int rx = x;//用rx代替x进行查找
while(pre[rx] != rx)rx = pre[rx];
//只要x不是根节点,就一直回溯
pre[x] = rx;//直接将x指向根节点
return rx;
}
这样的话,当我们进行第一次 查询 1 2连通 时,在执行root函数时1会直接指向根而放弃指向2(终究是错付了)
如下图
由于我们的操作都在根上完成,所以这样并不会影响操作,反而可以将速度提高很多。
并查集的其他基本操作
查询连通块的个数,即根的个数
int rootsum()
{
int cnt = 0;
for(int i = 1; i <= n; ++i)if(pre[i] == i)cnt++;
return cnt;
}
讲完了,没了。