并查集
1 概念及其应用
1.1 基础概念
并查集:
一种用于处理集合合并和查找问题的数据结构,它支持两种基本操作:
1.查找(Find):确定某个元素属于哪个子集。
2.合并(Merge):将两个子集合并为一个集合。
1.2 常见应用
并查集主要用于求解图论中的连通性问题
比如 求两个元素是否在同一集合,求有多少个连通块。
利用这种特性,可以使用并查集来解决最小生成树问题和最近公共祖先问题等。 例如, 在最小生成树问题中,可以使用并查集来判断两个节点是否在同一个连通块中,从而避免形成环路; 在最近公共祖先问题中,可以使用并查集来维护每个节点的祖先节点,从而快速找到两个节点的最近公共祖先。
2 代码实现
2.1引入
2.1.1 实现方法
-
用编号最小的元素标记所在集合;
-
定义一个数组Set[1..n],其中Set[i]表示i所在的集合;
效率分析
find1(x) { return Set[x]; } //O(1) Merge(a,b) { i = min(a,b); j = max(a,b); for(k = 1;k <= N;k ++) { if(Set[k] == j) { Set[k] = i; } } } // O(n)
缺点:合并操作必须搜索所有元素
2.1.2 实现方法
-
每个集合用一颗“有根树”表示
-
定义数组Set[1..n]
-
-
Set[i] = i,则表示本集合,并是集合对应树的根
-
-
-
Set[i] = j,若i != j,则 j 是 i 的父节点
-
效率分析4
find2(x) { r = x; while(Set[r] != r) { r = Set[r]; } return r; } // 最坏情况O(n) // 一般情况...O(logn) merge2(a,b) { Set[a] = b; } // O(1)
2.1.3为了避免2.1.2的查找的最坏情况
-
方法:将深度小的数合并到深度最大的树
-
实现:假设两棵树的深度分别是h_1和h_2,则合并后的树高h是:
-
-
max(h_1,h_2), if h_1<>h_2
-
-
-
h_1 + 1, if h_1= h_2
-
-
效果:任意顺序的合并操作以后,包括k个节点的树的最大高度不超过
[lgk]
find2(x) { r = x; while(Set[r] != r) { r = Set[r]; } return r; } // 最坏O(logn) merge3(a,b) { if(height(a) == height(b)) { height[a] ++; Set[b] = a; } else if(height(a) < height(b)) { Set[a] = b; } else { height[b] = a; } } // O(1)
*2.2 实际使用(路径压缩)
上面的都不重要,实际使用只需使用带路径压缩的并查集即可。
-
具体方案:
1)找到根节点
2)修改查找路径上的是所有节点,将他们都指向根节点
-
代码如下:
find3(x) { if(Set[x] != x) { Set[x] = find3(Set[x]); // 递归 } return Set[x]; } // 路径压缩的查找操作
或
find3(x){return Set[x] = (Set[x] == x ? x : find3(Set[x]));}
3 题目练习
3.1 P68 联通块问题(0)【模板】
题意:给定无向图,有n个点,m条边(无重边自环),求图中所有联通块的大小。
思路:连通块问题,想到并查集来存储。那么如何计算每个连通块大小呢?用桶来计数,再存入vector中排序输出。
具体代码实现如下:
#include <bits/stdc++.h> using namespace std; using ll = long long; const int N = 2e5 + 9; ll pre[N]; // 上一节点 ll c[N]; // 桶 用来计数(大小) vector<int> v; // 存储各个连通块的大小,且方便排序,以从小到大输出 int find(int x) // 查找 { return pre[x] = (pre[x] == x ? x : find(pre[x])); } void merge(int a,int b) // 合并 { pre[find(a)] = find(b); } void solve() { int n,m;cin >> n >> m; for(int i = 1;i <= n;i ++) pre[i] = i; // 预处理 for(int i = 1;i <= m;i ++) { int x,y;cin >> x >> y; merge(x,y); } for(int i = 1;i <= n;i ++) { c[find(i)] ++; // 根节点相同即在同一个连通块,大小++ } for(int i = 1;i <= n;i ++) { if(c[i]) v.push_back(c[i]); // 如果>0,说明有以此为根的连通块 } sort(v.begin(),v.end()); // 排序 for(auto &i :v) cout << i << ' '; } int main(void) { ios::sync_with_stdio(0),cin.tie(0),cout.tie(0); int _ = 1;// cin >> _; while(_ --) { solve(); } return 0; }
3.2 F. Microcycle
题意:给定一个无向加权图,图中有 n个顶点和 m条边。
图中每对顶点之间最多有一条边,图中不包含循环(从顶点到自身的边)。该图不一定连通。
找出该图中最轻边的权重最小的简单循环(如果图中的循环不经过同一顶点两次,也不包含相同的边两次,则称为简单循环)。
思路:并查集判环 + dfs
查找最小的边权
2种做法:targan
/ 并查集 ==> 判环
这里采用并查集做法
枚举边 i ,对应2个点u , v。find(u) == find(v) 即表示已经在集合中了,加入边i那么会形成一个环。
题目要求我们最小的边权尽可能小,因此:
1)边权从大到小排序
2)枚举边i , 这保证了边权w[i]是当前的并查集中所有边权里面最小的
加入第i条边之后,有环,当前边权可能作为答案,枚举直至结束
mn
表示最小的在环里的边权
id表示相应mn
的编号
mn
=>id id=>u , v
建图,去掉第id条边,从起点u-->终点v的路径
具体实现请看代码注释,如下:
#include <bits/stdc++.h> using namespace std; using ll = long long; // 并查集判环 + dfs找最小的边权 ll n,m,root[200005],vis[200005]; // 分别是节点数 边数 根 判断是否搜索过 struct edge{ll u,v,w;}a[200005]; // 存边的信息 vector<ll> e[200005],ans; // 建图 和 路径 ll find(ll u) // 并查集查找根节点(路径压缩) { if(root[u] != u) root[u] = find(root[u]); return root[u]; } ll merge(ll u,ll v) // 并查集合并节点 并返回判断是否可成环 { u = find(u);v = find(v); if(u == v) return 1; root[u] = v; return 0; } void dfs(ll u,ll tag) // u表示节点 tag即目标taget { vis[u] = 1; // 记录搜索过的节点 ans.push_back(u); // 记录路径 if(u == tag) // 如果找到目标 { cout << ans.size() << "\n"; // 输出路径大小 for(ll &x :ans) cout << x << " "; // 输出路径 cout << "\n"; return; // 终止递归 } for(ll &v :e[u]) if(!vis[v]) dfs(v,tag); // 若此节点未被搜索过继续搜索 ans.pop_back(); // 向上回溯时 将存储在路径的节点删除 } void solve() { // input ans pre_work cin >> n >> m; ans.clear(); for(ll i = 1;i <= n;++ i) root[i] = i,e[i].clear(),vis[i] = 0; for(ll i = 1;i <= m;++ i) { ll u,v,w;cin >> u >> v >> w; a[i] = {u,v,w}; } // 按边权从大到小排序 保证最后得到的边权是最小的 sort(a + 1,a + m + 1,[](edge x,edge y){return x.w > y.w;}); ll mn = 1e9,id = 0; // mn表示最小边权 id表示相应编号 for(ll i = 1;i <= m;++ i) { if(merge(a[i].u,a[i].v) && a[i].w < mn) // 如果此边的两节点已经在同一集合里 并且· 该边权更小 { mn = a[i].w,id = i; } } // 建图 for(ll i = 1;i <= m;++ i) { if(i == id) continue; // 不存最小的边权的节点 不然路径会找错 抄近路啦 ll u = a[i].u,v = a[i].v; e[u].push_back(v); e[v].push_back(u); } cout << mn << " "; // 输出找到的循环中最小的边权 dfs(a[id].u,a[id].v); } int main() { ios::sync_with_stdio(0),cin.tie(0),cout.tie(0); int T = 1;cin >> T; while(T --) solve(); return 0; }
3.3 P9 【模板】可撤销并查集
最后提一嘴:这是b站up:Erik_Tse
开发的编程学习网站中的题目。
该网站StarryCoding是面向计算机专业学生的综合学习与刷题平台。由于课程的准备、录制、平台(前后端和评测机)的开发、推广都是由up一人完成,所以成本压的非常非常低,算法基础课仅售39元,此外,up还推出了前端Web的课程,,每周还有多种比赛,希望大家来玩一玩。
官网:starrycoding点com