最短路
给定一张图,边上有边权(无权图可以用 BFS 求最短路),定义一条路径的长度为这条路径上经过的边的边权之和,两点间的最短路即为经过的边的边权之和最小的路径。
- 多源(全源):要对每一个点作为起点的情况,都做最短路,要求出任意两个点之间的最短路。
- 单源:固定起点,只求这个起点到其他点的最短路。
最短路是图论中最经典的模型之一,在生活中也有很多应用。例如,城市之间有许多高速公路相连接,想从一个地方去另一个地方,有很多种路径,如何选择一条最短路的路径就是最基本的最短路问题。有时候问题会更加复杂,加上别的限制条件,例如某些城市拥有服务区,连续开车达到一定的时间就必须要进服务区休息,或者是某些路段在特定的时间内会堵车,需要绕行等等,这需要更加灵活地使用最短路算法来解决这些问题。
除了解决给定图的最短路问题,最短路模型还可以解决许多看起来不是图的问题。如果解决一个问题的最佳方案的过程中涉及很多状态,这些状态是明确的且数量合适,而且状态之间可以转移,可以考虑建立图论模型,使用最短路算法求解。
Floyd
基于 DP 的思想,设
其中初始化为
for (int k = 1; k <= n; k++) for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) dp[k][i][j] = min(dp[k - 1][i][j], dis[k - 1][i][k] + dis[k - 1][k][j]);
一般写的时候第一维会优化掉,可以直接在邻接矩阵上做转移。代码非常简洁,三层循环,但要注意,最外层一定是中间点
g 是邻接矩阵 for (int k = 1; k <= n; k++) for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) g[i][j] = min(g[i][j], g[i][k] + g[k][j]);
三层循环一定是
时间复杂度为
例题:P2910 [USACO08OPEN] Clear And Present Danger S
题意:给定一张
数据范围:
参考代码
#include <cstdio> #include <algorithm> using namespace std; const int N = 105; const int M = 10005; int a[M], dis[N][N]; int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= m; i++) scanf("%d", &a[i]); for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) scanf("%d", &dis[i][j]); for (int k = 1; k <= n; k++) for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]); int ans = 0; for (int i = 1; i < m; i++) ans += dis[a[i]][a[i + 1]]; printf("%d\n", ans); return 0; }
例题:P1119 灾后重建
询问
解题思路
Floyd 算法本身就是加点过程,最外层循环就是在加点,至于加点的顺序,不一定非要按编号的顺序,像这道题就希望按照重建完成的时间加点。
可以将询问离线,按询问的
加点的时候要注意
最后要注意,Floyd 加点是往路径中间加,所以在回答询问时还要判断起点和终点是否已经重建完成,如果没完成,输出
参考代码
#include <cstdio> #include <algorithm> const int N = 205; const int INF = 1e9; int tm[N], g[N][N]; bool ok[N]; int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 0; i < n; i++) scanf("%d", &tm[i]); for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) g[i][j] = i == j ? 0 : INF; for (int i = 1; i <= m; i++) { int x, y, w; scanf("%d%d%d", &x, &y, &w); g[x][y] = g[y][x] = w; } int q; scanf("%d", &q); int idx = 0; for (int id = 1; id <= q; id++) { int x, y, t; scanf("%d%d%d", &x, &y, &t); while (idx < n && tm[idx] <= t) { for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) g[i][j] = std::min(g[i][j], g[i][idx] + g[idx][j]); ok[idx] = true; idx++; } printf("%d\n", !ok[x] || !ok[y] || g[x][y] == INF ? -1 : g[x][y]); } return 0; }
拓展:加边 Floyd
刚开始给一个
解题思路
针对每一次加的边,枚举
时间复杂度为
例题:P1613 跑路
解题思路
很明显,直接走最短路不对,如果
应该建一个图:如果两个点之间
问题变成:怎么知道两个点之间能不能
原图的边权都是
用一个 bool
数组 long long
上界
if (f[i][k][x - 1] && f[k][j][x - 1]) { f[i][j][x] = g[i][j] = true; }
最后在新的图上计算
参考代码
#include <cstdio> #include <queue> const int N = 55; bool f[N][N][63], g[N][N]; int ans[N]; int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= m; i++) { int x, y; scanf("%d%d", &x, &y); g[x][y] = true; f[x][y][0] = true; } for (int t = 1; t < 63; t++) { for (int k = 1; k <= n; k++) { for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) { if (f[i][k][t - 1] && f[k][j][t - 1]) { f[i][j][t] = g[i][j] = true; } } } } std::queue<int> q; for (int i = 1; i <= n; i++) ans[i] = -1; q.push(1); ans[1] = 0; while (!q.empty()) { int u = q.front(); q.pop(); if (u == n) break; for (int v = 1; v <= n; v++) if (g[u][v] && ans[v] == -1) { ans[v] = ans[u] + 1; q.push(v); } } printf("%d\n", ans[n]); return 0; }
例题:P1730 最小密度路径
解题思路
经过的边数最多会是多少?因为是无环图,所以任何一条路径的长度都不超过
所以可以枚举路径长度,用
一条路径可以拆成两段,枚举中间点
假设计算经过
所以实际上不用考虑分配长度,只需要用
对于每个询问,遍历不同边数下的最短路计算最小密度。
参考代码
#include <cstdio> #include <algorithm> const int N = 55; const int INF = 1e9; int g[N][N][N]; // g[x][y][z]表示x到y经过z条边的情况下最短路 bool f[N][N][N]; int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) { for (int k = 1; k <= n; k++) g[i][j][k] = i == j ? 0 : INF; } for (int i = 1; i <= m; i++) { int a, b, w; scanf("%d%d%d", &a, &b, &w); g[a][b][1] = std::min(g[a][b][1], w); // 注意重边 } for (int cnt = 2; cnt <= n; cnt++) { for (int k = 1; k <= n; k++) for (int i = 1; i <= n; i++) { if (i == k) continue; for (int j = 1; j <= n; j++) { if (j == k) continue; g[i][j][cnt] = std::min(g[i][j][cnt], g[i][k][cnt - 1] + g[k][j][1]); } } } int q; scanf("%d", &q); for (int i = 1; i <= q; i++) { int x, y; scanf("%d%d", &x, &y); double ans = INF; for (int cnt = 1; cnt <= n; cnt++) if (g[x][y][cnt] != INF) // 有总共cnt条边的路径 ans = std::min(ans, 1.0 * g[x][y][cnt] / cnt); if (ans == INF) printf("OMG!\n"); else printf("%.3f\n", ans); } return 0; }
例题:P1841 [JSOI2007] 重要的城市
判定一个点是否是某两个点之间最短路的必经点(删了这个点,存在其他点
解题思路
Floyd 算法可以算出任意两点距离
更进一步,如果经过
设
if (dis[i][k] + dis[k][j] < dis[i][j]) f[i][j] = f[i][k] * f[k][j] // 在i到j的最短路上只经过编号<=k的点的方案数 else if (dis[i][k] + dis[k][j] == dis[i][j]) f[i][j] += f[i][k] * f[k][j]
但是
参考代码
#include <cstdio> using ll = long long; const int N = 205; const int INF = 1e9; int dis[N][N]; ll f[N][N]; bool key[N]; int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) dis[i][j] = i == j ? 0 : INF; for (int i = 1; i <= m; i++) { int u, v, w; scanf("%d%d%d", &u, &v, &w); dis[u][v] = dis[v][u] = w; f[u][v] = f[v][u] = 1; } for (int k = 1; k <= n; k++) for (int i = 1; i <= n; i++) { if (i == k) continue; for (int j = 1; j <= n; j++) { if (j == k) continue; if (dis[i][k] + dis[k][j] < dis[i][j]) { dis[i][j] = dis[i][k] + dis[k][j]; f[i][j] = f[i][k] * f[k][j]; } else if (dis[i][j] == dis[i][k] + dis[k][j]) { f[i][j] += f[i][k] * f[k][j]; } } } for (int k = 1; k <= n; k++) { for (int i = 1; i <= n; i++) { if (i == k) continue; for (int j = 1; j <= n; j++) { if (j == k) continue; if (dis[i][k] + dis[k][j] == dis[i][j] && f[i][k] * f[k][j] == f[i][j]) key[k] = true; } } } int cnt = 0; for (int i = 1; i <= n; i++) if (key[i]) { printf("%d ", i); cnt++; } if (cnt == 0) printf("No important cities.\n"); return 0; }
拓展:求
解题思路
考虑 Floyd 算法在添加一个点
设
if (dis[i][k] + dis[k][j] < dis[i][j]) { // i->j目前的最短路经过的点的最大编号一定是k key[i][j].clear(); key[i][j].push_back(k); } else if (dis[i][k] + dis[k][j] == dis[i][j]) { // i->j目前的最短路经过的点的最大编号可能是以前存下来的,也可能是k key[i][j].push_back(k); }
对于一对点
- 如果
只记了一个值,那这个记下来的就是重要城市 - 如果记了不止一个,可以都不考虑
为什么可以不考虑?如果删除记下来的最大的点,这个点肯定没用;如果删除的是记下来的点里边编号小的点,并且这个点确实是重要城市,它也一定会在
因此 vector
)。
设
若
在 Floyd 过程中,如果
注意
最后对于
参考代码
#include <cstdio> const int N = 205; const int INF = 1e9; int dis[N][N], key[N][N]; bool f[N]; int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) dis[i][j] = i == j ? 0 : INF; for (int i = 1; i <= m; i++) { int u, v, w; scanf("%d%d%d", &u, &v, &w); dis[u][v] = dis[v][u] = w; } for (int k = 1; k <= n; k++) for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) { if (dis[i][k] + dis[k][j] < dis[i][j]) { dis[i][j] = dis[i][k] + dis[k][j]; key[i][j] = k; } else if (i != k && k != j && dis[i][k] + dis[k][j] == dis[i][j]) { key[i][j] = 0; } } int cnt = 0; for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) if (key[i][j] > 0) { cnt++; f[key[i][j]] = true; } if (cnt == 0) printf("No important cities.\n"); else for (int i = 1; i <= n; i++) if (f[i]) printf("%d ", i); return 0; }
另一种方法:
对于每个起点
枚举边
只考虑这些有用的边,会构成一个怎样的图?由于最短路上一定没环(无环),
找到入度为
参考代码
#include <cstdio> #include <algorithm> const int N = 205; const int INF = 1e9; int g[N][N], dis[N][N], ind[N], pre[N]; bool key[N]; int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) { dis[i][j] = i == j ? 0 : INF; g[i][j] = dis[i][j]; } for (int i = 1; i <= m; i++) { int u, v, w; scanf("%d%d%d", &u, &v, &w); g[u][v] = g[v][u] = w; dis[u][v] = dis[v][u] = w; } // Floyd for (int k = 1; k <= n; k++) for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) dis[i][j] = std::min(dis[i][j], dis[i][k] + dis[k][j]); for (int i = 1; i <= n; i++) { // 枚举起点 for (int u = 1; u <= n; u++) ind[u] = pre[u] = 0; for (int u = 1; u <= n; u++) { for (int v = 1; v <= n; v++) { if (u == v) continue; if (dis[i][u] + g[u][v] == dis[i][v]) { ind[v]++; pre[v] = u; } } } for (int u = 1; u <= n; u++) { if (ind[u] == 1 && pre[u] != i) key[pre[u]] = true; } } int cnt = 0; for (int i = 1; i <= n; i++) if (key[i]) { printf("%d ", i); cnt++; } if (cnt == 0) printf("No important cities.\n"); return 0; }
Dijkstra
Dijkstra 算法用于处理单源最短路问题,并且边权不能有负的。
例题:P3371 【模板】单源最短路径(弱化版)
题意:给定一个
数据范围:
分析:最简单的想法是,使用搜索来寻找所有可能的路径并比较它们的长度,选取最短的路径作为最短路。然而,由于可能的路径太多,时间复杂度是指数级别的,无法在规定时间内运行完毕。因此需要使用最短路算法,从起点开始尝试往外走,不断用已知点的最短路长度来更新其他点的最短路长度。
用一个
- 在初始时刻,先将整个
数组设为 - 对于一条从
到 ,长度为 的边,如果 ,那么就可以将 的值变成 ,因为这代表着从 走到 经过这条边再走到 是一条未发现过的,且比原先的最优路径还要短的路径。这个操作称为松弛。假设 ,那么 且不会再变化。于是就可以用 号点的所有出边来进行松弛并将 标记,代表这个点的 值将不再变化 - 可以发现
号点的 值是当前未标记的点中最小的。由于从 走来的边已经全部完成松弛,且 和 都大于 (这意味着从 号点走到 号点的边都无法成功松弛),所以此时的 就是从 到 的最短路。接着就可以用与 号点相连的所有边进行松弛并标记 - 同样地,发现
号点的 是当前未标记的点中最小的,它的 值不会再改变,接着用 号点进行松弛并标记 - 最后,用
号点进行松弛并标记得到最终结果
这种求最短路的方法是 Dijkstra 算法,其过程可以概括为:
- 将起始点的
置为 - 选择当前未标记的点中
值最小的一个 - 对该点的所有连边依次进行松弛操作
- 对该点进行标记
- 重复第二步至第四步,直到不存在一条从已标记点通往未标记点的连边
这个算法的核心在于每次取出来的最小的
可以用反证法证明:假设有一个点
参考代码
#include <cstdio> #include <vector> using namespace std; const int N = 10005; const int INF = 2147483647; struct Edge { int to, w; }; vector<Edge> g[N]; int dis[N]; bool vis[N]; int main() { int n, m, s; scanf("%d%d%d", &n, &m, &s); while (m--) { int u, v, w; scanf("%d%d%d", &u, &v, &w); g[u].push_back({v, w}); } for (int i = 0; i <= n; i++) dis[i] = INF; dis[s] = 0; while (true) { int u = 0; for (int i = 1; i <= n; i++) if (!vis[i] && dis[i] < dis[u]) u = i; if (u == 0) break; vis[u] = true; for (Edge e : g[u]) { int to = e.to, w = e.w; if (dis[u] + w < dis[to]) dis[to] = dis[u] + w; } } for (int i = 1; i <= n; i++) printf("%d%c", dis[i], i == n ? '\n' : ' '); return 0; }
上述算法的时间复杂度是
例题:P4779 【模板】单源最短路径(标准版)
本题的
需要对 Dijkstra 算法加一点优化:使用一个小根堆来维护
为什么在堆顶取出时,要验证其是否已被标记?如果在更新的时候,更新了一个已经被放进优先队列的点(也就是优先队列里有它更新前的 vis
被标记时,这一次取出的信息不用来更新其它点的最短路,直接 continue
。这是一种惰性删除的思想,因为优先队列没法删除非堆顶的元素。
每个点会扫一次边,所以扫边的循环是
参考代码
#include <cstdio> #include <vector> #include <queue> using namespace std; const int N = 100005; const int INF = 2147483647; struct Edge { int to, w; }; vector<Edge> g[N]; int dis[N]; bool vis[N]; struct Node { int i, d; }; struct NodeCmp { bool operator()(const Node& a, const Node& b) const { return a.d > b.d; } }; priority_queue<Node, vector<Node>, NodeCmp> q; int main() { int n, m, s; scanf("%d%d%d", &n, &m, &s); while (m--) { int u, v, w; scanf("%d%d%d", &u, &v, &w); g[u].push_back({v, w}); } for (int i = 1; i <= n; i++) dis[i] = INF; dis[s] = 0; q.push({s, 0}); while (!q.empty()) { int u = q.top().i; q.pop(); if (vis[u]) continue; vis[u] = true; for (Edge e : g[u]) { int to = e.to, w = e.w; if (dis[u] + w < dis[to]) { dis[to] = dis[u] + w; q.push({to, dis[to]}); } } } for (int i = 1; i <= n; i++) printf("%d%c", dis[i], i == n ? '\n' : ' '); return 0; }
拓展:vis
数组是在弹出时标记还是放入时标记?
分析
容易发现,当
例题:P1629 邮递员送信
题意:给定一张
数据范围:
解题思路
由于快递员必须每次都要从
参考代码
#include <cstdio> #include <vector> #include <algorithm> using namespace std; const int N = 2005; const int INF = 1e9; int dis[N]; bool vis[N]; struct Edge { int to, w; }; vector<Edge> g[N]; void update(int u) { vis[u] = true; for (Edge e : g[u]) { int to = e.to, w = e.w; dis[to] = min(dis[to], dis[u] + w); } } int main() { int n, m; scanf("%d%d", &n, &m); while (m--) { int u, v, w; scanf("%d%d%d", &u, &v, &w); g[u].push_back({v, w}); g[v + n].push_back({u + n, w}); } for (int i = 0; i <= n * 2; i++) dis[i] = INF; dis[1] = 0; while (true) { int u = 0; for (int i = 1; i <= n; i++) if (!vis[i] && dis[i] < dis[u]) u = i; if (u == 0) break; update(u); } dis[n + 1] = 0; while (true) { int u = 0; for (int i = n + 1; i <= n * 2; i++) if (!vis[i] && dis[i] < dis[u]) u = i; if (u == 0) break; update(u); } int ans = 0; for (int i = 2; i <= n; i++) ans += dis[i] + dis[i + n]; printf("%d\n", ans); return 0; }
例题:P2176 [USACO11DEC] RoadBlock S / [USACO14FEB]Roadblock G/S
给定
解题思路
枚举每一条边,将其修改后再跑最短路。取这些情况中最短路的最大值减去一开始的最短路,即为答案,时间复杂度为
实际上,只有
如果有多条最短路,只考虑一条也够了。因为这一条路径上可以分成必经边和非必经边,必经边都考虑到了,而非必经边多考虑一下也不会造成影响。
怎么记录一条最短路?只需要在 Dijkstra 算法更新距离的时候,记录一下是谁造成的更新。跑出一条路径后,从
这样一来可以将时间复杂度优化到
参考代码
#include <cstdio> #include <algorithm> const int N = 105; const int INF = 1e9; int g[N][N], dis[N], pre[N], n; bool vis[N]; void dijkstra(bool rec) { for (int i = 1; i <= n; i++) { dis[i] = INF; vis[i] = false; } dis[1] = 0; while (true) { int u = -1; for (int i = 1; i <= n; i++) if (!vis[i] && (u == -1 || dis[i] < dis[u])) u = i; if (u == -1) break; vis[u] = true; for (int i = 1; i <= n; i++) if (dis[u] + g[u][i] < dis[i]) { dis[i] = dis[u] + g[u][i]; if (rec) pre[i] = u; } } } int main() { int m; scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) g[i][j] = INF; g[i][i] = 0; } for (int i = 1; i <= m; i++) { int a, b, l; scanf("%d%d%d", &a, &b, &l); g[a][b] = std::min(g[a][b], l); g[b][a] = std::min(g[b][a], l); } dijkstra(true); int tmp = dis[n]; int u = n, ans = 0; while (u != 1) { // 把边权翻倍 g[pre[u]][u] *= 2; g[u][pre[u]] *= 2; dijkstra(false); ans = std::max(ans, dis[n] - tmp); // 把图恢复原样 g[pre[u]][u] /= 2; g[u][pre[u]] /= 2; u = pre[u]; } printf("%d\n", ans); return 0; }
拓展:最短路树、最短路 DAG
记下来的这个路径形成的这个图是一种特殊图吗?如果连通的话是一棵树,起点是根,每个点的父亲是更新它最短距离的那个点。如果不连通的话就是森林(多棵树)。这被称为最短路树。
区分最短路树、最短路 DAG:
- 最短路树:从起点到每个点只记了一条最短路(有多条的时候根据实现的最短路算法相应记了一条)。
- 最短路 DAG:是在跑完最短路以后,留下所有
的边,这个时候如果有多条最短路,都会保留下来。
Floyd 算法其实也能记路径,就是记
例题:P7100 [W1] 团
解题思路
直接建图边太多了,并且有一些点之间是全都有边的。因此要把边的数量降下来。
考虑在每个集合
参考代码
#include <cstdio> #include <vector> #include <utility> #include <queue> using ll = long long; using node = std::pair<ll, ll>; const int N = 600005; const ll INF = 4557430888798830399ll; std::vector<node> g[N]; bool vis[N]; ll dis[N]; int main() { int n, k; scanf("%d%d", &n, &k); for (int i = 1; i <= k; i++) { int s; scanf("%d", &s); for (int j = 1; j <= s; j++) { int t, w; scanf("%d%d", &t, &w); g[t].push_back({n + i, w}); g[n + i].push_back({t, w}); } } for (int i = 1; i <= n + k; i++) dis[i] = INF; std::priority_queue<node, std::vector<node>, std::greater<node>> q; dis[1] = 0; q.push({0, 1}); while (!q.empty()) { node tmp = q.top(); q.pop(); int u = tmp.second; if (vis[u]) continue; vis[u] = true; for (node nd : g[u]) { int v = nd.first, w = nd.second; if (dis[u] + w < dis[v]) { dis[v] = dis[u] + w; q.push({dis[v], v}); } } } for (int i = 1; i <= n; i++) printf("%lld ", dis[i]); return 0; }
习题:P1462 通往奥格瑞玛的道路
解题思路
题目目的:求出到达路线中最大收费的最小值
看到“最小化……最大值”问题,可以先考虑二分答案可不可做,需要分析题目是否符合某种单调性。
本题中若最多一次交的费越大,能用的结点就越多,可以走到终点的可能性也就越大,反之则可能性越小,因此可以使用二分答案实现
而判断某个最大交费限制下能否到达终点则可以将损失血量看作边权转化为最短路问题,即此时“不完整的图”(由于最大交费限制)上的最短路是否小于等于初始血量
参考代码
#include <cstdio> #include <algorithm> #include <vector> #include <queue> using namespace std; typedef long long LL; const int N = 10005; const LL INF = 1e14; int f[N], n, m, b; LL dis[N]; bool vis[N]; struct Edge { int to, c; }; vector<Edge> g[N], sub[N]; struct Node { int id; LL d; }; struct NodeCmp { bool operator()(const Node& a, const Node& b) const { return a.d > b.d; } }; bool check(int x) { if (x < f[1]) return false; for (int i = 1; i <= n; i++) vis[i] = false; for (int i = 1; i <= n; i++) dis[i] = INF; priority_queue<Node, vector<Node>, NodeCmp> q; q.push({1, 0}); dis[1] = 0; while (!q.empty()) { int u = q.top().id; q.pop(); if (vis[u]) continue; vis[u] = true; for (Edge e : g[u]) { if (f[e.to] <= x && dis[u] + e.c < dis[e.to]) { dis[e.to] = dis[u] + e.c; q.push({e.to, dis[e.to]}); } } } return dis[n] <= b; } int main() { scanf("%d%d%d", &n, &m, &b); int ans = -1, l = 0, r = 0; for (int i = 1; i <= n; i++) { scanf("%d", &f[i]); r = max(r, f[i]); } while (m--) { int x, y, z; scanf("%d%d%d", &x, &y, &z); g[x].push_back({y, z}); g[y].push_back({x, z}); } while (l <= r) { int mid = (l + r) / 2; if (check(mid)) { ans = mid; r = mid - 1; } else l = mid + 1; } if (ans == -1) printf("AFK\n"); else printf("%d\n", ans); return 0; }
分层图最短路
例题:P4568 [JLOI2011] 飞行路线
由于购买机票需要花费金钱,所以肯定不会重复乘坐多次同样的航线或者多次访问同一个城市。如果
从上面一层跳到下面一层就是乘坐免票的飞机,花费的代价是
#include <cstdio> #include <vector> #include <queue> #include <algorithm> using namespace std; const int N = 110005; const int INF = 1e9; struct Edge { int to, w; }; vector<Edge> g[N]; int dis[N]; bool vis[N]; struct Node { int i, d; }; struct NodeCmp { bool operator()(const Node& a, const Node& b) { return a.d > b.d; } }; priority_queue<Node, vector<Node>, NodeCmp> q; int main() { int n, m, k, s, t; scanf("%d%d%d%d%d", &n, &m, &k, &s, &t); while (m--) { int a, b, c; scanf("%d%d%d", &a, &b, &c); g[a].push_back({b, c}); g[b].push_back({a, c}); for (int i = 1; i <= k; i++) { int u = a + i * n, v = b + i * n; g[u].push_back({v, c}); g[v].push_back({u, c}); g[u - n].push_back({v, 0}); g[v - n].push_back({u, 0}); } } for (int i = 0; i < k * n + n; i++) dis[i] = INF; dis[s] = 0; q.push({s, 0}); while (!q.empty()) { int u = q.top().i; q.pop(); if (vis[u]) continue; vis[u] = true; for (Edge e : g[u]) { int to = e.to, w = e.w; if (dis[u] + w < dis[to]) { dis[to] = dis[u] + w; q.push({to, dis[to]}); } } } int ans = INF; for (int i = 0; i <= k; i++) ans = min(ans, dis[t + i * n]); printf("%d\n", ans); return 0; }
0-1 图最短路
一般求解最短路径,高效的方法是 Dijkstra 算法,如果用优先队列实现则时间复杂度为
例题:P2937 [USACO09JAN] Laserphones S
给一个网格图,里边有两个 C
,求一条拐弯最少的连接两个 C
的路径。(注意输入是先列数再行数)
解题思路
状态肯定不能只设计成现在在哪儿,因为转移跟方向也有关系。
假设现在在
接下来就是看要走的方向和当前方向是不是一致,决定边权是
可以想象一下最短路计算过程中优先队列的变化过程:0......1 -> 1......2 -> 2......3 -> ......
。由于边权只有
每个点最多入队两次,时间复杂度为
这个问题叫做 01 最短路,这个做法可以看作是 Dijkstra 算法在特殊情况下的一种优化。
参考代码
#include <cstdio> #include <deque> #include <algorithm> const int N = 105; const int INF = 1e9; char s[N][N]; int dx[4] = {-1, 0, 0, 1}; int dy[4] = {0, -1, 1, 0}; struct Node { int x, y, d; }; int dis[N][N][4]; bool vis[N][N][4]; int main() { int n, m; scanf("%d%d", &m, &n); for (int i = 1; i <= n; i++) scanf("%s", s[i] + 1); int x1, y1, x2, y2; x1 = y1 = x2 = y2 = 0; for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) { for (int dir = 0; dir < 4; dir++) dis[i][j][dir] = INF; if (s[i][j] == 'C') { if (x1 == 0) { x1 = i; y1 = j; } else { x2 = i; y2 = j; } } } std::deque<Node> q; for (int dir = 0; dir < 4; dir++) { q.push_back({x1, y1, dir}); dis[x1][y1][dir] = 0; } while (!q.empty()) { Node tmp = q.front(); q.pop_front(); int x = tmp.x, y = tmp.y, d = tmp.d; if (vis[x][y][d]) continue; vis[x][y][d] = true; for (int dir = 0; dir < 4; dir++) { int xx = x + dx[dir], yy = y + dy[dir]; if (xx < 1 || xx > n || yy < 1 || yy > m || s[xx][yy] == '*') continue; int w = 1 - (dir == d); if (dis[x][y][d] + w < dis[xx][yy][dir]) { dis[xx][yy][dir] = dis[x][y][d] + w; if (w == 0) q.push_front({xx, yy, dir}); else q.push_back({xx, yy, dir}); } } } int ans = INF; for (int dir = 0; dir < 4; dir++) ans = std::min(ans, dis[x2][y2][dir]); printf("%d\n", ans); return 0; }
例题:P4667 [BalticOI 2011 Day1] Switch the Lamp On
时间限制为 150ms;内存限制为 125MB
问题描述:Casper 正在设计电路。有一种正方形的电路元件,在它的两组相对顶点中,有一组会用导线连接起来,另一组则不会。有个这样的元件( ),排列成 行,每行 个。电源连接到电路板的左上角,灯连接到电路板的右下角。只有在电源和灯之间有一条电线连接的情况下,灯才会亮。为了亮灯,任何数量的电路元件都可以转动 90°(两个方向)。
编写一个程序,求出最少需要旋转多少电路元件。
本题可以建模为最短路径问题。把起点
如果用 Dijkstra 算法,复杂度为
在优先队列优化的 Dijkstra 算法中,优先队列的作用是在队列中找到距离起点最短的那个结点,并弹出它。使用优先队列的原因是,每个结点到起点的距离不同,需要用优先队列保证单调性。
本题是一种特殊情况,边权为 0 或 1。简单地说,就是“边权为 0,插到队头;边权为 1,插入队尾”,这样就省去了优先队列维护有序性的代价,从而减少了计算,优化了时间复杂度。这个操作用双端队列实现,这样保证了距离更近的点总是在队列的前面,队列中元素是单调的。每个结点只入队和出队一次,总的时间复杂度是线性的。
#include <cstdio> #include <deque> #include <utility> #include <string> using namespace std; typedef pair<int, int> PII; const int N = 505; const int INF = 1e9; int dis[N][N]; // dis记录从起点出发的最短路径 char s[N][N]; // 4个点 int d1[4][2] = {{-1, -1}, {-1, 1}, {1, -1}, {1, 1}}; // 4个电子元件 int d2[4][2] = {{-1, -1}, {-1, 0}, {0, -1}, {0, 0}}; int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) scanf("%s", s[i] + 1); for (int i = 1; i <= n + 1; i++) for (int j = 1; j <= m + 1; j++) dis[i][j] = INF; deque<PII> q; q.push_back({1, 1}); dis[1][1] = 0; string match = "\\//\\"; // 注意反斜杠需要加转义字符 while (!q.empty()) { PII cur = q.front(); q.pop_front(); // 弹出队头 int x = cur.first, y = cur.second; for (int i = 0; i < 4; i++) { // 4个方向 char ch = match[i]; int px = x + d1[i][0], py = y + d1[i][1]; int cx = x + d2[i][0], cy = y + d2[i][1]; if (px >= 1 && px <= n + 1 && py >= 1 && py <= m + 1) { if (cx >= 1 && cx <= n && cy >= 1 && cy <= m) { if (s[cx][cy] == ch && dis[x][y] < dis[px][py]) { dis[px][py] = dis[x][y]; q.push_front({px, py}); } if (s[cx][cy] != ch && dis[x][y] + 1 < dis[px][py]) { dis[px][py] = dis[x][y] + 1; q.push_back({px, py}); } } } } } if (dis[n + 1][m + 1] == INF) printf("NO SOLUTION\n"); else printf("%d\n", dis[n + 1][m + 1]); return 0; }
次短路
例题:P1491 集合位置
路径上不允许经过重复的点(如果最短路有多条,次短路长度就是最短路长度)。
解题思路
先求出一条最短路的路径,次短路一定不会把求出来的那条最短路全都经过一遍,至少有一条边不属于求出来的那条最短路径上的边。
枚举删除最短路径上的一条边,重新跑最短路。
时间复杂度为
参考代码
#include <cstdio> #include <algorithm> #include <cmath> const int N = 205; const double INF = 1e9; int n, x[N], y[N], pre[N]; double g[N][N], dis[N]; bool vis[N]; double distance(int i, int j) { double dx = x[i] - x[j]; double dy = y[i] - y[j]; return sqrt(dx * dx + dy * dy); } void dijkstra(bool rec) { for (int i = 1; i <= n; i++) { dis[i] = INF; vis[i] = false; } dis[1] = 0; while (true) { int u = -1; for (int i = 1; i <= n; i++) if (!vis[i] && (u == -1 || dis[i] < dis[u])) u = i; if (u == -1) break; vis[u] = true; for (int i = 1; i <= n; i++) { double w = g[u][i]; if (dis[u] + w < dis[i]) { dis[i] = dis[u] + w; if (rec) pre[i] = u; } } } } int main() { int m; scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) scanf("%d%d", &x[i], &y[i]); for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) g[i][j] = INF; g[i][i] = 0; } for (int i = 1; i <= m; i++) { int p, q; scanf("%d%d", &p, &q); g[p][q] = g[q][p] = std::min(g[p][q], distance(p, q)); } dijkstra(true); if (dis[n] == INF) { printf("-1\n"); return 0; } int u = n; double ans = INF; while (u != 1) { double tmp = g[pre[u]][u]; g[pre[u]][u] = g[u][pre[u]] = INF; dijkstra(false); ans = std::min(ans, dis[n]); g[pre[u]][u] = g[u][pre[u]] = tmp; u = pre[u]; } if (ans == INF) printf("-1\n"); else printf("%.2f\n", ans); return 0; }
例题:P2865 [USACO06NOV] Roadblocks G
允许经过重复点和重复边的最短路。
解题思路
类似分层图,用
初始化 (dis, 点)
,刚开始放 (0, 1)
。
vis
标记也要给每个点准备两份(有两次取出机会),一个点
对于边
- 如果当前这一次能更新最短路,就把原最短路放到次短路上,更新最短路,将点
和这次更新后的 放入优先队列。 - 如果当前这一次只能更新次短路,就把次短路更新,将点
和这次更新后的 放入优先队列。
continue
。
参考代码
#include <cstdio> #include <vector> #include <queue> using pr = std::pair<int, int>; const int N = 5005; const int INF = 1e9; std::vector<pr> g[N]; int dis[N][2]; bool vis[N][2]; int main() { int n, r; scanf("%d%d", &n, &r); for (int i = 1; i <= r; i++) { int a, b, d; scanf("%d%d%d", &a, &b, &d); g[a].push_back({b, d}); g[b].push_back({a, d}); } std::priority_queue<pr, std::vector<pr>, std::greater<pr>> q; for (int i = 1; i <= n; i++) dis[i][0] = dis[i][1] = INF; dis[1][0] = 0; q.push({0, 1}); while (!q.empty()) { pr p = q.top(); q.pop(); int u = p.second, x = -1; if (!vis[u][0]) { vis[u][0] = true; x = 0; } else if (!vis[u][1]) { vis[u][1] = true; x = 1; } else continue; for (pr e : g[u]) { int v = e.first, w = e.second; int tmp = dis[u][x] + w; if (tmp < dis[v][0]) { dis[v][1] = dis[v][0]; dis[v][0] = tmp; q.push({dis[v][0], v}); } else if (tmp > dis[v][0] && tmp < dis[v][1]) { dis[v][1] = tmp; q.push({dis[v][1], v}); } } } printf("%d\n", dis[n][1]); return 0; }
同余最短路
同余最短路就是把余数相同的情况归为一类,找形成这种情况的最短路径的问题,通常与周期性问题有关。
例题:P3403 跳楼机
给定
,对于 ,有多少个 能够满足 。
()
令
可以得到两种转移:
这相当于对
接下里只需要求出
答案即为:
#include <cstdio> #include <queue> #include <utility> using namespace std; typedef long long LL; typedef pair<LL, int> PLI; const int N = 100005; const LL INF = 1e15; LL dis[N]; bool vis[N]; int main() { LL h; int x, y, z; scanf("%lld%d%d%d", &h, &x, &y, &z); for (int i = 0; i < z; i++) dis[i] = INF; dis[1 % z] = 1; priority_queue<PLI, vector<PLI>, greater<PLI>> q; // <dis, id> q.push({1, 1 % z}); while (!q.empty()) { int u = q.top().second; q.pop(); if (vis[u]) continue; vis[u] = true; // +x int nxt = (u + x) % z; if (dis[u] + x < dis[nxt]) { dis[nxt] = dis[u] + x; q.push({dis[nxt], nxt}); } // +y nxt = (u + y) % z; if (dis[u] + y < dis[nxt]) { dis[nxt] = dis[u] + y; q.push({dis[nxt], nxt}); } } LL ans = 0; for (int i = 0; i < z; i++) if (dis[i] <= h && dis[i] != INF) ans += (h - dis[i]) / z + 1; printf("%lld\n", ans); return 0; }
习题:[ABC077D] Small Multiple
给定
,求 的倍数中,数位和最小的那一个的数位和。
解题思路
任意一个正整数都可以从
对于所有的
每个
时间复杂度为
参考代码
#include <cstdio> #include <deque> using namespace std; const int N = 100005; const int INF = 1e9; int dis[N]; int main() { int k; scanf("%d", &k); for (int i = 0; i < k; i++) { dis[i] = INF; } dis[1] = 1; deque<int> dq; dq.push_back(1); while (!dq.empty()) { int u = dq.front(); dq.pop_front(); // *10 int v = u * 10 % k; if (dis[u] < dis[v]) { dq.push_front(v); dis[v] = dis[u]; } // +1 v = (u + 1) % k; if (dis[u] + 1 < dis[v]) { dq.push_back(v); dis[v] = dis[u] + 1; } } printf("%d\n", dis[0]); return 0; }
Bellman-Ford
设
在没有负环的图上,最短路最多经过
如果求至多经过
时间复杂度为
类似于分层图,但因为没有
压维前:
for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) dis[i][j] = dis[i - 1][j]; for (int j = 1; j <= n; j++) for (Edge e : g[j]) dis[i][e.v] = min(dis[i][e.v], dis[i - 1][j] + e.w); }
压维后:
for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) for (Edge e : g[j]) dis[e.v] = min(dis[e.v], dis[j] + e.w); }
例题:B3601 [图论与代数结构 201] 最短路问题_1
参考代码
#include <cstdio> #include <vector> #include <utility> #include <algorithm> using ll = long long; const int N = 2005; const ll INF = 1e18; std::vector<std::pair<int, int>> g[N]; ll dis[N]; int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= m; i++) { int u, v, w; scanf("%d%d%d", &u, &v, &w); g[u].push_back({v, w}); } for (int i = 1; i <= n; i++) dis[i] = INF; dis[1] = 0; for (int i = 1; i < n; i++) { for (int j = 1; j <= n; j++) for (auto e : g[j]) { int v = e.first, w = e.second; dis[v] = std::min(dis[v], dis[j] + w); } } for (int i = 1; i <= n; i++) printf("%lld ", dis[i] == INF ? -1 : dis[i]); return 0; }
队列优化的 Bellman-Ford
回顾 Bellman-Ford 算法的计算过程,如果
于是可以用一个队列维护哪些点已经被更新(处于活跃状态)。
每次将最短路
常见的其它常数优化:
- SLF:将普通队列换成双端队列,每次将入队结点距离和队首比较,如果更大则插入队尾,否则插入队首。
- LLL:将普通队列换成双端队列,每次将入队结点和队内距离平均值比较,如果更大则插入至队尾,否则插入队首。
注意最差时间复杂度仍然是
在一般随机图上跑不满,制作 hack 数据的方法详见知乎:如何看待 SPFA 算法已死这种说法?
例题:B3601 [图论与代数结构 201] 最短路问题_1
参考代码
#include <cstdio> #include <algorithm> #include <vector> #include <queue> #include <utility> using ll = long long; const int N = 2005; const ll INF = 1e18; std::vector<std::pair<int, int>> g[N]; ll dis[N]; bool inq[N]; int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= m; i++) { int u, v, w; scanf("%d%d%d", &u, &v, &w); g[u].push_back({v, w}); } for (int i = 1; i <= n; i++) dis[i] = INF; std::queue<int> q; q.push(1); dis[1] = 0; inq[1] = true; while (!q.empty()) { int u = q.front(); q.pop(); inq[u] = false; for (auto e : g[u]) { int v = e.first, w = e.second; if (dis[v] > dis[u] + w) { dis[v] = dis[u] + w; if (!inq[v]) { q.push(v); inq[v] = true; } } } } for (int i = 1; i <= n; i++) printf("%lld ", dis[i] == INF ? -1 : dis[i]); return 0; }
判负环
一种判负环的简单做法是用一个数组
每次更新一个点最短路时,该点的
需要注意,如果只从
例题:P3385 【模板】负环
参考代码
#include <cstdio> #include <vector> #include <utility> #include <queue> const int N = 2005; const int INF = 1e9; std::vector<std::pair<int, int>> g[N]; int dis[N], cnt[N]; bool inq[N]; void solve() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) { g[i].clear(); dis[i] = INF; inq[i] = false; cnt[i] = 0; } for (int i = 1; i <= m; i++) { int u, v, w; scanf("%d%d%d", &u, &v, &w); g[u].push_back({v, w}); if (w >= 0) g[v].push_back({u, w}); } std::queue<int> q; q.push(1); dis[1] = 0; inq[1] = true; while (!q.empty()) { int u = q.front(); q.pop(); inq[u] = false; for (auto e : g[u]) { int v = e.first, w = e.second; if (dis[u] + w < dis[v]) { dis[v] = dis[u] + w; cnt[v] = cnt[u] + 1; if (cnt[v] >= n) { printf("YES\n"); return; } if (!inq[v]) { q.push(v); inq[v] = true; } } } } printf("NO\n"); } int main() { int t; scanf("%d", &t); for (int i = 1; i <= t; i++) solve(); return 0; }
差分约束系统
用于求解
两种建图方式:
向 连边权为 的有向边,建立虚点 向每个点连边权为 的有向边,从点 开始跑最短路,得到的是每个 且 的最大值,如果图中有负环则无解。 向 连边权为 的有向边,建立虚点 向每个点连边权为 的有向边,从点 开始跑最长路,得到的是每个 且 的最大值,如果图中有负环则无解。
例题:P5960 【模板】差分约束
参考代码
#include <cstdio> #include <vector> #include <queue> #include <utility> const int N = 5005; const int INF = 1e9; std::vector<std::pair<int, int>> g[N]; int cnt[N], dis[N]; bool inq[N]; int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= m; i++) { int c1, c2, y; scanf("%d%d%d", &c1, &c2, &y); g[c2].push_back({c1, y}); } for (int i = 1; i <= n; i++) { dis[i] = INF; g[0].push_back({i, 0}); } std::queue<int> q; q.push(0); dis[0] = 0; inq[0] = true; while (!q.empty()) { int u = q.front(); q.pop(); inq[u] = false; for (auto e : g[u]) { int v = e.first, w = e.second; if (dis[u] + w < dis[v]) { dis[v] = dis[u] + w; cnt[v] = cnt[u] + 1; if (cnt[v] >= n + 1) { // 注意多了一个虚点0 printf("NO\n"); return 0; } if (!inq[v]) { q.push(v); inq[v] = true; } } } } for (int i = 1; i <= n; i++) printf("%d ", dis[i]); return 0; }
例题:P1993 小 K 的农场
解题思路
实际上就是 ,可以让 向 连一条边权为 的边 是差分约束系统的标准形式,让 向 连一条边权为 的边 也可以转化成不等式关系,即 且 ,让 和 之间互相连边权为 的边
参考代码
#include <cstdio> #include <vector> #include <utility> #include <queue> const int N = 5005; const int INF = 1e9; std::vector<std::pair<int, int>> g[N]; int cnt[N], dis[N]; bool inq[N]; int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= m; i++) { int t, a, b, c; scanf("%d%d%d", &t, &a, &b); if (t != 3) scanf("%d", &c); if (t == 1) g[a].push_back({b, -c}); else if (t == 2) g[b].push_back({a, c}); else { g[a].push_back({b, 0}); g[b].push_back({a, 0}); } } for (int i = 1; i <= n; i++) { g[0].push_back({i, 0}); dis[i] = INF; } std::queue<int> q; q.push(0); inq[0] = true; dis[0] = 0; while (!q.empty()) { int u = q.front(); q.pop(); inq[u] = false; for (auto e : g[u]) { int v = e.first, w = e.second; if (dis[u] + w < dis[v]) { dis[v] = dis[u] + w; cnt[v] = cnt[u] + 1; if (cnt[v] >= n + 1) { printf("No\n"); return 0; } if (!inq[v]) { q.push(v); inq[v] = true; } } } } printf("Yes\n"); return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!