并查集

134 阅读2分钟

并查集功能

  1. 有若干个样本a、b、c、d…类型假设是V
  2. 在并查集中一开始认为每个样本都在单独的集合里
  3. 用户可以在任何时候调用如下两个方法:boolean isSameSet(V x, V y) : 查询样本x和样本y是否属于一个集合;void union(V x, V y) : 把x和y各自所在集合的所有样本合并成一个集合
  4. isSameSet和union方法的代价越低越好

实现方法

  1. 每个节点都有一条往上指的指针
  2. 节点a往上找到的头节点,叫做a所在集合的代表节点
  3. 查询x和y是否属于同一个集合,就是看看找到的代表节点是不是一个
  4. 把x和y各自所在集合的所有点合并成一个集合,只需要小集合的代表点挂在大集合的代表点的下方即可

优化

  1. 节点往上找代表点的过程,把沿途的链变成扁平的
  2. 小集合挂在大集合的下面
  3. 如果方法调用很频繁,那么单次调用的代价为O(1),两个方法都如此

547. 省份数量

    public int findCircleNum(int[][] isConnected) {
        int N = isConnected.length;
        UnionFind uf = new UnionFind(N);
        for(int i = 0; i <N - 1; i++) {
            for(int  j= i + 1; j < N; j++) {
                if(isConnected[i][j] == 1) {
                    uf.union(i,j);
                }
            }
        }
        return uf.sets;
    }

    public class UnionFind {
        int sets;
        private int[] parent;
        private int[] help;
        private int[] size;

        public UnionFind(int N) {
            sets =N;
            parent = new int[N];
            help = new int[N];
            size = new int[N];
            for (int i = 0; i < N; i++) {
                 parent[i] = i;
                 size[i] = 1;
            }
        }

        private int find(int i) {
            int hi = 0;
            while(i != parent[i]) {
                help[hi++] = i;
                i = parent[i];
            }
            for(hi--;hi >= 0; hi--) {
                parent[help[hi]] = i;
            }
            return i;
        }

        private void union(int i,int j) {
            int f1 = find(i);
            int f2 = find(j);
            if(f1 != f2) {
                if(size[f1] >= size[f2]) {
                    size[f1] += size[f2];
                    parent[f2] = f1;
                } else {
                    size[f2] += size[f1];
                    parent[f1] = f2;
                }
                sets--;
            }
        }

    }

200. 岛屿数量

public static int numIslands3(char[][] board) {
   int islands = 0;
   for (int i = 0; i < board.length; i++) {
      for (int j = 0; j < board[0].length; j++) {
         if (board[i][j] == '1') {
            islands++;
            infect(board, i, j);
         }
      }
   }
   return islands;
}

// 从(i,j)这个位置出发,把所有连成一片的'1'字符,变成0
public static void infect(char[][] board, int i, int j) {
   // 越界,或者不是1,就停
   if (i < 0 || i == board.length || j < 0 || j == board[0].length || board[i][j] != '1') {
      return;
   }
   board[i][j] = 0;  // 感染当前节点
   infect(board, i - 1, j); // 感染上下左右节点
   infect(board, i + 1, j);
   infect(board, i, j - 1);
   infect(board, i, j + 1);
}

305. 岛屿数量 II

public static List<Integer> numIslands21(int m, int n, int[][] positions) {
   UnionFind1 uf = new UnionFind1(m, n);
   List<Integer> ans = new ArrayList<>();
   for (int[] position : positions) {
      // connect方法,把某个位置改为1,
      ans.add(uf.connect(position[0], position[1]));
   }
   return ans;
}

public static class UnionFind1 {
   private int[] parent;
   private int[] size;
   private int[] help;
   private final int row;
   private final int col;
   private int sets;

   public UnionFind1(int m, int n) {
      row = m;
      col = n;
      sets = 0; // 初始没有岛,设置为 0 
      int len = row * col;
      parent = new int[len];
      size = new int[len];
      help = new int[len];
   }

   private int index(int r, int c) {
      return r * col + c;
   }

   private int find(int i) { // find方法不变
      int hi = 0;
      while (i != parent[i]) {
         help[hi++] = i;
         i = parent[i];
      }
      for (hi--; hi >= 0; hi--) {
         parent[help[hi]] = i;
      }
      return i;
   }

   private void union(int r1, int c1, int r2, int c2) {
      if (r1 < 0 || r1 == row || r2 < 0 || r2 == row || c1 < 0 || c1 == col || c2 < 0 || c2 == col) {
         return;
      }
      int i1 = index(r1, c1);
      int i2 = index(r2, c2);
      if (size[i1] == 0 || size[i2] == 0) { // size为零,即对应数字为零,合并不了
         return;
      }
      int f1 = find(i1);
      int f2 = find(i2);
      if (f1 != f2) {
         if (size[f1] >= size[f2]) {
            size[f1] += size[f2];
            parent[f2] = f1;
         } else {
            size[f2] += size[f1];
            parent[f1] = f2;
         }
         sets--;
      }
   }

   public int connect(int r, int c) {
      int index = index(r, c); // 通过公式计算所在数组下标
      if (size[index] == 0) { // size等于零,说明第一次从 0 到 1,如果不是,那么显然重复操作了,集合数量不变
         parent[index] = index; // 自己是代表节点
         size[index] = 1; // 之后size记录不会消除
         sets++; // 右多了一个集合
         union(r - 1, c, r, c); // 与上下左右节点合并
         union(r + 1, c, r, c);
         union(r, c - 1, r, c);
         union(r, c + 1, r, c);
      }
      return sets; // 返回加入后,集合数量
   }

}