并查集(基本原理+示例)

处理图和树的问题。

例如:两个元素是否连通,各个连通图的元素总个数,合并两个元素所在的集合。等等还可以维护其他额外信息(带有权重...)。

基本原理:

每一个集合用一棵树来表示,树根的编号就是整个集合的编号,每个节点存储父节点信息。

(ex:p[x] 表示x的父节点)

        寻找一切信息的基础都要先找根节点,即集合编号。根节点保存信息。

问题:

  • 如何判断树根:p[x] = x
  • 如何求x所在的集合编号:while(p[x]!=x) x = p[x]
  • 如何合并两个集合:p[a] = p[b] = b

        优化:在求集合编号,即向上寻找树根的时候,时间复杂度相当于树的深度。可以使用路径压缩来减少时间复杂度。即,在寻找根节点的时候,将根节点下面所有的节点的父节点都指向根节点,大大的减少时间。使用递归可以实现,最后将根节点返回给每个子节点。

// 寻找根节点
int find(int x){
    if(p[x] != x) p[x] = find(p[x]);
    return p[x];
}

 

给定一个包含 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 b 或 Q2 a 中的一种。

输出格式

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

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

每个结果占一行。

数据范围

1≤n,m≤10^5

输入样例:
5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5
输出样例:
Yes
2
3
import java.io.*;

class Main{
    static int N = 100010;
    static int n,m;
    static int[] p = new int[N];  // 存储该点的根节点
    static int[] count = new int[N]; // 存储每个集合点的个数
    public static void main(String[] args) throws IOException{
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        String[] s = in.readLine().split(" ");
        n = Integer.parseInt(s[0]);
        m = Integer.parseInt(s[1]);
        for(int i=1;i<=n;i++){
            p[i] = i; // 初始化,每个元素都是一个集合,一棵树
            count[i] = 1; // 每个点都是一棵树,数量就为1
        }
        while(m-->0){
            s = in.readLine().split(" ");
            
            if(s[0].equals("C")){ // 连通两个点,相当于将 a的根节点 作为 b的根节点 的子节点
                int a = Integer.parseInt(s[1]);
                int b = Integer.parseInt(s[2]);
                if(find(a) != find(b)){
                    count[find(b)] += count[find(a)]; // 只需要修改b的根节点的信息,因为我将a的根节点作为b根节点的子节点        
                    p[find(a)] = find(b); // 
                }
            }
            else if(s[0].equals("Q1")){
                int a = Integer.parseInt(s[1]);
                int b = Integer.parseInt(s[2]);
                if(find(a) == find(b))
                    System.out.println("Yes");
                else 
                    System.out.println("No");
            }
            else{
                int a = Integer.parseInt(s[1]);
                System.out.println(count[find(a)]); // a节点所在集合的点的数量保存在根节点中。
            }
        }
    }
    public static int find(int x){ // 找根节点,并进行路径压缩
        if(p[x]!=x) p[x] = find(p[x]);
        return p[x];
    }
}

 

带权并查集

食物链: 难度↑

动物王国中有三类动物 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

输入样例:
100 7
1 101 1 
2 1 2
2 2 3 
2 3 3 
1 1 3 
2 3 1 
1 5 5
输出样例:
3

思路(带权并查集):

        该题需要维护一个额外信息,即这个动物被谁吃,吃谁的问题。其中a吃b,b吃c,c又吃a,构成一个循环。可以想到用一个有向图来做,但是考虑到根据k个信息,最终可能会组成很多个图,无法判定图之间的关系,因此考虑到将环拆开,将所有的点都放到一个集合中去,组成一棵树。因此可以想到用并查集,通过父子,子孙关系来判断吃与被吃的关系。

        模3。考虑每个节点与根节点的路径长度来判定吃与被吃的关系。

  • 余0:与根节点是同类
  • 余1:吃根节点
  • 余2:被根节点吃

        从上图中我们可以通过节点与根节的路径长度判断出各个节点与根节点之间的关系,从而判断出其他的关系。

        代码中问题:

  • 如何计算路径长度:在找根节点路径压缩的时候进行路径长度的计算。
  • 给出两个点是同类或者吃或被吃的关系,如何合并两个集合:

 代码:

import java.io.*;

class Main{
    static int N = 50010;
    static int n,k;
    static int[] p = new int[N]; // 集合
    static int[] d = new int[N]; // 到根节点的路径长度
    public static void main(String[] args) throws IOException{
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        String[] s = in.readLine().split(" ");
        n = Integer.parseInt(s[0]);
        k = Integer.parseInt(s[1]);

        for(int i=1;i<=n;i++)
            p[i] = i; // 初始化,每个点自己一个集合
            
        int res = 0; // 假话的个数
        while(k-->0){ 
            s = in.readLine().split(" ");
            int t = Integer.parseInt(s[0]);
            int x = Integer.parseInt(s[1]);
            int y = Integer.parseInt(s[2]);
            // 同类
            if(t==1){ 
                if(x>0&&y>0&&x<=n&&y<=n){ // 在n个以内
                    int px = find(x);
                    int py = find(y);
                    if(px==py&&(d[x]-d[y])%3!=0) res++; // 在一个集合
                    else if(px!=py){ // 不在同一个集合,因此需要合并集合
                        p[px] = py;
                        d[px] = d[y]-d[x];
                    }
                }
                else res++;
            }
            // x吃y
            else{  
                if(x>0&&y>0&&x<=n&&y<=n){
                    int px = find(x);
                    int py = find(y);
                    if(px==py&&(d[x]-d[y]-1)%3!=0) res++; // (x-y)%3==1或-2,因此再-1后%3==0
                    else if(px!=py){
                        p[px] = py;
                        d[px] = d[y]-d[x]+1;
                    }
                }
                else res++;
            }
        }
        System.out.println(res);
    }
    public static int find(int x){
        if(p[x]!=x){
            int t = find(p[x]); // 用t记录返回来的根节点
            d[x] += d[p[x]]; // 此时p[x]还是该节点的父节点,并且父节点已经更新
            p[x] = t; // 最后再将p[x]指向根节点
            /* p[x] = find(p[x]);
               d[x] += d[p[x]]; 
            错的*/
        } 
        return p[x];
    }
}
  • 21
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值