并查集教程


ps. 2023.4.1发现文章中的笔误,进行修改,错误的图片稍后重新制作

并查集

名词说明

用ri替换root_i,即find(i)

一般用union代表合并,但是我实在看不惯名词,所以我使用的是merge,纯看个人爱好,勿喷

“组织”思想

把一个个节点当成一个个组织,一开始所有人都“占山为王”,做自己的“光杆司令”。在合并时,两个帮派打架,然后输的帮派的帮主认另一个帮助做老大(啧啧,真刺激

初始化

ps.此乃“各自为政”

fa = list(range(n+1))  # 这样写方便且运行速度快

合并merge(注意是前“归顺”后)

code

fa[find(x)] = find(y)  # 都是对根节点的操作,x的根节点接到y的根节点上

思想

第一步,假设是2并到(归顺)1, 4并到(归顺)3。从原本各自为政到几个小集合(组织),还是挺简单的吧

在这里插入图片描述

那么如果3并到(归顺)2会出现什么情况呢?

是这样吗?

在这里插入图片描述

从这个大×可以看出,这是十分错误的,因为2并没有收3为“小弟”的权限(他不是领导)

所以实际上还是1收了小弟,结果会变成这样

在这里插入图片描述

到此已经可以处理最简单的并查集合并操作了。但是这样有一点不好。就从这图举例,虽然从2,3找到领导1都是一步就能找到,但是从4找到1就得先找到3发现它不是首领之后才能找到1,这样会有效率的浪费。

在链越来越长时重复查找造成的时间损失是巨大的,那么我们采取一种措施,叫“路径压缩”

查询根节点find

要是一直按上面那样加下去,就会导致链越来越长,find的时间也随之变长,所以需要路径压缩,将4直接指向根节点1,即在查询时将父节点指向祖宗节点实现压缩

在这里插入图片描述

ps.这是压缩后的结果图,对三来说,下一次查询的路径便被压缩了

路径压缩的查询

def find(x, f):
    if x != f[x]:
        fa[x] = find[f[x]]
    return f[x]

查询是否同一集合

if find(x) == find(y):  # 也是对根节点的判断

根据是否在一个集合可以进行优化

fx, fy = find(x), find(y)  # 如果编辑器版本较高我更倾向于用海象运算符
if fx != fy:
	fa[fx] = fy

查询有几个集合

  1. print(sum([1 for i in range(1, n+1) if i == fa[i]]))  # 因为并查集只进行并的操作,所以一个集合的根节点是始终没动过的,可以通过有几个没动过的节点来判断有几个集合
    
  2. print(len({fa[i] for i in range(1, n+1)}))  # 虽然,但是,下面这个更快
    
  3. # 适用于多阶段查询,可以在每次的基础上查下一次添加后的集合数
    res += 1  # 添加进了一个节点,因为没合并之前都是单独的,所以先+1
    ri = find(i)  # 理论上只需产生一次ri,为了下面不把它替换掉,必须rj归顺ri
    for j in g[i]:  # 邻接表里根据接入的节点i考虑合并的操作
        rj = find(j)
        if rj != ri:  # 合并自己组织的成员没有意义
            fa[rj] = ri  # 就相当于打一场fi合并一个组织
    

题型

一般型

题目传送门

[P8686 蓝桥杯 2019 省 A] 修改数组 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

code
def find(x):
    if x != f[x]:
        f[x] = find(f[x])
    return f[x]


def merge(x, y):  # 这题根节点都是集合最大的那个数, 取x小y大
    f[x] = find(y)  # 这题只能小的并到大的里面,且小的根节点一定是本身x


def check(x):
    f[x] = x  # 都是未出现过的数,要先初始化集合
    if f[x+1]:
        merge(x, x+1)
    if f[x-1]:
        merge(x-1, x)


n = int(input())
a = list(map(int, input().split()))
f = [0 for _ in range(max(a)+n+1)]
for i in a:
    if not f[i]:
        print(i, end=' ')
        check(i)
    else:
        m = find(i)+1
        print(m, end=' ')
        check(m)

逆向转化

题目传送门

[P1197 JSOI2008] 星球大战 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

在并查集里难以删除节点,如果有删除节点的操作要将剩下的先建立并查集,再将删除的一个一个逆序的添加进去

code
def find(x, f):
    if x != f[x]:
        f[x] = find(f[x], f)
    return f[x]


n, m = map(int, input().split())
fa = list(range(n))
a = [list(map(int, input().split())) for _ in range(m)]
b = {}
c = []
k = int(input())
vis = [1]*n
res = n-k
for _ in range(k):
    i = int(input())
    c.append(i)
    b[i] = []
    vis[i] = 0
ans = []
for u, v in a:
    if u in b:
        b[u].append(v)
    if v in b:
        b[v].append(u)
    if u not in b and v not in b:
        fu, fv = find(u, fa), find(v, fa)
        if fu != fv:
            res -= 1
            fa[fu] = fv
ans.append(res)
for i in range(len(c)-1, -1, -1):
    vis[c[i]] = 1
    res += 1
    ri = find(c[i], fa)
    for j in b[c[i]]:
        if vis[j]:
            rj = find(j, fa)
            if ri != rj:
                fa[rj] = ri
                res -= 1
    ans.append(res)
for i in range(k, -1, -1):
    print(ans[i])

声明:因为是我自己写着练习,所以变量名的命名方式相当随意,还请谅解。并且因为python自身特性,这题可能跑不了满分

带权并查集

题目传送门

[P8779 蓝桥杯 2022 省 A] 推导部分和 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

code
import sys
def find(x, w):
    if x != fa[x]:
        fa[x], w[x] = find(fa[x], w), w[x]+w[t]
    return fa[x]


n, m, q = map(int, input().split())
fa = list(range(n+1))
w = [0]*(n+1)
ans = []
for _ in range(m):
    l, r, s = map(int, input().split())
    rl, rr = find(l-1, w), find(r, w)
    if rl != rr:
        fa[rl] = rr
        w[rl] = w[r]-w[l-1]+s
for _ in range(q):
    l, r = map(int, input().split())
    ans.append(str(w[l-1]-w[r]) if find(l-1, w) == find(r, w) else 'UNKNOWN')
sys.stdout.write('\n'.join(ans))

权重计算的原理

带你们看一段晦涩难懂的文字

带权并查集中的权重计算原理涉及到路径压缩优化,它用于在合并集合时更新节点的权重。

在并查集中,每个节点都有一个指向其父节点的指针。在查找根节点时,如果节点 x 的父节点不是根节点,那么我们可以将 x 的父节点指向其祖父节点,以缩短查找路径并优化性能。

在带权并查集中,我们为每个节点维护一个权重,表示该节点与其根节点之间的差异。具体地,节点 x 的权重等于其值与其父节点的权重之差。

当我们在合并节点 xy 所在的集合时,我们需要更新它们之间的权重。假设节点 xy 的根节点分别是 root_xroot_y。如果我们将 y 的根节点合并到 x 的根节点中,那么我们需要将 y 的权重更新为 val + weight[x] - weight[y],其中 valxy 之间的权重差。这是因为 y 的根节点现在是 x 的根节点,所以我们需要用 val 减去 xy 之间的权重差来更新 y 的权重。

在查找节点的根节点时,我们也需要更新节点的权重,以反映它与其根节点之间的差异。在查找 x 的根节点时,我们需要递归地查找 x 的父节点,并将 x 的权重更新为其值与其父节点的权重之差,以便在以后的查询中使用。最后,我们将 x 的父节点更新为根节点,并返回根节点和权重。


其实路径压缩的权值一并压缩挺好理解的。难以理解的是上面当然上面还是晦涩难懂,肿么办——图形来相助

在这里插入图片描述

∵ x 到新的根节点 r y 的两条路径权重一定一样,即 s + w [ y ] = w [ x ] + d [ r x ] ∴ 要将 d [ r x ] 从 0 更新到 w [ y ] + s − w [ x ] \begin{align} &∵x到新的根节点ry的两条路径权重一定一样,即s+w[y]=w[x]+d[rx]\\ &∴要将d[rx]从0更新到w[y]+s-w[x] \end{align} x到新的根节点ry的两条路径权重一定一样,即s+w[y]=w[x]+d[rx]要将d[rx]0更新到w[y]+sw[x]
是不是无比清晰(乐)

注意

  1. 合并操作永远是对根节点的操作
  2. w是到根节点的权重
  3. 实际上x到rx的整条路径都是要更新的,但是在查找rx时路径压缩过后已经经过了第一次更新,下一次更新只要在rx更新过的基础上在find压缩一次路径就会全部更新成正确的值
查询find
def find(x, w):
	if x != fa[x]:
        t = fa[x]  # 如果不存则
        fa[x] = find(fa[x])
        w[x] += w[t]  # 加深一下对dfs的理解,在递归下面的都是回溯的过程,这样回溯d[i]就是i到结点的权
    return fa[x]

#  可以用等价的方法改写
def find(x, w):
	if x != fa[x]:
        fa[x],w[x] = find(fa[x]), w[x]+w[fa[x]]  # 必须是fa在前面,这样才能确保先递归再用。w[fa[x]]中fa[x]是原先的fa[x], 但是
    return fa[x]
合并merge
rl, rr = find(l-1, w), find(r, w)
    if rl != rr:
        fa[rl] = rr
        w[rl] = w[r]-w[l-1]+s

ps. 跟一般的并查集比就差了这两个地方

  • 7
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陈子昂-北工大

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值