代码随想录第五十五天| 并查集理论基础 寻找存在的路径

并查集理论基础

背景

并查集是一种数据结构,主要用于解决元素的连通性问题。简单来说,当我们需要判断多个元素是否属于同一个集合时,并查集可以高效地完成这一任务。它支持两种基本操作:将两个元素合并到同一个集合中,以及判断两个元素是否属于同一个集合。

原理讲解

并查集的核心思想是通过一个数组来记录每个元素的父节点,从而形成一种树形结构。每个集合用一棵树来表示,树的根节点即为该集合的代表元素。具体来说:

  1. 初始化:每个元素最初都是独立的集合,其父节点指向自身。
  2. 合并操作:将两个元素所在的集合合并。这通过找到它们各自的根节点,然后将其中一个根节点指向另一个根节点来实现。
  3. 查找操作:确定一个元素所属的集合。这通过不断查找该元素的父节点,直到找到根节点为止。

示例

假设我们有元素 A、B、C,初始时每个元素都是独立的集合:

  • father[A] = A
  • father[B] = B
  • father[C] = C

当我们合并 A 和 B 时,将 B 的父节点指向 A:

  • father[B] = A

此时,A 和 B 属于同一个集合。接下来合并 B 和 C:

  • 首先找到 B 的根节点 A,然后将 C 的父节点指向 A
  • father[C] = A

现在,A、B、C 都属于同一个集合,其根节点为 A。

路径压缩

路径压缩是一种优化技术,用于减少查找根节点所需的时间。在查找过程中,将每个访问过的节点直接指向根节点,从而扁平化树的结构。这样,后续的查找操作将更加高效。

实现

在查找函数中,通过递归的方式找到根节点,并在返回时将当前节点的父节点直接设置为根节点:

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

代码模板

以下是并查集的基本代码模板:

class UnionFind {
private:
    vector<int> father;

public:
    UnionFind(int n) {
        father.resize(n);
        for (int i = 0; i < n; ++i) {
            father[i] = i; // 初始化,每个元素的父节点指向自身
        }
    }

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

    void unite(int u, int v) {
        int rootU = find(u);
        int rootV = find(v);
        if (rootU != rootV) {
            father[rootV] = rootU; // 合并两个集合
        }
    }

    bool isConnected(int u, int v) {
        return find(u) == find(v);
    }
};

常见误区

  1. 合并操作的顺序:在合并两个集合时,必须先找到它们的根节点,然后再进行合并。否则可能导致错误的连接关系。
  2. 路径压缩的理解:路径压缩并不是在合并操作中进行,而是在查找操作中完成的。每次查找都会优化树的结构,使得后续操作更快。

拓展

除了路径压缩,并查集还可以通过按秩合并来进一步优化。按秩合并是指在合并两个集合时,将高度较小的树合并到高度较大的树上,从而控制树的高度增长。这通常与路径压缩结合使用,以获得更好的性能。

复杂度分析

  • 时间复杂度:在路径压缩优化下,单次查找操作的均摊时间复杂度接近于 O(1),使得并查集在处理大量元素和操作时非常高效。
  • 空间复杂度:主要取决于存储父节点的数组,空间复杂度为 O(n),其中 n 是元素的数量。

总结

并查集是一种简单而高效的处理动态连通性问题的数据结构。通过路径压缩和按秩合并等优化技术,它能够在接近常数的时间内完成元素的合并和查找操作。理解并查集的核心思想和优化方法,对于解决许多实际问题(如图的连通性、集合的合并等)具有重要意义。

并查集模板

//并查集模板
class DisJoint{
    private int[] father;
 
    public DisJoint(int N) {
        father = new int[N];
        for (int i = 0; i < N; ++i){
            father[i] = i;
        }
    }
 
    public int find(int n) {
        return n == father[n] ? n : (father[n] = find(father[n]));
    }
 
    public void join (int n, int m) {
        n = find(n);
        m = find(m);
        if (n == m) return;
        father[m] = n;
    }
 
    public boolean isSame(int n, int m){
        n = find(n);
        m = find(m);
        return n == m;
    }
 
}

寻找存在的路径

题目描述

给定一个包含 n 个节点的无向图,节点编号从 1 到 n。判断是否存在一条从节点 source 到节点 destination 的路径。

解题思路

并查集算法

并查集算法是一种高效处理图连通性问题的数据结构,适合判断两个节点是否属于同一连通分量。

  1. 初始化:每个节点初始化为独立集合,父节点指向自身。
  2. 合并操作:对于图中的每条边,将两个节点所属的集合合并。
  3. 查找操作:判断两个节点是否属于同一集合,即查找它们的根节点是否相同。

算法步骤

  1. 输入处理:读取节点数 n 和边数 m,以及 m 条边。
  2. 并查集初始化:创建大小为 n+1 的数组(节点编号从 1 到 n)。
  3. 合并边:对每条边的两个节点执行合并操作。
  4. 判断连通性:读取源节点和目标节点,判断它们是否属于同一集合。

代码实现

import java.util.*;

public class Main {
    public static void main(String[] args) {
        int N, M;
        Scanner scanner = new Scanner(System.in);
        N = scanner.nextInt();
        M = scanner.nextInt();
        
        // 创建并查集对象,节点编号从 1 到 N
        DisJoint disJoint = new DisJoint(N + 1);
        
        // 处理每条边,合并节点
        for (int i = 0; i < M; ++i) {
            int u = scanner.nextInt();
            int v = scanner.nextInt();
            disJoint.join(u, v);
        }
        
        // 读取源节点和目标节点
        int source = scanner.nextInt();
        int destination = scanner.nextInt();
        
        // 判断是否连通
        if (disJoint.isSame(source, destination)) {
            System.out.println("1");
        } else {
            System.out.println("0");
        }
    }
}

// 并查集模板
class DisJoint {
    private int[] father;

    // 构造函数,初始化父节点数组
    public DisJoint(int size) {
        father = new int[size];
        for (int i = 0; i < size; ++i) {
            father[i] = i; // 每个节点初始时指向自身
        }
    }

    // 查找根节点,路径压缩
    public int find(int node) {
        if (father[node] != node) {
            father[node] = find(father[node]); // 递归查找并路径压缩
        }
        return father[node];
    }

    // 合并两个节点所在的集合
    public void join(int u, int v) {
        int rootU = find(u);
        int rootV = find(v);
        if (rootU != rootV) {
            father[rootV] = rootU; // 将一个根节点指向另一个
        }
    }

    // 判断两个节点是否属于同一集合
    public boolean isSame(int u, int v) {
        return find(u) == find(v);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值