ACM - 最短路 - AcWing 851 spfa求最短路
题解
以此题为例介绍一下图论中的最短路算法 - 算法。算法的步骤和正确性证明参考文章最短路径(Bellman-Ford算法)
松弛函数
对边集合 中任意边, 表示顶点 到顶点 的边的权值,用 表示当前从起点 出发到顶点 的最短距离。
若存在边 ,权值为 ,使得:
则更新 值
松弛函数作用:判断经过某个顶点,或者某条边,是否可以缩短起点到终点的当前最短距离(路径权值)。
松弛函数迭代
以对边集合 中每条边执行一次迭代函数作为一次迭代(称为松弛函数迭代),我们可以判断,只需经过有限次迭代,即可确保计算出起点到每个顶点的最短距离。
以下图作为示例示例演示松弛函数迭代过程。
为了凸显出过程的特点,我们直接在最坏情况下分析。
用 表示起点 到顶点 的当前最短距离,以 表示顶点 到顶点 的全局最短距离,集合 表示当前已找到全局最短距离的顶点集。
迭代前,分别设置为
第一次迭代
- 对边 执行松弛函数,则
- 对边 执行松弛函数,则
- 对边 执行松弛函数,则
- 对边 执行松弛函数,则
- 对边 执行松弛函数,则
第一轮迭代,有三条边起到了松弛效果,直观的可以看出 ,此时有一个顶点获得了全局最短距离,。
第二次迭代
- 对边 执行松弛函数,则
- 对边 执行松弛函数,则
- 对边 执行松弛函数,则
- 对边 执行松弛函数,则
- 对边 执行松弛函数,则
第一轮迭代,有三条边起到了松弛效果,直观的可以看出 ,此时有一个顶点获得了全局最短距离,。
第三次迭代
- 对边 执行松弛函数,则
- 对边 执行松弛函数,则
- 对边 执行松弛函数,则
- 对边 执行松弛函数,则
- 对边 执行松弛函数,则
第一轮迭代,有三条边起到了松弛效果,直观的可以看出 ,此时有一个顶点获得了全局最短距离,。
迭代次数(算法正确性)
对于顶点 ,若此时有 ,即顶点 的全局最短距离已被确定,则称顶点 为已确定顶点。若 ,则需要将顶点 加入到集合 中。
有下面定理:
若一个图中存在未确认顶点,则对边集合的一次松弛迭代后,会增加至少一个已确认顶点。具体地说,至少增加的已确认顶点为 。
定理证明
令
-
为图的顶点集;
-
为已找到全局最短距离的顶点集;
-
为未找到全局最短距离的顶点集。
初始情形
初始情况下,只有顶点 属于已确定顶点(), 此时有两种情况:起点 不存在相邻顶点;起点 存在相邻顶点。对于第一种情况,此时已找到全局最短距离,算法结束。对于第二种情况,我们证明,在对边集合进行一次松弛迭代后,必定会增加至少一个已确认顶点。
对于第二种情形,初始
第一轮松弛迭代后,令 ,则 是起点 的相邻顶点(所有非相邻顶点在第一轮迭代后当前最短距离都仍为 )。此时 ,我们说 此时达到了全局最短距离,否则若 没有达到全局最短距离,存在一条路径更短(下式),其中 为相邻顶点。
相比较而言,当前最短距离 对应的路径为
- 若 ,则 ,路径 的权值小于路径 的权值,说明路径 权值为负,即图中存在负权回路,矛盾。如下图所示
- 若 ,对于路径 中的 部分,由于对于任意相邻顶点 ,存在 ,所以有 ,导出矛盾。
一般情形
已确认顶点集合为 ,进行一次松弛迭代后,令 ,则 是集合 中某个点的相邻顶点(所有非相邻顶点此时当前最短距离都仍为 ,更详细的证明用归纳法)。
我们说 此时达到了全局最短距离,否则若 没有达到全局最短距离,存在一条路径更短(下式),其中 。
相比较而言,当前最短距离 对应的路径为下式,其中 , 为 到 的最短路径,归纳法易知 路径经过的所有点 ,有 。
对于 ,有两种情况: 是集合 中某个点的相邻顶点,由于 ,故 的路径权值必定比 的路径权值要大,矛盾; 不是集合 中某个点的相邻顶点,此时 ,矛盾。
故当前最短距离 达到了全局最短距离。
证毕。
程序设计
为方便展示,用 Python 代码实现松弛函数:
# distance 列表存储从起点到当前顶点的路径权值
# parent 列表存储到当前顶点的前驱顶点下标值。
# 初始 distance 列表和parent 列表元素皆为 None,表示路径权值无穷大
def releax(edge, distance, parent):
if distance[edge.begin - 1] == None:
pass
elif distance[edge.end - 1] == None or distance[edge.end - 1] > distance[edge.begin - 1] + edge.weight:
distance[edge.end - 1] = distance[edge.begin - 1] + edge.weight
parent[edge.end - 1] = edge.begin - 1
return True
return False
利用松弛函数实现 算法。
# times 变量用于记录迭代的执行次数
# edges 列表是存储边的集合
# getEdgesFromAdjacencyList 函数用于从邻接矩阵中转换出边的集合
def bellman_ford(graph, start, end):
distance, parent = [None for i in range(graph.number)], [None for i in range(graph.number)]
times, distance[start - 1], edges = 1, 0, getEdgesFromAdjacencyList(graph)
while times < graph.number:
for i in range(len(edges)):
releax(edges[i], distance, parent)
times += 1
for i in range(len(edges)):
if releax(edges[i], distance, parent):
return False
return True
算法优化
对于 算法的一种常见实现是使用队列优化。在中国的 IO 届也常称该算法为 算法。
在 算法介绍过程中我们会发现,有些边的松弛操作没有必要,我们可以证明如下结论:
一条边执行了松弛操作,当且仅当这条边的起点的当前最短距离之前被更新了。
在迭代过程中,每次取出队列的首节点,遍历该节点的所有出边,判断每条出边是否执行了松弛操作,若该边执行了松弛,则说明边对应的终点的当前最短距离 被更新了,就将该点压入到队列 中。
若存在负权回路,则该迭代过程会是一个死循环,但我们可以通过负权回路的特点来判断负权回路从而终止循环。负权回路的判断见另一篇文章。
算法正确性证明
在一轮取节点——遍历出边结束后,我们会发现此时图的所有边只有两种:
-
松弛一定无效的边
-
松弛可能有效的边
当前队列若为:
对图中任意一条边,它一定有起点和终点,起点若在队列内,说明该边为松弛可能有效的边;起点若不在队列内,说明该边为松弛一定无效的边。
因此,当退出循环时,退出的判定条件为队列为空,说明了此时所有边都一定是松弛一定无效的边,这是我们说所有点的 都达到的最短路径。否则存在至少一点没有达到最短路径,即对于该点存在一条更短的路径,而更短路径的存在说明松弛可能有效的边也一定存在,矛盾。
符号说明一下,设原路径为
更短路径为
则此时必有
即说明了松弛有效的边的存在性。
我们还需证明当无负权回路时,循环一定终止。反证法,若循环为死循环,则我们断言存在一点 ,它在队列 中出现了无穷多次,则 被松弛了无穷多次。由于以 为终点的边只有有限条,而松弛了 点的边有无限条,因此负权回路必定存在,矛盾,循环的终止性成立。
综上, 算法正确。
队列优化算法的程序设计
实现代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<vector>
#include<queue>
#define PII pair<int, int>
#define INF 0x3f3f3f3f
using namespace std;
const int N = 100010;
int n, m, q;
vector<PII> E[N]; // 用邻接表存储边
int dist[N]; // 第i号点距离源点的当前最短距离
bool vis[N]; // vis数组存的是当前结点是否在队列中
int spfa()
{
// 初始化
memset(dist, 0x3f, sizeof(dist));
dist[1] = 0;
queue<int> q;
q.push(1);
vis[1] = true;
// 队列优化的 Bellman-Ford 算法
while (!q.empty()) {
// 弹出队列首节点
int t = q.front();
q.pop();
// 首节点弹出,vis更新
vis[t] = false;
// 遍历首节点的所有出边
for (int i = 0, len = E[t].size(); i < len; ++i) {
int j = E[t][i].first, k = E[t][i].second;
if (dist[j] > dist[t] + k) {
dist[j] = dist[t] + k;
if (!vis[j]) {
vis[j] = true;
q.push(j);
}
}
}
}
return dist[n];
}
int main()
{
cin >> n >> m;
int u, v, w;
for (int i = 0; i < m; ++i) {
cin >> u >> v >> w;
E[u].push_back({ v, w });
}
int tmp = spfa();
if (tmp >= INF) cout << "impossible" << endl;
else cout << tmp << endl;
return 0;
}
最短路算法对比
算法 | - | ||
---|---|---|---|
空间复杂度 | |||
时间复杂度 | 看具体实现 | ||
负权边时是否可以处理 | 可以 | 不能 | 可以 |
判断是否存在负权回路 | 不能 | 不能 | 可以 |
其中 表示图的顶点数, 表示图的边数。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)