并查集

(本来想写个并查集的文章,发现这一篇写得很好,就直接摘抄过来了,也做个记录【union-find】。

1. 概述

并查集是一种树型的数据结构,用于处理一些不交集(Disjoint Sets)的合并及查询问题。有一个联合-查找算法(Union-find Algorithm)定义了两个用于此数据结构的操作:

find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
union:将两个子集合并成同一个集合。
初始化每一个点都是一个连通域,类似下图:



由于支持这两种操作,一个不相交集也常被称为联合-查找数据结构(Union-find Data Structure)或合并-查找集合(Merge-find Set)。为了更加精确的定义这些方法,需要定义如何表示集合。一种常用的策略是为每个集合选定一个固定的元素,称为代表,以表示整个集合。接着,find(x) 返回 x 所属集合的代表,而 union 使用两个集合的代表作为参数。

2. 核心API

2.1 find

    def find(self, x):
        # 未进行路径压缩
        while x != self.parent[x]:
            x = self.parent[x]
        return x

比如下面这个图,调用 find(0) 会逐步找到 3 。


若采用路径压缩方法,在找到 3 的过程上会将路径上的节点都指向根节点。


image
    def find(self, x):
        """
        查找节点的父(根)节点
        """
        if x != self.parent[x]:
            # 进行了路径压缩
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

极限情况下,每一个路径都会被压缩,这种情况下继续查找的时间复杂度就是 O(1)

2.2 union

将其中一个节点挂到另外一个节点的祖先上,这样两者祖先就一样了。也就是说,两个节点联通了。

对于如下的一个图:



图中 r:1 表示 秩为 1,r 是 rank 的简写。记录以该结点为根结点的树的深度。

    def union(self, x, y):
        """
        合并两个节点,小的树挂到大的树上,使树尽量平衡
        """
        p, q = self.find(x), self.find(y)
        if p == q:
            return
        if self.rank[p] < self.rank[q]:
            self.parent[p] = q
        else:
            self.parent[q] = p
        if self.rank[p] == self.rank[q]:
            self.rank[p] += 1
        self.count -= 1

如果我们将 0 和 7 进行一次合并。即 union(0, 7) ,则会发生如下过程。


3. 不带权并查集

完整代码如下:


class UnionFind:

    def __init__(self, n):
        """
        并查集
        :param n: 节点个数
        """
        # 记录父节点
        self.parent = {}
        # 记录以该结点为根结点的树的深度
        self.rank = {}
        # 记录连通分量的个数
        self.count = 0
        # 初始化
        for i in range(n):
            self.parent[i] = I
            self.rank[i] = 1
            self.count += 1

    def find(self, x):
        """
        查找节点的父(根)节点
        """
        if x != self.parent[x]:
            # 进行了路径压缩
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, x, y):
        """
        合并两个节点,小的树挂到大的树上,使树尽量平衡
        """
        p, q = self.find(x), self.find(y)
        if p == q:
            return
        if self.rank[p] < self.rank[q]:
            self.parent[p] = q
        else:
            self.parent[q] = p
        if self.rank[p] == self.rank[q]:
            self.rank[p] += 1
        self.count -= 1

4. 带权并查集

实际上并查集就是图结构,我们使用了哈希表来模拟这种图的关系。 而上面讲到的其实都是有向无权图,因此仅仅使用 parent 表示节点关系就可以了。而如果使用的是有向带权图呢?实际上除了维护 parent 这样的节点指向关系,我们还需要维护节点的权重,一个简单的想法是使用另外一个哈希表 weight 存储节点的权重关系。比如 weight[a] = 1 表示 a 到其父节点的权重是 1。

如果是带权的并查集,其查询过程的路径压缩以及合并过程会略有不同,因为我们不仅关心节点指向的变更,也关心权重如何更新。比如:

a    b
^    ^
|    |
|    |
x    y

如上表示的是 x 的父节点是 a,y 的父节点是 b,现在我需要将 x 和 y 进行合并。

a    b
^    ^
|    |
|    |
x -> y

假设 x 到 a 的权重是 w(xa),y 到 b 的权重为 w(yb),x 到 y 的权重是 w(xy)。合并之后会变成如图的样子:

a -> b
^    ^
|    |
|    |
x    y

那么 a 到 b 的权重应该被更新为什么呢?我们知道 w(xa) + w(ab) = w(xy) + w(yb),也就是说 a 到 b 的权重 w(ab) = w(xy) + w(yb) - w(xa)。

当然上面关系式是加法,减法,取模还是乘法,除法等完全由题目决定,我这里只是举了一个例子。不管怎么样,这种运算一定需要满足可传导性。

完整代码如下:

class WeightedUnionFind:

    def __init__(self, n):
        """
        :param n: 节点个数
        """
        # 记录父节点
        self.parent = {}
        # 记录到父节点的权重
        self.weight = {}
        for i in range(n):
            self.parent[i] = I
            self.weight[i] = 0

    def find(self, x):
        if x != self.parent[x]:
            p, w = self.find(self.parent[x])
            self.parent[x] = p
            self.weight[x] += w
        return self.parent[x], self.weight[x]

    def union(self, x, y, w):
        p_x, w_x = self.find(x)
        p_y, w_y = self.find(y)
        if p_x == p_y:
            return
        self.parent[p_x] = p_y
        self.weight[p_x] = w + w_y - w_x

5. leetcode

721. 账户合并

1202. 交换字符串中的元素

1697. 检查边长度限制的路径是否存在

399. 除法求值

参考文献

  1. https://github.com/azl397985856/leetcode/blob/master/thinkings/union-find.md
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,366评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,521评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,689评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,925评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,942评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,727评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,447评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,349评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,820评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,990评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,127评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,812评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,471评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,017评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,142评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,388评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,066评论 2 355

推荐阅读更多精彩内容