-
概述
并查集是一种数据结构,主要处理一些不相交的集合的合并问题。就是集合的合并操作。经典的例子有:连通子图、最小生成树Kruskal算法和最近公共祖先等。并查集主要操作有初始化、合并、查询。 -
优化
合并优化:在合并两个不同集合的元素时,需要找到他们的根结点,将根结点合并。在合并的时候将高度较小的树合并在高度较高的树下,能有效减小树的高度,有利于查询。路径压缩:在直接进行简单合并的情况下,在某个集合中会形成树状的结构,这种结构不利于根结点的查询(需要逐层向上查询,速度比较慢),所以在许多情况下需要在查询根结点的同时,将同一集合中的各元素的双亲直接更改为根结点,这样在查询的时候就会快很多,这个操作称为路径压缩。
并不是在所有情况下都需要进行这两种优化,例如接下来龙珠的的题目中,合并后树的高度可以作为龙珠搬运的次数,在时间允许的范围内可以简化代码,思路也比较好理解。
-
练习
How Many Tables
Ubiquitous Religions
The Suspects
Find them, Catch them
Wireless Network
A Bug’s Life
Cube Stacking
食物链
Dragon Balls
More is better
小希的迷宫
Is It A Tree?
Farm Irrigation题意:n个人参加晚宴,完全不认识的两个人不能被分配在同一餐桌,认识具有传递性:A认识B B认识C,那么A和C也认识,所以A、B、C可以在同一张餐桌上。
#include<iostream> #include<cstdio> #include<cstdlib> using namespace std; int pe[1050]; int high[1050];//树的高度 int N, M; int ans; void init_set() { for(int i=1; i<=N; i++) { pe[i] = i; high[i] = 0; } } int find_set(int x) { int root = x; while(root!= pe[root]) root = pe[root]; int i = x, temp; while(pe[i]!=i) { temp = pe[i]; pe[i] = root; i = temp; } return root; } void union_set(int A, int B) { A = find_set(A); B = find_set(B); if(high[A] == high[B]) { pe[B] = A; high[A]++; } if(high[A]>high[B]) pe[B] = A; else pe[A] = B; } int main() { int T; cin>> T; while(T--) { scanf("%d%d", &N, &M); init_set(); while(M--) { int A, B; scanf("%d%d", &A, &B); union_set(A, B); } ans = 0; for(int i=1; i<=N; i++) { if(pe[i] == i) ans++; } printf("%d\n", ans); } }
题意:你的学校有n名学生(0 < n <= 50000),你不太可能询问每个人的宗教信仰,因为他们不太愿意透露。但是当你同时找到2名学生,他们却愿意告诉你他们是否信仰同一宗教,你可以通过很多这样的询问估算学校里的宗教数目的上限。你可以认为每名学生只会信仰最多一种宗教。
#include<iostream> #include<cstdio> #include<cstdlib> using namespace std; int pe[50010]; int high[50010]; int N, M; int ans; void init_set() { for(int i=1; i<=N; i++) { pe[i] = i; high[i] = 0; } } int find_set(int x) { int root = x; while(root!= pe[root]) root = pe[root]; int i = x, temp; while(pe[i]!=i) { temp = pe[i]; pe[i] = root; i = temp; } return root; } void union_set(int A, int B) { A = find_set(A); B = find_set(B); if(high[A] == high[B]) { pe[B] = A; high[A]++; } if(high[A]>high[B]) pe[B] = A; else pe[A] = B; } int main() { int c=1; while(scanf("%d%d", &N, &M)) { if(N==0 && M==0) break; init_set(); while(M--) { int A, B; scanf("%d%d", &A, &B); union_set(A, B); } ans = 0; for(int i=1; i<=N; i++) { if(pe[i] == i) ans++; } printf("Case %d: %d\n", (c++), ans); } }
题意:有n个学生,m个社团。(0<n<=3000)(0<=m<=500),学生学号从0到n-1。已知0号学生已感染病毒。每个学生可能参加多个社团。
输入:第一行,两个整数n,m;(当n=m=0时,结束),接下来的m行输入一个数k(这个社团的人数),后跟学生的编号。
输出:对于每一个例子,输出一个数代表可能感染的总人数,占一行。
明显是有和零号有接触的人都有事,只要将有联系的人分入同一组即可。#include<iostream> #include<cstdio> #include<cstdlib> using namespace std; int pe[50010]; int N, M; int ans; void init_set() { for(int i=0; i<=N; i++) { pe[i] = i; } } int find_set(int x) { int root = x; while(root!= pe[root]) root = pe[root]; int i = x, temp; while(pe[i]!=i) { temp = pe[i]; pe[i] = root; i = temp; } return root; } void union_set(int A, int B) { A = find_set(A); B = find_set(B); if(B==0) pe[A] = B; else pe[B] = A; } int main() { while(scanf("%d%d", &N, &M)) { if(N==0 && M==0) break; init_set(); while(M--) { int k, first, another; scanf("%d%d", &k,&first); for(int i=1; i<k; i++) { scanf("%d", &another); union_set(first, another); } } ans = 0; int r = find_set(0); for(int i=0; i<=N; i++) { if(r == find_set(i)) ans++; } printf("%d\n", ans); } }
题意:第一行表示用例组数,第二行表示人数和信息组数有两个帮派,D a b表示a和b属于不同帮派,A a b表示要你回答a和b是属于同一帮派、不同帮派还是并不确定。
思路1:N个人分属于两个帮派。所以我们可以初始化一个N*2的并查集,然后给定D a b就unit(a,b+n),unit(b,a+n)。
思路2:给的是a,b的敌对关系,可以用数组enemy把a的敌人存起来,然后如果a,x是敌人,把s[a]和x用并查集合并,再更新s[a]=x;
#include<iostream> #include<cstdio> #include<string.h> using namespace std; #define MAX 100010 int high[MAX]; int s[MAX], enemy[MAX]; int t, n, m; char c; int a, b; void init_set() { for(int i=0; i<=n+1; i++) { // enemy[i] = 0; s[i] = i; high[i] = 0; } } int find_set(int x) { int root = x; while(root!= s[root]) root = s[root]; int i = x, temp; while(s[i]!=i) { temp = s[i]; s[i] = root; i = temp; } return root; } void union_set(int A, int B) { A = find_set(A); B = find_set(B); if(high[A] == high[B]) { s[B] = A; high[A]++; } if(high[A]>high[B]) s[B] = A; else s[A] = B; } int main() { scanf("%d", &t); while(t--) { scanf("%d%d", &n, &m); init_set(); memset(enemy, 0, sizeof(enemy)); while(m--) { getchar(); scanf("%c%d%d", &c, &a, &b); if(c=='D') { if(enemy[a]) { union_set(enemy[a], b); } if(enemy[b]) { union_set(enemy[b], a); } enemy[a] = b; enemy[b] = a; } else { if(find_set(a)==find_set(b)) { printf("In the same gang.\n"); } else if(find_set(a)==find_set(enemy[b])) { printf("In different gangs.\n"); } else { printf("Not sure yet.\n"); } } } } return 0; }
题意:给你n个电脑的位置,和信号覆盖的的半径d,一开始所有的电脑都坏了,你每单位时间可以进行 O(修复一台电脑),S(检查两台电脑是否联通,间接联通也算),只有修理好的计算机才能连通,如果两台计算机的距离不超过d,则两台电脑之间可以直接连接。
#include<iostream> #include<cstdio> #include<string.h> using namespace std; #define MAX 1010 typedef struct { int x, y; }computer; bool fcp[MAX]; computer cp[MAX]; int high[MAX]; int s[MAX]; int n, d, dd; int p, q; char c; void init_set() { for(int i=0; i<=n+1; i++) { s[i] = i; high[i] = 0; } } int find_set(int x) { int root = x; while(root!= s[root]) root = s[root]; int i = x, temp; while(s[i]!=i) { temp = s[i]; s[i] = root; i = temp; } return root; } void union_set(int A, int B) { A = find_set(A); B = find_set(B); if(high[A] == high[B]) { s[B] = A; high[A]++; } if(high[A]>high[B]) s[B] = A; else s[A] = B; } int main() { memset(fcp, false, sizeof(fcp)); scanf("%d%d", &n, &d); dd = d*d; init_set(); for(int i=1; i<=n; i++) { scanf("%d%d", &cp[i].x, &cp[i].y); } getchar(); while((scanf("%c", &c))!=EOF) { if(c=='S') { scanf("%d%d", &p, &q); if((find_set(p)==find_set(q))) printf("SUCCESS\n"); else printf("FAIL\n"); } else { bool flag=true; scanf("%d", &p); for(int i=1; i<=n; i++) { if(fcp[i] && i!=p) { int xx = (cp[p].x-cp[i].x)*(cp[p].x-cp[i].x); int yy = (cp[p].y-cp[i].y)*(cp[p].y-cp[i].y); if((xx+yy)<=dd) { union_set(i, p); } } } fcp[p]=true; } getchar(); } return 0; }
题意:t组数据,n个虫子,m组相互喜爱的关系,虫子分为雌雄两种,每个虫子只有一个性别,问是否存在同性恋的虫子。
与Find them, Catch them思路相似。#include<iostream> #include<cstdio> #include<string.h> using namespace std; #define MAX 2010 int high[MAX]; int s[MAX], enemy[MAX]; int t, n, m; bool flag; int a, b; void init_set() { for(int i=0; i<=n+1; i++) { s[i] = i; high[i] = 0; } } int find_set(int x) { int root = x; while(root!= s[root]) root = s[root]; int i = x, temp; while(s[i]!=i) { temp = s[i]; s[i] = root; i = temp; } return root; } void union_set(int A, int B) { A = find_set(A); B = find_set(B); if(high[A] == high[B]) { s[B] = A; high[A]++; } if(high[A]>high[B]) s[B] = A; else s[A] = B; } int main() { scanf("%d", &t); for(int i=1; i<=t; i++) { scanf("%d%d", &n, &m); init_set(); memset(enemy, 0, sizeof(enemy)); flag = true; while(m--) { scanf("%d%d", &a, &b); if(flag) { if(find_set(a)==find_set(b)) { flag = false; } else { if(enemy[a]) { union_set(enemy[a], b); } if(enemy[b]) { union_set(enemy[b], a); } enemy[a] = b; enemy[b] = a; } } } printf("Scenario #%d:\n", i); if(flag) printf("No suspicious bugs found!\n\n"); else printf("Suspicious bugs found!\n\n"); } return 0; }
题意:有n个从1到n编号的箱子,将每个箱子当做一个栈,对这些箱子进行p次操作,每次操作分别为以下两种之一:
1.输入 M x y:表示将编号为x的箱子所在的栈放在编号为y的箱子所在栈的栈顶.
2.输入 C x:计算编号为x的所表示的栈中在x号箱子下面的箱子数目.带权并查集。(待完善)
#include<iostream> #include<cstdio> #include<string.h> using namespace std; #define MAX 30010 int s[MAX], val[MAX], dis[MAX]; int t; char c; int a, b; void init_set() { for(int i=0; i<MAX; i++) { dis[i] = 0; s[i] = i; val[i] = 1; } } int find_set(int x) { if(x!=s[x]) { int temp = s[x]; s[x] = find_set(s[x]); dis[x] += dis[temp]; } return s[x]; } void union_set(int A, int B) { A = find_set(A); B = find_set(B); if(A!=B) { s[B] = A; dis[B] = val[A]; val[A] += val[B]; } } int main() { scanf("%d", &t); init_set(); while(t--) { getchar(); scanf("%c", &c); if(c=='M') { scanf("%d%d", &a, &b); union_set(a, b); } else { scanf("%d", &a); int x=find_set(a); printf("%d\n", val[x]-dis[a]-1); } } return 0; }
题意:动物王国中有三类动物A,B,C,这三类动物的食物链构成了有趣的环形。A吃B, B吃C,C吃A。现有N个动物,以1-N编号。每个动物都是A,B,C中的一种,但是我们并不知道它到底是哪一种。有人用两种说法对这N个动物所构成的食物链关系进行描述:
第一种说法是"1 X Y",表示X和Y是同类。
第二种说法是"2 X Y",表示X吃Y。
此人对N个动物,用上述两种说法,一句接一句地说出K句话,这K句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
1) 当前的话与前面的某些真的话冲突,就是假话;
2) 当前的话中X或Y比N大,就是假话;
3) 当前的话表示X吃X,就是假话。
你的任务是根据给定的N(1 <= N <= 50,000)和K句话(0 <= K <= 100,000),输出假话的总数。
与Find them, Catch them相似,用的思路1的思想,开三倍大的数组。参考#include<iostream> #include<cstdio> #include<string.h> using namespace std; #define MAX 50010 int s[3*MAX]; //x<-x+n,x+n<-x+2*n,x+2*n<-x int n, k, d; int x, y; int ans; void init_set() { for(int i=1; i<3*MAX; i++) { s[i] = i; } } int find_set(int x) { int root = x; while(root!= s[root]) root = s[root]; int i = x, temp; while(s[i]!=i) { temp = s[i]; s[i] = root; i = temp; } return root; } void union_set(int A, int B) { A = find_set(A); B = find_set(B); s[A] = B; } int main() { scanf("%d%d", &n, &k); init_set(); while(k--) { scanf("%d%d%d", &d, &x, &y); if(x>n || y>n) { ans++; continue ; } if(d==1) { if(find_set(x+2*n)==find_set(y) || find_set(x)==find_set(y+2*n)) ans++; else { union_set(x, y); union_set(x+n, y+n); union_set(x+2*n, y+2*n); } } else { if(x==y || find_set(x)==find_set(y+2*n) || find_set(x)==find_set(y)) ans++; else { union_set(x+2*n, y); union_set(x+n, y+2*n); union_set(x, y+n); } } } printf("%d", ans); return 0; }
题意:先说每个城市和龙珠都有编号,对应的第i个龙珠放在第i个城市。T A B表示把A号龙珠所在的城市的所有龙珠全部搬运到B号龙珠所在的城市。
Q A表示要求出X(第A号龙珠所在的城市编号),Y(第X号城市存放的龙珠个数),Z(第A号龙珠被搬运的次数)。带权并查集。但是可以知道,龙珠每搬运一次,在并查集中的深度就会加一(未优化),所以在不优化并查集的前提下,龙珠的深度就是搬运次数。
#include<iostream> #include<cstdio> #include<string.h> using namespace std; #define MAX 10010 //int trans[MAX]; int s[MAX], sum[MAX]; int ans[10010][3]; int t; int n, q; int a, b; int deep; void init_set() { for(int i=1; i<=n; i++) { s[i] = i; sum[i] = 1; // trans[i] = 0; } } int find_set(int x) { int root = x; while(root!= s[root]) { root = s[root]; deep++; } return root; } void union_set(int A, int B) { A = find_set(A); B = find_set(B); if(A!=B) { s[A] = B; sum[B] += sum[A]; sum[A] = 0; } } int main() { scanf("%d", &t); for(int i=1; i<=t; i++) { printf("Case %d:\n", i); scanf("%d%d", &n, &q); init_set(); while(q--) { char c; getchar(); scanf("%c", &c); if(c=='T') { scanf("%d%d", &a, &b); union_set(a, b); } else { scanf("%d", &a); deep = 0; b = find_set(a); printf("%d %d %d\n", b, sum[b], deep); } } } return 0; }
题意:要找一些人完成一项工程。要求最后挑选出的人之间都是朋友关系,可以说直接的,也可以是间接地。问最多可以挑选出几个人(最少挑一个)。
在基础的并查集上加个数组记录集合的数量。#include<iostream> #include<cstdio> using namespace std; #define MAX 10000005 int sum[MAX]; int s[MAX]; int ans; int n; int a, b; void init_set() { for(int i=1; i<MAX; i++) { s[i] = i; sum[i] = 1; } } int find_set(int x) { int root = x; while(root!= s[root]) root = s[root]; int i = x, temp; while(s[i]!=i) { temp = s[i]; s[i] = root; i = temp; } return root; } void union_set(int A, int B) { A = find_set(A); B = find_set(B); if(A!=B) { s[B] = A; sum[A] += sum[B]; if(sum[A]>ans) ans = sum[A]; } } int main() { while((scanf("%d", &n))!=EOF) { init_set(); if(n==0) { printf("1\n"); continue; } ans=0; while(n--) { scanf("%d%d", &a, &b); union_set(a, b); } printf("%d\n", ans); } return 0; }
思路:每输入一组数据,都要对其进行连接,如果两个点find(a)==find(b),那么说明他们已经是一个集合的了,如果再连接a,b两个点,就会构成回路,这里也就是要输出no.
坑点:给出的图不一定是连通图,可能有多个连通分量。#include<iostream> #include<cstdio> #include<cstdlib> #include<string.h> using namespace std; #define MAX 100010 bool vis[MAX]; int s[MAX]; int a, b; bool flag; char c; void init() { for(int i=0; i<MAX; i++) { s[i] = i; vis[i] = false; } } int find_set(int x) { int root=x; while(root!=s[root]) root = s[root]; int i = x, temp; while(s[i]!=i) { temp = s[i]; s[i] = root; i = temp; } return root; } void union_set(int a, int b) { a = find_set(a); b = find_set(b); if(a==b) flag = false; else s[a] = b; vis[a] = true; vis[b] = true; } int main() { while(~scanf("%d%d", &a, &b)) { flag = true; if(a==-1 && b==-1) break; if(a==0 && b==0) { printf("Yes\n"); continue; } if(a==0 || b==0) flag = false; init(); union_set(a, b); while(scanf("%d%d", &a, &b)&&a&&b) { if(!flag) continue; else union_set(a, b); } int r=0; if(flag) { for(int i=1; i<MAX; i++) { if(vis[i] && find_set(i)==i) { r++; } } } if(flag && r==1) printf("Yes\n"); else printf("No\n"); } return 0; }
题意:根据给出的数据判断是否能构成一棵树。特判0 0 是一棵空树。
#include<iostream> #include<cstdio> #include<cstdlib> #include<string.h> using namespace std; #define MAX 1000000 bool vis[MAX]; int mx; int s[MAX]; int a, b; bool flag; char c; void init() { flag = true; for(int i=0; i<MAX; i++) { s[i] = i; vis[i] = false; } } int find_set(int x) { int root=x; while(root!=s[root]) root = s[root]; int i = x, temp; while(s[i]!=i) { temp = s[i]; s[i] = root; i = temp; } return root; } void union_set(int a, int b) { vis[a] = true; vis[b] = true; int aa = find_set(a); int bb = find_set(b); if(aa==bb || bb!=b) flag=false; else s[bb] = aa; } int main() { for(int i=1; ; i++) { scanf("%d%d", &a, &b); if(a<0 && b<0) break; if(a==0 && b==0) { printf("Case %d is a tree.\n", i); continue; } init(); union_set(a, b); while(scanf("%d%d", &a, &b)&&a&&b) { if(!flag) continue; else union_set(a, b); } int root = 0; if(flag) { for(int i=0; i<MAX; i++) { if(vis[i] && find_set(i)==i) { root++; } } } if(flag && root==1) printf("Case %d is a tree.\n", i); else printf("Case %d is not a tree.\n", i); } return 0; }
题意:给你不同形状 的水管,看哪些水管聚类连通,分成 N 堆答案就是 N。 更容易想到DFS
#include<iostream> #include<cstdlib> #include<cstdio> using namespace std; #define MAX 55 typedef struct { int u, d, l, r; }square; int m ,n; char c; int ans; int farm[MAX][MAX]; square squares[11]={{1,0,1,0}, {1,0,0,1}, {0,1,1,0}, {0,1,0,1}, {1,1,0,0}, {0,0,1,1}, {1,0,1,1}, {1,1,1,0}, {0,1,1,1}, {1,1,0,1}, {1,1,1,1}}; int dir[4][2]={{-1, 0}, {0, -1}, {1, 0}, {0, 1}}; //上左下右 bool check(int i, int j) { if(i<0 || i>=m || j<0 || j>=n) return false; else return true; } void DFS(int i, int j) { int ii, jj, x, p; p = farm[i][j]; farm[i][j] = -1; ii = i+dir[0][0]; jj = j+dir[0][1]; if(check(ii, jj) && farm[ii][jj]!=-1) { x = squares[p].u + squares[farm[ii][jj]].d; if(x == 2) DFS(ii, jj); } ii = i+dir[1][0]; jj = j+dir[1][1]; if(check(ii, jj) && farm[ii][jj]!=-1) { x = squares[p].l + squares[farm[ii][jj]].r; if(x == 2) DFS(ii, jj); } ii = i+dir[2][0]; jj = j+dir[2][1]; if(check(ii, jj) && farm[ii][jj]!=-1) { x = squares[p].d + squares[farm[ii][jj]].u; if(x == 2) DFS(ii, jj); } ii = i+dir[3][0]; jj = j+dir[3][1]; if(check(ii, jj) && farm[ii][jj]!=-1) { x = squares[p].r + squares[farm[ii][jj]].l; if(x == 2) DFS(ii, jj); } } int main() { while(scanf("%d%d", &m, &n)) { if(m==-1 && n==-1) break; if(m==0 && n==0) { printf("0\n"); continue; } ans = 0; for(int i=0; i<m; i++) { string s; cin>> s; for(int j=0; j<n; j++) farm[i][j] = s[j]-'A'; } for(int i=0; i<m; i++) { for(int j=0; j<n; j++) { if(farm[i][j]!=-1) { ans++; DFS(i, j); } } } printf("%d\n", ans); } }
并查集
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.