图论连通性相关
并查集
普通并查集
路径压缩写法:
struct Union_Find_Set {
int f[N];
inline void init() {
for(int i = 1 ; i <= n ; ++ i)
f[i] = i;
}
inline int find(int x) {
if(x != f[x]) f[x] = find(f[x]);
return f[x];
}
inline void merge(int a, int b) {
int x = find(a), y = find(b);
f[y] = f[x];
}
} Set;
启发式合并写法:
struct Union_Find_Set {
int f[N], h[N];
inline void init() {
for(int i = 1 ; i <= n ; ++ i)
f[i] = i, h[i] = 1;
}
inline int find(int x) {
if(x != f[x]) return find(f[x]);
return f[x];
}
inline void merge(int a, int b) {
int x = find(a), y = find(b);
if(h[x] > h[y]) f[y] = f[x];
else {
f[x] = f[y];
if(h[x] == h[y]) ++ h[y];
}
}
} Set;
这是按秩合并的,当然也可以按元素个数启发式合并。
这俩可以写一起:
struct Union_Find_Set {
int f[N], h[N];
inline void init() {
for(int i = 1 ; i <= n ; ++ i)
f[i] = i, h[i] = 1;
}
inline int find(int x) {
if(x != f[x]) f[x] = find(f[x]);
return f[x];
}
inline void merge(int a, int b) {
int x = find(a), y = find(b);
if(h[x] > h[y]) f[y] = f[x];
else {
f[x] = f[y];
if(h[x] == h[y]) ++ h[y];
}
}
} Set;
要注意的是 find
里面的 if
写 return
的复杂度是假的。
例题
多而且杂,一般可用并查集维护的性质很突出。
CF217A Ice Skating
很傻逼的网格图问题。
同列 / 同行放到一个连通块里面,并查集轻松维护。
P2658 汽车拉力比赛
有点傻逼的网格图问题。
分析:
1.由于题目要求保证所有路标相互可达,于是想到并查集
2.发现对于任意一个 \(D\),模拟一次都是 \(O(nm)\) 的,且 \(D\) 越大所有路标越可能相互可达,考虑二分 \(D\)
3.每次扫网格图,对于每个点,若和其相邻点高度差小于当前二分的 \(D\) 则连边,最后判断所有路标是否在一个连通块内
4.坐标 \((x, y)\) 可以改写成 \((x - 1)m + y\)
时间复杂度 \(O(nm\log V)\),其中 \(V\) 为值域。
check
部分代码:
inline bool check(int x) {
init();
for(int i = 1 ; i <= n ; ++ i)
for(int j = 1 ; j <= m ; ++ j) {
//to(i, j) 为坐标的转换函数。
int pos = to(i, j), pos1 = to(i + 1, j), pos2 = to(i, j + 1);
if(i + 1 <= n && abs(a[pos] - a[pos1]) <= x) merge(pos, pos1);
if(j + 1 <= m && abs(a[pos] - a[pos2]) <= x) merge(pos, pos2);
}
int Fa = 0;
for(int i = 1 ; i <= n ; ++ i)
for(int j = 1 ; j <= m ; ++ j)
if(vis[to(i, j)]) {
if(! Fa) Fa = find(to(i, j));
else if(Fa != find(to(i, j))) return false;
}
return true;
}
扩展域并查集(种类并查集)
应用于有多个集合且有关系时。
另外这东西还能判二分图。
具体的就是建多倍点。
例题
P1892 [BOI2003] 团伙
算是板题?
考虑如何去维护关系。我们用 \([1, n] \to [1, n],[n + 1, 2n] \to [n + 1, 2n]\) 表示朋友关系,用 \([1, n] \to [n + 1, 2n],[n + 1, 2n] \to [1,n]\) 表示敌人关系。
按照题意,每次朋友操作就 \(x \to y\),每次敌人操作则 \(x \to y + n, y \to x + n\)。
那么为什么朋友操作的时候不要 \(x + n \to y + n\) 呢?是因为题目中没有描述一个人的朋友的敌人是敌人。
查询代码:
while(m --) {
cin >> op >> x >> y;
if(op == 'E') merge(x, y + n), merge(y, x + n);
else merge(x, y);
}
P2024 [NOI2001] 食物链
种类变成 \(3\) 个,同时要判断是否和之前冲突。
维护方式和上一题类似,冲突判断的具体方式为:
1.本次操作为同类操作但之前有过捕食操作
2.本次操作为捕食操作但之前有过同类操作或逆向的捕食操作
查询部分代码:
while(m --) {
cin >> op >> x >> y;
if((x == y && op == 2) || x > n || y > n) {
++ ans;
continue;
}
if(op == 1) {
if(find(x) == find(y + n) || find(y) == find(x + n)) ++ ans;
else merge(x, y), merge(x + n, y + n), merge(x + 2 * n, y + 2 * n);
}
else {
if(find(x) == find(y) || find(y) == find(x + n)) ++ ans;
else merge(x, y + n), merge(x + n, y + 2 * n), merge(x + 2 * n, y);
}
}
P5937 [CEOI1999] Parity Game
很好的一道 trick。
首先一段区间 \(1\) 的奇偶怎么判?维护一个前缀和转换为判区间和的奇偶性即可。又出现关键字眼“是否出现过”,因此联想到并查集。奇偶性恰好可以使用并查集去维护。
不难发现将区间 \([l, r]\) 可以转换成单点 \(l - 1, r\)。若两点奇偶性相同则区间为偶数,否则为奇数。
考虑怎么去具体地维护奇偶。用种类并查集维护奇偶性,如果当前奇偶性与先前的发生矛盾,则直接退出询问。
注意值域过大,需要离散化。
给出询问的代码:
for(int i = 1 ; i <= q ; ++ i) {
a[i].l = lower_bound(b + 1, b + 1 + tot, a[i].l) - b;
a[i].r = lower_bound(b + 1, b + 1 + tot, a[i].r) - b;
if(a[i].op) {
if(find(a[i].l) == find(a[i].r + tot)) return cout << i - 1, 0;
else {
merge(a[i].l, a[i].r);
merge(a[i].l + tot, a[i].r + tot);
}
}
else {
if(find(a[i].l) == find(a[i].r)) return cout << i - 1, 0;
else {
merge(a[i].l, a[i].r + tot);
merge(a[i].l + tot, a[i].r);
}
}
}
带权并查集
维护边的时候带权。
一般用路径压缩能够减少维护的信息。
合并时候的权值更新可以用向量去理解。
struct Union_Find_Set {
int f[N], val[N];
inline void init() {
memset(val, 0, sizeof val);
for(int i = 1 ; i <= n ; ++ i)
f[i] = i;
}
inline int find(int x) {
if(x != f[x]) val[x] += val[f[x]], f[x] = find(f[x]);
return f[x];
}
inline void merge(int a, int b, int Val) {
int x = find(a), y = find(b);
f[y] = f[x], val[y] = -val[a] + Val + val[b];
}
} Set;
当然操作不仅限于加法。
可撤销并查集
按加入的时间从后往前撤销。
用启发式合并写法实现(路径压缩改变树的形态),同时维护上述操作可以用栈来实现。
那么对于一条边为什么一定要是有顺序的撤销呢?如果不是按出栈的顺序撤销,那么必定有比他晚一些连边的集合的大小没法维护,所以必须按出栈顺序撤销。
struct Union_Find_Set {
int f[N], h[N];
stack<int> s;
inline void init() {
memset(val, 0, sizeof val);
for(int i = 1 ; i <= n ; ++ i)
f[i] = i, h[i] = 1;
}
inline int find(int x) {
if(x != f[x]) return find(f[x]);
return f[x];
}
inline void merge(int a, int b) {
int x = find(a), y = find(b);
if(h[x] > h[y]) f[y] = f[x];
else {
f[x] = f[y];
if(h[x] == h[y]) ++ h[y];
}
}
inline void Delete() {
if(s.empty()) return;
int k = s.top(); s.pop();
h[f[k]] -= h[k], f[k] = k;
}
inline void revoke(int x) {while(s.size() > x) Delete();}
} Set;