文章目录
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
查询有几个集合
-
print(sum([1 for i in range(1, n+1) if i == fa[i]])) # 因为并查集只进行并的操作,所以一个集合的根节点是始终没动过的,可以通过有几个没动过的节点来判断有几个集合
-
print(len({fa[i] for i in range(1, n+1)})) # 虽然,但是,下面这个更快
-
# 适用于多阶段查询,可以在每次的基础上查下一次添加后的集合数 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
的权重等于其值与其父节点的权重之差。
当我们在合并节点 x
和 y
所在的集合时,我们需要更新它们之间的权重。假设节点 x
和 y
的根节点分别是 root_x
和 root_y
。如果我们将 y
的根节点合并到 x
的根节点中,那么我们需要将 y
的权重更新为 val + weight[x] - weight[y]
,其中 val
是 x
和 y
之间的权重差。这是因为 y
的根节点现在是 x
的根节点,所以我们需要用 val
减去 x
和 y
之间的权重差来更新 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]+s−w[x]
是不是无比清晰(乐)
注意
- 合并操作永远是对根节点的操作
- w是到根节点的权重
- 实际上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. 跟一般的并查集比就差了这两个地方