并查集问题

一、基本知识

1. 作用

(1)将两个集合合并
(2)询问两个元素是否在一个集合当中

2.基本原理

以树的形式维护每个集合, 每个集合的根节点编号就是该集合的编号。
每个节点都存储他的父节点。 p[x] 表示 x 的父节点

3.三个问题

  1. 如何判断树根:
    if(p[x] == x)
  2. 如何求 x 的 集合编号
    while(p[x] != x) x = p[x]
  3. 如何合并两个集合
    p[x] 是 x 集合编号, p[y] 是 y 集合编号
    p[x[ = y 即可。

4.一个优化

路径压缩
找到根节点后,直接把路径上所有点直接指向根节点

二、合并集合

模板题
题目链接
题目描述
一共有n个数,编号是1~n,最开始每个数各自在一个集合中。
现在要进行m个操作,操作共有两种:
“M a b”,将编号为a和b的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
“Q a b”,询问编号为a和b的两个数是否在同一个集合中;
输入格式
第一行输入整数n和m。
接下来m行,每行包含一个操作指令,指令为“M a b”或“Q a b”中的一种。
输出格式
对于每个询问指令”Q a b”,都要输出一个结果,如果a和b在同一集合内,则输出“Yes”,否则输出“No”。
每个结果占一行。

//一共n个数, 编号1-n, 最开始每个数各自在一个集合中, 
//进行m个操作 "Mab"将编号为a,b的两个数的集合合并, "Qab"询问看是否在同一集合
#include <iostream>
using namespace std;
const int N = 1e5+10;
int n, m;
int p[N]; //存储每个元素的父节点是谁

int find(int x) //返回x所在集合的编号(祖宗结点) 路径压缩
{
    if(p[x] != x) p[x] = find(p[x]); //x不是根节点, 让父节点等于祖宗节点
    return p[x];
}

int main(void){
    scanf("%d %d", &n, &m);
    for(int i = 1; i <= n; i++) p[i] = i;  //初始化
    while(m--){
        char op[2];  //读入单个字符,%c 容易读入一些莫名其妙的字符
        int a, b;
        scanf("%s%d%d", op, &a, &b);
        if(op[0] == 'M') p[find(a)] = find(b);
        else{
            if(find(a) == find(b)) puts("Yes");
            else puts("No");
        }
    }
    return 0;
}

三、连通块中点的数量

题目链接
题目描述
给定一个包含n个点(编号为1~n)的无向图,初始时图中没有边。
现在要进行m个操作,操作共有三种:
“C a b”,在点a和点b之间连一条边,a和b可能相等;
“Q1 a b”,询问点a和点b是否在同一个连通块中,a和b可能相等;
“Q2 a”,询问点a所在连通块中点的数量;
输入格式
第一行输入整数n和m。
接下来m行,每行包含一个操作指令,指令为“C a b”,“Q1 a b”或“Q2 a”中的一种。
输出格式
对于每个询问指令”Q1 a b”,如果a和b在同一个连通块中,则输出“Yes”,否则输出“No”。
对于每个询问指令“Q2 a”,输出一个整数表示点a所在连通块中点的数量
每个结果占一行。

分析

可以再设一个数组,只保证根节点的size有意义即可
每次合并两集合时,更新根节点的值即可。

代码

//一共n个数, 编号1-n, 最开始每个数各自在一个集合中, 
//进行m个操作 "C a b"将编号为a,b的两个数的集合合并, "Q1ab"询问看是否在同一集合
//Q2a 询问a所在连通块中点数量
#include <iostream>
using namespace std;
const int N = 1e5+10;
int n, m;
int p[N], size1[N]; //存储每个元素的父节点是谁
//只保证根节点的size是有意义的

int find(int x) //返回x所在集合的编号(祖宗结点) 路径压缩
{
    if(p[x] != x) p[x] = find(p[x]); //x不是根节点, 让父节点等于祖宗节点
    return p[x];
}

int main(void){
    scanf("%d %d", &n, &m);
    for(int i = 1; i <= n; i++) p[i] = i, size1[i] = 1;  //初始化
    while(m--){
        char op[2];  //读入单个字符, 容易读入一些莫名其妙的字符
        int a, b;
        scanf("%s", op);
        if(op[0] == 'C'){
            scanf("%d%d", &a, &b);
            if(find(a) == find(b)) continue;//当a,b已经在同一集合里,直接跳过即可
           // p[find(a)] = find(b);find函数会用到p[]数组,所以不能先改变
            size1[find(b)] += size1[find(a)];
            p[find(a)] = find(b);
        }
        else if(op[1] == '1'){
            scanf("%d%d", &a, &b);
            if(find(a) == find(b)) puts("Yes");
            else puts("No");
        }
        else{ 
            scanf("%d", &a);
            printf("%d\n", size1[find(a)]);
        }
    }
    return 0;
}

四、食物链

题目链接
题目描述:
动物王国中有三类动物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和K句话,输出假话的总数。
输入格式
第一行是两个整数N和K,以一个空格分隔。
以下K行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中D表示说法的种类。
若D=1,则表示X和Y是同类。
若D=2,则表示X吃Y。
输出格式
只有一个整数,表示假话的数目。
数据范围
1≤N≤50000,
0≤K≤100000

1.分析

  1. 同类、被吃等情况,我们都可以把他们放到一个集合里, 集合里的元素,我们可以判断,他们都是有关系的。比如: A 吃 B, B, C同类,我们可以知道,A和C一定有关系
  2. 如何确定两元素之间的关系?我们可以 记录元素与根节点之间的关系来确定
  3. 用每个点到根节点的距离来确定关系
    A到根节点的距离是1,表示可以吃根节点, %3 == 1
    B到根节点的距离是二,到A的距离是1,可以吃A, 表示他是被根节点吃的(由题意可知, 围成的是个环) %3 == 2
    C到根节点的距离是3,表示他跟根节点是同类。%3 == 0
    我们可以用%3来判断
    余1的点可以吃根节点,
    余2的点可以吃余1的点,被余0的点吃
    余0 与根节点同类。

距离: x吃y, 表示y到x的距离是1
4. 当两者x, y不属于同一类,不在同一个集合里,
我们把y根节点 p[x]接在 p[y]上, 要保证如何记录p[x] d到 p[y]的距离?
(d[x] + ? - d[y] ) % 3 == 0
? = d[y] - d[x]

  1. 当x, y不属于同一类, x吃y的关系
    操作同上,
    (d[x] + ? - d[y] - 1 ) % 3 == 0;
    ? = d[y] + 1 - d[x]
  2. //不用初始化k[],定义在全局,初始便是0

2.代码

/*
该点到父节点的距离:d[x], d[p[x]] 就是父节点到根节点的距离,
相加即为该点到根节点的距离
*/

#include <algorithm>
#include <iostream>
using namespace std;
const int N = 5e4;
int n, m;
int p[N], d[N]; 

int find(int x){
    if(x != p[x]){// 如果x不是树根
        int t = find(p[x]); 
  //在调用find函数时,会把父节点到根节点的距离返回,此时t存储的是根节点的值
        d[x] += d[p[x]];
        p[x] = t;
    }
    return p[x];
}



int main(void){
    cin >> n >> m;
    for(int i = 1; i <= n; i ++) p[i] = i;
    int res = 0;
    while(m --){
        int t, x, y;
        scanf("%d%d%d", &t, &x, &y);
        int px = find(x);
        int py = find(y);
        if(x > n || y > n) res++;
        else if(t == 1){
            if(px == py && (d[x] - d[y]) % 3){//如果两者已经在一棵树上了
                res ++; //两者不是同一类
            }
            else if(px != py){//没有在同一个集合里,我们要合并
                p[px] = py;//把x接向y的那颗树
                d[px] += d[y] - d[x]; //两根之间距离
            }
        }
        else{//判断吃与被吃关系
            if(px == py && (d[x] - d[y] - 1) % 3) res ++;
            else if(px != py){
                p[px] = py;
                d[px] = d[y] + 1 - d[x];
            }
        }
    }
    cout << res << endl;
    return 0;
}

五、小希的迷宫

题目描述
上次Gardon的迷宫城堡小希玩了很久(见Problem B),现在她也想设计一个迷宫让Gardon来走。但是她设计迷宫的思路不一样,首先她认为所有的通道都应该是双向连通的,就是说如果有一个通道连通了房间A和B,那么既可以通过它从房间A走到房间B,也可以通过它从房间B走到房间A,为了提高难度,小希希望任意两个房间有且仅有一条路径可以相通(除非走了回头路)。小希现在把她的设计图给你,让你帮忙判断她的设计图是否符合她的设计思路。比如下面的例子,前两个是符合条件的,但是最后一个却有两种方法从5到达8。
在这里插入图片描述
输入:
输入包含多组数据,每组数据是一个以0 0结尾的整数对列表,表示了一条通道连接的两个房间的编号。房间的编号至少为1,且不超过100000。每两组数据之间有一个空行。
整个文件以两个-1结尾。
输出:
对于输入的每一组数据,输出仅包括一行。如果该迷宫符合小希的思路,那么输出"Yes",否则输出"No"。

分析

  1. 不能产生环
  2. 只能有一个图,如 1 2 3 4,是两个子图, 不成立

代码

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5+5;
int p[N], vis[N]; //存储父节点, 标记数组vis, 标记顶点是否被使用过
int a, b;

int find(int x){//查找根节点
    int r = x;  //这样不会改变x的值
    while(p[r] != r){  //毕竟递归需要时间
        r = p[r];
    }
    return r;
}

int main(void){
    while(1){
        scanf("%d %d", &a, &b);
        if(a == -1 && b == -1)return 0;
        if(a == 0 && b == 0){
            printf("Yes\n");
            continue;
        }
        for(int i = 1; i <= 1e5; i++){  //每组数据都这样处理,初始化的意味
            p[i] = i;
            vis[i] = 1;
        }
        vis[a] = 0;
        vis[b] = 0;
        if(a != b) p[find(a)] = find(b);  //连接
        int Min = min(a,b);
        int Max = max(a, b);
        int mark = 1;//标记是否存在环路
        while(1){
            scanf("%d %d", &a, &b);
            if(a == 0 && b == 0) break;
            Max = max(Max, a);
            Max = max(Max, b);//便于之后遍历
            Min = min(Min, a);
            Min = min(Min, b);
            vis[a] = 0;
            vis[b] = 0;
            if(find(a) != find(b)) p[find(a)] = find(b);
            else if(a != b) mark = 0;//成环了
        }
        int cnt = 0;
      //  printf("%d %d\n", Max, Min);
        for(int i = Min; i <= Max; i++){
            if(vis[i] == 0 && p[i] == i) cnt ++;
            //统计有几个子图
        }
        if(mark == 1 &&  cnt == 1) printf("Yes\n");
        else printf("No\n");
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Xuhx&

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值