最短路问题目录
最短路问题分类
最短路:多源最短路、单源最短路、边权为1(相同)的最短路问题
单源最短路:以是否有负边权为区分
边权都为正 -> 朴素的 Dijkstra 算法(n ^ 2) 稠密图 --> 邻接矩阵
堆优化的 Dijkstra 算法 稀疏图 --> 邻接表
存在负权边 -> Bellman-Ford 算法(O(k * m)) --> 任意方式存储
spfa 算法(O(m) ~ O(n * m)) --> 邻接表
多源最短路:Floyd 算法 O(n ^ 3)
边权相同的多源、单源最短路可以采用队列来实现
Dijkstra 算法求最短路
Dijkstra 算法用于求解边权为正的最短路问题
朴素版 Dijkstra 算法求最短路
使用朴素版本的 Dijkstra 算法,针对稠密图(m > nlogn),m 为边数,n 为点数
建稠密图:使用 邻接矩阵 存储 ,初始将矩阵全部初始化为正无穷,然后根据题目要求建图,邻接矩阵中保留每条有向边的最短长度 g[i][j] = min(g[i][j], len)
时间复杂度:O(n ^ 2)
循环 n 次,每次确定一个点到起点的最短距离:
- 从所有没有确定最短路的点中选择一个距离起点最近的点 t
- 使用这个点 t 更新其他点距离起点的最短距离:
dist[j] = min(dist[j], dist[t] + g[t][j])
代码易错点:st[1]
不能提前改为 true,只能在循环里改
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510;
int g[N][N];
int dist[N]; // 从 1 号点走到 i 号点的距离
bool st[N]; // 记录每个点是是否确定了最短路
// 当 dist[i] 对应的 st[i] 为 true 时, 则 dist 里存的就是 i 到起点的最短距离
int n, m;
int Dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for(int i = 0; i < n; i++) // 循环 n 次, 每次找到一个最短路
{
int t = -1;
// 从所有没有确定最短路的点中选择一个dist 最小的
for(int j = 1; j <= n; j++)
if(!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
st[t] = true; // 这个点就确定了最短路
for(int j = 1; j <= n; j++) // 用 1->t + g[t][j] 更新 1->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);
// 因为 memset 是对字节操作, 因此这时 0x3f 对于整数相当于 0x3f3f3f3f
while(m--)
{
int a, b, c;
cin >> a >> b >> c;
g[a][b] = min(g[a][b], c); // 保证在有重边的情况下,录入最少的
}
cout << Dijkstra() << endl;
return 0;
}
堆优化版 Dijkstra 算法求最短路
堆优化版的 Dijkstra 算法,针对稀疏图(边较少,m < nlogn)
建稀疏图:使用 邻接表 存储,邻接表存储不用考虑重边和自环对结果的影响
时间复杂度:O(m logn)*
定义一个小根堆堆 priority_queue<PII, vector<PII>, greater<PII>>
PII 是 pair<int, int>
的重定义,映射为:(first) 距离 -> 节点编号 (second)
先将起点加入堆,开始循环,并取堆顶元素,拿到编号和距离,起点的 first 肯定是最短距离,遍历此 first 能达到的所有点,并更新他们的 dist 数组,并将这些新得出的距离->点
的序列对依次加入堆,取堆顶元素 (此时堆顶元素的 first 就是没有确定最短路的点中,距离最短的那一个,如果没有被访问过的话,就可以将这个元素的 first 变为从起点到 second(序号) 的最短路),然后就重复前面的操作,每次遍历新的最短节点下一步能到达的所有节点,并依据最新确定节点的最短路距离更新这些点的 dist 数组…
最终的 dist[n]
就是从起点到节点 n 的最短距离
代码易错点:
判断是否需要更新的时候, 需要将所有被更新后的点都加入到堆中
if(st[ver]) continue; // 当发现这个点已经被遍历过了, 就直接进行 continue
st[ver] = true; // 这两句必须有, 不然就可能会超时
if(distance + w[i] < dist[j]) // 这里是加 `w[i]`, 因为距离和两个点之间的距离有关 w[idx] = c
#include <iostream>
#include <cstring>
#include <queue>
#include <algorithm>
using namespace std;
const int N = 150010;
typedef pair<int, int> PII;
int e[N], ne[N], h[N], idx; // 建邻接表
int w[N]; // 存邻接表边的长度
int dist[N]; // 从 1 号点走到 n 号点的距离
bool st[N]; // 记录每个点是是否确定了最短路
int n, m;
void add(int a, int b, int c) // 建邻接表
{
e[idx] = b, w[idx] = c;
ne[idx] = h[a];
h[a] = idx;
idx ++;
}
int Dijkstra()
{
priority_queue<PII, vector<PII>, greater<PII>> Heap; // 建小堆
memset(dist, 0x3f, sizeof dist); // 初始化
dist[1] = 0;
Heap.push({
0, 1}); // 到起点的距离 --> 节点编号
while(Heap.size() > 0)
{
auto t = Heap.top();
Heap.pop();
int distance = t.first, ver = t.second;
if(st[ver]) continue; // 已经被访问过了, 直接跳过
st[ver] = true; // 将这个点标记为 “最短”
for(int i = h[ver]; i != -1; i = ne[i])
{
int j = e[i];
if(dist[j] > distance + w[i]) // 判断是否需要更新
{
dist[j] = distance + w[i];
Heap.push({
dist[j], j}); // 更新并加入优先级队列
}
}
}
if(dist[n] == 0x3f3f3f3f) return -1;
else return dist[n];
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
while(m--)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c); // 建立有向边
}
cout << Dijkstra() << endl;
return 0;
}
带负权边的最短路
带负权边的最短路可以使用 Belllman-ford 算法和 spfa 算法来求解。
Bellman-ford 算法求带负权边的最短路
Bellman-ford 算法使用于:带负权边,有边数限制 的最短路
但如果没有边数限制,应选择效率更优的 spfa 算法
时间复杂度:O(k * M),k 为题目限制的边数,M 为边数
从起点开始,循环 k 次,每次遍历每条边,更新距离数组 dist
循环 k 次,所求的 dist 数组的含义为:为从起点开始,经过不超过 k 条边,走到每个点的最短距离
如果迭代了 n 次,第 n 次还更新了的话,说明存在负环边(因为第 n 次如果还更新了的话,就说明一共经过了 n 条边,意味着有 n + 1个点,但只有 1-n 个点,说明有环,而这个环还是更新过的,说明有负权环)
代码易错点:
dist[b] = min(dist[b], backup[a] + w);
,更新 dist[b]
的时候是在 dist[b]
和 backup[a] + w
之间去更新的,而不是 backup[b]
和 backup[a] + w
if(ans > 0x3f3f3f3f / 2) cout << "impossible";
,也许最后一个点的最短值被更新后,依然是争取穷,但已经不等于 0x3f3f3f3f
了,比它略小一点
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10010;
struct g
{
int a, b, c;
}edgs[N];
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++) //循环 k 次
{
memcpy(backup, dist, sizeof dist);
for(int j = 0; j < m; j++) // m 条边
{
int a = edgs[j].a, b = edgs[j].b, w = edgs[j].c;
dist[b] = min(dist[b], backup[a] + w);
}
}
return dist[n];
}
int main()
{
cin >> n >> m >> k;
for(int i = 0; i < m; i++)
{
int a, b, c;
cin >> a >> b >> c;
edgs[i] = {
a, b, c};
}
int ans = Bellman_ford();
if(ans > 0x3f3f3f3f / 2) cout << "impossible";
else cout << ans;
return 0;
}
spfa算法 求最短路
spfa 算法是对 Bellman-ford 算法做了优化
时间复杂度:一般为 O(m),最坏为 O(nm)
在遍历每条边后,后续并不是所有边都需要遍历,只有前面更新过的节点,才能作为最短路来更新其他节点,因此每次只需要将更新过的节点加入到队列,每次取队头节点作为最短路来更新节点(更新邻接表中它作为队头所能到达的所有节点,如果更新成功并且那个节点不在队列中,就放入队列)
当队列为空时,就更新完了所有节点!
代码易错点:st 数组标志的是某个节点是否在队列中,当将一个节点加入到队列和 pop 出队列后,都要及时修改 st 数组
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 100010;
int n, m;
int e[N], ne[N], w[N], h[N], idx;
int dist[N];
int st[N]; // 记录节点是否在队列中
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c;
ne[idx] = h[a];
h[a] = idx;
idx ++;
}
int spfa()
{
memset(dist, 0x3f3f3f3f, sizeof dist); // 初始化 dist 数组, 保证每次更新正确
dist[1] = 0;
queue<int> q;
q.push(1);
st[1] = true;
while(q.size())
{
int t = q.front();
q.pop();
st[t] = false; // 节点出队列后, 就要马上改为 false
for(int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if(dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i]; // 更新
if(!st[j]) // 只有不在队列中时, 才将其加入队列
{
st[j] = true;
q.push(j);
}
}
}
}
return dist[n];
}
int main()
{
memset(h, -1, sizeof h);
cin >> n >> m;
while(m--)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
int ans = spfa();
if(ans > 0x3f3f3f3f / 2) cout << "impossible";
else cout << ans;
return 0;
}
spfa 算法判断负权回路
可以适用 spfa 算法判断有向图中是否有负权回路
但相比于 spfa 在求带负权边最短路,它在求是否存在负权回路时,需要将所有点都加入到队列中,因为某些点可能带有负权回路,但它不能到终点或者和最短路无关。
改动方法:只需要多维护一个 cnt 数组,先初始化为 0,每当 dist[j]
被更新一次,就把对应的 cnt[j] ++
,当cnt 数组里的某个值大于等于 n 的时候,就说明出现了负权回路(抽屉原理)
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 100010;
int n, m;
int e[N], ne[N], w[N], h[N], idx;
int dist[N], cnt[N]; // dist 数组记录距离, cnt 数组记录边数
int st[N]; // 记录节点是否在队列中
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c;
ne[idx] = h[a];
h[a] = idx;
idx ++;
}
bool spfa()
{
// memset(dist, 0x3f3f3f3f, sizeof dist);
// dist[1] = 0; // 没有必要初始化了, 因为有负权边, dist[j] > dist[t] + w[i] 可以成立
queue<int> q;
for(int i = 1; i <= n; i++)
{
q.push(i);
st[i] = true;
}
while(q.size())
{
int t = q.front();
q.pop();
st[t] = false; // 节点出队列后, 就要马上改为 false
for(int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if(dist[j] > dist[t] + w[i])
{
dist[j] = dist[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()
{
memset(h, -1, sizeof h);
cin >> n >> m;
while(m--)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
bool ans = spfa();
if(ans) cout << "Yes" << endl;
else cout << "No" << endl;
return 0;
}
Floyd 算法求多源最短路
多源最短路(含负权),可以使用 Floyd 算法求某任意两个点之间的最短路
借助动态规划的思想,进行三层循环,在循环中持续更新 dist 数组,在三层循环结束后,dist[x][y]
中存的就是 x -> y 的最短路径
三层循环,k 在最外,i 次之,j 在最里面 ( 都是循环 n 次 )
宏观上记忆,i 到 j 的最短路,就等于 i 先到 k ,再从 k 到 j 的最短路之和
直接利用邻接矩阵,三层循环!
for(int k = 1; k <= n; k++) // 动态规划
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
{
if(i == j) d[i][j] = 0; // 如果有自环, 那么询问 i->i 时的距离就是 0
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
// 宏观上记忆, i->j == i->k + k->j
}
代码实现:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 210;
const int INF = 0x3f3f3f3f;
int n, m, k;
int d[N][N];
void floyd()
{
for(int k = 1; k <= n; k++) // 动态规划
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
{
if(i == j) d[i][j] = 0; // 如果有自环, 那么询问 i->i 时的距离就是 0
d[i][j] = min(d[i][j], d[i][k] + d[k][j]); // 宏观上记忆, i->j == i->k + k->j
}
}
int main()
{
cin >> n >> m >> k;
memset(d, INF, sizeof d);
while(m--)
{
int a, b, c;
cin >> a >> b >> c;
d[a][b] = min(d[a][b], c); // 保留最小的长度
}
floyd();
while(k--)
{
int x, y;
cin >> x >> y;
if(d[x][y] > INF / 2) cout << "impossible" << endl;
else cout << d[x][y] << endl;
}
return 0;
}
宽搜解决边权为1的最短路问题
只需要将起点加入队列,然后进行一次宽搜,即可将所有距起点的最短路记录在 d 数组里
如果起点有多个(多源最短路问题),那么只需要将所有起点初始加入到队列中即可!
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 100010;
int m, n;
int e[N], ne[N], d[N], idx, h[N];
void add(int a, int b) // 建立从 a --> b 的有向边
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
int bfs()
{
queue<int> q;
memset(d, -1, sizeof d); // 初始化距离数组全部为 -1, 并且如果等于 -1 表示未被遍历过
d[1] = 0; // 初始位置距离初始化为 0
q.push(1); // 宽搜模板
while(q.size() > 0)
{
int t = q.front();
q.pop();
for(int i = h[t]; i != -1; i = ne[i]) // 遍历图的方法
{
int j = e[i];
if(d[j] == -1)
{
q.push(j);
d[j] = d[t] + 1;
}
}
}
return d[n];
}
int main()
{
memset(h, -1, sizeof d); // *** 初始化所有的头节点!***
cin >> n >> m;
for(int i = 0; i < m; i ++)
{
int a, b;
cin >> a >> b;
add(a, b);
}
cout << bfs() << endl;
}