数据结构--并查集-高效处理连通性问题
一、理论基础
(1)并查集的功能及实现原理
首先要知道并查集可以解决什么问题呢?
当我们需要判断两个元素是否在同一个集合里的时候,我们就要想到用并查集。
并查集主要有两个功能:
- 将两个元素添加到一个集合中。
- 判断两个元素在不在同一个集合
如何将两个元素添加到一个集合中?
将三个元素A,B,C (分别是数字)放在同一个集合,其实就是将三个元素连通在一起,如何连通呢。
只需要用一个一维数组来表示,即:father[A] = B,father[B] = C 这样就表述 A 与 B 与 C连通了(有向连通图)。
// 将v,u 这条边加入并查集 void join(int u, int v) {u = find(u); // 寻找u的根v = find(v); // 寻找v的根if (u == v) return; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回father[v] = u; }
find函数是如何实现的呢?其实就是通过数组下标找到数组元素,一层一层寻根过程,代码如下:
// 并查集里寻根的过程 int find(int u) {if (u == father[u]) return u; // 如果根就是自己,直接返回else return find(father[u]); // 如果根不是自己,就根据数组下标一层一层向下找 }
路径压缩:
// 并查集里寻根的过程 int find(int u) {if (u == father[u]) return u;else return father[u] = find(father[u]); // 路径压缩 }
默认自己指向自己,所以
// 并查集初始化 void init() {for (int i = 0; i < n; ++i) {father[i] = i;} }
如何判断两个元素是否在同一个集合里?
如果通过 find函数 找到 两个元素属于同一个根的话,那么这两个元素就是同一个集合,代码如下:
// 判断 u 和 v是否找到同一个根 bool isSame(int u, int v) {u = find(u);v = find(v);return u == v; }
(2)代码模版
int n = 1005; // n根据题目中节点数量而定,一般比节点数量大一点就好
vector<int> father = vector<int> (n, 0); // C++里的一种数组结构// 并查集初始化
void init() {for (int i = 0; i < n; ++i) {father[i] = i;}
}
// 并查集里寻根的过程
int find(int u) {if (u == father[u]) return u;else return father[u] = find(father[u]); // 路径压缩
}// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {u = find(u);v = find(v);return u == v;
}// 将v->u 这条边加入并查集
void join(int u, int v) {u = find(u); // 寻找u的根v = find(v); // 寻找v的根if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回father[v] = u;
}
通过模板,我们可以知道,并查集主要有三个功能。
- 寻找根节点,函数:find(int u),也就是判断这个节点的祖先节点是哪个
- 将两个节点接入到同一个集合,函数:join(int u, int v),将两个节点连在同一个根节点上
- 判断两个节点是否在同一个集合,函数:isSame(int u, int v),就是判断两个节点是不是同一个根节点
(3)模拟过程
join(3, 8)
在图中为什么 将 元素1 连向元素 3 而不是将 元素 8 连向 元素 3 呢?因为在
join(int u, int v)
函数里 要分别对 u 和 v 寻根之后再进行关联。
为什么 图中 8 又直接指向了 3 了呢?
代码在寻找根的过程中,会有路径压缩的优化 ,减少 下次查询的路径长度。
这里为什么是 2 指向了 6,因为 9的根为 2,所以用2指向6。
(4)应用
连通性问题(图论):判断两点是否在同一个连通块、连通块计数
【把图中每条边两端的点用并查集合并,判断某两个点是否在同一个集合里,就等于判断它们是否在同一个连通块中。查询连通性:判断两个点 a
和 b
是否连通,只需看 find(a) == find(b)
是否成立。】
【遍历所有节点,统计父节点为自身的节点数量(即根节点数量),即为连通块个数。if (fa[s] == s) landcount++;】
最小生成树
二、基础题练习
(1)排座位
#include <bits/stdc++.h>
using namespace std;const int N = 110; // 最大节点数
int fa[N], g[N][N]; // fa数组表示并查集的父节点,g是邻接矩阵记录关系(朋友或敌人)// 并查集的查找函数,带路径压缩
int find(int x) {if (x == fa[x]) return x; // 如果当前节点是自己的根,返回它自己else return fa[x] = find(fa[x]); // 否则递归查找,并路径压缩
}// 并查集的合并函数
void merge(int x, int y) {int xx = find(x); // 找到x的根int yy = find(y); // 找到y的根if (xx == yy) return; // 如果已经在同一个集合中,不用合并fa[yy] = xx; // 否则把y的集合合并到x中return;
}int main() {int n, m, k;cin >> n >> m >> k; // n个点,m条关系,k次查询// 初始化并查集:每个点的父亲是自己for (int i = 1; i < N; i++) fa[i] = i;// 输入m条关系for (int i = 0; i < m; i++) {int x, y, z;cin >> x >> y >> z;g[x][y] = g[y][x] = z; // 建图,g[x][y] = z,z是关系类型(1表示朋友,-1表示敌人)if (z == 1) // 如果是朋友merge(x, y); // 用并查集把他们合并为一个集合}// k 次查询while (k--) {int x, y;cin >> x >> y;int xx = find(x), yy = find(y); // 找到两个点所在的集合根if (xx == yy && g[x][y] != -1) // 如果在一个集合,并且不是敌人cout << "No problem" << endl;else if (xx != yy && g[x][y] != -1) // 如果不在一个集合,但不是敌人(可能认识但不熟)cout << "OK" << endl;else if (xx == yy && g[x][y] == -1) // 如果在一个集合但是敌人(朋友里有矛盾)cout << "OK but..." << endl;else // 不在一个集合,又是敌人(八竿子打不着)cout << "No way" << endl;}
}
(2)部落
#include <bits/stdc++.h>
using namespace std;const int N = 1e4 + 10; // 最大编号范围,保证能容纳所有人
int fa[N]; // 并查集的父亲数组
map<int, bool> st; // 用于记录输入中出现过的人(防止0~N全部初始化)// 查找函数(带路径压缩)
int find(int x) {if (x == fa[x]) return x;else return fa[x] = find(fa[x]);
}// 合并函数:把x和y所在的集合合并
void merge(int x, int y) {int xx = find(x), yy = find(y);if (xx == yy) return;fa[yy] = xx; // 将y的根指向x的根return;
}int main() {int n;cin >> n;// 初始化:fa[i] = ifor (int i = 0; i < N; i++) fa[i] = i;// 处理输入的朋友圈信息for (int i = 0; i < n; i++) {int k, first_;cin >> k; // 该组朋友圈人数for (int j = 0; j < k; j++) {if (j == 0) {cin >> first_; // 记录第一个人if (st.count(first_) == 0) st[first_] = 1; // 记录这个人出现了} else {int x;cin >> x; // 输入其余的人if (st.count(x) == 0) st[x] = 1; // 记录新出现的人merge(x, first_); // 把当前人和第一个人合并(建朋友圈)}}}// 输出所有出现过的人数cout << st.size() << " ";// 统计朋友圈数量(根节点数量)int num = 0;for (int i = 0; i < N; i++) {if (fa[i] == i && st.count(i) != 0) // 是自己父亲+出现过的人num++;}cout << num << endl;// 查询部分int q;cin >> q;while (q--) {int x, y;cin >> x >> y;if (find(x) == find(y)) cout << "Y" << endl; // 在同一个朋友圈else cout << "N" << endl; // 不在一个朋友圈}return 0;
}