连通性的终极武器,近O(1)判断两个元素是否属于同一集合
并查集(Union-Find / Disjoint Set Union, DSU)是一种树形数据结构,用于处理不相交集合的合并与查询。它支持两个核心操作:find(查找元素所属集合的代表)和 union(合并两个集合)。凭借路径压缩和按秩合并,单次操作接近 O(1)。
find 操作时,将路径上的所有节点直接指向根节点。递归实现最简洁:parent[x] = find(parent[x])。这使得后续查询更快,树高度趋近1。
union 时,将较小的树合并到较大的树下。用 rank 数组记录树的高度,避免树退化成链表。与路径压缩结合后,单次操作均摊 O(α(n)),α 是反阿克曼函数,可视为常数。
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
self.count = n # 连通分量数
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x]) # 路径压缩
return self.parent[x]
def union(self, x, y):
px, py = self.find(x), self.find(y)
if px == py:
return False # 已经在同一集合
# 按秩合并
if self.rank[px] < self.rank[py]:
px, py = py, px
self.parent[py] = px
if self.rank[px] == self.rank[py]:
self.rank[px] += 1
self.count -= 1
return True
def connected(self, x, y):
return self.find(x) == self.find(y)
count 记录连通分量数,很多题目直接用。题目描述:用并查集重新解决岛屿数量问题。遍历网格,将相邻的陆地合并,最终统计连通分量数。
'1' 只向右和向下检查,如果邻居也是 '1' 则合并。最后统计连通分量数(parent 等于自己的数量)。
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
if not grid:
return 0
rows, cols = len(grid), len(grid[0])
uf = UnionFind(rows * cols + 1) # +1 给水域用
water = rows * cols # 水域的虚拟节点
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
idx = r * cols + c
# 向右和向下合并
if r + 1 < rows and grid[r+1][c] == '1':
uf.union(idx, (r+1) * cols + c)
if c + 1 < cols and grid[r][c+1] == '1':
uf.union(idx, r * cols + c + 1)
else:
# 水域连接到虚拟节点
uf.union(r * cols + c, water)
return uf.count - 1 # 减去水域虚拟节点
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
self.count = n
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x, y):
px, py = self.find(x), self.find(y)
if px == py: return
if self.rank[px] < self.rank[py]: px, py = py, px
self.parent[py] = px
if self.rank[px] == self.rank[py]: self.rank[px] += 1
self.count -= 1
时间复杂度:O(m×n×α(m×n)) ≈ O(m×n)
空间复杂度:O(m×n)
题目描述:有 n 个城市,给定 n×n 的邻接矩阵 isConnected,如果 isConnected[i][j]=1 表示城市 i 和 j 直接相连。省份是一组直接或间接相连的城市。返回省份数量。
isConnected[i][j]=1 则 union(i, j)。最终统计不同的根节点数量 = 省份数。
class Solution:
def findCircleNum(self, isConnected: List[List[int]]) -> int:
n = len(isConnected)
parent = list(range(n))
rank = [0] * n
def find(x):
if parent[x] != x:
parent[x] = find(parent[x])
return parent[x]
def union(x, y):
px, py = find(x), find(y)
if px == py: return
if rank[px] < rank[py]: px, py = py, px
parent[py] = px
if rank[px] == rank[py]: rank[px] += 1
for i in range(n):
for j in range(i + 1, n):
if isConnected[i][j] == 1:
union(i, j)
return len(set(find(i) for i in range(n)))
时间复杂度:O(n²·α(n)) ≈ O(n²)(遍历矩阵)
空间复杂度:O(n)
题目描述:一棵树有 n 个节点(无环无向图),多加一条边后变成有环图。给定这 n 条边的图,删除一条边使图变回树。返回最后出现的那条冗余边。
union(u, v) 返回 False(u 和 v 已在同一集合),说明这条边会形成环,即为冗余边。class Solution:
def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
n = len(edges)
parent = list(range(n + 1))
rank = [0] * (n + 1)
def find(x):
if parent[x] != x:
parent[x] = find(parent[x])
return parent[x]
def union(x, y):
px, py = find(x), find(y)
if px == py:
return False # 已连通,形成环
if rank[px] < rank[py]: px, py = py, px
parent[py] = px
if rank[px] == rank[py]: rank[px] += 1
return True
for u, v in edges:
if not union(u, v):
return [u, v]
return []
时间复杂度:O(n·α(n)) ≈ O(n)
空间复杂度:O(n)
掌握基础的 find/union 后,并查集还有几个进阶变体:
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.size = [1] * n # 每个集合初始大小为1
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x, y):
px, py = self.find(x), self.find(y)
if px == py: return
if self.size[px] < self.size[py]: px, py = py, px
self.parent[py] = px
self.size[px] += self.size[py] # 更新大小
# 示例: LC 399 除法求值
# 维护节点到根的权重比值
class WeightedUF:
def __init__(self, n):
self.parent = list(range(n))
self.weight = [1.0] * n # 到根的权重
def find(self, x):
if self.parent[x] != x:
orig = self.parent[x]
self.parent[x] = self.find(self.parent[x])
self.weight[x] *= self.weight[orig]
return self.parent[x]
标准并查集的路径压缩是不可撤销的。如果需要回溯(如回溯法中撤销合并),使用按秩合并但不用路径压缩,配合栈记录操作历史。