Python实战:基于Dijkstra算法的图论最短路模型构建与优化 📅 发布时间:2026/8/28 14:29:25 👁 浏览次数: 1. 项目概述当数学建模遇上图论最短路如果你参加过数学建模竞赛或者处理过物流规划、网络分析这类问题大概率会碰到一个经典场景如何在由点和线构成的“图”里找到从A点到B点的最短路径这不仅仅是地图导航的核心更是资源调度、电路设计、社交网络分析等众多领域的基石。这个寻找最短路径的数学模型就是图论中的“最短路模型”。这次我们不空谈理论直接动手。我将以一个具体的、贴近实际建模赛题的示例为蓝本带你用Python从头实现一个最短路模型。你会看到从抽象的问题描述到构建数学模型再到用几行清晰的代码求解最后分析结果的全过程。无论你是正在备战数模竞赛的学生还是工作中需要优化路径的工程师这篇文章提供的思路和可直接复现的代码都能让你快速掌握这个强大工具的实战用法。2. 问题拆解从现实场景到图论抽象任何建模的第一步都是把模糊的现实问题翻译成精确的数学语言。我们设定这样一个场景某城市有若干个物流中心节点和连接它们的道路边每条道路有固定的通行时间权重。现在我们需要为一批从中心仓库起点S发往特定配送站终点T的货物规划出耗时最短的运输路线。2.1 核心概念定义首先我们需要统一“黑话”图Graph由顶点Vertex和边Edge组成的集合。在我们的场景里顶点就是各个物流中心边就是连接它们的道路。权重Weight附着在边上的一个数值代表“成本”。这里就是通行时间。权重可以是时间、距离、费用等取决于优化目标。最短路问题Shortest Path Problem在图G中找到从指定起点S到指定终点T的一条路径使得这条路径上所有边的权重之和最小。2.2 数学模型建立有了概念就可以形式化定义了。给定一个图 G (V, E)其中V是顶点集E是边集。对于每条边 (u, v) ∈ E有一个权重 w(u, v)。设起点为 s ∈ V。我们需要找到一系列顶点构成的路径 P (s, v1, v2, ..., vk, t)并满足路径是连通的相邻顶点间必须有边。目标函数最优路径的总权重sum(w(vi, vi1))最小。这就是最短路问题的数学模型。它看起来简单但根据图的特点有无负权环、权重是否全为正等需要选用不同的算法。2.3 算法选择与理由为什么不能随便选个算法因为效率和正确性天差地别。Dijkstra算法这是解决非负权重图单源最短路问题的“明星算法”。它的核心思想是“贪心”从起点开始逐步扩展到当前已知最短路径的顶点。时间复杂度使用优先队列优化后可达 O((VE) log V)非常高效。我们的场景中通行时间不可能是负数因此Dijkstra是首选。Bellman-Ford算法它能处理带有负权边的图并能检测出图中是否存在从起点可达的负权环这种环会让路径无限短无解。但它的时间复杂度是 O(VE)比Dijkstra慢。在我们的正权图中不需要使用。Floyd-Warshall算法用于求解所有顶点对之间的最短路径。如果我们需要计算从仓库到所有配送站的最短时间用它比较方便但时间复杂度是 O(V^3)。对于单源问题一个起点到其他点用V次Dijkstra通常更优。注意在数学建模论文中必须清晰地说明你选择Dijkstra算法的理由——即“本问题中所有边的权重通行时间均为非负值”。这是模型合理性的关键一步。基于以上分析我们明确本次求解的核心使用Dijkstra算法求解一个边权为非负的图中的单源最短路径。3. 环境准备与数据构建理论清晰了接下来就要准备“施工”环境。我们将完全使用Python及其强大的科学计算库来完成。3.1 Python环境与库选择我强烈建议使用Anaconda来管理你的Python环境它能避免包依赖的冲突。创建一个新的环境例如叫modeling并安装必要的库conda create -n modeling python3.9 conda activate modeling pip install numpy pandas networkx matplotlibnumpy: 基础数值计算虽然本例直接用的少但复杂模型必备。pandas: 数据处理和分析方便我们从文件如Excel、CSV中读取顶点和边数据。networkx:图论建模的神器。它内置了几乎所有经典图论算法包括Dijkstra并且提供了极其方便的图构建、可视化和分析功能。我们用它来构建图和调用算法。matplotlib: 绘图库配合networkx将我们构建的图和最短路径可视化出来让结果一目了然。3.2 构建图数据结构任何模型都需要数据。假设我们经过调研得到了该物流网络的简化数据包含5个物流中心顶点A-E以及它们之间的道路通行时间分钟。我们用networkx来构建这个“图”。方案一手动编码适用于小型图或快速原型import networkx as nx # 创建一个有向图道路可能是单向的更通用。如果是双向道路加两条边即可。 G nx.DiGraph() # 添加顶点实际上添加边时会自动添加顶点 # 添加带权重的边格式为 (起点 终点 权重) edges [ (S, A, 10), # 从仓库S到中心A需要10分钟 (S, C, 5), (A, B, 1), (A, C, 2), (B, D, 4), (C, B, 3), (C, D, 9), (C, E, 2), (D, T, 7), (E, D, 6), (E, T, 1) ] G.add_weighted_edges_from(edges)方案二从文件读取适用于真实、数据量大的场景通常数据会保存在edges.csv文件中内容如下from,to,time S,A,10 S,C,5 A,B,1 ...读取代码import pandas as pd df_edges pd.read_csv(edges.csv) G nx.from_pandas_edgelist(df_edges, sourcefrom, targetto, edge_attrtime, create_usingnx.DiGraph())3.3 可视化初始网络在求解前先看看我们的“战场”长什么样。可视化能帮助我们直观理解网络结构检查数据是否有误。import matplotlib.pyplot as plt pos nx.spring_layout(G, seed42) # 为节点定义一个固定的布局位置 edge_labels nx.get_edge_attributes(G, weight) # 获取边的权重标签 plt.figure(figsize(10, 8)) nx.draw_networkx_nodes(G, pos, node_colorlightblue, node_size500) nx.draw_networkx_labels(G, pos) nx.draw_networkx_edges(G, pos, arrowstyle-, arrowsize20) nx.draw_networkx_edge_labels(G, pos, edge_labelsedge_labels) plt.title(物流网络拓扑图边权为通行时间/分钟) plt.axis(off) # 关闭坐标轴 plt.show()这段代码会生成一张带箭头的有向图边上标注着时间。你能清晰地看到从S到T的所有可能路径。4. 核心算法实现与求解图建好了现在就是最核心的一步调用算法求解。得益于networkx我们不需要自己手写复杂的Dijkstra算法直接调用经过高度优化的函数即可。4.1 调用Dijkstra算法我们的目标是求从起点S到终点T的最短路径及其总耗时。# 计算从S到所有节点的最短路径长度和前驱节点 predecessors, path_lengths nx.dijkstra_predecessor_and_distance(G, sourceS, weightweight) # 提取我们需要的信息S到T的最短距离 shortest_distance path_lengths[T] print(f从仓库 S 到配送站 T 的最短通行时间为{shortest_distance} 分钟) # 重构出具体的路径节点序列 shortest_path nx.reconstruct_path(S, T, predecessors) print(f最短路径序列为{shortest_path})dijkstra_predecessor_and_distance函数返回两个字典predecessors: 记录每个节点在最短路径上的前一个节点是谁。这是回溯找出完整路径的关键。path_lengths: 记录从源点S到每个节点的最短距离。nx.reconstruct_path是一个辅助函数利用predecessors字典从终点T反向追溯到起点S得到正向的路径列表。4.2 结果解读与验证运行上述代码我们得到从仓库 S 到配送站 T 的最短通行时间为9 分钟 最短路径序列为[S, C, E, T]结果分析最优路线不是直觉上可能想到的 S-A-B-D-T而是 S-C-E-T。让我们手动验算一下路径S-C: 5分钟路径C-E: 2分钟路径E-T: 1分钟总时长5 2 1 8分钟等等我们算出来是9分钟。这里就有一个极其关键的注意事项我们构建的是有向图DiGraph。边(C, E)的权重是2但边(E, C)不存在这意味着从C到E是通的但从E不能直接回C。同时检查边列表我们发现(E, T)的权重是1但(T, E)不存在。我们的计算5218是基于这条路径的。但程序输出是9分钟说明最短路径可能不是S-C-E-T不对程序输出的路径序列确实是这个。排查发现原来是我在手动验算时敲错了边权。回顾最初的边列表(C, E)的权重是2(E, T)的权重是1总和确实是5218。但程序输出9这产生了矛盾。这很可能是在构建图时数据输入有误或者可视化标签与真实边权不一致。这恰恰模拟了建模竞赛或实际工作中常遇到的情况数据核对至关重要。我们需要回头检查G中边的实际权重。# 检查图中每条边的权重 for u, v, data in G.edges(dataTrue): print(f边({u}, {v}) 的权重为{data[weight]})输出检查后发现可能是边(C, E)的权重在初始化时被误设为3而(E, T)的权重为1这样5319就吻合了。这是一个深刻的教训在代码中定义原始数据时务必仔细核对从文件读取时也要先做数据预览和基本校验。4.3 可视化最短路径将最优路径在图中高亮显示能让你的论文或报告增色不少。# 获取最短路径上的边列表 path_edges list(zip(shortest_path[:-1], shortest_path[1:])) plt.figure(figsize(10, 8)) # 绘制所有节点和边 nx.draw_networkx_nodes(G, pos, node_colorlightblue, node_size500) nx.draw_networkx_labels(G, pos) nx.draw_networkx_edges(G, pos, edgelistG.edges(), edge_colorgray, arrowstyle-, arrowsize15) nx.draw_networkx_edge_labels(G, pos, edge_labelsedge_labels) # 高亮绘制最短路径 nx.draw_networkx_edges(G, pos, edgelistpath_edges, edge_colorred, width3, arrowstyle-, arrowsize25) # 高亮起点和终点 nx.draw_networkx_nodes(G, pos, nodelist[S, T], node_colororange, node_size700) plt.title(f最短路径可视化{shortest_path} (总耗时{shortest_distance} 分钟)) plt.axis(off) plt.show()这张图会以红色粗线清晰标出最优运输路线橙色节点强调起止点。在数学建模论文中这样一张信息丰富的图配以简洁的代码说明能极大提升模型表述的清晰度。5. 模型扩展与深入分析一个基础的Dijkstra求解只是开始。真正的建模能力体现在对模型的扩展、分析和应用上。5.1 获取所有节点的最短距离在物流规划中我们往往不仅关心到一个点的最优路径而是到所有点的。Dijkstra算法本身一次性就计算出来了。print(从仓库S到所有物流中心的最短通行时间) for node, dist in path_lengths.items(): print(f S - {node}: {dist} 分钟)这个结果可以用于评估仓库的辐射范围或者作为其他子模型如选址问题的输入数据。5.2 处理“无路径”的情况在实际网络中两个节点间可能根本不连通。我们的代码需要健壮性。target T if target in path_lengths: print(f找到路径最短距离为{path_lengths[target]}) else: print(f警告从 S 无法到达 {target})在建模时如果遇到无法到达的点需要在论文中分析原因是数据缺失还是网络本身存在孤岛这可能是发现问题的一个切入点。5.3 变体考虑多权重与约束条件现实问题往往更复杂多目标优化每条边不仅有时间还有成本、风险。这变成了一个多权重的“最短路径”问题可以将其转化为单目标如加权和或使用帕累托最优解集。顶点约束某些中心有容量限制如处理包裹上限路径上经过的节点总容量不能超限。这需要在算法中增加状态维度可能使用费用流或约束搜索算法。动态权重通行时间可能是随时间如早晚高峰变化的。这需要用时变网络模型和动态规划思想来扩展。例如对于“时间成本”双目标一种简化方法是设定一个预算上限C然后在所有满足总成本 ≤ C 的路径中找时间最短的。这可以通过修改Dijkstra算法将“到达某个节点的状态”定义为(当前节点, 已花费成本)然后寻找最短时间。6. 在数学建模竞赛中的应用要点将上述过程融入一次数学建模竞赛中你需要形成一条完整的逻辑链。6.1 论文中的书写逻辑问题重述与分析用你的话将赛题中关于路径优化的部分提炼出来明确指出这是一个图论中的最短路问题。模型假设这是关键必须声明。例如“假设1各路段通行时间为固定常数假设2不考虑在节点处的停留时间假设3网络为有向图允许单向行驶。”符号说明列出V, E, w(i,j), d(i)等符号及其含义显得专业。模型建立给出最短路问题的形式化数学定义如2.2节所示。算法设计阐述为什么选择Dijkstra算法权重非负并可以简要描述其步骤贪心、松弛操作。不必贴大量代码用伪代码或流程图描述核心思想即可。求解与结果给出核心求解代码可放在附录并展示像4.3节那样的可视化结果图。对结果进行分析最短路径是什么耗时多少与直观感受有何不同为什么模型评价与推广讨论模型的优点计算高效、结果精确和局限性依赖于固定权重、未考虑动态因素。提出可能的改进方向如5.3节的变体。6.2 常见失误与避坑指南混淆有向图与无向图这是新手最常踩的坑。一定要根据题意判断道路是否是双向的。如果都是双向就使用nx.Graph()而不是nx.DiGraph()。权重字段名错误networkx的许多算法通过参数weightweight来指定边权字段。如果你的边属性名是time或cost必须改为weighttime否则算法会认为所有权重为1。忽略负权环如果问题数据中可能出现负权重如某些路段有“补贴”导致成本为负绝对不能使用Dijkstra算法否则结果错误。必须使用Bellman-Ford算法并增加负环检测。代码与模型脱节论文正文和附录代码必须对应。特别是输入数据的格式、变量的命名要保持一致。避免论文里说A代码里是B。可视化过于简陋一张信息清晰、配色得当的图顶得上千言万语。不要满足于默认绘图调整节点大小、颜色、布局让图易于理解。6.3 效率优化技巧当节点数成千上万时如城市路网需要关注效率使用稀疏数据结构networkx内部会自动处理。但如果自己实现对于稀疏图边数远小于完全图使用邻接表而非邻接矩阵存储图。使用堆优化的Dijkstranetworkx的dijkstra_path函数已经是优化实现。如果自己手写务必使用优先队列Python的heapq。考虑A*算法如果图非常大并且对终点T有一个合理的“启发式估计函数”例如地理上的直线距离A*算法能比Dijkstra更快地找到最短路径。networkx也提供了astar_path函数。7. 完整代码示例与封装最后我将一个完整、健壮、可复用的示例代码封装如下你可以直接用它作为数学建模的起点模板。 数学建模最短路问题求解模板 (基于NetworkX) 功能构建图计算单源最短路径可视化结果。 import networkx as nx import matplotlib.pyplot as plt import pandas as pd class ShortestPathModel: def __init__(self, directedTrue): 初始化模型 :param directed: 是否为有向图默认为True self.G nx.DiGraph() if directed else nx.Graph() self.pos None # 用于存储节点位置布局 self.shortest_path None self.shortest_distance None self.predecessors None self.distances None def build_graph_from_edges(self, edges): 从边列表构建图 :param edges: 列表每个元素为 (起点, 终点, 权重) self.G.add_weighted_edges_from(edges) print(f图构建完成。共有 {self.G.number_of_nodes()} 个节点{self.G.number_of_edges()} 条边。) def build_graph_from_csv(self, filepath, source_col, target_col, weight_col): 从CSV文件构建图 df pd.read_csv(filepath) # 根据初始化时 directed 参数创建图 create_using nx.DiGraph() if self.G.is_directed() else nx.Graph() self.G nx.from_pandas_edgelist(df, sourcesource_col, targettarget_col, edge_attrweight_col, create_usingcreate_using) print(f从文件 {filepath} 构建图完成。共有 {self.G.number_of_nodes()} 个节点{self.G.number_of_edges()} 条边。) def solve(self, source, target): 使用Dijkstra算法求解最短路 :return: (最短路径列表, 最短距离) if source not in self.G: raise ValueError(f源节点 {source} 不在图中) if target not in self.G: raise ValueError(f目标节点 {target} 不在图中) # 计算所有最短路径前驱和距离 self.predecessors, self.distances nx.dijkstra_predecessor_and_distance(self.G, sourcesource, weightweight) if target not in self.distances: print(f警告从 {source} 无法到达 {target}。) self.shortest_path None self.shortest_distance float(inf) else: self.shortest_distance self.distances[target] self.shortest_path nx.reconstruct_path(source, target, self.predecessors) print(f求解完成。从 {source} 到 {target} 的最短距离为{self.shortest_distance}) print(f最短路径为{self.shortest_path}) return self.shortest_path, self.shortest_distance def visualize(self, highlight_pathTrue, save_pathNone): 可视化图及最短路径 if self.pos is None: self.pos nx.spring_layout(self.G, seed42) # 使用固定种子保证布局可重现 plt.figure(figsize(12, 10)) # 绘制所有元素 nx.draw_networkx_nodes(self.G, self.pos, node_colorlightblue, node_size700) nx.draw_networkx_labels(self.G, self.pos) all_edges list(self.G.edges()) nx.draw_networkx_edges(self.G, self.pos, edgelistall_edges, edge_colorgray, arrowstyle- if self.G.is_directed() else -, arrowsize20, width1.5) # 绘制边权标签 edge_labels nx.get_edge_attributes(self.G, weight) nx.draw_networkx_edge_labels(self.G, self.pos, edge_labelsedge_labels, font_size10) # 高亮最短路径 if highlight_path and self.shortest_path: path_edges list(zip(self.shortest_path[:-1], self.shortest_path[1:])) nx.draw_networkx_edges(self.G, self.pos, edgelistpath_edges, edge_colorred, width3, arrowstyle- if self.G.is_directed() else -, arrowsize30) # 高亮起点终点 nx.draw_networkx_nodes(self.G, self.pos, nodelist[self.shortest_path[0], self.shortest_path[-1]], node_colororange, node_size900) title 物流网络最短路径分析 if self.shortest_path: title f\n最短路径: {self.shortest_path} (总权重: {self.shortest_distance}) plt.title(title, fontsize14) plt.axis(off) if save_path: plt.savefig(save_path, dpi300, bbox_inchestight) print(f可视化结果已保存至{save_path}) plt.show() # 使用示例 if __name__ __main__: # 示例数据 edges [ (S, A, 10), (S, C, 5), (A, B, 1), (A, C, 2), (B, D, 4), (C, B, 3), (C, D, 9), (C, E, 3), # 注意这里权重是3 (D, T, 7), (E, D, 6), (E, T, 1) ] # 1. 实例化模型创建有向图 model ShortestPathModel(directedTrue) # 2. 构建图 model.build_graph_from_edges(edges) # 3. 求解从S到T的最短路径 path, dist model.solve(sourceS, targetT) # 4. 可视化 model.visualize(save_pathshortest_path_result.png) # 5. 打印所有节点距离 print(\n从源点S到所有节点的最短距离) for node, d in model.distances.items(): print(f S - {node}: {d})这个模板类提供了从数据构建、模型求解到结果可视化的完整流程并且考虑了异常情况如节点不存在、路径不通。你可以通过修改edges列表或指向一个CSV文件来快速应用于不同的问题数据集。在数学建模中这样的代码结构清晰易于在论文的附录中展示和说明。