并查集模板——核心就是路径压缩
2023/2/2更新:
补充一个二维矩阵并查集的leetcode题目
130. 被围绕的区域
m x n
的矩阵 board
,由若干字符 'X'
和 'O'
,找到所有被 'X'
围绕的区域,并将这些区域里所有的 'O'
用 'X'
填充。
示例 1:

'O'
'X'
'O'
'O'
'X'
示例 2:
输入:board = [["X"]] 输出:[["X"]]
当然,使用dfs也可以做!
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 Solution: def solve( self , board) - > None : """ Do not return anything, modify board in-place instead. """ m, n = len (board), len (board[ 0 ]) def need_fill(x, y): # print("seen:", seen) board[x][y] = '#' for dx, dy in [( 1 , 0 ), ( - 1 , 0 ), ( 0 , 1 ), ( 0 , - 1 )]: x2, y2 = x + dx, y + dy if 0 < = x2 < m and 0 < = y2 < n and (x2, y2) and board[x2][y2] = = 'O' : need_fill(x2, y2) for j in range (n): if board[ 0 ][j] = = 'O' : need_fill( 0 , j) if board[m - 1 ][j] = = 'O' : need_fill(m - 1 , j) for i in range ( 1 , m - 1 ): if board[i][ 0 ] = = 'O' : need_fill(i, 0 ) if board[i][n - 1 ] = = 'O' : need_fill(i, n - 1 ) for i in range (m): for j in range (n): if board[i][j] = = 'O' : board[i][j] = 'X' elif board[i][j] = = '#' : board[i][j] = 'O' return board |
我们用并查集写下,杜绝了繁琐的判定seen过程,将临界边上的O和其连接的O统统归到dummy node上:
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 45 46 47 48 49 50 51 52 | class UionFind: def __init__( self , cnt): self .parent = {i:i for i in range (cnt)} def union( self , i, j): self .parent[ self .find(i)] = self .find(j) def find( self , i): path = [] node = i while node ! = self .parent[node]: path.append(node) node = self .parent[node] root = node for node in path: self .parent[node] = root return root def is_connect( self , i, j): return self .find(i) = = self .find(j) class Solution: def solve( self , board) - > None : """ Do not return anything, modify board in-place instead. """ m, n = len (board), len (board[ 0 ]) uf = UionFind(m * n + 1 ) dummy = m * n def node(i, j): return i * n + j for i in range (m): for j in range (n): if board[i][j] = = 'O' : if i = = 0 or i = = m - 1 or j = = 0 or j = = n - 1 : uf.union(node(i,j), dummy) for dx, dy in [( 1 , 0 ), ( 0 , 1 )]: i2, j2 = i + dx, j + dy if i2 < m and j2 < n and board[i2][j2] = = 'O' : uf.union(node(i, j), node(i2, j2)) for i in range (m): for j in range (n): if board[i][j] = = 'O' and not uf.is_connect(node(i, j), dummy): board[i][j] = 'X' return board |
---------以上为更新
547. 朋友圈
班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。
给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。
示例 1:
输入: [[1,1,0], [1,1,0], [0,0,1]] 输出: 2 说明:已知学生0和学生1互为朋友,他们在一个朋友圈。 第2个学生自己在一个朋友圈。所以返回2。
示例 2:
输入: [[1,1,0], [1,1,1], [0,1,1]] 输出: 1 说明:已知学生0和学生1互为朋友,学生1和学生2互为朋友,所以学生0和学生2也是朋友,所以他们三个在一个朋友圈,返回1。
注意:
- N 在[1,200]的范围内。
- 对于所有学生,有M[i][i] = 1。
- 如果有M[i][j] = 1,则有M[j][i] = 1。
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 | class Solution( object ): def findCircleNum( self , M): """ :type M: List[List[int]] :rtype: int """ N = len (M) self .init(N) for i in range ( 0 , N): for j in range (i + 1 , N): if M[i][j]: self .connect(i, j) return self .count() def count( self ): cnt = 0 for i,n in enumerate ( self .father): if self .father[i] = = i: cnt + = 1 return cnt def init( self , N): self .father = [ 0 ] * N for i in range (N): self .father[i] = i def connect( self , a, b): self .father[ self .find(a)] = self .find(b) def find( self , node): path = [] while self .father[node] ! = node: path.append(node) node = self .father[node] for n in path: self .father[n] = node return node |
684. 冗余连接
在本问题中, 树指的是一个连通且无环的无向图。
输入一个图,该图由一个有着N个节点 (节点值不重复1, 2, ..., N) 的树及一条附加的边构成。附加的边的两个顶点包含在1到N中间,这条附加的边不属于树中已存在的边。
结果图是一个以边
组成的二维数组。每一个边
的元素是一对[u, v]
,满足 u < v
,表示连接顶点u
和v
的无向图的边。
返回一条可以删去的边,使得结果图是一个有着N个节点的树。如果有多个答案,则返回二维数组中最后出现的边。答案边 [u, v]
应满足相同的格式 u < v
。
示例 1:
输入: [[1,2], [1,3], [2,3]] 输出: [2,3] 解释: 给定的无向图为: 1 / \ 2 - 3
示例 2:
输入: [[1,2], [2,3], [3,4], [1,4], [1,5]] 输出: [1,4] 解释: 给定的无向图为: 5 - 1 - 2 | | 4 - 3
注意:
- 输入的二维数组大小在 3 到 1000。
- 二维数组中的整数在1到N之间,其中N是输入数组的大小。
更新(2017-09-26):
我们已经重新检查了问题描述及测试用例,明确图是无向 图。对于有向图详见冗余连接II。对于造成任何不便,我们深感歉意。
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 | class Solution( object ): def findRedundantConnection( self , edges): """ :type edges: List[List[int]] :rtype: List[int] """ N = len (edges) self .init(N) ans = [ None , None ] for i,j in edges: is_success, same_nodes = self .connect(i, j) if not is_success: ans = same_nodes return ans def connect( self , a, b): f1 = self .find(a) f2 = self .find(b) if f1 ! = f2: self .father[f1] = f2 return True , [ None , None ] else : return False , [a,b] def init( self , N): self .father = {} for i in range ( 1 , N + 1 ): self .father[i] = i def find( self , node): path = [] while self .father[node] ! = node: path.append(node) node = self .father[node] for n in path: self .father[n] = node return node |
591. 连接图 III
给一个图中的 n
个节点, 记为 1
到 n
. 在开始的时候图中没有边.
你需要完成下面两个方法:
connect(a, b)
, 添加一条连接节点 a, b的边query()
, 返回图中联通区域个数
样例
例1:
输入:
ConnectingGraph3(5)
query()
connect(1, 2)
query()
connect(2, 4)
query()
connect(1, 4)
query()
输出:[5,4,3,3]
例2:
输入:
ConnectingGraph3(6)
query()
query()
query()
query()
query()
输出:
[6,6,6,6,6]
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 | class ConnectingGraph3: """ @param a: An integer @param b: An integer @return: nothing """ def __init__( self , n): # initialize your data structure here. self .father = [ 0 ] * (n + 1 ) self .n = n for i in range ( 1 , n + 1 ): self .father[i] = i self .zone_cnt = n def find( self , p): paths = [] while p ! = self .father[p]: paths.append(p) p = self .father[p] root = p for node in paths: self .father[node] = root return root def connect( self , a, b): # write your code here f1 = self .find(a) f2 = self .find(b) if f1 ! = f2: self .zone_cnt - = 1 self .father[f1] = f2 """ @return: An integer """ def query( self ): # write your code here return self .zone_cnt |
当然,这个题目使用BFS也是可以的!我在面试一个小伙的时候他就这么做过。
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 45 46 47 48 49 50 51 52 53 54 55 | class ConnectingGraph3: """ @param a: An integer @param b: An integer @return: nothing """ def __init__( self , n): # initialize your data structure here. self .seen = set () self .node_map = {} self .n = n def connect( self , a, b): # write your code here if a not in self .node_map: self .node_map[a] = set () self .node_map[a].add(b) if b not in self .node_map: self .node_map[b] = set () self .node_map[b].add(a) """ @return: An integer """ def query( self ): # write your code here ans = 0 self .seen = set () for i in range ( 1 , self .n + 1 ): if i not in self .seen: self .bfs(i) ans + = 1 return ans def bfs( self , root): q = collections.deque([root]) self .seen.add(root) while q: node = q.popleft() if node not in self .node_map: continue for neighbor in self .node_map[node]: if neighbor not in self .seen: self .seen.add(neighbor) q.append(neighbor) |
434. 岛屿的个数II
给定 n, m, 分别代表一个二维矩阵的行数和列数, 并给定一个大小为 k 的二元数组A. 初始二维矩阵全0. 二元数组A内的k个元素代表k次操作, 设第i个元素为 (A[i].x, A[i].y)
, 表示把二维矩阵中下标为A[i].x行A[i].y列的元素由海洋变为岛屿. 问在每次操作之后, 二维矩阵中岛屿的数量. 你需要返回一个大小为k的数组.
样例
样例 1:
输入: n = 4, m = 5, A = [[1,1],[0,1],[3,3],[3,4]]
输出: [1,1,2,2]
解释:
0. 00000
00000
00000
00000
1. 00000
01000
00000
00000
2. 01000
01000
00000
00000
3. 01000
01000
00000
00010
4. 01000
01000
00000
00011
样例 2:
输入: n = 3, m = 3, A = [[0,0],[0,1],[2,2],[2,1]]
输出: [1,1,2,2]
注意事项
设定0表示海洋, 1代表岛屿, 并且上下左右相邻的1为同一个岛屿.
这个题目有点难,主要是要通过100%的用例还是不好搞,是GG的。。。
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | """ Definition for a point. class Point: def __init__(self, a=0, b=0): self.x = a self.y = b """ class Solution: """ @param n: An integer @param m: An integer @param operators: an array of point @return: an integer array """ def numIslands2( self , n, m, operators): # write your code here self .father = {} self .matrix = [[ 0 ] * m for i in range (n)] for i in range (n): for j in range (m): self .father[(i, j)] = (i, j) ans = [] self .cnt = 0 for p in operators: a, b = p.x, p.y if self .matrix[a][b]: ans.append( self .cnt) continue self .matrix[a][b] = 1 self .cnt + = 1 for x, y in [( - 1 , 0 ), ( 1 , 0 ), ( 0 , - 1 ), ( 0 , 1 )]: if 0 < = a + x < n and 0 < = b + y < m and self .matrix[a + x][b + y]: f1 = self .find(a + x, b + y) f2 = self .find(a, b) if f1 ! = f2: self .father[f1] = f2 self .cnt - = 1 ans.append( self .cnt) return ans def find( self , p1, p2): a, b = p1, p2 path = [] while (a, b) ! = self .father[(a, b)]: path.append((a, b)) a, b = self .father[(a, b)] for p in path: self .father[p] = (a, b) return (a, b) |
主要是细节处理,比较容易出错!!!
对于每一次操作(x, y), 如果(x, y)的上下左右都是0, 那么计数器加一; 如果不全为0, 则:
- 并查集查询其四周的1所属的集合, 假设它们属于 k 个不同的集合
- 计数器减去 k-1
- 将这 k 个集合, 连同 (x, y), 合并
178. 图是否是树
给出 n
个节点,标号分别从 0
到 n - 1
并且给出一个 无向
边的列表 (给出每条边的两个顶点), 写一个函数去判断这张`无向`图是否是一棵树
样例
样例 1:
输入: n = 5 edges = [[0, 1], [0, 2], [0, 3], [1, 4]]
输出: true.
样例 2:
输入: n = 5 edges = [[0, 1], [1, 2], [2, 3], [1, 3], [1, 4]]
输出: false.
注意事项
你可以假设我们不会给出重复的边在边的列表当中. 无向
边 [0, 1]
和 [1, 0]
是同一条边, 因此他们不会同时出现在我们给你的边的列表当中。
class Solution:
"""
@param n: An integer
@param edges: a list of undirected edges
@return: true if it's a valid tree, or false
"""
def validTree(self, n, edges):
if n - 1 != len(edges): # 这个是必须的,因为tree最终只能是n-1条变
return False
self.father = {i: i for i in range(n)}
self.size = n
for a, b in edges:
self.union(a, b)
return self.size == 1 #最终只能是一个孤岛
def union(self, a, b):
root_a = self.find(a)
root_b = self.find(b)
if root_a != root_b:
self.size -= 1
self.father[root_a] = root_b
def find(self, node):
path = []
while node != self.father[node]:
path.append(node)
node = self.father[node]
for n in path:
self.father[n] = node
return node
1070. 账户合并
给定一个帐户列表,每个元素accounts [i]
是一个字符串列表,其中第一个元素accounts [i] [0]
是账户名称,其余元素是这个帐户的电子邮件。
现在,我们想合并这些帐户。
如果两个帐户有相同的电子邮件地址,则这两个帐户肯定属于同一个人。
请注意,即使两个帐户具有相同的名称,它们也可能属于不同的人,因为两个不同的人可能会使用相同的名称。
一个人可以拥有任意数量的账户,但他的所有帐户肯定具有相同的名称。
合并帐户后,按以下格式返回帐户:每个帐户的第一个元素是名称,其余元素是按字典序排序后的电子邮件。
帐户本身可以按任何顺序返回。
样例
样例 1:
输入:
[
["John", "johnsmith@mail.com", "john00@mail.com"],
["John", "johnnybravo@mail.com"],
["John", "johnsmith@mail.com", "john_newyork@mail.com"],
["Mary", "mary@mail.com"]
]
输出:
[
["John", 'john00@mail.com', 'john_newyork@mail.com', 'johnsmith@mail.com'],
["John", "johnnybravo@mail.com"],
["Mary", "mary@mail.com"]
]
解释:
第一个第三个John是同一个人的账户,因为这两个账户有相同的邮箱:"johnsmith@mail.com".
剩下的两个账户分别是不同的人。因为他们没有和别的账户有相同的邮箱。
你可以以任意顺序返回结果。比如:
[
['Mary', 'mary@mail.com'],
['John', 'johnnybravo@mail.com'],
['John', 'john00@mail.com', 'john_newyork@mail.com', 'johnsmith@mail.com']
]
也是可以的。
注意事项
账户个数在1~1000之间
每个账户下的电子邮件在1~10之间
每个字符串的长度在1~30之间
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 45 46 47 48 49 50 51 52 53 54 55 | class Solution: """ @param accounts: List[List[str]] @return: return a List[List[str]] """ def accountsMerge( self , accounts): # write your code here acc_dict = self .init(accounts) clusters = self .join_accounts(acc_dict) return self .merge(clusters, acc_dict) def init( self , accounts): acc_dict = {} self .father = {} for acc in accounts: for i in range ( 1 , len (acc)): if acc[i] not in self .father: self .father[acc[i]] = acc[ 1 ] else : # detail CAUSTION f1 = self .find(acc[i]) f2 = self .find(acc[ 1 ]) self .father[f1] = f2 acc_dict[acc[i]] = acc[ 0 ] return acc_dict def find( self , x): path = [] while x ! = self .father[x]: path.append(x) x = self .father[x] for p in path: self .father[p] = x return x def join_accounts( self , acc_dict): clusters = collections.defaultdict( set ) for acc in acc_dict: f = self .find(acc) clusters[f].add(acc) return clusters def merge( self , clusters, acc_dict): ans = [] for c in clusters: ans.append([acc_dict[c]] + sorted ( list (clusters[c]))) return ans |
有没有发现这种题目比较XXX!小细节处理易错,一次性100%通过用例比较难!
我自己处理的逻辑不清晰,应该用下面的方式!!!
1396. 集合合并
有一个集合组成的list,如果有两个集合有相同的元素,将他们合并。返回最后还剩下几个集合。
样例
样例1:
输入:list = [[1,2,3],[3,9,7],[4,5,10]]
输出:2 .
样例:剩下[1,2,3,9,7]和[4,5,10]这2个集合。
样例 2:
输入:list = [[1],[1,2,3],[4],[8,7,4,5]]
输出 :2
解释:剩下[1,2,3]和[4,5,7,8] 2个集合。
注意事项
- 集合数 n <=
1000
。 - 每个集合的元素个数
m <= 100
。 - 元素一定是非负整数,且不大于
100000
。
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 45 | class Solution: """ @param sets: Initial set list @return: The final number of sets """ def setUnion( self , sets): # Write your code here self .init(sets) self .connect(sets) return self .get_clusters() def init( self , sets): self .data_set = set () self .father = {} for items in sets: for x in items: self .father[x] = x self .data_set.add(x) def connect( self , sets): for items in sets: for i in range ( 1 , len (items)): self .union(items[ 0 ], items[i]) # 关键!!!就按部就班来就好,不要去创新!!! def get_clusters( self ): clusters = collections.defaultdict( set ) for x in self .data_set: f = self .find(x) clusters[f].add(x) return len (clusters) def union( self , x, y): self .father[ self .find(x)] = self .find(y) def find( self , x): path = [] while x ! = self .father[x]: path.append(x) x = self .father[x] for p in path: self .father[p] = x return x |
非常清晰和简单的逻辑,写起来代码就非常不容易出错了!!!关键就是找到union!!!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」
2018-04-03 leetcode 401. Binary Watch
2018-04-03 Apriori算法实例