并查集(Disjoint Set Union,DSU)是一种处理不相交集合合并与查询问题的树型数据结构。我第一次接触这个数据结构是在解决网络连通性问题时,当时需要快速判断数万个节点中任意两个是否属于同一连通分量。传统遍历方法时间复杂度太高,而并查集能在近乎常数时间内完成这类操作。
并查集的核心操作可以概括为:
在实际编码中,我们常用整数数组来表示并查集结构。数组下标对应元素编号,数组值存储该元素的父节点。例如parent[i] = j表示元素i的父节点是j,当parent[i] = i时,说明i就是集合的根节点。
提示:初学者常犯的错误是混淆节点索引和节点值。在标准实现中,我们通常用0到n-1的连续整数表示n个独立元素。
让我们从最基本的并查集实现开始。以下是一个完整的Python模板:
python复制class DSU:
def __init__(self, n):
self.parent = list(range(n)) # 初始化每个元素的父节点为自己
def find(self, x):
while self.parent[x] != x:
x = self.parent[x]
return x
def union(self, x, y):
rootX = self.find(x)
rootY = self.find(y)
if rootX != rootY:
self.parent[rootY] = rootX
def connected(self, x, y):
return self.find(x) == self.find(y)
这个基础版本已经能解决许多问题,比如判断社交网络中两个人是否属于同一个朋友圈。但在处理大规模数据时,它的效率可能不够理想。find操作在最坏情况下需要O(n)时间,因为树可能退化成链表。
路径压缩是优化find操作的经典技术。在执行find时,我们将查找路径上的所有节点直接指向根节点,使树变得更扁平:
python复制def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x]) # 递归路径压缩
return self.parent[x]
这种递归实现简洁但可能引发栈溢出。迭代版本更安全:
python复制def find(self, x):
root = x
while self.parent[root] != root:
root = self.parent[root]
# 路径压缩
while self.parent[x] != root:
next_node = self.parent[x]
self.parent[x] = root
x = next_node
return root
路径压缩后,find操作的平均时间复杂度降为O(α(n)),其中α是反阿克曼函数,对于任何实际应用都可以认为是常数时间。
另一种常见优化是按秩合并(Union by Rank)。我们维护一个rank数组记录每棵树的深度,合并时总是将较浅的树合并到较深的树下:
python复制class DSU:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n # 初始化深度为0
def union(self, x, y):
rootX = self.find(x)
rootY = self.find(y)
if rootX == rootY:
return
# 按秩合并
if self.rank[rootX] > self.rank[rootY]:
self.parent[rootY] = rootX
elif self.rank[rootX] < self.rank[rootY]:
self.parent[rootX] = rootY
else:
self.parent[rootY] = rootX
self.rank[rootX] += 1
这种优化可以防止树变得过深,与路径压缩配合使用时效果最佳。实际应用中,两种优化通常同时使用。
LeetCode 547题"省份数量"是典型的并查集应用。给定n×n矩阵表示城市之间的连接关系,求省份数量(连通分量数):
python复制def findCircleNum(isConnected):
n = len(isConnected)
dsu = DSU(n)
for i in range(n):
for j in range(i+1, n):
if isConnected[i][j] == 1:
dsu.union(i, j)
# 统计根节点数量
return len(set(dsu.find(i) for i in range(n)))
这个解法时间复杂度为O(n²α(n)),空间复杂度O(n)。相比DFS/BFS解法,并查集在处理动态连接问题时更具优势。
有些问题需要处理带权关系,比如"食物链"问题(POJ 1182)。这时我们可以扩展标准并查集,维护节点到根节点的相对关系:
python复制class WeightedDSU:
def __init__(self, n):
self.parent = list(range(n))
self.weight = [0] * n # 记录到父节点的权重
def find(self, x):
if self.parent[x] != x:
orig_parent = self.parent[x]
self.parent[x] = self.find(self.parent[x])
self.weight[x] += self.weight[orig_parent]
return self.parent[x]
def union(self, x, y, w):
rootX = self.find(x)
rootY = self.find(y)
if rootX == rootY:
return
# 合并时维护权重关系
self.parent[rootY] = rootX
self.weight[rootY] = self.weight[x] + w - self.weight[y]
这种带权并查集可以处理更复杂的等价关系问题,如判断两个变量的相对关系是否矛盾。
新手常犯的错误是错误初始化parent数组。正确的初始化应该是:
python复制self.parent = list(range(n)) # 每个元素初始父节点是自己
而不是:
python复制self.parent = [0] * n # 错误!所有节点初始指向0
后者会导致所有find操作最终都返回0,除非0的父节点被修改过。
理论上,路径压缩会改变树的深度,使得按秩合并中的rank不再准确反映实际深度。但在实践中,我们仍然使用rank作为合并时的参考,因为:
当处理元素数量极大(如1e6以上)时,可以考虑以下优化:
python复制class SparseDSU:
def __init__(self):
self.parent = {}
self.rank = {}
def find(self, x):
if x not in self.parent:
self.parent[x] = x
self.rank[x] = 0
elif self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x, y):
# 类似标准实现...
调试并查集问题时,可以:
注意:在竞赛编程中,通常不需要删除并查集中的元素。如果需要支持删除操作,可以考虑使用"惰性删除"或更复杂的数据结构如"持久化并查集"。
标准实现需要预先知道元素数量。对于动态增删的场景:
python复制class DynamicDSU:
def __init__(self):
self.parent = {}
self.rank = {}
def add(self, x):
if x not in self.parent:
self.parent[x] = x
self.rank[x] = 0
def find(self, x):
self.add(x) # 自动添加新元素
# 其余与标准实现相同
某些场景需要支持回滚操作。实现方式是用栈记录所有修改:
python复制class ReversibleDSU:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
self.history = []
def find(self, x):
while self.parent[x] != x:
x = self.parent[x]
return x
def union(self, x, y):
x_root = self.find(x)
y_root = self.find(y)
if x_root == y_root:
self.history.append(None)
return False
# 记录合并前的状态
if self.rank[x_root] < self.rank[y_root]:
self.history.append(('parent', x_root, self.parent[x_root]))
self.parent[x_root] = y_root
else:
self.history.append(('parent', y_root, self.parent[y_root]))
self.parent[y_root] = x_root
if self.rank[x_root] == self.rank[y_root]:
self.history.append(('rank', x_root, self.rank[x_root]))
self.rank[x_root] += 1
return True
def undo(self):
if not self.history:
return False
while True:
record = self.history.pop()
if record is None:
break
typ, x, val = record
if typ == 'parent':
self.parent[x] = val
else:
self.rank[x] = val
if not self.history or self.history[-1] is None:
break
return True
这种实现可以支持回滚到最后一次union操作前的状态,适用于需要试探性连接的场景。
在大规模分布式环境中,可以考虑将并查集分区处理。基本思路:
虽然并行化会引入额外开销,但对于超大规模数据(如社交网络分析)是必要的。