并查集(Disjoint set,Union-Find)是一种用来管理元素分组情况的数据结构。并查集的一种高效实现是:使用树的根节点「代表」一个集合,同一棵树上的所有节点属于一个集合。只要两个元素的根节点相同,就视为属于同一个集合。使用树的根节点代表一个集合的方法,称之为「代表元」法。

  • 初始化(Init):将每个元素所在集合初始化为其自身。
  • 合并(Union):将两个元素所属的集合合并为一个集合。
  • 查找(Find):查找元素所在的集合,即根节点。

应用

  • 最小生成树:Kruskal 算法
  • 图的联通分量
  • 动态联通性格
  • 二分图判断等

局限

  • 不支持拆分(split)操作:任何节点一旦成为其他节点的子节点后,将永远不可能再成为树根。
  • 集合必须是不相交的(disjoint):同一个元素不能属于多个集合。
  • 代表元不记录集合成员信息:
    • 父节点不记录子节点的信息。
    • 查找图中某节点所在的连通分量的所有节点需要再次扫描或者维护额外信息。

代码模板

  1. //并查集 init(n),Find(x) 以及 Union(x, y) 函数,分别对应初始化,查找,合并操作
  2. class UnionFind {
  3. int count;
  4. int[] parent;
  5. int[] rank;
  6. //init初始化
  7. public UnionFind(int n) {
  8. //连通分量个数
  9. count = 0;
  10. //集合的代表元素 parent 数组
  11. parent = new int[n];
  12. /*考虑到优化并查集的算法时间复杂度可以采用路径压缩和按秩合并两种方式
  13. 下面使用按秩合并,为了实现按秩合并,秩是衡量一颗树的指标,它可以是树中节点数,
  14. 也可以是树的最大深度。我们使用 rank 数组记录额外信息*/
  15. rank = new int[n];
  16. for (int i = 0; i < n; ++i) {
  17. // 初始时每个集合的代表元素就是自身
  18. parent[i] = i;
  19. ++count;
  20. //初始化的时候每个集合节点rank都一样 只有一个元素
  21. rank[i] = 1;
  22. }
  23. }
  24. //最坏情况下树退化为链 Find 函数最坏时间复杂度是:O(n),n 为节点数。
  25. /* 查找 x 所在集合的代表元素,即父节点 */
  26. public int find(int x) {
  27. while (x != parent[x]) {
  28. // 不断循环查找根结点
  29. x = parent[x];
  30. }
  31. return parent[x];
  32. }
  33. /* union函数 合并 x y 所在集合 */
  34. public void union(int x, int y) {
  35. //查询x所在集合
  36. int rootx = find(x);
  37. //查询y所在集合
  38. int rooty = find(y);
  39. if (rootx != rooty) {
  40. // x 所在集合比 y 所在集合节点数多
  41. // 将 y 所在集合合并到 x 所在集合,并更新 x 所在集合的节点数信息
  42. if (rank[rootx] > rank[rooty]) {
  43. parent[rooty] = rootx;
  44. rank[rootx] +=rank[rooty];
  45. } else {
  46. // x 所在集合不比 y 所在集合节点数多
  47. // 将 x 所在集合合并到 y 所在集合,并更新y所在集合的信息
  48. parent[rootx] = rooty;
  49. rank[rooty] += rank[x];
  50. }
  51. //合并一次就要减少一次连通分量的个数
  52. --count;
  53. }
  54. }
  55. //返回连通分量个数
  56. public int getCount() {
  57. return count;
  58. }
  59. // 判断两个节点是否在一个连通分量里面
  60. public boolean isConnect(int x,int y){
  61. int rootx = find(x),rooty = find(y);
  62. return rootx == rooty;
  63. }
  64. }

例题1:

lc785 二分图的判定 https://leetcode-cn.com/problems/is-graph-bipartite/
IMG20211112175730.png

AC 代码

  1. class Solution {
  2. public boolean isBipartite(int[][] graph) {
  3. int len = graph.length;
  4. // 初始化并查集
  5. UnionFind uf = new UnionFind(len);
  6. // 遍历每个顶点,将当前顶点的所有邻接点进行合并
  7. for (int i = 0; i < len; i++) {
  8. int[] adjs = graph[i];
  9. // 遍历该顶点的邻接点
  10. for (int w: adjs) {
  11. // 若某个邻接点与当前顶点已经在一个集合中了,说明不是二分图,返回 false。
  12. if (uf.isConnected(i, w)) {
  13. return false;
  14. }
  15. // 所有邻接点进行合并
  16. uf.union(adjs[0], w);
  17. }
  18. }
  19. return true;
  20. }
  21. }
  22. // 并查集
  23. class UnionFind {
  24. int[] roots;
  25. public UnionFind(int n) {
  26. roots = new int[n];
  27. for (int i = 0; i < n; i++) {
  28. roots[i] = i;
  29. }
  30. }
  31. public int find(int i) {
  32. if (roots[i] == i) {
  33. return i;
  34. }
  35. return roots[i] = find(roots[i]);
  36. }
  37. // 判断 p 和 q 是否在同一个集合中
  38. public boolean isConnected(int p, int q) {
  39. return find(q) == find(p);
  40. }
  41. // 合并 p 和 q 到一个集合中
  42. public void union(int p, int q) {
  43. roots[find(p)] = find(q);
  44. }
  45. }

例题二

lc839:https://leetcode-cn.com/problems/similar-string-groups/

IMG20211113114612.png
思路将每个字符串看成图中的一个顶点,判断是否为相似的字符串,如果相似则用并查集合并
最后检查并查集里面有多少连通分量,即相似的字符串组

AC代码:

  1. class Solution {
  2. public int numSimilarGroups(String[] strs) {
  3. int len = strs.length;
  4. int strlen = strs[0].length();
  5. UnionFind uf = new UnionFind(len);
  6. for(int i=0;i<len;i++){
  7. for(int j=i+1;j<len;j++){
  8. // 如果两个点已经在一个连通分量里面了 就不需要合并了
  9. if(uf.isConnect(i,j))continue;
  10. if(check(strs[i],strs[j],strlen)){
  11. // 合并两个字符串
  12. uf.union(i,j);
  13. }
  14. }
  15. }
  16. // 返回连通分量个数
  17. return uf.getCount();
  18. }
  19. // 判断给定字符串是否相似
  20. public boolean check(String a, String b, int len) {
  21. int num = 0;
  22. for (int i = 0; i < len; i++) {
  23. if (a.charAt(i) != b.charAt(i)) {
  24. num++;
  25. if (num > 2) {
  26. return false;
  27. }
  28. }
  29. }
  30. return num == 0 || num == 2;
  31. }
  32. class UnionFind {
  33. int count;
  34. int[] parent;
  35. int[] rank;
  36. public UnionFind(int n) {
  37. //连通分量个数
  38. count = 0;
  39. parent = new int[n];
  40. rank = new int[n];
  41. for (int i = 0; i < n; ++i) {
  42. parent[i] = i;
  43. ++count;
  44. rank[i] = 1;
  45. }
  46. }
  47. public int find(int x) {
  48. while (x != parent[x]) {
  49. x = parent[x];
  50. }
  51. return parent[x];
  52. }
  53. public void union(int x, int y) {
  54. int rootx = find(x);
  55. int rooty = find(y);
  56. if (rootx != rooty) {
  57. if (rank[rootx] > rank[rooty]) {
  58. parent[rooty] = rootx;
  59. rank[rootx] +=rank[rooty];
  60. } else {
  61. parent[rootx] = rooty;
  62. rank[rooty] += rank[x];
  63. }
  64. //合并一次就要减少一次连通分量的个数
  65. --count;
  66. }
  67. }
  68. //返回连通分量个数
  69. public int getCount() {
  70. return count;
  71. }
  72. // 判断两个节点是否在一个连通分量里面
  73. public boolean isConnect(int x,int y){
  74. int rootx = find(x),rooty = find(y);
  75. return rootx == rooty;
  76. }
  77. }
  78. }

IMG20211113114832.png

并查集的时间复杂度

理论上,「路径压缩」与「按秩合并」,
执行 mFind操作, n-1union操作的时间复杂度是 数据结构之【并查集】 - 图4
其中 数据结构之【并查集】 - 图5 数据结构之【并查集】 - 图6 是反阿克曼函数。
反阿克曼函数是一个渐进复杂度很低的函数,通常可以认为是 常数级别的时间复杂度。感兴趣的可以自行查询
Theorem. [Tarjan 1975] Link-by-size with path compression performs any intermixed sequence of m \geq nm≥n FIND and n-1n−1 UNION operations in O(m \space \alpha(m, n))O(m α(m,n)) time, where \alpha(m, n)α(m,n) is a functional inverse of the Ackermann function. [1, 2]