并查集模板——核心就是路径压缩
2023/2/2更新:
补充一个二维矩阵并查集的leetcode题目
130. 被围绕的区域
m x n
的矩阵 board
,由若干字符 'X'
和 'O'
,找到所有被 'X'
围绕的区域,并将这些区域里所有的 'O'
用 'X'
填充。
示例 1:
输入:board = [["X","X","X","X"],["X","O","O","X"],["X","X","O","X"],["X","O","X","X"]] 输出:[["X","X","X","X"],["X","X","X","X"],["X","X","X","X"],["X","O","X","X"]] 解释:被围绕的区间不会存在于边界上,换句话说,任何边界上的'O'
都不会被填充为'X'
。 任何不在边界上,或不与边界上的'O'
相连的'O'
最终都会被填充为'X'
。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。
示例 2:
输入:board = [["X"]] 输出:[["X"]]
当然,使用dfs也可以做!
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上:
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。
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。对于造成任何不便,我们深感歉意。
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]
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也是可以的!我在面试一个小伙的时候他就这么做过。
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的。。。
""" 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之间
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
。
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!!!