【笔记】最短路总结
最短路 #图论
UPD: 2024/02/20
添加 Dijkstra 的路径记录,上算法课的时候临时学的。
UPD: 2024/04/27
添加 Floyd 判断负环
算法描述#
对于一个图 ,找出一条从 到 的路径,使得上面的权值和最小。
Floyd#
多源最短路算法,就是 dp 的一种。容易实现,支持负权边,不支持负环,适用于无向图有向图,代码如下:
rep(i, 1, n) {
rep(j, 1, n) {
rep(k, 1, n) {
f[i][j][k] = min(f[i - 1][j][k], f[i - 1][j][i] + f[i - 1][i][k]);
}
}
}
显然 为 dp 数组,f[0][i][j]
表示方法如下:
- ,。
- 与 之间没有边,。
- 否则,为点 与点 的边的权值。
另外, 数组的首个下标可优化:
rep(k, 1, n) {
rep(i, 1, n) {
rep(j, 1, n) {
f[i][j] = min(f[i][j], f[i][k] + f[k][j]);
}
}
}
时间复杂度:。
空间复杂度: 或 。
Floyd 判断负环#
当 dp[i][i] < 0
时,有负环。
证明: 到 点的最短路小于 时,证明存在一个到自身小于 的最短路,也是环。
Dijkstra#
一种贪心算法,只适用于非负权边的单源最短路算法。
先定一个起点 ,并将点分为两个不同的集合: 表示已经确认最短路的点, 表示尚未确认最短路的电脑。设数组 ,初始化 ,其余均为 。
重复这些操作(直到 为空):
- 取出 中最短路长度最小的点加入 。
- 对新加点的出边执行一次松弛(即比较,对与这个出边 ,若 ,更新 。
时间复杂度 。
暴力算法:
struct edge {
int v, w;
}
vector<edge> e[N];
int d[N], vis[N]; // 注意访问过的点不能再去访问啦。
void dijkstra(int n, int s) {
memset(d, 127, sizeof(d));
dis[s] = 0;
rep(i, 1, n) {
int u = 0, INF = 1234567890;
rep(j, 1, n) {
if (!vis[j] && d[i] < INF) { // 没访问过且有最短路
INF = d[i];
u = j;
}
vis[u] = 1;
for (auto E : e[u]) {
// 这里如果 u = 0 其实默认已经干掉了
int v = E.v, w = E.w;
if (d[v] > d[u] + w) {
d[v] = d[u] + w;
// 松弛
}
}
}
}
}
Dijkstra 的路径记录#
记数组 path[v]
为结点 的前驱,在松弛时做更新,那有代码:
if (d[v] > d[u] + w) {
d[v] = d[u] + w;
path[v] = u; // 记录前驱
}
输出方法如下:
vct<int> res;
res.push_back(end);
int x = end;
res.push_back(x);
while (path[x]) {
res.push_back(path[x]);
x = path[x];
}
for (int i = path.size() - 1; i >= 0; i--) {
cout << path[i] << ' ';
}
这段作者临时补充,没经过代码验证,请大家不要直接使用。
Dijkstra 的优先队列优化#
观测到暴力代码有一个不足之处:它必须通过遍历来寻找 集合中最短路最小的,这里我们用优先队列优化:因为优先队列可以自行排序 集合,这样大大降低了复杂度:降到了 (因为优先队列堆排序也需要 时间)。
代码如下:
int head[N], vis[N], dis[N], n, m, s, tot;
struct edge {
int u, v, w, next;
}e[M];
struct node {
int w, now;
bool operator < (const node &x) const {
return w > x.w;
}
};
priority_queue<node> q;
void dijkstra() {
rep(i, 1, n) {
dis[i] = INF;
}
dis[s] = 0;
q.push({0, s});
while (q.size()) {
node frt = q.top(); q.pop();
int u = frt.now;
if (vis[u]) {
continue;
}
vis[u] = 1;
for (int i = head[u]; i; i = e[i].next) {
int v = e[i].v;
if (dis[v] > e[i].w + dis[u]) {
dis[v] = e[i].w + dis[u];
q.push({dis[v], v});
}
}
}
}
Dijkstra 正确性证明#
使用归纳法。
设 为 。
- 显然是最短路径。
- 设第 次加入的 为最短路径,那么第 次加入的 仍为最短路径。
- 反证,若 非最短路径。那必定存在另一条路径为最短的,设 为该路径中不在 的点,那么存在 ,dijkstra 第一个步骤为从 基于 数组选取最小的一个加入 集合,因此 。矛盾,得证。
Bellman-Ford#
和 Dijkstra 一样,也是基于 relax 完成的最短路算法。可以求出有负权的图的最短路,还可以判断最短路是否存在。
过程#
松弛操作不用再讲了,就是:
这么做为啥这不废话吗,想想就能明白, 与 连通的情况下就是将 已有的最短路与 以及 的和作比较。
与 Dijkstra 不同的是,Bellman-Ford 尝试对图上的每一条边做松弛,每过一轮循环,就对循环到的这个点的所有边尝试做一次松弛,直到松弛不了一点。
时间复杂度:考虑最坏情况,最坏情况你说能是啥?不就是每一次循环都会把所有边都给串上嘛!Bellman-Ford 最多进行 次松弛,循环是 次。复杂度就是 。
Bellman-Ford 判负环#
Bellman-Ford 还有一个神奇的用法:判负环。因为到了个负环这松弛操作就像永动机一样停不下来了。前面时间复杂度已经论证了松弛最多搞 轮,因此当最后所有都遍历完了还能松弛就代表有负环。
这样做其实不严谨,如果只是这样做,只能证明源点 无法抵达负环。最好是建一个点 ,然后把它和所有点连一条权值为 的边,再去跑 Bellman-Ford。像这样:
rep(i, 1, n) {
add(0, i, 0);
// void add(u, v, w);
}
Code#
Talk is cheap. Show me your code.
struct edge {
int u, v, w;
};
vector<edge> E;
int dis[N];
bool bellmanford(int n, int s) {
memset(dis, 0x3f, sizeof(dis));
// 不 memset 见祖宗
dis[s] = 0;
bool flg = 0;
rep(i, 1, n) {
flg = 0;
for (auto [u, v, w] : edge) {
if (dis[u] == INF) {
continue;
}
// 没最短路的松弛没啥用
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w; flg = 1; // 注意说明是可以松弛的,flg = 0 代表没法松弛了
flg = 1;
}
}
if (!flg) {
break;
}
// 无法松弛就 Over 了
}
return flg;
}
SPFA#
关于 SPFA,她死了;关于 NOIP,他死了;关于 CCF,它死了。
NOIP2019
SPFA 全称 Shortest Path Faster Algorithm。其实很多时候有些松弛相当于脱裤子放屁。能想到,上一次被松弛了的点,所连到的边,才能引起下一次松弛。
用队列来维护【哪些结点可能会引起松弛操作】,只访问有必要访问的边即可。
SPFA 也可以来判负环,对于任意一个点最多访问 轮,因此我们搞一个数组 ,每路过一个点 就让 等于 。如果哪次 了说明就有负环。直接掐掉。
SPFA 跑得确实很快,但数据经过构造可以退化成 ,也就是 Bellman-Ford。典型案例请辱骂某金钱收集组织。
Code
bool spfa() {
memset(hdis, 63, sizeof(hdis));
hdis[s] = 0, vis[s] = 1;
queue<int> q;
q.push(s);
while (q.size()) {
int u = q.front(); q.pop();
vis[u] = 0;
for (int i = head[u]; i; i = E[i].next) {
int v = E[i].v;
if (hdis[v] > hdis[u] + E[i].w) {
hdis[v] = hdis[u] + E[i].w;
if (!vis[v]) {
vis[v] = 1;
q.push(v);
tot[v]++;
if (tot[v] == n + 1) {
return false;
}
}
}
}
}
return true;
}
缝合怪 Johnson#
2024/01/17:™花了一天学 Johnson。最后发现数组开小了。
参考文献。原文写的更好,作者也是看原文学会哒。
因为太烦了实在不想单个单个写了,这里直接 copy 洛谷模板题:
【模板】全源最短路(Johnson)#
题目描述#
给定一个包含 个结点和 条带权边的有向图,求所有点对间的最短路径长度,一条路径的长度定义为这条路径上所有边的权值和。
注意:
-
边权可能为负,且图中可能存在重边和自环;
-
部分数据卡 轮 SPFA 算法。
输入格式#
第 行: 个整数 ,表示给定有向图的结点数量和有向边数量。
接下来 行:每行 个整数 ,表示有一条权值为 的有向边从编号为 的结点连向编号为 的结点。
输出格式#
若图中存在负环,输出仅一行 。
若图中不存在负环:
输出 行:令 为从 到 的最短路,在第 行输出 ,注意这个结果可能超过 int 存储范围。
如果不存在从 到 的路径,则 ;如果 ,则 。
样例 #1#
样例输入 #1
5 7
1 2 4
1 4 10
2 3 7
4 5 3
4 2 -2
3 4 -3
5 3 4
样例输出 #1
128
1000000072
999999978
1000000026
1000000014
样例 #2#
样例输入 #2
5 5
1 2 4
3 4 9
3 4 -3
4 5 3
5 3 -2
样例输出 #2
-1
提示#
【样例解释】
左图为样例 给出的有向图,最短路构成的答案矩阵为:
0 4 11 8 11
1000000000 0 7 4 7
1000000000 -5 0 -3 0
1000000000 -2 5 0 3
1000000000 -1 4 1 0
右图为样例 给出的有向图,红色标注的边构成了负环,注意给出的图不一定连通。
【数据范围】
对于 的数据,。
对于 的数据,,不存在负环(可用于验证 Floyd 正确性)
对于另外 的数据,(可用于验证 Dijkstra 正确性)
upd. 添加一组 Hack 数据:针对 SPFA 的 SLF 优化
解法#
求多源最短路最暴力的解法当之无愧属于 Floyd,复杂度 。
也可以 次 Bellman-Ford,复杂度 。SPFA 题目说了会温馨地帮您卡成 Bellman-Ford。Floyd 的 受不了,Bellman-Ford 的 更难受。
队列优化完的 Dijkstra 当然更优,所以我们选择先用 SPFA 来判负环(题面说要判负环),再跑 次 Dijkstra。
但是,™这道题有负权……
容易想到的方法是每个边权加个 ,事完再减去 。但其实这是个错误的方法,考虑下图(懒得画搬 StudyingFather 的):
的最短路 ,长度 。
Dijkstra,你走了我怎么活啊~~~
所以我们先建一个点 ,然后与其他所有点建边权为 的边。
这个工作我们要在跑 SPFA 之前完成,这里顺便再把 SPFA 跑了,注意 SPFA 这里源点要设为 。于是我们得到了 点到其它所有点的最短路,记为 。
这是如果 SPFA 能检查出负权边,那么直接掐掉输出即可。
接下来,对于每一条边 重新将 赋值为 。
接下来跑 轮队列优化过的 Dijkstra 即可。(厉害在队列优化)
时间复杂度:SPFA 退化后是 ,Dijkstra 是 (要跑 轮),合算还是 。无压力通过
证明#
在重新标记后的图上,设最短路依次经过的路径为 。这条路径表达式如下:
化简后得:
前面这个求和式就是最短路。后面这俩玩意儿是不变的。
另外你可能还想要我证明 修改后为非负,也好办:一条边重新标记后的边权为 ,根据松弛的公式可以发现 ,因此 ,所以 。
Code#
太 ™长了,注意这道题跑完 SPFA 以及每轮 Dijkstra 数组要清空,数组要开大,因为会有新边加进来。
#include <bits/stdc++.h>
#define rty printf("Yes\n");
#define RTY printf("YES\n");
#define rtn printf("No\n");
#define RTN printf("NO\n");
#define rep(v,b,e) for(int v=b;v<=e;v++)
#define repq(v,b,e) for(int v=b;v<e;v++)
#define rrep(v,e,b) for(int v=b;v>=e;v--)
#define rrepq(v,e,b) for(int v=b;v>e;v--)
#define stg string
#define vct vector
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
#define int ll
void solve() {
}
const int INF = 1e9;
const int N = 1e4 + 5, M = 1e4 + 5;
int head[N], cnt, vis[N], hdis[N], tot[N], dis[N], n, m;
struct edge {
int u, v, w, next;
}E[M];
struct node {
int w, now;
bool operator < (const node& x) const {
return w > x.w;
}
};
priority_queue<node> q;
void add(int u, int v, int w) {
E[++cnt] = {u, v, w, head[u]};
head[u] = cnt;
}
bool spfa() {
int s = 0;
memset(hdis, 63, sizeof(hdis));
hdis[s] = 0, vis[s] = 1;
queue<int> q;
q.push(s);
while (q.size()) {
int u = q.front(); q.pop();
vis[u] = 0;
for (int i = head[u]; i; i = E[i].next) {
int v = E[i].v;
if (hdis[v] > hdis[u] + E[i].w) {
hdis[v] = hdis[u] + E[i].w;
if (!vis[v]) {
vis[v] = 1;
q.push(v);
tot[v]++;
if (tot[v] == n + 1) {
return false;
}
}
}
}
}
return true;
}
void input() {
cin >> n >> m;
rep(i, 1, m) {
int u, v, w;
cin >> u >> v >> w;
add(u, v, w);
}
}
void add_edge() {
rep(i, 1, n) {
add(0, i, 0);
}
if (!spfa()) {
cout << "-1\n";
exit(0);
}
rep(u, 1, n) {
for (int i = head[u]; i; i = E[i].next) {
E[i].w += hdis[u] - hdis[E[i].v];
}
}
}
void dijkstra(int s) {
rep(i, 1, n) {
dis[i] = INF;
}
q.push({0, s});
memset(vis, 0, sizeof(vis));
dis[s] = 0;
while (q.size()) {
node frt = q.top(); q.pop();
int u = frt.now;
if (vis[u]) {
continue;
}
vis[u] = 1;
for (int i = head[u]; i; i = E[i].next) {
int v = E[i].v;
if (dis[v] > dis[u] + E[i].w) {
dis[v] = dis[u] + E[i].w;
if (!vis[v]) {
q.push({dis[v], v});
}
}
}
}
}
main() {
// int t; cin >> t; while (t--) solve();
input();
add_edge();
rep(i, 1, n) {
dijkstra(i);
int ans = 0;
rep(j, 1, n) {
if (dis[j] == INF) {
ans += j * INF;
} else {
ans += j * (dis[j] + hdis[j] - hdis[i]);
}
}
cout << ans << endl;
}
return 0;
}
作者:2044-space-elevator
出处:https://www.cnblogs.com/2044-space-elevator/articles/17970148
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战