并查集

什么是并查集

并查集主要用于解决分组问题,它用来管理一系列不想交的集合,支持如下两种操作:

  • 合并:把两个不相交的集合合并为一个集合
  • 查询:查询某个元素的根节点,可以判断两个元素的根节点是否相等判断两个元素是否在一个并查集中。
    最佳应用: 亲戚问题

题目背景
若某个家族人员过于庞大,要判断两个人是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。
题目描述
规定:x和y是亲戚,y和z是亲戚,那么x和z也是亲戚。如果x,y是亲戚,那么x的亲戚都是y的亲戚,y的亲戚也都是x的亲戚。

可以建模,把所有有亲戚关系的人放到一个集合中,判断两个人是否是亲戚,只需要看是否在同一个集合中。因此,可以考虑用并查集进行维护。

并查集在一开始时每个集合中只有自己,然后对集合进行查找看是否在集合中,再对集合进行合并

初始化

public int[] fa;
public void init(int n) {
  fa = new int[n + 1];
  for (int i = 1; i < n + 1; i++) {
    fa[i] = i;
  }
}

使用数组fa来存储当前节点的父节点,一开始是n个不相关的集合,所以每个元素的父元素是他自己
查询

public int find(int x) {
  if (fa[x] == x) {
    return x;
  }
  return find(fa[x]);
}

查询查找的是该元素的父元素,一路往上找,直到找到根节点,如果两个元素的根节点相同,则证明他俩属于同一集合。

合并

public void merge(int i, int j) {
  fa[find(i)] = find(j);
}

i的父节点的父节点设置为j的父节点。合并两个集合,相当于x和y是亲戚,则x和y要合并到一个并查集中。
路径压缩

这个查询的效率是很低的,因为如果我们一直合并且查找,会形成一个链式的集合,复杂度会变高:

我们首先merge(2, 1)
在这里插入图片描述

然后要merge(2, 3),从2找到1,fa[1] = 3,于是就会形成如下:
在这里插入图片描述
此时再来一个元素4,我们要merge(2, 4),2找到1,1找到3,执行fa[3] = 4
在这里插入图片描述
逐渐变得复杂,可以使用路径压缩,当前节点的fa[i]记录自己的根节点,而不是父节点:

public int find(int x) {
  if (x == fa[x]) {
    return x;
  }
  // 父节点记录根节点
  fa[x] = find(fa[x]);
  // 返回父节点(根节点)
  return fa[x];
}

可以简化为一行:

public int find(int x) {
  return x == fa[x] ? x : (fa[x] = find(x);
}

按秩合并

以上图为例,我们合并2和4时。是将3置为4的父节点还是4置为3的父节点?应该是将4置为3的父节点,这样2和1的父节点还是3,不会影响到其他节点。所以我们选择深度较深的集合来合并浅集合。

初始化

public int[] fa;
public int[] rank;
public void init(int n) {
  fa = new int[n + 1];
  rank = new int[n + 1];
  for (int i = 0; i < n + 1; i++) {
    fa[i] = i;
    rank[i] = 1;
  }
}

合并

public void merge(int i, int j) {
  int x = find(i);
  int j = find(j);
  if (rank[x] <= rank[y]) {
    fa[x] = y;
  } else {
    fa[y] = find(x);
  }
  if (rank[x] == rank[y] && x != y) {
    rank[y]++;
  }
}

当深度相同的时候,我们设置y为父节点,为什么rank[y]++呢?如下图:
在这里插入图片描述
将2的父节点设为5,会变成如下:
在这里插入图片描述
即当前元素加上要合并的元素的深度,而此时两者深度相同,则为rank[y]++
实际问题: 力扣684
https://leetcode-cn.com/problems/redundant-connection/

class Solution {
    public int[] findRedundantConnection(int[][] edges) {
      int n = edges.length;
      int[] fa = new int[n + 1];
      int[] ranks = new int[n + 1];
      // init
      for (int i = 1; i < n + 1; i++) {
        fa[i] = i;
        ranks[i] = 1;
      }
      for (int[] item:
          edges) {
        int x = item[0];
        int y = item[1];
        if (find(x, fa) == find(y, fa)) {
          return item;
        } else {
          merge(x, y, fa, ranks);
        }
      }
      return new int[0];
    }
    public int find(int i, int[] fa) {
      return fa[i] == i ? i : (fa[i] = find(fa[i], fa));
    }

    public void merge(int i, int j, int[] fa, int[] rank) {
      int x = find(i, fa);
      int y = find(j, fa);
      if (rank[x] <= rank[y]) {
        fa[x] = y;
      } else {
        fa[y] = x;
      }
      if (rank[x] == rank[y] && x != y) {
        rank[y]++;
      }
    }
}

题解:https://leetcode-cn.com/problems/redundant-connection/solution/bing-cha-ji-lu-jing-ya-suo-he-an-zhi-he-o2a78/

本文中的图片以及最佳时间摘自: https://zhuanlan.zhihu.com/p/93647900


天行健,君子以自强不息。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值