几分钟搞懂并查集

简介: 本文主要讲解并查集

一、概念

什么是并查集?

并查集是一种用来管理元素分组情况的数据结构。并查集可以高效地查询两个元素是否在同一个集合、合并两个不同的集合。
不过需要注意并查集虽然可以进行合并操作,但是却无法进行分割操作。

初始化:

在初始化并查集时,每个元素都是单独的一个集合
在这里插入图片描述

void init() {
    for (int i = 0; i < n; ++i) {
        father[i] = i;
    }
}

查找:

查找两个元素是否属于同一个集合就是查找两个元素是否在同一个树上
这里采用了路径压缩
在这里插入图片描述

int find(int u) {
    if(fater[u] != u) father[u] = find(father[u]);
    return father[u];
}

合并:

我们将两个集合看作两个树,合并存在于两个树的元素时,将另一颗树的根连到另一个树上。

在这里插入图片描述

void join(int u, int v) {
    u = find(u);    
    v = find(v);
    if (u == v) return ;
    father[v] = u;
}

合并时有可能会存在链路过长,我们可以用一个rank[]数组记录每个树的高度,然后将矮树联合到高树上。(按秩合并)
在这里插入图片描述

//按秩合并初始化
void init() {
    for (int i = 0; i < n; ++i) {
        father[i] = i;
        rank[i] = 0;
    }
}
void join(int u, int v) {
    u = find(u);    
    v = find(v);
    if (u == v) return ;
    if(rank[u] < rank[v]) {
        father[u] = v;
    } else {
        father[v] = u;
        if(rank[u] == rank[v]) {
            rank[u] ++;
        }
    }
}

二、模板

int n = 1005; // 节点数量3 到 1000
int father[1005];

// 并查集初始化
void init() {
    for (int i = 0; i < n; ++i) {
        father[i] = i;
    }
}
// 并查集里寻根的过程
int find(int u) {
    if(fater[u] != u) father[u] = find(father[u]);
    return father[u];
}
// 将v->u 这条边加入并查集
void join(int u, int v) {
    u = find(u);
    v = find(v);
    if (u == v) return ;
    father[v] = u;
}
// 判断 u 和 v是否找到同一个根
bool same(int u, int v) {
    u = find(u);
    v = find(v);
    return u == v;
}

并查集主要有三个功能。

  1. 寻找根节点,函数:find(int u),也就是判断这个节点的祖先节点是哪个
  2. 将两个节点接入到同一个集合,函数:join(int u, int v),将两个节点连在同一个根节点上
  3. 判断两个节点是否在同一个集合,函数:same(int u, int v),就是判断两个节点是不是同一个根节点

注:

合并:

在这里插入图片描述

路径压缩:
在这里插入图片描述

三、例题

题:684. 冗余连接

树可以看成是一个连通且 无环无向 图。

给定往一棵 n 个节点 (节点值 1~n) 的树中添加一条边后的图。添加的边的两个顶点包含在 1 到 n 中间,且这条附加的边不属于树中已存在的边。图的信息记录于长度为 n 的二维数组 edges ,edges[i] = [ai, bi] 表示图中在 ai 和 bi 之间存在一条边。

请找出一条可以删去的边,删除后可使得剩余部分是一个有着 n 个节点的树。如果有多个答案,则返回数组 edges 中最后出现的边。

示例 1:
在这里插入图片描述

输入: edges = [[1,2], [1,3], [2,3]]
输出: [2,3]

示例 2:
在这里插入图片描述

输入: edges = [[1,2], [2,3], [3,4], [1,4], [1,5]]
输出: [1,4]

提示:

n == edges.length
3 <= n <= 1000
edges[i].length == 2
1 <= ai < bi <= edges.length
ai != bi
edges 中无重复元素
给定的图是连通的 

解:

解题思路:

题目大意是给定一个无向图,删除一条边,使得结果图是一个有n个节点的树(其实就是找环,删边)

  1. 遍历每一条边,如果边的两个节点不在同一个集合中,就加入集合。
  2. 如果边的两个节点已经出现在集合中了,说明如果着边的两个节点已经连在一起了,如果在加入这条边一定会出现环。

AC代码:

class Solution {
    int n = 1005; // 节点数量从3-1000
    int father[] = new int[1005];
    // 并查集初始化
    void init() {
        for(int i = 0; i < n; ++ i) father[i] = i;
    }
    // 并查集寻根
    int find(int u) {
        if(u == father[u]) {
            return u;
        }
        father[u] = find(father[u]);
        return father[u];    
    }
    // 将 v -> u 这条边加入并查集
    void join(int u, int v) {
        u = find(u);
        v = find(v);
        if(u != v) father[v] = u; 
    }
    // 判断 u 和 v 是否找到同一个根
    boolean isSame(int u, int v) {
        return find(u) == find(v);
    }
    public int[] findRedundantConnection(int[][] edges) {
        init();
        for(int i = 0; i < edges.length; ++ i) {
            if(isSame(edges[i][0], edges[i][1])) return edges[i];
            else join(edges[i][0], edges[i][1]);
        }
        return new int[]{};
    }
    
}

题:685. 冗余连接 II

在本问题中,有根树指满足以下条件的 有向 图。该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。

输入一个有向图,该图由一个有着 n 个节点(节点值不重复,从 1 到 n)的树及一条附加的有向边构成。附加的边包含在 1 到 n 中的两个不同顶点间,这条附加的边不属于树中已存在的边。

结果图是一个以边组成的二维数组 edges 。 每个元素是一对 [ui, vi],用以表示 有向 图中连接顶点 ui 和顶点 vi 的边,其中 ui 是 vi 的一个父节点。

返回一条能删除的边,使得剩下的图是有 n 个节点的有根树。若有多个答案,返回最后出现在给定二维数组的答案。

示例 1:
在这里插入图片描述

输入:edges = [[1,2],[1,3],[2,3]]
输出:[2,3]

示例 2:
在这里插入图片描述

输入:edges = [[1,2],[2,3],[3,4],[4,1],[1,5]]
输出:[4,1]

提示:

n == edges.length
3 <= n <= 1000
edges[i].length == 2
1 <= ui, vi <= n

解:

解题思路:

树区别与图的特点是:没有环(不论是对于有向边还是无向边)

有根树的特点:

  • 只有唯一的一个入度为 00 的结点,它是根结点;
  • 不是根结点的其它所有的结点入度为 11;
  • 不可能存在入度为 22 的结点。

在这里插入图片描述

AC代码:

class Solution {
    int N = 1005;
    int[] parent = new int[N];
    // 1.有入度为2(删边检查树) 2.没有入度为2(检测环)
    public int[] findRedundantDirectedConnection(int[][] edges) {
        // 1.入度统计
        int len = edges.length;
        int[] inDegree = new int[N];
        for(int i = 0; i < len; ++ i) {
            inDegree[edges[i][1]] ++;
        }
        // 2.找入度为2的点,优先要后边的节点,倒序遍历
        List<Integer> twoDegree = new ArrayList<>();
        for(int i = len - 1; i >= 0; -- i) {
            if(inDegree[edges[i][1]] == 2) {
                twoDegree.add(i);
            }
        }
        // 存在入度为2的点,一定是两条边删一条
        if(!twoDegree.isEmpty()) {
            if(isTreeAfterRemoveEdge(edges, twoDegree.get(0))) {
                return edges[twoDegree.get(0)];
            }
            return edges[twoDegree.get(1)];
        }
        return getRemoveEdge(edges);
    }
    // 删除一条边后看是不是树
    boolean isTreeAfterRemoveEdge(int[][] edges, int deleteEdge) {
        init();
        for(int i = 0; i < edges.length; ++ i) {
            if(i == deleteEdge) continue;
            if(find(edges[i][0]) == find(edges[i][1])) { // 构成有向环
                return false;
            }
            join(edges[i][0], edges[i][1]);
        }
        return true;    
    }
    // 在有向图里找到要删除的边,使其变成树
    int[] getRemoveEdge(int[][] edges) {
        init();
        for(int i = 0; i < edges.length; ++ i) {
            if(find(edges[i][0]) == find(edges[i][1])) {
                return edges[i];
            }
            join(edges[i][0], edges[i][1]);
        }
        return new int[] {};
    }
    // 初始化并查集
    void init() {
        for(int i = 0; i < N; ++ i) {
            parent[i] = i;
        }
    }
    // 并查集寻根
    int find(int u) {
        if(u == parent[u]) {
            return u;
        }
        parent[u] = find(parent[u]);
        return parent[u];
    }
    // 将 v -> u 加入并查集
    void join(int u, int v) {
        u = find(u);
        v = find(v);
        if(u == v) return;
        parent[v] = u; 
    }
}
相关文章
|
11月前
|
Java API 调度
Java 日期与时间处理:精准掌控时间流转
Java 8引入了全新的日期和时间API,解决了旧版`java.util.Date`和`Calendar`类设计不佳、操作繁琐的问题。新API包括`LocalDate`、`LocalTime`和`LocalDateTime`类,操作简洁直观,符合日常思维习惯。同时提供了`Period`和`Duration`处理时间间隔,以及`DateTimeFormatter`进行格式化输出。这些改进使开发者能更高效、准确地处理日期和时间,极大提升了开发效率与代码质量。 (239字符)
249 6
|
8月前
|
文字识别 网络协议 开发工具
GitHub封锁?推荐5个国产的Git仓库替代平台
近日,GitHub对中国区IP的部分限制引发了广泛关注。未登录用户被拒,已登录用户功能受限,南北网络环境差异更显“内卷”。为应对这一挑战,本文推荐了多个国产Git平台:Gitee(码云)、GitCode(CSDN旗下)、CODING(腾讯系)、CodeUP(阿里云支持)及微信代码管理工具。这些平台功能全面、稳定性强,是开发者迁移项目的理想选择。通过同步代码、配置CI/CD流水线等简单步骤,可确保项目平稳过渡。此次事件提醒我们,掌握核心技能与支持国产平台同样重要!
5724 11
|
11月前
|
消息中间件 NoSQL 架构师
招行面试:亿级秒杀,超卖问题+少卖问题,如何解决?(图解+秒懂+史上最全)
45岁资深架构师尼恩在读者交流群中分享了如何系统化解决高并发下的库存抢购超卖少买问题,特别是针对一线互联网企业的面试题。文章详细解析了秒杀系统的四个阶段(扣库预扣、库存扣减、支付回调、库存补偿),并通过Redis分布式锁和Java代码示例展示了如何防止超卖。此外,还介绍了使用RocketMQ延迟消息和xxl-job定时任务解决少卖问题的方法。尼恩强调,掌握这些技术不仅能提升面试表现,还能增强实际项目中的高并发处理能力。相关答案已收入《尼恩Java面试宝典PDF》V175版本,供后续参考。
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
329 5
|
10月前
|
计算机视觉
YOLOv11改进策略【卷积层】| CVPR-2024 利用DynamicConv 动态卷积 结合C3k2进行二次创新,提高精度
YOLOv11改进策略【卷积层】| CVPR-2024 利用DynamicConv 动态卷积 结合C3k2进行二次创新,提高精度
766 0
|
计算机视觉
目标检测笔记(二):测试YOLOv5各模块的推理速度
这篇文章是关于如何测试YOLOv5中不同模块(如SPP和SPPF)的推理速度,并通过代码示例展示了如何进行性能分析。
552 3
|
存储 算法 文件存储
详细解读7z文件格式及其源码的分析(三)
详细解读7z文件格式及其源码的分析(三)
668 0
|
算法 安全 Java
三种方法教你实现多线程交替打印ABC,干货满满!
本文介绍了多线程编程中的经典问题——多线程交替打印ABC。通过三种方法实现:使用`wait()`和`notify()`、`ReentrantLock`与`Condition`、以及`Semaphore`。每种方法详细讲解了实现步骤和代码示例,帮助读者理解和掌握线程间的同步与互斥,有效解决并发问题。适合不同层次的开发者学习参考。
852 11
|
设计模式 算法 开发者
深入理解工厂模式与策略模式:设计模式的灵活应用
深入理解工厂模式与策略模式:设计模式的灵活应用
|
小程序 Java 数据管理
Java智慧校园-中小学校园管理系统源码
数据中心 设备管理系统:智慧校园平台用户数据信息统一管理;实现系统之间的区域、教室、设备资产等信息同步;实现优化集成配置、统一设备管控。
363 1