介紹
摘自羅勇軍,郭衛斌的《算法競賽入門到進階》上的說明:
並查集(Disjoint Set)是一種非常精巧而且食用的數據結構,它主要用於處理一些不相交集合的合併問題。經典的例子有連通子圖、最小生成樹Kruskal算法和最近公共祖先(Lowser Common Ancestors, LCA)等。在歐拉路上也常常會用到並查集。
並查集:將編號分別爲1-n的n個對象劃分爲不相交集合,在每個集合中,選擇其中某個元素代表所在集合。在這個集合中,並查集的操作有初始化,合併和查找。
以下有模板和習題,習題都是來自於該書的,大概分爲並查集模板題(1,2,3,9),帶權並查集(4,5,6,7,8),並查集的應用(10,11,12,13)。
這裏根據我認爲的難度做了標記"<?>",如果有十分相似的題難度不一樣,是因爲如果把難度高的同類題做完了自然就感覺難度下降了。
文章目錄
- 介紹
- 模板
- 習題
- 1.POJ - 2524 Ubiquitous Religions <1>
- 2.POJ - 1611 The Suspects <1>
- 3.POJ - 2236 Wireless Network <1>
- 4.POJ - 1703 Find them, Catch them <3>
- 5.POJ - 2492 A Bug's Life <2>
- 6.POJ - 1182 食物鏈<2>
- 7.POJ - 1988 Cube Stacking <4>
- 8.HDU - 3635 Dragon Balls <3>
- 9.HDU - 1856 More is better <1>
- 10.HDU - 1272 小希的迷宮 <2>
- 11.HDU - 1325 Is It A Tree? <2>
- 12 HDU - 1198 Farm Irrigation <2>
- 13.HDU - 6109 數據分割 <2>
- 結語
模板
初級的模板
int s[maxn];
void init_set() {//初始化
for (int i = 1; i <= maxn; i++)
s[i] = i;
}
int find_set(int x) {//查找
return x == s[x] ? x : find_set(s[x]);
}
void union_set(int x, int y) {//合併
x = find_set(x);
y = find_set(y);
if (x != y)s[x] = s[y];
}
評價:由於這些操作搜索深度是樹的長度,時間複雜度爲O(n)。
模板題:HDU - 1213 How Many Tables <1>
題目鏈接:http://acm.hdu.edu.cn/showproblem.php?pid=1213
#include<cstdio>
using namespace std;
const int maxn = 1000 + 5;
int s[maxn];
void init_set(int n) {
for (int i = 1; i <= n; i++)
s[i] = i;
}
int find_set(int x) {
return x == s[x] ? x : find_set(s[x]);
}
void union_set(int x, int y) {
x = find_set(x);
y = find_set(y);
if (x != y)s[x] = s[y];
}
int main(void) {
int t, n, m, a, b;
scanf("%d", &t);
while (t--) {
scanf("%d %d", &n, &m);
init_set(n);
for (int i = 1; i <= m; i++) {
scanf("%d %d", &a, &b);
union_set(a, b);
}
int cnt = 0;
for (int i = 1; i <= n; i++)
if (s[i] == i)cnt++;
printf("%d\n", cnt);
}
return 0;
}
合併的優化
合併操作時特意將高度較小的集合併到較大的集合上,能減少樹的高度,時間複雜度爲θ(nlogn) (證明未知)。
int s[maxn],height[maxn];
void init_set(int n) {
for (int i = 1; i <= n; i++){
s[i] = i; height[i]=0;
}
}
void union_set(int x, int y) {
x = find_set(x);
y = find_set(y);
if (height[x] == height[y]) {
height[x]++; s[y] = x;//還有一種方法是點數小的集合併到大的集合
}
else {
if (height[x] < height[y])//高度小的集合併到大的集合
s[x] = y;
else
s[y] = x;
}
}
查詢的優化——路徑壓縮
將所有後代結點直接指向祖先結點即根結點,可以達到O(1)的查詢效果。
遞歸形式:
int find_set(int x) {
return x == s[x] ? x : s[x] = find_set(s[x]);
}
非遞歸形式:
int find_set(int x) {
int r = x, i = x, j;
while (s[r] != r)r = s[r];//先確立根結點
while (i != j) {//把路徑上的結點依次指向根節點
j = s[i];
s[i] = r;
i = j;
}
}
習題
1.POJ - 2524 Ubiquitous Religions <1>
題目鏈接:https://vjudge.net/problem/POJ-2524
#include<cstdio>
using namespace std;
const int maxn = 50000 + 5;
int s[maxn], n, m, a, b, kase, cnt;
void init_set(int n) {
cnt = 0;
for (int i = 1; i <= n; i++)
s[i] = i;
}
int find_set(int x) {
return x == s[x] ? x : s[x] = find_set(s[x]);
}
void union_set(int x, int y) {
x = find_set(x);
y = find_set(y);
if (x != y)s[x] = y;
}
int main(void) {
while (~scanf("%d %d", &n, &m) && n) {
init_set(n);
for (int i = 1; i <= m; i++) {
scanf("%d %d", &a, &b);
union_set(a, b);
}
for (int i = 1; i <= n; i++)
if (s[i] == i)cnt++;
printf("Case %d: %d\n", ++kase, cnt);
}
return 0;
}
2.POJ - 1611 The Suspects <1>
題目鏈接:https://vjudge.net/problem/POJ-1611
#include<cstdio>
using namespace std;
const int maxn = 30000 + 5;
int s[maxn], n, m, k, a, cnt[maxn];
void init_set(int n) {
for (int i = 0; i < n; i++) {
s[i] = i; cnt[i] = 1;
}
}
int find_set(int x) {
return x == s[x] ? x : s[x] = find_set(s[x]);
}
void union_set(int x, int y) {
x = find_set(x);
y = find_set(y);
if (x != y) {
if (!x) {
s[y] = x; cnt[x] += cnt[y]; cnt[y] = 0;
}
else {
s[x] = y; cnt[y] += cnt[x]; cnt[x] = 0;
}
}
}
int main(void) {
while (~scanf("%d %d", &n, &m) && n) {
init_set(n);
for (int i = 1; i <= m; i++) {
scanf("%d", &k);
int root = -1;
for (int i = 0; i < k; i++) {
scanf("%d", &a);
if (root < 0)root = a;
union_set(root, a);
}
}
printf("%d\n", cnt[0]);
}
return 0;
}
3.POJ - 2236 Wireless Network <1>
題目鏈接:https://vjudge.net/problem/POJ-2236
分析:又一個模板題,數據寬鬆,直接暴力也不卡時間…
#include<cmath>
#include<iostream>
using namespace std;
const int maxn = 1000 + 5;
int s[maxn], x[maxn], y[maxn], ok[maxn], n, d, p, q, sum;
void init(int n) {
for (int i = 1; i <= n; i++)
s[i] = i;
}
int fa(int x) {
return x == s[x] ? x : s[x] = fa(s[x]);
}
void bing(int x, int y) {
x = fa(x);
y = fa(y);
if (x != y)s[x] = s[y];
}
int main(void) {
cin >> n >> d;
init(n);
for (int i = 1; i <= n; i++) {
cin >> x[i] >> y[i];
}
char op;
while (cin >> op) {
cin >> p;
if (op == 'O') {
int cnt = 0;
for (int i = 1; i <= n; i++) {
if (!ok[i])continue;
double dis = sqrt(pow(1.0 * (x[p] - x[i]), 2.0) + pow(1.0 * (y[p] - y[i]), 2.0));
if (dis <= d)bing(p, i);
if (cnt == sum)break;
}
ok[p] = 1; sum++;
}
else {
cin >> q;
if (fa(p) == fa(q))cout << "SUCCESS\n";
else cout << "FAIL\n";
}
}
return 0;
}
4.POJ - 1703 Find them, Catch them <3>
題目鏈接:https://vjudge.net/problem/POJ-1703
知識點:關係型並查集
分析:根據題意總共有兩個集合,那麼可以建立父集的大小爲2*n,假想結點x有對立面x+n。對於結點a,b,若a,b爲不同類,則認爲a+n和b同類,a和b+n同類,然後查找的時候判斷4個結點的關係即可得知a,b是否同類。
參考博客:https://blog.csdn.net/ky961221/article/details/53384638
#include<cstdio>
using namespace std;
const int maxn = 1e5 + 5;
int s[2 * maxn], n, m, a, b;
char cmd;
void init_set(int n) {
for (int i = 0; i < 2 * n; i++)
s[i] = i;
}
int find_set(int x) {
return x == s[x] ? x : s[x] = find_set(s[x]);
}
void union_set(int x, int y) {
x = find_set(x);
y = find_set(y);
if (x != y)s[x] = s[y];
}
int main(void) {
int T; scanf("%d", &T);
while(T--){
scanf("%d %d", &n, &m);
init_set(n);
for (int i = 1; i <= m; i++) {
getchar();
scanf("%c %d %d", &cmd, &a, &b);
if (cmd == 'D') {
union_set(a + n, b);
union_set(a, b + n);
}
else {//集合相同,要麼直接在同一集合,要麼對立面在同一集合
if (find_set(a) == find_set(b) || find_set(a + n) == find_set(b + n))
printf("In the same gang.\n");
else if (find_set(a) == find_set(b + n) || find_set(a + n) == find_set(b))
printf("In different gangs.\n");
else
printf("Not sure yet.\n");
}
}
}
return 0;
}
5.POJ - 2492 A Bug’s Life <2>
題目鏈接:https://vjudge.net/problem/POJ-2492
分析:和上一題一模一樣。
#include<cmath>
#include<cstdio>
using namespace std;
const int maxn = 2000 + 5;
int s[2 * maxn], T, n, m, x, y;
void init(int n) {
for (int i = 1; i <= 2 * n; i++)
s[i] = i;
}
int fa(int x) {
return x == s[x] ? x : s[x] = fa(s[x]);
}
void bing(int x, int y) {
x = fa(x);
y = fa(y);
if (x != y)s[x] = s[y];
}
int main(void) {
scanf("%d", &T);
for (int kase = 1; kase <= T; kase++) {
scanf("%d %d", &n, &m);
init(n); int flag = 0;
for (int i = 1; i <= m; i++) {
scanf("%d %d", &x, &y);
if (fa(x) == fa(y)||fa(x+n)==fa(y+n))flag = 1;
bing(x + n, y);
bing(x, y + n);
}
if (kase != 1)printf("\n");
printf("Scenario #%d:\n", kase);
if (flag)printf("Suspicious bugs found!\n");
else printf("No suspicious bugs found!\n");
}
return 0;
}
6.POJ - 1182 食物鏈<2>
題目鏈接:https://vjudge.net/problem/POJ-1182
分析:(帶權)也爲關係型並查集,該題有三類集合,同樣可以用第4題的辦法,a與b同類,則吃a的(即a+n)與吃b的(即b+n)同類,被a吃的(即a+2*n)與被b吃的(即b+2n)同類。若a吃b,則a於b+n同類,a+n與b+2*n同類,a+2n與b同類。總之就是a爲一類,吃a的爲一類,被a吃的爲一類。
#include<cstdio>
using namespace std;
const int maxn = 5e5 + 5;
int s[3 * maxn], n, k, d, x, y, ans;
void init_set(int n) {
for (int i = 1; i <= 3 * n; i++)
s[i] = i;
}
int fa(int x) {
return x == s[x] ? x : s[x] = fa(s[x]);
}
void union_set(int x, int y) {
x = fa(x);
y = fa(y);
if (x != y)s[x] = s[y];
}
int main(void) {
scanf("%d %d", &n, &k);
init_set(n);
while (k--) {
scanf("%d %d %d", &d, &x, &y);
if (x > n || y > n) {
ans++; continue;
}
if (d == 1) {//X吃Y 或 X被Y吃 THEN false
if (fa(x) == fa(y + n) || fa(x) == fa(y + 2 * n)) {
ans++; continue;
}
union_set(x, y);//y的同類
union_set(x + n, y + n);//吃y的爲一類
union_set(x + 2 * n, y + 2 * n);//被y吃的爲一類
}
else {
if (x == y)ans++;
else {//X同Y 或 Y吃X THEN false
if (fa(x) == fa(y) || fa(y) == fa(x + n)) {
ans++; continue;
}
union_set(x, y + n);//吃y的爲一類
union_set(x + n, y + 2 * n);//被y吃的爲一類
union_set(x + 2 * n, y);//y的同類
}
}
}
printf("%d\n", ans);
return 0;
}
總結:這類題可以根據題意來分出k個關係,若元素最多爲n,則元素a與a,a+n,a+2*n,… ,a+(k-1)*n分別有着對應的關係,再通過這些關係解決問題。
7.POJ - 1988 Cube Stacking <4>
題目鏈接:https://vjudge.net/problem/POJ-1988
新知識點,通過其他博客得知這類也爲帶權並查集。主要的要點是利用好了遞歸來更新數據,要做到在路徑壓縮的同時還使得數據可以保證正確的動態變化真的好神奇…
參考博客:https://blog.csdn.net/qq_43750980/article/details/98500722
代碼說明: 增加數組d和size,d表示該點到根結點要走幾步,size表示集合一共有多少個結點,則答案爲size-d-1(除去自身)。
重點:更新距離=原距離(到原祖先結點距離)+原祖先到新祖先的距離
#include<cmath>
#include<cstdio>
using namespace std;
const int maxn = 3e5 + 5;
int f[maxn], d[maxn], size[maxn], P, x, y;
void init() {
for (int i = 1; i < maxn; i++) {
f[i] = i; size[i] = 1; d[i] = 0;
}
}
int find(int x) {
if (x == f[x])return x;
int fa = find(f[x]);//必須先進入下一層更新父節點的d[x]
d[x] += d[f[x]];
return f[x] = fa;//父節點更新的是父節點的父節點因此f[x]仍可用
}
void bing(int x, int y) {
x = find(x);
y = find(y);
f[y] = x;
d[y] = size[x];
size[x] += size[y];
}
int main(void) {
char op;
scanf("%d", &P);
init();
while (P--) {
getchar();
scanf("%c %d", &op, &x);
if (op == 'M') {
scanf("%d", &y); bing(x, y);
}
else printf("%d\n", size[find(x)] - d[x] - 1);
}
return 0;
}
8.HDU - 3635 Dragon Balls <3>
題目鏈接:http://acm.hdu.edu.cn/showproblem.php?pid=3635
分析:上一題的套路,每次新轉移發生時,原根結點必定是第一次轉移,而子節點都隨着根節點的轉移而轉移,因此也可以用遞歸的方式來更新移動次數。
#include<cstdio>
using namespace std;
const int maxn = 10000 + 5;
int T, N, M, x, y, f[maxn], times[maxn], cnt[maxn];
void init(int N) {
for (int i = 1; i <= N; i++) {
f[i] = i; cnt[i] = 1; times[i] = 0;
}
}
int find(int x) {
if (x == f[x])return x;
int fa = find(f[x]);
times[x] += times[f[x]];
return f[x] = fa;
}
void bing(int x, int y) {
x = find(x);
y = find(y);
if (x != y) {//將x轉移到y
f[x] = y;
cnt[y] += cnt[x];
times[x]++;
}
}
int main(void) {
char op;
scanf("%d", &T);
for (int kase = 1; kase <= T; kase++) {
scanf("%d %d", &N, &M);
init(N);
printf("Case %d:\n", kase);
while (M--) {
getchar();
scanf("%c %d", &op, &x);
if (op == 'T') {
scanf("%d", &y); bing(x, y);
}
else {
int id = find(x);
printf("%d %d %d\n", id, cnt[id], times[x]);
}
}
}
return 0;
}
9.HDU - 1856 More is better <1>
題目鏈接:http://acm.hdu.edu.cn/showproblem.php?pid=1856
分析:又來模板題了,注意答案可能爲1,即輸入爲0。
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 10000000 + 5;
int T, x, y, f[maxn], cnt[maxn], ans;
void init(int N) {
ans = 1;
for (int i = 1; i < maxn; i++) {
f[i] = i; cnt[i] = 1;
}
}
int find(int x) {
if (x == f[x])return x;
return f[x] = find(f[x]);
}
void bing(int x, int y) {
x = find(x);
y = find(y);
if (x != y) {
f[x] = y;
cnt[y] += cnt[x];
ans = max(ans, cnt[y]);
}
}
int main(void) {
while (~scanf("%d", &T)) {
init(T);
while (T--) {
scanf("%d %d", &x, &y);
bing(x, y);
}
printf("%d\n", ans);
}
return 0;
}
10.HDU - 1272 小希的迷宮 <2>
題目鏈接:http://acm.hdu.edu.cn/showproblem.php?pid=1272
分析:該題的並查集作用是判斷是否輸入完之後只有一個集合。n個結點用n-1條邊連接起來,如果任意再連上兩個結點必將構成迴路,不合題意,根據這點判斷迷宮是否合法。
#include<set>
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 100000 + 5;
set<int>vised;
int T, x, y, f[maxn];
void init() {
vised.clear();
for (int i = 1; i < maxn; i++)
f[i] = i;
}
int find(int x) {
if (x == f[x])return x;
return f[x] = find(f[x]);
}
void bing(int x, int y) {
vised.insert(x);
vised.insert(y);
x = find(x);
y = find(y);
if (x != y) f[x] = y;
}
int main(void) {
while (~scanf("%d %d", &x, &y) && x != -1) {
if (x == 0) { printf("Yes\n"); continue; }
init(); bing(x, y);
int num = 0, fa = -1, cnt = 1;
while (~scanf("%d %d", &x, &y) && x) {
bing(x, y); cnt++;
}
for (set<int>::iterator it = vised.begin(); it != vised.end(); ++it) {
if (*it == f[*it])num++;
}
if (num!=1)printf("No\n");
else {;
if (cnt != vised.size() - 1)printf("No\n");
else printf("Yes\n");
}
}
return 0;
}
11.HDU - 1325 Is It A Tree? <2>
題目鏈接:http://acm.hdu.edu.cn/showproblem.php?pid=1325
分析:乍看一下,似乎和上題一模一樣,但是其實不對。該題是有方向的,而且需要記錄入度,還有結束標誌是輸入爲負數而不是-1 -1。
知識點:只有一個結點入度爲0,其它結點入度均爲1的圖是樹。
#include<set>
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 100000 + 5;
set<int>vised;
int T, x, y, f[maxn], in[maxn], kase;
void init() {
vised.clear();
for (int i = 1; i < maxn; i++) {
f[i] = i; in[i] = 0;
}
}
int find(int x) {
if (x == f[x])return x;
return f[x] = find(f[x]);
}
void bing(int x, int y) {
vised.insert(x);
vised.insert(y);
x = find(x);
y = find(y);
if (x != y) f[x] = y;
}
int main(void) {
while (~scanf("%d %d", &x, &y) && x >= 0) {
printf("Case %d is ", ++kase);
if (x == 0) { printf("a tree.\n"); continue; }
init(); bing(x, y); in[y]++;
int num = 0, fa = -1, num2 = 0, num3 = 0;
while (~scanf("%d %d", &x, &y) && x) {
bing(x, y); in[y]++;
}
for (set<int>::iterator it = vised.begin(); it != vised.end(); ++it) {
if (*it == f[*it])num++;
if (in[*it] == 1)num2++;
if (in[*it] == 0)num3++;
}
if (num != 1 || num3 != 1 || num2 != vised.size() - 1)printf("not a tree.\n");
else printf("a tree.\n");
}
return 0;
}
12 HDU - 1198 Farm Irrigation <2>
題目鏈接:http://acm.hdu.edu.cn/showproblem.php?pid=1198
分析:並查集的應用,其實DFS,BFS也差不多,我猜想代碼量都不會差很大。存好各種塊的上下左右是否可以連通就可以了。
#include<cstdio>
using namespace std;
const int maxn = 256;
int T, u[maxn], d[maxn], l[maxn], r[maxn], f[2505], m, n;
char g[55][55];
void init() {
for (int i = 0; i < 2505; i++)
f[i] = i;
}
int find(int x) {
return x == f[x] ? x : f[x] = find(f[x]);
}
void bing(int x, int y) {
x = find(x);
y = find(y);
if (x != y) f[x] = y;
}
void solve() {
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++) {
if (u[g[i][j]] && i-1 >=0 && d[g[i - 1][j]])bing(i * n + j, (i - 1) * n + j);
if (d[g[i][j]] && i + 1 < m && u[g[i + 1][j]])bing(i * n + j, (i + 1) * n + j);
if (l[g[i][j]] && j - 1 >= 0 && r[g[i][j - 1]])bing(i * n + j, i * n + j - 1);
if (r[g[i][j]] && j + 1 < n && l[g[i][j + 1]])bing(i * n + j, i * n + j + 1);
}
}
int main(void) {
u['A'] = u['B'] = u['E'] = u['G'] = u['H'] = u['J'] = u['K'] = 1;
d['C'] = d['D'] = d['E'] = d['H'] = d['I'] = d['J'] = d['K'] = 1;
l['A'] = l['C'] = l['F'] = l['G'] = l['H'] = l['I'] = l['K'] = 1;
r['B'] = r['D'] = r['F'] = r['G'] = r['I'] = r['J'] = r['K'] = 1;
while (~scanf("%d %d", &m, &n) && m > 0) {
init();
for (int i = 0; i < m; i++) {
getchar();
for (int j = 0; j < n; j++) {
scanf("%c", &g[i][j]);
}
}
solve();
int cnt = 0;
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
if (f[i * n + j] == i * n + j)
cnt++;
printf("%d\n", cnt);
}
return 0;
}
13.HDU - 6109 數據分割 <2>
題目鏈接:http://acm.hdu.edu.cn/showproblem.php?pid=6109
分析:並查集存同類,set存異類,暴力初始化,時間也過得去,數據不強。
解釋一下題意:輸入L行,本來是有k組數據被分隔符分開了,但是分隔符被小w刪去看不出來數據分組,但已知每組數據的最後一個約束條件會弄假該組數據的邏輯,也就是一旦遇到某約束條件使得邏輯爲假則出現獨立的一組數據,如果到最後也沒有遇到,則不爲一組數據。
#include<cstdio>
#include<vector>
#include<set>
using namespace std;
const int maxn = 100000 + 5;
int L, i, j, e, s[maxn], cnt;
vector<int>ans;
set<int>dif[maxn];
void init() {
for (int i = 0; i < maxn; i++) {
s[i] = i; dif[i].clear();
}
}
int find(int x) {
return x == s[x] ? x : s[x] = find(s[x]);
}
void merge(int x, int y) {
x = find(x);
y = find(y);
if (x != y) s[x] = y;
for (set<int>::iterator it = dif[x].begin(); it != dif[x].end(); ++it)
dif[y].insert(*it);
}
int main(void) {
scanf("%d", &L);
init();
while (L--) {
scanf("%d %d %d", &i, &j, &e);
cnt++;
if (e) {
if (dif[find(i)].count(find(j)) || dif[find(j)].count(find(i))) {
ans.push_back(cnt);
cnt = 0; init();
continue;
}
merge(i, j);
}
else {
if (find(i) == find(j)) {
ans.push_back(cnt);
cnt = 0; init();
continue;
}
dif[find(i)].insert(find(j));
dif[find(j)].insert(find(i));
}
}
printf("%d\n", ans.size());
for (int i = 0; i < ans.size(); i++)
printf("%d\n", ans[i]);
return 0;
}
結語
差不多了,本篇到此爲止,這裏記錄的大都是以並查集爲主的題,很多算法是以並查集爲輔的,還有很多地方需要探索啊,這只是基礎而已。書上剩餘一個題hdu2586需要用到Tarjan,暫時不會,習題中有7個是和kuangbin專題重合了,還有7個是沒有的,估計寫了也不是放到一篇文章了,而且應該也有的是水題沒必要記錄了,慢慢來吧,留到以後複習來寫也不錯。