什么是并查集

image.png

并查集接口:

  1. public interface UF {
  2. int getSize();
  3. boolean isConnected(int p,int q);
  4. void unionElements(int p,int q);
  5. }

Quick Find

  1. public class UnionFind1 implements UF{
  2. //存储相应元素的集合编号
  3. private int[] id;
  4. public UnionFind1(int size){
  5. id=new int[size];
  6. for(int i=0;i<id.length;i++){
  7. id[i]=i;
  8. }
  9. }
  10. @Override
  11. public int getSize() {
  12. return id.length;
  13. }
  14. //元素属于同一个集合,则这两个元素就是相连的
  15. //时间复杂度:O(1)
  16. @Override
  17. public boolean isConnected(int p, int q) {
  18. return find(p)==find(q);
  19. }
  20. //查找p元素所属的集合
  21. private int find(int p){
  22. if(p<0 || p>=id.length){
  23. throw new IllegalArgumentException("p is out of bound");
  24. }
  25. return id[p];
  26. }
  27. //时间复杂度:O(n)
  28. @Override
  29. public void unionElements(int p, int q) {
  30. int pId=find(p);
  31. int qId=find(q);
  32. if(pId==qId){
  33. return;
  34. }
  35. //连接两个元素后,如果它们是在两个不同的集合中,
  36. //则现在由于p、q的连接,就会成为一个集合
  37. for(int i=0;i<id.length;i++){
  38. if(id[i]==pId){
  39. id[i]=qId;
  40. }
  41. }
  42. }
  43. }

image.pngimage.png

Quick Union

初始化并查集,每个节点指向自身,构成了森林。

image.png

union(4,3),就是将4的父指针指向3,也就是说,此时3是4的父节点

image.png

union(9,4),就是将9的父指针指向4所在集合的根节点

image.png

  1. public class UnionFind2 implements UF{
  2. //parent[i]表示i所指向的父节点
  3. private int[] parent;
  4. public UnionFind2(int size){
  5. parent=new int[size];
  6. //初始化时,将每个元素看成一个树节点,它们指向自身,构成森林
  7. for(int i=0;i<parent.length;i++){
  8. parent[i]=i;
  9. }
  10. }
  11. @Override
  12. public int getSize() {
  13. return parent.length;
  14. }
  15. //查找p元素所在的树(也就是集合)的根节点
  16. //时间复杂度:O(h),其中h为该集合树的深度
  17. private int find(int p){
  18. if(p<0 || p>=parent.length){
  19. throw new IllegalArgumentException("p is out of bound.");
  20. }
  21. while(p!=parent[p]){
  22. p=parent[p];
  23. }
  24. //返回的是p所在的集合的编号,同时也是p集合非根节点
  25. return p;
  26. }
  27. @Override
  28. public boolean isConnected(int p, int q) {
  29. //根节点相同,就是同一个集合
  30. return find(p)==find(q);
  31. }
  32. @Override
  33. public void unionElements(int p, int q) {
  34. int pRoot=find(p);
  35. int qRoot=find(q);
  36. if(pRoot==qRoot){
  37. return;
  38. }
  39. //将p所在集合的根节点指向q所在集合的根节点
  40. parent[pRoot]=qRoot;
  41. }
  42. }

基于size的优化

  • 对Union Find 2的改进:

根据两个元素所在的集合的元素个数的不同,判断合并方向。
将元素少的集合合并到元素多的集合上,降低树的高度。

  1. public class UnionFind3 implements UF{
  2. //parent[i]表示i所指向的父节点
  3. private int[] parent;
  4. //sz[i]表示i元素所在的集合的元素个数
  5. private int[] sz;
  6. public UnionFind3(int size){
  7. parent=new int[size];
  8. sz=new int[size];
  9. //初始化时,将每个元素看成一个树节点,它们指向自身,构成森林
  10. for(int i=0;i<parent.length;i++){
  11. parent[i]=i;
  12. //初始化时,每个节点就是一个集合,那么元素个数就是1
  13. sz[i]=1;
  14. }
  15. }
  16. @Override
  17. public int getSize() {
  18. return parent.length;
  19. }
  20. //查找p元素所在的树(也就是集合)的根节点
  21. //时间复杂度:O(h),其中h为该集合树的深度
  22. private int find(int p){
  23. if(p<0 || p>=parent.length){
  24. throw new IllegalArgumentException("p is out of bound.");
  25. }
  26. while(p!=parent[p]){
  27. p=parent[p];
  28. }
  29. //返回的是p所在的集合的编号,同时也是p集合非根节点
  30. return p;
  31. }
  32. @Override
  33. public boolean isConnected(int p, int q) {
  34. //根节点相同,就是同一个集合
  35. return find(p)==find(q);
  36. }
  37. //根据两个元素所在的集合的元素个数的不同,判断合并方向。
  38. //将元素少的集合合并到元素多的集合上,降低树的高度。
  39. @Override
  40. public void unionElements(int p, int q) {
  41. int pRoot=find(p);
  42. int qRoot=find(q);
  43. if(pRoot==qRoot){
  44. return;
  45. }
  46. //将p所在集合的根节点指向q所在集合的根节点
  47. //parent[pRoot]=qRoot;
  48. //改进:
  49. if(sz[pRoot]<sz[qRoot]){
  50. //p节点所在的集合元素较少,pRoot的父指针指向qRoot
  51. parent[pRoot]=qRoot;
  52. sz[qRoot]+=sz[pRoot];
  53. }else{
  54. parent[qRoot]=pRoot;
  55. sz[pRoot]+=sz[qRoot];
  56. }
  57. }
  58. }

基于rank的优化

对于如下并查集,union(4,2)

image.png

按照“基于size的优化”将8指向7,此时树的深度增加了。

image.png

我们应该是这样合并合理:将7指向8,树的深度没有增加,效率更高

image.pngimage.png

  • 基于rank的优化:rank[i]表示根节点为i的树的深度

根据两个元素所在的集合的树的深度不同,判断合并方向。
将深度浅的集合合并到深度深的的集合上,降低树的高度。

  1. public class UnionFind4 implements UF{
  2. //parent[i]表示i所指向的父节点
  3. private int[] parent;
  4. //rank[i]表示根节点为i的树的深度
  5. private int[] rank;
  6. public UnionFind4(int size){
  7. parent=new int[size];
  8. rank=new int[size];
  9. //初始化时,将每个元素看成一个树节点,它们指向自身,构成森林
  10. for(int i=0;i<parent.length;i++){
  11. parent[i]=i;
  12. //初始化时,每个节点就是一个集合,那么元素所在集合的深度就是1
  13. rank[i]=1;
  14. }
  15. }
  16. @Override
  17. public int getSize() {
  18. return parent.length;
  19. }
  20. //查找p元素所在的树(也就是集合)的根节点
  21. //时间复杂度:O(h),其中h为该集合树的深度
  22. private int find(int p){
  23. if(p<0 || p>=parent.length){
  24. throw new IllegalArgumentException("p is out of bound.");
  25. }
  26. while(p!=parent[p]){
  27. p=parent[p];
  28. }
  29. //返回的是p所在的集合的编号,同时也是p集合非根节点
  30. return p;
  31. }
  32. @Override
  33. public boolean isConnected(int p, int q) {
  34. //根节点相同,就是同一个集合
  35. return find(p)==find(q);
  36. }
  37. //根据两个元素所在的集合的树的深度不同,判断合并方向。
  38. //将深度浅的集合合并到深度深的的集合上,降低树的高度。
  39. @Override
  40. public void unionElements(int p, int q) {
  41. int pRoot=find(p);
  42. int qRoot=find(q);
  43. if(pRoot==qRoot){
  44. return;
  45. }
  46. //将p所在集合的根节点指向q所在集合的根节点
  47. //parent[pRoot]=qRoot;
  48. /*改进:
  49. if(sz[pRoot]<sz[qRoot]){
  50. //p节点所在的集合元素较少,pRoot的父指针指向qRoot
  51. parent[pRoot]=qRoot;
  52. sz[qRoot]+=sz[pRoot];
  53. }else{
  54. parent[qRoot]=pRoot;
  55. sz[pRoot]+=sz[qRoot];
  56. }*/
  57. //改进
  58. if(rank[pRoot]<rank[qRoot]){
  59. //pRoot集合的深度浅,pRoot的父指针指向qRoot
  60. parent[pRoot]=qRoot;
  61. }else if(rank[pRoot]>rank[qRoot]){
  62. parent[qRoot]=pRoot;
  63. }else{ //rank[pRoot]==rank[qRoot]
  64. parent[pRoot]=qRoot;
  65. //pRoot的父指针指向qRoot,则qRoot所在的集合深度+1
  66. rank[qRoot]+=1;
  67. }
  68. }
  69. }

路径压缩I

前面的 Union Find 4的find(4),算法过程:

image.png

路径压缩后,算法过程:

image.png

  1. //查找p元素所在的树(也就是集合)的根节点
  2. //时间复杂度:O(h),其中h为该集合树的深度
  3. private int find(int p){
  4. if(p<0 || p>=parent.length){
  5. throw new IllegalArgumentException("p is out of bound.");
  6. }
  7. while(p!=parent[p]){
  8. parent[p]=parent[parent[p]];
  9. p=parent[p];
  10. }
  11. //返回的是p所在的集合的编号,同时也是p集合非根节点
  12. return p;
  13. }

路径压缩II

image.png

  1. //查找p元素所在的树(也就是集合)的根节点
  2. //时间复杂度:O(h),其中h为该集合树的深度
  3. private int find(int p){
  4. if(p<0 || p>=parent.length){
  5. throw new IllegalArgumentException("p is out of bound.");
  6. }
  7. if(p!=parent[p]){
  8. parent[p]=find(parent[p]);
  9. }
  10. return parent[p];
  11. }