数据结构之并查集小结

数据结构---并查集小结

                By-Missa

    并查集是一种树型的数据结构,用于处理一些不相交集合Disjoint Sets)的合并及查询问题。 (百度百科)

大体分为三个:普通的并查集,带种类的并查集,扩展的并查集(主要是必须指定合并时的父子关系,或者统计一些数据,比如此集合内的元素数目。) 

View Code
 1 #define MAXN 100005
 2 int n,m,k,fa[MAXN];
 3 int rank[MAXN];
 4 void init(int n)//初始化
 5 {
 6     for(int i=0;i<=n;i++)
 7     {
 8         fa[i]=i;
 9         rank[i]=0;
10     }
11 }
12 //查找的时候,进行路径压缩fa[x]=find(fa[x])
13 //把查找路径上的结点都指向根结点,减少树的高度。
14 int find(int x)
15 {
16     if(x != fa[x])
17         fa[x]=find(fa[x]);//路径压缩
18     return fa[x];
19 }
20 //合并
21 void unio(int x,int y)
22 {
23     int fx=find(x),fy=find(y);
24     if(fx==fy) return ;
25     if(rank[fy]<rank[fx])//将rank值小的合并到大的中
26         fa[fy]=fx;
27     else
28     {
29         fa[fx]=fy;
30         if(rank[fx]==rank[fy])
31             rank[fy]++;
32     }
33 }
34 //或(忽略按秩合并,懒的时候经常这么敲.....时间上也不知道会差多少,没有试过。。):
35 void unio(int x,int y)
36 {
37     int fx=find(x),fy=find(y);
38     if(fx==fy) return ;
39     fa[fy]=fx;
40 }

一.普通并查集:

Poj 1611 ,2524,2236.都是裸的并查集。

 

简单并查集的一个应用:kruskal需要并查集判断点是否在同一个集合里。

Poj1287

模版最小生成树:

View Code
 1 #include <iostream>
 2 #include <cstdio>
 3 #include <cstring>
 4 #include <algorithm>
 5 using namespace std;
 6 #define MAXN 55
 7 #define MAXM 10000
 8 int fa[MAXN];
 9 int n,m,e,ans;
10 struct Edge
11 {
12     int u;
13     int v;
14     int c;
15 }p[MAXM];
16 void addEdge(int u,int v,int c)
17 {
18     p[e].v=v;p[e].c=c;p[e].u=u;
19     e++;
20 }
21 void init()
22 {
23     for(int i=0;i<=n;i++)
24         fa[i]=i;
25 }
26 int find(int x)//查找点所在的集合
27 {
28     if(fa[x]!=x)
29         fa[x]=find(fa[x]);
30     return fa[x];
31 }
32 int cmp(const Edge &a,const Edge & b)
33 {
34     return a.c<b.c;
35 }
36 bool kru(int n,int m)
37 {
38     int i,j;
39     sort(p,p+m,cmp);
40     ans=0;
41     init();
42     int cnt=0;
43     for(i=0;i<m;i++)
44     {
45     //使用并查集的地方,在每次加入边之前先判断下点是否已经在同    //一个集合了
46         int uu=find(p[i].u);
47         int vv=find(p[i].v);
48         if(uu==vv)
49             continue;
50         fa[uu]=vv;
51         ans+=p[i].c;
52         cnt++;
53     }
54     if(cnt != n-1)
55         return false;
56     else
57         return true;
58 }
59 int main()
60 {
61     while(scanf("%d",&n))
62     {
63         e=0;
64         if(!n)
65             break;
66         scanf("%d",&m);
67         for(int i=0;i<m;i++)
68         {
69             int a,b,c;
70             scanf("%d%d%d",&a,&b,&c);
71             addEdge(a,b,c);
72         }
73         kru(n,m);
74         printf("%d\n",ans);
75     }
76     return 0;
77 }

二.种类并查集:

 

最经典的就是 POJ 1182 食物链

 

题目告诉有3种动物,互相吃与被吃,现在告诉你m句话,其中有真有假,叫你判断假的个数(如果前面没有与当前话冲突的,即认为其为真话)

 

在做这题之前就知道是很经典的并查集了,还是不会做。。。,看了网上很多份解题报告,花了很长的时间来理解这题,下面这份报告的思路http://cavenkaka.iteye.com/blog/1489588 讲的很不错。下面是我根据从网上的解题报告中整理总结的:

 

思路:

 fa[x]表示x的根结点。relat[x]表示fa[x]x的关系。relat[x] == 0 表示fa[x]x同类;1表示fa[x]x2表示xfa[x]relat[]可以抽象成元素i到它的父亲节点的逻辑距离,见下面。}

    怎样判断一句话是不是假话?
       假设已读入 D , X , Y , 先利用find()函数得到X , Y 所在集合的代表元素 fx,fy ,若它们在同一集合(即 fx== fy )则可以判断这句话的真伪:
       1.若 D == 1 (XY同类)而 relat[X] != relat[Y] 则此话为假。D == 1 表示XY为同类,而从relat[X] != relat[Y]可以推出 与 不同类。比如relat[x]=0  fxx同类,relat[y]=1 fyy,fx==fy,故矛盾。)
       2.若 D == 2 XY)而 relat[X] == relat[Y] Y为同类,故矛盾。)或者 relat[X] == ( relat[Y] + 1 ) % 3 Y)则此话为假。

上个问题中 r[X] == ( r[Y] + 1 ) % 3这个式子怎样推来?

我们来列举一下:   假设有YX注意fx==fy的前提条件),那么r[X]r[Y]的值是怎样的?
                            r[X] = 0 && r[Y] = 2 Xfx同类,Yfy,YX) 
                            r[X] = 1 && r[Y] = 0  (Xfx吃,Yfy同类,即YX)
                            r[X] = 2 && r[Y] = 1  (Xfx,Yfy吃,一个环,YX)
通过观察得到r[X] = ( r[Y] + 1 ) % 3;

对于上个问题有更一般的判断方法(来自poj 1182中的Discuss ):
   若 ( r[x] - r[y] + 3 ) % 3 != d - 1(d-1 值是1或者0.....) ,则此话为假。

当判断两个元素的关系时,若它们不在同一个集合当中,则它们还没有任何关系,直接将它们按照给出的关系合并就可以了。若它们在同一个集合中,那么它们的关系就是xy的距离:如图所示为(r[x]+3-r[y])%3,即xy的已有的关系表达,判断和给出的关系是否一致就可以知道是真话还是假话了。 

 

注意事项:

A、find()函数里面的那句relat[x]=(relat[x] + relat[t])%3解释:

我们x--r-->y表示xy之间的关系是r,比如x--1--y代表xy。现在,若已知x--r1-->yy--r2-->z,如何求x--?-->z

即如何在路径压缩的时候更新x与当前父亲的relat值?

X--r[x]--t(t就是还未压缩的父亲)t---r[t]---root(压缩后的父亲)。

x---r[x]+r[t]--->root;举例:

r[x]=0;r[t]=1;xroot的关系是xroot吃。。。。其他类似。

用逻辑距离理解如下(向量思想):

 

 

B、当D X Y时,则应合并X的根节点和Y的根节点,同时修改各自的relat。那么问题来了,合并了之后,被合并的根节点的relat值如何变化呢?
现有xydxy的关系,fxfy分别是xy的根节点,于是我们有x--relat[x]-->fxy--relat[y]-->fy,显然我们可以得到fx--(3-relat[x])-->xfy--(3-relat[y])-->y。假如合并后fx为新的树的根节点,那么原先fx树上的节点不需变化,fy树则需改变了,因为relat值为该节点和树根的关系。这里只改变relat(fx)即可,因为在进行find操作时可相应改变fy树的所有节点的relat值。于是问题变成了fx--?-->fy。我们不难发现fx--(3-relat[x])-->x--d-->y--relat[y]-->fy,我们有fx--(3-relat[x])-->x--d-->y--relat[y]-->fy。我们求解了fxfy的关系。即fx----(relat[y] - relat[x] +3 +d)%3--->fy(如下图:)

 

View Code
 1 //食物链
 2 //!!!!!!!
 3 #include <iostream>
 4 #include <cstdio>
 5 #include <cstring>
 6 
 7 using namespace std;
 8 
 9 #define MAXN 50010
10 int N,M,K,fa[MAXN],relat[MAXN];//ralat 表示与父亲的关系,0表示是同类,1表示是x被fa[x]吃,2表示是吃父亲
11 int ans=0;
12 void init(int n)
13 {
14     for(int i=0;i<=n;i++)
15     {
16         fa[i]=i;
17         relat[i]=0;
18     }
19 
20 }
21 
22 int find(int x)
23 {
24     if( x != fa[x])
25     {
26         int t=fa[x];
27         fa[x]=find(fa[x]);
28         relat[x]=(relat[x] + relat[t])%3;//A
29     }
30     return fa[x];
31 }
32 
33 void unio(int x,int y,int d)//d是x,y的关系
34 {
35     int fx=find(x);
36     int fy=find(y);
37     fa[fx]=fy;
38     relat[fx]=(relat[y] - relat[x] +3 +d)%3;//B
39 }
40 
41 int main()
42 {
43     int d,x,y;
44     scanf("%d%d",&N,&K);
45     ans=0;
46     init(N);
47     while(K--)
48     {
49         scanf("%d%d%d",&d,&x,&y);
50         if(x>N || y>N)
51         {
52             ans++;
53             continue;
54         }
55         if(d==2 && x==y)
56         {
57             ans++;
58             continue;
59         }
60         int fx=find(x);
61         int fy=find(y);
62         if(fx==fy)
63         {
64             if((relat[x] - relat[y] +3)%3 != d-1)//
65                 ans++;
66         }
67         else
68         {
69             unio(x,y,d-1);//d-1==1表示的是x与y的关系
70         }
71     }
72     printf("%d\n",ans);
73     return 0;
74 }

POJ上的种类并查集还有:

POJ-1703、POJ-2492、POJ-1733、POJ-1988等。

 

POJ-1703  Find them, Catch them(两个互斥集合)

题目大意是:有两个帮派,告诉你那两个人属于不同的帮派,让你判断某两个人得是否在一个帮派中。

 

并查集的核心是用集合里的一个元素代表整个集合,集合里所有元素都指向这个元素,称它为根元素。集合里任何一个元素都能到达根元素。这一题里,设数组fa[x]表示x的父亲是fa[x](x,fa[x]在一个帮派),diff[x]表示x与diff[x]不在同一个集合里面。

如果是D[b][c]命令的话,即bc不在同一个帮派,故bdiff[c]在同一个帮派。把b放到diff[c]的集合里,同理把c放到diff[b]里面。

 

View Code
1     if(diff[b] == -1)
2                     diff[b]=c;
3                 if(diff[c] == -1)
4                     diff[c]=b;
5                 unio(b,diff[c]);
6                 unio(c,diff[b]);

 

如果是A命令的话,查询b,c的根元素:

1. 根元素相同,b,c在同一个集合里;

2. 根元素不同,但bdiff[c]的根元素相同,b,c不在一个集合里;

3.否则,bc还没有确定。

POJ-2492与1703基本一样。

另解(更一般的解)这两题可以用食物链的形式写,即简化的食物链,比食物链少一个关系,即相当于1221

对应关系即为:

x--(r1+r2)%2->z

fx----(relat[y] - relat[x] +2+d)%2--->fy

下面给出1703的食物链改编版:

 

View Code
 1 //食物链改编版
 2 //!!!!!!!
 3 #include <iostream>
 4 #include <cstdio>
 5 #include <cstring>
 6 using namespace std;
 7 #define MAXN 100010
 8 int N,M,K,fa[MAXN],relat[MAXN];//此题中0表示在同一类,1表示不在同一类。
 9 int ans=0;
10 void init(int n)
11 {
12     for(int i=0;i<=n;i++)
13     {
14         fa[i]=i;
15         relat[i]=0;
16     }
17 }
18 int find(int x)
19 {
20     if( x != fa[x])
21     {
22         int t=fa[x];
23         fa[x]=find(fa[x]);
24         relat[x]=(relat[x] + relat[t])%2;//A
25     }
26     return fa[x];
27 }
28 void unio(int x,int y,int d)//d是x,y的关系
29 {
30     int fx=find(x);
31     int fy=find(y);
32     fa[fx]=fy;
33     relat[fx]=(relat[y] - relat[x] +2 +d)%2;//B
34 }
35 int main()
36 {
37     int x,y;
38     char op;
39     char buf[10];
40     int t;
41     scanf("%d",&t);
42     while(t--)
43     {
44         scanf("%d%d",&N,&M);
45         ans=0;
46         init(N);
47         while(M--)
48         {
49             getchar();
50             scanf("%s%d%d",&buf,&x,&y);//用cin tle。。。。
51             op=buf[0];
52             if(op=='D')
53             {
54                 unio(x,y,1);//1代表着x,y不在同一类,即食物链中的x吃y。
55             }
56             else
57             {
58                 int fx=find(x),fy=find(y);
59                 if(fx==fy)
60                 {
61                     if((relat[x] - relat[y] +2)%2 ==1)//用食物链的观点来看,即x,y不在同一类。
62                     {
63                         printf("In different gangs.\n");
64                     }
65                     else
66                         printf("In the same gang.\n");
67                 }
68                 else
69                 {
70                     printf("Not sure yet.\n");
71                 }
72             }
73         }
74     }
75     return 0;
76 }

 

POJ-1733 Parity game 

题目大意这题的大意是对于一个正整数的区间,有若干句话,判断第一句错误的位置,每句话所描述的意思是对于一个区间[a, b]有奇数个1或是偶数个1 

 

思路:

设s[i]表示前i个数中1的个数,则s[0]=0;则信息i j even等价于s[j]-s[i-1]为偶数,即s[j]s[i-1]同奇偶。这样,每条信息都可以变为s[i-1]s[j]是否同奇偶的信息。

若记:
     fa[j]为当前和fa[j]同奇偶的元素集合,
     diff[j]为和fa[j]不同奇偶的元素集合,
 则一条信息i j even表明i-1j有相同的奇偶性,将导致fa[j]fa[i-1]合并,diff[j]diff[i-1]合并;
     i j odd表明i-1,j的奇偶性不同,将导致fa[j]diff[i-1]合并(fa[j]fa[i-1]奇偶性不同即与diff[i-1]奇偶性相)diff[j]fa[i-1]合并。

 

最后这题还必须得离散化,因为原来的区间太大,可以直接HASH一下,离散化并不会影响最终的结果,

 

View Code
 1 #include <iostream>
 2 #include <cstdio>
 3 #include <cstring>
 4 using namespace std;
 5 #define MAXN 10010
 6 #define HASH 9941
 7 int N,K;
 8 int fa[MAXN],diff[MAXN],rank[MAXN];
 9 int hash[MAXN];
10 void init()
11 {
12     for(int i=0;i<MAXN;i++)
13     {
14         hash[i]=-1;
15         diff[i]=-1;
16         fa[i]=i;
17         rank[i]=0;
18     }
19 }
20 int find(int x)
21 {
22     if(x==-1) return -1;
23     if(x == fa[x]) return x;
24     fa[x]=find(fa[x]);
25     return fa[x];
26 }
27 void unio(int x,int y)
28 {
29     if(x==-1 || y==-1) return;
30     int fx=find(x),fy=find(y);
31     if(fx==fy) return ;
32     if(rank[fx]>rank[fy])
33         fa[fy]=fx;
34     else
35     {
36         fa[fx]=fy;
37         if(rank[fx]==rank[fy])
38             rank[fy]++;
39     }
40 }
41 int main()
42 {
43     scanf("%d%d",&N,&K);
44     init();
45     int a,b,sa,sb,da,db,ha,hb;
46     char s[10];
47     for(int i=1;i<=K;i++)
48     {
49         scanf("%d%d%s",&a,&b,&s);
50         a--;
51         ha=a%HASH;
52         while(hash[ha] != -1 && hash[ha] !=a)
53             ha = (ha+1) %HASH;
54         hash[ha] = a;
55         a=ha;
56         hb = b % HASH;
57         while(hash[hb] != -1 && hash[hb] != b)
58             hb =(hb+1) %HASH;
59         hash[hb] =b;
60         b=hb;
61 //将a,b,diff[a],diff[b]的根结点找出来,再按要求合并
62         sa=find(a);
63         da=find(diff[a]);
64         sb=find(b);
65         db=find(diff[b]);
66         if(s[0]=='e')
67         {
68             if(sa== db || da==sb)
69             {
70                 printf("%d\n",i-1);
71                 return 0;
72             }
73             if(diff[a]==-1) diff[a]=db;
74             if(diff[b]==-1) diff[b]=da;
75             unio(sa,sb);
76             unio(da,db);
77         }
78         else if(s[0]=='o')
79         {
80             if(sa ==sb || (da != -1 && da== db))
81             {
82                 printf("%d\n",i-1);
83                 return 0;
84             }
85             if(diff[a] == -1) diff[a] = sb;        
86             if(diff[b] == -1) diff[b] = sa;
87             unio(sa, db);                    
88             unio(da, sb);
89         }
90     }
91     printf("%d\n",K);
92     return 0;
93 }

 

POJ-1988  Cube Stacking 

题意:是给出N个立方体,可以将立方体移动到其它立方体形成堆,然后有P个下面的操作: 1) M X Y ,X立方体所在的堆移到Y立方体所在的堆的上面;  2) C X 输出在X所在的堆上,X立方体下面的立方体个数。 

思路:

用三个数组,fa,ans,sum, fa[i]表示i的根结点,ans[i]表示i的结果,即压在i下面的立方体个数,sum[i]表示i所在的堆的立方体总个数。对于每一堆立方体,根结点使用堆底的立方体,而且在这个堆所对应的集合内,通过更新,使得只有根结点的sum值为这堆的总个数,ans值为0(因为它在堆底),其它的立方体的sum值都为0ans值在并查集的查找步骤中进行递归更新。   
  在并查集的查找函数的执行中,先向上找到根结点,并且保存当前结点x的父节点为tmp,找到根结点后,向下依次一更新结点的ans,sum值。
      1)sum[x]不为0,即表示x是一个堆的堆底元素,ans[x]0,其父节点是另外一堆的堆底(因为在并查集的操作中,通过将一个堆的堆底指向另一个堆的堆底来实现合并), ans[x]+=sum[tmp],sum[tmp]+=sum[x],sum[x]=0 ,这三个语句将xans值加上父结点的总个数(因为是将x所在的堆放在父节点的堆,所有x下面的正方体个数加上刚刚放上去的父亲的值),然后将父节点的sum值加上xsum(父节点的堆的总数变为两者之和),然后再将xsum值置0.
      2)sum[x]0,即表示x不是堆底,那么只要将xans值加上父节点(此父亲是原来的父亲tmp,因为在前面的更新中此父亲已经被更新了。所有他的ans值即为压在他下面的正方体个数)ans值即可。ans[x]+=ans[tmp]。下面是并查集的几个函数。在合并操作里面,合并完后我们再对x,y执行一次查找操作以更新对应堆的值,因为在下次合并的时候可能堆还没有来得及更新。 

 

View Code
 1 #include <iostream>
 2 #include <cstdio>
 3 #include <cstring>
 4 using namespace std;
 5 #define MAXN 30010
 6 int fa[MAXN],ans[MAXN],sum[MAXN];
 7 int P;
 8 void init()
 9 {
10     for(int i=0;i<MAXN;i++)
11     {
12         fa[i]=i;
13         ans[i]=0;
14         sum[i]=1;
15     }
16 }
17 int find(int x)
18 {
19     int tmp;
20     if(x != fa[x])
21     {
22         tmp=fa[x];
23         fa[x]=find(fa[x]);
24         if(sum[x] != 0)
25         {
26             ans[x] += sum[tmp];
27             sum[tmp] += sum[x];
28             sum[x] =0;
29         }
30         else
31         {
32             ans[x] += ans[tmp];
33         }
34     }
35     return fa[x];
36 }
37 void unio(int x,int y)
38 {
39     int fx=find(x);
40     int fy=find(y);
41     fa[fx]=fy;
42 }
43 int main()
44 {
45     char c;
46     int a,b;
47     init();
48     scanf("%d",&P);
49     while(P--)
50     {
51         getchar();
52         scanf("%c",&c);
53         if(c=='M')
54         {
55             scanf("%d%d",&a,&b);
56             unio(a,b);
57             find(a);//每次合并后都得更新,防止下次合并出错
58             find(b);
59         }
60         else
61         {
62             scanf("%d",&a);
63             find(a);
64             printf("%d\n",ans[a]);
65         }
66     }
67 }

 

一点心得:

个人感觉对于那些种类并查集应该都可以用食物链的关系来理解的,通过记录与根结点的关系来判断是否在一个集合。。。刚刚把poj1703翻译成食物链版本,下次试试把上面这些题都翻译成食物链版本。。。。

 

一些其他的并查集题目汇总:

http://hi.baidu.com/czyuan%5Facm/blog/item/531c07afdc7d6fc57cd92ab1.html

 

posted @ 2012-07-22 23:29  Missa  阅读(2439)  评论(3编辑  收藏  举报