【置顶】最短路总结
cnblogs 一个人也没有,快快前来!
算法描述
对于一个图
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]);
}
}
}
显然 f[0][i][j]
表示方法如下:
-
-
- 否则,为点
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]);
}
}
}
时间复杂度:
空间复杂度:
Dijkstra
一种贪心算法,只适用于非负权边的单源最短路算法。
先定一个起点
重复这些操作(直到
- 取出
T 中最短路长度最小的点加入S 。 - 对新加点的出边执行一次松弛(即比较,对与这个出边
(u,v,w) ,若d_v>d_u+w ,更新d_v 。 时间复杂度O(n^2) 。
暴力算法:
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 的优先队列优化
观测到暴力代码有一个不足之处:它必须通过遍历来寻找
代码如下:
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 正确性证明
其实我自己也不会,感谢某知乎网友的文章。
使用归纳法。
设
-
- 设第
k 次加入的v_{x_k} 为最短路径,那么第k+1 次加入的v_{x_{k+1}} 仍为最短路径。 - 反证,若
v_{x_{k+1}} 非最短路径。那必定存在另一条路径为最短的,设v_u 为该路径中不在S 的点,那么存在d_{v_u}<d_{v_{x_{k+1}}} ,dijkstra 第一个步骤为从T 基于d 数组选取最小的一个加入S 集合,因此d_{v_{x_{k+1}}} \le d_{v_u} 。矛盾,得证。
Bellman-Ford
和 Dijkstra 一样,也是基于 relax 完成的最短路算法。可以求出有负权的图的最短路,还可以判断最短路是否存在。
本段摘自 OI-WIKI。
过程
松弛操作不用再讲了,就是:
这么做为啥这不废话吗,想想就能明白,
与 Dijkstra 不同的是,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 跑得确实很快,但数据经过构造可以退化成
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)
题目描述
给定一个包含
注意:
-
边权可能为负,且图中可能存在重边和自环;
-
部分数据卡
n 轮 SPFA 算法。
输入格式
第
接下来
输出格式
若图中存在负环,输出仅一行
若图中不存在负环:
输出
如果不存在从
样例 #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
右图为样例
【数据范围】
对于
对于
对于另外
upd. 添加一组 Hack 数据:针对 SPFA 的 SLF 优化
解法
求多源最短路最暴力的解法当之无愧属于 Floyd,复杂度
也可以 SPFA 题目说了会温馨地帮您卡成 Bellman-Ford。Floyd 的 Bellman-Ford 的
队列优化完的 Dijkstra 当然更优,所以我们选择先用 SPFA 来判负环(题面说要判负环),再跑
但是,™这道题有负权……
容易想到的方法是每个边权加个