并查集
作用
- 将两个集合合并
- 询问两个元素是否在一个集合当中(或是否根出同源)。
常见用途有求连通子图、求最小生成树的 K r u s k a l Kruskal Kruskal算法和求最近公共祖先(LCA)等。
基本原理
每一个集合用一个树来表示,树根的编号就是整个集合的编号,每个节点存储它的父节点, p [ x ] p[x] p[x]表示 x x x的父节点。
主要问题
- 树根的定义: i f ( p [ x ] = = x ) if(p[x]==x) if(p[x]==x)
- 如何求x的集合编号,即找到树根:find操作
- 如何合并两个集合
基于以上几个问题,产生下列几个操作。
基本操作
1. 初始化 i n i t init init
首先将给出的数据以自身作为集合(以自己为下标指向自己),创建一个数组,记为p,即
nums = list() # 数据内容
p = [0 for _ in range(len(nums))] # p数组(大小为nums的长度)
for num in nums:
p[num] = num
图像解释:
在一系列合并操作后,某一节点仍然指向自己,即 p [ n u m ] = = n u m p[num] == num p[num]==num,则说明该节点没有被合并到其他节点上(无父节点),要么它是孤零零没被操作(在合并完所有数一般没有这种情况(以我贫瘠的知识储备没有遇到过,欢迎举例指出)),要么就是根节点(是其他节点的祖宗),所以树根被定义为 i f ( p [ x ] = = x ) if(p[x]==x) if(p[x]==x)。
2. 查询 f i n d find find
有三种方法
1. 直接迭代
从要查找的数(记为x)开始,向x的父节点开始查询,直到父节点等于本身,即 p [ x ] = = x p[x] == x p[x]==x,说明找到了树根。
Code
def find(x):
while p[x] != x:
x = p[x] # 继续遍历父节点
return x
这种方法时间复杂度比较高,容易TLE,所以不建议使用。
2. 直接迭代 + 路径压缩
路径压缩:如果出现要重复查询x的祖宗节点,在不用路径压缩的情况下,就要重复查询的次数次,时间消耗多,若树较长,则容易TLE。所以,为节省时间,在第一次查询后,直接将x到根节点之间的所有节点(包括x)直接指向根节点,则在下一次查询x与x到根节点之间的数时,只用向上找一次就可找到根节点
Code
def find(x):
j = 0
k = 0
i = x # 用i找到x的祖宗节点
while p[i] != i:
i = p[i]
k = x # k用来从x开始一层层向上遍历
# 路径压缩
while k != i:
j = p[k] # j存储k当前的父节点
p[k] = i # 让k的父节点指向祖宗节点
k = j # 让k继续向上遍历
return i
3. 递归
Code
def find(x):
if p[x] == x:
return x
else:
return find(p[x])
还是容易TLE
4. 递归 + 路径压缩
Code
def find(x):
if p[x] != x:
# find会一直递归下去直到找到根节点,最后返回根节点给当前的p[x]
# 对于x到根节点的所有数也相同,都将父节点改为根节点
p[x] = find(p[x])
return p[x]
3. 合并 u n i o n n unionn unionn
将该集合的根节点指向另一集合的根节点即可。
Code
def unionn(x, y):
p[find(x)] = find(y)
如果还没有看懂,推荐这个视频,更加全面细节,点这
接下来还会更新并查集的相关做题笔记,欢迎关注,提示更新。