并查集(Union-Find)算法详解 :: labuladong的算法小抄 #1083
Replies: 32 comments 2 replies
-
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) |
Beta Was this translation helpful? Give feedback.
-
Really appreciate this brilliant explanation of Union Find! But I think the implementation of UF in UC Berkely cs61b course is better. |
Beta Was this translation helpful? Give feedback.
-
@yzw-Joey The layers will be decrease when you find other nodes in the union. |
Beta Was this translation helpful? Give feedback.
-
like: 0 <- 1 <- 2, 3 <- 4 <- 5, union(0,3) |
Beta Was this translation helpful? Give feedback.
-
@MrHedwig It's still gonna happen. |
Beta Was this translation helpful? Give feedback.
-
将Union find这个算法描述成等价关系太精彩了。 Union find利用根节点,巧妙的实现了自反性,对称性和传递性。 生活中除了等价关系还有一类比较常见的关系,偏序关系(自反性,反对称性,传递性)。请问有人了解偏序关系应该用什么算法处理?或者是否能在UF上做修改,使其适用于偏序关系呢? |
Beta Was this translation helpful? Give feedback.
-
@xixiang87 1-> 0 <- 2 <- 3, for this case, I don't thin it is 3 level. It belongs to 2 levels. |
Beta Was this translation helpful? Give feedback.
-
@xixiang87 I guess you are right. Here the issue is each time we Union with root, so path compression in find method is not ever triggered... |
Beta Was this translation helpful? Give feedback.
-
最后的那个find的优化的gif图是不是错了?将parent[x]=parent[parent[x]]之后,x不应该是原来的父节点啊 |
Beta Was this translation helpful? Give feedback.
-
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;
}
} |
Beta Was this translation helpful? Give feedback.
-
进行路径压缩的时候不需要调整size数组么? |
Beta Was this translation helpful? Give feedback.
-
As I revisited this question I raised... I realized that although tree size grows, it doesn't matter. Thus if we always Union by root, it doesn't violate the amortized O(1) rule, which is really what matters eventually. |
Beta Was this translation helpful? Give feedback.
-
本文内容已更新,合并了 union-find 算法的应用,并且在使用路径压缩的技巧时去除了 |
Beta Was this translation helpful? Give feedback.
-
private int find(int x) {
while (parent[x] != x) {
// 进行路径压缩
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
} 东哥这种路径压缩可以在多数条件下保证树的高度不会太高(也就是查找任意节点的根节点的时间复杂度为 在 LeetCode 上刷题看见官方的解法,在 public int find(int i) {
if (parent[i] != i) {
parent[i] = find(parent[i]);
}
return parent[i];
} |
Beta Was this translation helpful? Give feedback.
-
@Feyl 你说的官方题解这种递归实现的 |
Beta Was this translation helpful? Give feedback.
-
看到很多人的模板是加入一个 int[] rank 而不是 int[] size, 有必要吗? |
Beta Was this translation helpful? Give feedback.
-
@labuladong 这个递归和迭代不太一样,东哥你写的迭代,是在从当前结点到根结点遍历的过程中,所有遍历的到的结点的父节点都指向其祖先(爷爷)结点;那个递归是路径上的所有结点的父节点都设置为根结点。 |
Beta Was this translation helpful? Give feedback.
-
@Feyl 你说的对哦👍,我大意了,这个递归解法确实精妙,我补充到文章里。 |
Beta Was this translation helpful? Give feedback.
-
东哥,为啥我感觉这两种压缩算法find都会出现数不断增高的情况。复杂度是怎么为O(1)的呢 |
Beta Was this translation helpful? Give feedback.
-
@CNforwardmarch 不会啊,你举个例子画个图就明白了, |
Beta Was this translation helpful? Give feedback.
-
check in |
Beta Was this translation helpful? Give feedback.
-
这个find的写法
|
Beta Was this translation helpful? Give feedback.
-
c++ 并查集Union-Findclass UF{
private:
int count;//连通分量
vector<int> parent;//存储每个节点的父节点
public:
UF(int n){
this->count = n;//连通分量初始化
parent.resize(n);
//初始化节点的父节点为节点自身
for (int i = 0; i < n; ++i) {
parent[i]=i;
}
}
//将节点p和q连通
void doUnion(int p, int q){
int rootP = find(p);
int rootQ = find(q);
//p和q已经连通
if (rootP == rootQ){
return;
}
parent[rootQ] = rootP;
count--;//两个连通分量合并为一个
}
bool connected(int p, int q){
int rootP = find(p);
int rootQ = find(q);
return rootP == rootQ;
}
int find(int x){
if (parent[x] != x){
parent[x] = find(parent[x]);
}
return parent[x];
}
//const成员
const int& getCount() const{
return count;
}
}; |
Beta Was this translation helpful? Give feedback.
-
请问,力扣第 130 题「 被围绕的区域」中,判断内层数组的元素与相邻边界的元素都为'O'的时候,为什么内层的元素要与相邻边界元素相连而不能直接和dummy连接呢?
还请各位大佬解答。 |
Beta Was this translation helpful? Give feedback.
-
我觉得递归的find可以这样理解,因为并查集底部可以看作由数组实现的一个链表, 即parent[x] = x.next(我觉得这样好懂一些),那么这段代码可以翻译成: def find(x):
if x != root:
x.next = find(x.next)
return x.next 看到函数的返回条件,就可以理解为函数会一直往前走,直到走到根节点的时候才会return。由于根节点的next指向的是自己,所以函数会返回的就是根节点。然后这个根节点把值依次赋给一路走来的节点,这样就把所有节点都拉平了。 |
Beta Was this translation helpful? Give feedback.
-
union(p, q) {
let rootQ = this.find(q);
let rootP = this.find(p);
// 如果两个节点已经在同一个连通分量内,不用合并
if (rootP === rootQ) {
return;
}
// 根据树高,优化合并性能,避免树变的很高
if (this.size[rootP] > this.size[rootQ]) {
this.parent[rootQ] = rootP;
} else if(this.size[rootP] < this.size[rootQ]){
this.parent[rootP] = rootQ;
} else {
this.parent[rootQ] = rootP;
this.size[rootP] += this.size[rootQ];
// this.parent[rootP] = rootQ;
// this.size[rootQ] += this.size[rootP];
}
this.count--;
} ["a==b","e==c","b==c","a!=e"] |
Beta Was this translation helpful? Give feedback.
-
引用:“使用UF解决岛屿系列问题的部分” void solve(char[][] board) {
if (board.length == 0) return;
int m = board.length;
int n = board[0].length;
// 给 dummy 留一个额外位置
UF uf = new UF(m * n + 1);
int dummy = m * n;
// 将首列和末列的 O 与 dummy 连通
for (int i = 0; i < m; i++) {
if (board[i][0] == 'O')
uf.union(i * n, dummy);
if (board[i][n - 1] == 'O')
uf.union(i * n + n - 1, dummy);
}
// 将首行和末行的 O 与 dummy 连通
for (int j = 0; j < n; j++) {
if (board[0][j] == 'O')
uf.union(j, dummy);
if (board[m - 1][j] == 'O')
uf.union(n * (m - 1) + j, dummy);
}
// 方向数组 d 是上下左右搜索的常用手法
int[][] d = new int[][]{{1,0}, {0,1}, {0,-1}, {-1,0}};
for (int i = 1; i < m - 1; i++)
for (int j = 1; j < n - 1; j++)
if (board[i][j] == 'O')
// 将此 O 与上下左右的 O 连通
for (int k = 0; k < 4; k++) {
int x = i + d[k][0];
int y = j + d[k][1];
if (board[x][y] == 'O')
uf.union(x * n + y, i * n + j);
}
// 所有不和 dummy 连通的 O,都要被替换
for (int i = 1; i < m - 1; i++)
for (int j = 1; j < n - 1; j++)
if (!uf.connected(dummy, i * n + j))
board[i][j] = 'X';
}
class UF {
// 见上文
} 比如对于博主所列出的矩阵中的[1, 1]节点如果也为'O',那么将漏掉这个点 |
Beta Was this translation helpful? Give feedback.
-
Leetcode 323: class Solution {
class UF{
private int count;
private int[] parent;
public UF(int n){
this.count = n;
parent = new int[n];
for(int i=0; i < n; i++){
parent[i] = i;
}
}
public int find(int n){
if(parent[n] != n){//写成 while() 会造成死循环——但不知道为啥
parent[n] = find(parent[n]);
}
return parent[n];
}
public void union(int p, int q){
int rootP = find(p);
int rootQ = find(q);
if(rootP == rootQ){
return;
}
parent[rootP] = rootQ;
count--;
}
public int count(){
return count;
}
}
public int countComponents(int n, int[][] edges) {
UF uf = new UF(n);
for(int[] edge: edges){
uf.union(edge[0], edge[1]);
}
return uf.count();
}
} |
Beta Was this translation helpful? Give feedback.
-
那个递归的路径压缩的方法,可以不可以理解为,一开始我们合并的时候在极端的情况下,得到的树是一直在增高的。伴随着后面在查找的过程当中对其进行调整,进行压缩,查找的多了,之后再次进行查找的时候,时间复杂度就是O(1)。 |
Beta Was this translation helpful? Give feedback.
-
find 递归路径压缩可以写成以前讲过的后序的形式,更容易理解。
仔细对照,是不是完全等效于原文的方法(如下),而且更好理解
|
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
文章链接点这里:并查集(Union-Find)算法详解
评论礼仪 见这里,违者直接拉黑。
Beta Was this translation helpful? Give feedback.
All reactions