1、问题描述

动态连通性问题。给定一组数,为0-N-1,然后给定一组整数对,每个整数对表明这两个整数是相连的,在接受整数对是,如果二者不相连,则输出该整数对。2与3相连,1与2相连,则1与3相连。

  1. 需要实现一个类,四个函数
  2. UF(int N);//初始化该类,有N个整数,0-N-1
  3. void union(int q , int p);//在p和q上建立一条连接
  4. int find(int p);//返回p所在的连通分量标识
  5. boolean connected(int p . int q);//如果p和q存在于同一个分量中则返回true
  6. int count();//返回连通分量的个数

2、quick-find算法

保留一个数组,记录该整数对应的连通分量标识,初始时连通分量为自身,连接两个数时,如果两个数的连通分量不一样,需要把两个连通分量的所有数的标识统一。

  1. package union_find;
  2. import java.util.Queue;
  3. /**
  4. * @author zwlstart
  5. * @create 2020-12-19 20:56
  6. */
  7. public class QuickFind {
  8. //记录每个数的连通分量的标识
  9. int[] id;
  10. //记录连通分量的个数
  11. int count;
  12. public QuickFind() {
  13. }
  14. public QuickFind(int n) {
  15. id = new int[n];
  16. //连通分量个数初始化为n
  17. count = n;
  18. //初始化每个整数的连通分量标识为自己
  19. for (int i = 0; i < n; i++) {
  20. id[i] = i;
  21. }
  22. }
  23. public int find(int p){
  24. //返回每个数的连通分量标识
  25. return id[p];
  26. }
  27. public void union(int p,int q){
  28. //将两个数连接起来,需要将两个连通分量所有的数标识统一
  29. //需要遍历整个数组
  30. if (find(p) == find(q)){
  31. return;
  32. }
  33. int pID = find(p);
  34. int qID = find(q);
  35. for (int i = 0; i < id.length; i++) {
  36. if (id[i] == pID){
  37. id[i] = qID;
  38. }
  39. }
  40. //每次连接都需要将连通分量数减一
  41. count--;
  42. }
  43. public boolean connected(int p , int q){
  44. //判断两个数是否连通
  45. return find(p) == find(q);
  46. }
  47. public int count(){
  48. return count;
  49. }
  50. }

该算法的主要问题是,每次建立新的连接都需要遍历数组,在极端情况下,当需要建立很多连接时,算法是n的平方级别,这对于大规模问题是不可接受的。

3、quick-union算法

可知,上述算法最主要的问题是每次建立连接时都需要遍历数组,造成了极大地时间开销。因此优化思路是降低建立连接时的开销。
具体思路是采用并查集,让一个连通分量中的所有数都指向一个共同的祖先,那样将建立连接时,只需要一个改动即可。数的组织形式在逻辑上是树形结构。

  1. package union_find;
  2. /**
  3. * @author zwlstart
  4. * @create 2020-12-19 20:56
  5. */
  6. public class QuickUnion {
  7. //记录每个数的连通分量的标识
  8. int[] id;
  9. //记录连通分量的个数
  10. int count;
  11. public QuickUnion() {
  12. }
  13. public QuickUnion(int n) {
  14. id = new int[n];
  15. //连通分量个数初始化为n
  16. count = n;
  17. //初始化每个整数的连通分量标识为自己
  18. for (int i = 0; i < n; i++) {
  19. id[i] = i;
  20. }
  21. }
  22. public int find(int p){
  23. //返回每个数的连通分量标识
  24. //一个连通分量中的所有数都指向共同的祖先,即根节点
  25. //根节点指向自身
  26. while (p != id[p]){
  27. p = id[p];
  28. }
  29. return p;
  30. }
  31. public void union(int p,int q){
  32. //将两个数连接起来,需要将两个连通分量所有的数标识统一
  33. if (find(p) == find(q)){
  34. return;
  35. }
  36. int pID = find(p);
  37. int qID = find(q);
  38. //只需要将p的连通分量的根节点指向p的连通分量的根节点即可
  39. id[pID] = qID;
  40. //每次连接都需要将连通分量数减一
  41. count--;
  42. }
  43. public boolean connected(int p , int q){
  44. //判断两个数是否连通
  45. return find(p) == find(q);
  46. }
  47. public int count(){
  48. return count;
  49. }
  50. }

该算法与上一个算法相比性能有很大提升,每次插入所需要的时间消耗是类似的树从底部到根节点,即是树的深度。在极端情况下,为一个单项树时,时间复杂度也为n的平方级别。

3、加权quick-union算法

针对上一个问题,主要问题是每次建立连接时树的深度问题,如何降低树的深度是优化算法的关键,即采用加权插入的思想,即每次将小树插入到大树,那么树的深度就会很小,当有n个数时,采用这种插入方式,树的深度最多为log(n)。

  1. package union_find;
  2. /**
  3. * @author zwlstart
  4. * @create 2020-12-19 20:56
  5. */
  6. public class WeightedQuickUnion {
  7. //记录每个数的连通分量的标识
  8. int[] id;
  9. //记录每个连通分量的大小
  10. int[] num;
  11. //记录连通分量的个数
  12. int count;
  13. public WeightedQuickUnion() {
  14. }
  15. public WeightedQuickUnion(int n) {
  16. id = new int[n];
  17. //连通分量个数初始化为n
  18. count = n;
  19. //初始化每个整数的连通分量标识为自己
  20. for (int i = 0; i < n; i++) {
  21. id[i] = i;
  22. }
  23. //初始化每个连通分量的大小,初始值均为1
  24. num = new int[n];
  25. for (int i = 0; i < n; i++) {
  26. num[i] = 1;
  27. }
  28. }
  29. public int find(int p){
  30. //返回每个数的连通分量标识
  31. //一个连通分量中的所有数都指向共同的祖先,即根节点
  32. //根节点指向自身
  33. while (p != id[p]){
  34. p = id[p];
  35. }
  36. return p;
  37. }
  38. public void union(int p,int q){
  39. //将两个数连接起来,需要将两个连通分量所有的数标识统一
  40. if (find(p) == find(q)){
  41. return;
  42. }
  43. int pID = find(p);
  44. int qID = find(q);
  45. //每次将小树插入大树
  46. if (num[pID] < num[qID]){
  47. id[pID] = qID;
  48. num[qID] += num[pID];
  49. }else{
  50. id[qID] = pID;
  51. num[pID] += num[qID];
  52. }
  53. //每次连接都需要将连通分量数减一
  54. count--;
  55. }
  56. public boolean connected(int p , int q){
  57. //判断两个数是否连通
  58. return find(p) == find(q);
  59. }
  60. public int count(){
  61. return count;
  62. }
  63. }

加权插入算法已经是非常优秀的算法了,可以保证在任何输入下,每次的建立连接的时间在log(n)级别。

4、最优算法,路径压缩算法

优化的思路是尽量将每次建立连接的时间为常数级别,但这是一件做不到的事情,路径压缩算法的思想是每次遍历的时候,直接将该节点指向根节点。
只需要修改find方法

  1. package union_find;
  2. /**
  3. * @author zwlstart
  4. * @create 2020-12-19 20:56
  5. */
  6. public class CompressionWeightedQuickUnion {
  7. //记录每个数的连通分量的标识
  8. int[] id;
  9. //记录每个连通分量的大小
  10. int[] num;
  11. //记录连通分量的个数
  12. int count;
  13. public CompressionWeightedQuickUnion() {
  14. }
  15. public CompressionWeightedQuickUnion(int n) {
  16. id = new int[n];
  17. //连通分量个数初始化为n
  18. count = n;
  19. //初始化每个整数的连通分量标识为自己
  20. for (int i = 0; i < n; i++) {
  21. id[i] = i;
  22. }
  23. //初始化每个连通分量的大小,初始值均为1
  24. num = new int[n];
  25. for (int i = 0; i < n; i++) {
  26. num[i] = 1;
  27. }
  28. }
  29. public int find(int p){
  30. //返回每个数的连通分量标识
  31. //一个连通分量中的所有数都指向共同的祖先,即根节点
  32. //根节点指向自身
  33. while (p != id[p]){
  34. //进行路径压缩
  35. id[p] = id[id[p]];
  36. p = id[p];
  37. }
  38. return p;
  39. }
  40. public void union(int p,int q){
  41. //将两个数连接起来,需要将两个连通分量所有的数标识统一
  42. if (find(p) == find(q)){
  43. return;
  44. }
  45. int pID = find(p);
  46. int qID = find(q);
  47. //每次将小树插入大树
  48. if (num[pID] < num[qID]){
  49. id[pID] = qID;
  50. num[qID] += num[pID];
  51. }else{
  52. id[qID] = pID;
  53. num[pID] += num[qID];
  54. }
  55. //每次连接都需要将连通分量数减一
  56. count--;
  57. }
  58. public boolean connected(int p , int q){
  59. //判断两个数是否连通
  60. return find(p) == find(q);
  61. }
  62. public int count(){
  63. return count;
  64. }
  65. }