最短路
最短路
Floyd 算法
适用于无负环的图。
思路:枚举所有点对 \((i, j)\) 以及中转点 \(k\) ,再对邻接矩阵进行松弛操作。
时间复杂度 $ O(n^{3}) $ ,可以一次求出任意两点最短路。
inline void Floyd() {
for (int k = 1; k <= n; ++k) // 枚举中转点
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
a[i][j] = min(a[i][j], a[i][k] + a[k][j]);
}
bitset 优化传递闭包
用 Floyd 转递闭包时可以用 bitset
优化,时间复杂度 \(O(\dfrac{n^{3}}{\omega})\) 。
for (int k = 1; k <= n; ++k)
for (int i = 1; i <= n; ++i)
if (f[i][k])
f[i] = f[i] | f[k];
应用
给出一张无向图,第 \(i\) 个点在 \(t_i\) 时刻被修复,若 \(t_i = 0\) 则 \(i\) 未损坏。
\(q\) 次询问,每次询问 \(t\) 时刻 \(x\) 到 \(y\) 的最短路,保证给出的 \(t\) 不降。
\(n \leq 200\)
用 Floyd 求最短路,按修复时间枚举中转点松弛即可。
给一个正权无向图,找一个最小权值和的环。
枚举中转点 \(k\) 时,我们已经得到了前 \(k-1\) 个点的最短路径。\(x \rightsquigarrow y\) 、 \(y \to k\) 和 \(k \to x\) 共同构成了环,所以连接起来就得到了一个经过 \(x , y , k\) 的最小环。
Bellman–Ford 算法
首先介绍一下松弛操作:\(dis_v \leftarrow \min(dis_v, dis_u + w(u, v))\) 。
该算法不断尝试对图上每一条边进行松弛。由于每次松弛成功都会使得最短路长度增加 \(1\) ,所以循环 \(n - 1\) 次即可求出最短路。
时间复杂度 \(O(nm)\) 。
但还有一种情况,如果从 \(S\) 出发,抵达一个负环时,松弛操作会无休止地进行下去。注意到前面的论证中已经说明了,对于最短路存在的图,松弛操作最多只会执行 \(n - 1\) 轮。因此若第 \(n\) 轮循环时仍然存在能松弛的边,说明从 \(S\) 点出发能够抵达一个负环,但不能说明图中不存在负环。
SPFA 算法
经过队列优化的 Bellman-Ford 算法。
Bellman-Ford 算法中很多点是不用松弛的,只有上一次被松弛的结点所连接的边才有可能引起下一次松弛操作。
于是用队列来维护哪些结点可能会引起松弛操作,就能只访问必要的边了。
若要判负环,则记录一下每个点的松弛次数(即入队次数)即可。
SPFA 算法在随机图上时间复杂度为 \(O(km)\) ,但是可以被卡到 \(O(nm)\) 。
inline bool SPFA(int s) {
fill(dis + 1, dis + 1 + n, inf);
queue<int> q;
dis[s] = 0, q.emplace(s), inque[s] = true, cnt[s] = 1;
while (!q.empty()) {
int u = q.front();
q.pop(), inque[u] = false;
if (cnt[u] == n - 1)
return false;
for (int it : G.e[u]) {
int v = it.first, w = it.second;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
if (!inque[v])
q.emplace(v), inque[v] = true, ++cnt[v];
}
}
}
return true;
}
一般来说判负环的时候用 dfs 版的 SPFA 更快
bool SPFA(int u) {
vis[u] = true;
for (int i = G.head[u]; i; i = G.e[i].nxt) {
int v = e[i].v, w = e[i].w;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
if (vis[v] || !SPFA(v))
return false;
}
}
return vis[u] = false, 1;
}
优化
比较弱的优化:
- LLL 优化:使用双端队列,每次将入队结点距离和队内距离平均值比较,如果更大则插入至队尾,否则从队头插入。
- SLF 优化:使用双端队列,每次将入队结点距离和队首比较,如果更大则插入至队尾,否则从队头插入。
强一点的优化:
- SLF 带容错:每次将入队结点距离和队首比较,如果比队首大超过一定值则插入至队尾,否则从队头插入。
- mcfx 优化:定义区间 \([l, r]\) ,当入队点入队次数属于这个区间时从队首插入,否则从队头插入。通常取 \([2, \sqrt{n}]\) 。
- SLF + swap:每当队列改变时,如果队首距离大于队尾,则交换首尾。
- 这个 SLF 看起来很弱,但却通过了所有 Hack 数据,而且非常难卡。
玄学优化:
- 随机打乱边。
- 以一定概率从队首/队尾插入。
- 入队次数一定周期就随机打乱队列。
Hack
原理:让点尽可能多次入队,反复更新。
普通 SPFA 很好卡:
- 随机网格图:如果在网格图中走错了一次,就会走很多步无用步,于是就死了。
- 一个构造过的链套菊花:如果一个点被多个边连着,那么当这些边先依次走向它的时候,这个点就会被调用很多次,然后往下走。于是对于菊花的那个点会反复入队很多次,然后往下每次都要走很多次,于是就死了。
结合两种卡法,对于普通的 SPFA 最好的卡法就是将图构造为一个网格套链套菊花。
针对一些优化的 Hack 方法:
- LLL 优化:向 \(1\) 连一条权值巨大的边。
- SLF 优化:链套菊花,在链上用几个并列在一起的小边权边让它多次进入菊花。
- SLF 带容错:类似卡 SLF 的做法,注意要开打大边权总和才能有一定效果。
- mcfx 优化:菊花图。
- SLF + swap:与卡 SLF 类似,外挂诱导节点即可。但是卡的难度略大。
Dijkstra 算法
将结点分成两个集合:已确定最短路长度的点集 \(S\) 的和未确定最短路长度的点集 \(T\) 。
初始时所有的点都属于 \(T\) ,令 \(dis_s = 0\) ,其它点的 \(dis\) 均为 \(+ \infty\) 。
重复操作直到 \(T\) 为空:从 \(T\) 中选一个 \(dis\) 最小的点移到 \(S\) 中,并用该点松弛其它点。
Dijkstra 算法只能解决正权图上的最短路问题问题。
具体实现:
- 暴力:每次暴力找到 \(dis\) 最小的点松弛其它点,时间复杂度 \(O(n^2 + m) = O(n^2)\) 。
- 优先队列:每次松弛 \((u, v)\) 后将 \(v\) 插入优先队列中,每次从优先队列中选 \(dis\) 最小的点松弛其它点。由于不能在优先队列中删除元素,所以取出时要判重,时间复杂度 \(O(m \log n)\) 。
- 线段树:基本不用,将上面的操作改为单点修改和全局查询最小值,时间复杂度 \(O(m \log n)\) 。
优先队列优化的 Dijkstra 的实现:
inline void Dijkstra(int S) {
fill(dis + 1, dis + 1 + n, inf);
priority_queue<pair<int, int> > q;
dis[S] = 0, q.emplace(0, S);
while (!q.empty()) {
int u = q.top().second;
q.pop();
if (vis[u])
continue;
vis[u] = true;
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (dis[v] > dis[u] + w)
dis[v] = dis[u] + w, q.emplace(-dis[v], v);
}
}
}
另一种写法(取消了 \(vis\) 数组):
inline void Dijkstra(int S) {
fill(dis + 1, dis + 1 + n, inf);
priority_queue<pair<int, int> > q;
dis[S] = 0, q.emplace(0, S);
while (!q.empty()) {
auto c = q.top();
q.pop();
if (dis[c.second] != -c.first)
continue;
int u = c.second;
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (dis[v] > dis[u] + w)
dis[v] = dis[u] + w, q.emplace(-dis[v], v);
}
}
}
应用
给定一张图,每条边 \(s, t\) 有两个权值 \(w, v\) ,分别表示 \((s, t, w)\) 与 \((t, s, v)\) 的有向边,求边权和最小的包含 \(1\) 的简单环。
解法一:考虑对 \(1\) 的出点进行二进制拆分,每次按位将其拆为两个集合,将其中一个集合内的点向 \(n + 1\) 连边,并标记这条边不能走,那么一个环就被拆为 \(1\) 到 \(n + 1\) 的一条路径,不难证明一定有一种拆分方案满足原图中和 \(1\) 相连的两条边一定在新图中分别与 \(1, n + 1\) 相连。时间复杂度 \(O(m \log m \log n)\) 。
解法二:考虑一个简单环 \(S \to x \to y \to S\) ,且 \(S \to x\) 与 \(y \to S\) 不交。为了使答案最优,\(S \to x\) 与 \(y \to S\) 应当在满足无交集的情况下长度最短。先求出 \(S \to x\) 的最短路 \(d(x)\) 以及走的第一个点 \(p(x)\) ,再在反图上跑一边相同的流程,记为 \(d(y)\) 与 \(rp(y)\) 。接下来枚举所有有向边 \((u, v, w)\) :
- 若 \(u = S\) :不操作,因为这种情况会被其他情况覆盖。
- 若 \(v = S\) :
- 若 \(p(u) \neq u\) :则用 \(d(u) + w\) 更新答案。
- 若 \(p(u) = u\) :则 \(S \to u\) 走最短路径不合法,要经过其他的边,这种情况会被其他情况覆盖。
- 否则:
- 若 \(p(u) \neq rp(v)\) :则用 \(d(u) + w + rd(v)\) 更新答案。
- 若 \(p(u) = rp(v)\) :则 \(S \to u\) 和 \(v \to S\) 走最短路径有重叠,也要走其他的路径,这种情况也会被其他情况覆盖。
上面的更新方式可以覆盖所有的简单环情况,所以不会漏过然后一个可能的答案。同时对于经过某条边且可以更新的简单环情况,其长度是最优的。时间复杂度 \(O(m \log m)\) 。
Johnson 全源最短路
如果没有负权边,那直接跑 \(n\) 次 Dijkstra 即可。下面考虑怎么处理负权边。
建一个超级源点,所有点与其连一条边权为 \(0\) 的边。先用 SPFA 求每个点与超级源点的最短路径长度 \(h_i\) 。
将每条边 \(u \to v\) 的边权增加 \(h_u-h_v\) ,最后统计 \(i \to j\) 的最短路时减去 \(h_i - h_j\) 即可,于是就能直接跑 \(n\) 次 Dijkstra 了。
时间复杂度 \(O(km + nm \log m)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int inf = 1e9;
const int N = 3e3 + 7;
struct Graph {
vector<pair<int, int> > e[N];
inline void insert(const int u, const int v, const int w) {
e[u].emplace_back(v, w);
}
} G;
int dis[N][N];
int h[N], cnt[N];
bool inque[N];
int n, m;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
inline bool SPFA() {
fill(h + 1, h + 1 + n, inf);
queue<int> q;
q.emplace(0), inque[0] = true, ++cnt[0];
while (!q.empty()) {
int u = q.front();
q.pop(), inque[u] = false;
if (cnt[u] == n - 1)
return false;
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (h[v] > h[u] + w) {
h[v] = h[u] + w;
if (!inque[v])
q.emplace(v), inque[v] = true, ++cnt[v];
}
}
}
return true;
}
inline void Dijkstra(int S, int *dis) {
fill(dis + 1, dis + 1 + n, inf);
priority_queue<pair<int, int> > q;
dis[S] = 0, q.emplace(0, S);
while (!q.empty()) {
auto c = q.top();
q.pop();
if (-c.first != dis[c.second])
continue;
int u = c.second;
for (auto it : G.e[u]) {
int v = it.first, w = it.second + h[u] - h[v];
if (dis[v] > dis[u] + w)
dis[v] = dis[u] + w, q.emplace(-dis[v], v);
}
}
}
inline bool Johnson() {
for (int i = 1; i <= n; ++i)
G.insert(0, i, 0);
if (!SPFA())
return false;
for (int i = 1; i <= n; ++i) {
Dijkstra(i, dis[i]);
for (int j = 1; j <= n; ++j)
if (dis[i][j] != inf)
dis[i][j] -= h[i] - h[j];
}
return true;
}
signed main() {
n = read(), m = read();
for (int i = 1; i <= m; ++i) {
int u = read(), v = read(), w = read();
G.insert(u, v, w);
}
if (!Johnson())
return puts("-1"), 0;
for (int i = 1; i <= n; ++i) {
ll res = 0;
for (int j = 1; j <= n; ++j)
res += 1ll * j * dis[i][j];
printf("%lld\n", res);
}
return 0;
}
BFS 相关
在一些特殊的图上,可以用 BFS 求解最短路做到 \(O(n + m)\) 的时间复杂度。
- 无权图上的最短路直接用 BFS 求解即可。
- 01BFS:若边权仅有 \(0\) 和 \(1\) ,考虑有
deque
维护 BFS ,若走的边权为 \(0\) 则从队首入队,若走的边权为 \(1\) 则从队尾入队。
应用
一个 \(n \times m\) 的图,现在有一束激光从左上角往右边射出,每遇到
#
,你可以选择光线往四个方向射出,或者什么都不做。问最少需要多少个
#
往四个方向射出才能使光线在第 \(n\) 行往右边射出。\(n, m \leq 1000\)
将柱子改为 #
后,一条光线经过的时候实际效果是该行该列都会有光线。于是视该操作代价为 \(1\) 跑 BFS 即可。
#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2e3 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
queue<int> q;
int dis[N];
char str[N];
bool vis[N];
int n, m;
inline void bfs() {
memset(dis + 1, inf, sizeof(int) * (n + m));
dis[1] = 0, q.emplace(1), vis[1] = true;
while (!q.empty()) {
int u = q.front();
q.pop(), vis[u] = true;
for (int v : G.e[u])
if (!vis[v])
dis[v] = dis[u] + 1, q.emplace(v), vis[v] = true;
}
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) {
scanf("%s", str + 1);
for (int j = 1; j <= m; ++j)
if (str[j] == '#')
G.insert(i, j + n), G.insert(j + n, i);
}
bfs();
printf("%d", dis[n] == inf ? -1 : dis[n]);
return 0;
}
次短路
考虑每一条非最短路上的边 \(u \to v\) ,答案即为:
\(dis_{1, u}, dis_{v, n}\) 建立正反图跑两次 Dijkstra 即可求得。
求严格次短路时,我们不必记录最短路的路径,只需枚举每条边,若路径长度严格小于最短路时更新答案即可。
另一种方式是对于每个点都记录一下最短路与次短路,只要被更新就去松弛别的点。
P2865 [USACO06NOV] Roadblocks G
注意本题求的是严格次短路,下面给出第二种方法的实现。
#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 5e3 + 7;
struct Graph {
vector<pair<int, int> > e[N];
inline void insert(int u, int v, int w) {
e[u].emplace_back(v, w);
}
} G;
int dis[N][2];
int n, m;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
inline void Dijkstra(int S) {
memset(dis, inf, sizeof(dis));
priority_queue<pair<int, int> > q;
dis[S][0] = 0, q.emplace(-dis[S][0], S);
while (!q.empty()) {
int d = -q.top().first, u = q.top().second;
q.pop();
if (d != dis[u][0] && d != dis[u][1])
continue;
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (dis[v][0] > d + w)
dis[v][1] = dis[v][0], dis[v][0] = d + w, q.emplace(-dis[v][0], v);
else if (dis[v][0] < d + w && dis[v][1] > d + w)
dis[v][1] = d + w, q.emplace(-dis[v][1], v);
}
}
}
signed main() {
n = read(), m = read();
for (int i = 1; i <= m; ++i) {
int u = read(), v = read(), w = read();
G.insert(u, v, w), G.insert(v, u, w);
}
Dijkstra(1);
printf("%d", dis[n][1]);
return 0;
}
注意本题选取的第二短路径不会重复经过同一条路,所以只能把最短路上的边依次删去然后跑最短路。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e2 + 7;
struct Graph {
struct Edge {
int nxt, v;
double w;
} e[N * N];
int head[N];
int tot;
inline void insert(int u, int v, double w) {
e[++tot] = (Edge) {head[u], v, w}, head[u] = tot;
}
} G;
struct Point {
int x, y;
} a[N];
pair<int, int> pre[N];
double dis[N];
int n, m;
inline double dist(Point a, Point b) {
return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
inline double Dijkstra(int S, int restriction) {
priority_queue<pair<double, int> > q;
fill(dis + 1, dis + 1 + n, 1e9);
dis[S] = 0, q.push(make_pair(-dis[S], S));
while (!q.empty()) {
auto c = q.top();
q.pop();
if (-c.first != dis[c.second])
continue;
int u = c.second;
for (int i = G.head[u]; i; i = G.e[i].nxt) {
if (i == restriction)
continue;
int v = G.e[i].v;
double w = G.e[i].w;
if (dis[u] + w < dis[v]) {
dis[v] = dis[u] + w;
if (restriction == -1)
pre[v] = make_pair(u, i);
q.push(make_pair(-dis[v], v));
}
}
}
return dis[n];
}
signed main() {
n = read(), m = read();
for (int i = 1; i <= n; ++i)
a[i].x = read(), a[i].y = read();
for (int i = 1; i <= m; ++i) {
int u = read(), v = read();
G.insert(u, v, dist(a[u], a[v])), G.insert(v, u, dist(a[u], a[v]));
}
Dijkstra(1, -1);
double ans = 1e9;
for (int cur = n; cur != 1; cur = pre[cur].first)
ans = min(ans, Dijkstra(1, pre[cur].second));
printf("%.2lf", ans);
return 0;
}
最短路图
即求出所有最短路(多条也算)组成的 DAG,只需将 \(dis_v = dis_u + w\) 的边连边即可
某国有 \(n\) 座城市与 \(m\) 条单向铁路线,构成一张连通图。第 \(i\) 条单向铁路线由 \(v_{i, 1}, v_{i, 2}, \cdots, v_{i, s_i + 1}\) 城市组成,城市 \(v_{i, j}\) 通过该线路到城市 \(v_{i, j + 1}\) 花费的时间为 \(t_{i, j}\) 。
求 \(1\) 到 \(n\) 花费时间最少的情况下,经过任意两个相邻城市所花费时间的平方和的最大值。
\(n, m \leq 10^6\)
首先求出最短路图,那么只要在最短路图上找到平方和最大的路径。
这里的最短路图是 DAG, 于是可以按拓扑序设计 DP 。
设 \(dp_x\) 表示以 \(x\) 为终点的最大权值,枚举上一个换乘点,有:
斜率优化即可,复杂度瓶颈为最短路。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 1e6 + 7, S = 2e6 + 7;
struct Graph {
vector<pair<int, int> > e[N];
inline void insert(int u, int v, int w) {
e[u].emplace_back(make_pair(v, w));
}
} G, nG;
vector<pair<int, int> > belong[N];
vector<int> City[N], Time[N], ts[N], sta[S];
ll f[N];
int s[N], dis[N], id[N];
int n, m;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = c == '-';
while (c < '0' || c > '9')
c = getchar(), sign |= c == '-';
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
inline void Dijkstra(int S) {
fill(dis + 1, dis + 1 + n, inf);
priority_queue<pair<int, int> > q;
dis[S] = 0, q.push(make_pair(-dis[S], S));
while (!q.empty()) {
auto c = q.top();
q.pop();
if (-c.first != dis[c.second])
continue;
int u = c.second;
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (dis[v] > dis[u] + w)
dis[v] = dis[u] + w, q.push(make_pair(-dis[v], v));
}
}
}
inline void prework() {
int cnt = 0;
for (int i = 1; i <= m; ++i) {
ts[i].resize(s[i] + 1);
for (int j = 0; j <= s[i]; ++j) {
if (!j) {
ts[i][j] = ++cnt;
continue;
}
int u = City[i][j - 1], v = City[i][j], w = Time[i][j - 1];
if (dis[u] + w > dis[v])
ts[i][j] = ++cnt;
else
ts[i][j] = ts[i][j - 1];
}
}
}
inline bool cmp(const int &x, const int &y) {
return dis[x] < dis[y];
}
inline ll slope(int x, int d) {
return -2ll * dis[x] * d + 1ll * dis[x] * dis[x] + f[x];
}
inline bool check(int a, int b, int c) {
ll ka = -2ll * dis[a], kb = -2ll * dis[b], kc = -2ll * dis[c];
ll ta = 1ll * dis[a] * dis[a] + f[a];
ll tb = 1ll * dis[b] * dis[b] + f[b];
ll tc = 1ll * dis[c] * dis[c] + f[c];
return (__int128) (tc - ta) * (ka - kb) >= (__int128) (tb - ta) * (ka - kc);
}
signed main() {
n = read(), m = read();
for (int i = 1; i <= m; ++i) {
s[i] = read();
int u = read();
City[i].emplace_back(u);
belong[u].emplace_back(make_pair(i, 0));
for (int j = 1; j <= s[i]; ++j) {
int w = read(), v = read();
G.insert(u, v, w);
City[i].emplace_back(v), Time[i].emplace_back(w);
belong[v].emplace_back(make_pair(i, j));
u = v;
}
}
Dijkstra(1), prework();
for (int i = 1; i <= n; ++i)
id[i] = i;
sort(id + 1, id + 1 + n, cmp);
for (auto it : belong[1])
sta[ts[it.first][it.second]].emplace_back(1);
for (int i = 2; i <= n; ++i)
if (dis[id[i]] < inf) {
int x = id[i];
for (auto it : belong[x]) {
int ns = ts[it.first][it.second];
if (sta[ns].empty())
continue;
while (sta[ns].size() >= 2 && slope(sta[ns][sta[ns].size() - 2], dis[x]) >= slope(sta[ns][sta[ns].size() - 1], dis[x]))
sta[ns].pop_back();
f[x] = max(f[x], slope(sta[ns][sta[ns].size() - 1], dis[x]) + 1ll * dis[x] * dis[x]);
}
for (auto it : belong[x]) {
int ns = ts[it.first][it.second];
if (!sta[ns].empty() && slope(sta[ns][sta[ns].size() - 1], dis[x]) >= slope(x, dis[x]))
continue;
while (sta[ns].size() >= 2 && check(sta[ns][sta[ns].size() - 2], sta[ns][sta[ns].size() - 1], x))
sta[ns].pop_back();
sta[ns].emplace_back(x);
}
}
printf("%d %lld", dis[n], f[n]);
return 0;
}
最短路径树(SPT)
即由最短路径组成的树,和最短路图的区别就是少了几条边。可以通过求解最短路时记录每个点的前驱更新节点求得。
但是很多情况下要求边权和最小。可以考虑贪心,在松弛时若遇到松弛前后边权相等时取边权较小者即可。
应用
给定一张无向简单带权连通图, 要求删边至最多剩余 \(k\) 条边,最大化删边后满足 \(1\) 到其最短路不变的点的数量。
建出 SPT 后从 \(1\) 开始找一个大小为 \(k + 1\) 的连通块即可。
CF1005F Berland and the Shortest Paths
给出一张无向无边权简单连通图,求 SPT 方案数与方案(若超过 \(k\) 种则只取 \(k\) 种即可)。
然后对每个点维护可能成为前驱节点的集合,此时总的方案数就是所有集合大小的乘积,求解方案直接暴力从每个集合中选一个元素组合即可。
差分约束系统
差分约束系统是一种特殊的 \(n\) 元一次不等式组。每个不等式都形如 \(x_i - x_j \leq c_k\) ,其中 \(c_k\) 为常数且 \(i \not = j\) 。需要求出一组整数解。
将每个不等式都转化为 \(x_i \leq x_j + c_k\) ,这与三角形不等式 \(dis_v \leq dis_u + w\) 十分相似。那么对于一组不等式 \(x_v - x_u \leq w\) ,建边 \((u, v, w)\) 。
从超级源点向每个点连一条边权为 \(0\) 的边,若建图后图中有负环则方程组无解,否则 \(x_i = dis_i\) 就是方程组的一组解。
若 \(\{ x_1, x_2, \cdots, x_n \}\) 是方程的一组解,则 \(\{ x_1 + d, x_2 + d, \cdots, x_n + d \}\) 也是方程的一组解。
tricks:
- \(x_i - x_j < c_k\) 可以转化为 \(x_i - x_j \leq c_k - 1\) 。
- \(x_i = x_j\) 可以转化为 \(x_i - x_j \leq 0\) 且 \(x_j - x_i \leq 0\) 。
差分约束系统的一个性质:如果跑的是最短路,则固定一个值时,其余的值都会取到最大值。
若 \(v_0 \to v_u\) 经过的路径为
\[v_{i_1} - v_0 \leq l_0 \\ v_{i_2} - v_{i_1} \leq l_1 \\ \cdots \\ v_u - v_{i_k} \leq l_k \]则加起来得到 \(v_u - v_0 \leq l_0 + l_1 + \cdots + l_k = dist(0, u)\) 。
模板:P1993 小 K 的农场 P3275 [SCOI2011] 糖果
应用
给出 \(m\) 组不等式:
\[\begin{cases} x_{a_i} \geq (k_i - t) \times x_{b_i} \\ (k_i + t) \times x_{a_i} \geq x_{b_i} \end{cases} \]求最大使得不等式组无解的 \(t\) 。
\(n, m \leq 1000\)
考虑用对数将乘法化为加减:
二分 \(t\) 跑 SPFA 判断有无负环即可。
有 \(n\) 个砝码,分别重 \(x_{1 \sim n}\) ,给出一些重量大小关系,求有多少对 \(x_a, x_b, x_c, x_d\) 一定满足:
- \(x_a + x_b > x_c + x_d\)
- \(x_a + x_b = x_c + x_d\)
- \(x_a + x_b < x_c + x_d\)
分别求解三种情况的方案数。
\(n \leq 50\)
建立差分约束系统后,先跑一边最短路和最长路,求出 \(i, j\) 之间的质量差最小值 \(mn_{i, j}\) 和最大值 \(mx_{i, j}\) 。
转化一下三种情况:
- \(x_a - x_c > x_b - x_d\) ,即 \(mn_{a, c} > mx_{b, d}\) 。
- \(x_a - x_c = x_b - x_d\) ,即 \(mn_{a, c} = mx_{a, c} = mn_{b, d} = mx_{b,d}\) 。
- \(x_a - x_c < x_b - x_d\) ,即 \(mn_{a, c} < mx_{b, d}\) 。
分别统计方案数即可。
构造长度为 \(n\) 的字典序最小的 \(01\) 字符串,满足 \(m\) 组子串 \([l_i, r_i]\) 含相同数量的 \(0\) 和 \(1\) 。
\(n \leq 10^6, m \leq 2 \times 10^5\) ,保证 \(r- l + 1\) 是偶数
将 \(0\) 当作 \(1\) ,\(1\) 当作 \(-1\) 。因为要让答案的字典序最小,即 \(s_i\) 尽可能大,即需要求出字典序最大的一组解,于是可以用差分约束系统求解。
相邻两个位置的限制从 \(|s_i - s_{i - 1}| = 1\) 弱化为 \(|s_i - s_{i - 1}| \leq 1\) 。
不可能存在 \(s_{i - 1} = s_i\) :若存在可以构造出 \(\pm 1\) 交错的 \(s\) 使得字典序更大。
对于一组限制,转化为 \(s_{l - 1} = s_r\) 。
于是 01bfs 即可求解。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7;
struct Graph {
vector<pair<int, int> > e[N];
inline void insert(int u, int v, int w) {
e[u].emplace_back(v, w);
}
} G;
int dis[N];
int n, m;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
inline void bfs() {
fill(dis, dis + 1 + n, -1);
deque<int> q;
dis[0] = 0, q.emplace_back(0);
while (!q.empty()) {
int u = q.front();
q.pop_front();
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (dis[v] == -1) {
dis[v] = dis[u] + w;
if (w)
q.emplace_back(v);
else
q.emplace_front(v);
}
}
}
}
signed main() {
n = read(), m = read();
for (int i = 1; i <= n; ++i)
G.insert(i - 1, i, 1), G.insert(i, i - 1, 1);
for (int i = 1; i <= m; ++i) {
int l = read(), r = read();
G.insert(l - 1, r, 0), G.insert(r, l - 1, 0);
}
bfs();
for (int i = 1; i <= n; ++i)
putchar(dis[i] < dis[i - 1] ? '1' : '0');
return 0;
}
同余最短路
同余最短路利用同余来构造一些状态,并将其看作单源最短路中的点。
给出 \(x, y, z, h\) ,求有多少 \(k \in [1, h]\) 满足 \(ax + by + cz = k\) 。
\(x, y, z \leq 10^5, h \leq 2^{63} - 1\)
不妨设 \(x < y < z\) 。
令 \(d_i\) 表示仅通过 \(by + cz\) 后能得到的模 \(x\) 下与 \(i\) 同余的最小数,用来计算该同余类满足条件的数个数。可以建边:\((i, (i + y) \bmod x, y), (i, (i + z) \bmod x, z)\) ,于是跑一次最短路即可求出 \(d_i\) 。
令 \(1\) 作为源点,此时 \(dis_1 = 1\) 最小,即可得到最小的一组解,类比差分约束即可得到所有解。
答案即为:
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = (1ull << 63) - 1;
const int N = 1e5 + 7;
struct Graph {
vector<pair<int, int> > e[N];
inline void insert(int u, int v, int w) {
e[u].emplace_back(v, w);
}
} G;
ll dis[N];
ll h, x, y, z;
inline void Dijkstra() {
fill(dis, dis + x, inf);
priority_queue<pair<ll, int> > q;
dis[0] = 0, q.emplace(-dis[0], 0);
while (!q.empty()) {
auto c = q.top();
q.pop();
if (-c.first != dis[c.second])
continue;
int u = c.second;
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (dis[v] > dis[u] + w)
dis[v] = dis[u] + w, q.emplace(-dis[v], v);
}
}
}
signed main() {
scanf("%lld%lld%lld%lld", &h, &x, &y, &z);
if (x == 1 || y == 1 || z == 1)
return printf("%lld\n", h), 0;
--h;
for (int i = 0; i < x; ++i)
G.insert(i, (i + y) % x, y), G.insert(i, (i + z) % x, z);
Dijkstra();
ll ans = 0;
for (int i = 0; i < x; ++i)
if (h >= dis[i])
ans += (h - dis[i]) / x + 1;
printf("%lld", ans);
return 0;
}
[ABC077D/ARC077B] Small Multiple
给定一个整数 \(K\)。求一个 \(K\) 的正整数倍 \(S\),使得 \(S\) 的数位累加和最小。
\(2 \leq K \leq 10^5\) 。
注意到一个数都可以通过 \(+1\) 和 \(\times 10\) 得到。\(+1\) 时数位累加和增加,\(\times 10\) 时不变。
因为不需要求出具体数值,输出数位累加和即可,所以我们在 \(\bmod k\) 意义下利用同余最短路配合 01BFS 计算即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7;
deque<pair<int, int> > q;
bool vis[N];
int K, ans;
signed main() {
scanf("%d", &K);
q.emplace_back(1, 1), vis[1] = true;
while (!q.empty()) {
int num = q.front().first, w = q.front().second;
q.pop_front();
if (!num) {
printf("%d", w);
break;
}
if (!vis[num * 10 % K])
vis[num * 10 % K] = true, q.emplace_front(num * 10 % K, w);
if (!vis[num + 1])
q.emplace_back(num + 1, w + 1);
}
return 0;
}
删边最短路
给出一张无向带正权图, \(q\) 次询问,每次询问给出 \(t, x\),求若将 \(t\) 这条边的长度修改为 \(x\) 时 \(1\) 到 \(n\) 的最短路长度。
\(n, m, q \leq 2 \times 10^5\)
首先,若这条边不在最短路上,则答案要么为原来的最短路,要么为经过这条边的最短路,即:
否则又分两种情况。若走这条边,答案为 \(dis_{1, u} + w(u, v) + dis_{v, n}\) 。
若不走这条边,设删掉这条边后找出的最短路为 \(E\),共有 \(k\) 条边分别为 \(e_{1 \sim k}\) 。
结论:删掉任意一条边后,一定存在一条 \(1\) 到 \(n\) 的最短路有一个前缀(可能为空)和 \(E\) 重合,有一个后缀(也可能为空)和 \(E\) 重合,中间的部分都不在 \(E\) 上。
若有两段不在 \(E\) 上,因为只删掉了一条边,所以将其中一段换为 \(E\) 上的一段一定不劣。
设:
- \(l_x\) 表示最小的 \(i\) 使得在某条 \(1 \to x\) 的最短路上 \(e_i\) 是第一条 \(E\) 上的不在其中的边。
- \(r_x\) 表示最大的 \(i\) 使得在某条 \(x\to n\) 的最短路上 \(e_i\) 是最后一条 \(E\) 上的不在其中的边。
考虑求 \(l_x, r_x\) 。首先以 \(1\) 和 \(n\) 为源点分别求一遍最短路,找出一条最短路 \(E\) 。对于 \(E\) 上的第 \(i\) 个点 \(x\),初始化 \(l_x = i, r_x = i - 1\) 。
以 \(l\) 为例,\(r\) 同理。若边 \((u,v)\) 满足 \(d_{1,u}+w_{u,v}=d_{1,v}\),则 \(l_v=\min(l_v,l_u)\)。按照 \(dis_{1, i}\) 排序后则可以线性更新。注意此时需要满足 \(1\to x\) 和 \(E\) 只有一个前缀重合,所以不能用 \(E\) 上的边更新。
记 \(a_i\) 为删掉 \(e_i\) 之后的答案。求出 \(l, r\) 后枚举不在 \(E\) 上的边 \((u,v)\),用 \(d_{1,u}+w_{u,v}+d_{v,n}\) 更新 \([a_{l_u},a_{r_v}]\),用 \(d_{1,v}+w_{u,v}+d_{u,n}\) 更新 \([a_{l_v},a_{r_u}]\)。需要支持区间取 \(\min\) ,单点查询。因为查询都在修改之后,用 multiset
做一遍扫描线即可。
时间复杂度 \(O(m \log n + q)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 1e18;
const int N = 2e5 + 7;
struct Graph {
struct Edge {
int nxt, v, w;
} e[N << 1];
int head[N];
int tot = 1;
inline void insert(int u, int v, int w) {
e[++tot] = (Edge) {head[u], v, w}, head[u] = tot;
}
} G;
struct Edge {
int u, v, w, id;
} e[N];
vector<ll> ins[N], rmv[N];
ll dis1[N], disn[N], ans[N];
int l[N], r[N];
int n, m, q, Len = 1;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
inline void Dijkstra(int S, ll *dis) {
fill(dis + 1, dis + 1 + n, inf);
priority_queue<pair<ll, int> > q;
dis[S] = 0, q.emplace(-dis[S], S);
while (!q.empty()) {
auto c = q.top();
q.pop();
if (-c.first != dis[c.second])
continue;
int u = c.second;
for (int i = G.head[u]; i; i = G.e[i].nxt) {
int v = G.e[i].v, w = G.e[i].w;
if (dis[v] > dis[u] + w)
dis[v] = dis[u] + w, q.emplace(-dis[v], v);
}
}
}
signed main() {
n = read(), m = read(), q = read();
for (int i = 1; i <= m; ++i) {
e[i].u = read(), e[i].v = read(), e[i].w = read();
G.insert(e[i].u, e[i].v, e[i].w), G.insert(e[i].v, e[i].u, e[i].w);
}
Dijkstra(1, dis1), Dijkstra(n, disn);
fill(l + 1, l + 1 + n, n + 1), fill(r + 1, r + 1 + n, 0);
for (int u = 1; u != n;) {
l[u] = ++Len, r[u] = Len - 1;
for (int i = G.head[u]; i; i = G.e[i].nxt) {
int v = G.e[i].v, w = G.e[i].w;
if (disn[v] + w == disn[u]) {
u = v, e[i / 2].id = Len;
break;
}
}
}
l[n] = ++Len, r[n] = Len - 1;
vector<int> id(n);
iota(id.begin(), id.end(), 1);
sort(id.begin(), id.end(), [](const int &a, const int &b) { return dis1[a] < dis1[b]; });
for (int u : id)
for (int i = G.head[u]; i; i = G.e[i].nxt) {
int v = G.e[i].v, w = G.e[i].w;
if (!e[i / 2].id && dis1[u] + w == dis1[v])
l[v] = min(l[v], l[u]);
}
sort(id.begin(), id.end(), [](const int &a, const int &b) { return disn[a] < disn[b]; });
for (int u : id)
for (int i = G.head[u]; i; i = G.e[i].nxt) {
int v = G.e[i].v, w = G.e[i].w;
if (!e[i / 2].id && disn[u] + w == disn[v])
r[v] = max(r[v], r[u]);
}
for (int i = 1; i <= m; ++i) {
if (e[i].id)
continue;
int u = e[i].u, v = e[i].v, w = e[i].w;
if (l[u] <= r[v]) {
ins[l[u]].emplace_back(dis1[u] + w + disn[v]);
rmv[r[v]].emplace_back(dis1[u] + w + disn[v]);
}
if (l[v] <= r[u]) {
ins[l[v]].emplace_back(dis1[v] + w + disn[u]);
rmv[r[u]].emplace_back(dis1[v] + w + disn[u]);
}
}
multiset<ll> st;
for (int i = 1; i <= Len; ++i) {
for (ll it : ins[i])
st.insert(it);
ans[i] = st.empty() ? inf : *st.begin();
for (ll it : rmv[i])
st.erase(st.find(it));
}
while (q--) {
int x = read(), k = read();
int u = e[x].u, v = e[x].v, w = e[x].w;
if (e[x].id)
printf("%lld\n", min(dis1[n] + k - w, ans[e[x].id]));
else
printf("%lld\n", min(dis1[n], min(dis1[u] + k + disn[v], dis1[v] + k + disn[u])));
}
return 0;
}