並查集
普通並查集:
路徑壓縮寫法:
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
vjudge
很傻逼的網格圖問題。
同列 / 同行放到一個連通塊裡面,並查集輕鬆維護。
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;
}
擴充套件域並查集:
應用於有多個集合且有關係時。
另外這東西還能判二分圖。
具體的就是建多個並查集。
帶權並查集:
維護邊的時候帶權。
一般用路徑壓縮能夠減少維護的資訊。
合併時候的權值更新可以用向量去理解。
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;