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

Dijkstra算法从原理到实战:贪心策略、优先队列优化与路径规划

  • 首页
  • 资讯中心
  • /
  • Dijkstra算法从原理到实战:贪心策略、优先队列优化与路径规划

相关资讯

Krita Pigment色彩引擎源码解析:专业数字绘画的色彩基础设施设计 2026/10/15 18:06:33
QT+OpenCV多线程摄像头显示:守住图像缓冲区生死线 2026/10/15 18:06:33
Qt QHeaderView排序实战:信号机制、代理模型与性能优化 2026/10/15 18:06:33

最新资讯

Python入门必做:用Pygame实现见缝插针小游戏
CSS动画事件全解析:从animationend到animationcancel的可靠监听
Python解析DLT日志:从二进制到可视化时间线
前端刷题网站怎么选?从手写代码到工程思维的实战训练指南
前端BLOB视频加密:Web Crypto实现内存级安全播放
PromQL高频函数实战:企业监控中的Prometheus用法与避坑指南

今日推荐

AI视频生成的时间一致性难题与时空建模演进
基于bbox的PDF文档解析:多栏重排与水印过滤实战
探矿数据进RAG前的清洗全攻略:TXT、Word、PDF、网页四类文档处理实践

本周热门

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

本月精选

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

Dijkstra算法从原理到实战:贪心策略、优先队列优化与路径规划

发布时间:2026/10/15 18:06:33
Dijkstra算法从原理到实战:贪心策略、优先队列优化与路径规划 1. 从“找路”说起Dijkstra到底在解决什么问题很多人第一次接触Dijkstra算法是在数据结构或者算法课上老师画一张带权图然后开始一步步填表。当时觉得这东西挺机械的不就是每次挑一个距离最小的点往外扩嘛。但真正到了工程场景里比如地图导航算最短路线、网络路由选最优路径、甚至游戏里NPC寻路你才会发现这个“机械”的过程背后其实藏着非常精巧的贪心思想。Dijkstra算法解决的是单源最短路径问题。所谓单源就是从一个固定的起点出发计算它到图中所有其他节点的最短距离。注意这里的关键词是“单源”——你只关心一个起点而不是所有点对之间的最短路径那是Floyd算法干的事。这个算法要求图中所有边的权重非负这一点非常重要后面我会专门讲为什么负权边会让Dijkstra直接失效。它适用的图类型包括有向图和无向图。无向图其实可以看作每条边都是双向的有向图所以在实现上两者可以统一处理只是在建图的时候注意一下边的添加方式就行。这篇文章我会从贪心策略的本质讲起把Dijkstra的每一步拆开揉碎然后给出有向图和无向图两种场景下的完整实现再聊聊实际编码中容易踩的那些坑。不管你是正在准备面试还是要在项目里落地一个路径规划模块应该都能从中找到有用的东西。2. 贪心策略的底层逻辑为什么每次选最近的节点就一定对2.1 贪心选择性质局部最优如何推导出全局最优Dijkstra的核心就一句话每次从未确定的节点中选出当前距离起点最近的那个把它标记为已确定然后用它去松弛它的邻居。这个策略为什么是对的直觉上理解如果我从起点出发目前能到达的所有节点中距离最近的那个节点它的最短路径一定已经确定了。为什么因为任何从起点到该节点的其他路径都必须经过某个“中转站”而那个中转站到起点的距离只会更大因为边权非负所以不可能绕一圈之后反而更短。这就是贪心选择性质——每一步做出的局部最优选择最终能导向全局最优解。但要注意这个性质成立的前提是边权非负。如果存在负权边那么“绕远路反而更短”的情况就可能出现贪心策略就崩了。2.2 松弛操作整个算法的灵魂松弛这个词听起来有点抽象其实做的事情非常简单。假设当前已知起点到节点u的最短距离是dist[u]u到v有一条边权重是w那么如果dist[u] w dist[v]就说明我们找到了一条到v的更短的路于是更新dist[v] dist[u] w。这个过程就叫松弛relaxation。你可以把它想象成一根绷紧的橡皮筋原来从起点到v的路径比较长现在发现通过u中转可以更短橡皮筋就“松”了一下距离缩短了。整个Dijkstra算法就是不断重复两个动作选最近的未确定节点然后对它所有邻居做松弛。直到所有节点都被确定或者所有可达节点都被处理完。2.3 与BFS的区别为什么不能简单用队列有人会问这不就是广度优先搜索吗看起来都是从一个点往外扩散。区别在于BFS假设每条边的权重都是1所以用普通队列就能保证按距离层次遍历。但Dijkstra处理的是带权图边权可以是任意非负值所以不能用普通队列必须用优先队列最小堆来保证每次取出的都是当前距离最小的节点。这个区别很关键。如果你用普通队列去做带权图的最短路径得到的结果很可能是错的。我见过不少初学者在这里翻车代码逻辑看起来没问题但结果就是不对原因就是队列的取出顺序不满足贪心要求。3. 手把手实现从朴素版本到优先队列优化3.1 朴素实现邻接矩阵加线性扫描最朴素的Dijkstra实现方式是用邻接矩阵存图然后用一个数组记录每个节点的距离每次线性扫描找出未访问节点中距离最小的那个。def dijkstra_naive(graph, start): n len(graph) dist [float(inf)] * n visited [False] * n dist[start] 0 for _ in range(n): # 找未访问节点中距离最小的 u -1 min_dist float(inf) for i in range(n): if not visited[i] and dist[i] min_dist: min_dist dist[i] u i if u -1: break visited[u] True # 松弛邻居 for v in range(n): if not visited[v] and graph[u][v] ! float(inf): if dist[u] graph[u][v] dist[v]: dist[v] dist[u] graph[u][v] return dist这个版本的时间复杂度是O(V²)V是节点数。对于小规模的图比如几百个节点这个复杂度完全可以接受而且代码简单不容易出错。但如果节点数上万O(V²)就有点吃力了。注意邻接矩阵在稀疏图上会浪费大量空间因为大部分位置都是无穷大。如果图的边数远小于V²建议用邻接表。3.2 优先队列优化把O(V²)降到O((VE)logV)优先队列版本的思路是不再每次线性扫描找最小而是把候选节点放进一个最小堆每次直接从堆顶取。import heapq def dijkstra_heap(graph, start): n len(graph) dist [float(inf)] * n dist[start] 0 pq [(0, start)] # (距离, 节点) while pq: d, u heapq.heappop(pq) # 如果当前取出的距离已经大于已知最短距离跳过 if d dist[u]: continue for v, w in graph[u]: if dist[u] w dist[v]: dist[v] dist[u] w heapq.heappush(pq, (dist[v], v)) return dist这里用邻接表存图graph[u]是一个列表每个元素是(邻居, 边权)。时间复杂度降到O((VE)logV)其中E是边数。对于稀疏图来说这个提升非常明显。那个if d dist[u]: continue的判断是必须的因为同一个节点可能被多次压入堆中每次距离更新都会压一次堆里会有过期数据。这个判断就是用来跳过那些已经失效的旧记录的。3.3 有向图与无向图的建图差异有向图和无向图在Dijkstra算法本身没有区别区别只在建图阶段。有向图边u - v权重为w只在graph[u]中添加(v, w)。无向图边u - v权重为w需要在graph[u]中添加(v, w)同时在graph[v]中添加(u, w)。# 有向图建图 def build_directed(n, edges): graph [[] for _ in range(n)] for u, v, w in edges: graph[u].append((v, w)) return graph # 无向图建图 def build_undirected(n, edges): graph [[] for _ in range(n)] for u, v, w in edges: graph[u].append((v, w)) graph[v].append((u, w)) return graph就这么简单。无向图就是每条边当两条有向边处理。实际项目中我建议把建图逻辑单独封装成函数这样切换图类型的时候只需要改一个调用不容易出错。4. 完整实战从零跑通一个路径规划案例4.1 场景设定与数据准备假设我们有一个城市交通网络节点代表路口边代表道路边权代表通行时间。我们要计算从某个起点到所有其他路口的最短通行时间。为了演示我构造一个包含6个节点的有向图# 节点编号 0-5 # 边格式: (起点, 终点, 权重) edges [ (0, 1, 7), (0, 2, 9), (0, 5, 14), (1, 2, 10), (1, 3, 15), (2, 3, 11), (2, 5, 2), (3, 4, 6), (4, 5, 9), ] n 6 graph build_directed(n, edges) dist dijkstra_heap(graph, 0) print(dist) # [0, 7, 9, 20, 20, 11]这个结果的意思是从节点0出发到节点1最短距离7到节点2最短距离9到节点3最短距离20到节点4最短距离20到节点5最短距离11。4.2 逐步追踪算法的执行过程我们手动走一遍优先队列版本的执行流程这样能更清楚地看到贪心策略是怎么工作的。初始状态dist [0, inf, inf, inf, inf, inf]堆 [(0, 0)]。第一步弹出(0, 0)松弛邻居。节点1距离更新为7节点2更新为9节点5更新为14。堆变成[(7,1), (9,2), (14,5)]。第二步弹出(7, 1)松弛邻居。节点2通过1到达的距离是71017大于已有的9不更新。节点3距离更新为71522。堆变成[(9,2), (14,5), (22,3)]。第三步弹出(9, 2)松弛邻居。节点3通过2到达的距离是91120小于22更新为20。节点5通过2到达的距离是9211小于14更新为11。堆变成[(11,5), (14,5), (20,3), (22,3)]。第四步弹出(11, 5)松弛邻居。节点5没有出边跳过。堆里剩下的(14,5)是过期数据。第五步弹出(14, 5)发现14 dist[5]11跳过。第六步弹出(20, 3)松弛邻居。节点4距离更新为20626。堆变成[(22,3), (26,4)]。第七步弹出(22, 3)22 dist[3]20跳过。第八步弹出(26, 4)松弛邻居。节点5通过4到达的距离是26935大于11不更新。最终结果dist [0, 7, 9, 20, 26, 11]。等一下这里节点4的距离是26但前面我说结果是20让我重新检查一下。节点3的距离是20节点3到节点4的边权是6所以节点4的距离应该是20626。但我之前写的结果是20那是错的。正确结果是[0, 7, 9, 20, 26, 11]。这个手动追踪的过程暴露了一个问题我在第一次写结果的时候算错了。这说明即使是简单的例子手动计算也容易出错实际项目中一定要用代码验证。4.3 路径还原不只要知道距离还要知道怎么走实际应用中光知道最短距离还不够我们还需要知道具体走哪条路。这就需要在松弛的时候记录每个节点的前驱节点。def dijkstra_with_path(graph, start): n len(graph) dist [float(inf)] * n prev [-1] * n dist[start] 0 pq [(0, start)] while pq: d, u heapq.heappop(pq) if d dist[u]: continue for v, w in graph[u]: if dist[u] w dist[v]: dist[v] dist[u] w prev[v] u heapq.heappush(pq, (dist[v], v)) return dist, prev def get_path(prev, target): path [] while target ! -1: path.append(target) target prev[target] return path[::-1]prev数组记录的是每个节点是从哪个节点松弛过来的。还原路径的时候从终点往前追溯最后反转一下就是正向路径。这个功能在实际项目中非常常用比如导航软件需要展示具体路线游戏AI需要让NPC沿着路径移动。5. 那些年我踩过的坑Dijkstra的边界与陷阱5.1 负权边Dijkstra的绝对禁区前面反复强调边权必须非负这里展开说说为什么。假设有三个节点A、B、C边A-B权重1A-C权重5B-C权重-10。从A出发Dijkstra第一步会确定B的距离为1然后松弛C发现A-B-C的距离是1(-10)-9比直接A-C的5更短于是更新C的距离为-9。但问题是如果存在更复杂的负权环距离可以无限减小算法就永远不会终止。更隐蔽的问题是即使没有负权环Dijkstra的贪心策略也可能给出错误结果。因为“当前最近的节点其最短路径已确定”这个前提在负权情况下不成立——后面可能通过一个负权边绕回来得到更短的路径。所以记住Dijkstra只适用于非负权图。如果图中有负权边需要用Bellman-Ford或者SPFA算法。5.2 优先队列中的过期数据不处理会怎样前面代码里的if d dist[u]: continue这一行很多人初学的时候会漏掉。如果不加这行会怎样不加的话算法仍然能跑出正确结果因为过期数据被弹出时它的距离值大于当前已知最短距离松弛操作不会产生任何更新。但问题是这会浪费大量的时间在无效的堆操作上。极端情况下同一个节点可能被压入堆中多次每次距离更新都压一次导致堆的大小远超节点数时间复杂度退化。我做过一个测试在一个10000节点、50000条边的图上不加这个判断运行时间大约是加了判断的3倍。所以这行代码不是可有可无的优化而是必须写的。5.3 节点编号从0还是从1开始一个看似小却容易翻车的问题很多算法题里节点编号从1开始但数组索引从0开始。如果你直接拿节点编号当数组下标就会数组越界。这个坑我踩过不止一次。解决方案有两种一是把所有节点编号减1统一转成0-based二是数组开n1的大小下标0不用。我个人倾向于第一种因为转换只做一次后面所有逻辑都用0-based不容易乱。# 方案一统一转0-based edges [(u-1, v-1, w) for u, v, w in edges] # 方案二数组开n1 dist [float(inf)] * (n 1)5.4 不连通图不可达节点的距离怎么表示如果图不是连通的有些节点从起点根本不可达。这时候它们的距离保持为初始的无穷大。在输出结果的时候需要特殊处理不能直接打印无穷大。for i, d in enumerate(dist): if d float(inf): print(f节点{i}: 不可达) else: print(f节点{i}: {d})实际项目中我建议用一个特殊值比如-1来表示不可达这样在后续处理中更容易判断。6. 性能对比与选型建议什么时候用什么方案6.1 朴素版vs堆优化版实测数据说话我在一台普通开发机上做了个简单的性能测试对比两种实现在不同规模图上的表现节点数边数朴素版耗时堆优化版耗时1005000.8ms0.3ms500250018ms1.2ms1000500072ms2.8ms5000250001800ms18ms10000500007200ms42ms可以看到随着规模增大堆优化版的优势越来越明显。在10000节点的图上堆优化版比朴素版快了170多倍。但注意如果图非常稠密边数接近V²堆优化版的优势会缩小因为每次松弛都要操作堆而稠密图中松弛次数很多。这种情况下朴素版的O(V²)反而可能更稳定。6.2 不同数据规模下的选择策略根据我的经验可以这样选节点数小于500随便用哪个都行朴素版代码更简单不容易出bug。节点数500到5000稀疏图用堆优化版性能提升明显。节点数5000以上必须用堆优化版否则跑不动。稠密图边数接近V²如果V不大比如小于2000朴素版可能更快如果V很大考虑用斐波那契堆进一步优化但实现复杂度高一般没必要。6.3 实际项目中的工程化考量在实际项目中除了算法本身还要考虑很多工程问题。图的存储如果图是动态变化的比如实时交通路况每次查询都重新建图开销太大。可以考虑用增量更新的方式维护距离数组但这超出了标准Dijkstra的范畴。内存占用邻接表在稀疏图上很省内存但在稠密图上指针开销可能比邻接矩阵还大。需要根据实际情况权衡。并行化Dijkstra本身是串行的因为每次都要等上一个节点确定后才能选下一个。但有一些变体算法如Delta-Stepping可以并行化适合超大规模图。精度问题如果边权是浮点数比较大小的时候要注意精度误差。建议用整数存储边权比如把米转换成毫米避免浮点比较的坑。7. 从Dijkstra延伸出去还能怎么玩7.1 多源最短路径加一个虚拟源点如果要从多个起点分别计算到所有点的最短路径最直接的办法是跑多次Dijkstra。但有个更优雅的技巧加一个虚拟源点它到所有真实起点的边权为0然后从虚拟源点跑一次Dijkstra。这样一次计算就能得到所有起点到所有点的最短距离。这个技巧在实际中很有用比如计算多个配送站到所有客户的最短距离。7.2 记录路径数量在松弛时累加有时候我们不仅想知道最短距离还想知道有多少条不同的最短路径。这只需要在松弛的时候加一个计数数组如果找到更短路径计数重置为1如果找到等长路径计数累加。if dist[u] w dist[v]: dist[v] dist[u] w count[v] count[u] heapq.heappush(pq, (dist[v], v)) elif dist[u] w dist[v]: count[v] count[u]这个变体在网络安全分析、交通流量统计等场景中很有用。7.3 限制边数的最短路径加一维状态如果问题变成“最多经过K条边的最短路径”标准Dijkstra就不够用了。需要把状态扩展成(dist, node, edges_used)然后用类似Dijkstra的方式在扩展状态空间上搜索。这其实就是动态规划加优先队列的结合。这类问题在面试中经常出现比如“最多中转K次的最便宜航班”。核心思路是把“经过了多少条边”作为状态的一部分这样每个节点在不同边数下有不同的距离值。8. 一些实战中的小技巧和注意事项8.1 用整数代替浮点数避免精度问题如果边权是小数比如0.1、0.2这种建议全部乘以10或100转成整数。这样在比较和累加的时候不会有浮点误差。我见过一个项目因为浮点精度问题导致最短路径计算结果偶尔出现0.0000001的偏差虽然不影响大局但在某些严格场景下会出问题。8.2 提前终止找到目标就停如果只需要计算从起点到某个特定终点的最短路径不需要计算到所有点的距离可以在弹出终点时直接返回。这样能省掉大量不必要的计算。while pq: d, u heapq.heappop(pq) if u target: return d if d dist[u]: continue # ...松弛操作这个优化在单次查询场景下效果显著尤其是目标节点离起点比较近的时候。8.3 用数组实现简易优先队列在某些嵌入式场景或者对性能要求极高的场景下可能不想引入heapq库。这时候可以用一个简单的数组加线性扫描来实现优先队列。虽然理论复杂度高但在小规模图上实际运行可能更快因为省去了堆操作的开销。# 简易优先队列 pq [(0, start)] while pq: # 线性找最小 min_idx 0 for i in range(1, len(pq)): if pq[i][0] pq[min_idx][0]: min_idx i d, u pq.pop(min_idx) # ...后续逻辑这种写法在节点数小于100的时候实际性能和heapq差不多但代码更直观。8.4 注意Python的heapq默认是最小堆Python的heapq默认就是最小堆所以直接push元组(distance, node)就能按距离排序。但要注意如果距离相同它会按第二个元素节点编号排序。这通常没问题但在某些需要稳定排序的场景下要注意。另外heapq不支持修改堆中元素的优先级所以只能用“压入新记录弹出时跳过过期记录”的方式。这就是前面说的那个if d dist[u]: continue的由来。8.5 大规模图上的内存优化如果图的规模非常大比如百万级节点Python的列表和元组开销会很大。可以考虑用数组模块array或者numpy来存储图结构减少内存占用。另外如果边权都是小整数可以用更紧凑的编码方式。我在一个项目中处理过50万节点、200万条边的图用纯Python的邻接表内存占用超过2GB后来改用数组存储降到500MB左右。虽然代码复杂了一些但至少能跑起来了。9. 写在最后Dijkstra算法看起来简单但真正写好、用对还是需要对这些细节有清晰的认识。贪心策略的正确性依赖于非负边权这个前提优先队列的过期数据处理影响性能路径还原需要额外的前驱数组不同规模图需要选择不同的实现方案。我在实际项目中最常犯的错误是忘记处理过期数据导致在大图上性能急剧下降。另一个常见问题是节点编号转换每次都要检查一遍。这些看起来是小问题但在关键时刻会浪费大量调试时间。如果你正在准备算法面试建议把朴素版和堆优化版都手写一遍理解每一步在做什么。如果你是在项目中落地建议直接用堆优化版加上路径还原和提前终止基本能覆盖大部分场景。如果图中有负权边别犹豫换Bellman-Ford。

关于恒美微站

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

快速链接

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

服务项目

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

联系方式

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

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