学习笔记:最短路
使用的代码模板:
#include <bits/stdc++.h>
#define f(i, a, b) for (int i = (a); i <= (b); ++i)
#define g(i, a, b) for (int i = (a); i >= (b); --i)
#define fi first
#define se second
#define pb push_back
#define mp make_pair
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
int const INF = 0x3f3f3f3f;
void solve() {
// 代码主体
return;
}
signed main() {
cin.tie(0)->sync_with_stdio(false);
int tt = 1;
// cin >> tt; // 有多测就取消注释
while (tt--) solve();
return 0;
}
下面的代码省略了模板以及一些变量声明(一般 N 指点数,M 指边数)。
0 定义与记号
主要包括三种问题:单源最短路、多源最短路、全源最短路。
设 \(n\) 为节点数,\(m\) 为边数。
设 \(d(u,v)\) 表示 \(u\) 到 \(v\) 的最短路长度。
松弛操作:假设存在 \(u\) 到 \(v\) 的边,权值为 \(w(u,v)\),用 \(d(s,u)+w(u,v)\) 与当前 \(d(s,v)\) 进行比较,如果更优则更新。
三角形不等式:对于任意三个点 \(i,j,k\),有 \(d(i,k)\le d(i,j)+d(j,k)\)。特别地,对于边 \(u\to v\),边权为 \(w\),有 \(d(S,v)\le d(S,u)+w\),其中 \(S\) 为任意一个点(单源最短路中作为源点)。
1 单源最短路
单源最短路问题:给定源点 \(s\),求出 \(s\) 到任一点 \(i\) 的最短路长度 \(d(s,i)\)。下面将 \(d(s,i)\) 简记为 \(d(i)\)。
1.1 拓扑排序求 DAG 最短路
有向无环图(directed acyclic graph, DAG)中的单源最短路问题可以用拓扑排序结合 DP 解决。
拓扑排序:对节点序列 \(1,2,\cdots,n\) 标号 \(r_1,r_2,\cdots,r_n\),使得 \(r_i<r_j\) 当且仅当存在一条从 \(i\) 到 \(j\) 的路径。
过程:取入度为 \(0\) 的点入队,取队首 \(u\) 并删除 \(u\) 的所有出边,更新入度,如果出现入度为 \(0\) 则入队。出队的顺序即为拓扑排序的序号。
求最短路:初始令 \(d(i)=+\infty\),\(d(s)=0\),每次删除边 \(uv\) 的同时进行松弛,用 \(d(u)+w(u,v)\) 更新 \(d(v)\)。
时间复杂度:\(O(n+m)\)。
1.2 Bellman-Ford 算法及其队列优化(SPFA)
适用于可能含有负权边的图。
Bellman-Ford
BF 算法是一种 BF 算法。(笑点解析:前者指 Bellman-Ford,后者指 Brute Force,令人忍俊不禁。)
每一轮用所有边进行松弛,共进行 \(n-1\) 轮松弛,时间复杂度 \(O(nm)\)。
由于一条最短路边数最大为 \(n-1\),且每一轮松弛一定可以向最短路增加一条边,因此松弛 \(n-1\) 轮即可。
SPFA
队列优化的 Bellman-Ford 算法,国内一般称 SPFA(Shortest Path Faster Algorithm),在随机图上表现十分优异。
然而,特殊构造的数据可能使 SPFA 的最坏复杂度达到朴素算法的 \(O(nm)\),故应该谨慎使用 (关于SPFA,它死了)。
卡 SPFA:如何看待 SPFA 算法已死这种说法? - 知乎
具体来说,用队列存候选点,每次用队首 \(u\) 的所有出边 \((u,v)\) 进行松弛。如果松弛成功并且 \(v\) 不在队列中,则将 \(v\) 压入队列。
模板题 洛谷-P3371 【模板】单源最短路径(弱化版) 代码:
int n, m, s;
int head[N], cnt;
struct Edge {
int to, nxt, val;
} e[M];
inline void add(int from, int to, int val) {
e[++cnt].to = to, e[cnt].nxt = head[from], e[cnt].val = val, head[from] = cnt;
return;
}
int d[N];
bool in[N];
queue<int> q;
void SPFA(int s) {
f(i, 1, n) d[i] = 2147483647;
d[s] = 0;
q.push(s);
in[s] = true;
while (!q.empty()) {
int u = q.front(); q.pop();
in[u] = false;
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to, w = e[i].val;
if (d[v] > d[u] + w) {
d[v] = d[u] + w;
if (!in[v]) {
q.push(v);
in[v] = true;
}
}
}
}
return;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> m >> s;
int u, v, w;
f(i, 1, m) {
cin >> u >> v >> w;
add(u, v, w);
}
SPFA(s);
f(i, 1, n) cout << d[i] << ' ';
cout << '\n';
return 0;
}
SPFA 判负环
如果最短路的边数达到 \(n\),则出现负环。应用:见 1.5 差分约束问题。
模板题 洛谷-P3385 【模板】负环 代码:
void solve() {
int n, m;
cin >> n >> m;
vector<vector<pii> > g(n + 1);
for (int i = 0; i < m; ++i) {
int u, v, w;
cin >> u >> v >> w;
g[u].pb({v, w});
if (w >= 0) g[v].pb({u, w});
}
vector<int> dis(n + 1, INF), len(n + 1, 0);
queue<int> q;
vector<bool> in(n + 1, false);
dis[1] = 0;
q.push(1);
in[1] = true;
while (!q.empty()) {
int u = q.front();
q.pop();
in[u] = false;
for (auto [v, w] : g[u]) {
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
len[v] = len[u] + 1;
if (len[v] == n) {
cout << "YES\n";
return;
}
if (!in[v]) {
q.push(v);
in[v] = true;
}
}
}
}
cout << "NO\n";
return;
}
1.3 Dijkstra 算法及其堆优化
堆优化的 Dijkstra 是最常用、最稳定的一种最短路算法,需要熟练背诵。
不同于 Bellman-Ford 和 SPFA,Dijkstra 算法只适用于非负权图。
每次找 \(d(u)\) 最小的没被选过的 \(u\),用 \(u\) 的所有出边进行松弛。\(d(u)\) 之后不会再参与松弛,也不会再被松弛,即当前的 \(d(u)\) 已经是最短路了。
时间复杂度:\(O(n^2)\)。
堆优化:为了快速找到 \(d(u)\) 最小的 \(u\),用小根堆(优先队列)存储所有候选点的 (距离, 编号) 二元对。
时间复杂度 \(O((n+m)\log n)\)。
这是因为 C++ 中的 std::pair 默认先比较第一维,再比较第二维。由于 std::priority_queue 默认为大根堆,所以可以将第一维改为存储距离的相反数。当然也可以直接定义小根堆,定义方式为:
priority_queue<pair<int, int>,
vector<pair<int, int>>,
greater<pair<int, int>>> q;
对于稠密图,\(m\) 接近 \(n^2\),可以不用优化,时间 \(O(n^2)\)。
模板题 洛谷-P4779 【模板】单源最短路径(标准版) 代码:
void solve() {
int n, m, s;
cin >> n >> m >> s;
vector<vector<pii>> g(n + 1);
for (int i = 0; i < m; i++) {
int u, v, w;
cin >> u >> v >> w;
g[u].push_back({v, w});
}
vector<int> dis(n + 1, INF);
priority_queue<pii> q; // 默认大根堆
vector<bool> vis(n + 1, false);
dis[s] = 0;
q.push({0, s});
while (!q.empty()) {
int u = q.top().second;
q.pop();
if (vis[u]) continue;
vis[u] = true;
for (auto [v, w]: g[u]) {
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
q.push({-dis[v], v}); // 由于是大根堆, 故取负值, 就按 dis 从小到大排列
}
}
}
for (int i = 1; i <= n; i++) cout << dis[i] << ' ';
cout << '\n';
return;
}
边权的设置(或者说松弛的公式)是很重要的,例如:洛谷-P1073, QOJ-14309, CodeForces-2000G。
1.4 边权为 0 或 1 的最短路(0-1 BFS)
0-1 BFS - Algorithms for Competitive Programming
对于边权为 \(1\) 的图,显然直接 BFS 即可。
对于边权只可能为 \(0\) 或 \(1\) 的图,可以用双端队列 BFS 解决最短路问题。
具体算法过程如下:
- 将起点(可以是多个)push 到队列中。
- 取队首 \(u\),枚举所有出边 \(v\),如果 \(d(v)>d(u)+w(u,v)\),进行判断:
- 如果 \(w(u,v)=0\),则将 \(v\) push 到队首;
- 如果 \(w(u,v)=1\),则将 \(v\) push 到队尾。
- 反复执行步骤 2,直到队列为空。
性质:任意时刻队列中元素的 dis 值形如 \((d,d,\cdots,d,d+1,d+1,\cdots,d+1)\)。
证明:假设某一时刻满足上述性质,那么每次进行步骤 2 的操作后,仍然满足性质。根据归纳原理,任意时刻队列都满足上述性质。
判断 \(d(v)>d(u)+w(u,v)\) 是为了防止重复经过某一点。
这与 Dijkstra 的过程类似:每次松弛的起点都是当前没完成松弛的点中 \(d\) 最小的,而一个点只会被松弛一次。用双端队列就是为了快速找到最小的 \(d\)。省去了查找最小 \(d\) 的时间,时间复杂度 \(O(n+m)\)。
类似地,只要边权有 \(O(1)\) 种,都可以实现快速查找最小的 \(d\)。假设边权有 \(k\) 种(\(k\) 很小),分别为 \(w_1<w_2<\dots<w_k\),那么就开 \(k\) 个队列 \(Q_1,Q_2,\cdots,Q_k\)。每次松弛时,先 \(O(k)\) 找到第一个非空的队列中的队首 \(u\),如果 \(w(u,v)=w_i\),就将 \(v\) push 到 \(Q_i\) 的队尾。
故时间复杂度为 \(O(kn+m)\)。
1.5 应用:差分约束问题
对于若干待求变量 \(x_1,\cdots,x_n\),给定若干形如 \(x_a-x_b\le c\) 或 \(x_a-x_b\ge c\) 的不等式限制,求一组合法的解 \(x_1,\cdots,x_n\)。
所有限制均可以写为 \(x_a+c\ge x_b\) 的形式。建图,节点为 \(1,\cdots,n\),对所有形如 \(x_a+c\ge x_b\) 的限制条件,连 \(a\) 到 \(b\) 边权为 \(c\) 的边。
为了防止图不连通,并便于设置初值,建立超级源点 \(0\),对所有 \(i\) 建边 \((0,i)\),边权为零。求出最短路长度 \(d\),则 \(x_i=d(i)\) 即为一组解。
因为边权 \(c\) 可能为负值,用 SPFA 求解最短路。若出现负环则无解。
模板题 P5960 【模板】差分约束 - 洛谷 代码:
int n, m;
int head[N], cnt;
struct Edge {
int to, nxt, val;
} e[N << 1];
inline void add(int from, int to, int val) {
e[++cnt].to = to, e[cnt].nxt = head[from], e[cnt].val = val, head[from] = cnt;
return;
}
int d[N], c[N];
bool in[N];
bool SPFA(int s) {
memset(d, 0x3f, sizeof d);
d[s] = 0;
queue<int> q;
q.push(s);
in[s] = true;
while (!q.empty()) {
int u = q.front();
q.pop();
in[u] = false;
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to, w = e[i].val;
if (d[v] > d[u] + w) {
d[v] = d[u] + w;
c[v] = c[u] + 1;
if (c[v] >= n + 1) return true;
if (!in[v]) {
q.push(v);
in[v] = true;
}
}
}
}
return false;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> m;
f(i, 1, n) add(n + 1, i, 0); // 超级源点, 这里编号设置为 n+1
int u, v, w;
f(i, 1, m) {
cin >> v >> u >> w;
add(u, v, w);
}
if (SPFA(n + 1)) cout << "NO";
else f(i, 1, n) cout << d[i] << ' ';
cout << '\n';
return 0;
}
2 全源最短路
2.1 Floyd-Warshall 算法
简称为 Floyd 最短路算法或 Floyd 算法(另外还有一个 Floyd 判圈算法,不要混淆)。需要熟练背诵。
算法过程
设 \(d_k(u,v)\) 表示 \(u\) 到 \(v\) 途中经过编号不超过 \(k\) 的点的最短路长度。
以 \(k\) 为阶段,进行类似于动态规划的转移,用 \(k\) 更新所有点的最短路值,即用 \(d_k(i,k+1)+d_k(k+1,j)\) 更新 \(d_{k+1}(i,j)\)。
这样,最后得到的 \(d_n(i,j)\) 就是从 \(i\) 到 \(j\) 的最短路长度。
可以发现 \(d_{k+1}\) 中的状态不会互相更新,因此实现时可以省去 \(k\) 这一维。
注意:
- 由于 \(k\) 是动态规划的阶段,保证了无后效性,故 \(k\) 必须在最外层枚举。
- 初值:\(d(i,i)=0\),\(d(u,v)=w(u,v)\),其余为 \(+\infty\)。
- 如果有重边,取 min 即可。
中转点 \(k\) 枚举的顺序不重要,只要每个点至少中转一次即可。
时间复杂度 \(O(n^3)\)。
用 Floyd-Warshall 算法可以找负环:如果存在负环,则会使 \(d(i,i)<0\),其中 \(i\) 为负环上的点。
模板题 B3647 【模板】Floyd 代码:
void solve() {
int n, m;
cin >> n >> m;
vector<vector<int> > g(n + 1, vector<int>(n + 1, 0x3f3f3f3f));
for (int i = 0; i < m; i++) {
int u, v, w;
cin >> u >> v >> w;
g[u][v] = min(g[u][v], w); // 去掉重边
g[v][u] = g[u][v];
}
for (int i = 1; i <= n; i++) g[i][i] = 0;
for (int k = 1; k <= n; k++) // Floyd
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]);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++)
cout << g[i][j] << ' ';
cout << '\n';
}
}
传递闭包
笛卡儿积:集合 \(A\) 和 \(B\) 的笛卡儿积定义为二元组集合 \(A\times B=\{(x,y)\mid x\in A,y\in B\}\)。
映射:集合 \(X\) 到 \(Y\) 的映射 \(f\) 定义为 \(X\times Y\) 的一个子集,其满足 (1) 对任一 \(x\in X\),存在 \(y\in Y\) 使 \((x,y)\in f\);(2) 若 \((x,y),(x,y')\in f\),则 \(y=y'\)。记为 \(f:X\to Y\)。若 \((x,y)\in f\),则称 \(y\) 是 \(x\) 的像。
二元关系:从 \(A\) 到 \(B\) 的二元关系 \(R\) 定义为 \(A\times B\) 到 \(\{\mathrm{True},\mathrm{False}\}\) 的一个映射。若 \(A=B\),则称为 \(A\) 上的一个关系。若 \((a,b)\in A\times B\) 在 \(R\) 下的像是 \(\mathrm{True}\),则记为 \(aRb\),反之记为 \(a\not Rb\)。
闭包:设 \(\mathscr P\) 是 \(A\) 上关系 \(R\) 的一些性质的集合,关系 \(R\) 的 \(\mathscr P\) - 闭包定义为包含 \(R\) 且具有 \(\mathscr P\) 中所有性质的所有关系的交。
传递闭包:令 \(\mathscr P=\{\text{传递的}\}\),则 \(R\) 的传递闭包定义为 \(R^+=\displaystyle\bigcap_{\begin{array}{cc}R\subseteq R'\\R'\text{是传递的}\end{array}}R'\)。
简单地说,有向图的传递闭包,就是求出所有点对的可达性。
有向图的传递闭包,严格来说是有向图的邻接关系的传递闭包,可以用一个布尔矩阵 \(B=(b_{ij})_{n\times n}\) 来表示,其中 \(b_{ij}=1\) 当且仅当存在一条从 \(i\) 到 \(j\) 的途径,即 \(i\) 可达 \(j\)。用 Floyd-Warshall 算法实现时,对转移方程稍作修改即可:\(b_{ij}\gets b_{ij}\,\operatorname{or}\,(b_{ik}\,\operatorname{and}\,b_{kj})\)。
bool b[N][N];
// ... 读入图
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
b[i][j] |= b[i][k] && b[k][j];
由于都是 bool 型数组,可以使用 bitset 优化。先枚举 \(k\) 和 \(i\),由转移方程可知,只有当 \(b_{ik}=\mathrm{True}\) 的时候才会有影响。
当 \(b_{ik}=\mathrm{True}\) 时,相当于对所有 \(j\) 执行 b[i][j] |= b[k][j],因此直接用两个 bitset 进行或运算即可。
bitset<N> b[N];
// ... 读入图
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
if (b[i][k]) b[i] |= b[k];
2.2 Johnson 全源最短路
3 扩展问题
3.1 同余最短路
3.2 最短路树与最短路图
3.3 平面图的最小割
3.4 次短路与 k 短路
3.5 删边最短路
4 习题
4.1 BFS / 0-1 BFS
CSES-1707 Graph Girth
找无向图的最小环,边权为 \(1\)。\(1\le n\le2500\),\(1\le m\le5000\)。
枚举在环上的点 \(i\),从所有 \(i\) 分别开始 BFS,假设当前队首的点为 \(u\),下一个点为 \(v\) 且 \(v\) 已经去过,则 \(uv\) 是环边,用 \(d(u)+d(v)+1\) 更新环的长度。当然,这里的前提是 \(d(u)\le d(v)\),从而可以避免 \(v\) 在从 \(i\) 到 \(u\) 的最短路上的情况。Source code - Virtual Judge
AtCoder-abc197_f Construct a Palindrome - 洛谷
有一个包含 \(N\) 个顶点和 \(M\) 条边的连通无向图(不一定是简单图)。
第 \(i\) 条边连接顶点 \(A_i\) 和顶点 \(B_i\),并且上面写有一个字母 \(C_i\)。
你可以从顶点 \(1\) 走到顶点 \(N\),路径上可以多次经过同一条边或顶点。将经过的边上的字母按顺序排列,得到一个字符串。
求出使得该字符串为回文串的最短路径长度(边数);如果不存在,输出 \(-1\)。
\(2 \leq N \leq 1000\),\(1 \leq M \leq 1000\)。
双向 BFS。状态是一个 pair。
*洛谷-P6757 [BalticOI 2013] Tracks in the Snow
有一个含 \(H\) 行,\(W\) 列的字符矩阵,初始全为
.。有两种动物,狐狸和兔子,将会从左上角走到右下角,狐狸会留下
F的痕迹,兔子会留下R的痕迹。痕迹会相互覆盖。走路规则如下:
- 可以往返走;
- 不可以走对角线;
- 不可以跳格子;
- 不可能有两只动物一起走。
现在您得到了这个被动物们走过的矩阵,请求出至少有几个动物走过了该矩阵。
\(1\le H,W\le 4\times 10^3\)。
首先,最后通过的动物种类可以由左上角和右下角的痕迹种类确定。那么我们可以进行倒推。
其次,由于起终点是不变的,那么如果两只相同种类的动物相继出发,它们的痕迹总有交点,因此可以合并为一只动物。
进而,只有前后两次的动物种类不同,才是最优的方案。
对网格中所有连通块建点(两种不同颜色),相邻的连通块之间建边,然后从起点所在的连通块开始 BFS,得到的生成树高度即为答案。
洛谷-P3036 [USACO16DEC] Lasers and Mirrors G
建模,拆点,将一个点拆为四个方向。同时建立超级源点和超级汇点。类似:AT_abc436_e。
*AtCoder-arc084_b Small Multiple - 洛谷
给定一个整数 \(n\)。求一个 \(n\) 的倍数 \(S\),使得 \(S\) 的数位和最小,输出这个最小值。\(2 \le n \le 10^5\)。
任意一个正整数都可以从 \(1\) 开始,由若干 \(\times10\) 和 \(+1\) 操作得到,而其中 \(+1\) 操作的次数即为数位和。
由于求的是 \(n\) 的倍数,我们按照同余最短路的建图思想建图:
- 建立同余类的节点 \(0,1,\cdots,n-1\);
- 由 \(k\) 到 \(10k\pmod n\) 建权值为 \(0\) 的边,由 \(k\) 到 \(k+1\pmod n\) 建权值为 \(1\) 的边。
这样,每一条由 \(1\) 到 \(0\) 的路径,都对应一个 \(n\) 的倍数。因此跑 0-1 BFS 求最短路即可,时间 \(O(n)\)。
Submission #75327110 - AtCoder Beginner Contest 077
*洛谷-P4083 [USACO17DEC] A Pie for a Pie G
奶牛 Bessie 和 Elsie 各有 \(N\) 个派,分别编号为 \(1,2,\cdots,N\) 和 \(N+1,N+2,\cdots,2N\)。
并且她们对每个派都进行了评分,分别为 \(a_i\) 和 \(b_i\)(\(i=1,2,\cdots,2N\))。
Bessie 和 Elsie 交替地向对方赠送自己的派,Bessie 先作为赠送方。
设游戏的第 \(i\) 轮赠送的派为 \(p_i\)。注意:一个派不能被赠送两次,任何一头奶牛也不能回赠她收到的派。
如果当前是 Bessie 赠送给 Elsie,则要求 \(a_{p_i}\in[a_{p_{i-1}},a_{p_{i-1}}+D]\),反之则要求 \(b_{p_i}\in[b_{p_{i-1}},b_{p_{i-1}}+D]\)。第一轮无要求。
当其中一头奶牛收到一只她评定美味值为 \(0\) 的派时,游戏成功结束。
如果当前没有满足要求的派,则游戏也结束,但判定为失败。
问对于每一种初始的派 \(i\)(\(i=1,2,\cdots,N\)),最少赠送多少派之后,游戏可以成功结束?
\(1\le N\le10^5\),\(0\le D,a_i,b_i\le10^9\)。
建图。由于终点是固定的,我们不妨将边反向,考虑上一个赠送的派。于是可以连边:
- 由每一个 \(i\)(\(i=1,\cdots,N\))向 \(a_j\in[a_i-D,a_i]\) 内的所有 \(j\)(\(j=N+1,\cdots,2N\))连权值为 \(1\) 的边;
- 由每一个 \(j\)(\(j=N+1,\cdots,2N\))向 \(b_i\in[b_j-D,b_j]\) 内的所有 \(i\)(\(i=1,\cdots,N\))连权值为 \(1\) 的边。
但我们不可能遍历权值区间内所有下标去连边,因为这样是 \(O(N^2)\) 的。怎么办?
注意,如果将所有派排序,那么我们连到的就是一个下标区间,容易用二分找到该区间。
这样就可以用线段树优化建图来做了,其思想十分简单直观,不再赘述。
由于边权为 \(0\)(线段树上父子之间的边)或 \(1\),直接跑 0-1 BFS 即可。
然而用线段树的话,难免有一种大炮打蚊子的感觉。
我们充分利用 BFS 的性质:一个点最多只会被经过一次。
因此我们不显式地建边(不用存图),而是只有当目标点没有被访问过时,再访问它。
而为了快速查找,我们使用 set 维护未访问的集合,在其中二分查找,遍历后就扔掉。
这样,我们就保证了「建出来的每条边」都是有用的。
二分查找的复杂度为 \(O(\log N)\),故总体是 \(O(N\log N)\) 的。Source code - Virtual Judge
4.2 单源非负权最短路
QOJ-9802 [ICPC 2024 Shenyang R] Light Up the Grid(状态压缩建点)
对一个 \(2\times2\) 的 0-1 矩阵,有四种操作,分别以 \(a_0,a_1,a_2,a_3\) 的代价翻转一个、一行、一列、全部元素。
给定 \(m\) 种初始状态,要求求出一种操作序列,使得这 \(m\) 种初始状态在经过序列中的一系列操作时,在某一时刻变成全为 \(0\) 的矩阵。注意变为 \(0\) 时不要求所有操作都进行完毕。
\(1\le T\le10^4\),\(1\le a_i\le10^6\),\(1\le m\le16\)。
将每个状态用二进制表示,共有 \(2^4=16\) 种。
而每种状态是否存在于候选状态中,又有 \(2^{16}=65536\) 种状态集合。
每一种状态集合都可以经过一次操作变为另一种状态,共有 \(9\) 种操作。
以状态集合为节点、操作为边,建一个点数为 \(65536\)、边数为 \(65536\times9\approx6\times10^5\) 的边带权有向图。边权即为操作花费的代价。
对某个状态集合进行一系列操作,就是对集合中每一个状态进行操作,如果这个状态变成 \(0\),就把它从状态集合中删掉。
最终的目标就是把状态集合变成 \(\varnothing\),因此在反图中以 \(\varnothing\) 为起点,预处理单源最短路,从而 \(O(1)\) 回答从任意状态到 \(\varnothing\) 的最小代价。
设 \(N=65536\),\(M=6\times10^5\),则预处理 Dijkstra 为 \(O((N+M)\log N)\),回答询问为 \(O(m)\)。
洛谷-P10947 [BAPC 2006 资格赛] Sightseeing(次短路,路径计数)
有向图,求 \(S\) 到 \(F\) 的最短路及比最短路长度少 \(1\) 的路径数量。\(2\le N\le10^3\),\(1\le M\le10^4\)。
Dijkstra,每个点可能作为次短路和最短路上的点参与松弛,因此每个点入堆最多两次。同时维护长度及数量。
洛谷-P1073 [NOIP 2009 提高组] 最优贸易(最小点权 \(\min\) 路径;分层图)
给定有向图,点数为 \(n\),边数为 \(m\),以及点 \(i\) 上的价值 \(V_i\)(\(i=1,\cdots,n\))。
选取一条路径 \(P=(P_1,\cdots,P_k)\),其中 \(a_1=1,a_k=n\),使得 \(\max\{0,\max\limits_{1\le i<j\le k}\{V_{P_j}-V_{P_i}\}\}\) 最大。求这个最大值。
\(1\le n\le10^5\),\(1\le m\le5\cdot10^5\)。
枚举点 \(x\),从 \(1\) 到 \(x\) 时在某个点买入,从 \(x\) 到 \(n\) 时在某个点卖出。分别从 \(1\) 和 \(n\) 出发,在原图和反图上,求到 \(x\) 的路径上的最小价值及最大价值即可。以求最小为例,将松弛操作改为 \(d(v)=\min\limits_{u\to v}\{V_v,d(u)\}\) 即可。
或者,建立分层图,跑最长路,这篇题解写得很清楚。
*QOJ-14309 Knapsack Problem(距离定义)
一张 \(n\) 个点 \(m\) 条边的无向图,点的编号为 \(1,2,\cdots,n\)。第 \(i\) 条边上有体积为 \(w_i\) 的物品。
有一个人要走到终点 \(T\),他初始时有一个背包,容量为 \(V\),每经过一条边都会将物品 \(w_i\) 放到包里。
如果背包当前容积不够用,他会更换一个新的容积为 \(V\) 的背包。
问:分别以 \(k=1,2,\cdots,n\) 为起点,走到 \(T\) 至少需要几个背包?如果无法到达,输出 \(-1\)。
\(1\le n\le10^5\),\(1\le m\le10^5\)。
首先,从某一点走到终点 \(T\),跟从 \(T\) 走到这一点,沿同一路径,所需的背包数是相同的。
例如考虑容积为 \(7\),物品序列为 \((2,3,1,3,2,1)\),分成 \((2,3,1),(3,2,1)\) 两组,倒过来则为 \((1,2,3,1),(2,3)\) 两组。
(感性理解一下。。)
其实这是由于贪心的决策包容性。
严格证明:设某一最优划分的每段结束点为 \(b_1,\cdots,b_m\),贪心得到的划分为 \(g_1,\cdots,g_p\),其中 \(b_m=g_p=n\)。由于最优性,显然有 \(m\le p\),下面证明 \(p\le m\)。先归纳证明,\(g_i\ge b_i\)(\(i=1,\cdots,m\))。(感性理解,贪心肯定尽量装了,那么结束点一定更晚。)
首先,由于贪心,显然有 \(g_1\ge b_1\)。假设成立 \(g_k\ge b_k\),那么下一段的起始点为 \(g_k+1\ge b_k+1\),那么既然起点晚了,同样都是 \(V\) 的容积,贪心的终点不可能更早,因此有 \(g_{k+1}\ge b_{k+1}\)。进而证明了对任意 \(i=1,\cdots,m\) 有 \(g_i\ge b_i\)。
那么,一定有 \(g_m\ge b_m=n\),这说明贪心在第 \(m\) 段就已结束,因此 \(p\le m\)。
同时,正向的最优序列翻转即得反向的最优序列,并且反向的贪心也可以如此证明。
这就证明了不论是正向还是反向,最优段数都是 \(m\)。
设 \((k,r)\) 表示当前用了 \(k\) 个背包,最后一个背包还剩 \(r\) 的容量。这就是最短路的距离,使用字典序比较大小,是全序的。
考虑装背包的过程,这个距离满足三角形不等式。换句话说,下一步的最优决策,一定建立在当前的最优决策之上。
因此在 Dijkstra 的松弛操作中模拟这一过程即可。Submission #1299692 - QOJ.ac
*CodeForces-2000G Call During the Journey(松弛操作改写)
在城市中,路口数为 \(n\),街道为 \(m\),且通过街道 \(i\) 可以选择坐车或步行,时间分别为 \(l_{i1}\) 和 \(l_{i2}\)(\(l_{i2}>l_{i1}\))。
起点(家)在路口 \(1\),时间从 \(0\) 时刻开始。要求:
- 在 \(t_1\) 到 \(t_2\) 时间内,必须步行或者在某一路口不动;
- 在 \(t_0\) 时间内必须到达路口 \(n\)(\(t_1<t_2<t_0\))。
求离开家的最晚时刻。如果无法按时到达,输出 \(-1\)。
\(1\le t\le10^4\),\(2\le n\le10^5\),\(1\le m\le10^5\)。\(\sum n,\sum m\le10^5\)。\(1\le t_1,t_2,t_0,l_{i1},l_{i2}\le10^9\)。
由于 \(t_2\) 和 \(t_0\) 是固定的,不妨从终点 \(n\) 开始倒推。
由于步行一定没有坐车快,所以当时间小于 \(t_1\) 或者大于 \(t_2\) 的时候都选择坐车。
否则,只能选择步行,或者原地等待到 \(t_1\)。
设 \(f_u\) 表示从 \(u\) 到 \(n\) 所需要的最晚出发时间。初值为 \(f_n=t_0\),答案为 \(f_1\)。
那么使用边 \((u,v,l_1,l_2)\) 进行松弛,即从 \(u\) 到 \(v\) 经过街道的代价为 \(l_1\) 或者 \(l_2\),由于是倒推,所以从 \(f_v\) 推到 \(f_u\):
- 可以坐车的情况:从 \(u\) 出发的时间比从 \(v\) 早 \(l_1\),需要 \(f_v\le t_1\) 或者 \(f_v-l_1\ge t_2\)(如图);
- 否则:
- 选择步行:从 \(u\) 出发的时间比从 \(v\) 早 \(l_2\);
- 或在 \(v\) 原地等待到 \(t_2\):令 \(f_u=t_1-l_1\)。
因此:
直接把 Dijkstra 的松弛改成这个转移即可。Submission #346934066 - Codeforces
*QOJ-14313 [ICPC 2025 Online (I)] Teleporter(分层图,树上最短路)
给定一棵树,节点数为 \(n\),边权 \(w(u,v)\) 表示花费的时间。除了树上的边,还有 \(m\) 条双向传送路线 \((p_i,q_i)\),花费时间为零。
设 \(d_k(u)\) 表示从节点 \(u\) 到节点 \(1\),使用不超过 \(k\) 次传送,需要的最少时间。对于所有 \(k=0,1,\cdots,n\),求 \(\sum_{u=1}^nd_k(u)\)。
\(1\le n\le5000\),\(0\le m\le10000\),\(1\le w\le10^9\)。
图中的边都是无向的。故改为计算从 \(1\) 到所有 \(u\) 的时间。
一条最短路形如:若干树边 \(\to\) 传送 \(\to\) 若干树边 \(\to\cdots\) 交替进行。
先处理出只经过树边的最短路 \(d_0(i)\)(就是简单路径长度),然后用所有非树边 \((p_i,q_i,0)\) 以及树边进行松弛。
具体来说,由分层图的思想,每一轮先用所有树边松弛,然后求这一层的最短路。进行 \(k\) 轮之后,得到 \(d_k\),即至多 \(k\) 次传送的最短路。
注意这里不要在原数组上修改,因为经过的传送路线数 \(k\) 相当于动态规划中的「阶段」,需要用 \(d_{k-1}\) 求出 \(d_k\)。使用辅助数组完成。
每层求最短路的过程,可以用 SPFA 或 Dijkstra(时间比较紧),也可以使用一个技巧:树上最短路,一定是「先上后下」的形式,因此先用所有向上的边自叶子向根松弛,再用所有向下的边自根向叶子松弛,就求出了最短路。具体看代码。
时间复杂度 \(O(n(n+m))\)。Submission #1328632 - QOJ.ac
*QOJ-9319 [ICPC 2024 Online (I)] Bull Farm(最小瓶颈路,建边,桶优化 Dijkstra)
农场里有 \(n\) 个牛栏和 \(n - 1\) 头公牛。在任何时刻,同一个牛栏内绝不能有两头或以上的公牛。
Arthur 有一个遥控器,上面有 \(l\) 个按钮:
- 如果按下按钮 \(i\),位于牛栏 \(j\) 的公牛会移动到牛栏 \(t_{i, j}\)。所有公牛会同时按照该规则移动。
- 按下一个按钮是合法的,当且仅当移动后没有两个公牛进入同一个牛栏。
现在给出 \(q\) 次独立询问,每次询问给出三个参数 \((a, b, c)\):
- 初始状态:只有第 \(a\) 个牛栏是空,其余 \(n-1\) 个牛栏各有一头牛。
- 限制条件:只能按编号在 \([1, c]\) 范围内的按钮,每个按钮可按任意次,也可以不按。
- 目标:问能否通过合法操作序列,使得最终第 \(b\) 个牛栏变为空的。
对每个询问,如果可以达到目标输出
1,否则输出0。\(\sum n\le2000\),\(\sum l\le2000\),\(\sum q\le10^6\)。\(1\le t_{i,j},a,b\le n\),\(0\le c\le l\)。
建图,节点 \(u\) 表示牛栏 \(u\) 为空。那么,\(u\) 能到达 \(v\) 表示可以经过若干次操作,使得从牛栏 \(u\) 为空变成牛栏 \(v\) 为空。我们关心的只有可达性。
考虑按下按钮 \(i\) 的效果。设 \(t_{i,1},t_{i,2},\cdots,t_{i,n}\) 中的不同值个数为 \(k\)。
- 如果 \(k\le n-2\),根据鸽巢原理,按下之后必定有两头公牛在同一牛栏中,故一定不合法,不建边。
- 如果 \(k=n-1\),设唯一出现两次的值为 \(t_{i,p_1}=t_{i,p_2}=q\),只要 \(p_1\) 和 \(p_2\) 其中有一个在上一步是空的即可,故从 \(p_1,p_2\) 分别向 \(q\) 连有向边。
- 如果 \(k=n\),那么 \(t_i\) 是一个排列(置换),可以分解为若干不相交的置换环。并且由于环上所有点双向可达,因此有向边替换为无向边不影响连通性。
考虑将按钮编号作为边权,那么要求从 \(a\) 到 \(b\) 最早连通的时刻,就相当于是求从 \(a\) 到 \(b\) 的所有路径中,边权最大值最小的一个(即最小瓶颈路)。修改 dis 的定义与转移式子,跑一遍 Dijkstra 即可。
在本题中,由于边权范围 \(2000\) 较小,可以考虑用桶优化 Dijkstra(称为 Dial 算法),跑一遍的时间复杂度为 \(O(n+e+l)\),其中 \(e\) 为边数。加边后从所有起点 \(s=1,2,\cdots,n\) 开始跑 Dijkstra,预处理后 \(O(1)\) 查询。
(具体如何用桶进行优化:用 buc[d] 存所有当前 dis 值为 d 的节点。维护一个递增的指针 cur,初始为 0。不断取 buc[cur] 中的点 u,但只有 dis[u] == cur 的点才是真实有效的。取出 u 之后,枚举所有出边 v,正常进行松弛。松弛之后,将 v 插入更新的 dis[v] 对应的桶中,相当于旧的 v 就失效了。如果桶 buc[cur] 空了,代表 cur, cur-1, ..., 0 对应的桶中的所有点都用过了 ,于是令 cur++。)
然而,对于 \(k=n\) 的情况,不能直接暴力加边,否则总边数 \(O(n^2)\),再跑 \(n\) 次 Dijkstra 就变成 \(O(n^3)\) 的复杂度了。
注意到,对于边 \(u\overset{i}{\longleftrightarrow}v\),如果 \(u,v\) 已经用边权更小的无向边连通,那么这一条边就没用了。因此用并查集维护只考虑无向边时的连通性,如果 \((u,v)\) 当前还未连通,才加入 \((u,v)\) 这条边。这样,加入的总边数不超过 \(O(n)\)。总时间 \(O(n(n+l)+q)\)。
4.3 单源负权最短路 / 负环
洛谷-P3199 [HNOI2009] 最小圈(负环,0/1 分数规划)
给定带权有向图,求最小平均权值环。\(2\le n\le3000\),\(1\le m\le10000\)。
存在 \(O(nm)\) 的做法,即 Karp 算法。
0/1 分数规划:
洛谷-P2868 [USACO07DEC] Sightseeing Cows G(负环,0/1 分数规划,点权下放)
给定一张 \(L\) 个点、\(P\) 条边的有向图,每个点都有一个权值 \(f_i\),每条边都有一个权值 \(t_i\)。
求图中的一个环,使「环上各点的权值之和」除以「环上各边的权值之和」最大。输出这个最大值。
\(2\le L\le1000\),\(2\le P\le5000\)。
0/1 分数规划。二分答案 \(mid\),如果存在环 \(C=(C_1,\cdots,C_k)\),其上的边设为 \(E_1,\cdots,E_k\),使得
则式子转化为
因此二分 check 的时候,建立新图,将有向边 \(e_i=(u_i,v_i)\) 的边权设置为 \(f_{u_i}-mid\cdot t_i\),然后找负环即可。
4.4 多源最短路
一般的多源最短路就是从多个源点分别求最短路,或者多源 BFS(BFS 初始队列中有多个起点)。
Gym-105358E Escape
迷宫由 \(n\) 个房间组成,Sneaker 初始在起点 \(1\),他的终点为 \(n\)。
迷宫中有 \(m\) 条双向通道,每条通道连接两个不同的房间,并且每条通道的两个方向是相互隔离的。
保证没有两条不同的通道连接同一对房间,并且没有通道连接一个房间到它自己,保证任何两个房间可以通过一系列通道相互到达。
迷宫中有 \(k\) 个屠杀机器人,第 \(i\) 个屠杀机器人最初停留在房间 \(s_i\)。
Sneaker 和屠杀机器人同时开始移动,过程如下:
- Sneaker 选择一个与当前房间相连的通道,并移动到通道另一侧的房间。
- 同时,每个屠杀机器人将随机选择一个与它所在房间相连的通道,并移动到通道另一侧的房间。
- 每个屠杀机器人将与 Sneaker 同时进入和退出通道。
所有屠杀机器人将记录它们经过的通道。如果一个屠杀机器人经过的通道与最后记录的通道相同,它可以从记录中删除这两次记录的通道。换句话说,屠杀机器人可以选择撤销一条记录。
所有屠杀机器人共享一个相同的自毁参数 \(d\)。如果一个屠杀机器人到达一个房间并发现它的通道记录数量超过 \(d\),它将立即自毁。如果 Sneaker 进入一个房间并发现一个屠杀机器人正在自毁或已经自毁,则不算作与机器人的遭遇。
现在,Sneaker 想要规划一条通行的路线,确保通行的通道数量最少,并且确保他不会遇到任何屠杀机器人。
如果存在可行的路线,输出最少经过的通道数量,并输出对应的路线经过的房间序列。如果不存在可行的路线,输出 \(-1\)。
注意:
- 如果 Sneaker 从房间 \(x\) 通过一条通道移动到房间 \(y\),而一个屠杀机器人正朝相反方向移动,从房间 \(y\) 通过同一条通道移动到房间 \(x\),Sneaker 不会遇到屠杀机器人,因为通道的两个方向是相互隔离的。
- Sneaker 只知道每个屠杀机器人的初始位置,但不知道它们在每一时刻的位置。如果一个屠杀机器人和 Sneaker 同时到达房间 \(n\),那么 Sneaker 被视为遇到了屠杀机器人,并且无法逃离迷宫。
数据组数 \(1\le T\le10^5\)。每组数据中 \(2\le n\le2\cdot10^5\),\(n-1\le m\le\min\{5\cdot10^5,\frac{n(n-1)}2\}\),\(1\le d\le n\),\(0\le k\le n-2\)。
保证 \(\sum n\le5\cdot10^5\),\(\sum m\le2\cdot10^6\)。
Sneaker 和杀戮机器人每次变更位置前后的间隔,我们称为一单位时间。
对于一个杀戮机器人,它的活动范围限制在 \(d\) 条边之内,并且一旦到达了某个点,它可以通过反复横跳的方式,每隔一单位时间就回到那个点。
因此,Sneaker 如果要经过某个点,他就必须在机器人之前到达这个点,或者和机器人错开到达这个点(也就是到达这个点的时间的奇偶性不同)。
首先用多源 BFS 求出任一机器人到达某个点 \(u\) 的最早偶数时间和最早奇数时间,设为 \(d_0(u)\) 和 \(d_1(u)\),其中的值不超过 \(d\)。
再用单源 BFS 求 \(1\) 到 \(n\) 的最短路,分别求到达某个点的最早偶数时间 \(t_0\) 和奇数时间 \(t_1\)。如果 \(d_{t_k\bmod 2}(u)\le t_k\),说明 \(u\) 不能走。
单组数据时间复杂度 \(O(n+m)\)。Source code - Virtual Judge
4.5 全源最短路 / Floyd-Warshall 算法
有向图最小环问题
求有向图的最小环长度。
设 \(d(i,i)=\infty\),然后跑 Floyd,最后得到的 \(d(i,i)\) 即为以 \(i\) 为起终点的最小环,取 \(\min\) 即可。时间 \(O(N^3)\)。
或者,枚举起点 \(s\) 跑 Dijkstra,同样设置初值 \(d(s)=\infty\),如果 \(s\) 被第二次弹出堆,则此时的 \(d(s)\) 即为包含 \(s\) 的最小环长度。时间 \(O(N(N+M)\log N)\)。
洛谷-P10927 [CEOI 1999] Sightseeing trip
求无向图的最小环。\(N\le100\),\(M\le10^4\),边权小于 \(500\) 且为正整数。
如果像上面一样做,则会得到 \(u\to v\to u\) 的退化的环。
考虑一个环上必然有 \(i,j,k\) 三个点,而 Floyd 的过程中,枚举 \(k\) 作为中间点之前,一定没用到 \(k\)。
因此在用 \(k\) 更新最短路之前,求 \(\min\{d(i,j)+W_{ik}+W_{kj}\}\) 即可,其中 \(W\) 为边权矩阵。
HDU-5036 Explosion(bitset 优化传递闭包)
有 \(N\) 个房间,每个房间有一扇门。每个房间里都有一些钥匙(可能没有),每把钥匙只能打开一扇门。
Matt 在不断开门。当 Matt 手里没有可以打开门的钥匙时,他会均匀地选择一扇未打开的门,并用炸弹摧毁。
问 Matt 将所有门打开需要使用炸弹的期望数量(显然初始时一定要使用一次炸弹)。\(N\le1000\)。
如果房间 \(i\) 里有钥匙 \(j\),就从 \(i\) 连向 \(j\),显然要到达 \(j\) 一定先要到达某一指向它的点。
如果炸开一个房间门 \(i\),就相当于同时打开了 \(i\) 能到达的所有房间。
设打开门 \(i\) 需要炸弹的期望数量为 \(E_i\)。根据期望的线性性质,求和即为答案。
那么,打开门 \(i\) 可能是通过直接用炸弹炸开 \(i\),也可能是先打开某一能到达 \(i\) 的门。前者用了 \(1\) 个炸弹,后者用了 \(0\) 个炸弹。
设能到达 \(i\) 的点的数量为 \(g_i\)(设 \(i\) 能到达它本身),则 \(E_i=1/g_i\)。这一值可以通过对反图跑传递闭包快速求出。
使用 bitset 优化,时间复杂度 \(O(N^3/w)\)。Source code - Virtual Judge
4.6 差分约束问题
难点通常在于转化和建图。
P4926
P5590
P3530

浙公网安备 33010602011771号