力扣-并查集

力扣-并查集

解决问题:具有传递性的问题,判断两个点是否是联通的、以及路径压缩、按秩合并,不关心中间过程

TIPS:通过使用不同的按秩合并,可以知道节点的个数,和树的高度,增加一个变量n则可以知道进行了多少次连接操作.根据根节点的个数(根节点指向自己)可以求出分组的个数

  1. 剑指 Offer II 111. 计算除法
    我的题解:

    思路:因为不同变量之间可能存在传递性,传递性的问题考虑用并查集来解决。我们需要对每个字符串都给一个id,并维护两个数组,分别代表当前字符的父节点,和权值。

    初始化时,公式
    w e i g h t [ r o o t X ] = v a l ∗ w e i g h t [ y ] / w e i g h t [ x ] weight[rootX] = val * weight[y] / weight[x] weight[rootX]=valweight[y]/weight[x]
    然后我们根据要查询的结果,去并查集中找,找的过程中,更新父节点和权值,最终让父节点指向根节点,权值为路径的乘积。我们只需要判断两个字符的根节点是否相同就可以判断是否相连,若是则返回
    w e i g h t [ a ] / w e i g h t [ b ] weight[a] / weight[b] weight[a]/weight[b]
    即可,若不相连返回-1.0

    package demo04_10;
    
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    public class Solution {
        public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) {
           int equationSize = equations.size();
           UnionFind unionFind = new UnionFind(2 * equationSize);
            Map<String, Integer> map = new HashMap<>();
            int id = 0;
            //第一步,预处理,将变量的值与id进行映射,使得并查集的底层使用数组实现
            for (int i = 0; i < equationSize; i++) {
                String a = equations.get(i).get(0);
                String b = equations.get(i).get(1);
    
                if(!map.containsKey(a)){
                    map.put(a, id++);
                }
                if(!map.containsKey(b)){
                    map.put(b, id++);
                }
    			//初始化
                unionFind.union(map.get(a), map.get(b), values[i]);
            }
            //第二步,做查询
            int queriesSize = queries.size();
            double[] ans = new double[queriesSize];
            for (int i = 0; i < queriesSize; i++) {
                String a = queries.get(i).get(0);
                String b = queries.get(i).get(1);
                Integer id1 = map.get(a);
                Integer id2 = map.get(b);
    
                //若有一个不存在就赋为-1.0
                if(!map.containsKey(a) || !map.containsKey(b)){
                    ans[i] = -1.0;
                }else{
                    ans[i] = unionFind.isConnected(id1, id2);
                }
            }
            return ans;
        }
    	//并查集
        private class UnionFind{
            private int[] parent;
            /**
             * 指向父节点的权值
             */
            private double[] weight;
            public UnionFind(int n){
                this.parent = new int[n];
                this.weight = new double[n];
                for (int i = 0; i < n; i++) {
                    parent[i] = i;
                    weight[i] = 1.0;
                }
            }
    		//初始化
            public void union(int x, int y, double val){
                int rootX = find(x);
                int rootY = find(y);
                //若两个字符相等则不需要处理
                if(rootX == rootY) return;
                //将rootX的根节点指向rootY
                parent[rootX] = rootY;
                //求权值
                weight[rootX] = val * weight[y] / weight[x];
            }
    
            /**
             * 路径压缩-合并查找
             * @param x
             * @return 根节点的id
             */
            public int find(int x){
                //若不相等就一直往根节点找,直到找到根节点,中间权值相乘
                if(x != parent[x]){
                    int origin = parent[x];
                    parent[x] = find(parent[x]);
                    weight[x] *= weight[origin];
                }
                return parent[x];
            }
    
            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.0;
                }
            }
     }
    }
    
    
  2. 省份数量
    我的题解:

    思路: x - y, y - z => x - z,像这种传递性的问题我们使用并查集来解决是比较好的,并查集的优点是,可以在查询的过程中优化路径,将子节点的父节点直接指向根节点。这题是要求省会的个数,因此我们在初始化并查集时,若x,y是联通的我们用find分别找到x的根节点和y的根节点,然后让x的根节点的根节点指向y的根节点,从而实现了该方向的路径压缩。求省会的个数时只需要看有多少城市的根节点指向自己,若指向自己说明自己就是根节点。

    public int findCircleNum(int[][] isConnected) {
            int n = isConnected.length;
            //最大长度:每一个都不相连
            UnionFind unionFind = new UnionFind(n);
            //遍历数组,完成对其它间接联通的初始化
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    if(isConnected[i][j] == 1) unionFind.union(i, j);
                }
            }
    
            return unionFind.max();
        }
        //并查集
        class UnionFind{
            int[] parents;
            int n;
            //构造方法初始化
            public UnionFind(int n){
                this.n = n;
                parents = new int[n];
                //初始化
                for (int i = 0; i < n; i++) {
                    parents[i] = i;
                }
            }
    
            //初始化方法
            public void union(int x, int y){
                //分别查找x,y的根节点
                int rootX = find(x);
                int rootY = find(y);
                //将x的根节点的根节点指向y的根节点,从而实现x-y方向的路径压缩
                parents[rootX] = rootY;
            }
    
            //路径压缩
            public int find(int x){
                //当该节点和不是根节点时,去找根节点
                if(x != parents[x]){
                    parents[x] = find(parents[x]);
                }
                //返回x的根节点
                return parents[x];
            }
    
    		//求省会的个数
            public int max(){
                int max = 0;
                for (int i = 0; i < n; i++) {
                    if(parents[i] == i) max++;
                }
                return max;
            }
     }
    
  3. 冗余连接
    我的题解:

    思路: 判断一条边是否是冗余边,只需要判断在这两个点连接之前,这两条边是否已经联通,对应并查集中即是否存在公共的父节点,若是联通的则说明加上该边后会出现环,那么该边就是冗余边,否则就不是冗余边, 至于说返回最后一条冗余边,遇到环直接返回即可,因为连接n个点需要n-1条边,若出现环则肯定是第n条边,即是最后一条直接返回即可

    //冗余连接
        /*
        * 我们验证该边是不是冗余边,只需要看这两个点在臊面该边之前是否是联通的,若是联通的则有环出现,即该边式冗余边,否则不是冗余边
        * */
        int[] parents;
        public int[] findRedundantConnection(int[][] edges) {
            int n = edges.length;
            parents = new int[n + 1];
            //初始化,自己指向自己
            for (int i = 1; i < n + 1; i++) {
                parents[i] = i;
            }
    
            for (int i = 0; i < n; i++) {
                int[] edge = edges[i];
                //看当前两个点是否联通
                int x = find(edge[0]);
                int y = find(edge[1]);
                if(x != y){
                    union(x, y);
                }else{
                    return edge;
                }
            }
            //若没有就返回空
            return new int[0];
        }
        //初始化操作,若是联通的就将x的根节点的根节点指向y的根节点
        public void union(int x, int y){
            int rootX = find(x);
            int rootY = find(y);
            parents[rootX] = rootY;
        }
        //路径压缩
        public int find(int x){
            //若x的根节点并非指向自己,就去更新根节点,并将路径进行压缩,使之指向根节点
            if(x != parents[x]){
                parents[x] = find(parents[x]);
            }
            return parents[x];
     }
    
  4. 连通网络的操作次数
    我的题解:

    思路:路径连接问题不考虑中间过程,可以使用并查集,并查集中返回未连接点的个数是非常简单的,只需要在合并的过程中加一个参数count即可
    1.首先我们对c进行初始化,判断c与n的大小关系,若c < n - 1就说明不可能将这n个点相连,直接返回-1
    2.我们初始化并查集,并将相连的边相连,然后我们返回未连接点的个数-1就是需要边的最少个数

    public int makeConnected(int n, int[][] connections) {
            int c = connections.length;
         if(c < n - 1) return -1;
            UnionFind unionFind = new UnionFind(n);
            for(int[] connection : connections){
                unionFind.union(connection[0], connection[1]);
            }
            return unionFind.count - 1;
        }
    
        class UnionFind{
            //并查集中可以有三个参数,父亲节点,秩(节点的个数,或树的高度),未联通节点的个数count,每做一次有效连接,即连接之前这两个点未连接,连接后count-1
            int[] p;
            int[] s;
            int count;
            UnionFind(int n){
                count = n;
                p = new int[n];
                s = new int[n];
                for (int i = 0; i < n; i++) {
                    p[i] = i;
                    s[i] = 1;
                }
            }
    
            int find(int x){
                if(x != p[x]){
                    p[x] = find(p[x]);
                }
                return p[x];
            }
    
            void union(int x, int y){
                int rX = find(x), rY = find(y);
                if(rX == rY) return;
                count--;
                if(s[rX] < s[rY]){
                    p[rX] = rY;
                    s[rY] += s[rX];
                }else{
                    p[rY] = rX;
                    s[rX] += s[rY];
                }
            }
        }
    
  5. 最小体力消耗路径
    我的题解:

思路: 我们对数组中的每个点(即每个元素)的联通差值进行排序,是x方向和y方向的,然后逐个将list中的元素加入并查集中,每加入一个边就检查一下头节点(0, 0)和尾节点(m-1, n-1)是否联通,若是联通的返回该边的权值即可(因为权值是排过序的,能保证当前权值就是最小的高度差)

   int m, n;
    public int minimumEffortPath(int[][] heights) {
           //初始化
        m = heights.length;
           n = heights[0].length;
           parent = new int[m * n];
           List<int[]> sort = new ArrayList<>();
           for (int i = 0; i < m; i++) {
               for (int j = 0; j < n; j++) {
                   int id = i * n + j;
                   parent[id] = id;
                   if(i + 1 < m){
                       sort.add(new int[] {id, id + n, Math.abs(heights[i + 1][j] - heights[i][j])});
                   }
                   if(j + 1 < n){
                       sort.add(new int[] {id, id + 1, Math.abs(heights[i][j + 1] - heights[i][j])});
                   }
               }
           }
           int head = 0, tail = m * n - 1;
           //排序
           Collections.sort(sort, (o1, o2) -> o1[2] - o2[2]);
           //顺序将sort中的元素加入并查集中
           for (int i = 0; i < sort.size(); i++) {
               int[] arr = sort.get(i);
               union(arr[0], arr[1]);
               if(find(head) == find(tail)) return arr[2];
           }
   
           return 0;
       }
   
       int[] parent;

       public void union(int x, int y){
           parent[find(x)] = parent[find(y)];
       }
   
       public int find(int x){
           if(x != parent[x]){
               parent[x] = find(parent[x]);
           }
           return parent[x];
       }
   
       public int getIndex(int x, int y){
           return n * x + y;
       }
  1. 由斜杠划分区域
    我的题解:

    思路: 我们将一个单元格分割成四个部分,并对三种基本的情况进行判断,分别合并单元格内的小块,以及将相邻的单元格进行合并,这里使用并查集来合并路径,并查集内部维护一个count初始值为4 * n * n每次合并都会减少一个最后返回count即可

    /*
        * 我们将一个单元格分割成四个部分,并对三种基本的情况进行判断,分别合并单元格内的小块,以及将相邻的单元格进行合并,这里使用并查集来合并路径,
     * 并查集内部维护一个count初始值为4 * n * n每次合并都会减少一个最后返回count即可
        * */
     public int regionsBySlashes(String[] grid) {
            int n = grid.length;
         //初始化并查集
            UnionFind unionFind = new UnionFind(4 * n * n);
         //转化为字符数组
            char[][] chars = new char[n][n];
         for (int i = 0; i < n; i++) {
                chars[i] = grid[i].toCharArray();
         }
            for (int i = 0; i < n; i++) {
             for (int j = 0; j < n; j++) {
                    int id = 4 * (i * n + j);
                 //判断字符并进行合并
                    if(chars[i][j] == ' '){
                     unionFind.union(id, id + 1);
                        unionFind.union(id + 1, id + 2);
                     unionFind.union(id + 2, id + 3);
                    }else if(chars[i][j] == '/'){
                     unionFind.union(id, id + 3);
                        unionFind.union(id + 1, id + 2);
                 }else{
                        unionFind.union(id + 1, id);
                     unionFind.union(id + 2, id + 3);
                    }
    
                 //将相邻的单元格进行合并
                 //横向-向右合并
                 if(j + 1 < n){
                     unionFind.union(id + 1, id + 7);
                 }
                    //纵向-向下合并
                 if(i + 1 < n){
                        unionFind.union(id + 2, 4 * ((i + 1) * n + j));
                    }
                }
            }
            return unionFind.getCount();
    
     }
    
        class UnionFind{
            int[] parent;
            int count;
    
            public UnionFind(int n) {
                this.count = n;
                parent = new int[n];
                //初始化自己指向自己
                for (int i = 0; i < n; i++) {
                    parent[i] = i;
                }
            }
    
            //合并,每次合并的时候count--
            public void union(int a, int b){
                int rootA = find(a);
                int rootB = find(b);
                //若原来就已经合并就跳出
                if(rootA == rootB) return;
                //将a指向b
                parent[rootA] = rootB;
                count--;
            }
            //路径压缩-迭代
            public int find(int a){
                while(a != parent[a]){
                    parent[a] = parent[parent[a]];
                    a = parent[a];
                }
                return parent[a];
            }
    
            public int getCount(){
                return count;
            }
        }
    
  2. 交换字符串中的元素
    我的题解1.0:

    思路: 我们通过观察可以交换的pairs对,发现相同下标作为跳板那么该问题就具有了传递性,因为题目说可以在任意次数内交换,且只需要返回最终字典序最小的结果即可
    因此我们可以考虑用并查集,这里同时使用路径压缩和按秩合并进行处理,对于输入规模较大的问题时间复杂度比只按路径压缩要快上许多
    步骤:
    1.首先我们将字符串的下标作为点,然后用并查集进行合并操作
    2.然后我们将字符串转化为字符数组,然后遍历该数组,根据它的下标找到它的根节点,并以根节点作为锚创建优先队列,将相同根节点的字符传入优先队列中,从而达到了分组的效果
    3.最后我们创建一个StringBuilder对象,通过它来合并字符,我们遍历索引0~n-1,然后找到该点的根节点,并从中取出优先队列中队首的字符,就达到了字典序最小的目的

    public String smallestStringWithSwaps(String s, List<List<Integer>> pairs) {
        //特判
           if(pairs.size() == 0) return s;
        int n = s.length();
           UnionFind unionFind = new UnionFind(n);
        //step1
           for(int i = 0; i < pairs.size(); i++){
            int idx1 = pairs.get(i).get(0);
               int idx2 = pairs.get(i).get(1);
            unionFind.union(idx1, idx2);
           }

           //step2
        char[] chars = s.toCharArray();
           Map<Integer, Queue<Character>> map = new HashMap<>(n);
        for (int i = 0; i < n; i++) {
               int root = unionFind.find(i);
            //这里有对象机会返回对象,没有就会创建一个对象然后返回
               map.computeIfAbsent(root, key -> new PriorityQueue<>()).offer(chars[i]);
     }
           //step3
     StringBuilder stringBuilder = new StringBuilder(n);
           for (int i = 0; i < n; i++) {
               int parent = unionFind.find(i);
               stringBuilder.append(map.get(parent).poll());
           }
        return stringBuilder.toString();
       }
   
       class UnionFind{
           int n;
           int[] parent;
           //秩:存储的是树的高度
           int[] rank;
   
           public UnionFind(int n) {
               this.n = n;
               parent = new int[n];
               rank = new int[n];
               //初始化parent和rank,parent初始时自己的父节点是自己,rank初始高度为1
               for (int i = 0; i < n; i++) {
                   parent[i] = i;
                   rank[i] = 1;
               }
           }
   
           public int find(int x){
               if(x != parent[x]) {
                   parent[x] = find(parent[x]);
               }
               return parent[x];
           }
   
           public void union(int x, int y){
               int rootX = find(x);
               int rootY = find(y);
               if(rootX == rootY) return;
   
               //按秩合并和路径压缩
               if(rank[rootX] == rank[rootY]){
                   parent[rootX] = rootY;
                   rank[rootY]++;
                   //以高度大的为根节点
               }else if(rank[rootX] > rank[rootY]){
                   parent[rootY] = rootX;
               }else{
                   parent[rootX] = rootY;
               }
           }
       }
我的题解2.0:

思路:这里我们用桶排序来代替哈希表+优先队列的数据结构
1.我们创建一个长度为n,宽为26的二维数组,这样就建立了字符和索引的对应关系,并将可交换的索引位置-即路径放入并查集中。
2.同样的我们在遍历字符串的时候根据索引找出其对应的根节点,将字符存入根节点的相应位置,计数器+1,这样就建立了相应的顺序,且是按照字典顺序递增的。
3.最后我们利用StringBuilder对象重构字符串通过再次遍历索引从0~n-1,根据并查集找出对应的根节点,然后取出计数器大于0的字符,计数器-1,本轮迭代终止,继续直到n-1,最后返回结果即可

public String smallestStringWithSwaps(String s, List<List<Integer>> pairs) {
        //特判
        if(pairs.size() == 0) return s;
        //initialize
        int n = s.length();
        UnionFind unionFind = new UnionFind(n);
        //step1
        int[][] hash = new int[n][26];
        for(List<Integer> list : pairs){
            unionFind.union(list.get(0), list.get(1));
        }

        //step2
        char[] chars = s.toCharArray();
        for (int i = 0; i < n; i++) {
            int p = unionFind.find(i);
            hash[p][chars[i] - 'a']++;
        }

        //step3
        StringBuilder stringBuilder = new StringBuilder(n);
     for (int i = 0; i < n; i++) {
            int p = unionFind.find(i);
         for (int j = 0; j < 26; j++) {
                if(hash[p][j] > 0){
                 stringBuilder.append((char)(j + 'a'));
                    hash[p][j]--;
                 break;
                }
            }
     }
        return stringBuilder.toString();
    }

    class UnionFind{
        int n;
        int[] p;
        int[] r;

        public UnionFind(int n) {
            this.n = n;
            p = new int[n];
            r = new int[n];

            for (int i = 0; i < n; i++) {
                p[i] = i;
                r[i] = 1;
            }
        }

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

        void union(int x, int y){
            int rootX = find(x);
            int rootY = find(y);
            if(rootX == rootY) return;

            if(r[rootX] == r[rootY]){
                p[rootX] = rootY;
                r[rootY]++;
            }else if(r[rootX] < r[rootY]){
                p[rootX] = rootY;
            }else{
                p[rootY] = rootX;
            }
        }
    }
  1. 移除最多的同行或同列石头
    我的题解:

    思路: 移除最多的同行或同列的石头,可以用图的思维来理解,可以移除最多的同行或同列就是将所有联通的点都移除掉,这样ans = 点的个数 - 联通量的个数(按边合并)
    步骤:

1.首先我们先实例化UnionFind对象然后将矩阵的的点用并查集对横坐标和纵坐标进行合并操作,这里我们使用哈希表存储对应下标的父节点(因为下标的特殊性)
2.返回: n - getCount()

 public int removeStones(int[][] stones) {
        int n = stones.length;
        //step1
        UnionFind unionFind = new UnionFind();
     for(int[] arr : stones){
            int x = arr[0];
            int y = ~arr[1];
            unionFind.union(x, y);
        }
        //step2
        //返回的个数是顶点的个数 - 未合并的个数
        return n - unionFind.getCount();
    }

    class UnionFind{
        Map<Integer, Integer> parent = new HashMap();
        int count = 0;
        int find(int x){
            //若该点不存在就新建一个点,点的个数+1,并将该点的父节点指向自己
            if(parent.get(x) == null){
                parent.put(x, x);
                count++;
            }
            if(x != parent.get(x)){
                parent.put(x, find(parent.get(x)));
            }
            return parent.get(x);
        }

        public int getCount() {
            return count;
        }
        //合并操作,若可以合并-即是互联的,那么计数器-1
        void union(int x, int y){
            int rootX = find(x);
            int rootY = find(y);
            if(rootX == rootY) return;
            parent.put(rootX, rootY);
            count--;
        }
    }
  1. 账户合并
    我的题解:

    思路: 根据题意,我们需要将含有相同邮箱的账户进行升序合并,这里核心是相同的邮箱,因此我们建立 邮箱 - 账户id, 以及账户id - 账户名,这样的对应关系
    我们可以使用并查集,对相同邮箱账户,进行合并操作,然后我们将相同邮箱的账户,合并到一个账户上,并对该账户进行升序排序,最后重构后返回结果
    步骤:
    1.我们分别创建两个哈希表,分别表示邮箱 - 邮箱id, 邮箱id - 账户,并用实参对哈希表进行初始化操作
    2.我们根据实参中的信息将,含有相同邮箱的利用并查集连接在一起
    3.我们创建一个哈希表存储邮箱id-合并后的邮箱
    4.我们对哈希表进行重构,重构成list类型,将id所对应的账户名作为第一个元素,排序后的邮箱作为后续元素加入list中,最后返回list

    class Solution {
        public List<List<String>> accountsMerge(List<List<String>> accounts) {
             int id = 0;
            //step1
            Map<String, Integer> emailForId = new HashMap<>();
            Map<Integer, String> idForAccount = new HashMap<>();
            for(List<String> account : accounts){
                String name = account.get(0);
                for (int i = 1; i < account.size(); i++) {
                    //若不含有该邮箱,就给该邮箱创建id并就将该邮箱加入该账户
                    String email = account.get(i);
                   if(!emailForId.containsKey(email)){
                       emailForId.put(email, id++);
                       idForAccount.put(id - 1, name);
                   }
                }
            }
    
            //step2
            UnionFind unionFind = new UnionFind(id);
            for(List<String> account : accounts){
                int first = emailForId.get(account.get(1));
                for (int i = 2; i < account.size(); i++) {
                    int other = emailForId.get(account.get(i));
                    //合并
                    unionFind.union(first, other);
                }
            }
            //step3
            Map<Integer, List<String>> tmp = new HashMap<>();
            for(Map.Entry<String, Integer> entry : emailForId.entrySet()){
                String email = entry.getKey();
                int idx = entry.getValue();
                //获取根节点
                int root = unionFind.find(idx);
                List<String> list = tmp.getOrDefault(root, new ArrayList<>());
                //将当前邮箱加入
                list.add(email);
                tmp.put(root, list);
            }
    
            //step4
         List<List<String>> ans = new ArrayList<>();
            for(Map.Entry<Integer, List<String>> entry : tmp.entrySet()){
             int idx = entry.getKey();
                List<String> emails = entry.getValue();
             Collections.sort(emails);
                String account = idForAccount.get(idx);
    
                List<String> cur = new ArrayList<>();
                cur.add(account);
                cur.addAll(emails);
                ans.add(cur);
            }
    
            return ans;
        }
    
        class UnionFind{
            int[] p;
            int[] r;
    
            public UnionFind(int n) {
                p = new int[n];
                r = new int[n];
                for (int i = 0; i < n; i++) {
                    p[i] = i;
                    r[i] = 1;
                }
            }
    
            int find(int x){
                if(x != p[x]){
                    p[x] = find(p[x]);
                }
                return p[x];
            }
    
            void union(int x, int y){
                int rootX = find(x);
                int rootY = find(y);
                if(rootX == rootY) return;
                if(r[rootX] == r[rootY]){
                    p[rootX] = rootY;
                    r[rootY]++;
                }else if(r[rootX] < r[rootY]){
                    p[rootX] = rootY;
                }else{
                    p[rootY] = rootX;
                }
            }
        }
    }
    
  2. 打砖块
    我的题解:

    思路:通过分析该问题发现,顶部的节点与下面的节点具有连通性才会被保留下来,若移除目标元素后,该连通性被打断那么该点后续的元素都会被移除
    本次移除的元素就是后续的元素。我们可以用并查集来处理该问题。
    1.我们首先创建一个矩阵copy对grid中除了hits中的砖块加入到矩阵相应为止
    2.建图,把砖块和砖块的连接关系加入并查集,size表示二维网络的大小,也表示虚拟的屋顶在并查集中的编号
    3.按照hits的逆序,在copy中补回砖块,把每一次因为补回砖块而与屋顶相连的砖块的增量记录到ans中

    class Solution {
        int m, n;
        int[] fx = {0, 0, -1, 1};
        int[] fy = {-1, 1, 0, 0};
        public int[] hitBricks(int[][] grid, int[][] hits) {
            m = grid.length;
            n = grid[0].length;
    
            //step1
            int[][] copy = new int[m][n];
            for (int i = 0; i < m; i++) {
                for (int j = 0; j < n; j++) {
                    copy[i][j] = grid[i][j];
                }
            }
    
            for (int i = 0; i < hits.length; i++) {
                copy[hits[i][0]][hits[i][1]] = 0;
            }
    
            //step2
            int size = m * n;
            //这里最后一个位置代表屋顶
            UnionFind unionFind = new UnionFind(size + 1);
            //将第一行的砖块与屋顶连接
            for (int i = 0; i < n; i++) {
               if(copy[0][i] == 1){
                   unionFind.union(i, size);
               }
            }
    
            //合并其它行的砖块
            for (int i = 1; i < m; i++) {
                for (int j = 0; j < n; j++) {
                    if(copy[i][j] == 1){
                        //若当前行是砖块,且上方也是砖块就合并
                        if(copy[i - 1][j] == 1){
                            unionFind.union(getIdx(i - 1, j), getIdx(i, j));
                        }
                        //若当前列是砖块,左侧也是砖块就合并
                        if(j > 0 && copy[i][j - 1] == 1){
                            unionFind.union(getIdx(i, j - 1), getIdx(i, j));
                        }
                    }
                }
            }
    
            //step3
            int hitsLen = hits.length;
            int[] ans = new int[hitsLen];
            for (int i = hitsLen - 1; i >= 0 ; i--) {
                int x = hits[i][0], y = hits[i][1];
                //若原来本位置是空的,那么就跳过
                if(grid[x][y] == 0) continue;
                //获取补回前当前有多少砖块与屋顶相连
                int prev = unionFind.getNode(size);
    
                //若补回的该节点在第一行,要与屋顶相连
                if(x == 0) {
                    unionFind.union(y, size);
                }
                //四个方向上看,如果相邻的4个方向上有砖块就合并
                for (int j = 0; j < 4; j++) {
                    int newX = fx[j] + x;
                    int newY = fy[j] + y;
                    //若在范围内,且有砖块
                    if(inArea(newX, newY) && copy[newX][newY] == 1){
                        unionFind.union(getIdx(newX, newY), getIdx(x, y));
                    }
                }
    
                //补回后有多少砖块与屋顶相连
                int cur = unionFind.getNode(size);
                //更新ans[i]
                ans[i] = Math.max(0, cur - prev - 1);
                //补上该方块
                copy[x][y] = 1;
    
            }
            return ans;
    
     }
        //是否越界
     private boolean inArea(int x, int y){
            return x >= 0 && x < m && y >= 0 && y < n;
     }
    
     //将二维坐标转化为一维
        private int getIdx(int x, int y){
            return x * n + y;
        }
        //路径压缩和按秩合并
        class UnionFind{
         int[] parent;
            //节点的个数
            int[] size;
    
            public UnionFind(int n) {
                parent = new int[n];
                size = new int[n];
                for (int i = 0; i < n; i++) {
                    parent[i] = i;
                    size[i] = 1;
                }
            }
    
    
            //路径压缩
            int find(int x){
                if(x != parent[x]){
                    parent[x] = find(parent[x]);
                }
                return parent[x];
            }
    
            //合并
            void union(int x, int y){
                int rootX = find(x);
                int rootY = find(y);
                if(rootX == rootY) return;
                //合并的同时累加上元素的个数
                parent[rootX] = rootY;
                size[rootY] += size[rootX];
            }
    
            //获取节点个数
            int getNode(int x){
                return size[find(x)];
            }
        }
    }
    
    
  3. 保证图可完全遍历
    我的题解:

    思路: 我们通过一个个将边加入并查集中,来看加入前后,点的连通性来进行计数,这里我们需要优先考虑加入公共边,然后才是Alice和Bob的边
    1.我们先创建并查集common,并利用长度n进行初始化操作,并声明count进行计数
    2.我们扫描edges,看当前类型是不是公共边,若是则先看两点是否联通,若是,则不添加count++,否则添加
    3.我们将common拷贝两份,分别代表alice和bob,分别看是不是自己的路径边,若是则看加入前两点是否已联通,若是则不添加,count++,否则添加
    4.我们看alice和bob所有的联通分量是否相同,若相同的返回count,否则返回-1

     * @param n
         * @param edges
         * @return
         */
        public int maxNumEdgesToRemove(int n, int[][] edges) {
            //step1
            UnionFind common = new UnionFind(n);
            int count = 0;
    
            //step2
            for(int[] edge : edges){
                if(edge[0] == 3){
                    int x = edge[1] - 1, y = edge[2] - 1;
                    if(common.isConnected(x, y)) count++;
                    else common.union(x, y);
                }
            }
    
            //step3
            int[] parent = common.getParent();
            int[] size = common.getSize();
            UnionFind alice = new UnionFind(parent, size);
            UnionFind bob = new UnionFind(parent, size);
    
            for(int[] edge : edges){
                if(edge[0] == 1){
                    int x = edge[1] - 1, y = edge[2] - 1;
                    if(alice.isConnected(x, y)) count++;
                    else alice.union(x, y);
                }
            }
    
            for(int[] edge : edges){
                if(edge[0] == 2){
                    int x = edge[1] - 1, y = edge[2] - 1;
                    if(bob.isConnected(x, y)) count++;
                    else bob.union(x, y);
                }
            }
    
            //step4
            return alice.allConnected() && bob.allConnected() ? count : -1;
        }
        //实现路径压缩和按秩合并,其中按秩合并中的秩是节点的个数
        class UnionFind{
            int[] parent;
            int[] size;
            UnionFind(int n){
                parent = new int[n];
                size = new int[n];
                for (int i = 0; i < n; i++) {
                    parent[i] = i;
                    size[i] = i;
                }
            }
    
            /**
             * 实现复制
             * @param parent
             * @return
             */
            UnionFind(int[] parent, int[] size){
                int n = parent.length;
                this.parent = new int[n];
                this.size = new int[n];
                for (int i = 0; i < n; i++) {
                    this.parent[i] = parent[i];
                    this.size[i] = size[i];
                }
            }
    
            public int[] getParent() {
                return parent;
            }
     
            public int[] getSize() {
             return size;
            }
     
            /**
          * 查询-路径压缩
             * @param x
             * @return
             */
            int find(int x){
                if(x != parent[x]){
                    parent[x] = find(parent[x]);
                }
                return parent[x];
            }
    
            /**
             * 按秩合并
             * @param x
             * @param y
             */
            void union(int x, int y){
                int rootX = find(x);
                int rootY = find(y);
                if(rootX == rootY) return;
                parent[rootX] = rootY;
                size[rootY] += size[rootX];
            }
    
            /**
             * 判断两个点是否相连
             * @param x
             * @param y
             * @return
             */
            boolean isConnected(int x, int y){
                return find(x) == find(y);
            }
    
            /**
             * 所有点是否联通即父节点是否相同
             * @return
             */
            boolean allConnected(){
                for (int i = 1; i < parent.length; i++) {
                    if(!isConnected(i - 1, i)) return false;
                }
                return true;
            }
        }
    
  4. 水位上升的泳池中游泳
    我的题解:

    思路: 根据题意grid中元素的每个值都不一样,且值从0~n*n-1是连续的。因为我们可以新建一个数组,数组的索引代表元素的值,元素的位置代表,数组的值
    这样就能从0~n-1连续的将对应位置,遍历到,然后我们向四周进行扩展,若该位置的高度小于或等于该索引,那么就连接,若已经连接就跳过。然后我们看透节点0和末尾节点是否相连
    若相连就返回
    步骤:
    1.我们新建数组idx代表不同高度的位置,并对并查集进行初始化
    2.我们从前到后进行遍历,每次遍历时以当前位置为基础,看看四周是否有和当前高度相同和更新的,若有则将这些位置用并查集连接起来(若未连接)
    3.最后我们看首位是否相连,若相连就返回该索引

    class Solution {
       int n;
        int[] fx = {0, 0, -1, 1}, fy = {-1, 1, 0, 0};
        public int swimInWater(int[][] grid) {
            n = grid.length;
            //step1
            int[] idx = new int[n * n];
            UnionFind unionFind = new UnionFind(n * n);
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    idx[grid[i][j]] = getIdx(i, j);
                }
            }
    
            //step2
            for (int i = 0; i < n * n; i++) {
                int x = idx[i] / n;
                int y = idx[i] % n;
                for (int j = 0; j < 4; j++) {
                    int newX = x + fx[j];
                    int newY = y + fy[j];
                    if(newX >= 0 && newX < n && newY >= 0 && newY < n && grid[newX][newY] <= i){
                        unionFind.union(idx[i], getIdx(newX, newY));
                        //step3
                        if(unionFind.isConnected(0, n * n - 1)){
                            return i;
                        }
                    }
                }
    
            }
            return 0;
        }
    
       
    
        class UnionFind{
            int[] parent;
            UnionFind(int n){
                parent = new int[n];
                for (int i = 0; i < n; i++) {
                    parent[i] = i;
                }
            }
    
            int find(int x){
                if(x != parent[x]){
                    parent[x] = find(parent[x]);
                }
                return parent[x];
            }
    
            void union(int x, int y){
                int rootX = find(x);
                int rootY = find(y);
                if(rootX == rootY) return;
                parent[rootX] = rootY;
            }
    
            boolean isConnected(int x, int y){
                return find(x) == find(y);
            }
        }
    
        int getIdx(int x, int y){
            return x * n + y;
        }
    }
    
  5. 839. 相似字符串组
    我的题解:

    思路: 每一个字符串可以代表一个状态,若一个状态可以转移到另外一个状态,那么这两个点之间有一条边,因为不关心中间过程我们可以用并查集来解决。在并查集中,求组的个数是非常简单的,只需要对根节点指向自己的节点进行计数即可
    步骤:
    1.初始化m,代表m个点,n代表字符串的长度,初始化并查集
    2.枚举任意两个,字符串,看是否存在公共边,若存在就用并查集连接
    3.返回组数

    public int numSimilarGroups(String[] strs) {
            //step1
            int m = strs.length;
            UnionFind unionFind = new UnionFind(m);
            //step2
            for (int i = 0; i < m - 1; i++) {
                for (int j = i + 1; j < m; j++) {
                    if(isConnected(strs[i], strs[j])){
                        unionFind.union(i, j);
                    }
                }
            }
            //step3
            return unionFind.getGroup();
        }
    
        class UnionFind{
            int[] p;
            UnionFind(int n){
                p = new int[n];
                for (int i = 0; i < n; i++) {
                    p[i] = i;
                }
            }
    
            int find(int x){
                if(x != p[x]){
                    p[x] = find(p[x]);
                }
                return p[x];
            }
    
            void union(int x, int y){
                int rootX = find(x), rootY =find(y);
                if(rootX == rootY) return;
                p[rootX] = rootY;
            }
    
            /**
             * 获得分组的个数,只需要对根节点指向自己的节点进行计数即可,然后返回
             * @return
             */
            int getGroup(){
                int count = 0;
                for (int i = 0; i < p.length; i++) {
                    if(p[i] == i) count++;
                }
                return count;
            }
        }
    
        /**
         * 对字符串中不同的字符进行计数,合法的有为0,或为2
         * @param s1
         * @param s2
         * @return
         */
        boolean isConnected(String s1, String s2){
            int len = s1.length(), count = 0;
            char[] chars1 = s1.toCharArray(), chars2 = s2.toCharArray();
            for (int i = 0; i < len; i++) {
                if(chars1[i] != chars2[i]) count++;
            }
            return count == 0 || count == 2;
        }
    

你可能感兴趣的:(leetcode,算法,java)