installb
2019-09-14 14:05:16
updated 2019.11.30 填坑+补充
并查集是一种用于处理一些不相交的集合的合并与查询问题的树形数据结构
能够将两个集合合并 或者查询某个元素处于哪个集合中
<!--more-->
首先并查集维护的是一个有根树森林
森林中每一棵树代表一个集合(相当于连通块)
一棵树中所有点的祖先都是相同的 所以查询两个点是否在同一棵树中可以等价为查询它们的祖先是否相同
我们要高效维护祖先信息
先考虑朴素算法 首先我们对每一个点存下它父亲的值 祖先就是不断找父亲直到顶就可以了
为了判断一个点是祖先节点并且方便处理 我们把祖先节点的父亲节点设置成自己
于是父亲是自身的点都是祖先节点
一开始每一个点都自己组成一个连通块(一棵树)
记
for(int i = 1;i <= n;i ++) f[i] = i;
可以视为是在 对两棵树的根连边 和 树上找祖先 的过程
对于一个元素 不断找它的父亲节点 直到找到祖先节点为止 就可以了
这个点就是该节点所在树的根 相当于所在集合的编号
祖先节点的祖先是它本身
如果两个元素的祖先节点相同 它们就在同一个集合中了
int find(int x){
if(x == f[x]) return x;
return find(f[x]);
}// 查询祖先 采用递归方式实现
char judge(int x,int y){
if(find(x) == find(y)) return 'Y'; // 在同一个集合内
return 'N';
}
先找到待合并两个点的祖先
将一个祖先的父亲设为另一个祖先就可以了
注意这里被合并的祖先的子树中所有点的祖先信息都还没有更新为另一个祖先
所以每一个想调用一个元素的祖先信息时 一定要把它做一遍查询 更新它祖先的值 再去调用
void union(int x,int y){
x = find(x); // 找祖先
y = find(y);
f[x] = y; // f[y] = x 也行 并起来就可以了
}
不过这个算法显然可以被卡成单次查询
所以我们需要优化
虽然说是优化 但是前面的算法不优化复杂度太大了
所以优化是必须的
由于只需要祖先信息
所以对于一个元素 它的祖先是它的父亲 还是它父亲的父亲 其实是不重要的
所以我们可以在查询到一个点的祖先的时候 把这个点到祖先路径上所有点的 父亲节点编号(即
由于上面的过程是递归实现的 路径上每一个点都可以在找到祖先后修改父亲节点
相信还是看图更好理解
找祖先
改父亲节点编号
代码也很简单 就加了五个字符
int find(int x){
if(x == f[x]) return x;
return f[x] = find(f[x]);
}
时间复杂度(m次询问):
最坏
平均
路径压缩相当于是破坏了点的父子关系 所以不能够撤销
而按秩合并不会
按秩合并就是每一次合并都是把小的集合(含元素少)并到大的集合里面去
如果一开始所有集合都是只有一个元素的
那么经过这样的合并后 从根节点往下走 每走一步子树大小至少减半
这样可以保证查询复杂度
// siz记录子树的大小 初始化为1
void unionSet(int x, int y) {
x = find(x); y = find(y);
if(x == y) return;
if(siz[x] > siz[y]) swap(x,y);
f[x] = y; siz[y] += siz[x];
}
撤销的时候把之前连上去的那条边删掉就行了(把对应那个点的祖先改成自己)
树的结构没有改变 所以可行
时间复杂度
拿这题举例子了:NOI2001 食物链
这里同类关系可以简单的用并查集解决
但是天敌和猎物的关系就不能处理
所以对每一个生物
记作
在合并的过程中 我们需要使得对于任意一个连通块 该连通块内所有元素要么同时满足 要么同时都不满足
同一个集合内的元素可以互相推出
合并之前判断一下有没有矛盾
如果
如果此时
其实只需要判断A,B即可 如果
如果
这时候如果发现之前
这里用
// 同类合并
if(find(x) == find(y + n) || find(y) == find(x + n)) ans ++;
else{
f[find(x)] = find(y);
f[find(x + n)] = find(y + n);
f[find(x + n + n)] = find(y + n + n);
}
// x吃y
if(find(x) == find(y) || find(x + n) == find(y)) ans ++;
else{
f[find(x)] = find(y + n);
f[find(x + n)] = find(y + n + n);
f[find(x + n + n)] = find(y);
}
其实就是给森林中的每一个点都赋值 保存下这个点的信息 以及 它到父亲节点的边的信息
然后在合并的时候(路径压缩)改变这些值 查询的时候查询值 就行了
拿这题举例:NOI2002 银河英雄传说
这里记