【算法基础:数据结构】2.3 并查集

并查集

https://oi-wiki.org/ds/dsu/
在这里插入图片描述

操作:

  1. 将两个集合合并
  2. 询问两个元素是否在一个集合当中

(路径压缩优化之后):近乎 O ( 1 ) O(1) O(1)

算法原理(重要!⭐)

在这里插入图片描述

将每个集合使用树的形式存储。

每个集合的编号 是 树根的编号。

每个节点存储 它的父节点是谁。 p[x] 表示 x 的父节点。


Q:如何判断树根?
A:p[x] == x

Q:如何求 x 的集合编号?
A:while (p[x] != x) x = p[x];
使用 路径压缩 的话就是 if (p[x] != x) p[x] = find(x);

Q:如何合并两个集合?
A:p[x] = y,即 把一棵树的根节点当成另一个树根节点的儿子。 (优化!:将这棵树中的所有节点都当成另一棵树根节点的子节点——优化之后几乎就是 O ( 1 ) O(1) O(1) 的了)(这个优化叫做 路径压缩

时间和空间复杂度

经典例题

836. 合并集合(重要!模板!⭐)

https://www.acwing.com/activity/content/problem/content/885/
在这里插入图片描述

注意下面 find(x) 的写法。

import java.util.Arrays;
import java.util.Scanner;

public class Main {
    static int[] p;
    public static void main(String[] args){
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt(), m = scanner.nextInt();
        p = new int[n + 1];
        Arrays.setAll(p, e -> e);   // 初始化,各个节点的祖宗节点设置成自己
        while (m-- != 0) {
            char op = scanner.next().charAt(0);
            int a = scanner.nextInt(), b = scanner.nextInt();
            if (op == 'M') {
                p[find(a)] = find(b);
            } else {
                if (find(a) == find(b)) System.out.println("Yes");
                else System.out.println("No");
            }
        }
    }

    // 返回 x 的祖宗节点 + 路径压缩
    static int find(int x) {
        if (p[x] != x) p[x] = find(p[x]);	// 路径压缩
        return p[x];
    }
}

837. 连通块中点的数量(维护连通块大小的并查集)

https://www.acwing.com/activity/content/problem/content/886/

在这里插入图片描述

find(x) 方法没有变化。

在合并两个点时:
先检查两个点是否已经在同一个连通块里了,直接 continue;
否则,先将一个点的根节点维护的数量加到另一个点的根节点维护的数量,然后再合并这两个点。

import java.util.Arrays;
import java.util.Scanner;

public class Main {
    static int[] p, size;       // p[i][0]是父节点  p[i][1]是该连通块中点的数量
    public static void main(String[] args){
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt(), m = scanner.nextInt();
        p = new int[n + 1];
        size = new int[n + 1];
        Arrays.setAll(p, e ->e);   // 初始化,各个节点的祖宗节点设置成自己
        Arrays.fill(size, 1);   // 初始化每个连通块的大小是 1
        while (m-- != 0) {
            String op = scanner.next();
            int a = scanner.nextInt();
            if ("C".equals(op)) {
                int b = scanner.nextInt();
                if (find(a) == find(b)) continue;       // 已经在同一个连通块里了
                size[find(b)] += size[find(a)];
                p[find(a)] = find(b);
            } else if ("Q1".equals(op)) {
                int b = scanner.nextInt();
                if (find(a) == find(b)) System.out.println("Yes");
                else System.out.println("No");
            } else {
                System.out.println(size[find(a)]);
            }
        }
    }

    // 返回 x 的祖宗节点 + 路径压缩
    static int find(int x) {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
}

240. 食物链(维护额外信息的并查集)🚹🚹🚹

https://www.acwing.com/activity/content/problem/content/887/
在这里插入图片描述

同一个集合表示:这一个集合内的所有动物之间的关系都可以确定。

对于每一句话中的 x 和 y,先检查它们是否在一个集合里,如果在同一个集合里,那么就可以判断这句话是否是假话。
如果是假话—— ++ans
如果是真话—— 将它们所在的两个集合合并。

如何确定同一个集合里各个动物之间的关系?
使用 d 数组维护各个节点到根节点之间的距离。(具体可以看下图:)

在这里插入图片描述
注意看代码中注释的解释。

import java.util.Arrays;
import java.util.Scanner;

public class Main {
    static int[] p, d;
    public static void main(String[] args){
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt(), k = scanner.nextInt(), ans = 0;
        // 一个集合里的所有动物之间的关系是可以推理出来的(这里同一个连通块并不表示它们是同一个种类,而是它们之间的关系可以确定)
        // 记录每个点和根节点的关系(用每个点和根节点之间的距离来表示它和根节点之间的距离)
        p = new int[n + 1];
        Arrays.setAll(p, e ->e);    // 初始化,各个节点的祖宗节点设置成自己
        d = new int[n + 1];         // d 维护的是各个节点到根节点的距离(距离表示它和根节点之间的关系)

        while (k-- != 0) {
            int op = scanner.nextInt(), x = scanner.nextInt(), y = scanner.nextInt();
            if (x > n || y > n) {               // x 或 y 比 n 大 —— 是假话
                ++ans;
                continue;
            }

            int px = find(x), py = find(y);     // 找到 x 和 y 的祖宗节点
            if (op == 1) {
                if (px == py && (d[x] - d[y]) % 3 != 0) ++ans;  // 已经在一个集合里了,判断是否合理
                else if (px != py) {            // 不在一个集合里,确定关系
                    p[px] = p[py];
                    d[px] = d[y] - d[x];        // 是为了令 d[px] + d[x] == d[y]
                }
            } else {
                if (px == py && (d[x] - d[y] - 1) % 3 != 0) ans++;  // 已经在一个集合里了,判断是否合理
                else if (px != py) {
                    p[px] = py;
                    d[px] = d[y] - d[x] + 1;    // x 吃 y,是为了令d[px] + d[x] = d[y] + 1。
                }
            }
        }
        System.out.println(ans);
    }

    static int find(int x) {
        if (p[x] != x) {
            int t = find(p[x]);     // t 是 x 的父节点的父节点
            d[x] += d[p[x]];        // 在路径压缩的过程中,子节点需要继承父节点到根节点的距离。(因为没压缩之前 d[x]是x到p[x]之间的距离)
            p[x] = t;
        }
        return p[x];
    }
}

关于 d[px] 的确定:
在这里插入图片描述

相关题目练习

684. 冗余连接(并查集寻找冗余的边)

https://leetcode.cn/problems/redundant-connection/
在这里插入图片描述

从前往后枚举每个边,将边的两个节点所在的集合合并,如果两个节点已经在同一个集合里了,说明这条边是多余的,那就保留这条边作为答案。

class Solution {
    public int[] findRedundantConnection(int[][] edges) {
        int n = edges.length;
        int[] p = new int[n + 1], ans = new int[]{-1, -1};
        Arrays.setAll(p, e -> e);
        for (int[] edge: edges) { 
            int x = edge[0], y = edge[1];
            if (find(x, p) == find(y, p)) ans = edge;   // 如果已经在一个集合了,说明这条边是多余的
            else p[find(x, p)] = find(y, p);            // 合并两个集合
        }
        return ans;
    }

    int find(int x, int[] p) {
        if (x != p[x]) p[x] = find(p[x], p);
        return p[x];
    }
}

685. 冗余连接 II⭐(分情况讨论)

https://leetcode.cn/problems/redundant-connection-ii/

在这里插入图片描述

需要选择一条边删除,删除之后使得有向图变成一个有根树。(有根树的定义见 题目)。

删除边之前,删除边的原因有三种:

  1. 有环
  2. 有冲突,两个节点指向了同一个节点
  3. 既有环又有冲突

解法参考:https://programmercarl.com/0685.%E5%86%97%E4%BD%99%E8%BF%9E%E6%8E%A5II.html#%E6%80%9D%E8%B7%AF

如果图中存在冲突,则一定需要删除冲突的两个边其中之一。
如果没有冲突,则一定存在环,使用并查集找到哪条边出现后图中出现了环。

class Solution {
    int[] p = new int[1001];

    public int[] findRedundantDirectedConnection(int[][] edges) {
        int n = edges.length;
        Arrays.setAll(p, e -> e);
        int[] inDegree = new int[n + 1];
        for (int[] e: edges) inDegree[e[1]]++;
        List<Integer> ls = new ArrayList();       // 记录入度为2的节点所对应的边
        for (int i = 0; i < n; ++i) {
            if (inDegree[edges[i][1]] == 2) ls.add(i);
        }

        // 检查删除哪条造成入度=2的边可以让图变成有根树
        if (ls.size() > 0) {
            if (isTreeAfterRemoveEdge(edges, ls.get(1))) return edges[ls.get(1)];
            else return edges[ls.get(0)];
        }

        // 删除造成有向环的那个
        return getRemoveEdge(edges);
    }

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

    // 检查删除一条边之后是否是有根树
    boolean isTreeAfterRemoveEdge(int[][] edges, int k) {
        for (int i = 0; i < edges.length; ++i) {
            if (i == k) continue;
            int x = edges[i][0], y = edges[i][1];
            if (find(x) == find(y)) return false;
            p[find(x)] = find(y);
        }
        return true;
    }

    // 检查存在环的情况下应该删除哪条边
    int[] getRemoveEdge(int[][] edges) {
        for (int[] e: edges) {
            int x = e[0], y = e[1];
            if (find(x) == find(y)) return e;
            p[find(x)] = find(y);
        }
        return new int[]{-1, -1};
    }
}

734. 句子相似性(这题不需要使用并查集)

https://leetcode.cn/problems/sentence-similarity/
在这里插入图片描述
提示:

1 <= sentence1.length, sentence2.length <= 1000
1 <= sentence1[i].length, sentence2[i].length <= 20
sentence1[i] 和 sentence2[i] 只包含大小写英文字母
0 <= similarPairs.length <= 2000
similarPairs[i].length == 2
1 <= xi.length, yi.length <= 20
所有对 (xi, yi) 都是 不同 的

class Solution {
    public boolean areSentencesSimilar(String[] sentence1, String[] sentence2, List<List<String>> similarPairs) {
        int n = sentence1.length;
        if (n != sentence2.length) return false;
        // 构建相似映射
        Map<String, Set<String>> m = new HashMap<>();
        for (List<String> s: similarPairs) {
            String a = s.get(0), b = s.get(1);
            m.computeIfAbsent(a, key -> new HashSet<>()).add(b);
            m.computeIfAbsent(b, key -> new HashSet<>()).add(a);
        }
        // 逐个检查
        for (int i = 0; i < n; ++i) {
            String s1 = sentence1[i], s2 = sentence2[i];
            if (!s1.equals(s2) && !m.getOrDefault(s1, new HashSet<>()).contains(s2)) return false;
        }
        return true;
    }
}

737. 句子相似性 II

https://leetcode.cn/problems/sentence-similarity-ii/

在这里插入图片描述

这一题的相似关系是可传递的,所以可以使用并查集。
由于是字符串之间的关系,使用哈希表来代替数据存储并查集结构。

class Solution {
    // 使用哈希表记录每个节点的父节点
    Map<String, String> p = new HashMap<>();

    public boolean areSentencesSimilarTwo(String[] sentence1, String[] sentence2, List<List<String>> similarPairs) {
        int n = sentence1.length;
        if (n != sentence2.length) return false;
        // 构建并查集
        for (List<String> s: similarPairs) {
            String a = s.get(0), b = s.get(1);
            p.put(find(a), find(b));
        }
        // 逐个检查
        for (int i = 0; i < n; ++i) {
            String a = sentence1[i], b = sentence2[i];
            if (!find(a).equals(find(b))) return false;
        }
        return true;
    }

    public String find(String x) {
        if (!p.containsKey(x)) p.put(x, x);
        if (!x.equals(p.get(x))) p.put(x, find(p.get(x)));
        return p.get(x);
    }
}

721. 账户合并

https://leetcode.cn/problems/accounts-merge/description/

在这里插入图片描述
提示:

1 <= accounts.length <= 1000
2 <= accounts[i].length <= 10
1 <= accounts[i][j].length <= 30
accounts[i][0] 由英文字母组成
accounts[i][j] (for j > 0) 是有效的邮箱地址

使用并查集维护各个邮件之间的关系。
使用哈希表记录每个集合代表邮件与账户名之间的关系。

class Solution {
    Map<String, String> m = new HashMap<>();

    public List<List<String>> accountsMerge(List<List<String>> accounts) {
        for (List<String> acc: accounts) {
            for (int i = 1; i < acc.size() - 1; ++i) {
                m.put(find(acc.get(i)), find(acc.get(i + 1)));
            }
        }

        // 记录各个集合的邮件列表
        Map<String, Set<String>> m2 = new HashMap<>();
        // 记录各个集合代表邮件和姓名的映射
        Map<String, String> m3 = new HashMap<>();
        for (List<String> acc: accounts) {
            for (int i = 1; i < acc.size(); ++i) {
                String s = acc.get(i), k = find(s);
                if (!m2.containsKey(k)) {
                    m2.put(k, new HashSet<>());
                    m3.put(k, acc.get(0));
                }
                m2.get(k).add(s);
            }
        }

        // 得到答案
        List<List<String>> ans = new ArrayList<>();
        for (String k: m2.keySet()) {
            List<String> t = new ArrayList<>();
            Set<String> s = m2.get(k);
            for (String ss: s) t.add(ss);
            Collections.sort(t);
            t.add(0, m3.get(k));
            ans.add(t);
        }
        return ans;
    }

    public String find(String x) {
        if (!m.containsKey(x)) m.put(x, x);
        if (!x.equals(m.get(x))) m.put(x, find(m.get(x)));
        return m.get(x);
    }
}

相关链接

https://oi-wiki.org/ds/dsu/

其余相关题目

1851. 包含每个查询的最小区间

1851. 包含每个查询的最小区间
在这里插入图片描述
提示:
1 <= intervals.length <= 10^5
1 <= queries.length <= 10^5
intervals[i].length == 2
1 <= lefti <= righti <= 10^7
1 <= queries[j] <= 10^7

见:【LeetCode每日一题合集】2023.7.17-2023.7.23(⭐离线算法 & 环形子数组的最大和 & 接雨水)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wei *

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值