最短路

两顶点之间什么时候存在最短路呢?

存在负权回路不一定没有最短路,只要最短路不经过负权回路即可。
单源最短路
Dijkstra 算法
问题提出:给定一个带权有向图(可以有环)
算法:基于贪心法的Dijkstra算法。注意该算法无法处理带有负权边的图。
朴素 Dijkstra 算法思想
说明:
源点 - 辅助数组
。 表示当前找到的 的最短路径的长度。 - 集合
为已求得最短路径的终点集合
算法描述:
- 初始化
和 - 求出顶点
并加入集合 : 满足 - 更新与
相连且不在 中的顶点的最短路径长度: - 若
,算法结束,否则重复 2、3

模板习题
给定一个
输入格式
第一行包含整数
输出格式
输出一个整数,表示
数据范围
输入样例
3 3
1 2 2
2 3 1
1 3 4输出样例
3参考代码
#include <bits/stdc++.h>
using namespace std;
const int N = 510, INF = 0x3f3f3f3f;
int g[N][N]; // 思考:朴素版适用于稠密图,稠密图用什么存储?邻接矩阵
int s[N];
int dist[N];
int n, m;
int dijkstra(int st, int ed) { // st 源点 ed 需要输出最短路径长度的顶点
memset(dist, 0x3f, sizeof dist); // 除远点外所有顶点的 dist 值为无穷大
dist[st] = 0; // 源点的 dist 值为 0
// for (int i = 1; i <= n; i++) { // s != v 即循环 n 遍求出源点到 n 个点的最短路径
for (int i = 1; i < n; i++) { // s != v 即循环 n - 1 遍求出源点到 n - 1 个点的最短路径(源点已经求过了)
int k = -1; // 求满足条件的顶点 k
for (int j = 1; j <= n; j++) {
if (!s[j] && (k == -1 || dist[j] < dist[k])) k = j;
}
s[k] = 1; // 顶点 k 加入 s
for (int j = 1; j <= n; j++) { // 更新最短路径
if(!s[j] && g[k][j] != INF) dist[j] = min(dist[j], dist[k] + g[k][j]);
}
}
if (dist[ed] == INF) return -1;
else return dist[ed];
}
int main() {
cin >> n >> m;
memset(g, 0x3f, sizeof g); // 权值可能为 0 所以初始时选择无穷大
for (int i = 1; i <= m; i++) {
int a, b, w; cin >> a >> b >> w;
g[a][b] = min(g[a][b], w); // 可能有重边
}
cout << dijkstra(1, n) << endl;
return 0;
}思考:如何记录最短路径?
int path[N];
int dijkstra(int st, int ed) {
memset(dist, 0x3f, sizeof dist);
dist[st] = 0;
// 源点从 -1 走来
path[st] = -1;
for (int i = 1; i < n; i++) {
int k = -1;
for (int j = 1; j <= n; j++) {
if (!s[j] && (k == -1 || dist[j] < dist[k])) k = j;
}
s[k] = 1;
for (int j = 1; j <= n; j++) {
if(!s[j] && g[k][j] != INF && dist[j] > dist[k] + g[k][j]) {
dist[j] = dist[k] + g[k][j];
path[j] = k; // 更新路径 j 从何处走来
}
}
}
if (dist[ed] == INF) return -1;
for (int i = ed; i != -1; i = path[i]) {
if (path[i] == -1) cout << i << endl;
else cout << i << " <- ";
}
return dist[ed];
}堆优化版 Dijkstra
使用小根堆将寻找顶点
思考:稀疏图使用邻接表存储。
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
const int N = 2e5 + 10, INF = 0x3f3f3f3f;
int h[N], e[N], w[N], ne[N], idx;
int s[N];
int dist[N];
int n, m;
void init() {
memset(h, -1, sizeof h);
idx= 0;
}
void add(int a, int b, int c) { // 有重边也没事 算法保证正确
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx++;
}
int dijkstra(int st, int ed) {
memset(dist, 0x3f, sizeof dist);
dist[st] = 0;
priority_queue<PII, vector<PII>, greater<PII>> heap;
heap.push({dist[st], st}); // 先存距离 pair默认按 first 排序
while (heap.size()) {
auto k = heap.top();
heap.pop();
int v = k.second, dis = k.first;
if (s[v]) continue;
s[v] = 1;
for (int i = h[v]; i != -1; i = ne[i]) {
int j = e[i];
if (dist[j] > dis + w[i]) {
dist[j] = dis + w[i];
heap.push({dist[j], j});
}
}
}
if (dist[ed] == INF) return -1;
else return dist[ed];
}
int main() {
init();
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int a, b, c; cin >> a >> b >> c;
add(a, b, c);
}
cout << dijkstra(1, n) << endl;
return 0;
}为什么 Dijkstra 算法不能处理负权边?

如上图 dijkstra算法求得的最短路是
Bellman-Ford 算法
Bellman-Ford算法可以用来解决带有负权边的单源最短路问题,其基于动态规划思想,反复用已有的边更新最短路径的长度,该算法的核心思想便是松弛操作。
松弛:对于一条边
算法思想:对图中每条边进行
提示:每一次松弛操作其实际意义就是再多考虑一条边之后能得到的最短路。
算法描述:
- 初始化:
- 松弛:对所有边依次进行松弛操作,共
次
算法实现
// n 顶点数目
// m 边数目
// dist 记录源点到指定顶点的最小距离
// 因为算法只关注边,所以我们只存边
struct Edge {
int u, v, w;
}edges[M]; // M 最大边数
void bellman_ford(int st, int ed) { // st 源点 ed 需要输出最短路的点
memset(dist, 0x3f, sizeof dist);
dist[st] = 0;
for (int i = 1; i <= n; i++) { // 循环 n 遍是为了判断是否存在环
bool flag = false;
for (int j = 1; j <= m; j++) {
if (dist[edges[j].v] > dist[edges[j].u] + edges[j].w) {
dist[edges[j].v] = dist[edges[j].u] + edges[j].w;
flag = true;
}
}
if (!flag) break; // 本轮不存在距离更新,算法结束
if (i == n) {
cout << "图中存在负环" << endl;
return ;
}
}
// 特别注意这里的条件判断
if (dist[ed] > 0x3f3f3f3f / 2) cout << "st 到 ed 的最短路不存在" << endl;
else cout << "st -> ed 最短路为: " << dist[ed] << endl;
}有边数限制的最短路问题
在某种特定的情况下必须使用Bellman-Ford算法!那就是所求最短路是有边数限制的。在这种情况下我们还需要注意一个点:求解最短路时需要借助备份数组。
串联:
Bellman-Ford算法中的每次更新的意义是在多考虑 1 条边之后能得到的全局的最短路。而串联指的是一次更新之后考虑了不止一条边。由于使用了松弛操作,某节点的当前最短路依赖于其所有入度的节点的最短路。假如在代码中使用
dist[e.v]=min(dist[e.v],dist[e.u] + e.w);,我们无法保证dist[e.u]是否也在本次循环中被更新,如果被更新了,并且dist[e.v] > dist[e.u] + e.w,那么会造成当前节点在事实上“即考虑了一条从某个节点指向u的边,也考虑了u->v”,共两条边。而使用dist[e.v]=min(dist[e.v],last[e.u] + e.w);,可以保证u在dist更新后不影响对v的判定,因为后者使用last数组,保存着上一次循环中的dist的值。
问题:
给定一个 impossible。
输入样例:
3 3 1
1 2 1
2 3 1
1 3 3输出样例:
3参考代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 510, M = 10010;
struct Edge {
int u, v, w;
} edges[M];
int n, m, k;
int dist[N];
int last[N]; // 备份数组
void bellman_ford() {
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < k; i++) {
memcpy(last, dist, sizeof dist);
for (int j = 0; j < m; j++) {
auto e = edges[j];
dist[e.v] = min(dist[e.v], last[e.u] + e.w);
}
}
if (dist[n] > 0x3f3f3f3f / 2) puts("impossible");
else printf("%d\n", dist[n]);
}
int main() {
scanf("%d%d%d", &n, &m, &k);
for (int i = 0; i < m; i ++ ) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
edges[i] = {a, b, c};
}
bellman_ford();
return 0;
}SPFA 算法
在Bellman-Ford算法中,对于最短路径长度未曾更新过的顶点,不必松弛由它出发的边,因此可以使用一个队列来避免这种无效的松弛:该队列在初始状态下只包含源点,仅当某个顶点的最短路径长度更新后,将该顶点加入队列中。算法在每一轮去队首的顶点,并松弛其所关联的边,直到队列为空。
算法实现
// vis 数组记录顶点 i 是否在队列中
void spfa(int st, int ed) {
memset(dist, 0x3f, sizeof dist);
dist[st] = 0;
queue<int> q;
q.push(st);
vis[st] = 1;
while (!q.empty()) {
int k = q.front();
q.pop();
vis[k] = 0;
for (int i = h[k]; i != -1; i = ne[i]) {
int j = e[i];
if (dist[j] > dist[k] + w[i]) {
dist[j] = dist[k] + w[i];
if (!vis[j]) {
q.push(j);
vis[j] = 1;
}
}
}
}
if (dist[ed] == 0x3f3f3f3f) cout << "不存在最短路径" << endl;
else cout << dist[ed] << endl;
}SPFA 求最短路
给定一个 impossible。数据保证不存在负权回路。
输入格式
第一行包含整数
输出格式
输出一个整数,表示 impossible。
数据范围
图中涉及边长绝对值均不超过
输入样例
3 3
1 2 5
2 3 -3
1 3 4输出样例
2参考代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10, INF = 0x3f3f3f3f;
int h[N], e[N], w[N], ne[N], idx;
int vis[N];
int dist[N];
int n, m;
void init() {
memset(h, -1, sizeof h);
idx= 0;
}
void add(int a, int b, int c) {
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx++;
}
void spfa(int st, int ed) {
memset(dist, 0x3f, sizeof dist);
dist[st] = 0;
queue<int> q;
q.push(st);
vis[st] = 1;
while (!q.empty()) {
int k = q.front();
q.pop();
vis[k] = 0;
for (int i = h[k]; i != -1; i = ne[i]) {
int j = e[i];
if (dist[j] > dist[k] + w[i]) {
dist[j] = dist[k] + w[i];
if (!vis[j]) {
q.push(j);
vis[j] = 1;
}
}
}
}
if (dist[ed] == INF) cout << "impossible" << endl;
else cout << dist[ed] << endl;
}
int main() {
init();
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int a, b, c; cin >> a >> b >> c;
add(a, b, c);
}
spfa(1, n);
return 0;
}SPFA 判断负环
Bellman-Ford算法也可以判断负环,为什么不用它呢?时间复杂度太高!
思路:
设
cnt[u]表示从起点st到u的最短路径包含的边数,cnt[st] = 0。当执行更新dist[v] = dist[u] + w时,同样更新cnt[v] = cnt[u] + 1.此时若满足cnt[v] >= n,则图中有负环。若算法正常结束,则图中没有负环。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10, INF = 0x3f3f3f3f;
int h[N], e[N], w[N], ne[N], idx;
int vis[N];
int dist[N];
int cnt[N]; // 记录最短路经过的边数
int n, m;
void init() {
memset(h, -1, sizeof h);
idx= 0;
}
void add(int a, int b, int c) {
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx++;
}
bool spfa() {
queue<int> q;
for (int i = 1; i <= n; i++) {
q.push(i);
vis[i] = 1;
}
while (!q.empty()) {
int k = q.front();
q.pop();
vis[k] = 0;
for (int i = h[k]; i != -1; i = ne[i]) {
int j = e[i];
if (dist[j] > dist[k] + w[i]) {
dist[j] = dist[k] + w[i];
cnt[j] = cnt[k] + 1; // 从 k 到 j 经过边数 cnt[k] + 1
if (cnt[j] >= n) return true; // 存在负环
if (!vis[j]) {
q.push(j);
vis[j] = 1;
}
}
}
}
return false;
}
int main() {
init();
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int a, b, c; cin >> a >> b >> c;
add(a, b, c);
}
if (spfa()) cout << "Yes" << endl;
else cout << "No" << endl;
return 0;
}多源最短路
Floyd-Warshall 算法
算法思想
floyd算法的本质是动态规划。设图
的最短路径中间不经过顶点 : 。 的最短路径中间经过顶点 : 。
状态计算:
优化降维:
算法流程:
- 初始化(依题意)
( 之间无边) ( 之间有边)
- 利用状态转移方程计算结果
void floyd() {
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]);
}
}
}
}Floyd 求最短路
问题描述
给定一个 impossible。数据保证图中不存在负权回路。
输入格式
第一行包含三个整数
输出格式
共 impossible。
数据范围
图中涉及边长绝对值均不超过
输入样例
3 3 2
1 2 1
2 3 2
1 3 1
2 1
1 3输出样例
impossible
1参考代码
#include <bits/stdc++.h>
using namespace std;
const int N = 210, INF = 0x3f3f3f3f;
int d[N][N];
int n, m, q;
void floyd() {
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]);
}
}
}
}
int main() {
cin >> n >> m >> q;
// 初始化邻接矩阵
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (i == j) d[i][j] = 0;
else d[i][j] = INF;
}
}
for (int i = 1; i <= m; i++) {
int a, b, c; cin >> a >> b >> c;
d[a][b] = min(d[a][b], c); // 可能有重边,取边权最小的即可
}
floyd();
while (q--) {
int a, b; cin >> a >> b;
// 注意这里的条件判断
if (d[a][b] > INF / 2) cout << "impossible" << endl;
else cout << d[a][b] << endl;
}
return 0;
}