并查集(Union-Find)
解决的问题
并查集解决的是一个
连接问题
并查集支持两个操作:
union(p,q)
并! 两个节点所在的集合合并find(p)
查! 查询根节点
回答一个问题:
isConneted(p,q)
: 两个节点是否连通.
只需给出是否连通. 不需要给出具体路径.
建模思路
对于所有连通的节点. 认为他们是一个组. 因此不连通的节点必然不在同一个组.
将所有的节点用整数表示.即对于N个节点.使用0到N-1的整数来表示.
注意其中使用整数来表示节点
如果需要使用其他的数据类型表示节点
比如使用字符串,那么可以用哈希表来进行映射
即将String映射成这里需要的Integer类型
初始化时.各自为一个组. 组号 = 索引号.即对于节点i.它的组号也是i
动态连通图有几种可能的操作
查询节点所属的组
数组对应位置的值即为组号
判断两个节点是否属于同一个组
分别得到两个节点的组号,然后判断组号是否相等
连接两个节点,使之属于同一个组
分别得到两个节点的组号,组号相同时操作结束,不同时,将其中的一个节点的组号换成另一个节点的组号
获取组的数目
初始化为节点的数目,然后每次成功连接两个节点之后,递减1
Quick-Find 算法
- 用数组保存每个节点的组号.
namespace UF1 {
class UnionFind {
private:
int *id; // 表示组号
int count; // 一共有多少个节点
public:
UnionFind(int n) {
count = n;
id = new int[n];
// 初始化为各自一个组.
for (int i = 0; i < n; i++) {
id[i] = i;
}
}
~UnionFind() {
delete[]id;
}
// 并操作 O(n)
void unionElements(int p, int q) {
// 找个各自的组号.
// 若相等无需操作.若不等.找到所有组号为pID的节点.将其改成qID
int pID = find(p);
int qID = find(q);
if (qID == pID)
return;
for (int i = 0; i < count; i++) {
if (id[i] == pID)
id[i] = qID;
}
}
// 返回组号
int find(int p) {
assert(p >= 0 && p < count);
return id[p];
}
// 判断是否连通
bool isConnected(int p, int q) {
return find(p) == find(q);
}
};
}
Quick-Find 存在的问题
- 在查询的时,只需要一次操作就可以获得某个节点的组号.
- 但是在并操作时候,需要遍历整个数组.进行修改组号.
Quick-Union 算法
Quick-Find
每个节点各自记录自己的组号. 牵一发而动全身!!!- 通过
树形结构
. 每个节点记录自己的父亲节点. 根节点即为所属的组. 这样,合并操作就变成了. 将两个数合并到一起. find
查操作 需要查询到根节点.
// 每个节点自己的父亲节点.
namespace UF2 {
class UnionFind
{
private:
int *parent;
int count;
public:
UnionFind(int n) {
parent = new int[n];
count = n;
for (int i = 0; i < count; i++) {
parent[i] = i; // 初始化每个节点单独一个组.自己指向自己.
}
}
~UnionFind()
{
delete[] parent;
}
int find(int p) {
assert(p >= 0 && p < count);
// 若不是根节点. p=其父亲节点.
while (parent[p] != p) {
p = parent[p];
}
return p;
}
bool isConnected(int p, int q) {
return find(p) == find(q);
}
void unionElements(int p, int q) {
// 查询两个节点根节点.
// 若相等. 则不需操作.
// 若不等.则将某个根节点指向另一个根节点.完成合并.
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot)
return;
parent[qRoot] = pRoot;
}
};
}
Quick-Union 存在的问题
- 在
union
并操作的时候. 是按固定的顺序将两个节点的根节点合并.可能导致树的层数变得很多. - 改进1: 增加一个
sz数组
. 其保存以i节点作为根节点的数的节点个数. - 在合并时候,将 sz[i] 小的指向 sz[i] 大的.
// sz[i] 表示以元素i为根的集合中的元素个数
namespace UF3 {
class UnionFind
{
private:
int *parent;
int count;
int *sz; // sz[i] 表示以元素i为根的集合中的元素个数
public:
UnionFind(int n) {
parent = new int[n];
sz = new int[n];
count = n;
for (int i = 0; i < count; i++) {
parent[i] = i;
sz[i] = 1; // 初始化为1
}
}
~UnionFind()
{
delete[] parent;
delete[] sz;
}
int find(int p) {
assert(p >= 0 && p < count);
while (parent[p] != p) {
p = parent[p];
}
return p;
}
bool isConnected(int p, int q) {
return find(p) == find(q);
}
void unionElements(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot)
return;
// 判断根节点的sz谁大.大的作为根节点.
// 并维护sz
if (sz[pRoot] < sz[qRoot]) {
parent[pRoot] = qRoot;
sz[qRoot] += sz[pRoot];
}
else {
parent[qRoot] = pRoot;
sz[pRoot] += sz[qRoot];
}
}
};
}
sz数组的问题
sz数组
: sz[i] 表示以元素i为根的集合中的元素个数有些数可能节点数量多.. 但是它真实的高度并不高.
在合并时. 重要的是判断两个数的高度.
将 sz数组改成rank数组
rank[i] 表示以元素i为根的树的高度
// rank[i] 表示以元素i为根的树的高度
namespace UF4 {
class UnionFind
{
private:
int *parent;
int count;
int *rank; // rank[i] 表示以元素i为根的树的高度
public:
UnionFind(int n) {
parent = new int[n];
rank = new int[n];
count = n;
for (int i = 0; i < count; i++) {
parent[i] = i;
rank[i] = 1; // 初始化为1
}
}
~UnionFind()
{
delete[] parent;
delete[] rank;
}
int find(int p) {
assert(p >= 0 && p < count);
while (parent[p] != p) {
p = parent[p];
}
return p;
}
bool isConnected(int p, int q) {
return find(p) == find(q);
}
void unionElements(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot)
return;
if (rank[pRoot] < rank[qRoot]) {
parent[pRoot] = qRoot;
}
else if(rank[pRoot] > rank[qRoot]){
parent[qRoot] = pRoot;
}
else { //rank[pRoot] == rank[qRoot] 需要维护高度
parent[pRoot] = qRoot;
rank[qRoot] += 1;
}
}
};
}
路径压缩!!!
- 对
find
查询操作进行优化.
// 如父亲节点和该节点不相等.
// 则将将节点指向其爷爷节点.
// 再将p赋值成为其父亲节点(即原来的爷爷节点) 继续向上搜索.
int find(int p) {
assert(p >= 0 && p < count);
// 连跳两级
while (parent[p] != p) {
parent[p] = parent[parent[p]];
p = parent[p];
}
return p;
}
- 递归优化
将p节点直接挂到其根节点下.
// 路径压缩1
int find2(int p) {
if (parent[p] != p) {
parent[p] = find2(parent[p]);
}
return parent[p];
}
Algorithm | Constructor | Union | Find |
---|---|---|---|
Quick-Find | N | N | 1 |
Quick-Union | N | Tree height | Tree height |
Weighted Quick-Union | N | lgN | lgN |
Weighted Quick-Union With Path Compression | N | Very near to 1 (amortized) | Very near to 1 (amortized) |