news 2026/5/12 3:08:35

并查集(DSU)实战:从原理到LeetCode高频题解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
并查集(DSU)实战:从原理到LeetCode高频题解

1. 并查集:解决连通性问题的瑞士军刀

第一次听说并查集(Disjoint Set Union,简称DSU)是在解决LeetCode第547题"朋友圈"的时候。当时用DFS解法总感觉代码写得太啰嗦,直到发现评论区有人用不到20行代码就搞定了问题——那是我与这个神奇数据结构的初次相遇。

简单来说,并查集就是专门处理动态连通性问题的数据结构。想象你有一堆散落的节点,需要频繁回答"节点A和节点B是否相连"这样的问题,这时候普通的遍历方法效率太低,而并查集可以在近乎常数时间内完成查询和合并操作。它的核心操作只有两个:

  • Find:查找某个元素属于哪个集合(就像查快递包裹最后到了哪个配送站)
  • Union:合并两个不相交的集合(像把两个快递网点合并成一个)

实际项目中,我经常用它处理社交网络的好友关系、电路连接检查、游戏中的像素连通区域计算等问题。特别是在处理百万级节点的图数据时,传统DFS/BFS可能直接栈溢出,而经过优化的并查集依然能稳定运行。

2. 并查集的实现与优化技巧

2.1 基础版实现:数组与森林表示

我们先来看最朴素的实现方式。假设有5个节点(0-4),初始时每个节点自成一个集合:

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

这个版本虽然直观,但存在明显缺陷。当进行多次union操作后,树可能退化成链表,使得find操作的时间复杂度恶化到O(n)。我在第一次实现时就踩过这个坑——当处理10万个节点的数据时,程序运行了整整两分钟。

2.2 路径压缩:把树拍扁的技巧

路径压缩就像把家族族谱扁平化处理。当我们查找某个节点的根时,顺带把沿途所有节点的父节点直接指向根节点:

def find(self, x): if self.parent[x] != x: self.parent[x] = self.find(self.parent[x]) # 递归压缩 return self.parent[x]

这个优化让后续查询变得更快。实测在LeetCode 200题"岛屿数量"中,使用路径压缩后运行时间从68ms降到了40ms。不过要注意递归写法可能引发栈溢出,对于特别大的数据集,可以用迭代版本:

def find(self, x): root = x while self.parent[root] != root: root = self.parent[root] while x != root: # 二次遍历进行压缩 next_node = self.parent[x] self.parent[x] = root x = next_node return root

2.3 按秩合并:保持树的平衡

另一个重要优化是按秩合并(Union by Rank)。我们额外维护一个rank数组记录树的深度,总是将较浅的树合并到较深的树下:

class DSU: def __init__(self, n): self.parent = list(range(n)) self.rank = [0] * n 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 else: self.parent[rootX] = rootY if self.rank[rootX] == self.rank[rootY]: self.rank[rootY] += 1

这两个优化配合使用,能使单次操作的时间复杂度降到接近O(1)。我在处理社交网络数据时,对比发现优化后的并查集比普通BFS快了近百倍。

3. LeetCode经典题目实战解析

3.1 朋友圈问题(LeetCode 547)

题目描述:给定n*n的矩阵M表示朋友关系,M[i][j]=1表示第i和第j个学生是朋友(直接或间接)。求有多少个朋友圈。

这是最典型的并查集应用题。我的解题思路是:

  1. 初始化n个独立学生
  2. 遍历矩阵,对每个M[i][j]=1的关系执行union(i,j)
  3. 最后统计有多少个不同的根节点
def findCircleNum(M): n = len(M) dsu = DSU(n) for i in range(n): for j in range(i+1, n): if M[i][j] == 1: dsu.union(i, j) return len({dsu.find(i) for i in range(n)})

有个容易忽略的细节:矩阵是对称的,所以只需要遍历右上三角部分。这个优化让运行时间又减少了30%。

3.2 账户合并(LeetCode 721)

更复杂的应用场景出现在这道题:给定一组账户信息,其中每个账户包含多个邮箱,需要将有相同邮箱的账户合并。

我的建模方法是:

  1. 将每个邮箱映射到一个账户ID
  2. 当发现邮箱已存在时,执行union操作
  3. 最后整理合并结果
def accountsMerge(accounts): dsu = DSU(len(accounts)) email_to_id = {} # 第一遍:建立邮箱到账户ID的映射并合并 for i, account in enumerate(accounts): for email in account[1:]: if email in email_to_id: dsu.union(i, email_to_id[email]) else: email_to_id[email] = i # 第二遍:整理合并结果 id_to_emails = defaultdict(set) for email, id_ in email_to_id.items(): root = dsu.find(id_) id_to_emails[root].add(email) return [[accounts[i][0]] + sorted(emails) for i, emails in id_to_emails.items()]

这个案例展示了如何将现实问题抽象为连通性问题。我最初尝试用哈希表直接记录关系,结果发现处理交叉引用时非常麻烦,转而使用并查集后代码简洁了许多。

4. 高级应用与变种问题

4.1 带权并查集:处理关系型问题

有些问题不仅需要知道是否连通,还需要知道节点间的具体关系。比如LeetCode 399"除法求值"问题,需要处理变量间的比值关系。

解决方案是给边赋予权重:

class WeightedDSU: def __init__(self, n): self.parent = list(range(n)) self.weight = [1.0] * n def find(self, x): if self.parent[x] != x: origin_parent = self.parent[x] self.parent[x] = self.find(self.parent[x]) self.weight[x] *= self.weight[origin_parent] return self.parent[x] def union(self, x, y, value): rootX = self.find(x) rootY = self.find(y) if rootX == rootY: return self.parent[rootX] = rootY self.weight[rootX] = self.weight[y] * value / self.weight[x]

这种带权并查集在解决等式关系、图论中的距离问题时非常有用。我在一次数据一致性检查的任务中就应用了这个技巧,成功找出了存在矛盾的数据记录。

4.2 动态连通性与离线查询

有些问题需要处理动态变化的连通性状态,比如LeetCode 1697"检查边长度限制的路径是否存在"。这类问题通常需要:

  1. 对查询按特定条件排序
  2. 按顺序处理边和查询
def distanceLimitedPathsExist(n, edgeList, queries): edgeList.sort(key=lambda x: x[2]) queries = sorted([(q[0], q[1], q[2], i) for i, q in enumerate(queries)], key=lambda x: x[2]) dsu = DSU(n) res = [False] * len(queries) edge_ptr = 0 for q in queries: while edge_ptr < len(edgeList) and edgeList[edge_ptr][2] < q[2]: dsu.union(edgeList[edge_ptr][0], edgeList[edge_ptr][1]) edge_ptr += 1 res[q[3]] = dsu.find(q[0]) == dsu.find(q[1]) return res

这种离线处理技巧在数据库查询优化、网络路由等场景都有应用。我在处理用户行为日志分析时就借鉴了这个思路,将实时查询转化为批量处理,性能提升了5倍以上。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/12 3:03:34

问答系统:从检索到生成式模型

问答系统&#xff1a;从检索到生成式模型 1. 技术分析 1.1 问答系统类型 问答系统可分为多种类型&#xff1a; 问答系统分类检索式: 从知识库中检索答案抽取式: 从文本中抽取答案片段生成式: 直接生成答案多模态: 结合文本和视觉1.2 问答系统架构对比 类型架构特点代表模型检索…

作者头像 李华
网站建设 2026/5/12 3:00:33

国际空间站工程知识共享:从太空协作到地面工程实践的启示

1. 国际空间站&#xff1a;一个工程师眼中的知识共享金矿作为一名在航天工程领域摸爬滚打了十几年的工程师&#xff0c;我常常被问到一个问题&#xff1a;耗资巨大的国际空间站&#xff08;ISS&#xff09;&#xff0c;除了那些遥不可及的太空探索梦想&#xff0c;到底给我们这…

作者头像 李华
网站建设 2026/5/12 3:00:06

芯片产业的地缘政治博弈:从全球化理想到国家战略现实

1. 项目概述&#xff1a;一场关于芯片“国籍”的深度对话十年前&#xff0c;在EE Times的一篇专栏文章中&#xff0c;资深记者Junko Yoshida与全球半导体联盟&#xff08;GSA&#xff09;亚太区执行董事Jeremy Wang进行了一场发人深省的对话。核心议题直指产业本质&#xff1a;…

作者头像 李华
网站建设 2026/5/12 2:55:44

PGlite Explorer:在VS Code中无缝管理轻量级PostgreSQL数据库

1. 项目概述&#xff1a;在编辑器里直接管理你的PGlite数据库如果你和我一样&#xff0c;日常开发离不开VS Code或者Cursor&#xff0c;同时又经常需要和本地数据库打交道&#xff0c;那你肯定体会过那种频繁切换窗口的割裂感。写一段代码&#xff0c;切到数据库GUI工具里查个数…

作者头像 李华