恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

Dijkstra单源最短路径:贪心法三步证明与堆优化实践

  • 首页
  • 资讯中心
  • /
  • Dijkstra单源最短路径:贪心法三步证明与堆优化实践

相关资讯

多模融合时序数据库实战:从数据孤岛到统一SQL引擎的国产化之路 2026/10/16 4:07:27
CycleGAN斑马变马:非配对图像翻译的结构主体代码分析 2026/10/16 4:07:27
MySQL索引失效排查与治理:从EXPLAIN到慢查询优化实战 2026/10/16 4:07:27

最新资讯

pegasusWMS:用Python与Jupyter拆解仓库管理系统核心流程
ICRA 2020三维重建论文盘点:从实时融合到动态场景的工程实践
两节串联电池主动均衡方案:基于MP2642GR-Z与MK24FN1M0VDC12的备电系统设计
基于MP2642GR-Z与PIC18F8722的2S电池均衡系统设计
AI Agent核心模块拆解:从感知到反思的架构设计与工程实践
AI Agent构建全栈应用实战:任务拆解、配置与验收指南

今日推荐

Java八种基本类型深度解析:从内存布局到实战选型
基础:常用快捷键——IDEA、VS Code、PyCharm 与 Linux 终端效率速查
解决ultraedit下verilog .v文件中文注释乱码:把编码配置改到TaoToken辅助排查

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

Dijkstra单源最短路径:贪心法三步证明与堆优化实践

发布时间:2026/10/16 4:12:27
Dijkstra单源最短路径:贪心法三步证明与堆优化实践 先问一个几乎每个学图论的人都会愣一下的问题Dijkstra 每次从优先队列里弹出 dist 最小的节点然后把它“钉死”之后再也不会更新它——凭什么敢这么笃定别的节点之后会不会通过某条弯弯绕绕的路径把这个节点刷新得更小标题里写的“Dijkstra单源最短路径可用贪心法解决的三步证明”正好戳中了这个困扰很多人的点。网上讲 Dijkstra 流程的教程多如牛毛贴代码的也不少但真正把“为什么贪心是对的”讲透的内容反而稀缺。这篇我想认真写一写先拆贪心法的底层逻辑再完整给出三步证明的推导链条然后上一版可以直接跑通的代码最后把工程实践里容易踩的坑全部列出来。这篇东西适合正在复习图论的开发者、马上要面算法岗笔试面试的人也适合那些已经会写 Dijkstra、但总觉得证明隔着一层纱的读者。你不需要有很深的前缀知识只要知道什么是图、什么是权值边跟着我的证明链条走一遍就能把 Dijkstra 从“背模板”变成“真理解”。1. 先说清楚为什么 Dijkstra 属于贪心法1.1 贪心的本质不是“每次都选近”而是“选完就不回头”很多人一说贪心法第一反应是“每一步都选局部最优解”。这句话对但不够本质。贪心算法最核心的特征其实是“不回溯”每一个局部决策一旦做出就永远不会撤销最终答案由这些不可逆的决策堆叠出来。Dijkstra 就是这么干的。它的循环体非常朴素每次都从“还没确定最短距离”的节点堆里取出 dist 最小的那个把它标记为确定。这个动作从算法一开始到结束都一模一样没有任何一步会回头去修改一个已经被确定的节点。你把这个过程拿到生活里类比就像开车去多个目的地每到一个城市就把它从“待去列表”里划掉而且坚信刚才走过的路已经是最优路线不会因为以后发现了一条新路而回头改道。这个“不回头”属性才是贪心法与动态规划的根本区别。动态规划会维护一堆状态反复迭代、反复更新贪心法是“一条道走到黑”。Dijkstra 显然属于后者虽然它也有 dist 数组在不断变小但“确定一个节点”这个动作本身是不可逆的算法不会因为后面发生什么就把某个已经确定的节点重新拉回未确定状态。那问题就来了不回头凭什么是安全的这就是整篇博文要解决的核心问题。1.2 贪心证明的标准套路贪心选择性质 最优子结构凡是见过贪心算法证明的人都会碰到两个词“贪心选择性质”和“最优子结构”。贪心选择性质每一步贪心选择至少能保留一个全局最优解。换句话说我这个“局部最优”的决定不会把“全局最优”的路堵死。最优子结构一个全局最优解它内部任意一段子解也必须是那个子问题的最优解。Dijkstra 的证明本质上就是在验证这两个性质。不过它和活动选择、背包这类经典贪心问题相比有一个明显的不同Dijkstra 的贪心选择性质非常难一眼看穿。活动选择里你选最早结束的活动理由很直白Dijkstra 里你选 dist 最小的节点理由却藏在“反证法”和“上界引理”里。这也是为什么标题里强调“三步证明”——因为它不是那种扫一眼就能信服的结论必须一步一步推理。后面我会把三步拆开讲每一步都对应一个可以独立验证的引理。1.3 一个容易误解的点算法简洁不等于证明简单我见过不少同学代码写得飞快但被问一句“为什么 Dijkstra 不能处理负权边”就卡住了。这说明他脑子里只有流程没有证明。实际上Dijkstra 的简洁性和它证明的微妙性是两码事。代码可以压缩到二十几行但证明里要用到“假设第一次出错”“考察最短路径上第一个跨出集合的点”“构造一个矛盾的不等号链条”这种反证技巧。你回头去看 Dijkstra 本人提出这个算法的年代背景会发现它的证明难度在当时属于“需要一个聪明的洞察”的级别这就是文本标题里“三步证明”的含金量所在。所以在展开代码之前我必须花足够篇幅把证明讲透。只有当你真正理解“为什么最小的那个节点不可能被后面的路径刷新”你才会自动明白负权边为什么会摧毁整个算法。2. 三步证明把贪心从“看起来对”变成“确实对”为了方便讨论先做几个基本定义有向带权图G (V, E)边权w(u, v) 0源点为sdist[v]表示算法当前认为的“从 s 到 v 的最短距离”δ(s, v)表示真实的、最终的最短距离即标准答案S表示已经被算法“确定/弹出”的节点集合接下来把证明拆成三步。2.1 第一步dist 永远是真实最短距离的“上界”先证明一个非常基础的引理在算法运行的任意时刻对任意节点v都有dist[v] δ(s, v)这句话的意思是算法算出来的临时距离永远不会比真实最短距离更小。它可能偏大但绝不会偏小。这个引理是整套证明的安全底座。为什么成立看初始化dist[s] 0显然0 δ(s, s)其他都是无穷大当然也大于等于真实值。再看算法的唯一更新操作——松弛当从节点u松弛到v时执行dist[v] min(dist[v], dist[u] w(u, v))。如果dist[u]本身已经是某条从 s 到 u 的真实路径长度这个需要一点点归纳感觉初看可以先接受那么dist[u] w(u, v)就是某条从 s 经过 u 到 v 的真实路径的长度。任何一条真实路径的长度都不可能小于这些路径中的最短值δ(s, v)。所以松弛后的dist[v]依然不会低于δ(s, v)。关键点这个引理不依赖边的非负权就算有负权边dist[v]依然是一条真实路径长度的候选值不至于低估真实最短距离。所以第一步是普适的负权边的破坏力不在这里。这个上界引理的价值在于它排除了“算法算出一个比答案还小的数”的可能性。如果没有它后面证明的链条根本拉不起来——你连“当前值不会低于标准答案”都没把握就没法继续推理。2.2 第二步每次从堆里弹出的节点dist 就是正确答案这是三步里最核心、也最漂亮的反证法论证。假设第一次出错某个节点u从堆中被弹出时dist[u] δ(s, u)。也就是说算法以为的最短距离比真实最短距离大把它钉死成一个错误答案。现在看真实的从s到u的最短路径P。它从s出发一路走到u。我们把节点分成两类已经确定在S里和还没确定不在S里。s显然在S里u不在S里所以路径P上必定存在一条“跨界边”从某个已经确定的节点走到某个还没确定的节点。设这个还没确定的节点叫y而跨界边的起点是x即x已经确定y还没确定。因为x在u之前就已经被确定了而u是“第一个出错”的节点所以x被确定时是正确的即dist[x] δ(s, x)。又因为x被确定的那一刻算法会对它的所有出边做松弛所以dist[y] dist[x] w(x, y) δ(s, x) w(x, y)到这里需要补一个最优子结构的结论从s到x的这段路径也必须是真实最短路径的前缀即δ(s, x) w(x, y) δ(s, y)为什么如果从 s 到 x 可以走一条比 P 前缀更短的路再接上x - y再接上y到u的剩余部分就能得到一条比 P 更短的 s 到 u 的路这和 “P 是最短路”矛盾。由于边权非负y到u的剩余部分长度也非负所以又有δ(s, y) δ(s, u)现在把这几条串起来dist[y] δ(s, y) δ(s, u) dist[u]最后一个严格小于号来自反证假设dist[u] δ(s, u)。但别忘了u和y都是未确定节点而算法每次弹出的是未确定节点里dist最小的那个。既然u都被弹出来了排在它前面的更小的y不应该还在堆里。也就是说按弹出规则必然还有dist[u] dist[y]这跟刚才的dist[y] dist[u]直接矛盾。矛盾说明反证假设不成立。所以每次弹出的节点它的dist一定等于真实最短距离δ。注意这条证明里有两个必须依赖“边权非负”的地方一是“y到u的剩余路径长度非负因此δ(s, y) δ(s, u)”二是跨界边之后的部分不会让总路径变短。一旦出现负权边整个链条就会崩掉。这正是 Dijkstra 不能处理负权的根源不是代码层面的限制而是证明层面的限制。2.3 第三步用归纳法把单步正确性推广到整轮运行第二步只证明了“某一次弹出是正确的”但这不够。算法要跑很多轮每一轮都依赖上一轮的松弛结果。如果每一步都正确最终结果才正确。用数学归纳法基础情况第一次弹出的必然是源点sdist[s] 0 δ(s, s)正确。归纳假设假设前k次弹出的节点都已经被正确确定。那么算法会依据这些正确确定的节点去松弛它们的出边更新其他未确定节点的dist。归纳步第k 1次弹出的节点u在弹出时满足第二步的证明前提它是未确定节点中 dist 最小的。而由于前 k 次弹出都正确第二步证明中关于“跨界点 x 正确”的前提也成立所以第二次的证明结论对这次弹出同样成立u的 dist 等于真实最短距离。如此一环扣一环每一次弹出都正确。算法不断弹出节点直到优先队列为空。所有从源点可达的节点都会被正确确定不可达的节点保持无穷大也对应着“不可达”的真实答案。到这一步“三步证明”完整闭环上界引理打底弹出即正确打透归纳法收尾。2.4 三步缺一不可负权边到底破坏了哪一步很多人背过“Dijkstra 不能处理负权”但说不出破坏点在哪。现在可以清楚地回答负权边主要破坏的是第二步。第二步的证明链条是dist[u] dist[y] δ(s, y) δ(s, u) dist[u]其中δ(s, y) δ(s, u)这一步依赖“从y到u的剩余路径长度非负”。如果存在负权边那么 y 后面的路径可能是负的总长度导致δ(s, u)比δ(s, y)更小于是“最小未确定节点一定是安全的”这个结论就不再成立。典型反例s - a权重 4s - b权重 2b - a权重 -3。算法第一次弹出bdist2然后更新a为 -1但在此之前它可能已经把a作为 dist4 的最小节点弹出了不只要按弹出顺序当 dist[ b ]2 小于 dist[a]4所以先弹 b通过负权边更新 a-1再弹 a-1算法仍是正确的这里需要构造算法真正出错的例子。更经典的反例s - x权重 2s - y权重 3x - z权重 -5且 y2如果算法先确定 x2然后松弛 z-3没问题但若s - a1, a - b2, a - c4, b - c-6当 dist[a]1 确定后更新 b3、c5下一步弹 b3再通过 b-c-6 更新 c-3这时 c 已经不是最小算法弹出的顺序会在 c 出堆之前先被 b 弹出来修正所以还不出错。真正让 Dijkstra 出错的反例需要负权边能“拉低”一个已经被弹出的节点s - u 2s - v 3u - v 1v - u -5。第一次弹 u2更新 v3弹 v3u 已确定不再更新但真实 s-v-u 8 不是更小这里需要 racheting 更精确s - a 0? 构造s - a 2, s - b 3, a - c 10, b - c -12。第一次弹 a2更新 c12然后弹 b3更新 c-9c 还没确定最终 c-9 正确。算法仍对。真正出错需要负权边把已经弹出的节点的 dist 拉低。比如s - a 3, s - b 4, a - b 2, b - c 1, a - c 10, ?如果先弹 a3b4 a所以先 a更新 b5 而不是 4c13弹 b4来自 s 直达然后 b-c1 更新 c5c 没弹过正确。经典反例是s - u 3s - v 4u - v 5v - w -10u - w 100。先弹 u3更新 v8 和 w103然后弹 v4直达更新 w-6w 未弹出正确但如果 w 已经被确定为 103 并在 v 之前弹出不可能v4 小于 103先弹 v。更准确的错误例子要使得一个节点在“通过负边被刷新之前”就被弹出。考虑s - a 2, s - b 3, a - c 6, b - c -20且 c 当前距离由 a 传到 8 而 b3 会在 c 之前 a 先弹出 c8然后 b 未弹出dist[b]3 dist[c]8先弹 b 再更新 c-17不会错。若没有到 c 的其他更先路径s - a 2, s - c 10, a - b 1, b - c -5。弹 a2更新 b3弹 b3更新 c-2c 初始 10 从未弹出输出 -2正确。要想真正错误需要有一个节点在“某条路径将它拉到很小时”之前已经被弹出作为最小。例如s - x 5s - y 6x - z 1y - z -10z 的候选有 x16 和 y? y6 发生在? 弹 x5更新 z6堆里有 y6、z6弹 y 或 z 取决于堆序若弹 z则 z 根据 x 路径为 6但真实是 y-z -4此时 z 被错误确定。这个例子依赖 6 和 6 相等时的弹出顺序。若 y7弹 x5更新 z6堆里 y7、z6先弹 z6 出错因为真实 s-y-z -3。这个例子正确展示有向边 y-z-10但 y 距离 7z 通过 x 距离 6z 先被弹出之后 y 更新 z-3 为时已晚。我会在代码之后的常见问题里给一个更简明的负权反例在 4.2 部分列出。回到正题负权边的破坏本质上是“先被弹出的节点可能因为后面某个节点的负边而被拉低”而第二步证明恰恰排除了这种可能性因为“拉低”需要负的剩余路径违反非负假设。这也解释了为什么障碍网络中边权都为正或至少非负时算法稳如泰山。3. 证明之外直接可跑的代码实现到这一步证明的骨架已经立住了。但光懂证明不写代码等于“看菜谱没下厨”。接下来我给两版我平时最常用的实现Python 一版、C 一版都带堆优化并讲清楚为什么要这么写。3.1 从朴素实现到堆优化为什么非用堆不可先看朴素做法每次从未确定节点里扫描一遍找最小 dist然后松弛它的出边。找最小值是O(V)每个节点找一次松弛所有边是O(E)总复杂度O(V^2 E)。这在稠密图里表现还行但一旦图的节点数上到十万级、边数上到百万级平方复杂度直接把人送走。堆优化准确说是优先队列优化的思路是把“找全局最小 dist”这件事从O(V)降到O(log V)。每次松弛后把更新的(新dist, 节点)压入堆弹出时取出当前最小的候选。这一版在稀疏图里的表现是O((V E) log V)是竞赛和工程里的标准做法。有人会问为什么不用set或visited数组其实都可以但优先队列配“懒删除”是代码量最小、最不容易写错的一种。后面第 4 章我会专门聊懒删除和 visited 的区别。3.2 Python 30 行搞定附路径还原我常用的 Python 版本如下import heapq def dijkstra(n, edges, s): # 建邻接表 adj [[] for _ in range(n)] for u, v, w in edges: adj[u].append((v, w)) INF float(inf) dist [INF] * n prev [-1] * n dist[s] 0 pq [(0, s)] # (当前距离, 节点编号) while pq: d, u heapq.heappop(pq) # 懒删除如果堆里的这条记录已经过时直接跳过 if d ! dist[u]: continue for v, w in adj[u]: nd d w if nd dist[v]: dist[v] nd prev[v] u heapq.heappush(pq, (nd, v)) return dist, prev def get_path(prev, s, t): # 从终点回溯到起点再反转 path [] cur t while cur ! -1: path.append(cur) if cur s: break cur prev[cur] else: return [] # 说明不可达 return path[::-1]关键点就两个一是if d ! dist[u]这个懒删除判断保证同一个节点不会因为堆里有多条旧记录而重复松弛二是松弛成功才更新prev[v]这样路径还原时一定沿着最后一次成功更新的方向回溯。我用一个三元组(u, v, w)列表作为边的输入方便演示。实际工程里你换成从文件读、从数据库读都行核心逻辑不变。3.3 C 常用写法与 INF 的选择C 版本我一般写成这样#include bits/stdc.h using namespace std; const long long INF 4e18; vectorlong long dijkstra(int n, int s, vectorvectorpairint, long long adj) { vectorlong long dist(n, INF); dist[s] 0; priority_queuepairlong long, int, vectorpairlong long, int, greater pq; pq.push({0, s}); while (!pq.empty()) { auto [d, u] pq.top(); pq.pop(); if (d ! dist[u]) continue; for (auto [v, w] : adj[u]) { long long nd d w; if (nd dist[v]) { dist[v] nd; pq.push({nd, v}); } } } return dist; }这里有个我自己踩过的坑INF 的选取。早期我喜欢用0x3f3f3f3f大约是 10 亿够很多场景用。一旦边权很大或者路径很长d w会溢出变成负数然后算法就全乱了。后来我统一改用long long和4e18再配合if (d w dist[v])的判断把溢出风险从根上掐掉。竞赛里如果题目数据范围很大直接用long long是最省心的。3.4 复杂度算明白别等面试官追问堆优化版的时间复杂度需要拆开看每个节点最多因为一次成功的松弛而入堆一次严格说每个节点可能入堆多次但每次成功松弛都意味着它的 dist 变小了一次而 dist 只会变小有限次。最坏情况下每条边都可能触发一次入堆所以入堆操作不超过O(E)次。每次堆操作是O(log V)堆里同时存在的元素不超过O(E)个但log E与log V同阶。总时间复杂度O((V E) log V)。空间复杂度邻接表O(V E)堆O(E)dist 和 prev 数组O(V)总空间O(V E)。朴素版是O(V^2 E)空间如果用邻接矩阵就是O(V^2)。所以在稀疏图里堆优化是碾压性的优势在稠密图里朴素版反而可能更好。这个对比我会在第 4 章再展开。4. 实操中一定会踩的坑代码能跑通和代码能稳定跑对中间隔着一堆经验细节。这一章我把自己的踩坑记录直接分享出来每一节都能对应到一个真实发生过的场景。4.1 懒删除 vs visited 数组到底用哪个网上很多版本会维护一个visited数组visited [False] * n while pq: d, u heapq.heappop(pq) if visited[u]: continue visited[u] True ...这个写法也能过但我要提醒一点visited的作用是“节点一旦弹出就不再处理”这等价于“第一次弹出时堆里那条记录就是最新记录”。当堆里同时有同一个节点的新旧记录时旧记录先被弹出来visited 是 False于是它把节点标记成 True等新记录弹出来时被直接跳过。听起来没问题吧其实没问题。所以懒删除和 visited 在正确性上是等价的。真正的坑在于一些初学者会把visited[u] True写在dist更新之后或者放在循环外面搞成“还没弹出就标记”那就是灾难。我的建议是如果你刚学用懒删除if d ! dist[u]: continue少一个数组少一个出错点。等写熟了再按喜好换。4.2 负权边出错的最直观反例为了把这个事彻底钉死我放一个最经典的“死亡三角”s - x 权重 5 s - y 权重 7 x - z 权重 1 y - z 权重 -10算法流程初始化 dist[s]0dist[x]5dist[y]7。堆里是 (5, x), (7, y)。弹出 x松弛 zdist[z] 6入堆。此时堆里有 (6, z), (7, y)于是弹出 z把 z 确定成 6。但真正的答案是什么s - y - z 的路径长度是 7 (-10) -3比 6 小得多。等 y 稍后被弹出、再尝试松弛 z 时z 已经确定算法不会回头更新它。最终输出错误答案 6。这就是负权边的威力它让后面某个节点把“已经确定的节点”的距离拉得更低而贪心算法没有回头路。看过这个例子你再去理解证明里那句“依赖边权非负”感受会完全不一样。4.3 稠密图里堆优化反而变慢怎么选堆优化虽然名字听起来高大上但在稠密图里它并不是最优解。稠密图E ~ V^2堆优化复杂度O((V V^2) log V) O(V^2 log V)而朴素写的O(V^2 E) O(V^2)反而更快。那怎么判断该用哪种一句话稀疏图用堆优化稠密图用朴素版。竞赛里如果题目告诉你N 1000且边数接近N^2直接写朴素二维数组版如果N到十万级那就必须堆优化。工程场景里大多数图都是稀疏的堆优化是默认选择但你要知道还有这个权衡别在面试里被问“稠密图怎么办”时只说得出堆优化一个答案。4.4 路径还原和“不可达”的边界情况路径还原的代码虽然短但坑不少。最典型的是如果目标是不可达节点prev[t]一直保持 -1回溯时根本走不到s最后你会在cur s的重逢之前就掉进死循环吗不会因为cur -1时 while 条件就停了但返回的路径是不完整的。我习惯在get_path里先判断dist[t]是否是 INF如果是就直接返回空列表。另外如果图中存在多条最短路径prev数组只记录“最后一次成功松弛”的方向所以还原出的路径未必是字典序最小也未必是题目指定的那条。遇到这种题目要求时你需要额外定义比较规则而不是直接信任 prev。还有一个细节源点本身到源点的路径还原。prev[s]初始为 -1回溯时第一步就遇到cur s路径是[s]没问题。但如果你的prev数组初始化为某个“不可能的值”比如-2那就要保证在更新prev[v] u时s不会被覆盖成自己。实际写代码时直接保持 -1 最省事。4.5 常见问题速查表现象可能原因解决方案结果比答案大堆里的旧记录没有被跳过导致已确定节点被再次松弛用懒删除if d ! dist[u]或在弹出后维护 visited结果比答案小图里有负权边算法被错误应用换 Bellman-Ford / SPFA确认边权非负运行超时稠密图用了堆优化堆操作太多稠密图改用O(V^2)的朴素版路径还原死循环不可达节点 prev 指向 -1 但没加保护push 循环前先判断 dist 是否 INF结果溢出INF 或 dist 用了 int路径总长超过上限切换long longINF 用4e180 权边导致结果异常其实不会非负权重包含 0算法依然正确放心用别被“0 权会不会死循环”的直觉迷惑最后一行值得多说一句0 权边的存在不会破坏 Dijkstra。只要没有负权第二步证明里的“剩余路径长度非负”依然成立0 权边只是让某些 dist 相等而已堆的弹出顺序可能变化但正确性不变。5. 从单源最短路径向外走一步5.1 什么时候别用 DijkstraDijkstra 虽然好用但适用范围是“单源 边权非负”。实际工程里总有更复杂的场景到时候你得会换算法算法适用场景复杂度备注BFS无权图或边权全为 1O(V E)按层扩展比 Dijkstra 更快Dijkstra非负权有向/无向图O((VE) log V)或O(V^2)最常用的单源最短路Bellman-Ford有负权边、需要检测负环O(V * E)可以报告负环存在Floyd-Warshall全源最短路或 V 很小时O(V^3)代码极短适合 V ≤ 400 的稠密小图如果你拿到一道题第一件事不是套 Dijkstra 模板而是先看两条边权是否可能为负是单源还是多源。边权不为负、单源才轮到 Dijkstra 上场。否则该上 Bellman-Ford 上 Bellman-Ford该上 Floyd 上 Floyd。这个判断比背模板更重要我在几次面试里被追着问“这题为什么不能 Dijkstra”答对了才把印象分拉回来。5.2 几个工程/竞赛里高频的模型转换Dijkstra 本身的代码就这么多但它的应用变体很丰富。我挑三个自己经常用的第一个是“多源最短路”。问题给你多个起点要求求每个节点到最近的起点的距离。你不用跑多次 Dijkstra而是建一个虚拟源点向所有真实起点连一条权重为 0 的边然后从这个虚拟源点跑一次 Dijkstra。这个技巧在“多个仓库送货到各城市最短距离”这类题里非常常用。第二个是“分层图”。比如“最多可以免费坐 K 次飞机求最短路”。把每个节点复制成 K1 层第 i 层表示已经用了 i 次免费机会。层内边是常规花费层间边是 0 花费的“免费通道”。然后用 Dijkstra 在这个扩大 K1 倍的图上跑。正确性依然依赖 Dijkstra 本身的性质只要你建图建对。第三个是“次短路”。要求严格第二短的路径。可以维护两个 dist一个最短、一个次短松弛时先尝试更新最小再尝试更新次小。Dijkstra 的弹出思想可以扩展到“次小值”上但这已经是某种意义上的变形面试和竞赛里问得不少。初学者先掌握主算法次短路等你回来再研究。5.3 三步证明在笔试/面试里的实战价值职位面试中算法题最怕的不是你写不出代码而是写完代码被追问“为什么这样是对的”。我见过太多候选人可以默写 Dijkstra却解释不清“为什么弹出即确定”。如果你能把三步证明的逻辑链条讲清楚等于向面试官传递一个信号你不是背模板的你是理解算法的。我的答题节奏建议是先给结论“Dijkstra 是贪心法关键在证明每次弹出的节点距离已是最优”然后快速说三步框架不用推得太细最后补一句“正因为证明依赖边权非负所以负权边会使某些节点过早确定从而出错”。这个回答一分钟左右信息密度足够高面试官一般会很满意。工程场景里这个理解也有用。比如你负责的推荐系统里要算“用户到内容节点的最短路径”数据流偶尔混入异常负值如果你知道 Dijkstra 的边界你就能第一时间判断是“数据脏了”而不是“算法写错了”这种排查问题的能力往往是老手和新手的分水岭。结尾之前再分享一点我自己的体会我最早学 Dijkstra 的时候也是对着模板硬背刷了几十道题都觉得没什么问题直到有次被一道“为什么 Dijkstra 不能处理负权”的题卡住才回头认真啃证明。说句实话三步证明里的第二步反证法我第一遍看的时候差点绕晕真正看明白是在纸上把那个矛盾链条dist[u] dist[y] δ(s,y) δ(s,u) dist[u]抄了五遍之后。所以这个小技巧也送给你学任何贪心算法都试着把它证明拆成“上界引理 反证取关键节点 归纳收尾”的骨架Dijkstra 是训练这个能力最好的案例。当你遇到一个新问题直觉告诉你“应该选当前最优”别急着写代码先在草稿纸上套一遍这个骨架看第三步的链条在哪里会断断点往往就是算法不适用的边界条件。这个习惯帮我避过好几次“看着对、实际错”的坑希望你也能用上。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号