并查集
基本原理:
对于多个集合,每个集合中的多个元素用一颗树的形式表示,根节点的编号即为整个集合的编号,每个树上节点存储其父节点,使得当前集合的每个子节点都可以通过对父节点的询问来找到根节点,根节点的父节点为其本身。
本文中 f [ x ] f[x] f[x]表示 x x x点的父节点。
并查集的作用:
-
判断当前点是否为当前集合的根节点,即 i f ( f [ x ] = = x ) if(f[x] == x) if(f[x]==x)时,当前点为所属集合的根节点
-
求取当前点的集合编号(根节点),即 w h i l e ( f [ x ] ! = x ) x = f [ x ] while(f[x] != x) x = f[x] while(f[x]!=x)x=f[x],直到找到根节点
优化点:通过路径压缩,可以实现一次遍历,使得路径上所有节点均指向集合根节点,减少后续遍历次数
-
合并两集合,即将其中一个集合的根节点作为另一个集合的根节点的父节点,即 f [ x ] = f [ y ] f[x] = f[y] f[x]=f[y]
基础操作:
int find(int x)//返回x所在集合的编号,即x的根节点,用路径压缩优化
{
if(f[x] != x) f[x] = find(f[x]);//如何p[x]非根节点,则由父节点继续向上查找
return f[x];//根节点
}
基础运用:求连通块中点的数量
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 2e5+10;
int f[N], n, m;
int st[N];
int find(int x) {
if(f[x] != x) f[x] = find(f[x]);
return f[x];
}
int main() {
cin >> n >> m;
for(int i = 1; i <= n; i ++) {
f[i] = i;//各集合初始化,各自一个集合
st[i] = 1;//各自独立集合的数量为1
}
while(m --) {
string op; cin >> op;
int a, b;
if(op[0] == 'C'){//连接操作
cin >> a >> b;
if(find(a) == find(b)) continue; //若ab已在同一根节点下无需操作
//顺序不能搞反
st[find(b)] += st[find(a)];//a的集合归b则b集合要加上a集合
f[find(a)] = find(b);//将a的根节点连接到b的根节点之下
}
else if(op[1] == '1'){//询问ab是否在同一集合当中,即是否在同一联通块当中
cin >> a >> b;
if(find(a) == find(b)) puts("YES");
else puts("NO");
}
else {//询问a所在集合的大小,即联通块的数量
cin >> a;
cout << st[find(a)] << endl;
}
}
return 0;
}
增补应用:求取带权并查集
//在每条边添加权值
int find(int x){
if(f[x] != x){
int u = find(f[x]);
value[x] += value[f[x]];
f[x] = u;
}
return f[x];
}
//两个点
int pa = find(a);
int pb = find(b);
if(pa != pb){
p[pa] = pb;
value[pb] += value[pa];//b的根节点成为a的父节点,那么a的权值归入b
}
实例应用:
小苯的蓄水池:https://ac.nowcoder.com/acm/contest/93847/E
题意:
n个蓄水池,从1到n排成一排,第 i i i个蓄水池中的水量为 a i a_i ai;
相邻蓄水池有一个隔板,拿开隔板两个蓄水池的水合并,现在有两个操作:
- 1 将 l , r l, r l,r之间的所有隔板取出,使得这些水池合并
- 2 查询第 i i i个水池的水量,即当前合并水池的总水量 / 合并的水池数
思路:
-
easy:
-
使用并查集来维护每个水池,并记录当前集合中点的个数(即集合的权值),以及当前集合水池的总水量
-
对于l到r的水池,可以以l水池为基准,后续水池若与l水池的根节点不同,则进行合并操作
-
此时的时间复杂度为O(mn),我们可以通过区间维护来进行优化
-
-
hard:
- 将每个水池看做一个存在作用区间的集合,初始状态每个水池的左右边界均为本身
- 我们在进行合并操作时,不再遍历l到r的水池,这样会重复遍历,从而浪费时间,可以选择l水池的右边界到r水池的左区间进行遍历
- 每一次合并操作,对当前集合的区间进行拓展,不断压缩需要遍历的区间,从而大大减小时间复杂度。
注意:
- 不能在每次取出隔板后去修改原水池的数值,会有精度损失
- 会出现重复取出隔板的情况(隔板取走后便一直为取出状态)
AC code:
#include<bits/stdc++.h>
#define endl '\n'
#define fast() ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr)
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
const int N = 2e5+10, M = 2001;
const int INF = 0x3f3f3f3f, MOD = 1e9+7;
int a[N], f[N];
struct val{
int cnt;
double sum;
int L, R;
}va[N];
int find(int x) {
if (f[x] != x) f[x] = find(f[x]); return f[x];
}
void solve() {
int n, m; scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i ++) {
scanf("%d", &a[i]);
f[i] = i;
va[i].cnt = 1, va[i].sum = a[i];
va[i].L = i, va[i].R = i;
}
while (m --) {
int op; scanf("%d", &op);
if (op == 1) {
int l, r; scanf("%d %d", &l, &r);
for (int i = va[f[l]].R + 1; i <= va[f[r]].L; i ++) {
int fa = find(i), fb = find(l);
if (fa == fb) continue;
f[fb] = fa;
va[fa].cnt += va[fb].cnt, va[fa].sum += va[fb].sum;
va[f[l]].R = max(va[f[l]].R, va[f[i]].R);
va[f[l]].L = min(va[f[l]].L, va[f[i]].L);
}
} else {
int pos; scanf("%d", &pos);
pos = find(pos);
double ans = va[pos].sum / va[pos].cnt;
printf("%.9lf\n", ans);
}
}
}
int main() {
fast();
int T = 1;
//cin >> T;
while (T --) {
solve();
}
return 0;
}