fucking-algorithm icon indicating copy to clipboard operation
fucking-algorithm copied to clipboard

并查集(Union-Find)算法详解 :: labuladong的算法小抄

Open utterances-bot opened this issue 4 years ago • 23 comments

文章链接点这里:并查集(Union-Find)算法详解

评论礼仪 见这里,违者直接拉黑。

utterances-bot avatar Sep 25 '21 02:09 utterances-bot

in my comprehension, as long as we always union the root to another root, regardless the leaf, actually, it could reach more than 3 layer; like: 0 <- 1 <- 2, 3 <- 4 <- 5, union(0,3)

yzw-Joey avatar Sep 25 '21 02:09 yzw-Joey

Really appreciate this brilliant explanation of Union Find! But I think the implementation of UF in UC Berkely cs61b course is better.

Park-J-lab avatar Sep 29 '21 09:09 Park-J-lab

@yzw-Joey The layers will be decrease when you find other nodes in the union.

Feyl avatar Oct 10 '21 09:10 Feyl

like: 0 <- 1 <- 2, 3 <- 4 <- 5, union(0,3) This will not happen, coz 0<-1<-2 would be compressed to 2>-0<-1, with height 1, when you union them. so does 3,4,5.

MrHedwig avatar Oct 31 '21 07:10 MrHedwig

@MrHedwig It's still gonna happen. 0 <- 1 and 2 <- 3 going to make a 3 level relationship of 1 -> 0 <- 2 <- 3. Then @yzw-Joey 's case is going to happen when union 2 of 3 level relationship

xixiang87 avatar Nov 16 '21 01:11 xixiang87

将Union find这个算法描述成等价关系太精彩了。

Union find利用根节点,巧妙的实现了自反性,对称性和传递性。

生活中除了等价关系还有一类比较常见的关系,偏序关系(自反性,反对称性,传递性)。请问有人了解偏序关系应该用什么算法处理?或者是否能在UF上做修改,使其适用于偏序关系呢?

0xlightyear avatar Nov 20 '21 00:11 0xlightyear

@xixiang87 1-> 0 <- 2 <- 3, for this case, I don't thin it is 3 level. It belongs to 2 levels.

EhanDuan avatar Dec 22 '21 16:12 EhanDuan

@xixiang87 I guess you are right. In case we have 0, 1, 2, 3, 4, 5, 6, 7 with Union(0, 1), Union(2, 3), Union(4, 5), Union(6, 7) (and take smaller number as root) Then Uion(0, 2), Union(4, 6) Finally Union(2, 4).

Here the issue is each time we Union with root, so path compression in find method is not ever triggered... @labuladong idea how to avoid scenarios like this?

MrHedwig avatar Feb 03 '22 00:02 MrHedwig

最后的那个find的优化的gif图是不是错了?将parent[x]=parent[parent[x]]之后,x不应该是原来的父节点啊

LovesAsuna avatar Feb 13 '22 02:02 LovesAsuna

js 版

class UF {
  constructor(n) {
    // 一开始互不连通
    this._count = n;
    // 父节点指针初始指向自己
    this._parent = new Array(n).fill(0).map((_, index) => index);
    // 最初每棵树只有一个节点
    // 重量应该初始化 1
    this._weights = new Array(n).fill(1);
  }

  union(p, q) {
    const rootP = this.findRoot(p);
    const rootQ = this.findRoot(q);
    if (rootP === rootQ) return;
    // 将两棵树合并为一棵
    // 小树接到大树下面,较平衡
    const weights = this._weights;
    if (weights[rootP] > weights[rootQ]) {
      this._parent[rootQ] = rootP;
      weights[rootQ]++;
    } else {
      this._parent[rootP] = rootQ;
      weights[rootP]++;
    }
    this._count--; // 两个分量合二为一
  }

  // 返回某个节点 x 的根节点
  findRoot(x) {
    const parent = this._parent;
    while (parent[x] !== x) {
      // 进行路径压缩
      parent[x] = parent[parent[x]];
      x = parent[x];
    }
    return x;
  }

  connected(p, q) {
    const rootP = this.findRoot(p);
    const rootQ = this.findRoot(q);
    return rootP === rootQ;
  }

  count() {
    return this._count;
  }
}

jswxwxf avatar Feb 14 '22 06:02 jswxwxf

进行路径压缩的时候不需要调整size数组么?

qq1545372477 avatar Feb 27 '22 13:02 qq1545372477

@xixiang87 I guess you are right. In case we have 0, 1, 2, 3, 4, 5, 6, 7 with Union(0, 1), Union(2, 3), Union(4, 5), Union(6, 7) (and take smaller number as root) Then Uion(0, 2), Union(4, 6) Finally Union(2, 4).

Here the issue is each time we Union with root, so path compression in find method is not ever triggered... @labuladong idea how to avoid scenarios like this?

As I revisited this question I raised... I realized that although tree size grows, it doesn't matter. Coz we care about Union and Find are amortized O(1), if we Union root, then find(root) is itself O(1), i.e. returns immediately.

Thus if we always Union by root, it doesn't violate the amortized O(1) rule, which is really what matters eventually. @xixiang87 @yzw-Joey @labuladong

MrHedwig avatar Mar 07 '22 04:03 MrHedwig

本文内容已更新,合并了 union-find 算法的应用,并且在使用路径压缩的技巧时去除了 size 数组。因为就像有的读者说的,路径压缩之后 size 数组的作用已经不大,而且在路径压缩的过程中正确维护 size 数组有些麻烦,所以现在比较推荐的写法是只使用路径压缩技巧,即文中的 UF 类代码。

labuladong avatar Mar 07 '22 15:03 labuladong

private int find(int x) {
    while (parent[x] != x) {
        // 进行路径压缩
        parent[x] = parent[parent[x]];
        x = parent[x];
    }
    return x;
}

东哥这种路径压缩可以在多数条件下保证树的高度不会太高(也就是查找任意节点的根节点的时间复杂度为 O(1))。 但是想上面 @MrHedwig 哥举的这种特殊情况,就会导致树的高度逐渐增长。

在 LeetCode 上刷题看见官方的解法,在 find 的查找节点根节点的过程中,查找路径上的各个节点都会进行“压缩”操作,使路径上的所有节的直接父节点变为根结点,那么可以保证查找任意节点的根节点的时间复杂度为 O(1),代码如下:

    public int find(int i) {
        if (parent[i] != i) {
            parent[i] = find(parent[i]);
        }
        return parent[i];
    }

代码出处:1020.飞地的数量 官方题解的并查集解法

Feyl avatar Mar 09 '22 02:03 Feyl

@Feyl 你说的官方题解这种递归实现的 find 方法,也无法避免每次 union 两个根节点导致的树高增加的情况呀

labuladong avatar Mar 09 '22 14:03 labuladong

@labuladong 确实,没办法解决那种特殊情况的问题。不过这个压缩效果会好一点。

Feyl avatar Mar 11 '22 09:03 Feyl

@labuladong 确实,没办法解决那种特殊情况的问题。不过这个压缩效果会好一点。

这种递归的写法做的事情和迭代写法是一模一样的啊,就是把形式改成递归的了,还让人不容易理解。这种写法的效果好在哪里呢,是否能举出例子?

labuladong avatar Mar 11 '22 12:03 labuladong

看到很多人的模板是加入一个 int[] rank 而不是 int[] size, 有必要吗?

chaonanzhang1206 avatar Apr 03 '22 04:04 chaonanzhang1206

@labuladong 这个递归和迭代不太一样,东哥你写的迭代,是在从当前结点到根结点遍历的过程中,所有遍历的到的结点的父节点都指向其祖先(爷爷)结点;那个递归是路径上的所有结点的父节点都设置为根结点。

Feyl avatar Apr 13 '22 09:04 Feyl

@Feyl 你说的对哦👍,我大意了,这个递归解法确实精妙,我补充到文章里。

labuladong avatar Apr 18 '22 04:04 labuladong

东哥,为啥我感觉这两种压缩算法find都会出现数不断增高的情况。复杂度是怎么为O(1)的呢

CNforwardmarch avatar May 26 '22 09:05 CNforwardmarch

@CNforwardmarch 不会啊,你举个例子画个图就明白了,find 调用在不断减少树高,不会增高的。

labuladong avatar May 30 '22 13:05 labuladong

check in

deepbreath373 avatar Jul 17 '22 01:07 deepbreath373

这个find的写法

public int find(int x) {
    if (parent[x] != x) {
        parent[x] = find(parent[x]);
        // 要加上 x = parent[x]; x 不变会死循环吧。
    }
    return parent[x];
}

chouxi avatar Sep 07 '22 04:09 chouxi