Leetcode刷题笔记66:图论6(并查集理论基础-1971. 寻找图中是否存在路径-684. 冗余连接-685. 冗余连接 II)

88 阅读6分钟

导语

leetcode刷题笔记记录,主要记录题目包括:

知识点

并查集

并查集(Disjoint Set Union,简称 DSU)是一种数据结构,用于管理一组不相交的(disjoint)集合。这个数据结构支持如下几种主要操作:

  1. Union(x, y) :将包含元素 x 和元素 y 的两个集合合并为一个新的集合。
  2. Find(x) :查找包含元素 x 的集合的“代表元素”。这通常用于检查两个元素是否在同一集合中。

并查集的主要应用是解决一些与连接性或者网络有关的问题,例如:

  1. 连通分量:在一个无向图中,使用并查集可以快速地检查任意两个节点是否连通,即是否存在从一个节点到另一个节点的路径。
  2. 最小生成树:Kruskal 算法用于寻找一个图的最小生成树,其中并查集用于维护图中的连通分量。
  3. 等价关系:在一些应用中,你需要知道一组对象中哪些对象是“等价的”(具有相同的性质或关系),并查集可以用于此类问题。

数据结构实现:

通常,一个简单的并查集数据结构可以用一个一维数组(或哈希表)来实现,其中 parent[i] 存储元素 i 的“父元素”。在最开始时,每个元素自己就是一个集合,所以 parent[i] = i

当执行 Union(x, y) 操作时,一般选择其中一个集合的代表元素作为另一个集合的父元素,这样两个集合就合并为一个新的集合了。

Find(x) 操作中,一般从元素 x 出发,不断地找它的父元素,直到找到这个集合的代表元素(即父元素是自己的元素)。

为了优化查找和合并的速度,通常还会使用一些额外的技巧,如路径压缩(Path Compression)和按秩合并(Union by Rank)。

这样,高效实现的并查集的 FindUnion 操作的时间复杂度可以近乎视为常数时间 O(1)。

Python中的并查集实现

class UnionFind:
    def __init__(self, n):
        self.parent = [i for i in range(n)]
        
    def find(self, u):
        if self.parent[u] == u:
            return u
        self.parent[u] = self.find(self.parent[u])
        return self.parent[u]
    
    def union(self, u, v):
        u = self.find(u)
        v = self.find(v)
        if u == v:
            return
        self.parent[u] = v
    
# 使用示例
uf = UnionFind(5)
uf.union(0, 1)
uf.union(1, 2)
print(uf.find(0))  # 输出 0
print(uf.find(1))  # 输出 0
print(uf.find(2))  # 输出 0

Leetcode 1971. 寻找图中是否存在路径

题目描述

有一个具有 n 个顶点的 双向 图,其中每个顶点标记从 0 到 n - 1(包含 0 和 n - 1)。图中的边用一个二维整数数组 edges 表示,其中 edges[i] = [ui, vi] 表示顶点 ui 和顶点 vi 之间的双向边。 每个顶点对由 最多一条 边连接,并且没有顶点存在与自身相连的边。

请你确定是否存在从顶点 source 开始,到顶点 destination 结束的 有效路径 。

给你数组 edges 和整数 nsource 和 destination,如果从 source 到 destination 存在 有效路径 ,则返回 true,否则返回 false 。

 

示例 1:

输入: n = 3, edges = [[0,1],[1,2],[2,0]], source = 0, destination = 2
输出: true
解释: 存在由顶点 0 到顶点 2 的路径:
- 012 
- 02

示例 2:

输入: n = 6, edges = [[0,1],[0,2],[3,5],[5,4],[4,3]], source = 0, destination = 5
输出: false
解释: 不存在由顶点 0 到顶点 5 的路径.

 

提示:

  • 1 <= n <= 2 * 105
  • 0 <= edges.length <= 2 * 105
  • edges[i].length == 2
  • 0 <= ui, vi <= n - 1
  • ui != vi
  • 0 <= source, destination <= n - 1
  • 不存在重复边
  • 不存在指向顶点自身的边

解法

本题目是最基础的并查集实现,只需要将edges中的边构成一个并查集,然后判断起始和终止是否在一个集合中即可。

完整代码如下:

# 定义UnionFind类
class UnionFind:
    def __init__(self, n):
        # 初始化并查集,每个节点的父节点初始时是它自己
        self.parent = [i for i in range(n)]

    # 查找节点u的根节点
    def find(self, u):
        # 如果节点u是根节点,返回u
        if self.parent[u] == u:
            return u
        # 否则,递归地找到u的根节点,并执行路径压缩
        self.parent[u] = self.find(self.parent[u])
        return self.parent[u]

    # 合并两个集合
    def union(self, u, v):
        # 查找u和v的根节点
        u = self.find(u)
        v = self.find(v)
        # 如果u和v已经在同一个集合,无需合并,直接返回
        if u == v:
            return
        # 合并:将u的根节点设置为v
        self.parent[u] = v

# 定义Solution类
class Solution:
    def validPath(self, n: int, edges: List[List[int]], source: int, destination: int) -> bool:
        # 初始化并查集,大小为n
        uf = UnionFind(n)

        # 遍历图中的每条边,进行合并操作
        for u, v in edges:
            uf.union(u, v)

        # 检查source和destination是否在同一个集合
        # 如果是,说明存在从source到destination的路径
        return uf.find(source) == uf.find(destination)

Leetcode 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个节点的树(即:只有一个根节点)。如果有多个答案,则返回二维数组中最后出现的边。

那么我们就可以从前向后遍历每一条边(因为优先让前面的边连上),边的两个节点如果不在同一个集合,就加入集合(即:同一个根节点)。

如图所示:

节点A 和节点 B 不在同一个集合,那么就可以将两个 节点连在一起。(如果题目中说:如果有多个答案,则返回二维数组中最前出现的边。 那我们就要 从后向前遍历每一条边了)如果边的两个节点已经出现在同一个集合里,说明着边的两个节点已经连在一起了,再加入这条边一定就出现环了。

如图所示:

已经判断 节点A 和 节点B 在在同一个集合(同一个根),如果将 节点A 和 节点B 连在一起就一定会出现环。

代码如下:

# 定义一个并查集类 UnionFind
class UnionFind:
    def __init__(self, n):
        # 初始化并查集的父节点数组,数组大小为n+1(因为节点值从1开始)
        self.parent = [i for i in range(n+1)]

    # find函数用于查找节点u的根节点
    def find(self, u):
        # 如果u的父节点就是自己,说明u是根节点,直接返回u
        if self.parent[u] == u:
            return u
        # 如果不是,递归地查找u的根节点,并进行路径压缩
        self.parent[u] = self.find(self.parent[u])
        return self.parent[u]

    # union函数用于合并两个集合
    def union(self, u, v):
        # 分别找到u和v的根节点
        u = self.find(u)
        v = self.find(v)
        # 如果u和v的根节点相同,说明它们已经在同一个集合中,不需要合并
        if u == v:
            return
        # 否则,合并这两个集合。这里直接将u的根节点设置为v
        self.parent[u] = v

# 定义解决方案的类
class Solution:
    def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
        # 初始化一个并查集,集合的大小为边的数量(也就是节点的数量)
        uf = UnionFind(len(edges))

        # 遍历给定的所有边
        for u, v in edges:
            # 如果u和v不在同一个集合中,合并这两个集合
            if uf.find(u) != uf.find(v):
                uf.union(u, v)
            # 如果u和v已经在同一个集合中,那么添加这条边会形成一个环
            # 所以这条边就是多余的,应该被删除
            else:
                return [u, v]

Leetcode 685. 冗余连接 II

题目描述

解法

Leetcode

题目描述

解法