单源最短路径
dijkstra
算法描述
- 解决:正权重连通图中,源节点与其他节点的最短距离
- 应用:GPS寻路
历史背景
荷兰杰出计算机科学家、软件工程师 Dr. Edsger W. Dijkstra 创建并发布了这个算法。

1994 年,Edsger Dijkstra 博士 在 ETH Zurich(Andreas F. Borchert 摄)
- 博士的采访:
从 Rotterdam 到 Groningen 的最短路线是什么?我花了大概 20 分钟时间设计了这个寻找最短路径的算法。一天早上我正和我年轻的未婚妻在 Amsterdam 逛街,觉得有点累了,我们就坐在咖啡厅的露台上喝了一杯咖啡,我在想是否能够解决这个问题,然后,我设计出了这个最短路径算法。我说过,这是一个 20 分钟的设计。事实上,三年之后的 1959 年它才被发布,现在看来依然很不错,其原因之一是我当时设计的时候没有纸和笔,从而不得不极力避免所有可避免的复杂性。最终,令我惊讶的是,这个算法成为了我成名的基石之一。——引自文章《An interview with Edsger W. Dijkstra》.
看来生活中善于思考的人,不仅仅能取得更多成就,还能提升幸福感。仅仅 20 分钟的时间,Dijkstra 博士设计出了位列计算机科学史上最著名的算法之一的 Dijkstra 算法。
算法过程
- 寻找源节点最近节点(贪心思想)
- 记录已知最短路径,有更短的路径时更新,标记已访问
- 重复操作
必要条件
权重为正
有负边,会导致之前保存的最短距离无效,出现走的多但是距离短,不切实际。
C++实现(堆优化)
#include <bits/stdc++.h>
#define INF 0x3f3f3f3f // 定义一个代表无穷大的常量
using namespace std;
typedef pair<int, int> iPair; //边的终点和权重
/*
* 功能: 添加边到图的邻接表中
* 参数: adj - 邻接表,存储图的边
* u - 第一端点
* v - 第二端点
* w - 边的权重
*/
void addEdge(vector<pair<int, int> > adj[], int u, int v, int w){
// 将边 (u, v) 和权重 w 添加到邻接表中
adj[u].push_back(make_pair(v, w)); // 从 u 到 v
adj[v].push_back(make_pair(u, w)); // 从 v 到 u(无向图)
}
/*
* 功能: 使用Dijkstra算法计算从源点到其他所有顶点的最短路径
* 参数: adj - 邻接表
* V - 顶点数量
* src - 源点
*/
void dijkstra(vector<pair<int, int> > adj[], int V, int src){
// 优先队列,存储当前顶点及其最短路径的权重(堆优化)
priority_queue<iPair, vector<iPair>, greater<iPair> > pq;
// 存储从源点到每个顶点的最短距离,初始化为无穷大
vector<int> dist(V, INF);
// 访问标记数组,记录哪些顶点已经被访问过
vector<bool> visited(V, false);
// 将源点距离初始化为0,并将其加入到优先队列中
pq.push(make_pair(0, src));
dist[src] = 0;
// 当优先队列不为空时
while(!pq.empty()){
// 获取当前顶点
int u = pq.top().second;
pq.pop();
// 如果该顶点已经访问过,继续下一个
if(visited[u]) continue;
visited[u] = true; // 将顶点标记为已访问
// 遍历当前顶点的所有邻居
for(auto x : adj[u]){
int v = x.first; // 邻居顶点
int wt = x.second; // 边的权重
// 如果通过当前顶点到邻居的路径距离更短
if(dist[v] > dist[u] + wt){
dist[v] = dist[u] + wt; // 更新最短路径
pq.push(make_pair(dist[v], v)); // 将更新后的邻居顶点加入到优先队列中
}
}
}
// 输出每个顶点从源点的最短距离
printf("Vertex distance from source\n");
for(int i = 0; i < V; i++){
printf("%d \t\t %d\n", i, dist[i]); // 输出顶点编号及其距离
}
}
/*
* 主函数: 程序的入口点
*/
int main() {
int V = 9; // 顶点的数量
vector<iPair> adj[V]; // 定义邻接表,用于存储图
// 添加边到邻接表
addEdge(adj, 0, 1, 4);
addEdge(adj, 0, 7, 8);
addEdge(adj, 1, 2, 8);
addEdge(adj, 1, 7, 11);
addEdge(adj, 2, 3, 7);
addEdge(adj, 2, 8, 2);
addEdge(adj, 2, 5, 4);
addEdge(adj, 3, 4, 9);
addEdge(adj, 3, 5, 14);
addEdge(adj, 4, 5, 10);
addEdge(adj, 5, 6, 2);
addEdge(adj, 6, 7, 1);
addEdge(adj, 6, 8, 6);
addEdge(adj, 7, 8, 7);
// 从源点0开始进行Dijkstra算法计算
dijkstra(adj, V, 0);
return 0; // 程序结束
}
Bellman–Ford
算法介绍
对连通图,寻找源点与其他节点最短距离,有负环返回false。
通过N-1次对所有边遍历松弛,得到最短距离。因为最短距离取得时的最坏情况是源点通过N-1条边抵达目标点,所以N-1次对所有边遍历松弛后必然得到与所有点的最短路径。
与dijkstra 算法比较:
dijkstra 算法不能处理负权重问题,Bellman–Ford能dijkstra 算法贪心遍历,有负值时旧数据无法更新;Bellman–Ford算法每次进行数据更新,可以处理负权值情况,但随之时间复杂度更高
算法实现
伪代码
procedure BellmanFord(list vertices, list edges, vertex source)
// 读入边和节点的列表
// 初始化图 distance为∞,predecessor为空
for each vertex v in vertices:
if v is source then distance[v] := 0
else distance[v] := infinity
predecessor[v] := null
// 对所有节点
for i from 1 to size(vertices)-1:
//检查每条边
for each edge (u, v) with weight w in edges:
if distance[u] + w < distance[v]:
distance[v] := distance[u] + w
predecessor[v] := u
// 检查是否有负权重的回路
for each edge (u, v) with weight w in edges:
if distance[u] + w < distance[v]:
error "图包含负权重的回路"
C++实现
#include <bits/stdc++.h>
using namespace std;
// Define structure for edges
struct Edge
{
int u, v, w;
};
bool BellmanFord(int V, vector<Edge> &edges, int src){
// Initialize distances from source to all other vertices as infinity
vector<int> dist(V, INT_MAX);
dist[src] = 0;
// Relax all edges |V-1| times
for(int i=0; i<V-1; i++){
for(Edge e : edges){
if(dist[e.u] != INT_MAX && dist[e.v] > dist[e.u] + e.w){
dist[e.v] = dist[e.u] + e.w;
}
}
}
// Check for negative cycles
for(Edge e : edges){
if(dist[e.u] != INT_MAX && dist[e.v] > dist[e.u] + e.w){
return false; //Negative cycle detected
}
}
return true;
}
int main(){
int V = 5;
vector<Edge> edges = {{0, 1, -1}, {0, 2, 4}, {1, 2, 3}, {1, 3, 2}, {1, 4, 2}, {3, 2, 5}, {3, 1, 1}, {4, 3, -3}};
int src = 0;
if(BellmanFord(V, edges, src)){
cout << "No negative cycle found" << endl;
}
else{
cout << "Negative cycle detected" << endl;
}
return 0;
}
SPFA
简介
Bellman–Ford算法每次更新都重头开始,过于暴力;dijkstra 算法贪心遍历,有负值时旧数据无法更新;SPFA 算法在Bellman–Ford算法基础上优化,用队列储存已经,不仅能处理负权图,效率也更高。
队列储存已优化边,如果有能放松且不在队列中的点,则将其入队。
实现
伪码
src, V, edges
for each vertex except for src in Vertex(G):
d(v) : INF
d(src) : 0
push src into Q
while Q not empty:
u : front in Q
pop front in Q
for edge[u, v] in edges:
if d(v) > d(u) + w(u, v)
then d(v) = d(u) + w(u, v)
if v not in Q:
push v into Q
C++实现(优化版)
优化点在于只取比头结点小的点入队
其他优化策略:
- 每次正常入队后
if d(Q.front) > d(Q.back) then t = Q.pop_back, Q.push_front,保证短距离在前
#include <bits/stdc++.h> // 引入标准库
using namespace std;
// 定义边的结构体
struct Edge {
int u, v, w; // u表示起点,v表示终点,w表示边的权重
};
// SPFA算法实现
void SPFA(vector<Edge>& edges, int V, int Src) {
vector<int> dist(V, INT_MAX); // 初始化距离数组,设置为无穷大
dist[Src] = 0; // 源点到自身的距离为0
deque<int> q; // 使用双端队列来存储节点
q.push_back(Src); // 将源点加入队列
while (!q.empty()) { // 只要队列不为空
int u = q.front(); // 取出队首元素
q.pop_back(); // 弹出队首元素
// 遍历所有边
for (Edge e : edges) {
// 如果通过u到达e.v的距离更短
if (dist[e.v] > dist[e.u] + e.w) {
dist[e.v] = dist[e.u] + e.w; // 更新距离
// 如果队列不为空且更新的距离比队列中最前面的节点的距离短,则加入队列
if (!q.empty() && dist[e.v] < dist[q.front()])
q.push_back(e.v);
}
}
}
}
int main() {
int V = 5; // 图中节点的数量
vector<Edge> edges; // 存储图的边
// 添加边
edges.emplace_back(Edge{0, 1, 4}); // 从0到1,权重为4
edges.emplace_back(Edge{0, 2, 2}); // 从0到2,权重为2
edges.emplace_back(Edge{1, 2, 1}); // 从1到2,权重为1
edges.emplace_back(Edge{1, 3, 3}); // 从1到3,权重为3
edges.emplace_back(Edge{2, 4, 2}); // 从2到4,权重为2
edges.emplace_back(Edge{3, 2, 1}); // 从3到2,权重为1
edges.emplace_back(Edge{3, 4, -3}); // 从3到4,权重为-3(表示负权重)
SPFA(edges, V, 0); // 从源点0开始运行SPFA算法
return 0; // 程序结束
}
A*算法
算法简介
A*算法是贪心算法(dijkstra)与启发式算法的综合,贪心算法负责准确性,启发式负责方向指引。
F = G + H
这是核心思想体现,G是距离源点距离(贪心得出),H是距离目的地的估计值(启发式得出)。dijkstra算法H=0,没有方向指引,可以找到,但效率低;启发式算法G=0,只有估计值,不一定能够找到准确的最短路径。
实现
准备
- 评价函数:F = G + H
F:距离评价值 G:距离源点距离 H:距离目标点距离
H由启发式函数估计得出,常见的启发式有:欧几里得距离、曼哈顿距离、切夫雪比距离
- 开放集OPENING、封闭集CLOSED
分别对应两个集合
- RELAX(松弛操作)
有源点src, 对于u的邻点v,if Distance(src, v) > Distance(src, u) + Distance(u, v) then Distance(src, v) = Distance(src, u) + Distance(u, v)
重复的周期过程
//预备--tem属于CLOSED,其他节点均既不在OPENING也不在CLOSED。tem是循环内变化点,SRC是源点。
F: []
for neighbor in neighbors of tem:
if neighbor not in OPENING:
push neighbor into OPENING
neighbor.father = tem
else if neighbor in OPENING:
//relax-->neighbor加入OPENING后,G = Distance(SRC, neighbor)可能会发生改变。在此通过松弛操作,确保G值最小。
if Distance(SRC, neighbor) > Distance(src, tem) + Distance(tem, neighbor):
Distance(SRC, neighbor) = Distance(src, tem) + Distance(tem, neighbor)
neighbor.father = tem //找到destination后据此回溯最短路径
else if neighbor in CLOSED or other unavailable visiting elements:
pass
h = estimate_function(neighbor, destination)
g = G = Distance(SRC, neighbor)
f = h + g
push f into F
next_tem = F.min()
push next_tem into CLOSED
图示与说明
- 现在我设计一款游戏,想找到GREEN到RED的最短路径,中间BLUE是不可访问点。

- GREEN作为SRC加入CLOSED(带有浅蓝色轮廓),遍历他的neighbors,根据
不在OPENING中的点加入OPENING,father值更新;在OPENING中的点RELAX操作原则,显然都加入OPENING(带有浅绿色轮廓)并更新father值,赋予g,h,f值属性,f加入F集合

- 显然右边元素neighbor_right的F值最小,将neighbor_right边框标识浅蓝色(加入CLOSED),neighbor_father赋值为SRC

- 重复
2.3.直至destination加入CLOSED。

- 根据元素father值,依次恢复路径(即最短路径)

参考文章:
- dijkstra:维基百科、图文详解 Dijkstra 最短路径算法
- BellmanFord:维基百科、一文掌握Bellman-Ford算法
- SPFA:维基百科、
- A*:https://www.gamedev.net/reference/articles/article2003.asp

浙公网安备 33010602011771号