0095 经典算法系列——并查集(Union-Find)

概念TODO

UF的标准模板

算法实战

  • 等式方程的可满足性(medium)
  • 朋友圈(medium)
  • 最长连续序列(hard)

下面将介绍以上题目的实现:
等式方程的可满足性(medium)
分析

  • List item
class Solution {
    public boolean equationsPossible(String[] equations) {
        int n = equations.length;
        UF uf = new UF();
        
        for (int i = 0; i < n; i++) {
            if (equations[i].charAt(1) == '='){
                uf.union(equations[i].charAt(0) - 'a', equations[i].charAt(3) - 'a');
            }
        }
        
        for (int i = 0; i < n; i++) {
            if (equations[i].charAt(1) == '=' && !uf.connected(equations[i].charAt(0) - 'a', equations[i].charAt(3) - 'a') ){
               return false;         
            }
            if (equations[i].charAt(1) == '!' && uf.connected(equations[i].charAt(0) - 'a', equations[i].charAt(3) - 'a') ){
               return false;         
            }
        }     
        return true;
 
    }
    
    class UF {
        int[] id;
        int[] size;
        public UF() {
            id = new int[26];
            size = new int[26];
            
            for (int i = 0; i < 26; i++){
                id[i] = i;
                size[i] = 1;
            }
        }
        public int find(int p){
            while (p != id[p]) {
                id[p] = id[id[p]];
                p = id[p];
            }
            return p;
        }
        public void union(int p, int q) {
            int pRoot = find(p);
            int qRoot = find(q);
            if (pRoot == qRoot) return;
            if (size[pRoot] < size[qRoot]){
                id[pRoot] = qRoot;
                size[qRoot] += size[pRoot];
            }else {
                id[qRoot] = pRoot;
                size[pRoot] += size[qRoot];
            }
        }
        
        public boolean connected(int p, int q) {
            return find(p) == find(q);
        }
    }
}

执行用时 :1 ms, 在所有 Java 提交中击败了100.00%的用户
内存消耗 :39.3 MB, 在所有 Java 提交中击败了16.67%的用户

  • 547 朋友圈(medium)

班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。

给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j
个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。

示例 1:

输入: [[1,1,0], [1,1,0], [0,0,1]] 输出: 2 说明:已知学生0和学生1互为朋友,他们在一个朋友圈。
第2个学生自己在一个朋友圈。所以返回2

class Solution {
    //Union Find
    public int findCircleNum(int[][] M) {
        int n = M.length;
        UF uf = new UF(n);
        
        for (int i = 0; i < n; i++) 
            for (int j = 0; j < n; j++) {
                if (M[i][j] == 1)
                    uf.union(i, j);
            }
        
        return uf.count;
        
    }
    class UF {
        int[] id;
        int[] size;
        public int count;
        public UF(int n) {
            id = new int[n];
            size = new int[n];
            count = n;
            for (int i = 0; i < n; i++) {
                id[i] = i;
                size[i] = 1;
            }
        }
        public int find(int p) {
            while (p != id[p]) {
                id[p] = id[id[p]];
                p = id[p];
            }
            return p;
        }
        
        public void union(int p, int q) {
            int pRoot = find(p);
            int qRoot = find(q);
            if (pRoot == qRoot) return;
            if (size[pRoot] < size[qRoot]) {
                id[pRoot] = qRoot;
                size[qRoot] += size[pRoot];
                count--;
            } else {
                id[qRoot] = pRoot;
                size[pRoot] += size[qRoot];
                count--;
            }
        }
    }

DFS解法

public int findCircleNum(int[][] M) {
        int m  = M.length;
        int count = 0;
        int[] visited = new int[m];  //all 0
        
        for (int i = 0; i < m; i++)
            if (visited[i] == 0){
                dfs(M, i, visited);
                count++;
            }

        return count;
    }
    
    public void dfs(int[][] M, int i, int[] visited) {
        for (int j = 0; j < M.length; j++){
            if (visited[j] == 0 && M[i][j] == 1){
                visited[j] = 1;
                dfs(M, j, visited);
            }
        }
          
    }

  • 947. 移除最多的同行或同列石头(medium)
n 块石头放置在二维平面中的一些整数坐标点上。每个坐标点上最多只能有一块石头。

如果一块石头的 同行或者同列 上有其他石头存在,那么就可以移除这块石头。

给你一个长度为 n 的数组 stones ,其中 stones[i] = [xi, yi] 表示第 i 块石头的位置,返回 可以移除的石子 的最大数量。

方法:并查集
删到最后,留在图中的顶点一定位于不同的行和不同的列。因此,并查集里的元素是 描述「横坐标」和「纵坐标」的数值。因此我们需要遍历数组 stones,将每个 stone 的横坐标和纵坐标在并查集中进行合并。理解合并的语义十分重要。

「合并」的语义
「合并」的语义是:所有横坐标为 x 的石头和所有纵坐标为 y 的石头都属于同一个连通分量。

并查集里如何区分横纵坐标
然而会遇到这样一个问题:石头的位置是「有序数对(二维)」,并查集的底层是「一维数组」,我们在并查集里应该如何区分横纵坐标呢?

例如:如果一块石头的坐标为 [3, 3] ,那么所有横坐标为 3 的石头和所有纵坐标为 3 的石头都在一个连通分量中,但是我们需要在并查集里区分「横坐标」和「纵坐标」,它们在并查集里不能相等,根据题目的提示 0 <= x_i, y_i <= 10^40 <=xi ,yi <= 10^4
,可以把其中一个坐标 映射 到另一个与 [0, 10000] 不重合的区间,可以的做法是把横坐标全部减去 1000110001 或者全部加上 1000110001(视频讲解有误,以此处为准),或者按位取反([0, 10000] 里的 3232 位整数,最高位变成 11以后,一定不在 [0, 10000] 里)。

在并查集里我们需要维护连通分量的个数,新创建顶点的时候连通分量加 11;合并不在同一个连通分量中的两个并查集的时候,连通分量减 11。

class Solution {
    public int removeStones(int[][] stones) {
        int m = stones.length;
        UF uf = new UF();
        for (int i = 0; i < m; i++) {
            int p = stones[i][0] + 10001; //为了区分横坐标与纵坐标数值
            int q = stones[i][1];
            uf.union(p, q);
        }
        
        return m - uf.getCount();
        
        
    }
    
    private class UF {
        Map<Integer, Integer> parent;
        int count;
        
        public UF() {
            this.parent = new HashMap();
            this.count = 0;
        }
        
        public int getCount() {
            return this.count;
        }
        
        public void union(int p, int q) {
            int pRoot = find(p);
            int qRoot = find(q);
            if (pRoot == qRoot)
                return;
            
            parent.put(pRoot, qRoot);
            count--;
        }
        
        public int find(int x) {
            if( !parent.containsKey(x)) {
                parent.put(x, x);
                count++;
            }
            //如果x的父节点不是自己,则找到其父节点
            if (x != parent.get(x)) {
                parent.put(x, find(parent.get(x)));
            }
            return parent.get(x);
        }
        
        
    }
    
   
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值