并查集

栏目: 数据库 · 发布时间: 5年前

内容简介:并查集(Union-Find Set),也叫Disjoint Set,由Bernard A. Galler和Michael J. Fischer在1964年提出,它主要是用来解决动态连通性问题。什么是动态连通性问题?

一:背景

并查集(Union-Find Set),也叫Disjoint Set,由Bernard A. Galler和Michael J. Fischer在1964年提出,它主要是用来解决动态连通性问题。

什么是动态连通性问题?

并查集

如上图,若两点之间存在一条路线可以相互连接,那么这两个点就是连通的。现在如果我们一边新加入更多的点和路径,一边又要立即得到随机某两点是否连通,那么这该如何实现呢?

二:算法分析与实现

并查集的思想非常简单,把那些彼此连通的点连起来形成一棵树,如下图,

并查集

那么我们要判断某两点是否连通,只需判断它们的根是否相同即可。代码如下,

#include <iostream>

using namespace std;

#define N 10

int pre[N];

void Init()
{
    for (int i = 0; i < N; i++)
        pre[i] = i;
}

int Find(int i)
{
    if (pre[i] == i)
        return i;

    int i_parent = pre[i];
    int i_root = Find(i_parent);

    return i_root;
}

void Union(int i, int j)
{
    int i_root = Find(i);
    int j_root = Find(j);

    if (i_root == j_root)
        return;

    pre[i_root] = j_root;
}


int main()
{
    Init();

    Union(2, 9);
    Union(4, 9);
    Union(3, 4);
    Union(5, 6);

    // 判断 3 和 5 是否连通
    if (Find(3) == Find(5))
        cout << "3 和 5 连通.\n";
    else
        cout << "3 和 5 不连通.\n";

    return 0;
}

三:算法优化

从上面的代码可以看出,算法的复杂度主要在 Find 函数中,而 Find 的快慢主要由树的高度决定,那么我们的问题就转化为 如何降低树的高度 ,常用的方法有两个: Path CompressionUnion By Rank

(1)Path Compression(路径压缩)

因为我们只想要快速地找到根结点,对沿途经过的那些结点并不关心,因此可以在 Find 查找过程中,将沿途经过的每一个结点的父亲直接设置为根结点,如下图,

int Find(int i)
{
    if (pre[i] == i)
        return i;

    int i_parent = pre[i];
    int i_root = Find(i_parent);

    pre[i] = i_root; // 路径压缩

    return i_root;
}

并查集

从上图可以看到,压缩后,树的高度变低了。

但我们要清楚,路径压缩其实是牺牲了路径完整性来求得更高效的运行速度,对于某些需要输出路径的应用场景,路径压缩这种优化就无法使用了。

(2)Union By Rank(按秩合并)

Rank 翻译为"秩",在这里,我们可以简单地理解成"树的高度",见下图,

void Union(int i, int j)
{
    int i_root = Find(i);
    int j_root = Find(j);

    if (i_root == j_root)
        return;

    if (rank[i_root] > rank[j_root])
        swap(i_root, j_root);

    pre[i_root] = j_root;

    if (rank[i_root] == rank[j_root]) // 两个秩相同的树合并,则整体的秩就会增加 1
        rank[j_root]++;
}

并查集

(3)更多的优化方法

上面讲的只是平时常用的两种,在维基上还有更多的优化方法,包括Path Having、Path Splitting等等,需要了解的朋友可以到 这里

四:优化后的代码

下面的代码同时使用了路径压缩和按秩合并优化。

#include <iostream>
#include <algorithm>

using namespace std;

#define N 10

int pre[N];
int rank[N];

void Init()
{
    for (int i = 0; i < N; i++)
    {
        pre[i] = i;
        rank[i] = 0;
    }
}

int Find(int i)
{
    if (pre[i] == i)
        return i;

    int i_parent = pre[i];
    int i_root = Find(i_parent);

    pre[i] = i_root; // 路径压缩

    return i_root;
}

void Union(int i, int j)
{
    int i_root = Find(i);
    int j_root = Find(j);

    if (i_root == j_root)
        return;

    if (rank[i_root] > rank[j_root])
        swap(i_root, j_root);

    pre[i_root] = j_root;

    if (rank[i_root] == rank[j_root]) // 两个高度相同的树合并则整体的高度就会增加
        rank[j_root]++;
}

int main()
{
    Init();

    Union(2, 9);
    Union(4, 9);
    Union(3, 4);
    Union(5, 6);

    // 判断 3 和 5 是否连通
    if (Find(3) == Find(5))
        cout << "3 和 5 连通.\n";
    else
        cout << "3 和 5 不连通.\n";

    return 0;
}

五:算法复杂度

当不采用任何优化的情况下,并查集每次操作的最差时间复杂度为$O(n)$,即树退化为链表的时候。

当仅采用路径压缩优化时,每次操作的最差时间复杂度为$O(1+log_{2+f/n}n)$,这个值比$O(log_2n)$小,其中$f$为查找次数。

当仅使用按秩合并优化时,每次操作的最差时间复杂度为$Θ(log_2n) $。

当同时使用路径压缩和按秩合并优化时,摊还分析后,每次操作的最差时间复杂度为$O(log^∗n)​$,其中$log^∗n​$是 Iterated Logarithm 函数,实际使用中,它的值不超过5,如下图,这也就是说每次操作的实际复杂度,只有$O(1)​$。

并查集


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

区块链革命

区块链革命

[加]唐塔普斯科特(Don Tapscott)、[加]亚力克斯·塔普斯科特(Alex Tapscott) / 中信出版集团股份有限公司 / 2016-9 / 69

(1)国际大腕“数字经济之父”继畅销书《维基经济学》之后再出力作! (2)一本真正全景式描述区块链理论及应用的巨著! (3)苹果共同创始人史蒂夫·沃兹尼亚克、世界经济论坛创始人和论坛主席克劳斯·施瓦布、网景及硅谷安德森·霍洛维茨风险投资公司创始人马克·安德森、麦肯锡董事长兼全球总裁鲍达民、 百事公司首席执行官卢英德、丹·舒尔曼 Paypal公司首席执行官等全球政治界、学术界和商界精英联......一起来看看 《区块链革命》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具