最短路
最短路
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)
dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
}
用 Floyd 转递闭包时可以用 bitset
优化,时间复杂度 \(O(\frac{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];
P1119 灾后重建
给出一张无向图,第 \(i\) 个点在 \(t_i\) 时刻被修复,若 \(t_i = 0\) 则 \(i\) 未损坏。
\(q\) 次询问,每次询问 \(t\) 时刻 \(x\) 到 \(y\) 的最短路,保证给出的 \(t\) 不降。
\(n \le 200\)
用 Floyd 求最短路,按修复时间枚举中转点松弛即可,时间复杂度 \(O(n^3)\) 。
#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2e2 + 7;
int dis[N][N], t[N];
int n, m, q;
signed main() {
scanf("%d%d", &n, &m);
for (int i = 0; i < n; ++i)
scanf("%d", t + i);
memset(dis, inf, sizeof(dis));
for (int i = 0; i < n; ++i)
dis[i][i] = 0;
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;
}
scanf("%d", &q);
int k = 0;
while (q--) {
int x, y, w;
scanf("%d%d%d", &x, &y, &w);
while (t[k] <= w && k < n) {
for (int i = 0; i < n; ++i)
for (int j = 0; j < n; ++j)
dis[i][j] = dis[j][i] = min(dis[i][j], dis[i][k] + dis[k][j]);
++k;
}
printf("%d\n", t[x] > w || t[y] > w || dis[x][y] == inf ? -1 : dis[x][y]);
}
return 0;
}
P6175 无向图的最小环问题
给一个正权无向图,找一个最小权值和的环,或报告无解。
\(n \le 100\)
枚举中转点 \(k\) 时,不难发现此时前 \(k-1\) 个点的最短路径已经求得。而 \(x \to y \to k \to x\) 连接起来就得到了一个经过 \(x , y , k\) 的最小环,因此只要 Floyd 的时候顺带求出即可,时间复杂度 \(O(n^3)\) 。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f3f;
const int N = 1e2 + 7;
int e[N][N], dis[N][N];
int n, m;
signed main() {
scanf("%d%d", &n, &m);
memset(e, inf, sizeof(e));
for (int i = 1; i <= n; i++)
e[i][i] = 0;
for (int i = 1; i <= m; ++i) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
e[u][v] = e[v][u] = min(e[u][v], w);
}
memcpy(dis, e, sizeof(e));
int ans = inf;
for (int k = 1; k <= n; ++k) {
for (int i = 1; i < k; ++i)
for (int j = i + 1; j < k; ++j)
if (dis[i][j] != inf && e[i][k] != inf && e[k][j] != inf)
ans = min(ans, dis[i][j] + e[i][k] + e[k][j]);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
dis[j][i] = dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
}
if (ans == inf)
printf("No solution.");
else
printf("%lld", ans);
return 0;
}
Bellman–Ford
定义松弛操作为 \(dis_v \gets \min(dis_v, dis_u + w(u, v))\) 。
Bellman–Ford 算法不断尝试对图上每一条边进行松弛,循环 \(n - 1\) 次即可求出最短路。时间复杂度 \(O(nm)\) 。
若第 \(n\) 轮循环时仍然存在能松弛的边,说明从 \(S\) 点出发能够抵达一个负环。
SPFA
即队列优化的 Bellman-Ford。
事实上 Bellman-Ford 中会进行很多次无效松弛操作,只有上一次被松弛的结点所连接的边才有可能引起下一次松弛操作。
于是考虑用队列来维护哪些结点可能会引起松弛操作,就能只访问必要的边了。
若要判负环,则记录一下每个点的松弛次数即可,若被松弛超过 \(n\) 次则说明走到了负环。
SPFA 算法在随机图上时间复杂度为 \(O(km)\) (\(k\) 为常数),但是可以被卡到 \(O(nm)\) 。
inline bool SPFA(int S) {
memset(dis + 1, inf, sizeof(int) * n);
memset(cnt + 1, 0, sizeof(int) * n);
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;
for (int it : G.e[u]) {
int v = it.first, w = it.second;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w, ++cnt[v];
if (cnt[v] >= n)
return false;
if (!inque[v])
q.emplace(v), inque[v] = true;
}
}
}
return true;
}
一般来说判负环的时候用 dfs 版的 SPFA 更快
bool SPFA(int u) {
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;
if (vis[v] || !SPFA(v))
return false;
}
}
return vis[u] = false, true;
}
常用的一些优化:
-
LLL 优化:使用双端队列,每次将入队结点距离和队内距离平均值比较,如果更大则插入至队尾,否则从队头插入。
-
SLF 优化:使用双端队列,每次将入队结点距离和队首比较,如果更大则插入至队尾,否则从队头插入。
-
SLF 带容错:每次将入队结点距离和队首比较,如果比队首大超过一定值则插入至队尾,否则从队头插入。
-
mcfx 优化:定义区间 \([l, r]\) ,当入队点入队次数属于这个区间时从队首插入,否则从队头插入。通常取 \([2, \sqrt{n}]\) 。
-
SLF + swap:每当队列改变时,如果队首距离大于队尾,则交换首尾。
-
较为玄学的优化。
- 随机打乱边。
- 以一定概率从队首/队尾插入。
- 入队次数一定周期就随机打乱队列。
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)\) 。
需要权衡 \(O(n^2)\) 和 \(O(m \log n)\) 两种实现方式的优劣,一般稠密图使用 \(O(n^2)\) ,稀疏图用 \(O(m \log n)\) 。
\(O(n^2)\) 的实现:
inline void Dijkstra(int S) {
memset(dis + 1, inf, sizeof(int) * n);
dis[S] = 0;
for (;;) {
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 (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (dis[v] > dis[u] + w)
dis[v] = dis[u] + w;
}
}
}
优先队列优化的 Dijkstra 的实现:
inline void Dijkstra(int S) {
memset(dis + 1, inf, sizeof(int) * n);
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) {
memset(dis + 1, inf, sizeof(int) * n);
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);
}
}
}
P11131 【MX-X5-T3】「GFOI Round 1」Cthugha
给定一张 \(n \times m\) 的网格图,每个格子都有一个权值。
给定 \(q\) 个点,找到一个点,最小化其到这 \(q\) 个点的路径权值的最大值,若距离可以无限小则输出
No
。其中路径可以重复经过格子,多次经过同一个格子时权值重复计算。\(n \times m \le 10^5\) ,\(q \le 50\)
首先若相邻的两个格子加起来值 \(< 0\) 则无解,因为可以反复横跳。
此时把点权放在边上即可规避掉负权的限制,然后以每个人为源点跑 Dijkstra 即可。
由于 Dijkstra 的正确性基于第一次取到这个点是就是最优答案,而在这个图中如果出去绕一下回来距离变小了,证明不存在最小值。如果存在最小值,绕回来之后一定距离变大,满足条件。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int dx[] = {1, -1, 0, 0};
const int dy[] = {0, 0, 1, -1};
const int N = 1e5 + 7;
int n, m, q;
signed main() {
scanf("%d%d%d", &n, &m, &q);
vector<vector<int> > a(n, vector<int>(m));
for (int i = 0; i < n; ++i)
for (int &it : a[i])
scanf("%d", &it);
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j) {
if (i + 1 < n && a[i][j] + a[i + 1][j] < 0)
return puts("No"), 0;
else if (j + 1 < m && a[i][j] + a[i][j + 1] < 0)
return puts("No"), 0;
}
vector<vector<ll> > res(n, vector<ll>(m, -1e18));
auto Dijkstra = [&](int bx, int by) {
vector<vector<ll> > dis(n, vector<ll>(m, 1e18));
priority_queue<tuple<ll, int, int> > q;
dis[bx][by] = 0, q.emplace(-dis[bx][by], bx, by);
while (!q.empty()) {
auto c = q.top();
q.pop();
if (dis[get<1>(c)][get<2>(c)] != -get<0>(c))
continue;
int x = get<1>(c), y = get<2>(c);
res[x][y] = max(res[x][y], (dis[x][y] + a[x][y] + a[bx][by]) / 2);
for (int i = 0; i < 4; ++i) {
int nx = x + dx[i], ny = y + dy[i];
if (0 <= nx && nx < n && 0 <= ny && ny < m && dis[nx][ny] > dis[x][y] + a[x][y] + a[nx][ny])
dis[nx][ny] = dis[x][y] + a[x][y] + a[nx][ny], q.emplace(-dis[nx][ny], nx, ny);
}
}
};
while (q--) {
int x, y;
scanf("%d%d", &x, &y);
Dijkstra(x - 1, y - 1);
}
ll ans = 1e18;
for (int i = 0; i < n; ++i)
ans = min(ans, *min_element(res[i].begin(), res[i].end()));
printf("%lld", ans);
return 0;
}
P5304 [GXOI/GZOI2019] 旅行者
给定一张有向图和 \(k\) 个关键点,求关键点两两之间最短路的最小值。
\(k \le n \le 10^5\) ,\(m \le 5 \times 10^5\)
考虑两个关键点 \(x, y\) 之间的最短路,先不考虑 \(x, y\) 直接连边的情况。记路径上非端点的一个点为 \(z\) ,则路径形如 \(x \to z \to y\) 。
于是考虑对正图和反图各跑一遍以关键点为源点的最短路,则对于一个非关键点 \(z\) ,关键点之间经过 \(z\) 的最短路径即为正反两次 \(dis_z\) 的和,对所有 \(z\) 的贡献取 \(\min\) 即可求出答案。
但是这样是假的,因为可能出现 \(x = y\) 的情况。一个简单的想法是跑次短路,但是并不优美。
考虑不枚举路径上的点,转而枚举路径上的边,再用两端正反图上起点不同的边更新即可。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 0x3f3f3f3f3f3f3f3f;
const int N = 1e5 + 7;
struct Graph {
vector<pair<int, int> > e[N];
inline void clear(int n) {
for (int i = 1; i <= n; ++i)
e[i].clear();
}
inline void insert(int u, int v, int w) {
e[u].emplace_back(v, w);
}
} G, rG;
vector<int> kp;
ll dis1[N], dis2[N];
int st1[N], st2[N];
int n, m, k;
inline void Dijkstra(Graph &G, ll *dis, int *st) {
memset(dis + 1, inf, sizeof(ll) * n);
memset(st + 1, 0, sizeof(int) * n);
priority_queue<pair<ll, int> > q;
for (int it : kp)
dis[it] = 0, st[it] = it, q.emplace(-dis[it], it);
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, st[v] = st[u], q.emplace(-dis[v], v);
}
}
}
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d%d", &n, &m, &k);
kp.resize(k), G.clear(n), rG.clear(n);
for (int i = 1; i <= m; ++i) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
if (u != v)
G.insert(u, v, w), rG.insert(v, u, w);
}
for (int &it : kp)
scanf("%d", &it);
Dijkstra(G, dis1, st1), Dijkstra(rG, dis2, st2);
ll ans = inf;
for (int u = 1; u <= n; ++u)
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (st1[u] && st2[v] && st1[u] != st2[v])
ans = min(ans, dis1[u] + w + dis2[v]);
}
printf("%lld\n", ans);
}
return 0;
}
P7407 [JOI 2021 Final] 机器人 / Robot
给出一张无向图,每条边有颜色 \(c\) 和代价 \(w\) 。
定义 \(u\) 的一条出边是好的,当且仅当其不与 \(u\) 的其他任意出边颜色相同。
对于一条边,可以花其代价修改颜色。求 \(1\) 只走好边能到 \(n\) 的最小代价,或报告无解。
\(n \le 10^5\) ,\(m \le 2 \times 10^5\)
需要走 \((u, v, c, w)\) 这条边时,若其为 \(u\) 的好边,则直接走就行。否则要么修改它的颜色,要么修改 \(u\) 其余同色边的颜色。记 \(sum_{u, c}\) 表示 \(u\) 所有颜色 \(c\) 的出边的代价和,则将这条边的边权设为 \(\min(w, sum_{u, c} - w)\) 。称前者为操作一,后者为操作二。
但是这样没有考虑到一种情况:若 \(x \to y \to z\) 两边颜色相同,\(x \to y\) 使用操作一,\(y \to z\) 使用操作二,则只要付出 \(sum_{y, c} - w_{y \to z}\) 的代价。
考虑建虚点处理这种情况。对于 \(u = y\) 所有颜色为 \(c\) 的出边 \((u, v, c, w)\)(数量 \(\ge 2\) ),新建一个虚点 \(u_c\) ,将 \(v\) 向该 \(u_c\) 连一条边权为 \(0\) 的边,而 \(u_c\) 向 \(v\) 连一条边权为 \(sum_{u, c} - w\) 的边,则原先 \(x \to y \to z\) 的决策还可以被 \(x \to y_c \to z\) 的决策替代。
Dijkstra 跑最短路即可,由于虚点的数量上界为 \(2m\) ,时间复杂度 \(O((n + m) \log (n + m))\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 0x3f3f3f3f3f3f3f3f;
const int N = 5e5 + 7;
struct Graph {
vector<pair<int, ll> > e[N];
inline void insert(int u, int v, ll w) {
e[u].emplace_back(v, w);
}
} G;
map<int, vector<pair<int, ll> > > mp[N];
map<int, ll> sum[N];
ll dis[N];
int n, m, tot;
inline void Dijkstra(int S) {
memset(dis + 1, 0x3f, sizeof(ll) * tot);
priority_queue<pair<ll, 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;
ll w = it.second;
if (dis[v] > dis[u] + w)
dis[v] = dis[u] + w, q.emplace(-dis[v], v);
}
}
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v, c, w;
scanf("%d%d%d%d", &u, &v, &c, &w);
mp[u][c].emplace_back(v, w), sum[u][c] += w;
mp[v][c].emplace_back(u, w), sum[v][c] += w;
}
tot = n;
for (int u = 1; u <= n; ++u)
for (auto it : mp[u]) {
++tot;
for (auto x : it.second) {
int c = it.first, v = x.first;
ll w = x.second;
G.insert(u, v, it.second.size() == 1 ? 0 : min(w, sum[u][c] - w));
G.insert(v, tot, 0), G.insert(tot, v, sum[u][c] - w);
}
}
Dijkstra(1);
printf("%lld", dis[n] == inf ? -1 : dis[n]);
return 0;
}
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 = 0x3f3f3f3f;
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], h[N], cnt[N];
bool inque[N];
int n, m;
inline bool SPFA() {
memset(h + 1, inf, sizeof(int) * n);
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) {
memset(dis + 1, inf, sizeof(int) * n);
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() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v, w;
scanf("%d%d%d", &u, &v, & w);
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] == inf ? 1e9 : dis[i][j]);
printf("%lld\n", res);
}
return 0;
}
BFS 相关
在一些特殊的图上,可以用 BFS 求解最短路做到 \(O(n + m)\) 的时间复杂度。
- 无权图最短路:直接 BFS 即可。
- 01BFS:若边权只有 \(0\) 和 \(1\) ,考虑用
deque
维护 BFS ,若走的边权为 \(0\) 则从队首入队,若走的边权为 \(1\) 则从队尾入队。
CF173B Chamber of Secrets
一个 \(n \times m\) 的图,现在有一束激光从左上角往右边射出,每遇到
#
,你可以选择光线往四个方向射出,或者什么都不做。问最少需要多少个
#
往四个方向射出才能使光线在第 \(n\) 行往右边射出。\(n, m \le 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;
}
P9351 [JOI 2023 Final] 迷宫 / Maze
给出一张 \(n \times m\) 的网格图,其中一些格子为障碍。一次操作可以清空一个 \(k \times k\) 的矩阵内所有障碍,求使得\((x_1, y_1)\) 与 \((x_2, y_2)\) 联通的最少操作数。
\(n \times m \le 6 \times 10^6\) ,\(k \le \min(n, m)\)
考虑建立最短路模型,将 \((x, y)\) 向四联通的非障碍格子连边权为 \(0\) 的边,将 \((x, y)\) 向以其为中心、边长为 \(2k + 1\) 的格子(挖掉四个角)连边权为 \(1\) 的边,跑 01-bfs 即可。但是该做法边权为 \(1\) 的边数量为 \(O(nmk^2)\) 级别,无法接受。
考虑将第二类操作转化为走一步四联通后,接下来 \(k - 1\) 步可以走八连通。那么一个点的状态需要记录距离和接下来能走八连通的步数,以前者作为第一关键字(从小到大)、后者作为第二关键字(从大到小)跑 01-bfs 即可做到 \(O(nm)\) 。
#include <bits/stdc++.h>
using namespace std;
const int dx[] = {1, -1, 0, 0, 1, -1, 1, -1};
const int dy[] = {0, 0, 1, -1, 1, -1, -1, 1};
const int N = 6e6 + 7;
char buf1[N], *str[N];
bool buf2[N], *vis[N];
int n, m, k, bx, by, ex, ey;
signed main() {
scanf("%d%d%d%d%d%d%d", &n, &m, &k, &bx, &by, &ex, &ey);
--bx, --by, --ex, --ey;
str[0] = buf1, vis[0] = buf2;
for (int i = 0; i < n; ++i)
scanf("%s", str[i]), str[i + 1] = str[i] + m, vis[i + 1] = vis[i] + m;
deque<tuple<int, int, int, int> > q;
q.emplace_back(bx, by, 0, 0);
while (!q.empty()) {
int x = get<0>(q.front()), y = get<1>(q.front()), h = get<2>(q.front()), d = get<3>(q.front());
q.pop_front();
if (vis[x][y])
continue;
vis[x][y] = true;
if (x == ex && y == ey)
return printf("%d", d), 0;
if (h) {
for (int i = 0; i < 8; ++i) {
int nx = x + dx[i], ny = y + dy[i];
if (0 <= nx && nx < n && 0 <= ny && ny < m && !vis[nx][ny])
q.emplace_back(nx, ny, h - 1, d);
}
} else {
for (int i = 0; i < 4; ++i) {
int nx = x + dx[i], ny = y + dy[i];
if (0 <= nx && nx < n && 0 <= ny && ny < m && !vis[nx][ny]) {
if (str[nx][ny] == '#')
q.emplace_back(nx, ny, k - 1, d + 1);
else
q.emplace_front(nx, ny, 0, d);
}
}
}
}
return 0;
}
P3547 [POI 2013] CEN-Price List
给定一张无向连通图,初始有 \(m\) 条边权为 \(a\) 的无向边。对于 \(u, v, w\) ,若存在边 \((u, v, a), (v, w, a)\) 但不存在边 \((u, w, a)\) ,那么加入边 \((u, w, b)\) 。
求起点 \(k\) 到所有点的最短路。
\(n, m \le 10^5\)
考虑可能的最短路构成:
-
全走 \(a\) :直接 bfs 处理即可。
-
一部分走 \(a\) ,一部分走 \(b\) :不难发现该部分一定形如一条 \(a\) 和若干条 \(b\) (否则可以调整更优,或者不如全走 \(a\) ),记全走 \(a\) 的最短路为 \(d\) ,则该部分答案为 \(\lfloor \frac{d}{2} \rfloor \times b + [d \bmod 2] \times a\) 。
-
全走 \(b\) :此时需要求出 \(k\) 到所有点路径长度为偶数的最短路。一个想法是在 bfs 中直接枚举 \((u, v), (v, w)\) ,并尝试松弛 \(w\) 。注意 \(w\) 不能为 \(u\) 的邻域,可以打标记实现。时间复杂度 \(O(\sum deg^2) = O(n^2)\) ,无法接受。
注意到 bfs 每次取出 \(dis_u\) 更新 \(v\) 时,\(v\) 一定不会被其他的点更新。
考虑存两个边集,分别作为奇数边和偶数边。每次取出 \((u, v), (v, w)\) 松弛 \(w\) 时,若 \(w\) 被松弛成功,那么其他 \(u\) 就不会再松弛 \(w\) 了,因此 \((v, w)\) 作为偶数边就没有用了,可以直接删去。
分析一下复杂度,对于成功删去偶数边的部分,复杂度是 \(O(m)\) 的;对于未删去偶数边的部分,其形如一个三元环,且该三元环会被遍历到三次,复杂度是 \(O(m \sqrt{m})\) 的。
总时间复杂度 \(O(m \sqrt{m})\) 。
#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 1e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G, nG;
int dis1[N], dis2[N], tag[N];
int n, m, s, a, b;
inline void bfs1(int S) {
memset(dis1 + 1, inf, sizeof(int) * n);
queue<int> q;
dis1[S] = 0, q.emplace(S);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v : G.e[u])
if (dis1[v] == inf)
dis1[v] = dis1[u] + 1, q.emplace(v);
}
}
inline void bfs2(int S) {
memset(dis2 + 1, inf, sizeof(int) * n);
queue<int> q;
dis2[S] = 0, q.emplace(S);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v : G.e[u])
tag[v] = u;
for (int v : G.e[u]) {
vector<int> edg;
for (int w : nG.e[v]) {
if (tag[w] != u && dis2[w] == inf)
dis2[w] = dis2[u] + 1, q.emplace(w);
else
edg.emplace_back(w);
}
nG.e[v] = edg;
}
}
}
signed main() {
scanf("%d%d%d%d%d", &n, &m, &s, &a, &b);
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
nG.insert(u, v), nG.insert(v, u);
}
bfs1(s), bfs2(s);
for (int i = 1; i <= n; ++i) {
int ans = min(dis1[i] * a, dis1[i] / 2 * b + dis1[i] % 2 * a);
if (dis2[i] != inf)
ans = min(ans, dis2[i] * b);
printf("%d\n", ans);
}
return 0;
}
次短路
考虑每一条非最短路上的边 \(u \to v\) ,答案即为:
\(dis_{1, u}, dis_{v, n}\) 建立正反图跑两次 Dijkstra 即可求得。
求严格次短路时不必记录最短路的路径,只需枚举每条边,若路径长度严格小于最短路时更新答案即可。
另一种方式是对于每个点都记录一下最短路与次短路,只要被更新就去松弛别的点。
最短路图
即求出所有最短路(多条也算)组成的 DAG,只需将 \(dis_v = dis_u + w\) 的边连边即可。
P2149 [SDOI2009] Elaxia的路线
给出一张无向图和两对点,求图中两对点间最短路的最长公共路径,注意同一条边走的方向不同也算公共路径。
\(n \le 1.5 \times 10^3\) ,\(m \le 3 \times 10^5\)
一个显然的事实是最长公共路径一定是连续的一段。
考虑建立 \(s_1 \to t_1\) 的最短路图,仅保留 \(dis_{s \to u} + w(u, v) + dis_{v \to t_1} = dis_{s \to t}\) 的边即可。
然后在最短路图上拓扑排序,需要正反各跑一次。一个简单的实现是记 \(f_u, g_u\) 表示正反以 \(u\) 为端点的最长公共路径长度,转移时只要考虑 \(u \to v\) 这条边能不能在 \(s_2 \to t_2\) 或 \(t_2 \to s_2\) 的最短路上即可。
#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 1.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[4][N], indeg[N], f[N], g[N];
bool flag[N];
int n, m, s1, t1, s2, t2;
inline void Dijkstra(int S, int *dis) {
memset(dis + 1, inf, sizeof(int) * n);
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);
}
}
}
signed main() {
scanf("%d%d%d%d%d%d", &n, &m, &s1, &t1, &s2, &t2);
for (int i = 1; i <= m; ++i) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
G.insert(u, v, w), G.insert(v, u, w);
}
Dijkstra(s1, dis[0]), Dijkstra(t1, dis[1]), Dijkstra(s2, dis[2]), Dijkstra(t2, dis[3]);
for (int u = 1; u <= n; ++u)
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (dis[0][u] + w + dis[1][v] == dis[0][t1])
flag[v] = true, ++indeg[v];
}
queue<int> q;
q.emplace(s1);
while (!q.empty()) {
int u = q.front();
q.pop();
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (!flag[v])
continue;
if (dis[2][u] + w + dis[3][v] == dis[2][t2])
f[v] = max(f[v], f[u] + w);
if (dis[3][u] + w + dis[2][v] == dis[2][t2])
g[v] = max(g[v], g[u] + w);
--indeg[v];
if (!indeg[v])
q.emplace(v);
}
}
printf("%d", max(*max_element(f + 1, f + n + 1), *max_element(g + 1, g + n + 1)));
return 0;
}
「ROI 2017 Day 1」前往大都会
某国有 \(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 \le 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(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;
inline void Dijkstra(int S) {
memset(dis + 1, inf, sizeof(int) * n);
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);
}
}
}
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];
ts[i][j] = (dis[u] + w > dis[v] ? ++cnt : ts[i][j - 1]);
}
}
}
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], ta = 1ll * dis[a] * dis[a] + f[a],
tb = 1ll * dis[b] * dis[b] + f[b], tc = 1ll * dis[c] * dis[c] + f[c];
return (__int128) (tc - ta) * (ka - kb) >= (__int128) (tb - ta) * (ka - kc);
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v, w;
scanf("%d%d", s + i, &u);
City[i].emplace_back(u), belong[u].emplace_back(i, 0);
for (int j = 1; j <= s[i]; ++j) {
scanf("%d%d", &w, &v);
City[i].emplace_back(v), Time[i].emplace_back(w);
G.insert(u, v, w), belong[v].emplace_back(i, j), u = v;
}
}
Dijkstra(1), prework();
iota(id + 1, id + n + 1, 1);
sort(id + 1, id + 1 + n, [](const int &a, const int &b) {
return dis[a] < dis[b];
});
for (auto it : belong[1])
sta[ts[it.first][it.second]].emplace_back(1);
for (int i = 2; i <= n; ++i) {
int x = id[i];
if (dis[x] == inf)
break;
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)
即由最短路径组成的树,和最短路图的区别就是少了几条边。可以通过求解最短路时记录每个点的前驱更新节点求得。
CF545E Paths and Trees
给出一张无向图,给定源点,求边权和最小的 SPT。
\(n, m \le 3 \times 10^5\)
要求边权和最小,可以考虑贪心,在松弛时若遇到松弛前后边权相等时取边权较小者即可。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 0x3f3f3f3f3f3f3f3f;
const int N = 3e5 + 7;
struct Graph {
struct Edge {
int nxt, v, w;
bool tag;
} 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, false}, head[u] = tot;
}
} G;
ll dis[N];
int pre[N];
int n, m, s;
inline void Dijkstra(int S) {
memset(dis + 1, 0x3f, sizeof(ll) * n);
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, pre[v] = i, q.emplace(-dis[v], v);
else if (dis[v] == dis[u] + w && w < G.e[pre[v]].w)
pre[v] = i;
}
}
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
G.insert(u, v, w), G.insert(v, u, w);
}
scanf("%d", &s);
Dijkstra(s);
for (int i = 1; i <= n; ++i)
G.e[pre[i]].tag = true;
ll ans = 0;
for (int i = 1; i <= m; ++i)
if (G.e[i << 1].tag || G.e[i << 1 | 1].tag)
ans += G.e[i << 1].w;
printf("%lld\n", ans);
for (int i = 1; i <= m; ++i)
if (G.e[i << 1].tag || G.e[i << 1 | 1].tag)
printf("%d ", i);
return 0;
}
CF1005F Berland and the Shortest Paths
给出一张无向无边权简单连通图,求 SPT 方案数并给出 \(k\) 个方案(若超过 \(k\) 种则只取 \(k\) 种即可)。
\(n, m \le 2 \times 10^5\) ,\(mk \le 10^6\)
对每个点维护可能成为前驱节点的集合,总方案数就是所有集合大小的乘积,求解方案直接暴力从每个集合中选一个元素组合即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 7;
struct Graph {
struct Edge {
int nxt, v;
} e[N << 1];
int head[N];
int tot = 1;
inline void insert(int u, int v) {
e[++tot] = (Edge) {head[u], v}, head[u] = tot;
}
} G;
vector<int> pre[N];
int dis[N];
bool vis[N];
int n, m, k, ans = 1;
inline void bfs(int S) {
memset(dis + 1, -1, sizeof(int) * n);
queue<int> q;
dis[S] = 0, q.emplace(S);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = G.head[u]; i; i = G.e[i].nxt) {
int v = G.e[i].v;
if (dis[v] == -1)
dis[v] = dis[u] + 1, pre[v].emplace_back(i >> 1), q.emplace(v);
else if (dis[v] == dis[u] + 1)
pre[v].emplace_back(i >> 1);
}
}
}
void dfs(int u) {
if (u > n) {
for (int i = 1; i <= m; ++i)
putchar(vis[i] ? '1' : '0');
puts("");
if (!--ans)
exit(0);
return;
}
for (int it : pre[u])
vis[it] = true, dfs(u + 1), vis[it] = false;
return;
}
signed main() {
scanf("%d%d%d", &n, &m, &k);
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
bfs(1);
for (int i = 2; i <= n; ++i) {
if (ans * pre[i].size() > k) {
ans = k;
break;
} else
ans *= pre[i].size();
}
printf("%d\n", ans);
dfs(2);
return 0;
}
P6880 [JOI 2020 Final] 奥运公交 / Olympic Bus
给定一个有向图,每条边从 \(u_i\) 指向 \(v_i\),经过这条边的代价为 \(c_i\) ,反转这条边方向的额外代价为 \(d_i\) 。
可以反转一条边(或不反转),求 \(1 \to n \to 1\) 的最小代价和。
\(n \le 200\) ,\(m \le 5 \times 10^4\)
考虑求翻转一条边 \((u, v)\) 后 \(1 \to n\) 的最短路,\(n \to 1\) 是类似的。
一个显然的暴力是暴力每次都跑一次 Dijkstra,时间复杂度 \(O(mn^2)\) 。
注意到只有 SPT 上的边才需要跑 Dijkstra,其余情况只要用强制经过这条边的最短路与原最短路取较小者即可。
由于边数较多,用朴素 Dijkstra 即可做到 \(O(n^3)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 0x3f3f3f3f3f3f3f3f;
const int N = 2e2 + 7, M = 5e4 + 7;
struct Edge {
int u, v, w1, w2;
} edg[M];
pair<ll, ll> e[N][N];
ll dis[5][N];
int pre[5][N];
bool vis[N];
int n, m;
inline void Dijkstra(int S, ll *dis, int *pre) {
memset(dis + 1, inf, sizeof(ll) * n);
memset(pre + 1, -1, sizeof(int) * n);
memset(vis + 1, false, sizeof(bool) * n);
dis[S] = 0;
for (;;) {
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 v = 1; v <= n; ++v)
if (dis[v] > dis[u] + e[u][v].first)
dis[v] = dis[u] + e[u][v].first, pre[v] = u;
}
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
e[i][j] = make_pair(inf, inf);
for (int i = 1; i <= m; ++i) {
int u, v, w1, w2;
scanf("%d%d%d%d", &u, &v, &w1, &w2);
edg[i] = (Edge){u, v, w1, w2};
if (w1 <= e[u][v].first)
e[u][v].second = e[u][v].first, e[u][v].first = w1;
else if (w1 <= e[u][v].second)
e[u][v].second = w1;
}
Dijkstra(1, dis[0], pre[0]), Dijkstra(n, dis[1], pre[1]);
for (int i = 1; i <= n; ++i)
for (int j = i + 1; j <= n; ++j)
swap(e[i][j], e[j][i]);
Dijkstra(1, dis[2], pre[2]), Dijkstra(n, dis[3], pre[3]);
for (int i = 1; i <= n; ++i)
for (int j = i + 1; j <= n; ++j)
swap(e[i][j], e[j][i]);
ll ans = dis[0][n] + dis[1][1];
for (int i = 1; i <= m; ++i) {
int u = edg[i].u, v = edg[i].v, w1 = edg[i].w1, w2 = edg[i].w2;
auto pe = e[u][v], pe2 = e[v][u];
e[u][v].first = e[u][v].second, e[v][u].first = min(e[v][u].first, (ll)w1);
ll res = w2;
if (pre[0][v] != u || dis[0][u] + w1 != dis[0][v])
res += min(dis[0][n], dis[0][v] + w1 + dis[3][u]);
else
Dijkstra(1, dis[4], pre[4]), res += dis[4][n];
if (pre[1][v] != u || dis[1][u] + w1 != dis[1][v])
res += min(dis[1][1], dis[1][v] + w1 + dis[2][u]);
else
Dijkstra(n, dis[4], pre[4]), res += dis[4][1];
ans = min(ans, res), e[u][v] = pe, e[v][u] = pe2;
}
printf("%lld", ans >= inf ? -1 : ans);
return 0;
}
差分约束系统
差分约束系统用于求解 \(n\) 元一次不等式组。每个不等式都形如 \(x_i - x_j \le c_k\) ,其中 \(c_k\) 为常数且 \(i \not = j\) 。
将每个不等式都转化为 \(x_i \le x_j + c_k\) ,这与三角形不等式 \(dis_v \le dis_u + w\) 十分相似。那么对于一组不等式 \(x_v - x_u \le w\) ,建边 \((u, v, w)\) 。
从超级源点向每个点连一条边权为 \(0\) 的边,若建图后图中有负环则方程组无解,否则 \(x_i = dis_i\) 就是方程组的一组解。
一些技巧:
- \(x_i - x_j < c_k\) 可以转化为 \(x_i - x_j \le c_k - 1\) 。
- \(x_i = x_j\) 可以转化为 \(x_i - x_j \le 0\) 且 \(x_j - x_i \le 0\) 。
差分约束系统可以用于处理字典序极值的解,这基于变量有界的基础上。
不妨设希望求出当限制 \(x_i \le 0\) 时整个差分约束系统的字典序的最大值,考虑将 \(x_i \le 0\) 视为 \((x_0 = 0) + 0 \ge x_i\) ,这样跑出来的解就是字典序最大的。这是因为最短路上的边均有 \(x_u + w(u, v) = x_v\) ,若将 \(x_i\) 增大 \(1\) 则最短路上至少有一条边的限制无法被满足。
对于字典序最小解,考虑限制 \(x_i \ge 0\) ,字典序最小即字典序最大时的相反数。
注意下界可以动态调整,给整体加上 \(d\) 即可调整。
[AGC056C] 01 Balanced
构造长度为 \(n\) 的字典序最小的 \(01\) 字符串,满足 \(m\) 组子串 \([l_i, r_i]\) 含相同数量的 \(0\) 和 \(1\) 。
\(n \le 10^6\) ,\(m \le 2 \times 10^5\) ,保证 \(r- l + 1\) 是偶数
考虑将 \(0\) 当作 \(1\) ,\(1\) 当作 \(-1\) 。因为要让答案的字典序最小,即 \(s_i\) 尽可能大,即需要求出字典序最大的一组解,便于差分约束系统求解。
考虑相邻两个位置的限制从 \(|s_i - s_{i - 1}| = 1\) 弱化为 \(|s_i - s_{i - 1}| \le 1\) ,因为不可能存在 \(s_{i - 1} = s_i\) (若存在可以构造出 \(\pm 1\) 交错的 \(s\) 使得字典序更大)。
对于一组限制,转化为 \(s_{l - 1} = s_r\) ,于是 01bfs 即可求解。
#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
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;
inline void bfs() {
memset(dis + 1, inf, sizeof(int) * n);
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] > dis[u] + w) {
dis[v] = dis[u] + w;
if (w)
q.emplace_back(v);
else
q.emplace_front(v);
}
}
}
}
signed main() {
scanf("%d%d", &n, &m);
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, r;
scanf("%d%d", &l, &r);
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;
}
[AGC036D] Negative Cycle
有一张 \(n\) 个点的有向图,形如:
有 \(n - 1\) 条形如 \(i \to i + 1\) 边权为 \(0\) 的边,不可删去。
对于每一对 \(i < j\) ,存在边权为 \(-1\) 的边。
对于每一对 \(i > j\) ,存在边权为 \(1\) 的边。
删去边 \((i, j)\) 花费 \(a_{i, j}\) 的代价,求图中不存在负环的最小删边代价。
\(n \le 500\)
考虑差分约束系统,要求图上没有负环,等价于存在一组差分约束的合法解,那么可以把图上的边都写成不等式。
设差分约束系统的合法解为 \(x_{1 \sim n}\) ,记 \(x\) 的差分数组为 \(c_i = x_i - x_{i + 1}\) ,则:
-
对于边 \(i \to i + 1\) ,其等价于 \(x_i - x_{i + 1} = c_i \ge 0\) 。
-
对于边 \(i \to j\) ( \(i < j\) ),其等价于 \(x_i - x_j = \sum_{k = i}^{j - 1} c_k \ge 1\) ,即 \([i, j - 1]\) 的区间和非 \(0\) 。
-
对于边 \(i \to j\) ( \(i > j\) ),其等价于 \(x_j - x_i = \sum_{k = j}^{i - 1} c_k \le 1\) 。
可以发现,若 \(c_i \ge 2\) ,则可以调整为 \(c_i = 1\) ,答案不会变劣,因此仅需考虑 \(c_i \in \{ 0, 1 \}\) 的情况。
设 \(f_{i, j}\) 表示考虑到 \(i\) ,\(c_i = 1\) ,上一个 \(c\) 为 \(1\) 的位置为 \(j\) 的最小代价,考虑如何从 \(f_{j, k}\) 转移到 \(f_{i, j}\) :
- 左右端点都在 \([j + 1, i]\) 中的第二类边需要删去。
- 左端点 \(\in [k + 1, j]\) ,右端点 \(\in [i + 1, n]\) 的第三类边需要删去。
不难用二维前缀和统计,时间复杂度 \(O(n^2)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 5e2 + 7;
ll f[N][N], s2[N][N], s3[N][N];
int a[N][N];
int n;
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j) {
if (i < j)
scanf("%lld", s2[i] + (j - 1));
else if (i > j)
scanf("%lld", s3[j] + (i - 1));
}
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j) {
s2[i][j] += s2[i - 1][j] + s2[i][j - 1] - s2[i - 1][j - 1];
s3[i][j] += s3[i - 1][j] + s3[i][j - 1] - s3[i - 1][j - 1];
}
ll ans = 1e18;
auto calc = [](int k, int j, int i) {
return s2[i - 1][i - 1] - s2[j][i - 1] - s2[i - 1][j] + s2[j][j] +
s3[j][n] - s3[k][n] - s3[j][i - 1] + s3[k][i - 1];
};
for (int i = 1; i < n; ++i) {
f[i][0] = calc(0, 0, i);
for (int j = 1; j < i; ++j) {
f[i][j] = 1e18;
for (int k = 0; k < j; ++k)
f[i][j] = min(f[i][j], f[j][k] + calc(k, j, i));
}
for (int j = 0; j < i; ++j)
ans = min(ans, f[i][j] + calc(j, i, n));
}
printf("%lld", ans);
return 0;
}
P3971 [TJOI2014] Alice and Bob
对于序列 \(x_{1 \sim n}\) ,记 \(a_i\) 表示以 \(i\) 结尾的 LIS 的长度,\(b_i\) 表示以 \(i\) 开头的 LDS 的长度。
给出 \(a_{1 \sim n}\) ,求所有 \(x_{1 \sim n}\) 的解中 \(\sum b_i\) 的最大值。
\(n \le 10^5\)
首先有结论,\(x_{1 \sim n}\) 一定可以取到一个排列,因为若 \(x_i = x_j\) ,则可以调整为 \(x_i > x_j\) 使得 \(\sum b_i\) 更大。
考虑 \(a_{1 \sim n}\) 对 \(x_{1 \sim n}\) 有何限制,记 \(lst_x\) 表示上一个 \(a_i = x\) 的 \(i\) ,则 \(x_{lst_{a_i}} > x_i\) 且 \(x_{lst_{a_i - 1}} < x_i\) ,因为 \(a\) 相同时靠后的一定更小。
由此考虑建立差分约束系统,对于 \(x_i < x_j\) 的限制,连 \(i \to j\) 的有向边,则该图的拓扑序即为一组解。
为了最大化 \(\sum b_i\) ,显然在多个可以填的位置中选最靠后的最优,因此只要将拓扑排序中的队列换成优先队列即可。
时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e5 + 7;
struct Graph {
vector<int> e[N];
int indeg[N];
inline void insert(int u, int v) {
e[u].emplace_back(v), ++indeg[v];
}
} G;
int a[N], lst[N], b[N], f[N];
int n;
inline void Toposort() {
priority_queue<int> q;
for (int i = 1; i <= n; ++i)
if (!G.indeg[i])
q.emplace(i);
int cnt = 0;
while (!q.empty()) {
int u = q.top();
q.pop(), b[u] = ++cnt;
for (int v : G.e[u])
if (!--G.indeg[v])
q.emplace(v);
}
}
namespace BIT {
int c[N];
inline void update(int x, int k) {
for (; x <= n; x += x & -x)
c[x] = max(c[x], k);
}
inline int query(int x) {
int res = 0;
for (; x; x -= x & -x)
res = max(res, c[x]);
return res;
}
} // namespace BIT
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
scanf("%d", a + i);
if (lst[a[i]])
G.insert(i, lst[a[i]]);
if (lst[a[i] - 1])
G.insert(lst[a[i] - 1], i);
lst[a[i]] = i;
}
Toposort();
ll ans = 0;
for (int i = n; i; --i)
BIT::update(b[i], f[i] = BIT::query(b[i] - 1) + 1), ans += f[i];
printf("%lld", ans);
return 0;
}
P7515 [省选联考 2021 A 卷] 矩阵游戏
有一个 \(n \times m\) 的矩阵 \(a\) ,其中 \(a_{i, j} \le 10^6\) 。定义一个 \((n - 1) \times (m - 1)\) 的矩阵 \(b\) ,其中 \(b_{i, j} = a_{i, j} + a_{i, j + 1} + a_{i + 1, j} + a_{i + 1, j + 1}\) 。
给定矩阵 \(b\) ,构造一组合法的 \(a\) ,或判定无解。
\(n, m \le 300\)
考虑构造一组特解然后调整。构造一组特解是简单的,直接令 \(a_{n, i} = a_{i, m} = 0\) 然后递推即可,接下来考虑调整使其满足 \(a_{i, j} \le 10^6\) 的限制。可以发现存在一种调整方式为:
考虑差分约束:
- \(2 \mid i\) 且 \(2 \mid j\) :\(0 \le A_{i, j} - c_i - d_j \le 10^6\) 。
- \(2 \mid i\) 且 \(2 \nmid j\) :\(0 \le A_{i, j} + c_i - d_j \le 10^6\) 。
- \(2 \nmid i\) 且 \(2 \mid j\) :\(0 \le A_{i, j} - c_i + d_j \le 10^6\) 。
- \(2 \nmid i\) 且 \(2 \nmid j\) :\(0 \leq A_{i, j} + c_i + d_j \le 10^6\) 。
可以发现 \([2 \mid i] \ne [2 \mid j]\) 的形式均为 \(c - d \le k\) 和 \(d - c \le k\) ,已经可以表示为差分约束的形式,但是 \([2 \mid i] = [2 \mid j]\) 的情况并不能表示为差分约束系统。
考虑记 \(x_i = (-1)^i \times c_i, y_i = (-1)^{i + 1} \times d_i\) ,则:
- \(2 \mid i\) 且 \(2 \mid j\) :\(0 \le A_{i, j} -x_i + y_j \le 10^6\) 。
- \(2 \mid i\) 且 \(2 \nmid j\) :\(0 \le A_{i, j} + x_i + y_j \le 10^6\) 。
- \(2 \nmid i\) 且 \(2 \mid j\) :\(0 \le A_{i, j} + x_i - y_j \le 10^6\) 。
- \(2 \nmid i\) 且 \(2 \nmid j\) :\(0 \leq A_{i, j} -x_i + y_j \le 10^6\) 。
直接跑差分约束系统即可。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 6e2 + 7;
struct Graph {
vector<pair<int, int> > e[N];
inline void clear(int n) {
for (int i = 1; i <= n; ++i)
e[i].clear();
}
inline void insert(int u, int v, int w) {
e[u].emplace_back(v, w);
}
} G;
ll dis[N];
int b[N][N], a[N][N], cnt[N];
bool inque[N];
int n, m;
inline bool SPFA() {
queue<int> q;
for (int i = 1; i <= n + m; ++i)
dis[i] = cnt[i] = 0, q.emplace(i), inque[i] = true;
while (!q.empty()) {
int u = q.front();
q.pop(), inque[u] = false;
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w, ++cnt[v];
if (cnt[v] >= n + m)
return false;
if (!inque[v])
q.emplace(v), inque[v] = true;
}
}
}
return true;
}
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d", &n, &m);
for (int i = 1; i < n; ++i)
for (int j = 1; j < m; ++j)
scanf("%d", b[i] + j);
memset(a[n] + 1, 0, sizeof(int) * m);
for (int i = 1; i <= n; ++i)
a[i][m] = 0;
for (int i = n - 1; i; --i)
for (int j = m - 1; j; --j)
a[i][j] = b[i][j] - a[i + 1][j] - a[i][j + 1] - a[i + 1][j + 1];
G.clear(n + m);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j) {
if ((i + j) & 1)
G.insert(i, j + n, a[i][j]), G.insert(j + n, i, 1e6 - a[i][j]);
else
G.insert(j + n, i, a[i][j]), G.insert(i, j + n, 1e6 - a[i][j]);
}
if (!SPFA()) {
puts("NO");
continue;
}
puts("YES");
for (int i = 1; i <= n; ++i, puts(""))
for (int j = 1; j <= m; ++j)
printf("%d ", a[i][j] + ((i + j) & 1 ? dis[i] - dis[j + n] : dis[j + n] - dis[i]));
}
return 0;
}
同余最短路
同余最短路利用同余来构造一些状态。设模数为 \(m\) ,考虑将 \(0 \sim m - 1\) 看作单源最短路中的点,然后跑单源最短路求出每个余数的最小解(最小能表示的数),则该解加上若干倍的 \(m\) 均能被表示。
注意到这本质和模意义下的完全背包相同。对于体积为 \(v_i\) 的物品,其会形成 \(d = \gcd(v_i, m)\) 个环。由于至多在环上转一圈,因此只要加入 \(\frac{m}{\gcd(v_i, m)} - 1\) 个。对于每一个环,只要绕着这个环转两圈即可考虑到所有转移,因为每个点都转移到了子环上其它所有点。
P3403 跳楼机
给出 \(x, y, z, h\) ,求有多少 \(k \in [1, h]\) 满足 \(ax + by + cz = k\) 。
\(x, y, z \le 10^5\) ,\(h \le 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\) 最小,即可得到最小的一组解,类比差分约束即可得到所有解,答案即为 \(\sum_{i = 0}^{x - 1} (\frac{h - d_i}{x} + 1)\) ,时间复杂度 \(O(x \log x)\) 。
#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;
}
使用转圈技巧可以做到线性。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = (1ull << 63) - 1;
const int N = 1e5 + 7;
ll dis[N];
int vis[N];
ll h, x, y, z;
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, fill(dis, dis + x, inf), dis[0] = 0;
auto update = [](int m, int k) {
memset(vis, 0, sizeof(int) * m);
for (int i = 0; i < m; ++i) {
if (vis[i])
continue;
for (int x = i; vis[x] < 2; x = (x + k) % m) {
++vis[x];
if (dis[x] != inf)
dis[(x + k) % m] = min(dis[(x + k) % m], dis[x] + k);
}
}
};
update(x, y), update(x, z);
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] Small Multiple
给定一个整数 \(K\)。求一个 \(K\) 的正整数倍 \(S\),使得 \(S\) 的数位累加和最小。
\(2 \le K \le 10^5\) 。
注意到一个数都可以通过 \(+1\) 和 \(\times 10\) 得到。\(+1\) 时数位累加和增加,\(\times 10\) 时不变。
因为不需要求出具体数值,输出数位累加和即可,所以我们在 \(\bmod k\) 意义下利用同余最短路配合 01BFS 计算即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7;
bool vis[N];
int K, ans;
signed main() {
scanf("%d", &K);
deque<pair<int, int> > q;
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;
}
删边最短路
CF1163F Indecisive Taxi Fee
给出一张无向带正权图, \(q\) 次询问,每次询问给出 \(t, x\),求若将 \(t\) 这条边的长度修改为 \(x\) 时 \(1\) 到 \(n\) 的最短路长度。
\(n, m, q \le 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 = 0x3f3f3f3f3f3f3f3f;
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;
inline void Dijkstra(int S, ll *dis) {
memset(dis + 1, inf, sizeof(ll) * n);
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() {
scanf("%d%d%d", &n, &m, &q);
for (int i = 1; i <= m; ++i) {
scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w);
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, k;
scanf("%d%d", &x, &k);
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;
}
网格图最短路相关
P5897 [IOI 2013] wombats
给定一个 \(n \times m\) 的网格图,相邻两个格子之间有边,边带边权,移动时 \((x, y)\) 只能移动到 \((x + 1, y), (x, y - 1), (x, y + 1)\) 三者之一且不能移出网格。
\(q\) 次操作,操作有:
- 修改某条边的边权,共 \(C\) 次。
- 查询 \((1, x) \to (n, y)\) 的最短路,共 \(Q\) 次。
\(n \le 5000\) ,\(m \le 200\) ,\(C \le 500\) ,\(Q \le 2 \times 10^5\) ,TL = 8s,ML = 250MB
注意到 \(n, m\) 范围差别较大,考虑对 \(n\) 一维开线段树维护,线段树上每个区间 \([l, r]\) 维护矩阵 \(f_{i, j}\) 表示 \((l, i) \to (r, j)\) 的最短路,合并时做 \((\min, +)\) 矩阵乘法。预处理直接做是 \(O(n m^3 \log n)\) 的,无法通过。
考虑优化,观察矩乘的形式 \(f_{i, j} = \min \{ fl_{i, k} + fr_{k, j} \}\) ,可以发现当 \(i < j\) 和 \(i > j\) 时 \(k\) 均有决策单调性。记 \(p_{i, j}\) 为决策点,则 \(p_{i, j - 1} \le p_{i, j} \le p_{i + 1, j}\) ,因此可以用 Knuth's Optimization 优化到 \(O(m^2)\) ,预处理的复杂度降为 \(O(n m^2 \log n)\) 。
但是此时空间是 \(O(n m^2)\) 的,无法接受。考虑将连续 \(B\) 行的状态压缩到线段树的叶子上,即当 \(r - l + 1 \le B\) 的时候定义该点为叶子,每次更新叶子时暴力 \(O(B m^2)\) 处理。
时间复杂度 \(O(n m^2 \log n + C \times m^2 (B + \log \frac{n}{B}) + Q)\) ,空间复杂度 \(O(\frac{n m^2}{B})\) ,取 \(B = 15\) 即可。
#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 5e3 + 7, M = 2e2 + 7, B = 15;
int w1[N][M], w2[N][M];
int n, m, q;
struct Matrix {
int a[M][M];
inline Matrix(bool flag = false) {
memset(a, inf, sizeof(a));
if (flag) {
for (int i = 1; i <= m; ++i)
a[i][i] = 0;
}
}
};
inline Matrix getmatrix(int x) {
vector<int> s(m);
for (int i = 1; i < m; ++i)
s[i] = s[i - 1] + w1[x][i];
Matrix f;
for (int i = 1; i <= m; ++i)
for (int j = i; j <= m; ++j)
f.a[i][j] = f.a[j][i] = s[j - 1] - s[i - 1];
return f;
}
inline Matrix merge(Matrix fl, int mid, Matrix fr) {
Matrix f;
static int g[M][M];
for (int i = 1; i <= m; ++i)
for (int j = 1; j <= m; ++j)
if (fl.a[i][j] + w2[mid][j] + fr.a[j][i] < f.a[i][i])
f.a[i][i] = fl.a[i][j] + w2[mid][j] + fr.a[j][i], g[i][i] = j;
for (int len = 2; len <= m; ++len)
for (int i = 1, j = len; j <= m; ++i, ++j) {
for (int k = g[i][j - 1]; k <= g[i + 1][j]; ++k)
if (fl.a[i][k] + w2[mid][k] + fr.a[k][j] < f.a[i][j])
f.a[i][j] = fl.a[i][k] + w2[mid][k] + fr.a[k][j], g[i][j] = k;
for (int k = g[j - 1][i]; k <= g[j][i + 1]; ++k)
if (fl.a[j][k] + w2[mid][k] + fr.a[k][i] < f.a[j][i])
f.a[j][i] = fl.a[j][k] + w2[mid][k] + fr.a[k][i], g[j][i] = k;
}
return f;
}
namespace SMT {
Matrix mt[N / B << 2];
inline int ls(int x) {
return x << 1;
}
inline int rs(int x) {
return x << 1 | 1;
}
inline void pushup(int x, int l, int r) {
int mid = (l + r) >> 1;
mt[x] = merge(mt[ls(x)], mid, mt[rs(x)]);
}
void build(int x, int l, int r) {
if (r - l + 1 <= B) {
mt[x] = getmatrix(l);
for (int i = l; i < r; ++i)
mt[x] = merge(mt[x], i, getmatrix(i + 1));
return;
}
int mid = (l + r) >> 1;
build(ls(x), l, mid), build(rs(x), mid + 1, r);
pushup(x, l, r);
}
void update(int x, int nl, int nr, int p) {
if (nr - nl + 1 <= B) {
mt[x] = getmatrix(nl);
for (int i = nl; i < nr; ++i)
mt[x] = merge(mt[x], i, getmatrix(i + 1));
return;
}
int mid = (nl + nr) >> 1;
if (p <= mid)
update(ls(x), nl, mid, p);
else
update(rs(x), mid + 1, nr, p);
pushup(x, nl, nr);
}
} // namespace SMT
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
for (int j = 1; j < m; ++j)
scanf("%d", w1[i] + j);
for (int i = 1; i < n; ++i)
for (int j = 1; j <= m; ++j)
scanf("%d", w2[i] + j);
SMT::build(1, 1, n);
scanf("%d", &q);
while (q--) {
int op, x, y;
scanf("%d%d%d", &op, &x, &y);
++x, ++y;
if (op == 1) {
int w;
scanf("%d", &w);
w1[x][y] = w, SMT::update(1, 1, n, x);
} else if (op == 2) {
int w;
scanf("%d", &w);
w2[x][y] = w, SMT::update(1, 1, n, x);
} else
printf("%d\n", SMT::mt[1].a[x][y]);
}
return 0;
}
P3350 [ZJOI2016] 旅行者
给定一个 \(n \times m\) 的网格图,相邻两个格子之间有边,边带边权,\(q\) 次询问 \((x_1, y_1), (x_2, y_2)\) 两点间的最短路。
\(n \times m \le 2 \times 10^4\) ,\(q \le 10^5\)
考虑猫树分治,每次选取区间更长的维度分治,不妨设当前处理 \(x_1, x_2 \in [l_x, r_x]\) 且 \(y_1, y_2 \in [l_y, r_y]\) 的所有询问,当前分割区间为 \([l_x, mid]\) 和 \([mid + 1, r_x]\) 。
对于 \((mid, l_y \sim r_y)\) 的每个中转点,求出只经过 \(x \in [l_x, r_x], y \in [l_y, r_y]\) 的点时所有 \((x, y)\) 与它的距离,然后用 \(dis_{x_1, y_1} + dis_{x_2, y_2}\) 更新答案。
下面说明该做法的正确性,如果一个询问递归到了左区间,则所有中转点在右区间的路径一定经过 \(mid\) ,因此 \((mid, l_y \sim r_y)\) 同样在路径上,其作为中转点一定更新过答案,因此无需递归右区间更新该询问。
记 \(A = nm\) ,时间复杂度 \(O(A \sqrt A \log A + q \sqrt{A})\) 。
#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2e4 + 7, B = 1.5e2 + 7, Q = 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;
struct Query {
int bx, by, ex, ey;
} qry[Q];
int dis[N], ans[Q];
int n, m, q;
inline int getid(int x, int y) {
return (x - 1) * m + y;
}
inline pair<int, int> getpos(int k) {
return make_pair(k / m + (k % m ? 1 : 0), k % m ? k % m : m);
}
inline void Dijkstra(int S, int xl, int xr, int yl, int yr) {
memset(dis + 1, inf, sizeof(int) * (n * m));
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, x = getpos(v).first, y = getpos(v).second;
if (xl <= x && x <= xr && yl <= y && y <= yr && dis[v] > dis[u] + w)
dis[v] = dis[u] + w, q.emplace(-dis[v], v);
}
}
}
void solve(int xl, int xr, int yl, int yr, vector<int> &id) {
if (id.empty())
return;
if (xl == xr && yl == yr) {
for (int it : id)
ans[it] = 0;
return;
}
if (xr - xl + 1 >= yr - yl + 1) {
int mid = (xl + xr) >> 1;
for (int i = yl; i <= yr; ++i) {
Dijkstra(getid(mid, i), xl, xr, yl, yr);
for (int it : id)
ans[it] = min(ans[it], dis[getid(qry[it].bx, qry[it].by)] + dis[getid(qry[it].ex, qry[it].ey)]);
}
vector<int> ql, qr;
for (int it : id) {
if (max(qry[it].bx, qry[it].ex) <= mid)
ql.emplace_back(it);
else if (min(qry[it].bx, qry[it].ex) > mid)
qr.emplace_back(it);
}
solve(xl, mid, yl, yr, ql), solve(mid + 1, xr, yl, yr, qr);
} else {
int mid = (yl + yr) >> 1;
for (int i = xl; i <= xr; ++i) {
Dijkstra(getid(i, mid), xl, xr, yl, yr);
for (int it : id)
ans[it] = min(ans[it], dis[getid(qry[it].bx, qry[it].by)] + dis[getid(qry[it].ex, qry[it].ey)]);
}
vector<int> ql, qr;
for (int it : id) {
if (qry[it].by > qry[it].ey)
swap(qry[it].bx, qry[it].ex), swap(qry[it].by, qry[it].ey);
if (max(qry[it].by, qry[it].ey) <= mid)
ql.emplace_back(it);
else if (min(qry[it].by, qry[it].ey) > mid)
qr.emplace_back(it);
}
solve(xl, xr, yl, mid, ql), solve(xl, xr, mid + 1, yr, qr);
}
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
for (int j = 1; j < m; ++j) {
int w;
scanf("%d", &w);
G.insert(getid(i, j), getid(i, j + 1), w);
G.insert(getid(i, j + 1), getid(i, j), w);
}
for (int i = 1; i < n; ++i)
for (int j = 1; j <= m; ++j) {
int w;
scanf("%d", &w);
G.insert(getid(i, j), getid(i + 1, j), w);
G.insert(getid(i + 1, j), getid(i, j), w);
}
scanf("%d", &q);
for (int i = 1; i <= q; ++i)
scanf("%d%d%d%d", &qry[i].bx, &qry[i].by, &qry[i].ex, &qry[i].ey);
memset(ans + 1, inf, sizeof(int) * q);
vector<int> id(q);
iota(id.begin(), id.end(), 1);
solve(1, n, 1, m, id);
for (int i = 1; i <= q; ++i)
printf("%d\n", ans[i]);
return 0;
}
最短路径
给定一张 \(n \times m\) 的网格图,每个点 \((i, j)\) 可以移动到 \((i - 1, j)\) 、\((i + 1, j)\) 、\((i, j + 1)\) 中的一个位置(需满足在网格图内),每条横纵边带边权。
对于 \(i = 1, 2, \cdots, n\) ,求 \(\sum_{j = 1}^n \mathrm{dist}((i, 1), (j, m))\) ,其中 \(\mathrm{dist}\) 表示最短路。
\(n \times m \le 2 \times 10^5\)
考虑起点向下移动时 SPT 的变化,显然每个点的父亲只会不断向下移动。
考虑分治,假设已经求出 \(i = l\) 和 \(i = r\) 的 SPT,若某个点的父亲在这两棵树上相同,则 \(i = l, l + 1, \cdots, r\) 时该点的父亲都相同,可以将其与父亲缩起来。
由于每条边只会在 \(O(\log n)\) 个分治区间出现,使用线性求 SPT 的做法(从后往前做类似前缀和优化的操作),时间复杂度 \(O(nm \log(nm))\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 0x3f3f3f3f3f3f3f3f;
const int N = 2e5 + 7;
struct DSU {
int fa[N];
inline void prework(int n) {
iota(fa + 1, fa + n + 1, 1);
}
inline int find(int x) {
while (x != fa[x])
fa[x] = fa[fa[x]], x = fa[x];
return x;
}
inline void merge(int x, int y) {
fa[find(y)] = find(x);
}
} dsu;
vector<int> a[N], b[N];
ll ans[N];
int n, m;
struct SPT {
vector<tuple<int, int, ll> > edg;
vector<pair<int, ll> > ans;
vector<ll> dis;
int siz;
inline SPT() : siz(0) {}
inline void build(int s) {
dis.assign(siz + 1, inf), dis[s] = 0;
for (auto it : edg)
dis[get<1>(it)] = min(dis[get<1>(it)], dis[get<0>(it)] + get<2>(it));
}
inline SPT(int s) {
siz = n * m, ans.assign(siz + 1, make_pair(0, 0ll));
auto getid = [&](int x, int y) {
return (x - 1) * n + y;
};
for (int i = 1; i <= n; ++i)
ans[getid(m, i)].first = 1;
for (int i = 1; i <= m; ++i) {
for (int j = 1; j < n; ++j)
edg.emplace_back(getid(i, j), getid(i, j + 1), b[j][i]);
for (int j = n - 1; j; --j)
edg.emplace_back(getid(i, j + 1), getid(i, j), b[j][i]);
if (i < m) {
for (int j = 1; j <= n; ++j)
edg.emplace_back(getid(i, j), getid(i + 1, j), a[j][i]);
}
}
build(s);
}
inline ll calc() {
ll res = 0;
for (int i = 1; i <= siz; ++i)
res += dis[i] * ans[i].first + ans[i].second;
return res;
}
};
inline SPT maintain(SPT A, SPT B) {
SPT C;
dsu.prework(A.siz);
auto ans = A.ans;
for (auto it : A.edg) {
int u = get<0>(it), v = get<1>(it);
ll w = get<2>(it);
if (dsu.find(v) == v && A.dis[u] + w == A.dis[v] && B.dis[u] + w == B.dis[v]) {
dsu.merge(u = dsu.find(u), v);
ans[u].first += ans[v].first, ans[u].second += ans[v].second + w * ans[v].first;
}
}
vector<int> id(A.siz + 1, -1);
C.ans = {make_pair(0, 0)}, C.dis = {0};
for (int i = 1; i <= A.siz; ++i)
if (dsu.find(i) == i)
id[i] = ++C.siz, C.ans.emplace_back(ans[i]), C.dis.emplace_back(A.dis[i]);
for (auto it : A.edg) {
int u = get<0>(it), v = get<1>(it);
ll w = get<2>(it);
if (dsu.find(v) == v)
C.edg.emplace_back(id[dsu.find(u)], id[v], w + A.dis[u] - A.dis[dsu.find(u)]);
}
return C;
}
void solve(int l, int r, SPT Gl, SPT Gr) {
if (l + 1 == r) {
ans[l] = Gl.calc();
return;
}
int mid = (l + r) >> 1;
SPT Gm = Gl;
Gm.build(mid - l + 1);
solve(l, mid, maintain(Gl, Gm), maintain(Gm, Gl));
solve(mid, r, maintain(Gm, Gr), maintain(Gr, Gm));
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
a[i].resize(m), b[i].resize(m + 1);
for (int i = 1; i <= n; ++i)
for (int j = 1; j < m; ++j)
scanf("%d", &a[i][j]);
for (int i = 1; i < n; ++i)
for (int j = 1; j <= m; ++j)
scanf("%d", &b[i][j]);
if (n == 1)
return printf("%lld", SPT(1).calc()), 0;
SPT Gl(1), Gr(n);
ans[n] = Gr.calc(), solve(1, n, Gl, Gr);
for (int i = 1; i <= n; ++i)
printf("%lld\n", ans[i]);
return 0;
}