最短路径算法
在图论中,最短路径问题是一类非常经典且实用的问题。本节将详细介绍几种常用的最短路径算法。
问题定义
【定义】给定一个图G=(V,E),其中V是顶点集合,E是边集合,每条边有一个权值(距离或成本)。最短路径问题就是要找到从起点s到终点t之间的一条路径,使得路径上所有边的权值之和最小。
最短路径问题分为几种类型:
- 单源最短路径:从一个源点到其他所有点的最短路径
- 多源最短路径:任意两点之间的最短路径
- 带负权边的最短路径
- 带负权环的图中的最短路径问题(可能无解)
Dijkstra算法
【算法】Dijkstra算法是解决单源最短路径问题的经典算法,适用于所有边权为非负数的情况。
算法思想
- 维护一个距离数组dist,dist[i]表示从源点s到顶点i的当前最短距离
- 每次从未处理的顶点中选择距离最小的顶点u
- 更新u的所有邻居v的距离:如果dist[u] + weight(u,v) < dist[v],则更新dist[v]
- 重复步骤2-3直到所有顶点都被处理
代码实现
基于优先队列的Dijkstra算法实现:
const int MAXN = 100005;
const int INF = 0x3f3f3f3f;
struct Edge {
int to, weight;
Edge(int _to, int _weight) : to(_to), weight(_weight) {}
};
vector<Edge> graph[MAXN]; // 邻接表表示的图
int dist[MAXN]; // 存储从源点到各点的最短距离
bool visited[MAXN]; // 标记顶点是否已经处理过
void dijkstra(int start, int n) {
// 初始化
for (int i = 1; i <= n; i++) {
dist[i] = INF;
visited[i] = false;
}
dist[start] = 0;
// 使用优先队列优化,pair的first是距离,second是顶点编号
// 小顶堆,距离小的优先出队
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
pq.push({0, start});
while (!pq.empty()) {
int u = pq.top().second;
pq.pop();
if (visited[u]) continue; // 如果已经处理过,跳过
visited[u] = true;
// 更新u的所有邻居
for (const Edge& e : graph[u]) {
int v = e.to;
int weight = e.weight;
if (dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
pq.push({dist[v], v});
}
}
}
}
时间复杂度分析
- 使用邻接矩阵:O(V²)
- 使用优先队列优化的邻接表:O((V+E)log V)
应用场景
- 路径规划
- 网络路由算法
- 任何需要找到最小代价路径的问题
Bellman-Ford算法
【算法】Bellman-Ford算法也用于解决单源最短路径问题,但其可以处理带有负权边的图,还能检测负权环。
算法思想
- 初始化dist[source]=0,其他dist[v]=∞
- 对所有边进行V-1次松弛操作(因为最短路径最多包含V-1条边)
- 再对所有边进行一次松弛操作,如果还有更新,说明存在负权环
代码实现
struct Edge {
int from, to, weight;
};
vector<Edge> edges; // 存储所有边
int dist[MAXN]; // 存储从源点到各点的最短距离
// 返回true表示没有负环,false表示存在负环
bool bellmanFord(int start, int n, int m) {
// 初始化
for (int i = 1; i <= n; i++) {
dist[i] = INF;
}
dist[start] = 0;
// 进行n-1次松弛
for (int i = 1; i <= n - 1; i++) {
bool updated = false;
// 对每条边进行松弛操作
for (const Edge& e : edges) {
if (dist[e.from] != INF && dist[e.from] + e.weight < dist[e.to]) {
dist[e.to] = dist[e.from] + e.weight;
updated = true;
}
}
// 如果这一轮没有更新,提前退出
if (!updated) break;
}
// 检测负环
for (const Edge& e : edges) {
if (dist[e.from] != INF && dist[e.from] + e.weight < dist[e.to]) {
// 存在负环
return false;
}
}
return true; // 不存在负环
}
时间复杂度分析
- 时间复杂度:O(VE),其中V是顶点数,E是边数
SPFA算法
SPFA(Shortest Path Faster Algorithm)是Bellman-Ford的队列优化版本,平均复杂度为O(kE),k一般很小。
bool spfa(int start, int n) {
// 初始化
for (int i = 1; i <= n; i++) {
dist[i] = INF;
inQueue[i] = false;
count[i] = 0; // 记录顶点入队次数,用于判断负环
}
dist[start] = 0;
queue<int> q;
q.push(start);
inQueue[start] = true;
count[start]++;
while (!q.empty()) {
int u = q.front();
q.pop();
inQueue[u] = false;
// 更新u的所有邻居
for (const Edge& e : graph[u]) {
int v = e.to;
int w = e.weight;
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
if (!inQueue[v]) {
q.push(v);
inQueue[v] = true;
count[v]++;
// 如果一个顶点入队次数超过n,说明存在负环
if (count[v] > n) {
return false; // 存在负环
}
}
}
}
}
return true; // 不存在负环
}
Floyd-Warshall算法
【算法】Floyd-Warshall算法用于解决多源最短路径问题,即求出图中任意两点之间的最短路径。
算法思想
- 通过三重循环,考虑对于每一对顶点(i,j),是否存在一个顶点k,使得从i到k再到j的路径比已知的从i到j的路径更短
- 不断更新这些中间路径,最终得到任意两点间的最短路径
代码实现
const int MAXN = 505;
const int INF = 0x3f3f3f3f;
int dist[MAXN][MAXN]; // 存储任意两点间的最短距离
void floyd(int n) {
// 初始化,dist[i][j]已经包含了直接连接的边
// 核心算法:考虑所有可能的中转点
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (dist[i][k] != INF && dist[k][j] != INF) {
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
}
}
}
}
时间复杂度分析
- 时间复杂度:O(V³)
- 空间复杂度:O(V²)
应用场景
- 需要求解所有点对之间最短路径的场景
- 图的顶点数不是很大(小于1000)的情况
- 传递闭包问题
各算法对比与选择
算法 | 时间复杂度 | 适用情况 | 优点 | 缺点 |
---|---|---|---|---|
Dijkstra | O((V+E)log V) | 无负权边的单源最短路 | 效率高 | 不能处理负权边 |
Bellman-Ford | O(VE) | 可能有负权边的单源最短路 | 可检测负权环 | 效率较低 |
SPFA | 平均O(kE) | 可能有负权边的单源最短路 | 一般情况下比Bellman-Ford快 | 最坏情况下仍是O(VE) |
Floyd-Warshall | O(V³) | 多源最短路 | 代码简洁,可处理负权边 | 顶点数大时效率低 |
实战技巧
- 当图没有负权边时,优先使用Dijkstra算法
- 当顶点数较小(<1000)且需要求所有点对的最短路径时,使用Floyd-Warshall
- 当存在负权边且顶点数较大时,使用SPFA算法
- 检测负权环时,使用Bellman-Ford或SPFA算法
常见问题与优化
- 路径记录:通过添加prev数组记录前驱节点,可以重建最短路径
- 处理无穷大:使用合适的INF值,避免整数溢出
- 多起点:对于多个起点的问题,可以添加一个超级源点
- 启发式优化:针对特定问题,可以结合启发式算法(如A*)
练习题目推荐
- POJ 3259 Wormholes (负环检测)
- POJ 1502 MPI Maelstrom (Dijkstra)
- POJ 3660 Cow Contest (Floyd传递闭包)
- Codeforces 20C Dijkstra? (路径重建)
通过这些练习题,你可以深入理解和掌握不同最短路径算法的实际应用。