4、全局路径规划算法(上):Dijkstra算法原理、Python实现与可视化
各位同学,欢迎来到全局路径规划的第一讲。
今天咱们聊一个非常经典的算法——Dijkstra。说实话,我在工业AGV项目里,最开始接触的路径规划算法就是它。那时候刚入行,觉得这算法太简单了,不就是个最短路径吗?后来在实际产线上跑起来才发现,里面的门道还真不少。
好,咱们直接进入正题。
4.1 算法核心思想:贪心 + 松弛
Dijkstra算法要解决什么问题?说白了,就是在一个带权重的图里,找到从起点到所有其他节点的最短路径。
它的核心思想就两个词:贪心和松弛。
- 贪心:每次从「未确定最短路径」的节点中,选一个距离起点最近的节点,把它标记为已确定。
- 松弛:选完节点后,看看能不能通过这个节点,让它的邻居节点距离起点更近。如果能,就更新邻居的距离。
嗯,这里要注意:Dijkstra要求图中不能有负权边。为什么?因为一旦有负权边,贪心策略就失效了。我当年在调试一个仓储AGV时,就因为地图里有个负权重的「捷径」逻辑,导致算法一直跑不出正确路径。后来排查了半天才发现,是建模时把某个下坡路段的代价设成了负数。从那以后,我对负权边就特别敏感。
适用场景:
- 地图中所有边的权重都是非负数(比如距离、时间、能耗)
- 需要从单一起点到所有节点的最短路径
- 节点数量在几千以内(再大就需要考虑A*或分层规划了)
4.2 算法步骤详解
咱们用一个简单的例子来走一遍流程。假设有5个节点,起点是A。
| 步骤 | 当前节点 | 已确定集合 | 距离记录(A到各点) |
|---|---|---|---|
| 初始化 | - | {A} | A:0, B:∞, C:∞, D:∞, E:∞ |
| 1 | A | {A} | A:0, B:2, C:5, D:∞, E:∞ |
| 2 | B | {A, B} | A:0, B:2, C:4, D:7, E:∞ |
| 3 | C | {A, B, C} | A:0, B:2, C:4, D:6, E:9 |
| 4 | D | {A, B, C, D} | A:0, B:2, C:4, D:6, E:8 |
| 5 | E | {A, B, C, D, E} | 全部确定 |
你想想看,这个过程像不像我们平时用导航软件?每次到一个路口,就看看有没有更近的路。只不过Dijkstra把这个过程数学化了。
4.3 Python实现:从零手写
我个人习惯,学算法一定要手写一遍。光看别人代码,永远学不会。下面是我在项目中常用的一个实现版本,用到了优先队列(heapq)来优化性能。
import heapq
import math
def dijkstra(graph, start):
"""
graph: 邻接表表示的图,格式 {node: [(neighbor, weight), ...]}
start: 起点节点
返回: (distances, predecessors)
"""
# 初始化距离字典,起点为0,其他为无穷大
distances = {node: math.inf for node in graph}
distances[start] = 0
# 前驱节点字典,用于回溯路径
predecessors = {node: None for node in graph}
# 优先队列,存储 (距离, 节点)
pq = [(0, start)]
# 已确定最短路径的节点集合
visited = set()
while pq:
current_dist, current_node = heapq.heappop(pq)
# 如果当前节点已经处理过,跳过
if current_node in visited:
continue
# 标记为已确定
visited.add(current_node)
# 遍历邻居节点
for neighbor, weight in graph[current_node]:
if neighbor in visited:
continue
# 计算新的距离
new_dist = current_dist + weight
# 如果新距离更短,更新
if new_dist < distances[neighbor]:
distances[neighbor] = new_dist
predecessors[neighbor] = current_node
heapq.heappush(pq, (new_dist, neighbor))
return distances, predecessors
def reconstruct_path(predecessors, start, goal):
"""回溯路径"""
path = []
current = goal
while current is not None:
path.append(current)
current = predecessors[current]
path.reverse()
# 如果起点不在路径中,说明不可达
if path[0] != start:
return []
return path
避坑指南:
我曾经在项目里犯过一个低级错误——忘记检查节点是否已访问。结果在环形地图里,算法陷入了死循环。后来我养成了一个习惯:每次从优先队列取出节点后,先判断它是否在visited集合里。这个判断虽然简单,但能避免很多诡异的问题。
4.4 可视化:让算法「看得见」
光看代码和表格,你可能觉得Dijkstra很抽象。我建议你把它可视化出来。下面是一个简单的可视化实现,用matplotlib和networkx库。
import matplotlib.pyplot as plt
import networkx as nx
def visualize_dijkstra(graph, start, goal):
"""可视化Dijkstra算法的搜索过程"""
# 创建图对象
G = nx.Graph()
for node, neighbors in graph.items():
for neighbor, weight in neighbors:
G.add_edge(node, neighbor, weight=weight)
# 计算最短路径
distances, predecessors = dijkstra(graph, start)
path = reconstruct_path(predecessors, start, goal)
# 布局
pos = nx.spring_layout(G, seed=42)
# 绘制所有节点和边
plt.figure(figsize=(10, 8))
nx.draw_networkx_nodes(G, pos, node_color='lightblue',
node_size=500)
nx.draw_networkx_edges(G, pos, edge_color='gray', width=1)
nx.draw_networkx_labels(G, pos, font_size=12)
# 高亮最短路径
if path:
path_edges = list(zip(path[:-1], path[1:]))
nx.draw_networkx_nodes(G, pos, nodelist=path,
node_color='lightgreen', node_size=500)
nx.draw_networkx_edges(G, pos, edgelist=path_edges,
edge_color='red', width=3)
# 标记起点和终点
nx.draw_networkx_nodes(G, pos, nodelist=[start],
node_color='green', node_size=600)
nx.draw_networkx_nodes(G, pos, nodelist=[goal],
node_color='red', node_size=600)
# 显示边权重
edge_labels = nx.get_edge_attributes(G, 'weight')
nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels)
plt.title(f"Dijkstra: {start} → {goal}")
plt.axis('off')
plt.show()
# 测试用例
if __name__ == "__main__":
# 一个简单的图
graph = {
'A': [('B', 2), ('C', 5)],
'B': [('A', 2), ('C', 2), ('D', 5)],
'C': [('A', 5), ('B', 2), ('D', 2), ('E', 5)],
'D': [('B', 5), ('C', 2), ('E', 2)],
'E': [('C', 5), ('D', 2)]
}
visualize_dijkstra(graph, 'A', 'E')
可视化能帮你看到什么?
- 绿色节点:已确定最短路径的节点
- 红色边:最终的最短路径
- 灰色边:算法探索过但未选中的路径
你会发现,Dijkstra的搜索范围像一个不断扩大的圆。从起点开始,一圈一圈往外扩散。这就是为什么它叫「广度优先搜索的加权版本」。
4.5 工业场景中的实际考量
讲完了原理和代码,咱们聊聊实际应用。在工业AGV里,Dijkstra不是直接拿来用的。为什么?
- 地图太大:一个中型仓库可能有上千个节点,Dijkstra要遍历所有节点,效率太低。我一般用它做局部规划,或者作为其他算法的基准测试。
- 动态障碍物:Dijkstra假设地图是静态的。但AGV运行中,可能有其他车辆、人员突然出现。这时候需要结合局部规划(如DWA)来避障。
- 多AGV协同:如果多台AGV同时运行,Dijkstra算出的路径可能冲突。需要引入交通管制或路径预留机制。
注意:Dijkstra算出的路径是「最短」的,但不一定是「最优」的。比如,它不考虑转弯次数、道路宽度、AGV的转弯半径。在实际项目中,我通常会在Dijkstra的基础上,对路径做平滑处理,或者加入转弯代价。
4.6 小结与思考
好了,这一讲的内容就到这里。咱们总结一下:
- Dijkstra的核心是贪心+松弛,每次选最近节点,更新邻居距离
- 用优先队列实现,时间复杂度O((V+E)logV)
- 可视化能帮你直观理解算法的搜索过程
- 工业应用中需要结合实际情况做优化
下一讲,咱们会聊A*算法。它比Dijkstra多了个启发式函数,搜索效率高得多。但Dijkstra是基础,基础不牢,地动山摇。建议你把今天的代码跑一遍,改改参数,看看效果。
有问题随时交流。咱们下节课见。