并查集做题笔记

并查集的做题笔记

题都用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,m105

模板题,直接套并查集模板,对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]的求解
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)
  • 21
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值