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

来源:互联网 发布:linux grub2 修复 编辑:程序博客网 时间:2024/06/06 19:42

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

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

复制代码
 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     else28     {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

模版最小生成树:

复制代码
 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 Edge11 {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     else57         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 5001010 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;//A29     }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;//B39 }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         else68         {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的食物链改编版:

 

复制代码
 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;//A25     }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;//B34 }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             else57             {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                     else66                         printf("In the same gang.\n");67                 }68                 else69                 {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     else35     {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         else31         {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         else61         {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

0 0
原创粉丝点击