并查集详解

本文详细介绍了并查集的概念,包括其基本接口(union,find,count)以及如何通过数组parent[]和size[]实现。重点讲解了优化方法,如平衡性优化和路径压缩,以及在实际问题中的应用实例。
摘要由CSDN通过智能技术生成

并查集

顾名思义,并查集是来解决图的连通性问题

  • Union – 连接两个节点
  • Find – 查找所属的连通分量

所以,并查集主要就是实现以下接口:

class UF {
    /* 将 p 和 q 连接 */
    public void union(int p, int q);
    /* 判断 p 和 q 是否连通 */
    public boolean connected(int p, int q);
    /* 返回图中有多少个连通分量 */
    public int count();
    
    /* 返回当前节点的根节点 */
    private int find(int x);
}

存储数据结构

如何表示节点与节点之间的连通性关系呢??

  • 如果pq连通,则它们有相同的根节点

用数组parent[]来表示这种关系

  • 如果自己就是根节点,那么parent[i] = i,即自己指向自己
  • 如果自己不是根节点,则parent[i] = root id
private int count;
private int[] parent;
// 构造函数
public UF (int n) {
    this.count = n;
    parent = new int[n];
    for (int i = 0; i < n; i++) {
        // 最初,每个节点均是独立的
        parent[i] = i;
    }
}

Union 方法

介绍了存储的数据结构,那如何把两个节点连接起来呢??

很简单,只需将其中任一一个节点的根节点指向另一个节点的根节点即可

在这里插入图片描述

// 伪代码
public void union(int p, int q) {
    // 找到 p 的根节点 rootP
    // 找到 q 的根节点 rootQ
    // 如果已经在同一个连通分中,跳过
    // parent[rootP] = rootQ
    // 或 parent[rootQ] = rootP
}

现在的问题就变成了如何快速找到某一个节点的根节点!!

刚刚介绍数据结构的时候,强调了根节点的特点,即自己指向自己

private int find(int x) {
    while (x != parent[x]) {
        x = parent[x];
    }
    return x;
}

如何,是不是很简单,哈哈哈哈哈哈

connected() && count()

这两个方法的实现很简单

public boolean connected(int p, int q) {
    int rootP = find(p);
    int rootQ = find(q);
    return rootP == rootQ;
}

count()需要维护一个全局变量,来记录图的连通分量的数量

另外,我们需要明确的是:只有在调用union()方法时,才可能改变连通分量的数量

public void union(int p, int q) {
    int rootP = find(p);
    int rootQ = find(q);
    if (rootP == rootQ) return;
    parent[rootP] = rootQ;
    // 连通分量 -1
    count--;
}
public int count() {
    return this.count;
}

瓶颈分析

行文至此,已经把并查集的所有接口实现。但这远远不够,因为此时的代码还不完美,时间复杂度可能会很高

分析上述实现的方法,find()是决定并查集时间复杂度的重要因素。抛开find()因素,其他方法的时间复杂度均可视为O(1)。所以如果要优化算法的时间复杂度,需要从find()入手

对于有 n 个节点 1 个连通分量的并查集来说,最坏的时间复杂度为O(n),最好的时间复杂度为O(1)

  • 最坏情况:全部只有左孩子
  • 最好情况:n - 1 叉树,即根节点有 n - 1 个孩子

优化角度 1:平衡性优化

思路:当我们每次连接两个节点的时候,不希望出现头重脚轻的情况,而希望到达一种平衡的状态

使用额外的一个数组size[]记录每个连通分量中的节点数,每次均把节点数少的分量接到节点数多的分量上,如图
在这里插入图片描述

注意:只有每个连通分量的根节点的 size[] 才可以代表该连通分量中的节点数

private int count;
private int[] parent;
private int[] size;
// 构造函数
public UF (int n) {
    this.count = n;
    parent = new int[n];
    size = new int[n];
    for (int i = 0; i < n; i++) {
        parent[i] = i;
        // 最初,每个连通分量均为 1
        size[i] = 1;
    }
}
public void union(int p, int q) {
    int rootP = find(p);
    int rootQ = find(q);
    if (rootP == rootQ) return;
    /******** 修改部分 ********/
    if (size[rootP] < size[rootQ]) {
        parent[rootP] = rootQ;
        size[rootQ] += size[rootP]
    } else {
        parent[rootQ] = rootP;
        size[rootP] += size[rootQ]
    }
    /********** end **********/
    count--;
}

优化角度 2:路径压缩

思路:使树高始终保持为常数

private int find(int x) {
    while (parent[x] != x) {
        // 进行路径压缩
        parent[x] = parent[parent[x]];
        x = parent[x];
    }
    return x;
}

上面是用迭代实现的「路径压缩」,下面给出一种用递归实现的「路径压缩」,其效率更高!

private int find(int x) {
    if (parent[x] != x) {
        parent[x] = find(parent[x]);
    }
    return parent[x];
}

递归直接一次性把一棵树拉平了!!(强力推荐使用这种方法!!!✨✨✨)

注意:

  • 「路径压缩优化」比「平衡性优化」更为常用
  • 当使用了「路径压缩优化」后,「平衡性优化」可以不使用
  • 但是可以在某些题目中使用「平衡性优化」的思想,最长连续序列

完整模版

class UF {
    private int count;
    private int[] parent;
    private int[] size;
    public UF(int n) {
        this.count = n;
        parent = new int[n];
        size = new int[n];
        for (int i = 0; i < n; i++) {
            parent[i] = i;
            size[i] = 1;
        }
    }
    public void union(int p, int q) {
        int rootP = find(p);
        int rootQ = find(q);
        if (rootP == rootQ) return ;
        // 平衡性优化
        if (size[rootP] < size[rootQ]) {
            parent[rootP] = rootQ;
            size[rootQ] += size[rootP];
        } else {
            parent[rootQ] = rootP;
            size[rootP] += size[rootQ];
        }
        this.count--;
    }
    public boolean connected(int p, int q) {
        int rootP = find(p);
        int rootQ = find(q);
        return rootP == rootQ;
    }
    public int count() {
        return this.count;
    }
    private int find(int x) {
        // 路径压缩
        if (parent[x] != x) {
            parent[x] = find(parent[x]);
        }
        return parent[x];
    }
}

实战题目

题目 1: 被围绕的区域

题目详情可见 被围绕的区域

这个题目可以用「DFS」,也可以使用「并查集」去解决,这篇文章给出并查集的解决方法。想要了解「DFS」的方法,可见 秒杀所有岛屿题目(DFS)

题目 2: 最长连续序列

题目详情可见 最长连续序列

注意:size 别写反了!!!!🩸的教训

亮点

  • 利用Map进行了一个「下标」和「值」的对应
  • 利用Map进行重复元素的排除
  • 利用Map可快速判断当前并查集中已有元素
  • num[i]num[i] - 1 && num[i] + 1相连

题目 3: 操作后的最大异或和

题目详情可见 统计无向图中无法互相到达点对数

作者:LFool⚡
链接:https://leetcode.cn/problems/longest-consecutive-sequence/solutions/1453487/by-lfool-jdy4/
] - 1 && num[i] + 1`相连

题目 3: 操作后的最大异或和

题目详情可见 统计无向图中无法互相到达点对数

作者:LFool⚡
链接:https://leetcode.cn/problems/longest-consecutive-sequence/solutions/1453487/by-lfool-jdy4/
来源:力扣(LeetCode)

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值