并查集算法

目录

1.算法介绍

1.1什么是并查集呢,它又是用来干什么的呢?

1.2问题引入

 2.算法解析

2.1初始化

2.2合并操作

2.3查找 + 路径压缩 

 2.4问题解决代码

3.变式突破


1.算法介绍

1.1什么是并查集呢,它又是用来干什么的呢?

逐字拆解一下,并、查、集。这个三个字,其中前面两个字都是动词,第三个字是个名词。

(1)并查集可以进行集合合并的操作(并)
(2)并查集可以查找元素在哪个集合中(查)
(3)并查集维护的是一堆集合(集)

基本原理:每个集合用一棵树来表示。树的编号就是整个集合的编号。每个节点存储它的父节点,p[x]表示x的父节点

 并查集算法无论是集合合并还是元素查询,时间复杂度都是接近  0(1)

1.2问题引入

模板题:

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

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

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

输入格式

第一行输入整数 n和 m。

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

输出格式

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

每个结果占一行。

数据范围

1≤n,m≤10^5


 2.算法解析

一些说明:

1.假如 n==5,就是说有5个元素1~5,分别所属在集合1到5中。

2.p数组存集合的编号,表示是哪个集合。

3.查询元素在哪个集合的函数是find(int x),会返回该元素所在的集合编号。

学习并查集模板,其实学习的就是下面三步 

2.1初始化

做法:用一个数组保存对应位置元素所属集合的编号。

模板代码:

 for (int i = 1; i <= n; i++) {
            p[i] = i;
        }

很容易理解,就是将当前元素的父节点指向自己 

此时p[1]==1,就表示元素1在集合1中。  

用n==5举例,此时代码表示的集合的状态:

2.2合并操作

做法:若想A,B两个集合合并:可以将B集合的集合编号设置为A集合的集合编号。

 模板代码:

p[find(B)]=find(A)

此时代码表示的集合的状态: 

 

2.3查找 + 路径压缩 

正是因为进行了路径压缩, 并查集的时间复杂度接近 0(1)。

先看模板代码:

 public static int find(int x){ //返回x所在的集合编号 + 路径压缩
        //祖先节点的父节点是自己本身
        if(p[x] != x){
            //将x的父亲置为x父亲的祖先节点,实现路径的压缩
            p[x] = find(p[x]);
        }
        return p[x];
    }

路径压缩的最终目的:让一个集合中的任意元素(用x表示)的p[x]里都存储其集合编号。

此时的集合状态: 

我们从递归和回溯两方面分析:

最终 集合的状态

 2.4问题解决代码

import java.util.Scanner;

public class Main {
    /*一共有 n个数,编号是 1∼n,最开始每个数各自在一个集合中。现在要进行 m
        个操作,操作共有两种:M a b,将编号为 a和 b
        的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
        Q a b,询问编号为 a和 b的两个数是否在同一个集合中;*/
    /*输入格式
    第一行输入整数 n和 m。
    接下来 m行,每行包含一个操作指令,指令为 M a b 或 Q a b 中的一种。*/
    static int N = 100010;
    static int[] p = new int[N];

    //并查集的核心操作,寻找根节点祖先 + 路径压缩
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n = in.nextInt();
        int q = in.nextInt();
        //初始化
        for (int i = 1; i <= n; i++) {
            p[i] = i;
        }
        while (q-- > 0) {
            String s = in.next();
            int a = in.nextInt();
            int b = in.nextInt();
            //合并操作
            if (s.equals("M")) {
                p[find(a)] = find(b);
            } else if (s.equals("Q")) {
                //查找
                if (find(a) == find(b)) {
                    System.out.println("Yes");
                } else {
                    System.out.println("No");
                }
            }
        }
    }

    public static int find(int x){ //返回x所在的集合编号 + 路径压缩
        //祖先节点的父节点是自己本身
        if(p[x] != x){
            //将x的父亲置为x父亲的祖先节点,实现路径的压缩
            p[x] = find(p[x]);
        }
        return p[x];
    }
}

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 b 或 Q2 a 中的一种。

输出格式

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

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

每个结果占一行。

数据范围

1≤n,m≤10^5

与模板题不同处的说明:

与模板题唯一不同的是,我们要多维护数组size来记录该元素的集合中元素的个数。

1.对于数组size只保证根节点的size有意义。

2.初始化时,还要初始化每个集合内的元素数量为1。

初始化的代码:

for (int i = 1; i <= n; i ++ )
{
    p[i] = i;
    size[i] = 1;
}

3.合并a和b所在的两个集合
合并集合操作同时要记得维护集合大小关系

比如集合a合并到集合b里的代码:

p[find(a)] = find(b);
    size[b] += size[a];

代码:

import java.util.Scanner;

public class Main {
    /* 现在要进行 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所在连通块中点的数量 每个结果占一行。*/
    static int N = 100010;
    static int[] p = new int[N];
    static int[] size = new int[N];

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        int m = scanner.nextInt();
        for (int i = 1; i <= n; i++) {
            p[i] = i;
            size[i] = 1;
        }
        while (m-- > 0) {
            String s = scanner.next();
            if (s.equals("C")) {
                int a = scanner.nextInt();
                int b = scanner.nextInt();
                if (find(a) != find(b)) {
                    size[find(b)] += size[find(a)];
                    p[find(a)] = find(b);
                }
            } else if (s.equals("Q1")) {
                int a = scanner.nextInt();
                int b = scanner.nextInt();
                if (find(a) == find(b)) {
                    System.out.println("Yes");
                } else {
                    System.out.println("No");
                }
            } else {
                int a = scanner.nextInt();
                System.out.println(size[find(a)]);
            }
        }
    }

    private static int find(int x) {
        if (x != p[x]) {
            p[x] = find(p[x]);
        }
        return p[x];
    }
}

以上为我个人的小分享,如有问题,欢迎讨论!!! 

都看到这了,不如关注一下,给个免费的赞 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值