• 数据结构之并查集小结


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

                    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

     

  • 相关阅读:
    函数式编程理解
    Java8 lambda表达式10个示例
    MD5进行文件完整性校验的操作方法
    加密算法和MD5等散列算法的区别(转)
    随笔
    瑕疵(bug)严重性定义
    无需Cygwin,如果没有在命令行,Eclipse编NDK
    BZOJ 1878 SDOI 2009 HH项链 树状数组 + 脱机处理
    Teamcity+SVN+VisualStudio在持续集成简明教程
    UVALive3713-Astronauts(2-SAT)
  • 原文地址:https://www.cnblogs.com/Missa/p/2604220.html
Copyright © 2020-2023  润新知