常见的最短路模型
源点: 起点
汇点: 终点
单源最短路: 从1个点到其他所有点的最短路, 比如求1号点到第n号点的最短路
多源汇最短路 : 起点和终点不确定
最 短 路 { 单 源 最 短 路 { 所 有 边 权 重 为 正 数 { 朴 素 D i j k s t r a O ( n 2 ) 比 较 适 合 稠 密 图 , 边 数 m = n 2 比 较 多 堆 优 化 版 D i j k s t r a O ( m l o g n ) 稀 疏 图 , m = n ≤ 1 0 5 , n 2 会 超 时 , 用 堆 优 化 存 在 负 权 边 { B e l l m a n _ F o r d O ( n m ) n 是 点 数 , m 是 边 数 ( 经 过 不 超 过 k 条 边 , 只 能 用 此 方 法 ) S P F A 一 般 O ( m ) , 最 坏 O ( n m ) 多 源 汇 最 短 路 F l o y d 算 法 O ( n 3 ) 最短路\left\{\begin{matrix} 单源最短路\left\{\begin{matrix} 所有边权重为正数\left\{\begin{matrix} 朴素Dijkstra O(n^2) 比较适合稠密图, 边数m = n ^2比较多 \\ 堆优化版Dijkstra O(mlogn) 稀疏图, m = n \leq 10^5, n^2会超时,用堆优化 \end{matrix}\right. \\ 存在负权边\left\{\begin{matrix} Bellman\_Ford O(nm) n是点数,m是边数(经过不超过k条边,只能用此方法) \\ SPFA 一般O(m), 最坏O(nm) \end{matrix}\right. \end{matrix}\right. \\ 多源汇最短路 Floyd算法 O(n^3) \end{matrix}\right. 最短路⎩⎪⎪⎪⎪⎨⎪⎪⎪⎪⎧单源最短路⎩⎪⎪⎨⎪⎪⎧所有边权重为正数{
朴素Dijkstra O(n2) 比较适合稠密图,边数m=n2比较多堆优化版Dijkstra O(mlogn) 稀疏图,m=n≤105,n2会超时,用堆优化存在负权边{
Bellman_Ford O(nm) n是点数,m是边数(经过不超过k条边,只能用此方法) SPFA 一般O(m),最坏O(nm) 多源汇最短路 Floyd算法 O(n3)
最短路 比较难的 是建图
各个算法的原理简介
dijkstra 原理是 贪心
floyd 基于动态规划
bellman_ford 基于离散数学的知识
Dijkstra 算法
用来解决 单源最短路问题
朴素版Dijkstra
s集合 : 当前已经确定最短距离的点
1.dist[1] = 0, dist[i] = INF;
2.for (int i = 0; i < n; i ++ ){
// 每次循环出队一个元素,可以确定一个点的最短距离
// 循环n次, 可以确定n个点的最短距离
t <- 找到不在s中的最短距离的点
s <- t (t加入到s集合中)
用t更新其他所有点的距离(用t的出边去更新其他所有点的距离,
比如t->x, 看下1~x的距离能否用 1~t + t~x的距离更新
)即: dist[x] > dist[t] + w 的话, dist[x] = dist[t] + w;
}
例子
绿颜色表示确定了最短路的点
红颜色表示距离待定的点
初始状态:
第1次 发现2号点和3号点到1的距离可以更新
第二次寻找不在s
中的最短距离的点, 2号点确定为绿色
用2来更新出边到1的距离, 可以将3号点的距离更新成3, 此时这轮迭代结束
下轮迭代只剩一个点, 可以直接确定.
时间复杂度O(n^2)
外层循环n
次, 内层循环找不在s
中的距离最近的点n
次, 用t更新其他点n
次.
总共n^2
AcWing 849. Dijkstra求最短路 I
分析
裸题
正权自环, 显然不会出现在最短路中,忽略
重边, 直接对g[a][b] = min(g[a][b], c);
找不在s
中距离最短的点
int dijkstra(){
for (int i = 0; i < n; i ++ ){
int t = -1;
for (int j = 1; j <= n; j ++ )
if (!st[j] && (t == - 1 || dist[t] > dist[j])) t = j;
// !st[j] 不在s中
// t == -1表示第一次, dist[t] > dist[j] 找最小
...
st[t] = true; // 标记成绿色
}
}
代码
#include <iostream>
#include <cstring>
using namespace std;
const int N = 510, M = 10010;
int g[N][N];
int n, m;
bool st[N];
int dist[N];
int dijkstra(){
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
//st[1] = true; 一定不能加这句话.
for (int i = 0; i < n; i ++ ){
// 找不在s中最短距离的点
int t = -1;
for (int j = 1; j <= n; j ++ ){
if (!st[j] && (t == -1 || dist[t] > dist[j])) t = j; // 注意 !st[j] && ( || )
}
st[t] = true;
for (int j = 1; j <= n; j ++ )
dist[j] = min(dist[j], dist[t] + g[t][j]);
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
int main(){
cin >> n >> m;
memset(g, 0x3f, sizeof g);
while (m -- ){
int a, b, c;
cin >> a >> b >> c;
g[a][b] = min(g[a][b], c);
}
int t = dijkstra();
cout << t << endl;
return 0;
}
堆优化版dijkstra
如果是个稀疏图, n = 1 0 5 n = 10^5 n=105, n 2 n^2 n2时间必爆
优化
找不在s
中的点, 外部for循环n
次, 找点n
次,总共n^2s = t, 每次取1个点, 外部循环
n次, 总共n 用t更新其他点的距离, 外部循环
n次, 总共
m`条边, 因此是m.
所以第一步可以用堆来找最小值使得
每次找最小值O(1).
第二步总共仍然是n
而第三步在堆中寻找元素时间复杂度O(logn)
, 总共修改m
次, 所以总共 m l o g n mlogn mlogn次
因此堆优化dijkstra时间复杂为O(mlogn)
实现堆的方式
手写
麻烦.
STL
STL不支持修改堆中元素操作, 堆中可能存在冗余.
比如存了dist[1] = 10, 又存了dist[1] = 15;
直接用st
数组判重即可, 如果当前出队的元素, 已经确定为绿色,直接扔掉
AcWing 850. Dijkstra求最短路 II
分析
堆优化dijkstra 裸题
代码
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 1e6 + 10;
typedef pair<int, int> PII;
int h[N], e[N], w[N], ne[N], idx;
int n, m;
int dist[N];
bool st[N];
void add(int a, int b, int c){
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
int dijkstra(){
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
priority_queue<PII, vector<PII>, greater<PII>> heap;
heap.push({
0, 1});
while (heap.size()){
auto t = heap.top(); heap.pop();
int ver = t.second, distance = t.first;
if (st[ver]) continue;
st[ver] = true;
for (int i = h[ver]; ~i; i = ne[i]){
int j = e[i];
if (dist[j] > dist[ver] + w[i]){
dist[j] = dist[ver] + w[i];
heap.push({
dist[j], j});
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
int main(){
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m -- ){
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
cout << dijkstra() << endl;
return 0;
}
Bellman-Ford 算法
for
循环n
次 一定要对原来的距离备份,防止串连
for 所有边 a, b, w. a ⟶ w b a\stackrel{w}{\longrightarrow}b a⟶wb (从a到b的边,权重为w)(边的存储方式不一定写成邻接表, 可以是结构体)
dist[b] = min(dist[b], dist[a] + w);
串联问题(备份)
比如截图中右上角的问题
边权重1-2 权重1, 2-3 权重1, 1-3权重3, 求1到3 最多经过1条边的最短路.
应该是3.
但是如果不备份
点 1 2 3
dist 0 ∞ \infty ∞ ∞ \infty ∞
for
循环第1次
第二层循环(假如先枚举1-2这条边)
2号点距离更新成1
点 1 2 3
dist 0 1 ∞ \infty ∞
然后再枚举2-3(这时2到1的距离已经发生变化),因此3-1的距离就变成dist[2] + w[2,3] = 1 + 1 = 2
点 1 2 3
dist 0 1 2
虽然最外层只迭代了1
次, 但更新过程中发生了串联, 将已经更新点,又拿来作为条件,更新后面的点.
如何不发生串联呢
保证更新的时候只用最外层循环for
上一次的结果, 即:在最外层循环中+备份
使用上一次备份更新, 那么3的距离变成 ∞ + 1 \infty + 1 ∞+1,不会变成2
负权回路
图中存在负权回路的话,最短路不一定存在
可以求出图中是否存在负权回路
最外层循环次数的意义, 比如迭代k
次, 表示经过不超过k条边的最短距离,
如果第n
次迭代的时候,又更新了某些边, 说明:存在一条最短路, 这条最短路上的边数 >= n
.
n条边的话,意味这这条路径上有n + 1
个点, 因为总的点数只有n
,因此这条路径上必定有两个点相同,即:这条路径上必定存在环.
路径上存在环, 并且可以更新当前点的距离,那么一定是负环.
因此,第n
迭代的时候,有更新的话,说明存在边数为n
的最短路径,说明存在负环
一般找负环用spfa
,这里值是提及下,bellman-ford
可以用来找负环.
时间复杂度O(nm)
两重循环,外层n次,内层循环所有边数m
有负权回路有些情况可以求得最短路
比如2号点的负权回路,不在最短路径上. 但是, spfa算法要求图中任何点,不能包含负环
AcWing 853. 有边数限制的最短路
分析
题目说了边权可能为负数,因此一定不能用dijkstra
题目要求最多经过k条边, 有负环也无所谓了
代码
if (dist[n] > 0x3f3f3f3f / 2 ) return -1;
(1号点当前不能到5号点)
(1号点当前也不能到n号点)
但是5号点能更新1号点到5号点的距离
dist[n] <= dist[5] + w[5, n]
n号点,最终可能到不了,但是比正无穷小一点点,也算到不了
代码
#include <iostream>
#include <cstring>
using namespace std;
const int N = 510, M = 100010 * 2 + 10;
struct Edge{
int a, b, w;
}edge[M];
int n, m, k;
int dist[N], backup[N];
int bellman_ford(){
memset(dist, 0x3f, sizeof dist);
dist[1] = 0; // 注意初始化
for (int i = 0; i < k; i ++ ){
memcpy(backup, dist, sizeof dist);
for (int j = 0; j < m; j ++ ){
int a = edge[j].a, b = edge[j].b, c = edge[j].w;
if (dist[b] > backup[a] + c) // 一定要是 backup[a] + c
dist[b] = backup[a] + c;
}
}
if (dist[n] > 0x3f3f3f3f / 2) return -1;
return dist[n];
}
int main(){
cin >> n >> m >> k;
for (int i = 0; i < m; i ++ ){
scanf("%d%d%d", &edge[i].a, &edge[i].b, &edge[i].w);
}
int t = bellman_ford();
if (t == -1) puts("impossible");
else cout << t << endl;
return 0;
}
SPFA 算法
只要没有负环,就可以用
spfa
是对bellman_ford
算法的优化
如果被spfa
被出题人卡了,只能换堆优化版dikstra
因为每次进行dist[b] = min(dist[b], dist[a] + w)
不一定会更新距离, spfa
就是对这一步进行优化.
只有当dist[a]
变小了,dist[b]
才会变小.
优化方式
用宽搜进行优化
队列里存的就是能让dist[b]
变小的节点,比如上图中的a
queue ⟵ \longleftarrow ⟵ 1(起点)
while queue 不空
1. t ⟵ \longleftarrow ⟵ q.front(); q.pop();
2. 更新下t的所有出边, t ⟶ w b t\stackrel{w}{\longrightarrow}b t⟶wb (t即上图中的a
, 因为t变小,所以出边的距离b
才会变小)
queue ⟵ \longleftarrow ⟵ b (如果队列中有b的话,就不用重复加入了)
基本思路: 更新过谁,让再拿更新过的点去更新出边, 只有我变小了,我的出边才会变小.
时间复杂度 最坏情况下 O ( n m ) O(nm) O(nm)
因为是对bellman_ford
算法的改进, 最坏情况下 O ( n m ) O(nm) O(nm), 相当于队列优化没起任何作用
AcWing 851. spfa求最短路
分析
spfa
代码与dijkstra
相似, 直接copy过来,改下dijkstra
函数即可
代码
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 1e6 + 10;
int h[N], e[N], w[N], ne[N], idx;
int n, m;
int dist[N];
bool st[N];
typedef pair<int, int> PII;
void add(int a, int b, int c){
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
int spfa(){
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
queue<int> q;
q.push(1);
st[1] = true; // st数组表示当前点是否在队列中
while (q.size()){
auto t = q.front(); q.pop();
st[t] = false;
for (int i = h[t]; ~i; i = ne[i]){
int j = e[i];
if (dist[j] > dist[t] + w[i]){
dist[j] = dist[t] + w[i];
if (!st[j]){
// 只有当j不在队列中才加入, 防止重复加入j
q.push(j);
st[j] = true;
}
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
int main(){
cin >> n >> m;
memset(h, -1, sizeof h);
while (m -- ){
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
int t = spfa();
if (t == -1) puts("impossible");
else cout << t << endl;
return 0;
}
SPFA求负环
dist[x]:当前1~x的最短距离
cnt[x]:当前最短路的边数
每次更新的时候
dist[x] = dist[t] + w[i];
cnt[x] = cnt[t] + 1; // 从1到t的边 + 1条边
如果cnt[x] >= n, 意味着当前最短路经过n
条边, 那么一定有n + 1
个点, 而图中最多才n
个点, 一定有两个点相同,且出现在最短路径中, 比如i
出现两次, 即 路径中存在环, 并且能更新距离, 所以是负环
AcWing 852. spfa判断负环
分析
直接将spfa
算法,稍加改进即可.
可以删掉memset(dist, 0x3f, sizeof dist); dist[1] = 0
, 因为求的不是距离的绝对值,而是距离之间的相对值.
队列开始的时候, 需要将所有点加入进来(不仅仅是1号点), 因为题目判断的是是否存在负环, 不是是否存在以1为起点的负环
加入
int spfa(){
for (int i = 1; i <= n; i ++) {
q.push(i);
st[i] = true;
}
while (q.size()){
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n) return true;
.....
}
return false;
}
int main(){
...
if (spfa()) puts("YES");
else puts("NO");
}
代码
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 1e6 + 10;
int h[N], e[N], w[N], ne[N], idx;
int n, m;
int d[N];
bool st[N];// st数组表示当前的点是否在队列中,如果在队列中就不需要再入队了
int cnt[N];// 记录更新次数
void add(int a, int b, int c){
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
int spfa(){
queue<int> q;
for (int i = 1; i <= n; i ++ ){
// 加入所有点, 因为题目是判断所有点开始是否有负数环
st[i] = true;
q.push(i);
}
while (q.size()){
int t = q.front(); q.pop();
st[t] = false;//出队标记为false
for (int i = h[t]; ~i; i = ne[i]){
int j = e[i];
if (d[j] > d[t] + w[i]){
d[j] = d[t] + w[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n) return true;
if (!st[j]){
st[j] = true;
q.push(j);
}
}
}
}
return false;
}
int main(){
cin >> n >> m;
memset(h, -1, sizeof h);
while (m -- ){
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
int t = spfa();
if (spfa()) puts("Yes");
else puts("No");
return 0;
}
Floyd 算法
用来求多源汇最短路
用邻接矩阵来存储d[i, j], 表示i 到 j的距离
for (int k = 1; k <= n; k ++ )
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++)
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
原理(动态规划)
k表示阶段 d[k, i, j]
d[k, i, j] = d[k - 1, i, k] + d[k - 1, k , j];
// 当只经过1 ~ k - 1这些点的时候, 加上第k个点,
// 那么就是先从i ~ k + k ~ j(i, j表示1 ~ k - 1中的点)
// 可以发现第1维 没什么用, 可以去掉
d[i, j] = d[i, k] + d[k, j];
时间复杂度O(n^3)
AcWing 854. Floyd求最短路
分析
裸题
代码
#include <iostream>
#include <cstring>
using namespace std;
const int N = 210, M = 200010, INF = 0x3f3f3f3f;
int dist[N][N];
int n, m, Q;
int floyd(){
for (int k = 1; k <= n; k ++ )
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++ )
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
int main(){
cin >> n >> m >> Q;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++ )
if (i != j) dist[i][j] = INF;
else dist[i][j] = 0;
for (int i = 0; i < m; i ++ ){
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
dist[a][b] = min(dist[a][b], c);
}
floyd();
while (Q -- ){
int a, b;
cin >> a >> b;
int t = dist[a][b];
if (t > INF / 2) puts("impossible");
else printf("%d\n", t);
}
return 0;
}
if(d[a][b] > INF / 2)
a 与 b不存在通路的情况下, INF会被更新, 可能会比INF小点.
初始化问题
因为多源汇问题, 题目会询问1到1的距离, 所以不能用memset(dist, 0x3f, sizeof dist);
去初始化所有点的距离