55.【必备】并查集-上

本文的网课内容学习自B站左程云老师的算法详解课程,旨在对其中的知识进行整理和分享~

网课链接:算法讲解056【必备】并查集-上_哔哩哔哩_bilibili

一.并查集模板(路径压缩+小挂大)

题目:并查集的实现

算法原理

  • 并查集基础概念
    • 并查集主要用于处理不相交集合的合并与查询问题。
    • 在这个代码中,每个元素最初都属于自己单独的集合,然后通过合并操作将一些集合合并在一起,并且可以查询两个元素是否在同一个集合中。
  • 数据结构部分(build函数)
    • father数组
      • 用于存储每个元素的父节点,初始时每个元素的父节点是它自己,即father[i]=i。这表示每个元素都是一个单独集合的代表。
      • 随着合并操作的进行,father数组会不断更新,使得同一集合中的元素最终指向同一个代表元素。
    • size数组
      • 用于存储每个集合的大小。初始时每个集合只有一个元素,所以size[i]=1
      • 在合并操作时,会根据集合大小来决定合并的方式,以保持树形结构相对平衡,提高后续查询操作的效率。
    • stack数组
      • 这是一个辅助数组,用于在查找操作(find函数)中的路径压缩过程。
  • 查找操作(find函数)
    • 目的是找到给定元素所在集合的代表元素。
    • 从给定元素i开始,沿着father指针不断向上查找,直到找到一个元素j使得father[j]=j,这个j就是i所在集合的代表元素。
    • 在向上查找过程中,将经过的元素存储到stack数组中,记录经过的元素个数size
    • 找到代表元素后,再将stack数组中的元素的father指针直接指向代表元素,实现路径压缩。路径压缩可以减少后续查找操作的时间复杂度,使得查找操作接近常数时间。
  • 判断是否在同一集合(isSameSet函数)
    • 通过调用find函数分别找到元素xy的代表元素。
    • 如果这两个代表元素相同,就说明xy在同一个集合中,返回true(输出"Yes");否则,返回false(输出"No")。
  • 合并操作(union函数)
    • 首先调用find函数找到元素xy的代表元素fxfy
    • 如果fxfy不相等,说明xy不在同一个集合中,需要进行合并操作。
    • 比较fxfy所代表集合的大小(通过size数组)。
    • 如果size[fx]>=size[fy],则将fy所在集合合并到fx所在集合中,即将fy的父节点设为fxfather[fy]=fx),并且更新fx所在集合的大小(size[fx]+=size[fy]);否则,将fx所在集合合并到fy所在集合中,相应地更新fathersize数组。

代码实现

// 并查集模版(牛客)
// 路径压缩 + 小挂大
// 测试链接 : https://www.nowcoder.com/practice/e7ed657974934a30b2010046536a5372
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的code,提交时请把类名改成"Main",可以直接通过

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;

public class Code01_UnionFindNowCoder {

    public static int MAXN = 1000001;

    public static int[] father = new int[MAXN];

    public static int[] size = new int[MAXN];

    public static int[] stack = new int[MAXN];

    public static int n;

    public static void build() {
        for (int i = 0; i <= n; i++) {
            father[i] = i;
            size[i] = 1;
        }
    }

    // i号节点,往上一直找,找到代表节点返回!
    public static int find(int i) {
        // 沿途收集了几个点
        int size = 0;
        while (i != father[i]) {
            stack[size++] = i;
            i = father[i];
        }
        // 沿途节点收集好了,i已经跳到代表节点了
        while (size > 0) {
            father[stack[--size]] = i;
        }
        return i;
    }

    public static boolean isSameSet(int x, int y) {
        return find(x) == find(y);
    }

    public static void union(int x, int y) {
        int fx = find(x);
        int fy = find(y);
        if (fx != fy) {
            // fx是集合的代表:拿大小
            // fy是集合的代表:拿大小
            if (size[fx] >= size[fy]) {
                size[fx] += size[fy];
                father[fy] = fx;
            } else {
                size[fy] += size[fx];
                father[fx] = fy;
            }
        }
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StreamTokenizer in = new StreamTokenizer(br);
        PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
        while (in.nextToken() != StreamTokenizer.TT_EOF) {
            n = (int) in.nval;
            build();
            in.nextToken();
            int m = (int) in.nval;
            for (int i = 0; i < m; i++) {
                in.nextToken();
                int op = (int) in.nval;
                in.nextToken();
                int x = (int) in.nval;
                in.nextToken();
                int y = (int) in.nval;
                if (op == 1) {
                    out.println(isSameSet(x, y) ? "Yes" : "No");
                } else {
                    union(x, y);
                }
            }
        }
        out.flush();
        out.close();
        br.close();
    }

}

二.并查集模板(只有路径压缩)

题目:【模板】并查集

算法原理

  • 基础概念与数据结构
    • 并查集用于处理不相交集合的合并与查询问题。
    • father数组
      • 这个数组用于存储每个元素的父节点。初始时,在build函数中,每个元素的父节点被设置为它自己,即father[i]=i,表示每个元素是一个单独集合的初始代表。
  • 查找操作(find函数)
    • 这个函数的目的是找到元素所在集合的代表元素。
    • 采用递归的方式实现路径压缩。当i!=father[i]时,先递归调用find函数找到father[i]的代表元素,然后将father[i]直接指向这个代表元素。这样做的好处是在后续查询中,可以减少查找的深度,提高查询效率。最后返回找到的代表元素father[i]
  • 判断是否在同一集合(isSameSet函数)
    • 分别调用find函数找到元素xy的代表元素。
    • 如果这两个代表元素相同,说明xy在同一个集合中,返回true(输出"Y");否则,返回false(输出"N")。
  • 合并操作(union函数)
    • 直接将x所在集合的代表元素(通过find(x)找到)的父节点设置为y所在集合的代表元素(通过find(y)找到)。这里没有像之前的代码那样进行小挂大的优化,在数据规模不是非常大或者对效率要求不是极高的情况下,这种简单的合并操作也是可行的。

代码实现

// 并查集模版(洛谷)
// 本实现用递归函数实现路径压缩,而且省掉了小挂大的优化,一般情况下可以省略
// 测试链接 : https://www.luogu.com.cn/problem/P3367
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的code,提交时请把类名改成"Main",可以直接通过

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;

public class Code02_UnionFindLuogu {

    public static int MAXN = 200001;

    public static int[] father = new int[MAXN];

    public static int n;

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

    public static int find(int i) {
        if (i != father[i]) {
            father[i] = find(father[i]);
        }
        return father[i];
    }

    public static boolean isSameSet(int x, int y) {
        return find(x) == find(y);
    }

    public static void union(int x, int y) {
        father[find(x)] = find(y);
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StreamTokenizer in = new StreamTokenizer(br);
        PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
        while (in.nextToken() != StreamTokenizer.TT_EOF) {
            n = (int) in.nval;
            build();
            in.nextToken();
            int m = (int) in.nval;
            for (int i = 0; i < m; i++) {
                in.nextToken();
                int z = (int) in.nval;
                in.nextToken();
                int x = (int) in.nval;
                in.nextToken();
                int y = (int) in.nval;
                if (z == 1) {
                    union(x, y);
                } else {
                    out.println(isSameSet(x, y) ? "Y" : "N");
                }
            }
        }
        out.flush();
        out.close();
        br.close();
    }

}

三.情侣牵手

题目:情侣牵手

算法原理

  • 数据结构部分
    • father数组:用于构建并查集的数据结构,表示元素之间的关系。这里father[i]初始表示i的父节点,在初始构建(build函数)时,每个元素的父节点是它自己,即每个元素是一个单独集合的代表。
    • sets变量:用于记录当前集合的数量。初始时,集合数量为n / 2,因为有n / 2对情侣,即n / 2个初始独立的集合。
  • 构建并查集(build函数)
    • 对于给定的m(这里m = n / 2),函数遍历从0m - 1的每个元素,将其father设置为自己,表示每个元素是一个单独的集合,并且将sets初始化为m,代表初始的集合数量。
  • 查找操作(find函数)
    • 目的是找到给定元素所在集合的代表元素。
    • 如果i不等于father[i],说明i不是所在集合的代表元素,通过递归调用find函数找到father[i]的代表元素,然后将father[i]直接指向这个代表元素,实现路径压缩,以提高后续查询效率。最后返回找到的代表元素father[i]
  • 合并操作(union函数)
    • 首先通过find函数找到xy的代表元素fxfy
    • 如果fxfy不相等,说明xy属于不同的集合,此时将fx的父节点设置为fy,表示将x所在的集合合并到y所在的集合中,并且集合数量sets减1,因为合并后集合数量减少了一个。
  • 主函数(minSwapsCouples)
    • 首先确定情侣对数n = row.length,然后构建并查集(build(n / 2))。
    • 接着遍历座位数组row,每两个座位一组(ii + 1),将对应的情侣编号(row[i] / 2row[i + 1] / 2)进行合并操作(union)。
    • 最后,最少交换次数等于初始情侣对数(n / 2)减去最终的集合数量(sets)。这是因为每成功合并一对情侣(减少一个集合),就意味着减少了一次交换的需求,最终剩下的集合数量就是没有配对成功的情侣对数,用总的情侣对数减去它就是最少交换次数。

代码实现

// 情侣牵手
// n对情侣坐在连续排列的 2n 个座位上,想要牵到对方的手
// 人和座位由一个整数数组 row 表示,其中 row[i] 是坐在第 i 个座位上的人的ID
// 情侣们按顺序编号,第一对是 (0, 1),第二对是 (2, 3),以此类推,最后一对是 (2n-2, 2n-1)
// 返回 最少交换座位的次数,以便每对情侣可以并肩坐在一起
// 每次交换可选择任意两人,让他们站起来交换座位
// 测试链接 : https://leetcode.cn/problems/couples-holding-hands/
public class Code03_CouplesHoldingHands {

    public static int minSwapsCouples(int[] row) {
        int n = row.length;
        build(n / 2);
        for (int i = 0; i < n; i += 2) {
            union(row[i] / 2, row[i + 1] / 2);
        }
        return n / 2 - sets;
    }

    public static int MAXN = 31;

    public static int[] father = new int[MAXN];

    public static int sets;

    public static void build(int m) {
        for (int i = 0; i < m; i++) {
            father[i] = i;
        }
        sets = m;
    }

    public static int find(int i) {
        if (i != father[i]) {
            father[i] = find(father[i]);
        }
        return father[i];
    }

    public static void union(int x, int y) {
        int fx = find(x);
        int fy = find(y);
        if (fx != fy) {
            father[fx] = fy;
            sets--;
        }
    }

}

四.相似字符串组

题目:相似字符串组

算法原理

  • 数据结构
    • father数组:用于构建并查集,father[i]表示第i个字符串在并查集中的父节点,初始时每个字符串的父节点是它自己,即每个字符串是一个单独集合的代表。
    • sets变量:用于记录当前的集合数量,初始化为字符串的总数n
  • 构建并查集(build函数)
    • 对于给定的字符串数量n,函数遍历从0n - 1的每个索引。
    • father[i]设置为i,表示每个字符串初始为独立的集合,并且将sets设置为n,代表初始有n个集合。
  • 查找操作(find函数)
    • 查找操作的目的是找到给定字符串索引i所在集合的代表元素。
    • 如果i不等于father[i],说明i不是所在集合的代表元素,通过递归调用find函数找到father[i]的代表元素,然后将father[i]直接指向这个代表元素,实现路径压缩,这有助于提高后续查找操作的效率。最后返回找到的代表元素father[i]
  • 合并操作(union函数)
    • 首先通过find函数找到xy的代表元素fxfy
    • 如果fxfy不相等,说明xy属于不同的集合,此时将fx的父节点设置为fy,表示将x所在的集合合并到y所在的集合中,并且集合数量sets减1,因为合并后集合数量减少了一个。
  • 计算相似字符串组数量(numSimilarGroups函数)
    • 首先获取字符串的数量n和每个字符串的长度m
    • 构建并查集(build(n))。
    • 然后进行双层循环,外层循环i0n - 1,内层循环ji + 1n - 1
    • 对于每一对字符串strs[i]strs[j],如果它们不在同一个集合(find(i)!=find(j)),则计算它们不同字符的数量diff
    • 如果diff0(字符串相等)或者diff2(满足相似字符串的条件),则将这两个字符串所在的集合合并(union(i, j))。
    • 最后返回sets,即最终的集合数量,这个数量就是字符串组的数量。

代码实现

// 相似字符串组
// 如果交换字符串 X 中的两个不同位置的字母,使得它和字符串 Y 相等
// 那么称 X 和 Y 两个字符串相似
// 如果这两个字符串本身是相等的,那它们也是相似的
// 例如,"tars" 和 "rats" 是相似的 (交换 0 与 2 的位置);
// "rats" 和 "arts" 也是相似的,但是 "star" 不与 "tars","rats",或 "arts" 相似
// 总之,它们通过相似性形成了两个关联组:{"tars", "rats", "arts"} 和 {"star"}
// 注意,"tars" 和 "arts" 是在同一组中,即使它们并不相似
// 形式上,对每个组而言,要确定一个单词在组中,只需要这个词和该组中至少一个单词相似。
// 给你一个字符串列表 strs列表中的每个字符串都是 strs 中其它所有字符串的一个字母异位词。
// 返回 strs 中有多少字符串组
// 测试链接 : https://leetcode.cn/problems/similar-string-groups/
public class Code04_SimilarStringGroups {

    public static int MAXN = 301;

    public static int[] father = new int[MAXN];

    public static int sets;

    public static void build(int n) {
        for (int i = 0; i < n; i++) {
            father[i] = i;
        }
        sets = n;
    }

    public static int find(int i) {
        if (i != father[i]) {
            father[i] = find(father[i]);
        }
        return father[i];
    }

    public static void union(int x, int y) {
        int fx = find(x);
        int fy = find(y);
        if (fx != fy) {
            father[fx] = fy;
            sets--;
        }
    }

    public static int numSimilarGroups(String[] strs) {
        int n = strs.length;
        int m = strs[0].length();
        build(n);
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                if (find(i) != find(j)) {
                    int diff = 0;
                    for (int k = 0; k < m && diff < 3; k++) {
                        if (strs[i].charAt(k) != strs[j].charAt(k)) {
                            diff++;
                        }
                    }
                    if (diff == 0 || diff == 2) {
                        union(i, j);
                    }
                }
            }
        }
        return sets;
    }

}

五.岛屿数量

题目:岛屿数量

算法原理

  • 数据结构
    • father数组:用于构建并查集,father[i]表示第i个元素(这里对应网格中的陆地格子)在并查集中的父节点。初始时,每个陆地格子的父节点是它自己,表示每个陆地格子是一个单独集合的代表。
    • cols变量:用于记录网格的列数,方便在将二维坐标转换为一维索引时使用。
    • sets变量:用于记录当前的集合数量,初始时每一个独立的陆地格子都是一个集合,所以每发现一个'1'的格子,sets就加1。
  • 构建并查集(build函数)
    • 首先确定网格的列数cols = m,并将sets初始化为0。
    • 然后遍历二维网格,对于每个格子,如果它是陆地(board[a][b] == '1'),计算其在一维数组中的索引index = a * cols + b,将father[index]设置为index,表示这个格子是一个单独的集合,同时sets加1,表示发现了一个新的可能的岛屿。
  • 查找操作(find函数)
    • 查找操作的目的是找到给定元素所在集合的代表元素。
    • 如果i不等于father[i],说明i不是所在集合的代表元素,通过递归调用find函数找到father[i]的代表元素,然后将father[i]直接指向这个代表元素,实现路径压缩,这有助于提高后续查找操作的效率。最后返回找到的代表元素father[i]
  • 合并操作(union函数)
    • 这个函数用于合并两个集合。首先通过find函数分别找到坐标为(a, b)(c, d)的陆地格子在并查集中的代表元素fxfy
    • 如果fxfy不相等,说明这两个格子属于不同的集合,此时将fx的父节点设置为fy,表示将fx所在的集合合并到fy所在的集合中,并且集合数量sets减1,因为合并后集合数量减少了一个。
  • 计算岛屿数量(numIslands函数)
    • 首先获取网格的行数n和列数m,然后构建并查集(build(n, m, board))。
    • 接着遍历二维网格,对于每个陆地格子(board[i][j] == '1'),检查其左边(j > 0 && board[i][j - 1] == '1')和上边(i > 0 && board[i - 1][j] == '1')是否也是陆地,如果是,则将它们所在的集合合并(union(i, j, i, j - 1)union(i, j, i - 1, j))。
    • 最后返回sets,即最终的集合数量,这个数量就是岛屿的数量,因为每个岛屿最终会被合并成一个集合。

代码实现

// 岛屿数量
// 给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量
// 岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成
// 此外,你可以假设该网格的四条边均被水包围
// 测试链接 : https://leetcode.cn/problems/number-of-islands/
public class Code05_NumberOfIslands {

    // 并查集的做法
    public static int numIslands(char[][] board) {
        int n = board.length;
        int m = board[0].length;
        build(n, m, board);
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (board[i][j] == '1') {
                    if (j > 0 && board[i][j - 1] == '1') {
                        union(i, j, i, j - 1);
                    }
                    if (i > 0 && board[i - 1][j] == '1') {
                        union(i, j, i - 1, j);
                    }
                }
            }
        }
        return sets;
    }

    public static int MAXSIZE = 100001;

    public static int[] father = new int[MAXSIZE];

    public static int cols;

    public static int sets;

    public static void build(int n, int m, char[][] board) {
        cols = m;
        sets = 0;
        for (int a = 0; a < n; a++) {
            for (int b = 0, index; b < m; b++) {
                if (board[a][b] == '1') {
                    index = index(a, b);
                    father[index] = index;
                    sets++;
                }
            }
        }
    }

    public static int index(int a, int b) {
        return a * cols + b;
    }

    public static int find(int i) {
        if (i != father[i]) {
            father[i] = find(father[i]);
        }
        return father[i];
    }

    public static void union(int a, int b, int c, int d) {
        int fx = find(index(a, b));
        int fy = find(index(c, d));
        if (fx != fy) {
            father[fx] = fy;
            sets--;
        }
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值