并查集小结附leetcode三道题目

本文介绍了并查集数据结构在解决连通性问题(如判断连通图和计算省份数量)以及处理等式约束(如判断变量关系的可行性)的应用,通过实例展示了如何在LeetCode题目中运用并查集算法实现高效求解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概念

类比树是一种保留孩子节点的数据结构,并查集是一种保留父亲节点的数据结构。

查找-合并-获取连通分量/集合

适用场景

  • 处理元素分组问题;
  • 一些不交集的合并及查询问题;
  • 连通性问题,如判断图中的两个节点是否属于同一个连通分量/祖先,或判断一个无向图是否是连通图等;
  • 传递性问题,如亲戚关系或者带权的乘除法运算。

UnionFind模板代码

class UnionFind<T> {
    private Map<T, T> father; // Use generic types for the Map

    public UnionFind() {
        father = new HashMap<>(); // Initialize the Map
    }

    //把一个新节点添加到并查集中
    public void add(T x) {
        if (!father.containsKey(x)) {
            father.put(x, null);
        }
    }

    //外部判断两个节点连通/有相同祖先,则合并两个节点
    public void merge(T x, T y) {
        T rootX = find(x);
        T rootY = find(y);
    //如果它们不相同,则表示节点 x 和节点 y 不在同一个集合中,需要合并它们
        if (!rootX.equals(rootY)) { // Use equals() for object comparison
            father.put(rootX, rootY);
        }
    }

    //查找祖先,如果节点的父节点不为空就不断迭代
    public T find(T x) {
        T root = x;
        while (father.get(root) != null) {
            root = father.get(root);
        }
        //路径压缩,最终x指向最远的爹
        while (!x.equals(root)) { // Use equals() for object comparison
            T originalFather = father.get(x);
            father.put(x, root);
            x = originalFather;
        }
        return root;
    }

    //判断两个节点是否连通
    public boolean isConnected(T x, T y) {
        return find(x).equals(find(y)); // Use equals() for object comparison
    }
}

例题

547.省份数量-m

n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。

省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。

给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。

返回矩阵中 省份 的数量。

. - 力扣(LeetCode)

思路:每一个节点先生成一个集合,当两个节点连通时将其合并为一个集合,最后返回集合个数即可。获取集合的方法可以写在Union类中。

优化:矩阵关于主对角线对称,判断是否连通时,可以只遍历一个三角。

class UnionFind {
    private Map<Integer,Integer> father;//记录父节点
    private int numOfSets = 0;//记录集合的数量
    public UnionFind() {//构造函数
        father = new HashMap<Integer,Integer>();
        numOfSets = 0;//新增的
    }

    public void add(int x) {//把一个新节点添加到并查集中
        if (!father.containsKey(x)) {
            father.put(x, null);
            numOfSets++;//新增的
        }
    }
    public void merge(int x, int y) {//两个节点连通/有相同祖先,合并两个节点
        int rootX = find(x);
        int rootY = find(y);
        //如果它们不相同,则表示节点 x 和节点 y 不在同一个集合中,需要合并它们
        if (rootX != rootY) {
            father.put(rootX,rootY);
            numOfSets--;//新增的
        }
    }
    public int find(int x) {//查找祖先,如果节点的父节点不为空就不断迭代
        int root = x;
        while (father.get(root) != null) {
            root = father.get(root);
        }
        //路径压缩
        while (x != root) {
            int original_father = father.get(x);
            father.put(x,root);
            x = original_father;
        }
        return root;
    }
    public boolean isConnected(int x, int y) {//判断两个节点是否连通
        return find(x) == find(y);
    }
    public int getNumOfSets() {
        return numOfSets;
    }
}
class Solution {
    public int findCircleNum(int[][] isConnected) {
        //合并-查找-字典,树的每个节点记录子节点,并查集中的每个节点会记录父节点
        UnionFind uf = new UnionFind(); 
        for (int i = 0; i < isConnected.length; i++){
            uf.add(i);
            for (int j = 0; j < i; j++) {//矩阵是对称的,遍历一个三角即可
                if (isConnected[i][j] == 1) {
                    uf.merge(i,j);
                }
            }
        }
        return uf.getNumOfSets();
    }
}

时间复杂度:O(n^2log⁡n),n是城市的数量

空间复杂度:O(n)

990.等式方程的可满足性-m

给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程 equations[i] 的长度为 4,并采用两种不同的形式之一:"a==b" 或 "a!=b"。在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。

只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回 true,否则返回 false

. - 力扣(LeetCode)

思路:两次for循环遍历,第一次遍历,遇见==表明同一个等式中的两个变量属于同一个连通分量,故将其合并;第二次遍历,遇见!=表明同一个不等式中的两个变量不能属于同一个祖先,如果两个变量本身相连则推出矛盾,返回false。

class UnionFind<T> {
    private Map<T, T> father; // Use generic types for the Map

    public UnionFind() {
        father = new HashMap<>(); // Initialize the Map
    }

    public void add(T x) {
        if (!father.containsKey(x)) {
            father.put(x, null);
        }
    }
    
    public void merge(T x, T y) {
        T rootX = find(x);
        T rootY = find(y);
        if (!rootX.equals(rootY)) { // Use equals() for object comparison
            father.put(rootX, rootY);
        }
    }

    public T find(T x) {
        T root = x;
        while (father.get(root) != null) {
            root = father.get(root);
        }
        while (!x.equals(root)) { // Use equals() for object comparison
            T originalFather = father.get(x);
            father.put(x, root);
            x = originalFather;
        }
        return root;
    }

    public boolean isConnected(T x, T y) {
        return find(x).equals(find(y)); // Use equals() for object comparison
    }
}

class Solution {
    public boolean equationsPossible(String[] equations) {
        UnionFind<Character> uf = new UnionFind();
        for (int i = 0; i < equations.length; i++) {
            char[] ch = equations[i].toCharArray();
            uf.add(ch[0]);
            uf.add(ch[3]);
            if (ch[1] == '=') {//同一个等式中的两个变量属于同一个连通分量,故将其合并
                uf.merge(ch[0], ch[3]);
            }
        }
        for (int i = 0; i < equations.length; i++) {
            char[] ch = equations[i].toCharArray();
            if (ch[1] == '!') {//同一个不等式中的两个变量不能属于同一个祖先
                if(uf.isConnected(ch[0], ch[3])){
                    return false;
                }
            }
        }
        return true;
    }
}

时间复杂度:O(n+ClogC),n是equations中的方程数量,C是变量的总数,此题中变量为小写字母,故C <= 26。

空间复杂度:O(C)

399.除法求值-m

给你一个变量对数组 equations 和一个实数值数组 values 作为已知条件,其中 equations[i] = [Ai, Bi]values[i] 共同表示等式 Ai / Bi = values[i] 。每个 AiBi 是一个表示单个变量的字符串。

另有一些以数组 queries 表示的问题,其中 queries[j] = [Cj, Dj] 表示第 j 个问题,请你根据已知条件找出 Cj / Dj = ? 的结果作为答案。

返回 所有问题的答案 。如果存在某个无法确定的答案,则用 -1.0 替代这个答案。如果问题中出现了给定的已知条件中没有出现的字符串,也需要用 -1.0 替代这个答案。

注意:输入总是有效的。你可以假设除法运算中不会出现除数为 0 的情况,且不存在任何矛盾的结果。

注意:未在等式列表中出现的变量是未定义的,因此无法确定它们的答案。

 . - 力扣(LeetCode)

该题是在上一道题的基础上加了权重,难点在于find和merge都涉及到权重的更新。

值得下去再琢磨下这道题。

class UnionFind {
    private Map<String, String> father;//记录父节点(除数),son-father
    HashMap<String, Double> valueMap = new HashMap<>();// 记录指向父节点的权值
    
    public UnionFind(HashSet<String> StringSet) {//构造函数,传入String的哈希集合 
        father = new HashMap<>();
        for (String s : StringSet) {
            father.put(s, s);//初始化父节点为自身
            valueMap.put(s, 1.0);//初始权值赋为1.0
        }
    }
    //add在构造函数里实现了
    // public void add(int x) {//把一个新节点添加到并查集中
    //     if (!father.containsKey(x)) {
    //         father.put(x, null);
    //     }
    // }
    
    public void merge(String x, String y, Double value) {//x/y=value
        String rootX = find(x);
        String rootY = find(y);
        //如果父节点不相同,则表示节点 x 和节点 y 不在同一个集合中,需要合并它们
        if (!rootX.equals(rootY)) {
            father.put(rootX, rootY);
            // 两条路径上的有向边的权值的乘积是一定相等的,a->b->c = a->d->c
            valueMap.put(rootX, value * valueMap.get(y) / valueMap.get(x));
        }
    }
    public String find(String x) {//查找祖先
        // 祖先不存在就返回空
        if (!father.containsKey(x)) return null;

        String root = x;
        double base = 1;
        // 先从x找到根节点,并更新base为x到根节点的权重
        while (!root.equals(father.get(root))) {
            root = father.get(root);// 最终得到x的最远祖先
            base *= valueMap.get(root);
        }

        // 更新从x到根节点路径上节点的权重
        while (!x.equals(root)) {
            String original_father = father.get(x);
            valueMap.put(x, valueMap.get(x) * base);
            base /= valueMap.get(original_father);
            father.put(x,root);// 路径压缩
            x = original_father;
        }
        return root;
    }
    // public double isConnected(int x, int y) {//判断两个节点是否连通
    //     int rootX = find(x);
    //     int rootY = find(y);
    //     if (rootX == rootY) {
    //         return weight[x] / weight[y];
    //     } else {
    //         return -1.0d;
    //     }
    // }
    
}
class Solution {
    public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) {
        //并查集:查找-合并-获取连通分量/集合
        int n = queries.size();
        double[] res = new double[n];
        HashSet<String> hs = new HashSet<>();

        // 存放所有节点的哈希集合,每个节点都可能作为父节点
        for (int i = 0; i < equations.size(); i++) {
            hs.add(equations.get(i).get(0));
            hs.add(equations.get(i).get(1));
        }

        // 遍历方程,合并方程中的两个节点
        UnionFind uf = new UnionFind(hs);
        for (int i = 0; i < equations.size(); i++) {
            String x = equations.get(i).get(0);
            String y = equations.get(i).get(1);
            uf.merge(x, y, values[i]);
        }

        // 遍历问题
        for (int i = 0; i < queries.size(); i++) {
            String x = queries.get(i).get(0);
            String y = queries.get(i).get(1);
            //其中至少有一个节点没有祖先直接返回-1,独立的节点
            if (uf.find(x) == null || uf.find(y) == null) {
                res[i] = -1;
                // 两个节点的祖先相同,直接用指向父节点的权值作比
            } else if (uf.find(x).equals(uf.find(y))) {
                res[i] = uf.valueMap.get(x) / uf.valueMap.get(y);
                // 两个节点的祖先不同,两者没有关系,返回-1
            } else {
                res[i] = -1;
            }
        }
        return res;
    }
}

时间复杂度:O((N+Q)log⁡A),N 为输入方程 equations 的长度,每一次执行【合并】操作的时间复杂度是 O(log⁡A),A 是 equations 里不同字符的个数;Q 为查询数组 queries 的长度,每一次查询时执行「路径压缩」的时间复杂度是 O(log⁡A)。

空间复杂度:O(A),father和valueMap的长度为A。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值