zoukankan      html  css  js  c++  java
  • 并查集

    基础概念from:https://oi-wiki.org/ds/dsu/


    并查集

    并查集是一种树形的数据结构,顾名思义,它用于处理一些不交集的 合并 及 查询 问题。 它支持两种操作:

    • 查找(Find):确定某个元素处于哪个子集;

    • 合并(Union):将两个子集合并成一个集合。

    *也就是说,不支持集合的分离、删除。

    初始化

    1 void makeSet(int size) {
    2   for (int i = 0; i < size; i++)
    3     fa[i] = i;  // i就在它本身的集合里
    4   return;
    5 }

    查找

    举个例子

    几个家族进行宴会,但是家族普遍长寿,所以人数众多。由于长时间的分离以及年龄的增长,这些人逐渐忘掉了自己的亲人,只记得自己的爸爸是谁了,而最长者(称为「祖先」)的父亲已经去世,他只知道自己是祖先。为了确定自己是哪个家族,他们想出了一个办法,只要问自己的爸爸是不是祖先,一层一层的向上问,直到问到祖先。如果要判断两人是否在同一家族,只要看两人的祖先是不是同一人就可以了。

    在这样的思想下,并查集的查找算法诞生了。

    此处给出一种 C++ 的参考实现:

    1 int fa[MAXN];  //记录某个人的爸爸是谁,特别规定,祖先的爸爸是他自己
    2 int find(int x) {
    3   //寻找x的祖先
    4   if (fa[x] == x)  //如果x是祖先则返回
    5     return x;
    6   else
    7     return find(fa[x]);  //如果不是则x的爸爸问x的爷爷
    8 }

    显然这样最终会返回 x 的祖先。

    路径压缩

    这样的确可以达成目的,但是显然效率实在太低。为什么呢?因为我们使用了太多没用的信息,我的祖先是谁与我父亲是谁没什么关系,这样一层一层找太浪费时间,不如我直接当祖先的儿子,问一次就可以出结果了。甚至祖先是谁都无所谓,只要这个人可以代表我们家族就能得到想要的效果。 把在路径上的每个节点都直接连接到根上 ,这就是路径压缩。

    此处给出一种 C++ 的参考实现:

    1 int find(int x) {
    2   if (x != fa[x])  // x不是自身的父亲,即x不是该集合的代表
    3     fa[x] = find(fa[x]);  //查找x的祖先直到找到代表,于是顺手路径压缩
    4   return fa[x];
    5 }

    不太懂的话我们就上两张图吧

    p1

    p2

    合并

    宴会上,一个家族的祖先突然对另一个家族说:我们两个家族交情这么好,不如合成一家好了。另一个家族也欣然接受了。
    我们之前说过,并不在意祖先究竟是谁,所以只要其中一个祖先变成另一个祖先的儿子就可以了。

    此处给出一种 C++ 的参考实现:

    1 void unionSet(int x, int y) {
    2   // x与y所在家族合并
    3   x = find(x);
    4   y = find(y);
    5   if (x == y)  //原本就在一个家族里就不管了
    6     return;
    7   fa[x] = y;  //把x的祖先变成y的祖先的儿子
    8 }

    启发式合并(按秩合并)

    一个祖先突然抖了个机灵:「你们家族人比较少,搬家到我们家族里比较方便,我们要是搬过去的话太费事了。」

    由于需要我们支持的只有集合的合并、查询操作,当我们需要将两个集合合二为一时,无论将哪一个集合连接到另一个集合的下面,都能得到正确的结果。但不同的连接方法存在时间复杂度的差异。具体来说,如果我们将一棵点数与深度都较小的集合树连接到一棵更大的集合树下,显然相比于另一种连接方案,其期望复杂度更优(也会带来更优的最坏复杂度)。

    当然,我们不总能遇到恰好如上所述的集合————点数与深度都更小。鉴于点数与深度这两个特征都很容易维护,我们常常从中择一,作为估价函数。而无论选择哪一个,时间复杂度都为 Θ(mα(m,n)),具体的证明可参见 References 中引用的论文。

    在算法竞赛的实际代码中,即便不使用启发式合并,代码也往往能够在规定时间内完成任务。在 Tarjan 的论文[1]中,证明了不使用启发式合并、只使用路径压缩的最坏时间复杂度是 Θ (m log n)。在姚期智的论文[2]中,证明了不使用启发式合并、只使用路径压缩,在平均情况下,时间复杂度依然是 θ (mα(m,n))。

    此处给出一种 C++ 的参考实现,其选择深度作为估价函数:

    1 int size[N];  //记录子树的大小
    2 void unionSet(int x, int y) {
    3   int xx = find(x), yy = find(y);
    4   if (xx == yy) return;
    5   if (size[xx] > size[yy])  //保证小的合到大的里
    6     swap(xx, yy);
    7   fa[xx] = yy;
    8   size[yy] += size[xx];
    9 }

    时间复杂度及空间复杂度

    时间复杂度

    同时使用路径压缩和启发式合并之后,并查集的每个操作平均时间仅为 O(α(n)) ,其中 α 为阿克曼函数的反函数,其增长极其缓慢,也就是说其单次操作的平均运行时间可以认为是一个很小的常数。

    空间复杂度

    显然为 O(n) 。

    带权并查集

    我们还可以在并查集的边上定义某种权值、以及这种权值在路径压缩时产生的运算,从而解决更多的问题。比如对于经典的「NOI2011」食物链,我们可以在边权上维护模 3 意义下的加法群。

    经典题目

    「NOI2015」程序自动分析

    「JSOI2008」星球大战

    「NOI2001」食物链

    「NOI2002」银河英雄传说

    其他应用

    最小生成树算法中的 Kruskal 是基于并查集的算法。

    References


     HDU-1232-畅通工程

    HDU-1232

    Problem Description

    某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府“畅通工程”的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路? 

    Input

    测试输入包含若干测试用例。每个测试用例的第1行给出两个正整数,分别是城镇数目N ( < 1000 )和道路数目M;随后的M行对应M条道路,每行给出一对正整数,分别是该条道路直接连通的两个城镇的编号。为简单起见,城镇从1到N编号。 
    注意:两个城市之间可以有多条道路相通,也就是说
    3 3
    1 2
    1 2
    2 1
    这种输入也是合法的
    当N为0时,输入结束,该用例不被处理。 

    Output

    对每个测试用例,在1行里输出最少还需要建设的道路数目。 

    Sample Input

    4 2
    1 3
    4 3
    3 3
    1 2
    1 3
    2 3
    5 2
    1 2
    3 5
    999 0
    0

    Sample Output

    1
    0
    2
    998

    Hint

    Huge input, scanf is recommended.

     这道题就是赤裸裸的并查集啊,最基本的操作,套模板就能过,并查集的入门题

     1 #include <stdio.h>
     2 #include <string.h>
     3 #include <string>
     4 #include <algorithm>
     5 #include <iostream> 
     6 using namespace std;
     7 #define maxn 1005
     8 int father[maxn];
     9 //bool isroot[maxn]; 
    10 
    11 void init(int n)
    12 {
    13     for(int i=1;i<=n;i++)
    14     {
    15         father[i]=i;
    16 //        isroot[i]=false;
    17     }
    18 }
    19 
    20 int findfather(int n)
    21 {
    22     while(n!=father[n])
    23     {
    24         n=father[n];
    25     }
    26     return n;
    27 }
    28 
    29 void Union(int a,int b)
    30 {
    31     int x=findfather(a);
    32     int y=findfather(b);
    33     if(x!=y)
    34         father[x]=y;
    35 }
    36 
    37 int main()
    38 {
    39     //freopen("sample.txt","r",stdin);
    40     int m,n;
    41     while(~scanf("%d",&m)&&m)
    42     {
    43         scanf("%d",&n); 
    44         init(m);
    45         for(int j=0;j<n;j++)
    46         {
    47             int a,b;
    48             scanf("%d %d",&a,&b);
    49             Union(a,b);
    50         }
    51 //        for(int i=1;i<=m;i++)
    52 //        {
    53 //            isroot[findfather(i)]=true;
    54 //        }
    55         int count=0;
    56         for(int i=1;i<=m;i++)
    57         {
    58             if(i==findfather(i))
    59                 count++;
    60         }
    61         printf("%d
    ",count-1);
    62     }
    63     return 0;
    64 }
    View Code

    Wireless Network

    http://poj.org/problem?id=2236

    Description

    An earthquake takes place in Southeast Asia. The ACM (Asia Cooperated Medical team) have set up a wireless network with the lap computers, but an unexpected aftershock attacked, all computers in the network were all broken. The computers are repaired one by one, and the network gradually began to work again. Because of the hardware restricts, each computer can only directly communicate with the computers that are not farther than d meters from it. But every computer can be regarded as the intermediary of the communication between two other computers, that is to say computer A and computer B can communicate if computer A and computer B can communicate directly or there is a computer C that can communicate with both A and B. 

    In the process of repairing the network, workers can take two kinds of operations at every moment, repairing a computer, or testing if two computers can communicate. Your job is to answer all the testing operations. 

    Input

    The first line contains two integers N and d (1 <= N <= 1001, 0 <= d <= 20000). Here N is the number of computers, which are numbered from 1 to N, and D is the maximum distance two computers can communicate directly. In the next N lines, each contains two integers xi, yi (0 <= xi, yi <= 10000), which is the coordinate of N computers. From the (N+1)-th line to the end of input, there are operations, which are carried out one by one. Each line contains an operation in one of following two formats: 
    1. "O p" (1 <= p <= N), which means repairing computer p. 
    2. "S p q" (1 <= p, q <= N), which means testing whether computer p and q can communicate. 

    The input will not exceed 300000 lines. 

    Output

    For each Testing operation, print "SUCCESS" if the two computers can communicate, or "FAIL" if not.

    Sample Input

    4 1
    0 1
    0 2
    0 3
    0 4
    O 1
    O 2
    O 4
    S 1 4
    O 3
    S 1 4

    Sample Output

    FAIL
    SUCCESS

    简单并查集,就修复点的时候要仔细考虑考虑怎样合并,要判断两点之间的距离

     1 #include <stdio.h>
     2 #include <algorithm>
     3 #include <math.h>
     4 using namespace std;
     5 #define maxn 10010
     6 int fa[maxn];
     7 bool F[maxn];
     8 
     9 struct D{
    10     int x;
    11     int y;
    12 }P[maxn];
    13 
    14 int Find(int n)
    15 {
    16     while(n!=fa[n])
    17     {
    18         n=fa[n];
    19     }
    20     return n;
    21 }
    22 
    23 void Union(int a,int b)
    24 {
    25     int aa,bb;
    26     aa=Find(a);
    27     bb=Find(b);
    28     if(aa!=bb)
    29         fa[aa]=bb;
    30 }
    31 
    32 int main()
    33 {
    34     //freopen("sample.txt","r",stdin);
    35     int n,d;
    36     scanf("%d %d",&n,&d);
    37     for(int i=1;i<=n;i++)
    38     {
    39         scanf("%d %d",&P[i].x,&P[i].y);
    40         fa[i]=i;
    41     }
    42     char c[5];
    43     while(~scanf("%s",c))
    44     {
    45         if(c[0]=='S')
    46         {
    47             int a,b;
    48             scanf("%d %d",&a,&b);
    49             if(Find(a)==Find(b))
    50                 printf("SUCCESS
    ");
    51             else
    52                 printf("FAIL
    ");
    53         }
    54         else if(c[0]=='O')
    55         {
    56             int t;
    57             scanf("%d",&t);
    58             if(F[t]==true)
    59                 continue;
    60             
    61             for(int i=1;i<=n;i++)
    62             {
    63                 int a,b;
    64                 a=P[i].x-P[t].x;
    65                 b=P[i].y-P[t].y;
    66                 if(F[i]&&(a*a+b*b)<= d*d) //小技巧,就不用了开根号了
    67                 {
    68                     Union(i,t);
    69                 }
    70             }
    71             F[t]=true; 
    72         }
    73     }
    74     return 0;
    75 }
    View Code

     [Noi2015]程序自动分析

    https://www.luogu.org/problemnew/show/P1955

    Description

    在实现程序自动分析的过程中,常常需要判定一些约束条件是否能被同时满足。
    考虑一个约束满足问题的简化版本:假设x1,x2,x3,…代表程序中出现的变量,给定n个形如xi=xj或xi≠xj的变量相等/不等的约束条件,请判定是否可以分别为每一个变量赋予恰当的值,
    使得上述所有约束条件同时被满足。例如,一个问题中的约束条件为:x1=x2,x2=x3,x3=x4,x1≠x4,这些约束条件显然是不可能同时被满足的,因此这个问题应判定为不可被满足。
    现在给出一些约束满足问题,请分别对它们进行判定。

    Input

    输入文件的第1行包含1个正整数t,表示需要判定的问题个数。注意这些问题之间是相互独立的。
    对于每个问题,包含若干行:
    第1行包含1个正整数n,表示该问题中需要被满足的约束条件个数。
    接下来n行,每行包括3个整数i,j,e,描述1个相等/不等的约束条件,相邻整数之间用单个空格隔开。若e=1,则该约束条件为xi=xj;若e=0,则该约束条件为xi≠xj。

    Output

    输出文件包括t行。
    输出文件的第k行输出一个字符串“YES”或者“NO”(不包含引号,字母全部大写),“YES”表示输入中的第k个问题判定为可以被满足,“NO”表示不可被满足。

    Sample Input

    2
    2
    1 2 1
    1 2 0
    2
    1 2 1
    2 1 1

    Sample Output

    NO
    YES

    HINT

     在第一个问题中,约束条件为:x1=x2,x1≠x2。这两个约束条件互相矛盾,因此不可被同时满足。
    在第二个问题中,约束条件为:x1=x2,x2=x1。这两个约束条件是等价的,可以被同时满足。
    1≤n≤1000000
    1≤i,j≤1000000000

     先把e=1的用并查集合并,再判断一下e=0的是否在一个集合。i、j需要离散化。

    要使用并查集+离散化……不知道少写个路径压缩会不会T,反正我没试过
    愿意试试也行,反正我懒了(……)
    读入之后离线处理,把xi=xj的操作排在前面丢进一个并查集,然后把xi!=xj的当做查询去查,如果xi和xj的祖先是一样的就输出NO就好了 判完了还没输出NO就是符合条件的啦

    昂,那么这道题我sb错的点在——

    • 离散化完了数组fa要开两倍 因为第一次写离散化不清楚这个事情所以找了很久…(最后打开了fsy神犇的博客对着查错(咳)
    • 判断到不符条件的不是输出完NO直接break跑掉吗。。我输了NO就没干别的事情(没有break,check标记倒是打上了),所以当一组数据里面有多个不符条件的就会输出一堆NONONONONONONO(……)

    然后应该就没有啦,题很水,不是蓝题难度,应该也就绿题(……)


    [JSOI2008]星球大战starwar

    https://www.luogu.org/problemnew/show/P1197

    Description

      很久以前,在一个遥远的星系,一个黑暗的帝国靠着它的超级武器统治者整个星系。某一天,凭着一个偶然的
    机遇,一支反抗军摧毁了帝国的超级武器,并攻下了星系中几乎所有的星球。这些星球通过特殊的以太隧道互相直
    接或间接地连接。 但好景不长,很快帝国又重新造出了他的超级武器。凭借这超级武器的力量,帝国开始有计划
    地摧毁反抗军占领的星球。由于星球的不断被摧毁,两个星球之间的通讯通道也开始不可靠起来。现在,反抗军首
    领交给你一个任务:给出原来两个星球之间的以太隧道连通情况以及帝国打击的星球顺序,以尽量快的速度求出每
    一次打击之后反抗军占据的星球的连通快的个数。(如果两个星球可以通过现存的以太通道直接或间接地连通,则
    这两个星球在同一个连通块中)。

    Input

      输入文件第一行包含两个整数,N (1  < =  N  < =  2M) 和M (1  < =  M  < =  200,000),分别表示星球的
    数目和以太隧道的数目。星球用 0 ~ N-1的整数编号。接下来的M行,每行包括两个整数X, Y,其中(0 < = X <> 
    Y 表示星球x和星球y之间有“以太”隧道,可以直接通讯。接下来的一行为一个整数k,表示将遭受攻击的星球的
    数目。接下来的k行,每行有一个整数,按照顺序列出了帝国军的攻击目标。这k个数互不相同,且都在0到n-1的范
    围内。

    Output

    第一行是开始时星球的连通块个数。接下来的K行,每行一个整数,表示经过该次打击后现存星球
    的连通块个数。

    Sample Input

    8 13
    0 1
    1 6
    6 5
    5 0
    0 6
    1 2
    2 3
    3 4
    4 5
    7 1
    7 2
    7 6
    3 6
    5
    1
    6
    3
    5
    7

    Sample Output

    1
    1
    1
    2
    3
    3

    逆向思维+并查集

    这道题看似很长其实也不是十分的难

    如果我们去正这摧毁 想想都有点困难

    要不 我们使用逆向思维?

    没错 这道题就把摧毁转换成修建(和平就是好)

    利用并查集判断联通就好了

    破坏一个点的话是比较难处理的,所以我们逆向考虑重建一个点。每次重建用并查集统计一下联通块的数目就好了。

    先假设所有要被摧毁的已经被摧毁,然后一个一个拼接回去,存储答案倒序输出即可。

     1 #include <stdio.h>
     2 #include <string.h>
     3 #include <string>
     4 #include <algorithm>
     5 #include <iostream> 
     6 using namespace std;
     7 #define maxn 400005
     8 int fa[maxn],D[maxn],ans[maxn],H[maxn],En=0;
     9 //fa为并查集,D存打击点,ans存每次打击后的答案 ,En为结构体的数量 
    10 bool F[maxn];//F来判断是否被打击掉 
    11 
    12 struct Edge //定义一个结构体来存储邻接表 
    13 {
    14     int a;
    15     int b;
    16     int next;
    17 }E[maxn];
    18 
    19 int Find(int x)
    20 {
    21     while(x!=fa[x])
    22     {
    23         x=fa[x];
    24     }
    25     return x;
    26 }
    27 
    28 void Insert(int x,int y) //邻接表存储数据 
    29 {
    30     E[En].a=x;
    31     E[En].b=y;
    32     E[En].next=H[x];
    33     H[x]=En;
    34     En++;
    35 }
    36 
    37 int main()
    38 {
    39     freopen("sample.txt","r",stdin);
    40     int tot;
    41     int n,m;
    42     scanf("%d %d",&n,&m);
    43     for(int i=0;i<n;i++)
    44     {
    45         fa[i]=i;
    46         H[i]=-1;
    47     }
    48     for(int i=0;i<m;i++)
    49     {
    50         int x,y;
    51         scanf("%d %d",&x,&y);
    52         Insert(x,y);
    53         Insert(y,x); //双向存储数据 
    54     }
    55     int k;
    56     scanf("%d",&k);
    57     tot=n-k;  //打击K次后所剩的点 
    58     for(int i=1;i<=k;i++)
    59     {
    60         int t;
    61         scanf("%d",&t);
    62         D[i]=t;//被打击掉后就true,并把打击的点存储到D中
    63         F[t]=true;
    64     }
    65     for(int i=0;i<2*m;i++)
    66     {
    67         if(F[E[i].a]==false&&F[E[i].b]==false) //如果都没有被打击
    68         {
    69             if(Find[E[i].a]!=Find[E[i].b]) //且之前没有连通
    70             {
    71                 tot--;    //合并这两个点并在总数减去一个
    72                 fa[Find(E[i].a)]=fa[Find(E[i].b)];
    73             }
    74         }
    75     }
    76     ans[k+1]=tot;  //这时为打击k次之后所剩下的连通块 
    77     for(int i=k;i>=1;i--)  //从后往前"修复" 
    78     {
    79         int t=D[i];  
    80         tot++;  //因为"修复"这个点,所以多了一个点,现在总数加 1
    81         F[t]=false;  //false表示这个点没有被打击
    82         for(int j=H[t];j!=-1;j=E[j].next)//邻接表遍历它所连着的点
    83         {
    84             if(F[E[j].b]==false&&Find(t)!=Find(E[j].b))//如果被连通的点没有被打击并且之前没有连通
    85             {
    86                 tot--; //合并
    87                 fa[Find(E[j].b)]=Find(t);//注意尽量不要到过来赋值,这样会不断改变father
    88             }
    89         } 
    90         ans[i]=tot;//每“修复”一个点后的有的连通块 
    91     }
    92     for(int i=1;i<=k+1;i++)
    93     {
    94         printf("%d
    ",ans[i]);
    95     }
    96     return 0;
    97 }
    View Code

    食物链  (种类并查集)

    http://poj.org/problem?id=1182

    Description

    动物王国中有三类动物A,B,C,这三类动物的食物链构成了有趣的环形。A吃B, B吃C,C吃A。 
    现有N个动物,以1-N编号。每个动物都是A,B,C中的一种,但是我们并不知道它到底是哪一种。 
    有人用两种说法对这N个动物所构成的食物链关系进行描述: 
    第一种说法是"1 X Y",表示X和Y是同类。 
    第二种说法是"2 X Y",表示X吃Y。 
    此人对N个动物,用上述两种说法,一句接一句地说出K句话,这K句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。 
    1) 当前的话与前面的某些真的话冲突,就是假话; 
    2) 当前的话中X或Y比N大,就是假话; 
    3) 当前的话表示X吃X,就是假话。 
    你的任务是根据给定的N(1 <= N <= 50,000)和K句话(0 <= K <= 100,000),输出假话的总数。 

    Input

    第一行是两个整数N和K,以一个空格分隔。 
    以下K行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中D表示说法的种类。 
    若D=1,则表示X和Y是同类。 
    若D=2,则表示X吃Y。

    Output

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

    Sample Input

    100 7
    1 101 1 
    2 1 2
    2 2 3 
    2 3 3 
    1 1 3 
    2 3 1 
    1 5 5

    Sample Output

     3

    这道题要我们得出假话的数目,如果这就话和之前的话冲突就是假话。我开始没有什么思路,看了看样例,随手画出了图,感觉像是图论的东西,但其实再仔细想想,其实这是一种状态的划分!!!比如说a和b是同类关系,那就把a和b划分到一个集合中,之后再说a和b是捕食关系一定是假话了。但是这道题的问题在于某一个动物在食物链中的角色不是一定的,一个物种可能作为捕食者也可能是被捕食者,还有可能给出同类之间的关系,那该怎么办呢?将所扮演的三种状态全都表示出来。列如,对于物种x,x代表A类,x+n代表B类,x+2n代表c类,其中A吃B,B吃C,C吃A。

    对于两个动物如果是同类关系,那么一定不存在捕食和被捕食关系;如果存在捕食关系,那么一定不存在被捕食和同类关系。

    题解 from:https://www.luogu.org/problemnew/solution/P2024

    引入

    并查集能维护连通性、传递性,通俗地说,亲戚的亲戚是亲戚

    然而当我们需要维护一些对立关系,比如 敌人的敌人是朋友 时,正常的并查集就很难满足我们的需求。

    这时,种类并查集就诞生了。

    常见的做法是将原并查集扩大一倍规模,并划分为两个种类。

    在同个种类的并查集中合并,和原始的并查集没什么区别,仍然表达他们是朋友这个含义。

    考虑在不同种类的并查集中合并的意义,其实就表达 他们是敌人 这个含义了。

    按照并查集美妙的 传递性,我们就能具体知道某两个元素到底是 敌人 还是 朋友 了。

    至于某个元素到底属于两个种类中的哪一个,由于我们不清楚,因此两个种类我们都试试。

    具体实现,详见 P1525 关押罪犯


    概念解释

    再来看本题,每个动物之间的关系就没上面那么简单了。

    对于动物 xx 和 yy,我们可能有 xx 吃 yy,xx 与 yy 同类,xx 被 yy 吃。

    但由于关系还是明显的,11 倍大小、22 倍大小的并查集都不能满足需求,33 倍大小不就行了!

    类似上面,我们将并查集分为 33 个部分,每个部分代表着一种动物种类。

    设我们有 nn 个动物,开了 3n3n 大小的种类并查集,其中 1 sim n1n 的部分为 AA 群系,n + 1 sim 2nn+12n 的部分为 BB 群系,2n + 1 sim 3n2n+13n 的部分为 CC 群系。

    我们可以认为 AA 表示中立者,BB 表示生产者,CC 表示消费者。此时关系明显:AA 吃 BB,AA 被 CC 吃。

    当然,我们也可以认为 BB 是中立者,这样 CC 就成为了生产者,AA 就表示消费者。(还有 11 种情况不提及了)

    联想一下 22 倍大小并查集的做法,不难列举出:当 AA 中的 xx 与 BB 中的 yy 合并,有关系 xx 吃 yy;当 CC 中的 xx 和 CC 中的 yy 合并,有关系 xx 和 yy 同类等等……

    但仍然注意了!我们不知道某个动物属于 AA,BB,还是 CC,我们 33 个种类都要试试!

    也就是说,每当有 11 句真话时,我们需要合并 33 组元素。

    容易忽略的是,题目中指出若 xx 吃 yy,yy 吃 zz,应有 xx 被 zz 吃。

    这个关系还能用种类并查集维护吗?答案是可以的。

    若将 xx 看作属于 AA,则 yy 属于 BB,zz 属于 CC。最后,根据关系 AA 被 CC 吃可得 xx 被 zz 吃。

    既然关系满足上述传递性,我们就能放心地使用种类并查集来维护啦。


    图片解释

    理论太难懂?那就结合数据和图片来解释吧!

    假如我们有以下的输入数据:

    4 5
    1 1 3
    2 2 4
    2 3 2
    1 1 4
    2 2 1

    因为涉及 4 个动物(n = 4n=4),所以构建初始并查集如下图:

    先看第 1句话:动物 1 和 3 是同类的。

    我们可以在 3个群系中分别给 1 和 3 的集合合并,以表示动物 1 和 3 是一定友好的。

    再看第 2 句话:动物 2 吃 4。

    显然这不是矛盾的。但我们不知道 2 和 4 对应 A, B,C 中的哪个,所以我们只能根据 A 吃 B,合并 A 群系中的 2 和 B 群系中的 4;再根据 B 吃 C 和 C 吃 A,作出对应的处理。结果如下所示:

    接着看第 3 句话:动物 3 吃 2。这是句真话,具体的真假话判断方法看下面两句话。我们暂且先作出以下处理:

    第 4 句话中,表明 1 和 4 是同类动物。此时我再解释如何判断话的真假。

    对于同类动物,我们转换一下,如果我们知道 1 不吃 4 且 4 不吃 1,他们不就同类了吗?

    好,那我们的任务就变成:如何判断动物 x 吃动物 y?

    反观第 2 句话,我们知道如果要表示动物 x 吃动物 y,只要根据 A 吃 B,把 A 群系中的 x 和 B 群系中的 y 合并即可。另外 2次合并暂不讨论。

    那反过来,如果 A 群系中的 x 已经和 B 群系中的 y 在同一集合中了,不就表示了动物 x 吃动物 y 吗?

    于是,我们看到上面那张图,B 群系中的 11 按照并查集的递归操作,找出自己的终极上级是 A 群系中的 4。

    分析其含义,属于 B 群系的 1 已经与 A 群系的 4,应有 4 吃 1,而非同类。第 4 句话是假的。

    那么第 5 句话,2 吃 1。我们需要判断 2 和 1 是否是同类并且 2 是否被 1 吃即可。

    判断是否同类,我们同样可以反过来:判断在同个群系中的 2 和 1 的集合是否已经合并。

    得出 2 和 1 不是同类后,我们再看 1 是否吃 2。看图,A 群系中的 1 和 B 群系中的 2 在同一集合中。

    得出 1 吃 2。第 5 句话也是假话。

    因此,答案就是有两句假话。输出 2,问题完美解决。


    注意事项

    • 种类并查集求的并非具体种类,而是关系!

    • 在代码过程中,不要忘了特判编号大于 n 的情况!


    代码实现

    如果还没有理解,只能使用最终办法了,上程序!

    (当然我还是希望各位摸清种类并查集的本质,灵活运用)

     1 #include <cstdio>
     2 
     3 inline int read() {
     4     char c = getchar(); int n = 0;
     5     while (c < '0' || c > '9') { c = getchar(); }
     6     while (c >= '0' && c <= '9') { n = (n << 1) + (n << 3) + (c & 15); c = getchar(); }
     7     return n;
     8 }
     9 
    10 const int maxN = 100005;
    11 
    12 int n, m, ans, fa[maxN * 3];
    13 
    14 int find(int u) { return fa[u] == u ? u : fa[u] = find(fa[u]); }
    15 
    16 int main() {
    17     n = read(), m = read();
    18     for (int i = 1; i <= n * 3; i++) { fa[i] = i; }
    19     for (; m; m--) {
    20         int opt = read(), u = read(), v = read();
    21         if (u > n || v > n) { ans++; continue; }
    22         if (opt == 1) {
    23             if (find(u + n) == find(v) || find(u) == find(v + n)) { ans++; }
    24             else {
    25                 fa[find(u)] = find(v);
    26                 fa[find(u + n)] = find(v + n);
    27                 fa[find(u + n + n)] = find(v + n + n);
    28             }
    29         } else {
    30             if (find(u) == find(v) || find(u) == find(v + n)) { ans++; }
    31             else {
    32                 fa[find(u + n)] = find(v);
    33                 fa[find(u + n + n)] = find(v + n);
    34                 fa[find(u)] = find(v + n + n);
    35             }
    36         }
    37     }
    38     printf("%d
    ", ans);
    39     return 0;
    40 }
    View Code

    (推荐)用3倍的并查积的存各种动物的关系

    一倍存本身,二倍存猎物,三倍存天敌

    唯一容易忽略的点就是:一的猎物的猎物 就是一的天敌

    那么我们每次只要维护三个并查积的关系就可以了

     1 #include<cstdio>
     2 int fa[300005];
     3 int n,k,ans;
     4 inline int read()
     5 {
     6     int sum=0;
     7     char ch=getchar();
     8     while(ch>'9'||ch<'0') ch=getchar();
     9     while(ch>='0'&&ch<='9') sum=sum*10+ch-48,ch=getchar();
    10     return sum;
    11 }//读入优化
    12 int find(int x)
    13 {
    14     if(x!=fa[x]) fa[x]=find(fa[x]);
    15     return fa[x];
    16 }//查询
    17 int unity(int x,int y)
    18 {
    19     int r1=find(fa[x]),r2=find(fa[y]);
    20     fa[r1]=r2;
    21 }//合并
    22 int main()
    23 {
    24     int x,y,z;
    25     n=read(),k=read();
    26     for(int i=1;i<=3*n;++i) fa[i]=i; //对于每种生物:设 x 为本身,x+n 为猎物,x+2*n 为天敌
    27     for(int i=1;i<=k;++i) 
    28     {
    29         z=read(),x=read(),y=read();
    30         if(x>n||y>n) {ans++; continue;} // 不属于该食物链显然为假
    31         if(z==1)
    32         {
    33             if(find(x+n)==find(y)||find(x+2*n)==find(y)) {ans++; continue;}
    34             //如果1是2的天敌或猎物,显然为谎言
    35             unity(x,y); unity(x+n,y+n); unity(x+2*n,y+2*n);
    36             //如果为真,那么1的同类和2的同类,1的猎物是2的猎物,1的天敌是2的天敌
    37         }
    38         else if(z==2)
    39         {
    40             if(x==y) {ans++; continue;} //其实是废话但是可以稍微省点时间
    41             if(find(x)==find(y)||find(x+2*n)==find(y)) {ans++; continue;}
    42             //如果1是2的同类或猎物,显然为谎言
    43             unity(x,y+2*n); unity(x+n,y); unity(x+2*n,y+n);
    44             //如果为真,那么1的同类是2的天敌,1的猎物是2的同类,1的天敌是2的猎物
    45         }
    46     }
    47     printf("%d
    ",ans);
    48     return 0;
    49 }
    View Code

    自己根据题解写的

     1 #include <stdio.h>
     2 #include <string.h>
     3 #include <algorithm>
     4 
     5 using namespace std;
     6 #define maxn 50005
     7 int fa[maxn*3];
     8 
     9 int Find(int n)
    10 {
    11     while(n!=fa[n])
    12     {
    13         n=fa[n];
    14     }
    15     return n;
    16 }
    17 
    18 void Union(int a,int b)
    19 {
    20     int aa,bb;
    21     aa=Find(a);
    22     bb=Find(b);
    23     if(aa!=bb)
    24         fa[aa]=bb;
    25 }
    26 
    27 int main()
    28 {
    29     int n,k;
    30     scanf("%d %d",&n,&k);
    31     int count=0;
    32     for(int i=1;i<=n*3;i++)
    33     {
    34         fa[i]=i;
    35     }
    36     for(int i=0;i<k;i++)
    37     {
    38         int a,x,y;
    39         scanf("%d %d %d",&a,&x,&y);
    40         if(x<1||x>n||y<1||y>n)
    41         {
    42             count++;
    43             continue;
    44         }
    45         if(a==2&&x==y)
    46         {
    47             count++;
    48             continue;
    49         }
    50         if(a==1)
    51         {
    52             if(Find(x)==Find(y+n)||Find(x)==Find(y+n*2))
    53                 count++;
    54             else
    55             {
    56                 Union(x,y);
    57                 Union(x+n,y+n);
    58                 Union(x+n*2,y+n*2);
    59             }
    60         }
    61         else if(a==2)
    62         {
    63             if(Find(x)==Find(y)||Find(x)==Find(y+n))
    64             {
    65                 count++;
    66                 continue;
    67             }
    68             Union(x,y+n*2);
    69             Union(x+n,y);
    70             Union(x+n*2,y+n);
    71         }
    72     }
    73     printf("%d
    ",count);
    74     return 0;
    75 }
    View Code

  • 相关阅读:
    利用jquery修改href的部分字符
    javascript基础 思维导图2
    Javascript 思维导图 绘制基础内容(值得一看)
    JavaScript判断是否全为中文,是否含有中文
    将Jquery序列化后的表单值转换成Json
    连接Oracle时报错ORA-12541: TNS: 无监听程序
    A Java Runtime Environment (JRE) or Java Development Kit (JDK) must be available in order to run Eclipse. No Java virtual machine was found after searching the following locations: /usr/local/eclipse/
    (转)Debian 安装与卸载包命令
    Flume 1.7.0单机版安装
    Struts2.5学习笔记----org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter报错
  • 原文地址:https://www.cnblogs.com/jiamian/p/11182608.html
Copyright © 2011-2022 走看看