并查集的应用

并查集的应用

P1551 亲戚

题目背景

若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。

题目描述

规定:xxyy 是亲戚,yyzz 是亲戚,那么 xxzz 也是亲戚。如果 xxyy 是亲戚,那么 xx 的亲戚都是 yy 的亲戚,yy 的亲戚也都是 xx 的亲戚。

输入格式

第一行:三个整数 n,m,pn,m,p,(n,m,p5000n,m,p \le 5000),分别表示有 nn 个人,mm 个亲戚关系,询问 pp 对亲戚关系。

以下 mm 行:每行两个数 MiM_iMjM_j1Mi, Mjn1 \le M_i,~M_j\le n,表示 MiM_iMjM_j 具有亲戚关系。

接下来 pp 行:每行两个数 Pi,PjP_i,P_j,询问 PiP_iPjP_j 是否具有亲戚关系。

输出格式

pp 行,每行一个 YesNo。表示第 ii 个询问的答案为“具有”或“不具有”亲戚关系。

输入输出样例 #1

输入 #1

1
2
3
4
5
6
7
8
9
6 5 3
1 2
1 5
3 4
5 2
1 3
1 4
2 3
5 6

输出 #1

1
2
3
Yes
Yes
No

解决方案

对于森林的管理与查询,并查集是一种很高效的数据结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Dsu:
# 初始化
def __init__(self, size):
self.pa = list(range(size+1))

# 查询_路径压缩
def find(self, x):
if self.pa[x] != x:
self.pa[x] = self.find(self.pa[x])
return self.pa[x]

# 合并
def union(self, x, y):
self.pa[self.find(x)] = self.find(y)


def main():
n, m, p = map(int, input().split())
relatives = Dsu(n)

# 有亲戚关系,进行合并操作
for i in range(m):
x, y = map(int, input().split())
relatives.union(x, y)

# 已经结束合并,进行查询操作
for i in range(p):
x, y = map(int, input().split())
if relatives.find(x) == relatives.find(y):
print("Yes")
else:
print("No")

if __name__ == "__main__":
main()

Bingo!

下面来看蓝桥杯真题,这是带权并查集的一种应用:

P11005 [蓝桥杯 2024 省 Python B] 缴纳过路费

题目描述

在繁华的商业王国中,NN 座城市被 MM 条商路巧妙地连接在一起,形成了一个错综复杂的无向图网络。每条商路是双向通行的,并且任意两座城市之间最多只有一条直接的商路。每条商路都有它的规则,其中最引人注目的就是穿过
商路,需要缴纳过路费。因此,商人们在选择商路时必须格外认真。

有一位名叫小蓝的商人,他对于商路的花费有着自己独到的见解。在小蓝眼中,一条路线包含一条或多条商路,但路线的成本并不是沿途累积的过路费总和,而是这条路线上最贵的那一次收费。这个标准简单而直接,让他能迅速评估出一条路线是否划算。

于是,他设立了一个目标,即找出所有城市对,这些城市之间的最低路线成本介于他心中预设的两个数 LLRR 之间。他相信,这样的路线既不会太廉价,以至于路况糟糕;也不会过于昂贵,伤害他精打细算的荷包。

作为小蓝的助手,请你帮助小蓝统计出所有满足条件的城市对数量。

输入格式

输入的第一行包含四个整数 N,M,L,RN, M, L, R,表示有 NN 座城市和 MM 条双向通行的商路,以及小蓝心中预设的最高过路费的下限 LL 和上限 RR

接下来 MM 行,每行包含三个整数 u,v,wu, v,w,表示城市 uu 和城市 vv 之间有一条双向通行的商路,过路费为 ww。保证每对城市之间最多只有一条直接的商路。

输出格式

输出一行包含一个整数,表示满足条件的城市对数量。

输入输出样例 #1

输入 #1

1
2
3
4
5
6
5 5 1 2
1 2 2
1 3 5
1 4 1
2 4 5
2 5 4

输出 #1

1
3

说明/提示

对于 30% 的评测用例,1N103,1Mmin(2×103,N×(N1)2),1LR1051u,vN,uv,1w1051 \le N \le 10^3,1 \le M \le \min(2 \times 10^3,\frac{N×(N−1)}{2}), 1 \le L \le R \le 10^5,1 \le u, v \le N, u \ne v,1 \le w \le 10^5

对于所有评测用例,1N105,1Mmin(2×105,N×(N1)2),1LR109,1u,vN,uv1w1091 \le N \le 10^5,1 \le M \le \min(2 \times 10^5,\frac{N×(N−1)}{2}),1 \le L \le R \le 10^9,1 \le u, v \le N, u \ne v,1 \le w \le 10^9

样例解释

在样例中,满足条件的城市对有 (1,2),(1,4),(2,4)(1, 2),(1, 4),(2, 4)

解决方案

在这道题目当中我们可以将输入情况分为三种:

  • 小于左边界值的
  • 介于要求区间的
  • 大于右边界值的
    不难发现,对于第三种情况我们是应当舍弃的,因为一旦途径这条边,一定没有符合要求的路径。

    其次是前两种情况,其中第二种情况是真正起作用的,是使得路径合法的,因此这种输入下,只要联通,城市之间都可以两两互相配对。但是对于第一种情况而言,直接相连的城市并不合法,需要删除。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Dsu:
def __init__(self, size):
self.pa = list(range(size+1))
self.size = [1] * (size+1)
# 这里使用size数组来记录每个父节点下连接叶子节点的个数

def find(self, x):
if self.pa[x] != x:
self.pa[x] = self.find(self.pa[x])
return self.pa[x]

def union(self, x, y):
xx, yy = self.find(x), self.find(y)
if xx == yy:
return
self.pa[yy] = xx
self.size[xx] += self.size[yy]

def c(x):
return x * (x-1) // 2

def main():
n, m, l, r = map(int, input().split())

cities_1 = Dsu(n)
cities_2 = Dsu(n)

for i in range(m):
u, v, w = map(int, input().split())
if w <= r: cities_1.union(u, v)
if w < l: cities_2.union(u, v)

ans = 0
for i in range(1, n+1):
# 判断是不是父节点
if cities_1.find(i) == i:
ans += c(cities_1.size[i])
if cities_2.find(i) == i:
ans -= c(cities_2.size[i])

print(ans)

if __name__ == "__main__":
main()

Bingo!


并查集的应用
http://example.com/2025/04/07/note12/
作者
谢斐
发布于
2025年4月7日
许可协议