文章首发于:My Blog 欢迎大佬们前来逛逛
基础并查集 并查集就是用来维护一些元素之间的关系的集合。
例如 A的亲戚是B ,则我们可以把 A与B放到同一个集合中,表示AB属于同一种关系 (亲戚关系),并且标记A和B的关系数组都为B
B的亲戚是C,则我们可以把 B和C也放到同一个集合中,则BC也属于同一个集合 ,并且标记B和C的关系数组都为C
再往后我们需要知道A和C是否是 同一个关系 (即是否具有传递性 ),我们可以通过一直查找A和C的关系数组直到没有关系为止 ,看看他们是否是相同的元素值,即可知道他们是否属于同一个关系 :
A的关系数组的值是 B,而B的关系数组的值是C,所以A的关系为C
C的关系数组的值就是C,所以A和C属于同一关系
并查集的初始化
每个元素的关系数组的关系只有其一个人:A默认为A,B默认为B
1 2 3 4 5 void init () { for (int i=1 ;i<n;i++){ parent[i]=i; } }
并查集的查找 :(路径压缩)
每个元素在确定关系的时候都需要一直往上寻找,直到到达它的最顶级关系 :A的关系不是B,而是往上B的关系,直到到达C,C的关系为其本身,这就到到达了A的最顶级关系,就是C
1 2 3 4 5 6 int find_set () { if (x!=parent[x]){ parent[x]=find_set (parent[x]); } return parend[x]; }
并查集的合并
合并是并查集最重要的功能,就如同上述的A和B或者B和C的确认关系的时候,把他们合并到同一个关系中 ,A的关系是B,B的关系为其本身
1 2 3 4 5 6 7 void merge_set (int x,int y) { x=find_set (x); y=find_set (y); if (x!=y){ parent[x]=y; } }
基础并查集便可以解决一系列的存在关系 的问题
1. P1551 亲戚 P1551 亲戚
这道题让我们判断两个人之间是否是亲戚 ,和我们上面的描述的基本是一致的,只需要判断他们两个是否属于同一个集合即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 int n,m,p;const int N=1e5 +10 ;int parent[N];void init_set () { for (int i=1 ;i<=n;i++){ parent[i]=i; } } int find_set (int x) { if (x!=parent[x]){ parent[x]=find_set (parent[x]); } return parent[x]; } void merge_set (int x,int y) { x=find_set (x); y=find_set (y); if (x!=y){ parent[y]=x; } } signed main () { cin>>n>>m>>p; init_set (); for (int i=1 ;i<=m;i++){ int a,b; cin>>a>>b; merge_set (a,b); } while (p--){ int a,b; cin>>a>>b; if (find_set (a)==find_set (b)){ cout<<"Yes\n" ; }else { cout<<"No\n" ; } } #define one 1 return 0 ; }
2. P1536 村村通 P1536 村村通
这道题其实就是求有多少个集合的问题 。
如果一共有四个城市,有两条道路,分别为 1到3 和 3到4,则我们画图可以看出来 1 3 4 都属于同一个集合 ,2城市单独属于一个集合,总共有两个集合,因此如果要将两个集合合并,则需要添加的道路数量就是总集合数-1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 int n,m;const int N=1e5 +10 ;int parent[N];int find_set (int x) { if (x!=parent[x]){ parent[x]=find_set (parent[x]); } return parent[x]; } void merge_set (int x,int y) { x=find_set (x); y=find_set (y); if (x!=y){ parent[y]=x; } } void display () { for (int i=1 ;i<=n;i++){ printf ("i=%d,parent[%d]=%d\n" ,i,i,parent[i]); } cout<<endl; } signed main () { while (cin>>n>>m){ if (n==0 ){ break ; } for (int i=1 ;i<=n;i++){ parent[i]=i; } while (m--){ int a,b; cin>>a>>b; merge_set (a,b); } int ans=0 ; for (int i=1 ;i<=n;i++){ if (find_set (i)==i) ans++; } cout<<ans-1 <<endl; } #define one 1 return 0 ; }
种类并查集 种类并查集属于扩展域并查集一类。
不同的个体之间存在不同的关系 ,而每个相同个体 之间都属于同一关系 ,不同的关系的个体的种类不同 ,因此就有了种类并查集。
1. P1892 [BOI2003]团伙 P1892 [BOI2003]团伙
定义:
一个人的朋友的朋友是朋友
一个人的敌人的敌人是朋友
(很挺符合常理)
因此我们便可以确定两个并查集的种类 :朋友和敌人。
不妨设每个人都有一个 enemies数组,表示他的敌人 ,则它的 enemies数组存储的可能是它的另一个敌人的朋友 ,即它的两个敌人互为朋友。
我们便可以确定出这样的关系:
如果A没有敌人:则A的敌人包括B和B的朋友们
如果A有敌人,则A的敌人是B的朋友
对于B同理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 if (enemies[a]==0 ){ enemies[a]=find_set (b); } else { merge_set (enemies[a],b); } if (enemies[b]==0 ){ enemies[b]=find_set (a); } else { merge_set (enemies[b],a); }
也可以使用这种并查集的做法 :
表达敌人与朋友两种关系 ,不妨将并查集的数组开大两倍 ,作为并查集的扩展域,于是可以表达朋友以及敌人两部分,朋友对应第一倍,敌人对应第二倍 。于是我们可以得到如下合并公式
A和B是敌人的情况:
A的敌人:A+n 是B 的朋友
B的敌人: B+n是A 的朋友
A和B是朋友: A和B
1 2 3 4 5 6 7 8 if (ch=='E' ){ merge_set (a+n,b); merge_set (b+n,a); } else if (ch=='F' ){ merge_set (a,b); }
2. P1525 [NOIP2010 提高组] 关押罪犯 P1525 [NOIP2010 提高组] 关押罪犯
如题所示,我们需要把多个罪犯分成不同的种类,而每对 罪犯之间又存在着不同的仇恨值 。
我们需要把这些罪犯分到两个种类区域 中,使得合并后每两个罪犯之间的最大仇恨值为一个理论最小值 。
我们显然需要对每对罪犯仇恨值按照从大到小 的顺序排序。
如果A和B 是敌人 ,则A和B要尽量分在两个不同的区域中,那么A不能和B在一起,要和谁在一起呢?
我们可以让A和(B的其他敌人)在一起 ,因为按照贪心 的原理来看,如果A和B有仇,则A和B的仇人不一定有仇 ,相反B的仇人可能和A是朋友,因为他们都和B有仇,当然这句话是我的补充,题中并没有说他们是朋友,只是为了便于理解 。
因此寻找B仇人们,让他们和A是一伙的;同理寻找A的仇人们,让他们和B是一伙的。因为是按照仇恨值从大到小排序的,因此A和B的仇人们一伙,如果存在敌对关系的话,仇恨值一定是比B要小的 (A和B仇恨值大,先判断,越往后肯定仇恨值越小);同理对于B和A的仇人们也是一样的。
一直把A和B的仇人们合并,B和A的仇人们合并。
在对A合并,寻找B的仇人的时候,直到B的仇人们的仇人的仇人…. 又回到了A,则A和B不可避免的又碰到了一起 ,即这个时候已经无法在分了,A和B不管找谁的仇人他们都是有仇的,则此时我们直接输出这个组合,此时这个组合关系的仇恨值一定是一个理论最小值,因为是按照从大到小排序的,既然不存在其他组合了,则这种一定是最小的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 int n,m;const int N=2e5 +10 ;int parent[N],enemies[N];struct Node { int a,b,c; }node[N]; bool comp (Node a,Node b) { return a.c>b.c; } int find_set (int x) { if (x!=parent[x]){ parent[x]=find_set (parent[x]); } return parent[x]; } void merge_set (int x,int y) { x=find_set (x); y=find_set (y); if (x!=y){ parent[x]=y; } } bool check (int x,int y) { x=find_set (x); y=find_set (y); if (x==y){ return true ; } return false ; } void init () { for (int i=1 ;i<=n;i++){ parent[i]=i; } } void display () { for (int i=1 ;i<=n;i++){ printf ("i=%d,parent[%d]=%d\n" ,i,i,parent[i]); } cout<<endl; } signed main () { cin>>n>>m; init (); for (int i=1 ;i<=m;i++){ cin>>node[i].a>>node[i].b>>node[i].c; } sort (node+1 ,node+1 +m,comp); for (int i=1 ;i<=m+1 ;i++){ int a=node[i].a,b=node[i].b; if (check (a,b)){ cout<<node[i].c<<endl; break ; } if (enemies[a]==0 ){ enemies[a]=b; } else { merge_set (enemies[a],b); } if (enemies[b]==0 ){ enemies[b]=a; } else { merge_set (enemies[b],a); } } #define one 1 return 0 ; }
3. P2024 [NOI2001] 食物链 P2024 [NOI2001] 食物链 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
我们在这个题中能得到三种关系:
属于同一类
A吃B 的关系
A被B吃 的关系
那么我们就可以创建三个种类 ,即三个集合,来分别合并他们之间的关系。
如果他们两个是同类的关系:输入1 u v
u与v是同类(第一群系A)
u+n和v+n是同类(第二群系B)
u+n+n和v+n+n是同类(第三群系C)
1 2 3 merge_set (u,v)merge_set (u+n,v+n)merge_set (u+n+n,v+n+n)
如果它们两个是吃与被吃的关系: 输入 2 u v
由于他们三个群系属于一个环形生物链 ,并且同一群系中生物不能互吃
因此只能是 A群系吃B群系 , B群系吃C群系, C群系吃A群系
1 2 3 merge_set (u,v+n+n); merge_set (u+n,v); merge_set (u+n+n,v+n);
我们如何判断话的真假?
如果输入2 u v,则uv一定不属于同一群系,因为uv存在u吃v的关系。
如果它是个假的 :
只需要判断uv属于同一群系,则False
或者判断 v吃u ,则不满足 u吃v,则False
则这两种情况如下:
分别表示 uv 是同一群系 或者 u被v吃
1 if (find_set (u)==find_set (v) || find_set (u)==find_set (v+n)) ans++;
如果它是个假的:
只需要判断 存在 u吃v,则False;
或者判断 存在 v吃u,则False
则这两种情况如下:
1 if (find_set (v+n)==find_set (u) || find_set (u)==find_set (v+n)) ans++
完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 int n,m;const int N=1e5 +10 ;int parent[N*3 ];void init () { for (int i=1 ;i<=3 *n;i++){ parent[i]=i; } } int find_set (int x) { if (x!=parent[x]){ parent[x]=find_set (parent[x]); } return parent[x]; } void merge_set (int x,int y) { x=find_set (x); y=find_set (y); if (x!=y){ parent[x]=y; } } signed main () { int ans=0 ; cin>>n>>m; init (); for (int i=1 ;i<=m;i++){ int opt,a,b; cin>>opt>>a>>b; if (a>n || b>n){ ans++; continue ; } if (opt==1 ){ if (find_set (a)==find_set (b+n) || find_set (b)==find_set (a+n)){ ans++; } else { merge_set (a,b); merge_set (a+n,b+n); merge_set (a+2 *n,b+2 *n); } } else if (opt==2 ){ if (find_set (a)==find_set (b) || find_set (a)==find_set (b+n)){ ans++; } else { merge_set (a,b+n+n); merge_set (a+n,b); merge_set (a+n+n,b+n); } } } cout<<ans; #define one 1 return 0 ; }
带权并查集 就是在维护集合关系的树中添加边权的并查集,这样做可以维护更多的信息。
不难发现所谓的并查集本质上也是一种树,由于路径压缩的存在,使得一个并查集树中,其被压缩过的子节点总是直接指向根节点,于是我们可以给根节点与子节点之间的边进行赋权。
这个权值可以表示该节点与根节点之间的距离
待更新。。。。