【蓝桥集训】第七天——并查集

作者:指针不指南吗
专栏:Acwing 蓝桥集训每日一题

或许会很慢,但是不可以停下来

文章目录

  • 1.亲戚
  • 2.合并集合
  • 3.连通块中点的数量

有关并查集的知识学习可以移步至—— 【算法】——并查集

1.亲戚

或许你并不知道,你的某个朋友是你的亲戚。

他可能是你的曾祖父的外公的女婿的外甥女的表姐的孙子。

如果能得到完整的家谱,判断两个人是否是亲戚应该是可行的,但如果两个人的最近公共祖先与他们相隔好几代,使得家谱十分庞大,那么检验亲戚关系实非人力所能及。

在这种情况下,最好的帮手就是计算机。

为了将问题简化,你将得到一些亲戚关系的信息,如Marry和Tom是亲戚,Tom和Ben是亲戚,等等。

从这些信息中,你可以推出Marry和Ben是亲戚。

请写一个程序,对于我们的关于亲戚关系的提问,以最快的速度给出答案。

输入格式

输入由两部分组成。

第一部分以 N,M开始。N 为问题涉及的人的个数。这些人的编号为 1,2,3,…,N。下面有 M 行,每行有两个数 a i , b i a_i,b_i ai,bi ,表示已知 a i a_i ai b i b_i bi 是亲戚。

第二部分以 Q 开始。以下 Q 行有 Q 个询问,每行为 c i , d i c_i,d_i ci,di ,表示询问 c i c_i ci d i d_i di 是否为亲戚。

输出格式

对于每个询问 c i , d i c_i,d_i ci,di ,输出一行:若 c i c_i ci d i d_i di 为亲戚,则输出“Yes”,否则输出“No”。

数据范围

1≤N≤20000,
1≤M≤ 1 0 6 10^6 106 ,
1≤Q≤ 1 0 6 10^6 106 .

输入样例:

10 7
2 4
5 7
1 3
8 9
1 2
5 6
2 3
3
3 4
7 10
8 9

输出样例:

Yes
No
Yes
  • 思路

    • 把每个家族看成一个集合:人之间互为亲戚,则说明他们是一个家族的,用一个编号来表示;
    • 这个题比较简单,就是并查集的两个朴素操作:
      1. 两个人互为亲戚,进行家族合并,即并查集合并
      2. 查询两个人是否为亲戚,即看看这两人的家族是否一样
  • 代码实现

    #include
    using namespace std;
    
    const int N=200010;
    
    int n,m;   //n表示人数,m表示操作的次数
    int p[N];
    
    int find(int x)  //找到家族编号,即根节点
    {
        if(p[x]!=x) p[x]=find(p[x]);
        return p[x];
    }
    
    int main()
    {
       scanf("%d%d",&n,&m);
        
        for(int i=1;i<=n;i++)  p[i]=i; //初始化父节点
        
        while(m--){   //m次合并操作,亲戚互认
            int a,b;
        	scanf("%d%d",&a,&b);
            if(find(a)!=find(b))  p[find(a)]=find(b);   //家族集合合并
        }
        
        int q;
        cin>>q;
        while(q--){  //q次查询,是否是亲戚,一个家族集合的
            int x,y;
            scanf("%d%d",&x,&y);
            if(find(x)==find(y))  puts("Yes");   
            else puts("No");
        }
        
        
        return 0;
    }
    

2.合并集合

一共有 n 个数,编号是 1∼n,最开始每个数各自在一个集合中。

现在要进行 m 个操作,操作共有两种:

  1. M a b,将编号为 a 和 b 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
  2. Q a b,询问编号为 a 和 b 的两个数是否在同一个集合中;

输入格式

第一行输入整数 n 和 m。

接下来 m 行,每行包含一个操作指令,指令为 M a bQ a b 中的一种。

输出格式

对于每个询问指令 Q a b,都要输出一个结果,如果 a 和 b 在同一集合内,则输出 Yes,否则输出 No

每个结果占一行。

数据范围

1≤n,m≤ 1 0 5 10^5 105

输入样例:

4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4

输出样例:

Yes
No
Yes
  • 代码实现

    #include
    using namespace std;
    
    const int N=100010;
    
    int n,m; //n表示点的数量,m表示操作的次数
    int p[N];  //存的每个节点的父节点
    
    int find(int x) //返回x的祖宗节点+路径压缩
    {
        if(p[x]!=x)  p[x]=find(p[x]);
        return p[x];
    }
    
    int main()
    {
        scanf("%d%d",&n,&m);
        
        for(int i=1;i<=n;i++) p[i]=i; //最开始,每个点都各自在一个集合中,so父节点就是他本身;
        
        while(m--){
         
         char op[2];
         int a,b;
         scanf("%s%d %d",op,&a,&b);
         
         //合并
         if(op[0]=='M') p[find(a)]=p[find(b)];  //让a的祖宗节点等于b的祖宗节点,让a的祖宗节点直接插在b祖宗节点下面
         else{
             if(find(a)==find(b)) puts("Yes");  //判断是否属于同一个集合
             else puts("No");
         }  
        }
        
        return 0;
    }
    

注意

读入字母M或者是Q的时候,使用字符串op[2],是因为直接用char的话,可能会出现空格换行的问题作物,这种比较保险,记得在后面使用的时候,用op[0],不能直接使用op

puts自动包含换行

3.连通块中点的数量

给定一个包含 n 个点(编号为 1∼n)的无向图,初始时图中没有边。

现在要进行 m 个操作,操作共有三种:

  1. C a b,在点 a 和点 b 之间连一条边,a 和 b 可能相等;
  2. Q1 a b,询问点 a 和点 b 是否在同一个连通块中,a 和 b 可能相等;
  3. Q2 a,询问点 a 所在连通块中点的数量;

输入格式

第一行输入整数 n 和 m。

接下来 m 行,每行包含一个操作指令,指令为 C a bQ1 a bQ2 a 中的一种。

输出格式

对于每个询问指令 Q1 a b,如果 a 和 b 在同一个连通块中,则输出 Yes,否则输出 No

对于每个询问指令 Q2 a,输出一个整数表示点 a 所在连通块中点的数量

每个结果占一行。

数据范围

1≤n,m≤ 1 0 5 10^5 105

输入样例:

5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5

输出样例:

Yes
2
3
  • 思路

    • 连通块就是一个点的集合:集合中的点可以相互到达,直接或者是间接都是可以的;

    • 这时候我们可以把它类比成一个树,运用并查集,一个点集合,我们可以用一个编号来表示,属于同一个编号,就说明两个点之间可以相互到达,在一个连通块里面;

    • 有三个操作:

      1. 两点之间连一条边,那么这两个点所在集合中的点,都是可以相互到达的,即合成一个连通块,用并查集中的合并操作;

      2. 判断是否在一个连通块,用并查集的查询;

      3. 询问一个点集合的数量,需要我们额外维护,初始化的时候每个集合1个,合并的时候,两个集合数量相加,最后输出即可

  • 代码实现

    #include
    
    using namespace std;
    
    const int N=1000010;
    int n,m;
    int p[N],sizel[N];  //p表示父节点,sizel表示集合的大小,记住sizel里面放的是祖宗节点,后面容易出错
    
    int find(int n)  //返回祖宗节点
    {
        if(p[n]!=n) p[n]=find(p[n]);
        return p[n];
    }
    
    
    int main()
    {
        scanf("%d%d",&n,&m);  //读入点的数量和操作的次数
        
        for(int i=1;i<=n;i++){   //初始化,父节点就是它本身;集合大小都是1,只有他自己
            p[i]=i;
            sizel[i]=1;
        }
        
        char op[5];   
        while(m--){
            scanf("%s",op);  //读入操作的名字
            
            if(op[0]=='C'){   //合并
            int a,b;
            scanf("%d%d",&a,&b);
            if(find(a)==find(b)) continue;  //相同则进入下个循环
            else{   //不同即操作,两步的顺序不能反!!!
                sizel[find(b)]+=sizel[find(a)];  //b的集合大小加上a的集合大小
                p[find(a)]=find(b);   //让a的祖宗节点指向b的祖宗节点
            }
         }
         
            else if(op[1]=='1'){  //查询是否一个集合
                int a,b;
                scanf("%d%d",&a,&b);
                if(find(a)==find(b))  puts("Yes");
                else puts("No");
            }
            
            else{
                if(op[1]=='2') {   //输出集合大小
                    int d;
                    scanf("%d",&d);
                    printf("%d\n",sizel[find(d)]);  
                }
            }
        }
        
        return 0;
    }
    

Alt

你可能感兴趣的:(Acwing,蓝桥集训每日一题,算法,c++,数据结构,图论,并查集)