Loading

【转载】Union-Find算法详解

声明本文转载自 Union-Find算法详解 - GitHub

Union-Find 算法,也就是常说的并查集算法,主要是解决图论中「动态连通性」问题的。

问题描述

简单说,动态连通性其实可以抽象成给一幅有 N 个结点的图连线,其中结点按 0~N 编号。

Union-Find 算法主要需要实现以下API:

class UnionFind {
public:
    /* 将 a 和 b 连接(连通) */
    void connect(int a, int b);

    /* 判断 a 和 b 是否连通 */
    bool isConnected(int a, int b);

    /* 返回图中的连通分量 */
    int getCount();
}

这里所说的「连通」是一种等价关系,也就是说具有如下三个性质:

1、自反性:结点 aa 是连通的。

2、对称性:如果结点 ab 连通,那么 ba 也连通。

3、传递性:如果结点 ab 连通, bc 连通,那么 ac 也连通。

基本思路

假定我们使用森林(若干棵树)来表示图的动态连通性,用数组来具体实现这个森林。怎么用森林来表示连通性呢?我们设定树的每个节点有一个指针指向其父节点,如果是根节点的话,这个指针指向自己。

class UnionFind {
public:
    UnionFind(int n);
    /* 将 a 和 b 连接(连通) */
    void connect(int a, int b);

    /* 判断 a 和 b 是否连通 */
    bool isConnected(int a, int b);

    /* 返回图中的连通分量 */
    int getCount() { return count; }
    
private:
    /* 返回结点 x 的根节点 */
    int findRoot(int x);
    
    /** 其他函数 **/
private:
    int count;			// 连通分量
    vector<int> parent;	// parent[i]: 结点 i 的父节点
};

如果某两个节点被连通,则让其中的(任意)一个节点的根节点接到另一个节点的根节点上:

UnionFind::UnionFind(int n) : count(n) {
    // 初始时所有结点互不连通
    // 父节点指向自己
    for (int i = 0; i < n; ++i){
        parent.push_back(i);
    }
}
/* 判断 a 和 b 是否连通 */
void UnionFind::connect(int a, int b) {
    int rootA = findRoot(a);
    int rootB = findRoot(b);
    if (rootA == rootB)
        return ;
    // 将一棵树接在另一棵树上
    parent[a] = rootB;
    // 连通分量 -1
    --count;
}
/* 判断 a 和 b 是否连通 */
bool UnionFind::isConnected(int a, int b) {
    return findRoot(a) == findRoot(b);
}
/* 返回结点 x 的根节点 */
int UnionFind::findRoot(int x) {
    // 根结点有 x == parent[x]
    while (x != parent[x])
        x = parent[x];
    return x;
}

我们发现,主要 API isConnectedconnect 中的复杂度都是 findRoot 函数造成的,所以说它们的复杂度和 findRoot 一样。findRoot 主要功能就是从某个节点向上遍历到树根,其时间复杂度就是树的高度。我们可能习惯性地认为树的高度就是 logN,但这并不一定。logN 的高度只存在于平衡二叉树,对于一般的树可能出现极端不平衡的情况,使得「树」几乎退化成「链表」,树的高度最坏情况下可能变成 N

平衡性优化

我们一开始就是简单粗暴的把 a 所在的树接到 b 所在的树的根节点下面,那么这里就可能出现「头重脚轻」的不平衡状况。我们其实是希望,小一些的树接到大一些的树下面,这样就能避免头重脚轻,更平衡一些。解决方法是额外使用一个 size 数组,记录每棵树包含的节点数,我们不妨称为「重量」:

class UnionFind {
public:
    UnionFind(int n);
    /* 将 a 和 b 连接(连通) */
    void connect(int a, int b);

    /* 判断 a 和 b 是否连通 */
    bool isConnected(int a, int b);

    /* 返回图中的连通分量 */
    int getCount() { return count; }
    
private:
    /* 返回结点 x 的根节点 */
    int findRoot(int x);
    
    /** 其他函数 **/
private:
    int count;			// 连通分量
    vector<int> parent;	// parent[i]: 结点 i 的父节点
    // 新增一个数组记录树的 “重量”
    vector<int> size;
};
UnionFind::UnionFind(int n) : count(n) {
    // 初始时所有结点互不连通
    // 父节点指向自己
    for (int i = 0; i < n; ++i){
        parent.push_back(i);
        // 重量应该初始化 1
        size.push_back(1);
    }
}
/* 将 a 和 b 连接(连通) */
void UnionFind::connect(int a, int b) {
    int rootA = findRoot(a);
    int rootB = findRoot(b);
    if (rootA == rootB)
        return ;
    // 小树根节点接到大树根节点下 「较平衡」
    if (size[rootA] < size[rootB]){
        parent[rootA] = rootB;
        size[rootB] += size[rootA];
    } else {
        parent[rootB] = rootA;
        size[rootA] += size[rootB];
    }
    // 连通分量 -1
    --count;
}

这样,通过比较树的重量,就可以保证树的生长相对平衡,树的高度大致在 logN 这个数量级,极大提升执行效率。此时,findRootconnectisConnected 的时间复杂度都下降为 O(logN)

路径压缩

我们能不能进一步压缩每棵树的高度,使树高始终保持为常数?这样findRoot 就能以 O(1) 的时间找到某一节点的根节点,相应的, connectisConnected 复杂度都下降为 O(1) 。要做到这一点,非常简单,只需要在 findRoot 中加一行代码:

int UnionFind::findRoot(int x) {
    // 根结点有 x == parent[x]
    while (x != parent[x]) {
        // 「路径压缩」 减少查询迭代次数
        parent[x] = parent[parent[x]];
        x = parent[x];
    }
    return x;
}

代码整合

class UnionFind {
public:
    UnionFind(int n); 		
    void connect(int a, int b);		/* 将 a 和 b 连接(连通) */
    bool isConnected(int a, int b);	/* 判断 a 和 b 是否连通 */
	int getCount() { return count; } /* 返回图中的连通分量 */
	int findRoot(int x);			/* 返回结点 x 的根节点 */
    /** 其他函数 **/
private:
    int count;			// 连通分量
    vector<int> parent;	// parent[i]: 结点 i 的父节点    
    vector<int> size;	// 新增一个数组记录树的 “重量”
};

UnionFind::UnionFind(int n) : count(n) {
    // 初始时所有结点互不连通
    // 父节点指向自己
    for (int i = 0; i < n; ++i){
        parent.push_back(i);
        // 重量应该初始化 1
        size.push_back(1);
    }
}
void UnionFind::connect(int a, int b) {
    int rootA = findRoot(a);
    int rootB = findRoot(b);
    if (rootA == rootB)
        return ;
    // 小树根节点接到大树根节点下 「较平衡」
    if (size[rootA] < size[rootB]){
        parent[rootA] = rootB;
        size[rootB] += size[rootA];
    } else {
        parent[rootB] = rootA;
        size[rootA] += size[rootB];
    }
    // 连通分量 -1
    --count;
}
bool UnionFind::isConnected(int a, int b) {
    return findRoot(a) == findRoot(b);
}
int UnionFind::findRoot(int x) {
    // 根结点有 x == parent[x]
    while (x != parent[x]) {
        // 「路径压缩」 减少查询迭代次数
        parent[x] = parent[parent[x]];
        x = parent[x];
    }
    return x;
}
posted @ 2021-01-25 14:55  JakeLin  阅读(104)  评论(0编辑  收藏  举报