实用算法实现-第12篇 不相交集合(并查集)

12.1 不加按秩合并启发式的并查集

并查集可以进行两方面的启发式,一种是按秩合并,也就是使得包含较少结点的树的根指向包含较多结点的树的根。另一种是路径压缩,也就是使得查找路径上的每个结点都直接指向根结点。

但是有些问题只有使用不加按秩合并启发式的并查集才能够解决,因为按秩合并意味着不能够手动控制集合的合并方向。

12.1.1实例

PKU JudgeOnline, 1182, 食物链.

12.1.2问题描述

动物王国中有三类动物A,B,C,这三类动物的食物链构成了有趣的环形。A吃B, B吃C,C吃A。

现有N个动物,以1-N编号。每个动物都是A,B,C中的一种,但是我们并不知道它到底是哪一种。

有人用两种说法对这N个动物所构成的食物链关系进行描述:

第一种说法是"1X Y",表示X和Y是同类。

第二种说法是"2X Y",表示X吃Y。

此人对N个动物,用上述两种说法,一句接一句地说出K句话,这K句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。

1)当前的话与前面的某些真的话冲突,就是假话。

2)当前的话中X或Y比N大,就是假话。

3)当前的话表示X吃X,就是假话。

你的任务是根据给定的N(1 <= N <= 50,000)和K句话(0 <= K <= 100,000),输出假话的总数。

输入:

第一行是两个整数N和K,以一个空格分隔。

以下K行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中

表示说法的种类。

若D=1,则表示X和Y是同类。

若D=2,则表示X吃Y。

输出:

只有一个整数,表示假话的数目。

12.1.3输入

1007

1101 1

21 2

22 3

23 3

11 3

23 1

15 5

12.1.4输出

3

12.1.5分析

只需要一个并查集就够了,同时对每个结点保持其到根结点的相对类别偏移量,定义为:

0——同类。

1——食物。

2——天敌。

用向量的思考模式想整个过程相当简单。实用向量的方式思考问题的意思就是:将X和Y之间的关系用向量(X,Y)表示。设X所在集合的代表为A,Y所在集合的代表为B。

已知(A,X)、(B,Y)。在合并集合时,需要将A加入到B集合去,B作为新集合的代表,并更新A对代表B的相对类别偏移量。

可知:

(B,A)=(B,Y)-(A,X)+(X,Y)

因为:

(B,A)= A-B;

(B,Y)-(A,X)+(X,Y)= Y- B-(X-A)+(Y-X)=A-B

由上式就知道了合并集合时相对类别偏移量的变化情况。其它的相对类别偏移量的变化也可以类似地使用向量的思想算出来。

12.1.6程序

#include #include #include using namespace std; #define maxNum 50002 int ch[maxNum]; int p[maxNum]; int find(int k) { if (p[k]==k) returnk; int t=find(p[k]); ch[k]=(ch[k]+ch[p[k]])%3; //这个向量关系需要发现 p[k]=t; return t; } int lyn; int n; void check(int x,int y,int d) { int tp=find(x),tq=find(y); if(x>n||y>n) { ++lyn; return; } if (tp==tq) { if((ch[x]-ch[y]+3)%3!=d)//这个关系根据上面的可以自己用向量推 { ++lyn; } return; } p[tp]=tq; ch[tp]=(ch[y]-ch[x]+d+6)%3;//这个同理 } int main() { int K; int i, j; int x, y,d; scanf("%d%d",&n, &K); for(i = 1;i <= n; i++){ p[i] = i; } lyn = 0; for(i = 0;i < K; i++){ scanf("%d%d%d",&d, &x, &y); check(x, y, d-1); } cout << lyn << endl; return 1; }
12.2 启发式的不相交集合森林

12.2.1实例

PKU JudgeOnline, 2524, UbiquitousReligions.

12.2.2问题描述

输入n个人中m对人具有相同的宗教信仰,通过这个判定这n个人最多有多少中宗教信仰。先输入n、m,然后输入m行具有相同宗教信仰的一对。n=m=0标志着输入结束。

12.2.3分析

这是基本的并查集的用法。

PKU JudgeOnline, 1703, Find them, Catchthem则是反过来求两个元素是不是处于两个互斥且完全(两个集合的交集为全集)的集合之中。

12.2.4输入

109

12

13

14

15

16

17

18

19

110

104

23

45

48

58

00

12.2.5输出

Case1: 1

Case2: 7

12.2.6程序

#include #include #include using namespace std; #define maxNum 50002 int p[maxNum]; int rank[maxNum]; void makeSet(int x) { p[x] = x; rank[x] = 0; } int findSet(int x) { if(x !=p[x]) { p[x] = findSet(p[x]); } returnp[x]; } void link(int x, int y) { if(rank[x]> rank[y]){ p[y] = x; }else{ p[x] = y; if(rank[x]== rank[y]) { rank[y] = rank[y] + 1; } } } void unionSet(int x, int y) { link(findSet(x), findSet(y)); } int main() { int cases; int n, m; int i; int from,to; int set; int num; intchecked[maxNum]; cases = 0; while(scanf("%d%d", &n,&m)){ cases ++; if(n ==0 && m == 0) break; memset(p, 0, sizeof(p)); memset(rank, 0, sizeof(rank)); for(i =1; i <= n; i++){ makeSet(i); } for(i =0; i < m; i++){ scanf("%d%d",&from, &to); if(findSet(from)!= findSet(to)) unionSet(from, to); } memset(checked, 0, sizeof(checked)); num = 0; for(i =1; i <= n; i++) { set = findSet(i); if(checked[set]== 0) { checked[set] = 1; num++; } } cout << "Case" << cases << ": "<< num << endl; } return 1; }

12.3 Tarjan的脱机最小公共祖先算法

《算法导论》介绍并查集的章节的思考题给出了Tarjan的脱机最小公共祖先算法如下:

LCA(u)

1 MAKE-SET(u)

2 ancestor[FIND-SET(u)] ← u

3 for each child v of u inT

4 doLCA(v)

5 UNION(u,v)

6 ancestor[FIND-SET(u)] u

7 color[u]BLACK

8 for each node v such that {u,v} ∈ P

9 do if color[v] = BLACK

10 thenprint "The least common ancestor of"

u"and" v "is"ancestor [FIND-SET(v)]

可知有以下结论:

1. 对每一对{u, v}∈P,第10行恰执行一次

2. 在调用LCA(u)时,不相交集合数据结构中的集合数等于u在树T中的深度。

3. 对一对{u, v}∈P,LCA能正确地输出u和v的最小公共祖先。

证明:

1. 首先不难知LCA实际是对树T的一个DFS遍历,DFS遍历到{u,v}∈P的u或者v时,会执行第9行。由于u或者v必有一个先被遍历,不妨设为v。那么在遍历v时color[u]为WHITE,故此第10行不执行。而在遍历u时color[v]为BLACK,故此第10行执行。故此第10行恰被执行一次。命题1.得证。

2. 由于LCA实际是对树T的一个DFS遍历,且在遍历一结点开始时就执行MAKE-SET操作,而在遍历结束返回父结点时又会将集合与父结点的集合合并,故此最后的集合如下所示。不难得知:不相交集合数据结构中的集合数等于u在树T中的深度。命题2.得证。


3.分析上图,可以发现每个集合都包含着已经遍历的子树和本结点,不包含没有完全遍历的子树。也可以发现,从根结点一直遍历到当前结点u,u结点的每个祖先都处于不同的集合当中,而已经遍历过的结点v必定存在于其中一个集合中。由于集合的结构,可知v必定处于最小公共祖先集合内。命题3.得证。

证毕。

12.3.1实例

PKU JudgeOnline, 1470, Closest CommonAncestors.

12.3.2问题描述

给定一棵多叉树,给定一些无序对,求它们最小公共祖先。输出最小公共祖先结点,同时输出这些结点是多少无序对的公共祖先。

12.3.3输入

5

5:(3)1 4 2

1:(0)

4:(0)

2:(1)3

3:(0)

6

(15) (1 4) (4 2)

(2 3)

(1 3) (4 3)

12.3.4输出

2:1

5:5

12.3.5分析

这个题目的输入格式比较乱,需要仔细处理。

PKU JudgeOnline, 1330, Nearest CommonAncestors是同样的思想,不过该题的一个结点的儿子结点可能比较多,不适合用临接表来表示树,否则会超出内存。

12.3.6程序

#include #include #include using namespace std; #define maxNum 1002 int p[maxNum]; int rank[maxNum]; void makeSet(int x) { p[x] = x; rank[x] = 0; } int findSet(int x) { if(x !=p[x]) { p[x] = findSet(p[x]); } returnp[x]; } void link(int x, int y) { if(rank[x]> rank[y]){ p[y] = x; }else{ p[x] = y; if(rank[x]== rank[y]) { rank[y] = rank[y] + 1; } } } void unionSet(int x, int y) { link(findSet(x), findSet(y)); } int tree[maxNum][maxNum]; int pairNum; int Pair[maxNum][maxNum]; int ancestor[maxNum]; int visited[maxNum]; int commonNum[maxNum]; void LCA(int u) { int i; int v; makeSet(u); ancestor[findSet(u)] = u; for(i = 1;i <= tree[u][0]; i++){ v = tree[u][i]; LCA(v); unionSet(u, v); ancestor[findSet(u)] = u; } visited[u] = 1; for(i = 1;i <= Pair[u][0]; i++) { v = Pair[u][i]; if(visited[v]== 1) { //cout<< "LCA of " << u << " and " << v<< " is: " << ancestor[findSet(v)]< 12.4 实例

12.4.1不加按秩合并启发式的并查集实例

PKU JudgeOnline, 1182, 食物链.

12.4.2启发式的不相交集合森林实例

PKU JudgeOnline, 2524, UbiquitousReligions.

PKU JudgeOnline, 1703, Find them, Catchthem.

PKU JudgeOnline, 2236, Wireless Network.

PKU JudgeOnline, 1161, The Suspects.

PKU JudgeOnline, 1988, Cube Stacking.

12.4.3Tarjan的脱机最小公共祖先算法实例

PKU JudgeOnline, 1470, Closest CommonAncestors.

PKU JudgeOnline, 1330, Nearest CommonAncestors.

本文章欢迎转载,请保留原始博客链接http://blog.csdn.net/fsdev/article

你可能感兴趣的:(实用算法实现-第12篇 不相交集合(并查集))