算法整理之并查集

并查集

主要解决图论中的连通性问题

基本模板:
在这里插入图片描述
关键步骤:

  • 初始化将所有节点的父节点都指向自己
  • 需要路径压缩降低查询父节点的复杂度
class UnionFind{
    int[] parent; // 记录每个节点的父节点(但这个父节点不一定是顶级父节点)

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

        for(int i=0; i<n; i++){
            parent[i] = i; // 初始化每个节点的父亲就是自己
        }
    }

    // 查询节点x的顶级父节点
    public int find(int x){
        if(parent[x] == x) return x; // 当前节点的父节点就是自己,则当前节点就是顶级父节点
        else{
            // 路径压缩:当前节点父节点parent[x] = 当前节点父节点parent[x]的顶级父节点
            //         从而将路径上的所有节点都统一指向了顶级父节点
            parent[x] = find(parent[x]);
        }

        return parent[x];
    }

    // 合并两节点
    public void unite(int x, int y){
        int x_father = find(x); // x 的顶级父节点
        int y_father = find(y); // y 的顶级父节点

        // 建立连接关系
        parent[x_father] = y_father;
    }

    // 判断两个节点是否相连(等价于判读两个节点的顶级父节点是否相同)
    public boolean isConnected(int x, int y){
        int x_father = find(x);
        int y_father = find(y);
        return x_father == y_father;
    }
}

优化模板:

class UnionFind{
    int[] parent; // 记录每个节点的父节点(但这个父节点不一定是顶级父节点)
    int[] size;   // 记录每个节点对应的连通分量中的节点总数
    int n;        // 记录总节点数目
    int setCount; // 当前独立的连通分量数目 (初始化n个节点,有n个独立的连通分量)

    public UnionFind(int n){
        this.parent = new int[n];
        this.size = new int[n];
        this.n = n;
        this.setCount = n; // 初始化还没有任何连通关系,每个节点对应一个独立的连通分量.(一共n个连通分量)

        Arrays.fill(size, 1);
        for(int i=0; i<n; i++){
            parent[i] = i; // 初始化每个节点的父亲就是自己
        }
    }

    // 查询节点x的顶级父节点
    public int find(int x){
        if(parent[x] == x) return x; // 当前节点的父节点就是自己,则当前节点就是顶级父节点
        else{
            // 路径压缩:当前节点父节点parent[x] = 当前节点父节点parent[x]的顶级父节点
            //         从而将路径上的所有节点都统一指向了顶级父节点
            parent[x] = find(parent[x]);
        }

        return parent[x];
    }

    // 合并两节点 (返回是否合并成功)
    public boolean unite(int x, int y){
        int x_father = find(x); // x 的顶级父节点
        int y_father = find(y); // y 的顶级父节点

        if(x_father==y_father) return false;

        // 建立连接关系(路径压缩)
        if(size[x_father] < size[y_father]){ // y对应的连通分量大
            parent[x_father] = y_father; // x_parent父节点设置为y_parent:小集合合并到大集合中
            size[y_father] += size[x_father]; // 只需要维护顶级父节点的size即可
        }else{
            parent[y_father] = x_father; // y_parent父节点设置为x_parent
            size[x_father] += size[y_father];
        }

        setCount--; // 连通分量总数减一
        return true;
    }

    // 判断两个节点是否相连(等价于判读两个节点的顶级父节点是否相同)
    public boolean isConnected(int x, int y){
        int x_father = find(x);
        int y_father = find(y);
        return x_father == y_father;
    }
}

经典例题:

  1. LeeCode1631. 最小体力消耗路径

    • 答案

      // 并查集
      class Solution {
          public int minimumEffortPath(int[][] heights) {
              int m = heights.length;
              int n = heights[0].length;
              // 构建图中的连接关系 (int[] = {节点1的id, 节点2的id, 节点1和节点2的高度差})
              List<int[]> edges = new ArrayList<>();
              for (int i = 0; i < m; ++i) {
                  for (int j = 0; j < n; ++j) {
                      int id = i * n + j; // 顺序给每个节点确定id
                      if(i>0){ // 竖边
                          edges.add(new int[]{id, id-n, Math.abs(heights[i][j] - heights[i-1][j])});
                      }
      
                      if(j>0){ // 横边
                          edges.add(new int[]{id, id-1, Math.abs(heights[i][j] - heights[i][j-1])});
                      }
                  }
              }
      
              // 按照高度差将edges从小到大排序
              Collections.sort(edges, (a, b) -> { 
      	                return a[2] - b[2];
              });
      
              // 建立连通性
              UnionFind uf = new UnionFind(m * n); // 一共m*n个节点
              int ans = 0;
              for(int[] edge: edges){ // 遍历每条边(最先遍历高度差小的边)
                  int x = edge[0], y = edge[1], v = edge[2];
                  uf.unite(x, y); // 连接edge对应的两个节点
                  if(uf.isConnected(0, m*n-1)){ // 判断起点和终点是否已经连通
                      ans = v; // 当前的v就是目前遇到的最大高度差
                      return ans;
                  }
              }
              return ans;
          }
      }
      
      class UnionFind{
          int[] parent; // 记录每个节点的父节点(但这个父节点不一定是顶级父节点)
          int[] size;   // 记录每个节点对应的连通分量中的节点总数
          int n;        // 记录总节点数目
          int setCount; // 当前独立的连通分量数目 (初始化n个节点,有n个独立的连通分量)
      
          public UnionFind(int n){
              this.parent = new int[n];
              this.size = new int[n];
              this.n = n;
              this.setCount = n; // 初始化还没有任何连通关系,每个节点对应一个独立的连通分量.(一共n个连通分量)
      
              Arrays.fill(size, 1);
              for(int i=0; i<n; i++){
                  parent[i] = i; // 初始化每个节点的父亲就是自己
              }
          }
      
          // 查询节点x的顶级父节点
          public int find(int x){
              if(parent[x] == x) return x; // 当前节点的父节点就是自己,则当前节点就是顶级父节点
              else{
                  // 路径压缩:当前节点父节点parent[x] = 当前节点父节点parent[x]的顶级父节点
                  //         从而将路径上的所有节点都统一指向了顶级父节点
                  parent[x] = find(parent[x]);
              }
      
              return parent[x];
          }
      
          // 合并两节点 (返回是否合并成功)
          public boolean unite(int x, int y){
              int x_father = find(x); // x 的顶级父节点
              int y_father = find(y); // y 的顶级父节点
      
              if(x_father==y_father) return false;
      
              // 建立连接关系(路径压缩)
              if(size[x_father] < size[y_father]){ // y对应的连通分量大
                  parent[x_father] = y_father; // x_parent父节点设置为y_parent:小集合合并到大集合中
                  size[y_father] += size[x_father]; // 只需要维护顶级父节点的size即可
              }else{
                  parent[y_father] = x_father; // y_parent父节点设置为x_parent
                  size[x_father] += size[y_father];
              }
      
              setCount--; // 连通分量总数减一
              return true;
          }
      
          // 判断两个节点是否相连(等价于判读两个节点的顶级父节点是否相同)
          public boolean isConnected(int x, int y){
              int x_father = find(x);
              int y_father = find(y);
              return x_father == y_father;
          }
      }
      
  2. LeeCode765. 情侣牵手

    • 答案

      // 关键点1: (0,1)为一对情侣,(2,3)为一对情侣。为了方便确定情侣编号,使用人的ID/2表示一对情侣的编号。
      //         (0,1)/2 = (0,0)表示ID为0和1的情侣属于第0对情侣;(2,3)/2 = (1,1)表示ID为2和3的情侣属于第1对情侣。以此类推
      // 关键点2: 某一对情侣(a,b)被拆散了,那么其中一个人一定会被安排到其他的组合,其他组合也一定会安排一个新的人到这个组合中。
      // 关键点3: 把一对情侣看作一个节点,假设把一个组合中的人安排到其他组合中,就认为两个组合之间存在一条边。
      //         那么很显然,如果某一对情侣被拆散,那么这个节点一定会与其他节点存在边的关系。存在下面两种情况:
      //             1. 节点1中的一个人安排到了节点2中,节点2中的一个人被安排到节点1中,所以节点1和节点2之间只会存在一条边,且与其他节点之间没有边;
      //             2. 节点1中的一个人安排到了节点2中,节点2中的一个人被安排到另一个节点中......, 以此往复,直到某一个节点中的一个人被安排到节点1中。此时这些节点就会组成一个环。
      //         综上所述,根据上面这个模型,n对情侣(节点)可能会组成多个环、两个节点组成的节点对、以及单个节点(代表没有被拆分的情侣)
      // 关键点4: 包含m个节点的节点环需要进行m-1次交换,才能让环中的所有情侣牵手;
      //         包含2个节点的节点对需要进行1次交换,才能让节点对中的所有情侣牵手;
      //         单个节点不需要交换就可以直接牵手。
      // 关键点5: 将n个节点使用并查集建立连接关系,求解每个连通分量上的节点数k,(k-1)就是这个连通分量上所有节点成功牵手需要交换的次数。
      class Solution {
          public int minSwapsCouples(int[] row) {
              // 建立连接关系
              UnionFind uf = new UnionFind(row.length/2); // n表示情侣对数
              for(int i=0; i<row.length; i=i+2){
                  uf.unite(row[i]/2, row[i+1]/2); // 人的ID/2表示情侣(节点)的编号
              }
              
              // 存放每个连通分量上的顶级父节点
              HashSet<Integer> finalFather = new HashSet<>();
              for(int i=0; i<row.length/2; i=i+1){
                  finalFather.add(uf.find(i)); // uf.find(i)查找第i对情侣的顶级父节点
              }
      
              // 计算交换次数
              int result = 0;
              for(int param: finalFather){ // param表示当前连通分量的顶级父节点
                  result += uf.size[param] - 1; // uf.size[param]表示当前连通分量上节点总数
              }
      
              return result;
          }
      }
      
      class UnionFind{
          int[] parent; // 记录每个节点的父节点(但这个父节点不一定是顶级父节点)
          int[] size;   // 记录每个节点对应的连通分量中的节点总数
          int n;        // 记录总节点数目
          int setCount; // 当前独立的连通分量数目 (初始化n个节点,有n个独立的连通分量)
      
          public UnionFind(int n){
              this.parent = new int[n];
              this.size = new int[n];
              this.n = n;
              this.setCount = n; // 初始化还没有任何连通关系,每个节点对应一个独立的连通分量.(一共n个连通分量)
      
              Arrays.fill(size, 1);
              for(int i=0; i<n; i++){
                  parent[i] = i; // 初始化每个节点的父亲就是自己
              }
          }
      
          // 查询节点x的顶级父节点
          public int find(int x){
              if(parent[x] == x) return x; // 当前节点的父节点就是自己,则当前节点就是顶级父节点
              else{
                  // 路径压缩:当前节点父节点parent[x] = 当前节点父节点parent[x]的顶级父节点
                  //         从而将路径上的所有节点都统一指向了顶级父节点
                  parent[x] = find(parent[x]);
              }
      
              return parent[x];
          }
      
          // 合并两节点 (返回是否合并成功)
          public boolean unite(int x, int y){
              int x_father = find(x); // x 的顶级父节点
              int y_father = find(y); // y 的顶级父节点
      
              if(x_father==y_father) return false;
      
              // 建立连接关系(路径压缩)
              if(size[x_father] < size[y_father]){ // y对应的连通分量大
                  parent[x_father] = y_father; // x_parent父节点设置为y_parent:小集合合并到大集合中
                  size[y_father] += size[x_father]; // 只需要维护顶级父节点的size即可
              }else{
                  parent[y_father] = x_father; // y_parent父节点设置为x_parent
                  size[x_father] += size[y_father];
              }
      
              setCount--; // 连通分量总数减一
              return true;
          }
      
          // 判断两个节点是否相连(等价于判读两个节点的顶级父节点是否相同)
          public boolean isConnected(int x, int y){
              int x_father = find(x);
              int y_father = find(y);
              return x_father == y_father;
          }
      }
      
  3. LeeCode2867. 统计树中的合法路径数目

    • 答案

      // 1. 质数节点连接两个非质数集合,集合数目分别为s1,s2。则质数节点对应的合法路径数=s1+s2+s1*s2
      //    质数节点连接1个非质数集合,另一端连接质数或者null,非质数集合数目为s1。则质数节点对应的合法路径数=s1
      //    质数节点连接0个非质数集合,两端连接质数或者null。则质数节点对应的合法路径数=0
      /** 使用并查集解决
      for(遍历每一个连接关系[a,b]){
          if(a非质数){
              if(b非质数) 合并集合
              else 不合并集合,记录b连接的非质数节点a,之后通过a找到a对应的非质数集合
          }else{ // a为质数 
              if(b为质数) 不合并集合
              else(b为非质数) 不合并集合,记录a连接的非质数节点b,之后通过b找到b对应的非质数集合
          }
      }
      */
      class Solution {
          private static final int N = 100001;
          private static boolean[] isPrime = new boolean[N];
          static {
              Arrays.fill(isPrime, true);
              isPrime[1] = false;
              for (int i = 2; i * i < N; i++) {
                  if (isPrime[i]) {
                      for (int j = i * i; j < N; j += i) {
                          isPrime[j] = false;
                      }
                  }
              }
          }
      
          public long countPaths(int n, int[][] edges) {
              HashMap<Integer,ArrayList<Integer>> recordPrimeConnect = new HashMap<>(); // 记录所有质数节点连接的非质数节点集合
              UnionFind uf = new UnionFind(n+1);
      
              for(int[] edge: edges){
                  int a = edge[0], b = edge[1];
                   if(!isPrime[a]){
                      if(!isPrime[b]) uf.unite(a, b);
                      else{
                          // 不合并集合,记录b连接的非质数节点a,之后通过a找到a对应的非质数集合
                          ArrayList<Integer> connected = recordPrimeConnect.getOrDefault(b, new ArrayList<Integer>());
                          connected.add(a);
                          recordPrimeConnect.put(b, connected);
                      }
                  }else{ // a为质数 
                      if(isPrime[b]) continue;
                      else{
                          // (b为非质数) 不合并集合,记录a连接的非质数节点b,之后通过b找到b对应的非质数集合
                          ArrayList<Integer> connected = recordPrimeConnect.getOrDefault(a, new ArrayList<Integer>());
                          connected.add(b);
                          recordPrimeConnect.put(a, connected);
                      }
                  }
              }
      
              // 统计有效路径数目
              // 1. 质数节点连接两个非质数集合,集合数目分别为s1,s2。则质数节点对应的合法路径数=s1+s2+s1*s2
              //    质数节点连接1个非质数集合,另一端连接质数或者null,非质数集合数目为s1。则质数节点对应的合法路径数=s1
              //    质数节点连接0个非质数集合,两端连接质数或者null。则质数节点对应的合法路径数=0
              long result = 0;
              for(Map.Entry<Integer,ArrayList<Integer>> entry: recordPrimeConnect.entrySet()){
                  // System.out.println(""+ entry.getKey() + "value:"+ entry.getValue());
                  ArrayList<Integer> connectNotPrime = entry.getValue(); // 记录当前质数节点连接的非质数节点
                  if(connectNotPrime.size()==0) continue;
                  else if(connectNotPrime.size()>=1){
                      for(int i=0; i<connectNotPrime.size(); i++){
                          result += (long)uf.size[uf.find(connectNotPrime.get(i))];
                          for(int j=i+1; j<connectNotPrime.size(); j++){
                              long s1 = (long)uf.size[uf.find(connectNotPrime.get(i))];
                              long s2 = (long)uf.size[uf.find(connectNotPrime.get(j))];
                              result += s1*s2;
                          }
                      }
                  }
              }
      
              return result;
          }
      }
      
      class UnionFind{
          int[] parent; // 记录每个节点的父节点(但这个父节点不一定是顶级父节点)
          int[] size;   // 记录每个节点对应的连通分量中的节点总数
          int n;        // 记录总节点数目
          int setCount; // 当前独立的连通分量数目 (初始化n个节点,有n个独立的连通分量)
      
          public UnionFind(int n){
              this.parent = new int[n];
              this.size = new int[n];
              this.n = n;
              this.setCount = n; // 初始化还没有任何连通关系,每个节点对应一个独立的连通分量.(一共n个连通分量)
      
              Arrays.fill(size, 1);
              for(int i=0; i<n; i++){
                  parent[i] = i; // 初始化每个节点的父亲就是自己
              }
          }
      
          // 查询节点x的顶级父节点
          public int find(int x){
              if(parent[x] == x) return x; // 当前节点的父节点就是自己,则当前节点就是顶级父节点
              else{
                  // 路径压缩:当前节点父节点parent[x] = 当前节点父节点parent[x]的顶级父节点
                  //         从而将路径上的所有节点都统一指向了顶级父节点
                  parent[x] = find(parent[x]);
              }
      
              return parent[x];
          }
      
          // 合并两节点 (返回是否合并成功)
          public boolean unite(int x, int y){
              int x_father = find(x); // x 的顶级父节点
              int y_father = find(y); // y 的顶级父节点
      
              if(x_father==y_father) return false;
      
              // 建立连接关系(路径压缩)
              if(size[x_father] < size[y_father]){ // y对应的连通分量大
                  parent[x_father] = y_father; // x_parent父节点设置为y_parent:小集合合并到大集合中
                  size[y_father] += size[x_father]; // 只需要维护顶级父节点的size即可
              }else{
                  parent[y_father] = x_father; // y_parent父节点设置为x_parent
                  size[x_father] += size[y_father];
              }
      
              setCount--; // 连通分量总数减一
              return true;
          }
      
          // 判断两个节点是否相连(等价于判读两个节点的顶级父节点是否相同)
          public boolean isConnected(int x, int y){
              int x_father = find(x);
              int y_father = find(y);
              return x_father == y_father;
          }
      }
      
  • 39
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值