代码随想录 刷题记录-25 图论 (2)并查集

一、并查集理论基础

背景

首先要知道并查集可以解决什么问题呢?

并查集常用来解决连通性问题。

大白话就是当我们需要判断两个元素是否在同一个集合里的时候,我们就要想到用并查集。

并查集主要有两个功能:

  • 将两个元素添加到一个集合中。
  • 判断两个元素在不在同一个集合

接下来围绕并查集的这两个功能来展开讲解。

原理讲解

从代码层面,我们如何将两个元素添加到同一个集合中呢。

此时有录友会想到:可以把他放到同一个数组里或者set 或者 map 中,这样就表述两个元素在同一个集合。

那么问题来了,对这些元素分门别类,可不止一个集合,可能是很多集合,成百上千,那么要定义这么多个数组吗?

有录友想,那可以定义一个二维数组。

但如果我们要判断两个元素是否在同一个集合里的时候 我们又能怎么办? 只能把而二维数组都遍历一遍。

而且每当想添加一个元素到某集合的时候,依然需要把把二维数组都遍历一遍,才知道要放在哪个集合里。

这仅仅是一个粗略的思路,如果沿着这个思路去实现代码,非常复杂,因为管理集合还需要很多逻辑。

那么我们来换一个思路来看看。

我们将三个元素A,B,C (分别是数字)放在同一个集合,其实就是将三个元素连通在一起,如何连通呢。

只需要用一个一维数组来表示,即:father[A] = B,father[B] = C 这样就表述 A 与 B 与 C连通了(有向连通图)。

代码如下:

// 将v,u 这条边加入并查集
void join(int u, int v) {
    u = find(u); // 寻找u的根
    v = find(v); // 寻找v的根
    if (u == v) return; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
    father[v] = u;
}

可能有录友想,这样我可以知道 A 连通 B,因为 A 是索引下标,根据 father[A]的数值就知道 A 连通 B。那怎么知道 B 连通 A呢?

我们的目的是判断这三个元素是否在同一个集合里,知道 A 连通 B 就已经足够了。

这里要讲到寻根思路,只要 A ,B,C 在同一个根下就是同一个集合。

给出A元素,就可以通过 father[A] = B,father[B] = C,找到根为 C。

给出B元素,就可以通过 father[B] = C,找到根也为为 C,说明 A 和 B 是在同一个集合里。 大家会想第一段代码里find函数是如何实现的呢?其实就是通过数组下标找到数组元素,一层一层寻根过程,代码如下:

// 并查集里寻根的过程
int find(int u) {
    if (u == father[u]) return u; // 如果根就是自己,直接返回
    else return find(father[u]); // 如果根不是自己,就根据数组下标一层一层向下找
}

如何表示 C 也在同一个元素里呢? 我们需要 father[C] = C,即C的根也为C,这样就方便表示 A,B,C 都在同一个集合里了。

所以father数组初始化的时候要 father[i] = i,默认自己指向自己。

代码如下:

// 并查集初始化
void init() {
    for (int i = 0; i < n; ++i) {
        father[i] = i;
    }
}

最后我们如何判断两个元素是否在同一个集合里,如果通过 find函数 找到 两个元素属于同一个根的话,那么这两个元素就是同一个集合,代码如下:

// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
    u = find(u);
    v = find(v);
    return u == v;
}

并查集即一种便于在很多集合情况下,判断两个元素是否属于同一集合、把两个元素加入同一集合的数据结构。

路径压缩

在实现 find 函数的过程中,我们知道,通过递归的方式,不断获取father数组下标对应的数值,最终找到这个集合的根。

搜索过程像是一个多叉树中从叶子到根节点的过程,如图:

如果这棵多叉树高度很深的话,每次find函数 去寻找根的过程就要递归很多次。

我们的目的只需要知道这些节点在同一个根下就可以,所以对这棵多叉树的构造只需要这样就可以了,如图:

除了根节点其他所有节点都挂载根节点下,这样我们在寻根的时候就很快,只需要一步,

如果我们想达到这样的效果,就需要 路径压缩,将非根节点的所有节点直接指向根节点。 那么在代码层面如何实现呢?

我们只需要在递归的过程中,让 father[u] 接住 递归函数 find(father[u]) 的返回结果。

因为 find 函数向上寻找根节点,father[u] 表述 u 的父节点,那么让 father[u] 直接获取 find函数 返回的根节点,这样就让节点 u 的父节点 变成根节点。

代码如下,注意看注释,路径压缩就一行代码:

// 并查集里寻根的过程
int find(int u) {
    if (u == father[u]) return u;
    else return father[u] = find(father[u]); // 路径压缩
}

以上代码在C++中,可以用三元表达式来精简一下,代码如下:

int find(int u) {
    return u == father[u] ? u : father[u] = find(father[u]);
}

代码模板

那么此时并查集的模板就出来了, 整体模板C++代码如下:

int n = 1005; // n根据题目中节点数量而定,一般比节点数量大一点就好
vector<int> father = vector<int> (n, 0); // C++里的一种数组结构

// 并查集初始化
void init() {
    for (int i = 0; i < n; ++i) {
        father[i] = i;
    }
}
// 并查集里寻根的过程
int find(int u) {
    return u == father[u] ? u : father[u] = find(father[u]); // 路径压缩
}

// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
    u = find(u);
    v = find(v);
    return u == v;
}

// 将v->u 这条边加入并查集
void join(int u, int v) {
    u = find(u); // 寻找u的根
    v = find(v); // 寻找v的根
    if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
    father[v] = u;
}

通过模板,我们可以知道,并查集主要有三个功能。

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

Java模板:

public class UnionFind {
    private int[] parent;
    
    public UnionFind(int size) {
        parent = new int[size];
        init();
    }
    
    // 并查集初始化
    private void init() {
        for (int i = 0; i < parent.length; i++) {
            parent[i] = i;
        }
    }
    
    // 并查集里寻根的过程
    public int find(int u) {
        if (u != parent[u]) {
            parent[u] = find(parent[u]); // 路径压缩
        }
        return parent[u];
    }
    
    // 判断 u 和 v 是否找到同一个根
    public boolean isSame(int u, int v) {
        return find(u) == find(v);
    }
    
    // 将 v -> u 这条边加入并查集
    public void join(int u, int v) {
        int rootU = find(u); // 寻找 u 的根
        int rootV = find(v); // 寻找 v 的根
        if (rootU != rootV) { // 如果根不同,合并两个集合
            parent[rootV] = rootU;
        }
    }
    
    public static void main(String[] args) {
        int n = 1005; // 根据实际情况调整
        UnionFind uf = new UnionFind(n);
        
        // 示例: 加入一些连接
        uf.join(1, 2);
        uf.join(2, 3);
        
        // 检查是否在同一个集合
        System.out.println(uf.isSame(1, 3)); // 输出 true
        System.out.println(uf.isSame(1, 4)); // 输出 false
    }
}

常见误区

这里估计有录友会想,模板中的 join 函数里的这段代码:

u = find(u); // 寻找u的根
v = find(v); // 寻找v的根
if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回

与 isSame 函数的实现是不是重复了? 如果抽象一下呢,代码如下:

// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
    u = find(u);
    v = find(v);
    return u == v;
}

// 将v->u 这条边加入并查集
void join(int u, int v) {
    if (isSame(u, v)) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
    father[v] = u;
}

这样写可以吗? 好像看出去没问题,而且代码更精简了。

其实这么写是有问题的,在join函数中 我们需要寻找 u 和 v 的根,然后再进行连线在一起,而不是直接 用 u 和 v 连线在一起。

即,原来的写法有寻根的过程,但是如果直接调用isSame函数,寻根并没有传过来,是直接将u、v连接到了一起

举一个例子:

join(1, 2);
join(3, 2);

此时构成的图是这样的:

此时问 1,3是否在同一个集合,我们调用 join(1, 2); join(3, 2); 很明显本意要表示 1,3是在同一个集合。

但我们来看一下代码逻辑,当我们调用 isSame(1, 3)的时候,find(1) 返回的是1,find(3)返回的是3。 return 1 == 3 返回的是false,代码告诉我们 1 和 3 不在同一个集合,这明显不符合我们的预期,所以问题出在哪里?

问题出在我们精简的代码上,即 join 函数 一定要先 通过find函数寻根再进行关联

如果find函数是这么实现,再来看一下逻辑过程。

void join(int u, int v) {
    u = find(u); // 寻找u的根
    v = find(v); // 寻找v的根
    if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
    father[v] = u;
}

分别将 这两对元素加入集合。

join(1, 2);
join(3, 2);

当执行join(3, 2)的时候,会先通过find函数寻找 3的根为3,2的根为1 (第一个join(1, 2),将2的根设置为1),所以最后是将1 指向 3。

构成的图是这样的:

因为在join函数里,我们有find函数进行寻根的过程,这样就保证元素 1,2,3在这个有向图里是强连通的

此时我们在调用 isSame(1, 3)的时候,find(1) 返回的是3,find(3) 返回的也是3,return 3 == 3 返回的是true,即告诉我们 元素 1 和 元素3 是 在同一个集合里的。

拓展

在「路径压缩」讲解中,我们知道如何靠压缩路径来缩短查询根节点的时间。

其实还有另一种方法:按秩(rank)合并。

rank表示树的高度,即树中结点层次的最大值。

例如两个集合(多叉树)需要合并,如图所示:

树1 rank 为2,树2 rank 为 3。那么合并两个集合,是 树1 合入 树2,还是 树2 合入 树1呢?

我们来看两个不同方式合入的效果。

这里可以看出,树2 合入 树1 会导致整棵树的高度变的更高,而 树1 合入 树2 整棵树的高度 和 树2 保持一致。

所以在 join函数中如何合并两棵树呢?

一定是 rank 小的树合入 到 rank大 的树,这样可以保证最后合成的树rank 最小,降低在树上查询的路径长度。

按秩合并的代码如下:

int n = 1005; // n根据题目中节点数量而定,一般比节点数量大一点就好
vector<int> father = vector<int> (n, 0); // C++里的一种数组结构
vector<int> rank = vector<int> (n, 1); // 初始每棵树的高度都为1

// 并查集初始化
void init() {
    for (int i = 0; i < n; ++i) {
        father[i] = i;
        rank[i] = 1; // 也可以不写
    }
}
// 并查集里寻根的过程
int find(int u) {
    return u == father[u] ? u : find(father[u]);// 注意这里不做路径压缩
}

// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
    u = find(u);
    v = find(v);
    return u == v;
}

// 将v->u 这条边加入并查集
void join(int u, int v) {
    u = find(u); // 寻找u的根
    v = find(v); // 寻找v的根

    if (rank[u] <= rank[v]) father[u] = v; // rank小的树合入到rank大的树
    else father[v] = u;

    if (rank[u] == rank[v] && u != v) rank[v]++; // 如果两棵树高度相同,则v的高度+1,因为上面 if (rank[u] <= rank[v]) father[u] = v; 注意是 <=
}

可以注意到在上面的模板代码中,我是没有做路径压缩的,因为一旦做路径压缩,rank记录的高度就不准了,根据rank来判断如何合并就没有意义。

也可以在 路径压缩的时候,再去实时修生rank的数值,但这样在代码实现上麻烦了不少,关键是收益很小。

其实我们在优化并查集查询效率的时候,只用路径压缩的思路就够了,不仅代码实现精简,而且效率足够高。

按秩合并的思路并没有将树形结构尽可能的扁平化,所以在整理效率上是没有路径压缩高的。

说到这里可能有录友会想,那在路径压缩的代码中,只有查询的过程 即 find 函数的执行过程中会有路径压缩,如果一直没有使用find函数,是不是相当于这棵树就没有路径压缩,导致查询效率依然很低呢?

大家可以再去回顾使用路径压缩的 并查集模板,在isSame函数 和 join函数中,我们都调用了 find 函数来进行寻根操作。

也就是说,无论使用并查集模板里哪一个函数(除了init函数),都会有路径压缩的过程,第二次访问相同节点的时候,这个节点就是直连根节点的,即 第一次访问的时候它的路径就被压缩了。

所以这里推荐大家直接使用路径压缩的并查集模板就好,但按秩合并的优化思路我依然给大家讲清楚,有助于更深一步理解并查集的优化过程。


复杂度分析

这里对路径压缩版并查集来做分析。

空间复杂度: O(n) ,申请一个father数组。

关于时间复杂度,如果想精确表达出来需要繁琐的数学证明,就不在本篇讲解范围内了,大家感兴趣可以自己去深入研究。

这里做一个简单的分析思路。

路径压缩后的并查集时间复杂度在O(logn)与O(1)之间,且随着查询或者合并操作的增加,时间复杂度会越来越趋于O(1)。

了解到这个程度对于求职面试来说就够了。

在第一次查询的时候,相当于是n叉树上从叶子节点到根节点的查询过程,时间复杂度是logn,但路径压缩后,后面的查询操作都是O(1),而 join 函数 和 isSame函数 里涉及的查询操作也是一样的过程。

二、并查集习题

1.107. 寻找存在的路径

思路

本题是并查集基础题目。 

并查集可以解决什么问题呢?

主要就是集合问题,两个节点在不在一个集合,也可以将两个节点添加到一个集合中

这里整理出我的并查集模板如下:

int n = 1005; // n根据题目中节点数量而定,一般比节点数量大一点就好
vector<int> father = vector<int> (n, 0); // C++里的一种数组结构

// 并查集初始化
void init() {
    for (int i = 0; i < n; ++i) {
        father[i] = i;
    }
}
// 并查集里寻根的过程
int find(int u) {
    return u == father[u] ? u : father[u] = find(father[u]); // 路径压缩
}

// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
    u = find(u);
    v = find(v);
    return u == v;
}

// 将v->u 这条边加入并查集
void join(int u, int v) {
    u = find(u); // 寻找u的根
    v = find(v); // 寻找v的根
    if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
    father[v] = u;
}

对应Java模板:

public class UnionFind {
    private int[] parent;
    
    public UnionFind(int size) {
        parent = new int[size];
        init();
    }
    
    // 并查集初始化
    private void init() {
        for (int i = 0; i < parent.length; i++) {
            parent[i] = i;
        }
    }
    
    // 并查集里寻根的过程
    public int find(int u) {
        if (u != parent[u]) {
            parent[u] = find(parent[u]); // 路径压缩
        }
        return parent[u];
    }
    
    // 判断 u 和 v 是否找到同一个根
    public boolean isSame(int u, int v) {
        return find(u) == find(v);
    }
    
    // 将 v -> u 这条边加入并查集
    public void join(int u, int v) {
        int rootU = find(u); // 寻找 u 的根
        int rootV = find(v); // 寻找 v 的根
        if (rootU != rootV) { // 如果根不同,合并两个集合
            parent[rootV] = rootU;
        }
    }
    
    public static void main(String[] args) {
        int n = 1005; // 根据实际情况调整
        UnionFind uf = new UnionFind(n);
        
        // 示例: 加入一些连接
        uf.join(1, 2);
        uf.join(2, 3);
        
        // 检查是否在同一个集合
        System.out.println(uf.isSame(1, 3)); // 输出 true
        System.out.println(uf.isSame(1, 4)); // 输出 false
    }
}

以上模板中,只要修改 n 大小就可以。

并查集主要有三个功能:

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

介绍并查集之后,我们再来看一下这道题目。

为什么说这道题目是并查集基础题目,题目中各个点是双向图链接,那么判断 一个顶点到另一个顶点有没有有效路径其实就是看这两个顶点是否在同一个集合里。

如何算是同一个集合呢,有边连在一起,就算是一个集合。

此时我们就可以直接套用并查集模板。

使用 join(int u, int v)将每条边加入到并查集。

最后 isSame(int u, int v) 判断是否是同一个根 就可以了。

即思路:因为是无向图,可以来一条边,就把这两个节点判定为在同一个集合里,最后加入所有边后,在判断source和destination是不是在同一个集合里就可以判断source和destination是否可达了。

代码如下:

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner scanner =  new Scanner(System.in);
        int n = scanner.nextInt() + 1;
        int m = scanner.nextInt();
        UnionFind unionFind = new UnionFind(n);
        for(int i = 0 ; i < m ; i++){
            unionFind.join(scanner.nextInt(),scanner.nextInt());
        }
        if(unionFind.isSame(scanner.nextInt(),scanner.nextInt())) System.out.println(1);
        else System.out.println(0);
    }

}
class UnionFind {
    private int[] parent;
    public UnionFind(int size){
        parent = new int[size];
        init();
    }
    public void init(){
        for(int i = 0 ; i < parent.length ; i++){
            parent[i] = i;
        }
    }

    public int find(int u){
        if( u != parent[u]){
            parent[u] = find(parent[u]);//路径压缩
        }
        return parent[u];
    }

    public boolean isSame(int u , int v){
        return find(u) == find(v);
    }

    public void join(int u , int v){
        int rootU = find(u);
        int rootV = find(v);
        if(rootU != rootV){
            parent[rootV] = rootU;
        }
    }
}

注意1点:因为题目中的结点编号是从1开始的,所以初始化的unionFind是输入的n再加上1,或者n一开始加上1也可以。

从本题可以看出,并查集适合处理判断在同一个集合的问题及其各种变种。

2.108. 冗余连接

思路

这道题目也是并查集基础题目。

这里我依然降调一下,并查集可以解决什么问题:两个节点是否在一个集合,也可以将两个节点添加到一个集合中。

如果还不了解并查集,可以看这里:并查集理论基础

我们再来看一下这道题目。

题目说是无向图,返回一条可以删去的边,使得结果图是一个有着N个节点的树(即:只有一个根节点)。

如果有多个答案,则返回二维数组中最后出现的边。

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

如图所示:

节点A 和节点 B 不在同一个集合,那么就可以将两个 节点连在一起。

如果边的两个节点已经出现在同一个集合里,说明着边的两个节点已经连在一起了,再加入这条边一定就出现环了。

如图所示:

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

这个思路清晰之后,代码就很好写了。

代码如下:

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner scanner =  new Scanner(System.in);
        int n = scanner.nextInt();;
        UnionFind unionFind = new UnionFind(n+1);
        List<int[]> list = new ArrayList<>();
        for(int i = 0 ; i < n ; i++){
            int node1 = scanner.nextInt();
            int node2 = scanner.nextInt();
            if(!unionFind.isSame(node1,node2)){
                unionFind.join(node1,node2);
            }else{
                list.add(new int[]{node1,node2});
            }
        }
        int[] res = list.get(list.size()-1);
        System.out.println(res[0] + " " + res[1]);
    }

}
class UnionFind {
    private int[] parent;
    public UnionFind(int size){
        parent = new int[size];
        init();
    }
    public void init(){
        for(int i = 0 ; i < parent.length ; i++){
            parent[i] = i;
        }
    }

    public int find(int u){
        if( u != parent[u]){
            parent[u] = find(parent[u]);//路径压缩
        }
        return parent[u];
    }

    public boolean isSame(int u , int v){
        return find(u) == find(v);
    }

    public void join(int u , int v){
        int rootU = find(u);
        int rootV = find(v);
        if(rootU != rootV){
            parent[rootV] = rootU;
        }
    }
}

这道题思路有一个关键的一点:如果edge两个节点已经在一个集合里了,那么这条edge必将导致图成环。因此只要得到最后一条这样的edge就可以了。(这其中判断是否在一个集合里需要借助并查集)

3.109. 冗余连接II

思路

本题与 108.冗余连接 类似,但本题是一个有向图,有向图相对要复杂一些。

本题的本质是 :有一个有向图,是由一颗有向树 + 一条有向边组成的 (所以此时这个图就不能称之为有向树),现在让我们找到那条边 把这条边删了,让这个图恢复为有向树。

有向树的性质,如果是有向树的话,只有根节点入度为0,其他节点入度都为1(因为该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点)。

如果是有向图的话,则可能会出现有:(1)存在有2个入度及以上的结点 (2)存在一个环,但是没有2个入度及以上的结点。

对于第一种情况,有以下两种可能:

情况一:如果我们找到入度为2的点,那么删一条指向该节点的边就行了。

如图:

找到了节点3 的入度为2,删 1 -> 3 或者 2 -> 3 。选择删顺序靠后便可。

但 入度为2 还有一种情况,情况二,只能删特定的一条边,如图:

节点3 的入度为 2,但在删除边的时候,只能删 这条边(节点1 -> 节点3),如果删这条边(节点4 -> 节点3),那么删后本图也不是有向树了(因为找不到根节点)。

总结这两种情况,只需要判断对于入度为2的节点所具有的边,删除后是否构成有向树即可(通过并查集判断)

对于第二种情况,存在环,但是没有入度为2的结点,这时候顺序检查每一条边,对构成环的最后一条边删除即可:

整体思路如下:

(1)统计所有边和对应节点的入度,对入度为2的结点所具有的边作删除后是否为有向树的判断。

(2)如果1得不到答案,则判断该图中成环的边。

代码如下:

import java.util.*;

public class Main {

    static UnionFind unionFind;
    static List<int[]> edges = new ArrayList<>();//记录所有的边

    public static void main(String[] args) {
        int[] res = null;
        Scanner scanner =  new Scanner(System.in);
        int n = scanner.nextInt();
        unionFind = new UnionFind(n+1);
        int[] inDegree = new int[n+1];//记录每个节点的入度
        for(int i = 0 ; i < n ; i++){
            int from = scanner.nextInt();
            int to = scanner.nextInt();
            inDegree[to] ++;
            edges.add(new int[]{from,to});
        }
        //统计是否有入度>=2的节点,存储进入它的边
        List<Integer> deleteEdges = new ArrayList<>(); //deleteEdges存放要删除的边在Edges里对应的序号
        //要输出最后一条符合的边,倒序记录,这样正序遍历第一个符合要求的即为答案
        for(int i = n-1 ; i >= 0 ; i--){
            if(inDegree[edges.get(i)[1]] >= 2){
                deleteEdges.add(i);
            }
        }
        //存在入度>=2 的节点
        if(!deleteEdges.isEmpty()){
            for(int i = 0 ; i < deleteEdges.size() ; i++){
                if(isTreeAfterDelete(deleteEdges.get(i))){
                    res = edges.get(deleteEdges.get(i));
                    break;
                }
            }
        }else{
            //不存在入度 >= 2 的结点,找到edges里最后一条成环的边即可
            res =getRemoveEdge();
        }
        System.out.println(res[0] + " " + res[1]);

    }

    public static boolean isTreeAfterDelete(int deleted){
        unionFind.init();
        for(int i = 0 ; i < edges.size() ; i++){
            if(i == deleted) continue;
            if(unionFind.isSame(edges.get(i)[0],edges.get(i)[1])){
                return false;
            }else{
                unionFind.join(edges.get(i)[0],edges.get(i)[1]);
            }
        }
        return true;
    }

    public static int[] getRemoveEdge(){
        //没有入度为2的结点
        unionFind.init();
        for(int i = 0 ; i < edges.size() ; i++){
            if(unionFind.isSame(edges.get(i)[0],edges.get(i)[1])){
                return edges.get(i);
            }else{
                unionFind.join(edges.get(i)[0],edges.get(i)[1]);
            }
        }
        return new int[]{-1,-1};
    }

}
class UnionFind {
    private int[] parent;
    public UnionFind(int size){
        parent = new int[size];
        init();
    }
    public void init(){
        for(int i = 0 ; i < parent.length ; i++){
            parent[i] = i;
        }
    }

    public int find(int u){
        if( u != parent[u]){
            parent[u] = find(parent[u]);//路径压缩
        }
        return parent[u];
    }

    public boolean isSame(int u , int v){
        return find(u) == find(v);
    }

    public void join(int u , int v){
        int rootU = find(u);
        int rootV = find(v);
        if(rootU != rootV){
            parent[rootV] = rootU;
        }
    }
}

  • 7
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值