10_并查集

news2024/11/6 9:52:44

10_并查集

  • 并查集
    • 并查集结构
    • 并查集API设计
      • UF(int N)构造方法实现
      • union(int p,int q)合并方法实现
      • 代码
  • 并查集应用举例
      • UF_Tree算法优化
        • UF_Tree API设计
        • find(int p)查询方法实现
        • union(int p,int q)合并方法实现
        • 代码
        • 优化后的性能分析
      • 路径压缩
        • UF_Tree_Weighted API设计
        • 代码
      • 案例-畅通工程

并查集

并查集是一种树型的数据结构 ,并查集可以高效地进行如下操作: 查询元素p和元素q是否属于同一组
合并元素p和元素q所在的组

并查集结构

并查集也是一种树型结构,但这棵树跟我们之前讲的二叉树、红黑树、B树等都不一样,这种树的要求比较简单:

  1. 每个元素都唯一的对应一个结点;
  2. 每一组数据中的多个元素都在同一颗树中;
  3. 一个组中的数据对应的树和另外一个组中的数据对应的树之间没有任何联系;
  4. 元素在树中并没有子父级关系的硬性要求;

并查集API设计

类名UF
构造方法UF(int N):初始化并查集,以整数标识(0,N-1)个结点


成员方法

1. public int count():获取当前并查集中的数据有多少个分组
2. public boolean connected(int p,int q):判断并查集中元素p和元素q是否在同一分组中
3.public int find(int p):元素p所在分组的标识符
4.public void union(int p,int q):把p元素所在分组和q元素所在分组合并
成员变量
1. private int[] eleAndGroup: 记录结点元素和该元素所在分组的标识
2. private int count:记录并查集中数据的分组个数

## 并查集的实现

UF(int N)构造方法实现

  1. 初始情况下,每个元素都在一个独立的分组中,所以,初始情况下,并查集中的数据默认分为N个组;
  2. 初始化数组eleAndGroup;
  3. 把eleAndGroup数组的索引看做是每个结点存储的元素,把eleAndGroup数组每个索引处的值看做是该结点 所在的分组,那么初始化情况下,i索引处存储的值就是i

union(int p,int q)合并方法实现

:::info

  1. 如果p和q已经在同一个分组中,则无需合并
  2. 如果p和q不在同一个分组,则只需要将p元素所在组的所有的元素的组标识符修改为q元素所在组的标识符即 可
  3. 分组数量-1
    :::

代码

//并查集代码
public class UF {
    //记录结点元素和该元素所在分组的标识
    private int[] eleAndGroup;
    //记录并查集中数据的分组个数
    private int count;
    //初始化并查集
    public UF(int N){
        //初始情况下,每个元素都在一个独立的分组中,所以,初始情况下,并查集中的数据默认分为N个组
        this.count=N;
        //初始化数组
        eleAndGroup = new int[N];
        //把eleAndGroup数组的索引看做是每个结点存储的元素,把eleAndGroup数组每个索引处的值看做是该
        结点所在的分组,那么初始化情况下,i索引处存储的值就是i
        for (int i = 0; i < N; i++) {
            eleAndGroup[i]=i;
        }
    }
    //获取当前并查集中的数据有多少个分组
    public int count(){
        return count;
    }
    //元素p所在分组的标识符
    public int find(int p){
        return eleAndGroup[p];
    }
    //判断并查集中元素p和元素q是否在同一分组中
    public boolean connected(int p,int q){
        return find(p)==find(q);
    }
    //把p元素所在分组和q元素所在分组合并
    public void union(int p,int q){
        //如果p和q已经在同一个分组中,则无需合并;
        if (connected(p,q)){
            return;
        }
            //如果p和q不在同一个分组,则只需要将p元素所在组的所有的元素的组标识符修改为q元素所在组的标识
            符即可
        int pGroup = find(p);
        int qGroup = find(q);
        for (int i = 0; i < eleAndGroup.length; i++) {
            if (eleAndGroup[i]==pGroup){
                eleAndGroup[i]=qGroup;
            }
        }
        //分组数量-1
        count--;
    }
}
//测试代码
public class Test {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.println("请录入并查集中元素的个数:");
        int N = sc.nextInt();
        UF uf = new UF(N);
        while(true){
            System.out.println("请录入您要合并的第一个点:");
            int p = sc.nextInt();
            System.out.println("请录入您要合并的第二个点:");
            int q = sc.nextInt();
            //判断p和q是否在同一个组
            if (uf.connected(p,q)){
                System.out.println("结点:"+p+"结点"+q+"已经在同一个组");
                continue;
            }
            uf.union(p,q);
            System.out.println("总共还有"+uf.count()+"个分组");
        }
    }
}

并查集应用举例

如果我们并查集存储的每一个整数表示的是一个大型计算机网络中的计算机,则我们就可以通过connected(int p,int q)来检测,该网络中的某两台计算机之间是否连通?如果连通,则他们之间可以通信,如果不连通,则不能通信,此时我们又可以调用union(int p,int q)使得p和q之间连通,这样两台计算机之间就可以通信了。
一般像计算机这样网络型的数据,我们要求网络中的每两个数据之间都是相连通的,也就是说,我们需要调用很多 次union方法,使得网络中所有数据相连,其实我们很容易可以得出,如果要让网络中的数据都相连,则我们至少 要调用N-1次union方法才可以,但由于我们的union方法中使用for循环遍历了所有的元素,所以很明显,我们之前实现的合并算法的时间复杂度是O(N^2),如果要解决大规模问题,它是不合适的,所以我们需要对算法进行优 化。

UF_Tree算法优化

为了提升union算法的性能,我们需要重新设计find方法和union方法的实现,此时我们先需要对我们的之前数据结 构中的eleAndGourp数组的含义进行重新设定:

  1. 我们仍然让eleAndGroup数组的索引作为某个结点的元素;
  2. eleAndGroup[i]的值不再是当前结点所在的分组标识,而是该结点的父结点;

UF_Tree API设计
类名UF_Tree
构造方法UF_Tree(int N):初始化并查集,以整数标识(0,N-1)个结点


成员方法

1. public int count():获取当前并查集中的数据有多少个分组
2. public boolean connected(int p,int q):判断并查集中元素p和元素q是否在同一分组中
3.public int find(int p):元素p所在分组的标识符
4.public void union(int p,int q):把p元素所在分组和q元素所在分组合并
成员变量
1. private int[] eleAndGroup: 记录结点元素和该元素的父结点
2. private int count:记录并查集中数据的分组个数

find(int p)查询方法实现
  1. 判断当前元素p的父结点eleAndGroup[p]是不是自己,如果是自己则证明已经是根结点了;
  2. 如果当前元素p的父结点不是自己,则让p=eleAndGroup[p],继续找父结点的父结点,直到找到根结点为止;

union(int p,int q)合并方法实现
  1. 找到p元素所在树的根结点
  2. 找到q元素所在树的根结点
  3. 如果p和q已经在同一个树中,则无需合并;
  4. 如果p和q不在同一个分组,则只需要将p元素所在树根结点的父结点设置为q元素的根结点即可;
  5. 分组数量-1

代码
package cn.itcast;
public class UF_Tree {
    //记录结点元素和该元素所的父结点
    private int[] eleAndGroup;
    //记录并查集中数据的分组个数
    private int count;
    //初始化并查集
    public UF_Tree(int N){
        //初始情况下,每个元素都在一个独立的分组中,所以,初始情况下,并查集中的数据默认分为N个组
        this.count=N;
        //初始化数组
        eleAndGroup = new int[N];
        //把eleAndGroup数组的索引看做是每个结点存储的元素,把eleAndGroup数组每个索引处的值看做是该
        结点的父结点,那么初始化情况下,i索引处存储的值就是i
        for (int i = 0; i < N; i++) {
            eleAndGroup[i]=i;
        }
    }
    //获取当前并查集中的数据有多少个分组
    public int count(){
        return count;
    }
    //元素p所在分组的标识符
    public int find(int p){
        while(true){
            //判断当前元素p的父结点eleAndGroup[p]是不是自己,如果是自己则证明已经是根结点了;
            if (p==eleAndGroup[p]){
                return p;
            }
                //如果当前元素p的父结点不是自己,则让p=eleAndGroup[p],继续找父结点的父结点,直到找到根
                结点为止;
            p=eleAndGroup[p];
        }
    }
    //判断并查集中元素p和元素q是否在同一分组中
    public boolean connected(int p,int q){
        return find(p)==find(q);
    }
    //把p元素所在分组和q元素所在分组合并
    public void union(int p,int q){
        //找到p元素所在树的根结点
        int pRoot = find(p);
        //找到q元素所在树的根结点
        int qRoot = find(q);
        //如果p和q已经在同一个树中,则无需合并;
        if (pRoot==qRoot){
            return;
        }
        //如果p和q不在同一个分组,则只需要将p元素所在树根结点的父结点设置为q元素的根结点即可;
        eleAndGroup[pRoot]=qRoot;
        //分组数量-1
        count--;
    }
}

优化后的性能分析

我们优化后的算法union,如果要把并查集中所有的数据连通,仍然至少要调用N-1次union方法,但是,我们发现
union方法中已经没有了for循环,所以union算法的时间复杂度由O(N^2)变为了O(N)。
但是这个算法仍然有问题,因为我们之前不仅修改了union算法,还修改了find算法。我们修改前的find算法的时 间复杂度在任何情况下都为O(1),但修改后的find算法在最坏情况下是O(N):

在union方法中调用了find方法,所以在最坏情况下union算法的时间复杂度仍然为O(N^2)。

路径压缩

UF_Tree中最坏情况下union算法的时间复杂度为O(N^2),其最主要的问题在于最坏情况下,树的深度和数组的大 小一样,如果我们能够通过一些算法让合并时,生成的树的深度尽可能的小,就可以优化find方法。
之前我们在union算法中,合并树的时候将任意的一棵树连接到了另外一棵树,这种合并方法是比较暴力的,如果 我们把并查集中每一棵树的大小记录下来,然后在每次合并树的时候,把较小的树连接到较大的树上,就可以减小 树的深度。

只要我们保证每次合并,都能把小树合并到大树上,就能够压缩合并后新树的路径,这样就能提高find方法的效 率。为了完成这个需求,我们需要另外一个数组来记录存储每个根结点对应的树中元素的个数,并且需要一些代码 调整数组中的值。

UF_Tree_Weighted API设计
类名UF_Tree_Weighted
构造方法UF_Tree_Weighted(int N):初始化并查集,以整数标识(0,N-1)个结点


成员方法

1. public int count():获取当前并查集中的数据有多少个分组
2. public boolean connected(int p,int q):判断并查集中元素p和元素q是否在同一分组中
3. public int find(int p):元素p所在分组的标识符
4.public void union(int p,int q):把p元素所在分组和q元素所在分组合并


成员变量
1.private int[] eleAndGroup: 记录结点元素和该元素的父结点2.private int[] sz: 存储每个根结点对应的树中元素的个数3.private int count:记录并查集中数据的分组个数

代码
public class UF_Tree_Weighted {
    //记录结点元素和该元素所的父结点
    private int[] eleAndGroup;
    //存储每个根结点对应的树中元素的个数
    private int[] sz;
    //记录并查集中数据的分组个数
    private int count;
    //初始化并查集
    public UF_Tree_Weighted(int N){
        //初始情况下,每个元素都在一个独立的分组中,所以,初始情况下,并查集中的数据默认分为N个组
        this.count=N;
        //初始化数组
        eleAndGroup = new int[N];
        sz = new int[N];
        //把eleAndGroup数组的索引看做是每个结点存储的元素,把eleAndGroup数组每个索引处的值看做是该
        结点的父结点,那么初始化情况下,i索引处存储的值就是i
        for (int i = 0; i < N; i++) {
            eleAndGroup[i]=i;
        }
        //把sz数组中所有的元素初始化为1,默认情况下,每个结点都是一个独立的树,每个树中只有一个元素
        for (int i = 0; i < sz.length; i++) {
            sz[i]=1;
        }
    }
    //获取当前并查集中的数据有多少个分组
    public int count(){
        return count;
    }
    //元素p所在分组的标识符
    public int find(int p){
        while(true){
            //判断当前元素p的父结点eleAndGroup[p]是不是自己,如果是自己则证明已经是根结点了;
            if (p==eleAndGroup[p]){
                return p;
            }
                //如果当前元素p的父结点不是自己,则让p=eleAndGroup[p],继续找父结点的父结点,直到找到根
                结点为止;
            p=eleAndGroup[p];
        }
    }
    //判断并查集中元素p和元素q是否在同一分组中
    public boolean connected(int p,int q){
        return find(p)==find(q);
    }
    //把p元素所在分组和q元素所在分组合并
    public void union(int p,int q){
        //找到p元素所在树的根结点
        int pRoot = find(p);
        //找到q元素所在树的根结点
        int qRoot = find(q);
        //如果p和q已经在同一个树中,则无需合并;
        if (pRoot==qRoot){
            return;
        }
            //如果p和q不在同一个分组,比较p所在树的元素个数和q所在树的元素个数,把较小的树合并到较大的树if (sz[pRoot]<sz[qRoot]){
            eleAndGroup[pRoot] = qRoot;
            //重新调整较大树的元素个数
            sz[qRoot]+=sz[pRoot];
        }else{
            eleAndGroup[qRoot]=pRoot;
            sz[pRoot]+=sz[qRoot];
        }
        //分组数量-1
        count--;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class UF_Tree_Weighted {
//记录结点元素和该元素所的父结点private int[] eleAndGroup;
//存储每个根结点对应的树中元素的个数
private int[] sz;
//记录并查集中数据的分组个数private int count;
//初始化并查集
public UF_Tree_Weighted(int N){
//初始情况下,每个元素都在一个独立的分组中,所以,初始情况下,并查集中的数据默认分为N个组this.count=N;
//初始化数组
eleAndGroup = new int[N]; sz = new int[N];
//把eleAndGroup数组的索引看做是每个结点存储的元素,把eleAndGroup数组每个索引处的值看做是该
结点的父结点,那么初始化情况下,i索引处存储的值就是i
for (int i = 0; i < N; i++) { eleAndGroup[i]=i;
}
//把sz数组中所有的元素初始化为1,默认情况下,每个结点都是一个独立的树,每个树中只有一个元素
for (int i = 0; i < sz.length; i++) { sz[i]=1;
}
}
//获取当前并查集中的数据有多少个分组
public int count(){ return count;
}

//元素p所在分组的标识符public int find(int p){
while(true){
//判断当前元素p的父结点eleAndGroup[p]是不是自己,如果是自己则证明已经是根结点了;

案例-畅通工程

某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府“畅通工程”的目 标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问 最少还需要建设多少条道路?
在我们的测试数据文件夹中有一个trffic_project.txt文件,它就是诚征道路统计表,下面是对数据的解释:

总共有20个城市,目前已经修改好了7条道路,问还需要修建多少条道路,才能让这20个城市之间全部相通? 解题思路:

  1. 创建一个并查集UF_Tree_Weighted(20);
  2. 分别调用union(0,1),union(6,9),union(3,8),union(5,11),union(2,12),union(6,10),union(4,8),表示已经修建好的道路把对应的城市连接起来;
  3. 如果城市全部连接起来,那么并查集中剩余的分组数目为1,所有的城市都在一个树中,所以,只需要获取当前 并查集中剩余的数目,减去1,就是还需要修建的道路数目;

代码:

public class Traffic_Project {
    public static void main(String[] args)throws Exception {
        //创建输入流
        BufferedReader reader = new BufferedReader(new
                                                   InputStreamReader(Traffic_Project.class.getClassLoader().getResourceAsStream("traffic_projec
                                                                                                                                t.txt")));
        //读取城市数目,初始化并查集
        int number = Integer.parseInt(reader.readLine());
        UF_Tree_Weighted uf = new UF_Tree_Weighted(number);
        //读取已经修建好的道路数目
        int roadNumber = Integer.parseInt(reader.readLine());
        //循环读取已经修建好的道路,并调用union方法
        for (int i = 0; i < roadNumber; i++) {
            String line = reader.readLine();
            int p = Integer.parseInt(line.split(" ")[0]);
            int q = Integer.parseInt(line.split(" ")[1]);
            uf.union(p,q);
        }
        //获取剩余的分组数量
        int groupNumber = uf.count();
        //计算出还需要修建的道路
        System.out.println("还需要修建"+(groupNumber-1)+"道路,城市才能相通");
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1332852.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

10.3 uinput

uinput 简介 uinput 是一个内核驱动&#xff0c;应用程序通过它可以在内核中模拟一个输入设备&#xff0c;其设备文件名是 /dev/uinput 或 /dev/input/uinput。 uinput 使用 使用 uinput 时遵循以下步骤&#xff1a; 通过 open 打开 uinput 设备通过 ioctl 设置属性位图通过…

<avatar: frontiers of pandora>技术overview

https://www.eurogamer.net/digitalfoundry-2023-avatar-frontiers-of-pandora-and-snowdrop-the-big-developer-tech-interview https://www.youtube.com/watch?vLRI_qgVSwMY&t394s 主要来自euro gamer上digital foundry对于avatar的开发团队Massive工作室的采访&#xf…

STM32数码管

分类 按发光二极管单元连接方式可分为共阳极数码管和共阴极数码管。共阳数码管是指将所有发光二极管的阳极接到一起形成公共阳极(COM)的数码管&#xff0c;共阳数码管在应用时应将公共极COM接到5V&#xff0c;当某一字段发光二极管的阴极为低电平时&#xff0c;相应字段就点亮…

截断整型提升算数转换

文章目录 &#x1f680;前言&#x1f680;截断&#x1f680;整型提升✈️整型提升是怎样的 &#x1f680;算术转换 &#x1f680;前言 大家好啊&#xff01;这里阿辉补一下前面操作符遗漏的地方——截断、整型提升和算数转换 看这一篇要先会前面阿辉讲的数据的存储否则可能看不…

SQL之条件判断专题

Case when (case when 情况1 then 结果1 when 情况1 then 结果1 else &#xff0b;剩余结果 end ) 列名 IF表达式 IF(判断内容&#xff0c;0&#xff0c;1) SELECT IF( sex1&#xff0c;男 &#xff0c;女 )sex from student IFNULL表达式 IF(判断内容&#xff0c;x) 假如判…

华为OD机试 - 最少面试官数 - 深度优先搜索dfs(Java 2023 B卷 200分)

目录 专栏导读一、题目描述二、输入描述三、输出描述1、输入2、输出3、说明 四、解题思路1、核心思路&#xff1a;2、具体步骤 五、Java算法源码六、效果展示1、输入按照面试的开始时间升序排序&#xff0c;如果开始时间相同&#xff0c;按照结束时间的升序排序 2、输出3、说明…

KHBC靶场-->打不穿?笑死

最近这不是在上文件上传的课吗&#xff1f;刚好老师也布置了一堆靶场&#xff0c;刚好来挑一个显眼包 没错他就是KHBC靶场&#xff01;&#xff01;&#xff08;看他不顺眼很久了…

智能变电站协议系列-2、SV/SMV协议示例(IEC61850)以及5G专网下的电力方案分析

文章目录 一、前言二、资料准备三、libiec61850的SV运行示例及抓包分析1、单独编译示例程序2、运行示例程序及5G专网场景下部署3、wireshark抓包分析 四、最后 一、前言 之前我们对IEC61850协议有了整体的了解&#xff0c;对一些概念有了一定的认识&#xff0c;并针对GOOSE协议…

c++代码寻找USB00端口并添加打印机

USB00*端口的背景 插入USB端口的打印机&#xff0c;安装打印机驱动&#xff0c;在控制面板设备与打印机处的打印机对象上右击&#xff0c;可以看到打印机端口。对于不少型号&#xff0c;这个端口是USB001或USB002之类的。 经观察&#xff0c;这些USB00*端口并不是打印机驱动所…

每日一题——LeetCode859

方法一 个人方法&#xff1a; 首先s和goal要是长度不一样或者就只有一个字符这两种情况可以直接排除剩下的情况s和goal的长度都是一样的&#xff0c;s的长度为2也是特殊情况&#xff0c;只有s的第一位等于goal的第二位&#xff0c;s的第二位等于goal的第一位才能满足剩下的我们…

tensorboard可视化——No dashboards are active for the current data set.

No dashboards are active for the current data set. 出现问题的原因是事件的路径未用绝对路径&#xff0c;tensorboard --logdir./runs --port6007 改为tensorboard --logdirD:\Code\Python\Study\CL\hat-master\hat-master\run s\one --port6007就好了

Linux操作系统基础(一)系统和软件的安装

Linux操作系统简介 Linux是一种自由和开放源码的类Unix操作系统。该操作系统的内核由芬兰人林纳斯托瓦兹在1991年10月5日首次发布&#xff0c;再加上用户空间的应用程序之后&#xff0c;就成为了Linux操作系统。Linux也是自由软件和开放源代码软件发展中最著名的例子。 Linux…

Tomcat与Netty比较

Tomcat介绍Tomcat支持的协议Tomcat的优缺点Netty介绍Netty支持的协议Netty的优点和缺点Tomcat和Netty的区别Tomcat和Netty的应用场Tomcat和Netty来处理大规模并发连接的优化Tomcat与Netty的网络模型的区别Tomcat与Netty架构设计拓展 Tomcat介绍 Tomcat是一个免费的、开放源代码…

Matlab-修改默认启动路径

Matlab-修改默认启动路径 第一:找到MATLAB的安装路径 第二步&#xff1a;进入到…\toolbox\local下&#xff0c;找到matlabrc.m 第三部&#xff1a;编辑matlabrc.m&#xff0c;在文本最后一行加入启动文件路径

应急响应中的溯源方法

在发现有入侵者后&#xff0c;快速由守转攻&#xff0c;进行精准地溯源反制&#xff0c;收集攻击路径和攻击者身份信息&#xff0c;勾勒出完整的攻击者画像。 对内溯源与对内溯源 对内溯源&#xff1a;确认攻击者的行为 &#xff0c;分析日志 数据包等&#xff1b; 对外溯源&…

Flutter中鼠标 onEnter onExit onHover 实现代码分析

生活会给你任何最有益的经历&#xff0c;以助你意识的演变。 转载请注明出处: 这里对最近用到的一些 Flutter 开源的东西进行总结积累&#xff0c;希望能帮助到大家。 文章目录 背景测试代码flutter 代码onEnter & onExitonHover end 背景 Android设备在使用的时候&#…

3.认识HTML

一、HTML是什么&#xff1f; 超&#xff1a;超链接 二、W3C制定了HTML规范 2014年HTML5正式发布 三、HTML初体验 四、老师常用网站

大数据---35.HBase 常用的api的具体解释

Hbase是一个分布式的、面向列的开源数据库&#xff0c;HDFS文件操作常有两种方式&#xff0c;一种是命令行方式&#xff0c;即Hbase提供了一套与Linux文件命令类似的命令行工具。另一种是JavaAPI&#xff0c;即利用Hbase的Java库&#xff0c;采用编程的方式操作Hbase数据库。 …

红队攻防实战之DC1

如果额头终将刻上皱纹&#xff0c;你只能做到&#xff0c;不让皱纹刻在你的心上 0x01 信息收集: 1.1 端口探测 使用nmap工具 端口扫描结果如下&#xff1a; 由nmap扫描可以知道&#xff0c;目标开放了22,80,111,46204端口&#xff0c;看到端口号22想到ssh远程连接&#xff…

docker部署mysql主主备份 haproxy代理(swarm)

docker部署mysql主主备份 haproxy代理&#xff08;swarm&#xff09; docker部署mysql主主备份 docker部署mysql主主备份&#xff08;keepalived&#xff09;跨主机自动切换 docker部署mysql主主备份 haproxy代理&#xff08;swarm&#xff09; 1. 环境准备 主机IPnode119…