☰
并查集实战:LeetCode 1319连通网络操作次数全解析
2026/10/15 11:44:32 网站建设 项目流程

1. 从一个真实场景说起:为什么并查集能救你

做算法题最怕什么?不是不会写代码,而是看完题解“懂了”,换个题又“废了”。这道 LeetCode 1319「连通网络的操作次数」就是典型例子,它看起来像个图论题,实际上用并查集(Union-Find)能非常清爽地解决。

题目本身很直白:有n台电脑,编号从 0 到n-1,给你一个连接列表connections,其中connections[i] = [a, b]表示电脑a和b之间的网线。如果网络中有多余的网线,你可以把其中一根拔下来,接到别的电脑上,请问最少操作几次能让所有电脑都连通?如果无论如何都连不通,返回 -1。

我第一次见到这题时,第一反应是“这不就求连通分量个数吗?”确实,核心思路是这样的:如果连通分量有k个,要让整个网络变成一张连通图,至少需要k-1根线把它们连起来;而你需要确认现有网线中有没有这么多“富余”的线可以拿来用。

这里就藏着并查集最擅长的两件事:统计连通分量数量,以及检测一条边是不是多余的。很多新手把并查集只当作“判断两个节点是否连通”的工具,实际上它更值钱的能力,是在动态加边的过程中实时维护连通分量的个数,以及识别出哪些边是“冗余边”。LeetCode 1319 恰好把这两个能力同时考了一遍。

下面我带你从算法选型开始,一步步把这道题从思路到代码彻底拆开,最后附上几个我实际刷题踩过的坑,帮你少走弯路。

2. 算法选型:为什么不用 DFS,而选并查集

2.1 暴力 DFS/BFS 的局限

拿到这个题,很多人第一个念头是:建图,然后 DFS/BFS 数连通分量呗。这个思路没错,但有个隐藏问题——你很难同时高效统计“冗余边”。

如果走 DFS 的路线,标准的做法是:

  1. 用邻接表建图;
  2. 遍历所有节点,每遇到未访问的节点就发起一次 DFS/BFS,连通分量计数加 1;
  3. 用m - (n - k)计算冗余边数量。

这里m是边的总数,n是节点数,k是连通分量数。公式的含义是:一个n个节点的连通图最少需要n-1条边,那么对于k个连通分量,总的最小所需边数是n-k,多余的边就是m - (n-k)。这个公式没问题,但你需要额外维护访问数组,还要显式建邻接表,代码量不小,而且处理边的时候容易写乱。

2.2 并查集的天然优势

并查集的好处在于,它可以在线处理边的关系,不需要真正“建图”。你只需要维护一个parent数组,每来一条边,就看看两个端点是否已经连通:

  • 如果不连通,就把它们合并,同时连通分量数量减 1。
  • 如果已经连通,说明这条边是冗余的,冗余计数加 1。

这样一遍线性扫描下来,连通分量数k和冗余边数extra全都拿到了,时间复杂度和 DFS 一样是 O(n + m·α(n)),但代码量小、逻辑清晰、不容易出错。而且并查集本身就是面试高频考点,你把这个套路练熟了,遇到“判断图中是否存在环”的裸题、Kruskal 最小生成树的判环过程,都能直接迁移。

我在实际面试中被问到过类似问题,面试官并不满足于“你写出了 AC 代码”,而是会追问“为什么要用并查集”“如果数据量变大还能怎么优化”,甚至有追问“能不能用并查集不用图结构完成最小生成树中的判环”。这才是这个题目真正的考点。

2.3 时间复杂度对比

这里给出一组简单对比:

方法建图复杂度连通分量统计冗余边统计总复杂度
DFS/BFS + 邻接表O(n + m)需要访问数组需要通过公式间接计算O(n + m)
并查集无需建图直接在合并过程中维护直接在合并过程中检测O(n·α(m))

α是反阿克曼函数,实践中可以看作常数。因此并查集在实际运行中几乎就是线性的,而且省去了邻接表的内存开销。这道题的n最大可达 10 万级别,用邻接表其实也扛得住,但面试官更期待的是你能想到并查集这种更“聪明”的做法。

另外一个容易忽略的点:这题不必先统计冗余再后处理,而是可以在合并的同时判断问题是否有解。只要冗余边数extra >= k-1,就说明能通过k-1次操作把所有连通块连起来;否则直接返回 -1。这个判断逻辑如果放在最后做,逻辑会更清晰,也更好向别人解释你的思路。

3. 核心思路拆解:连通分量数与冗余边的关系

3.1 最小边数公式

先说数学基础。一个包含n个节点的无向连通图,最少需要n-1条边。这很容易理解:想象一根链条,n个节点要首尾相连,至少需要n-1根线。

现在网络中有k个互不相连的连通分量。每个连通分量内部已经是连通的,它们各自的节点数假设是s1, s2, ..., sk,那么每个分量分别需要s1-1, s2-1, ..., sk-1条边就足够支撑内部连通。整体最少需要的边数就是:

(s1-1) + (s2-1) + ... + (sk-1) = n - k

因此,只要当前的边总数m >= n-k,就说明至少有(m - (n-k))条边是冗余的。这里的“冗余”指的是:即使删掉这条边,也不会影响原本的连通性。

举个实际例子,假设有 4 台电脑,connections = [[0, 1], [0, 2], [1, 2]],边的总数是 3,节点数是 4。因为现有图里 0、1、2 已经连成一个连通块,3 是单独一块,所以k=2,最少需要 4-2=2 条边。现在有 3 条边,冗余就是 3-2=1。而这多余的一条到底在哪?你观察[1, 2]这条边,即使你把它去掉,0、1、2 还是通过[0,1]和[0,2]连着,所以[1,2]就是那根可以拔下来的“富余线”。

这个例子也是并查集检测冗余边的核心思想:当两个节点在加入这条边之前已经连通,那这条边就一定是冗余的。

3.2 操作次数的本质

回到问题本身:我们要把所有电脑连成一个整体。现在有k个连通分量,要把它们合并成一个,最少需要k-1根线。每根线从哪里来?只能从冗余边里拔。

所以答案就是:

如果 extra >= k - 1,最终答案为 k - 1 否则为 -1

这里有一个需要特别注意的边界情况:如果这台网络本来就只有 0 台或 1 台电脑,那么k可能为 0 或 1,答案自然是 0,不需要任何操作。代码里要处理好n <= 1的情况,否则并查集初始化时可能越界或者出现多余计数。

3.3 为什么返回 -1 的情况存在

题目给了明确约束:如果无法使所有电脑连通,就返回 -1。什么时候无法连通?直观地说,就是线的总数不够。

怎么理解“不够”?假设n=5,所有电脑都互相没有连接,即connections为空。此时每个电脑各自是一个连通分量,k=5,最少需要的边数是 4,而现在一条边都没有,extra=0-4=-4,显然不够,返回 -1。代码层面,extra是负数也没关系,用extra < k-1判断即可。

另一种“不够”的情况稍微隐蔽一点:有连接,但连接全集中在同一组电脑内部,导致另外一些电脑完全孤立。比如n=5,connections=[[0,1],[1,2],[2,0]],这是一个三角形,0、1、2 连通,3 和 4 孤立,那么k=3(三角形一个分量、节点3一个分量、节点4一个分量),最少需要 5-3=2 条线,而现在边数是 3,冗余边只有 1 条,不够补两条线,返回 -1。

这些情况在代码里不需要特判,因为统一公式会自然处理,但理解其背后的含义对于面试时清晰表达思路至关重要。

4. Python 实现与关键代码逐段精讲

4.1 标准并查集模板

先上一份我平时最常用的并查集模板,代码简洁,路径压缩和按秩合并都带上:

class UnionFind: def __init__(self, n): self.parent = list(range(n)) self.rank = [1] * n self.count = n # 连通分量数 def find(self, x): # 路径压缩(迭代版) root = x while root != self.parent[root]: root = self.parent[root] # 第二遍循环把路径上的节点直接指向根 while x != root: parent_x = self.parent[x] self.parent[x] = root x = parent_x return root def union(self, a, b): ra = self.find(a) rb = self.find(b) if ra == rb: return False # 已经连通,这条边是冗余边 # 按秩合并:让小树挂到大树上 if self.rank[ra] < self.rank[rb]: ra, rb = rb, ra self.parent[rb] = ra self.rank[ra] += self.rank[rb] self.count -= 1 return True

这里有几个细节值得展开讲:

  • find 的两遍循环:很多人习惯写递归版def find(self, x): if self.parent[x] != x: self.parent[x] = self.find(self.parent[x]) return self.parent[x],代码更短。但 Python 递归有深度限制,如果节点数是 10 万,最坏情况下递归深度可能达到 10 万层,直接触发RecursionError。虽然路径压缩会让深度迅速减小,但在最坏链状结构中,第一层find调用的递归深度确实可能很大。所以我更建议在刷题时用迭代版,避免不必要的翻车。
  • 按秩合并:这里我用的是rank数组存子树大小,而不是高度。两个都能用,但存大小有一个额外好处:union之后可以直接更新根节点的rank,方便后续想要查询连通分量大小的场景。对于本题没有这个需求,但模板通用性更好。
  • union 返回值:如果返回False,说明两个节点本来就在同一个连通分量里,这条边就是冗余的。这个返回值在 LeetCode 1319 中可以直接用来统计extra,非常自然。

有基础的读者可能看过另一种简化版模板:只做路径压缩,不管按秩合并。这在大多数题里也能过,但如果你在面试中被问到“如果数据是精心构造的最坏情况怎么办”,按秩合并就是一个很好的加分点。

4.2 主函数解析

用上面的模板,主函数代码非常短:

def makeConnected(n: int, connections: list[list[int]]) -> int: m = len(connections) # 边数太少,提前剪枝 if m < n - 1: return -1 uf = UnionFind(n) extra = 0 for a, b in connections: if not uf.union(a, b): extra += 1 # uf.count 是当前连通分量个数 need = uf.count - 1 if extra >= need: return need return -1

可以看到,思路完全按照前面拆解的逻辑落地:

  1. 先做一次快速判断:如果总边数m < n-1,无论如何都不够连成单个连通图,直接返回 -1。这一步不是必需的,因为后面的extra < need判断也能捕捉到,但提前判断可以省去构建整个并查集的时间,在m很小而n很大的时候非常有用。

  2. 初始化并查集,然后遍历每条边,如果两个端点已经在同一连通块中,extra加 1。这里要注意,union方法内部先find再比较,不会重复计算。

  3. 最后比较extra和uf.count - 1。uf.count是扫描完所有边之后的连通分量数。如果冗余边数量足够,返回need,否则返回 -1。

4.3 边界情况与样例演练

我们用题目自带的几个样例走一遍。

样例 1:n = 4, connections = [[0,1],[0,2],[1,2]]

  • m = 3,m < n-1不成立。
  • 初始化parent = [0,1,2,3],count = 4。
  • [0,1]:find(0)=0, find(1)=1,不同根,合并,count=3。
  • [0,2]:find(0)=0, find(2)=2,不同根,合并,count=2。
  • [1,2]:find(1)=0, find(2)=0,同根,extra=1。
  • 最终count=2,need=1,extra=1,返回1。

操作就是拔掉[1,2],接到[0,3]或[1,3]、[2,3]都可以,一次搞定。

样例 2:n = 6, connections = [[0,1],[0,2],[0,3],[1,2],[1,3]]

  • m = 5,m < n-1比较:5 < 5不成立。
  • 扫描过程:[0,1]合并,count=5;[0,2]合并,count=4;[0,3]合并,count=3;[1,2]同根,extra=1;[1,3]同根,extra=2。
  • 最终count=3,need=2,extra=2,返回2。

这里再多说一句:need=2的含义是 3 个连通分量之间需要再建 2 条线,而当前恰好有 2 条冗余线可以拔,所以答案是 2。

样例 3:n = 6, connections = [[0,1],[0,2],[0,3],[1,2]]

  • m = 4,m < n-1 = 5成立,直接返回 -1。

确实,4 条线对于 6 台电脑来说,即使全部利用上,也最多只能连起 5 台成一棵树,还有 1 台必然孤立,所以不可能连通。

4.4 空间与时间优化细节

这道题的数据范围是n <= 10^5,connections.length <= 2 * 10^4(实际题目约束可能略有不同,但量级类似),所以内存方面不用担心。不过有几个优化习惯我还是想提一下:

  1. 提前剪枝:m < n-1直接返回 -1 是一个很香的细节。它能避免在明显无解的情况下白做一遍并查集。比如n=100000,但connections为空,这个判断瞬间返回,不需要初始化 10 万个元素的数组和做两次find。

  2. 局部变量化:在 Python 中,循环里反复调用self.find和self.union会有属性查找的开销。如果追求极限性能,可以把find函数提取成局部变量,或者直接在主函数内嵌路径压缩函数。不过说实话,力扣上 10 万级别的数据,模板写法完全能过,不必过度优化。

  3. 秩的更新:注意rank[ra] += rank[rb]是在把rb的父节点设为ra之后更新。如果交换了ra, rb,代码要保证更新的是新根的rank。这是我写代码时曾经忽略的一点,一不留神就把两个rank加反了,导致后续find性能退化。

5. 实操过程:从读题到提交的完整心法

5.1 拿到题目后的前三分钟

很多同学刷题有个误区:拿到题立刻开始写代码。我建议前三分钟先做三件事:

  1. 读取输入规模:这决定了你的算法必须是什么复杂度。比如n是 10^5,那么 O(n^2) 就别想了,必须 O(n·log n) 或更优。同时,输入规模也暗示你该用什么数据结构:10^5 级别可以放心建数组和邻接表,但 10^6 级别就得考虑内存紧凑的方案。

  2. 推导最简情况:把问题缩小到最小规模,比如n=2, connections=[],手动推一遍预期结果。再想n=3, connections=[[0,1]],预期结果是什么?在这种极简情形下验证你的思路是否成立。这能帮你提前发现自己对题意的理解是否有偏差。

  3. 思考是否有更简单的问题模型:LeetCode 1319 本质上就是一个“数连通分量 + 数冗余边”的题。如果之前熟悉“973. K Closest Points”或“冗余连接”这类题,你会发现它们的核心都是并查集。识别出问题的底层模型,比记住这道题的答案重要得多。

5.2 编码中的常见坑

这里我把实际遇到的坑列一下,每一个都是我或身边朋友真踩过的。

坑 1:忘记处理n=0或n=1

如果n=1,那么connections里任何边都是自环或不存在。初始化UnionFind(1)没问题,但如果你写死parent = list(range(n-1)),就会得到[-1]这样的错误结构。更合理的写法是list(range(n))并允许n=0或1时直接返回 0。

坑 2:把 input 里的边当成有向边处理

LeetCode 1319 的边是无向边,[a, b]和[b, a]完全等价。如果你脑子里想的是“从 a 到 b 有一条单向线”,那你统计冗余边就会出错。并查集的union(a, b)天然无视方向,这是它的优势,但前提是你不要自己在前面加什么方向判断。

坑 3:边数剪枝写反

if m < n-1: return -1是对的。有人写成if m <= n-1,这就会把m == n-1的情况也判成无解,但事实上m == n-1时如果这些边恰好是一棵树,答案就是 0。比如n=3, connections=[[0,1],[1,2]],这已经是一棵连通的树,你需要 0 次操作,但m <= n-1会错误地返回 -1。

坑 4:count更新时机不对

UnionFind.__init__里设置了self.count = n,每当合并成功就self.count -= 1。这个逻辑必须在合并成功的分支里做,不能在函数末尾无条件减 1。否则冗余边也会导致count错误减少,最后算出来的need就会偏小,可能给出错误答案。

坑 5:find 没有路径压缩,导致超时

极端情况下,如果每次union都只是简单的链式挂接,find的复杂度会退化成 O(n),整体变成 O(n^2)。数据一大就超时。路径压缩配上按秩合并,find的平均复杂度才接近 O(1)。

5.3 样例调试的思考方式

写完代码不要急着提交,先在本地跑一遍题目自带的样例。关键是,如果你发现某个样例没过,不要只看输出对不对,要打印中间状态。我会在调试时临时加一些print,比如:

for a, b in connections: flag = uf.union(a, b) if not flag: extra += 1 print(f"冗余边: ({a}, {b}), extra={extra}") print(f"count = {uf.count}") print(f"extra = {extra}")

这种打印能很快暴露问题:是union判断错了,还是连通分量数算错了,或者剪枝条件写反了。调试完再把这些print删掉。虽然现在 IDE 支持断点调试,但刷题时快速 print 往往更高效。

6. 变体与扩展:这道题还能怎么考

6.1 变体一:输出具体的操作方案

LeetCode 1319 只问你最少操作次数,但如果面试官追问“具体拔哪根线,接在哪台电脑上”,你怎么回答?

实际上,这个问题可以转化为:

  • 收集所有冗余边的集合(那些导致union返回 False 的边)。
  • 收集所有连通分量的根(并查集里每个根i == parent[i])。
  • 从冗余边中依次取边,把它连接到相邻两个连通分量的根之间。

这个扩展考察的是你对并查集内部结构的理解程度。如果能当场写出这个方案,基本可以证明你不是背板子,而是真的懂。

6.2 变体二:最少线缆数

如果把题目改成“给你一些已有连接,求最少还需要额外购买多少根线才能连通所有电脑”,那就是need = k - 1,跟冗余边无关。因为你可以直接买新线,不需要从现有线里拔。这个变体更简单,但常被拿来当作铺垫题。

6.3 变体三:判断是否形成环

“冗余连接”这道题就是典型的变体:给定一棵树额外加一条边构成的图,要求找出那条多余的边。用并查集扫描所有边,第一个触发union返回 False 的边,就是答案。这和 1319 的冗余边检测逻辑完全一致,只是输出不同。

6.4 变体四:带权并查集

有些题目在连通关系之外还要求维护“点到根的距离”或者“两个节点之间的差值”,这时候就要在find和union里多维护一个weight数组。LeetCode 399 除法求值就是典型代表。虽然不能直接套在这道题上,但你先熟练无权的版本,再迁移到带权版本会容易很多。

这些变体出现的频率并不低,尤其是面试中。面试官问你“你还能想到别的解法吗”“如果改成输出路径呢”,本质上就是看你有没有建立知识迁移能力。

7. 从 LeetCode 1319 到并查集的完整知识地图

7.1 并查集在面试中的定位

从我刷题和准备面试的经验来看,并查集属于“必须熟练掌握”的数据结构。它本身的模板代码量不大,但考察点非常多:

  • 能否写出正确的find(路径压缩)
  • 能否写出正确的union(按秩合并)
  • 能否用并查集解决动态连通性问题
  • 能否把实际问题抽象成连通性问题

LeetCode 上涉及并查集的经典题至少有这些:

题目核心考点
1319 连通网络的操作次数连通分量数 + 冗余边
冗余连接判环并找出多余边
账户合并字符串节点下的连通性
岛屿数量(并查集版)二维网格上的连通分量
最长连续序列哈希 + 并查集优化
除法求值带权并查集

把这些题刷完,并查集基本就融会贯通了。你会发现它们有一个共同的模式:把输入里的“关系”变成并查集的“边”,然后问你有多少个连通块,或者某两个元素是否相连。

7.2 与图论其他知识的关联

这道题本质上是在考无向图的连通性。如果你熟悉课程里的“连通分量”“生成树”这些概念,会发现 1319 其实就是“生成树上的补边”问题。一个图的生成树需要n-1条边,多余的边就是“环边”,而并查集就是高效识别这些环边的工具。

另一个关联点是Kruskal 最小生成树算法。Kruskal 按边权排序后从小到大尝试加入,每次加入之前都要用并查集判断是否会在当前森林中形成环,如果会就跳过。这和本题判断冗余边的代码几乎一模一样。所以你不妨一石二鸟:刷完 1319,顺手把 Kruskal 的模板也想一遍。

7.3 为什么推荐掌握迭代式 find 模板

不少教科书上给的是递归 find,因为它简洁美观。但我个人刷题和实际工程中更推荐迭代式,原因有三:

  1. 避免 Python 递归深度限制:Python 默认递归深度约 1000,如果树比较高,递归版很容易RecursionError。
  2. 减少函数调用开销:在 10 万次 find 的循环里,函数调用开销积少成多。迭代版虽然代码多几行,但运行效率略好。
  3. 面试时更不容易出 bug:递归版需要在self.parent[x] = self.find(self.parent[x])这一行小心赋值顺序,写错会导致死循环。迭代版按两层循环写,逻辑更线性,不易出错。

当然,如果你对递归版有信心,用递归也没问题,最终 AC 才是目的。但如果你想一套模板走天下,我强烈建议把迭代版背熟。

8. 花式解法对比:并查集之外的思路

8.1 使用 DFS/BFS 的完整写法参考

为了对比,我给出 DFS 版本的参考代码,你可以直观感受一下两种解法的差异:

def makeConnected_dfs(n, connections): m = len(connections) if m < n - 1: return -1 graph = [[] for _ in range(n)] for a, b in connections: graph[a].append(b) graph[b].append(a) visited = [False] * n components = 0 for i in range(n): if not visited[i]: components += 1 stack = [i] visited[i] = True while stack: node = stack.pop() for nxt in graph[node]: if not visited[nxt]: visited[nxt] = True stack.append(nxt) extra = m - (n - components) return components - 1 if extra >= components - 1 else -1

这个写法也可以 AC,但相比并查集版,多了邻接表构建和visited数组,代码量明显更大。更重要的是,并查集版天然地同时统计了冗余边,而 DFS 版必须在最后通过公式计算extra。这里的公式extra = m - (n - components)和并查集版的extra是等价的,但你需要额外记住这个公式,否则容易算出负数还不自知。

8.2 并查集解法好在哪

我用一个生活化类比来解释:假设每个连通分量是一个“村子”,你现在要修路把所有村子连通。

  • DFS/BFS 是先把所有线路画在地图上,数出有几个村子,再看画的线里有哪些是多余的。
  • 并查集是边看边数:每来一条线路,先看这条路是不是连接两个已经能走通的村子,如果是,就把这条路标成“可拆”。

两者最终结果一样,但并查集更贴近“操作”这个过程本身。这也是为什么题目名字叫“连通网络的操作次数”——你最后要返回的need = k-1正是“操作次数”,而能不能操作取决于有多少可拆的线。

8.3 什么时候该用 DFS/BFS

虽然本题推荐并查集,但并非所有连通性问题都适合并查集。如果题目要求你输出具体的连通分量路径,或者求连通分量的内部详细结构,那么 DFS/BFS 更方便。比如“岛屿数量”这道题,DFS 思路非常直观,直接标陆地、沉岛,代码也简洁,不一定非要迁移并查集模板。

所以我的建议是:并查集和 DFS/BFS 都要会。面试官有时还会故意追问“你不用并查集还能怎么实现”,这时候如果你能流畅写出 DFS 版本,并说明两者时间复杂度的差异,就能加分。

9. 实战中的疑难杂症排查手册

下面是我把刷题过程中遇到过的坑整理成的一个速查表,你可以直接收藏备用。

现象可能原因解决方案
答案比预期多 1count初始化成n+1,或对n=1的情况多算了一个连通块初始化count=n;单独处理n<=1
答案为 -1 但预期不是剪枝条件写成m <= n-1改为m < n-1
参与合并次数不对忘记在union成功时count -= 1,或误在冗余边时也减了只在合并成功分支中减
find死循环递归版中self.parent[x] = self.find(self.parent[x])写错顺序改用迭代版
大 n 时超时没有路径压缩,或没有按秩合并两个都加上
输出操作次数需要具体方案不知道冗余边集合在union返回 False 时收集边

10. 从这道题到举一反三的训练方法

10.1 刷题训练三板斧

一道题刷完,别急着做下一道。我会花 10 分钟做三件收尾工作:

  1. 写第二种解法:DFS 版、BFS 版、并查集版,至少写两种。这能帮你确认自己是真的理解了题目,而不是背答案。
  2. 总结一句话核心:把这道题压缩成一句话,比如“数连通分量,统计冗余边,比较够不够”。下次见到类似题,先迁移这句话。
  3. 标注关联题目:在题号旁写上“类似 Kruskal”“并查集判环”这类标签。以后复习时扫一眼标签,就能快速唤醒记忆。

10.2 并查集的常见面试追问

如果有人面试时问你这道题,我预计至少有如下追问:

  • 为什么用并查集而不是 DFS?
  • 并查集的find时间复杂度为什么近似 O(1)?
  • 如果边数特别大,内存怎么办?(其实不需要额外优化,并查集内存就是 O(n))
  • 如果n大到 10^8,你还能用这道题的解法吗?(不能,需要换思路,但面试一般不会到这种极端规模)

每个追问背后都是对原理的考察。你只有亲手写过程序、跑过数据、甚至打印过中间状态,才谈得上有底气回答。

10.3 我的个人体会

这道题我第一次做用的 DFS,虽然也 AC 了,但总觉得代码绕。后来系统学了一遍并查集,再回头看这道题,发现它的美就在于“规模小、模型清晰、代码模板固定”。现在我刷并查集相关题目时,基本不需要怎么思考,直接套模板,很快能跑通。

另外想多说一句:LeetCode 1319 的难度是中等,但它其实是连接“基础图论”和“高级并查集应用”的一座桥。如果你能把这道题讲给一个从来没听过并查集的朋友听,让他听懂,那你对这道题的理解就到位了。这个方法比在讨论区里看别人解释有效得多。

10.4 后续扩展思路

有人可能觉得,做完这道题、记下模板就够了,但我会建议你再做两个小实验:

一是把n改成 200000,把connections构造成一条链,然后跑一下两种解法的时间对比。你会发现并查集版明显更快,这能让你对“路径压缩 + 按秩合并”的实际效果有一个直观感知。

二是把代码改成返回extra和need而不是最终答案,把它包装成一个工具函数。以后做类似“最少操作次数”“最少添加边数”的题目时,可以直接复用。我自己就会维护一个小工具库,里面放着并查集、树状数组、单调栈等常用模板,刷题前翻一翻,效率很高。

写到这里,这道 LeetCode 1319 的核心内容已经全部讲完了。最后再给你一个我实际干活时的小习惯:每道题 AC 之后,我并不会立刻关掉页面,而是会在题解区看看高赞解法,找一找有没有我没见过的优化点。比如这道题有人用“带延迟的路径压缩”,有人把rank换成“父节点编号比较”,这些思路未必更优,但能拓宽你的视野。保持这种好奇心,刷题才不会变成纯粹的体力劳动。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询