当前位置:   article > 正文

数据结构之并查集

数据结构之并查集

并查集原理

首先需要明确, 并查集是一个森林

在一些应用问题中, 需要将n个不同的元素划分成一些不相交的集合. 开始时, 每个元素自成一个单元素集合, 然后按一定的规律将归于同一组元素的集合合并. 在此过程中要反复用到查询某一个元素归属于哪个集合的运算. 适合于描述这类问题的抽象数据类型称为并查集(union-findset).

举例: 

有10个人来自不同的学校, 起先互不相识, 每个学生都是一个独立小团体, 现给这些学生进行编号:{0, 1, 2, 3,4, 5, 6, 7, 8, 9}; 给以下数组用来存储该小集体, 数组中的数字的绝对值代表: 该小集体中具有成员的个数

现在学生们按照家乡划分成了三组: 江苏学生小分队s1={0,6,7,8},山东学生小分队s2={1,4,9}, 浙江学生小分队s3={2,3,5}就相互认识了, 10个人形成了三个小团体. 假定以坐标小的0,1,2担任队长:

 一趟旅行之后, 每个小分队成员就互相熟悉, 形成了一个朋友圈, 3个小团体合并成了一个大团体:

仔细观察数组中的下标, 可以得出:

1. 数组中一个位置的值如果为负数, 那它就是树的根, 负数的绝对值就是这棵树的数据的个数.
2. 数组中一个位置的值如果为非负数, 代表该位置值双亲的下标 

通过以上例子可知,并查集一般可以解决以下问题:

1. 查找元素属于哪个集合
        沿着数组表示的树形关系, 往上一直找到根(即: 树中中元素为负数的位置)
2. 查看两个元素是否属于同一个集合
        沿着数组表示的树形关系往上一直找到树的根, 如果根相同表明在同一个集合, 否则不在
3. 将两个集合归并成一个集合
        将两个集合中的元素合并
        将一个集合名称改成另一个集合的名称
4. 集合的个数
        遍历数组, 数组中元素为负数的个数即为集合的个数。 


并查集实现 

主要实现三个接口, 其中FindRoot接口是Union接口的子函数.

1. 构造函数将数组全部初始化为-1

2. Union是将两个位置所在的并查集合并, 要想合并要先找它们的根看是否在一个并查集, 是的话就不需要合并, 所以先实现FindRoot. 合并注意一个细节, 可以把集合元素少的向集合元素多的去合并, 因为被合并的那个集合所有节点的深度都会+1, 所以让更少的元素集合合并到更多的元素集合可以提高FindRoot的效率.

3. FindRoot 一直查询位置对应的值是不是负值即可

4. SetSize查询共有几个并查集, 统计数组中负数的个数即可.

在查询的过程中我们还可以顺便实现压缩路径的操作:

在不断的Union之后, 可能有的节点的层数会很深, FindRoot的时候需要向上查找很多次, 所以在每一次查找完Root之后, 通过比对 当前位置的直接根 是否等于查找之后的Root, 如果不是说明该节点的层次大于2层, 就可以合并了, 把该节点的下标修改为之前查找到的Root即可, 而且可以顺着该节点依次向上遍历, 可以把路径上所有高度大于2的结点全部合并到Root之下.

  1. #include <iostream>
  2. #include <vector>
  3. class UnionFindSet
  4. {
  5. public:
  6. UnionFindSet(int n)
  7. :ufs(n,-1)
  8. {}
  9. void Union(int i, int j)
  10. {
  11. int rooti = FindRoot(i);
  12. int rootj = FindRoot(j);
  13. if (rooti == rootj)
  14. return;
  15. else
  16. {
  17. //保持rooti为集合内元素多的根, 注意ufs[rooti]内是负值, 数值大的元素反而少
  18. if (ufs[rooti] > ufs[rootj])
  19. std::swap(rooti, rootj);
  20. ufs[rooti] += ufs[rootj];
  21. ufs[rootj] = rooti;
  22. }
  23. }
  24. int FindRoot(int x)
  25. {
  26. int root = x;
  27. while (ufs[root] >= 0)
  28. {
  29. root = ufs[root];
  30. }
  31. //查找途中顺便压缩路径
  32. while(ufs[x] >=0)
  33. {
  34. int lastparent = ufs[x];
  35. ufs[x] = root;
  36. x = lastparent;
  37. }
  38. return root;
  39. }
  40. int SetSize()
  41. {
  42. int ret = 0;
  43. int n = ufs.size();
  44. for (int i = 0; i < n; i++)
  45. {
  46. if (ufs[i] < 0)
  47. ret++;
  48. }
  49. return ret;
  50. }
  51. //判断两个节点是否在同一个并查集, 克鲁斯卡尔算法会用到
  52. bool InSet(size_t a, size_t b)
  53. {
  54. size_t root1 = FindRoot(a);
  55. size_t root2 = FindRoot(b);
  56. return root1 == root2;
  57. }
  58. private:
  59. std::vector<int> ufs;
  60. };

并查集解决例题

 省份数量

直接把并查集拷贝过来, isConnected[i][j]==1就Union(i,j), 最后返回并查集的个数即可, 只需要调用并查集的两个接口:

  1. class UnionFindSet
  2. {
  3. public:
  4. UnionFindSet(int n)
  5. :ufs(n,-1)
  6. {}
  7. void Union(int i, int j)
  8. {
  9. int rooti = FindRoot(i);
  10. int rootj = FindRoot(j);
  11. if (rooti == rootj)
  12. return;
  13. else
  14. {
  15. if (rooti > rootj)
  16. std::swap(rooti, rootj);//保持rooti为小坐标
  17. ufs[rooti] += ufs[rootj];
  18. ufs[rootj] = rooti;
  19. }
  20. }
  21. int FindRoot(int x)
  22. {
  23. int parent = x;
  24. while (ufs[parent] >= 0)
  25. {
  26. parent = ufs[parent];
  27. }
  28. //查找途中顺便压缩路径
  29. if (x >= 0 && parent != ufs[x])
  30. {
  31. while (x != parent)
  32. {
  33. int lastparent = ufs[x];
  34. ufs[x] = parent;
  35. x = lastparent;
  36. }
  37. }
  38. return parent;
  39. }
  40. int SetSize()
  41. {
  42. int ret = 0;
  43. int n = ufs.size();
  44. for (int i = 0; i < n; i++)
  45. {
  46. if (ufs[i] < 0)
  47. ret++;
  48. }
  49. return ret;
  50. }
  51. private:
  52. std::vector<int> ufs;
  53. };
  54. class Solution {
  55. public:
  56. int findCircleNum(vector<vector<int>>& isConnected)
  57. {
  58. int x = isConnected.size();
  59. int y = isConnected[0].size();
  60. UnionFindSet s(x);
  61. for(int i = 0; i < x; i++)
  62. {
  63. for(int j = 0; j < y; j++)
  64. {
  65. if(i == j)
  66. continue;
  67. if(isConnected[i][j] == 1)
  68. {
  69. s.Union(i,j);
  70. }
  71. }
  72. }
  73. return s.SetSize();
  74. }
  75. };

用并查集的思想也可以, 只不过把并查集的关键代码重写一遍: 

  1. class Solution {
  2. public:
  3. int findCircleNum(vector<vector<int>>& isConnected)
  4. {
  5. int x = isConnected.size();
  6. int y = isConnected[0].size();
  7. vector<int> ufs(x,-1);
  8. auto FindRoot = [&ufs](int x)
  9. {
  10. int parent = x;
  11. while(ufs[parent] >= 0)
  12. {
  13. parent = ufs[parent];
  14. }
  15. return parent;
  16. };
  17. for(int i = 0; i < x; i++)
  18. {
  19. for(int j = 0; j < y; j++)
  20. {
  21. if(isConnected[i][j] == 1)
  22. {
  23. int root1 = FindRoot(i);
  24. int root2 = FindRoot(j);
  25. if(root1 != root2)
  26. {
  27. ufs[root1] += ufs[root2];
  28. ufs[root2] = root1;
  29. }
  30. }
  31. }
  32. }
  33. int ret =0;
  34. for(auto e:ufs)
  35. {
  36. if(e <0)
  37. ret++;
  38. }
  39. return ret;
  40. }
  41. };

 等式方程的可满足性

 将相等的元素合并到一个并查集, 最后查询不相等的元素的是不是在一个并查集下即可:

  1. class Solution {
  2. public:
  3. bool equationsPossible(vector<string>& equations)
  4. {
  5. vector<int> ufs(26,-1);
  6. auto FindRoot = [&ufs](int x)
  7. {
  8. while(ufs[x] >=0)
  9. {
  10. x = ufs[x];
  11. }
  12. return x;
  13. };
  14. for(auto& equation: equations)
  15. {
  16. if(equation[1] == '=')
  17. {
  18. int root1 = FindRoot(equation[0]-'a');
  19. int root2 = FindRoot(equation[3]-'a');
  20. if(root1 != root2)
  21. {
  22. ufs[root1] += ufs[root2];
  23. ufs[root2] = root1;
  24. }
  25. }
  26. }
  27. for(auto& equation: equations)
  28. {
  29. if(equation[1] == '!')
  30. {
  31. int root1 = FindRoot(equation[0]-'a');
  32. int root2 = FindRoot(equation[3]-'a');
  33. if(root1 == root2)
  34. return false;
  35. }
  36. }
  37. return true;
  38. }
  39. };

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/480528
推荐阅读
相关标签
  

闽ICP备14008679号