并查集的做题笔记
题都用python解释
题来源于AcWing和LeetCode
还会持续更新遇到的并查集相关的题解,欢迎收藏。
AcWing 836.合并集合
一共有 n个数,编号是 1∼n,最开始每个数各自在一个集合中。
现在要进行 m个操作,操作共有两种:
M a b M a b Mab,将编号为a和b的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
Q a b Q a b Qab,询问编号为 a和 b的两个数是否在同一个集合中;
输入格式
第一行输入整数 n和 m。
接下来 m行,每行包含一个操作指令,指令为 M a b 或 Q a b 中的一种。
输出格式
对于每个询问指令 Q a b,都要输出一个结果,如果 a和 b在同一集合内,则输出 Yes,否则输出 No。
每个结果占一行。
数据范围
1 < n , m ≤ 1 0 5 1<n,m≤10^5 1<n,m≤105
模板题,直接套并查集模板,对Q执行查询操作,对M执行合并操作。
看不懂先看这一篇点这
Code
n, m = map(int, input().split())
p = [0 for _ in range(100010)]
for i in range(1, n + 1):
p[i] = i
def find(x):
j = 0
k = 0
i = x
while p[i] != i:
i = p[i]
k = x
while k != i:
j = p[k]
p[k] = i
k = j
return i
while m:
m -= 1
work = list(map(str, input().split()))
a, b = int(work[1]), int(work[2])
if work[0] == "Q":
if find(a) == find(b):
print('Yes')
else:
print('No')
elif work[0] == "M":
p[find(a)] = find(b)
LeetCode 1971. 寻找图中是否存在路径
有一个具有 n 个顶点的 双向 图,其中每个顶点标记从 0 到 n - 1(包含 0 和 n - 1)。图中的边用一个二维整数数组 edges 表示,其中 edges[i] = [ui, vi] 表示顶点 ui 和顶点 vi 之间的双向边。 每个顶点对由 最多一条 边连接,并且没有顶点存在与自身相连的边。
请你确定是否存在从顶点 source 开始,到顶点 destination 结束的 有效路径 。
给你数组 edges 和整数 n、source 和 destination,如果从 source 到 destination 存在 有效路径 ,则返回 true,否则返回 false 。
连通性的问题,可以用并查集。
初始化并查集后,将每边的两个点合并,更新并查集。
给出的图
e
d
g
e
s
edges
edges肯定能以某个数作为起点遍历整个图(该数就作为并查集的根节点),将该图中的所有点作为一个集合,合并构建出并查集,只要给出的
s
o
u
r
c
e
source
source和
d
e
s
t
i
n
a
t
i
o
n
destination
destination在同一个图中,则肯定是相互连通存在有效路径的,也代表着肯定有共同的根节点,查询判断即可。
Code
class Solution:
def validPath(self, n: int, edges: List[List[int]], source: int, destination: int) -> bool:
p = [i for i in range(n)]
def find(x):
# 递归 + 路径压缩
if p[x] != x:
p[x] = find(p[x])
return p[x]
# 合并
for u, v in edges:
p[find(u)] = find(v)
return find(source) == find(destination)
Acwing 837.连通块中点的数量
操作1、2和合并、查询操作相同,所以直接套模板开抄。最主要的是操作3,查询点a在连通块中点的数量,即为在含有a的集合中的节点的数量。
所以我们只用在根节点中维护一个数表示该集合中节点的总数。即用
f
i
n
d
find
find找到根节点后,用数组存储该根节点对应的集合中的节点总数。
Code
n, m = map(int, input().split())
p = [i for i in range(n+1)]
cn = [1 for i in range(n+1)]
def find(x):
# 迭代 + 路径压缩
global count
i = x
while p[i] != i:
i = p[i]
k = x
while k != i:
j = p[k]
p[k] = i
k = j
return i
while m:
m -= 1
work = list(map(str, input().split()))
if work[0] == "C":
a, b = int(work[1]), int(work[2])
# 当a、b不在同一集合中才将b集合中的节点总数加上a集合中的节点总数,如果不特判则会重复相加。
if find(a) != find(b):
# 下面两句顺序不能换,因为第二句会合并两集合,导致根节点改变,进而对应的节点数量也改变,除非先用临时变量存放find(a)和find(b)
cn[find(b)] += cn[find(a)]
p[find(a)] = find(b)
elif work[0] == "Q1":
a, b = int(work[1]), int(work[2])
if find(a) == find(b):
print("Yes")
else:
print("No")
else:
a = int(work[1])
print(cn[find(a)])
AcWing 240.食物链
用并查集中两节点到根节点的距离之差为0或1表示同类或者捕食。
Code
n, k = map(int, input().split())
N = 500010
p = [0 for _ in range(N)]
d = [0 for _ in range(N)]
for i in range(1, n+1):
p[i] = i
def find(x):
if p[x] != x:
# 通过递归,计算出x到根节点的距离
# 计算d[x]要自上而下的计算,所以要先用一个变量去递归到根节点,从根节点开始向下更新d。
# 如果不从最上开始计算,d[p[x]]未更新,直接用d[x]去加就出错了
# d[x]存的是x到父节点的距离,因为在路径压缩后节点都指向了根节点
u = find(p[x])
d[x] += d[p[x]]
p[x] = u
return p[x]
res = 0 # 记录假话个数
while k:
k -= 1
work = list(map(str, input().split()))
x = int(work[1])
y = int(work[2])
# x,y不在这n个动物之中
if x > n or y > n:
res += 1
continue
if work[0] == "1": # 判断x、y是不是同类
a = find(x)
b = find(y)
# x、y在同一个集合中,直接判断即可
# 如果x、y是同类,则x到y的距离模3为0
# 因为d中的数不一定是正数,所以最好先运算在模3,下面的运算同
if a == b and (d[x] - d[y]) % 3:
# 距离不为0,假话+1
res += 1
# x、y不在同一集合中
elif a != b:
# 两集合合并,更新p
p[a] = b
# 根节点a连接根节点b的线的长度d[a]为d[y] - d[x]
# 因为(d[x] + d[a] - d[y]) % 3 == 0
# 此时不用更新d[x],在再次调用find时会自动更新
d[a] = d[y] - d[x]
else:
a = find(x)
b = find(y)
# x、y在同一个集合中
# 当x、y相隔距离不为1,则(d[x] - d[y] - 1) % 3 != 0
if a == b and (d[x] - d[y] - 1) % 3:
res += 1
# x、y不在同一集合中
elif a != b:
p[a] = b
# 此处其实就是d数组的数据来源,因为x吃y的关系在d中被设置为相隔距离为1
# 所以在一开始输入数据也是从谁吃谁开始,此时才在d中设置了距离,用于接下来的判断
d[a] = d[y] - d[x] + 1
print(res)
d [ a ] d[a] d[a]的求解
2368.受限条件下可到达节点的数目
2024.3.2
每日一题
2024.3.2每日一题
2024.3.2每日一题
因为有限制条件,所以如果忽略掉限制节点,则树会被分为多个连通块,我们只需要取得包含0的连通块的大小即可。
在使用并查集合并节点时,如果一条边中如果有一个点或两个点都为受限节点,则跳过当前边,不合并,用
s
z
sz
sz数组统计每个连通块的大小,最后返回包含0的连通块的大小。
Code
class Solution:
def reachableNodes(self, n: int, edges: List[List[int]], restricted: List[int]) -> int:
p = [i for i in range(n)] # 初始化集合,因为节点编号为0 - n-1,所以只用初始化0 - n-1个节点
sz = [1 for _ in range(n)] # 统计每个连通块的大小
# 查找根节点
# 递归 + 路径压缩
def find(x):
if p[x] != x:
p[x] = find(p[x])
return p[x]
# 记录受限节点
d = [0 for _ in range(n)]
for res in restricted:
d[res] = 1
for a, b in edges:
# 如果当前边中至少有一个受限节点,则跳过当前边
if d[a] or d[b]:
continue
# 当前边中没有受限节点
# 取得边上两点的根节点rx、ry
rx = find(a)
ry = find(b)
# 如果根节点不相同,则执行合并操作,防止sz的重复累加
if rx != ry:
p[ry] = rx
sz[rx] += sz[ry]
# 最后返回包含0的连通块的大小
return sz[find(0)]
684.冗余连接
树是无环的,边的数量为n-1(n为节点个数),而图是有环的,边的个数为n。
所以在要添加冗余的边将树变为图时,当前要添加的边上的两个点肯定都在树上,所以使用并查集,在两个节点出现祖宗节点相同时,则说明此时要添加冗余的边了。
Code
class Solution:
def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
def find(x):
if p[x] != x:
p[x] = find(p[x])
return p[x]
n = len(edges)
p = [i for i in range(0, n+1)]
for a, b in edges:
# 祖宗节点相同时,要此时节点a,b的边就为冗余的边
if p[find(b)] == p[find(a)]:
return [a, b]
# 反之合并
else:
p[find(b)] = p[find(a)]
547.省份数量
将 i s C o n n e c t e d [ i ] [ j ] = = 1 isConnected[i][j] == 1 isConnected[i][j]==1的省份 i i i、 j j j合并,遍历一遍数组,将所有连通的省份合并在一起后,查看并查集内祖宗节点的数量,返回即可。
Code
class Solution:
def findCircleNum(self, isConnected: List[List[int]]) -> int:
n = len(isConnected)
p = [i for i in range(n+1)]
# 查找
def find(x):
if p[x] != x:
p[x] = find(p[x])
return p[x]
# 合并
for i in range(n):
for j in range(len(isConnected[i])):
if isConnected[i][j] == 1 and p[find(i)] != p[find(j)]:
p[find(j)] = p[find(i)]
# 计算祖宗节点的数量
ans = set()
for i in range(n):
for j in range(len(isConnected[i])):
ans.add(p[find(j)])
return len(ans)