A - 長いだけのネクタイ (Just Long Neckties)
JOI 公司开了一个派对。有 \(n + 1\) 条领带,第 \(i\) 条领带的长度是 \(a_i\)。有 \(n\) 名员工,第 \(i\) 名员工适合长度不超过 \(b_i\) 的领带。
对于一种将 \(n\) 条领带配对给 \(n\) 的人的方案,设第 \(i\) 条领带匹配了第 \(j\) 个人,则我们称派对的奇怪值为 \(\max(a_i - b_j, 0)\) 的最大值。
现在 \(\forall i \in [1, n + 1]\),你需要求出,若移除第 \(i\) 条领带,将剩下的领带和员工配对,最小的奇怪值是多少。
\(n \le 2 \times 10^5\)。
首先如果人和领带的集合确定,那么将 \(a\) 和 \(b\) 分别排序后顺次配对一定最优。
那么我们将 \(a\) 和 \(b\) 排序,每次相当于一段前缀正常匹配,然后去掉一个位置,然后后缀继续匹配。那么我们做一个前缀 \(\max\) 和一个后缀 \(\max\),然后拼一下即可。
时间复杂度 \(O(n \log n)\)。
Code
#include <iostream>
#include <algorithm>using namespace std;
using Pii = pair<int, int>;const int N = 2e5 + 5; int n, ans[N];
Pii a[N];
int b[N], pre[N], suf[N];int main () {cin.tie(0)->sync_with_stdio(0);cin >> n;for (int i = 1; i <= n + 1; ++i) {cin >> a[i].first, a[i].second = i; }sort(a + 1, a + n + 2);for (int i = 1; i <= n; ++i) {cin >> b[i];}sort(b + 1, b + n + 1);for (int i = 1; i <= n; ++i) {pre[i] = max(pre[i - 1], a[i].first - b[i]);}for (int i = n + 1; i >= 2; --i) {suf[i] = max(suf[i + 1], a[i].first - b[i - 1]); } for (int i = 1; i <= n + 1; ++i) {ans[a[i].second] = max(pre[i - 1], suf[i + 1]);}for (int i = 1; i <= n + 1; ++i) {cout << ans[i] << ' '; }cout << '\n';return 0;
}
B - JJOOII 2 (JJOOII 2)
我们称由 \(k\) 个 \(\texttt{J}\),\(k\) 个 \(\texttt{O}\),\(k\) 个 \(\texttt{I}\) 顺次拼接得到的字符串称为 \(k\) 阶 \(\texttt{JOI}\) 串。
给定一个长度为 \(n\) 的字符串 \(s\) 和整数 \(k\),你可以进行以下三种操作:
- 删除 \(s\) 中开头的字符,代价为 \(0\)。
- 删除 \(s\) 中结尾的字符,代价为 \(0\)。
- 删除 \(s\) 中任意位置的字符,代价为 \(1\)。
求将 \(s\) 变成 \(k\) 阶 \(\texttt{JOI}\) 串的最小代价。
\(1 \le n \le 2 \times 10^5\)。
我们钦定 \(s\) 的一个子序列为答案,假设子序列的第一个元素为 \(s_l\),最后一个元素为 \(s_r\),显然不在 \([l, r]\) 中的可以以 \(0\) 的代价删除。于是题意可转化为求一个子串 \(s[l, r]\),使得其包含一 \(k\) 阶 \(\texttt{JOI}\) 串作为子序列,最小化 \((r -l + 1) - 3k\)。
枚举该子串的开头位置 \(l\),现在从左往右直接匹配 \(k\) 阶 \(\texttt{JOI}\) 串最优。双指针预处理 \(nx_{i, 0/1/2}\) 表示从 \(i\) 开始的第 \(k\) 个 \(\texttt{J/O/I}\) 出现在什么位置,即可加速子序列匹配。
时间复杂度 \(O(n)\)。
Code
#include <iostream>using namespace std;const int N = 2e5 + 5; int n, k, a[N], nx[3][N];int main () {cin.tie(0)->sync_with_stdio(0);cin >> n >> k;for (int i = 1; i <= n; ++i) {char s;cin >> s;a[i] = (s == 'J' ? 0 : (s == 'O' ? 1 : 2));}for (int o = 0; o < 3; ++o) {for (int i = 1, j = 0, cnt = 0; i <= n + 1; ++i) {while (j < n && cnt < k) {++j;cnt += a[j] == o;}nx[o][i] = (cnt == k ? j : n + 1);cnt -= a[i] == o;}}int ans = N;for (int i = 1; i <= n; ++i) {int j = i - 1; for (int o = 0; o < 3 && j != n + 1; ++o) {j = nx[o][j + 1];}if (j != n + 1) ans = min(ans, j - i + 1);}cout << (ans == N ? -1 : ans - k * 3) << '\n';return 0;
}
C - スタンプラリー 3 (Collecting Stamps 3)
有 \(n\) 个雕像坐落在一个长度为 \(l\) 的环上,第 \(i\) 个雕像的位置为 \(a_i\),出现时间为 \([0, b_i]\)。
你现在从位置 \(0\) 开始以 \(1\) 的速度行走,求最多能收集到多少雕像。
\(n \le 200\),\(1 \le a_i \le l \le 10^9\)。
显然可以区间 \(\text{dp}\),由于时间(距离)维度很大,我们将其设为最优化属性。
设 \(f_{i, j, k, 0/1}\) 表示当前尝试收集过 \(i\) 以前和 \(j\) 以后的蜡像,收集成功了 \(k\) 个,位于第 \(i/j\) 个蜡像处的最小时间。转移只有两种选择,代价是 \(O(1)\) 的。
时间复杂度 \(O(n^3)\)。
Code
#include <iostream>using namespace std;
using LL = long long;
using Pii = pair<int, int>;const int N = 205;
const LL Inf = 1e12;int n, l, a[N], b[N];
LL f[N][N][N][2];int Dis (int x, int y) {int d = abs(x - y);return min(d, l - d);
}int main () {cin.tie(0)->sync_with_stdio(0);cin >> n >> l; for (int i = 1; i <= n; ++i) {cin >> a[i];} for (int i = 1; i <= n; ++i) {cin >> b[i];}fill(f[0][0][0], f[n + 1][n + 1][n] + 2, Inf);f[0][n + 1][0][0] = 0; for (int l = n + 1; l >= 2; --l) {for (int i = 0, j = i + l; j <= n + 1; ++i, ++j) {for (int k = 0; k < n; ++k) {for (int l = 0; l < 2; ++l) {int cur = (!l ? a[i] : a[j]);for (int l0 = 0; l0 < 2; ++l0) {int p = (!l0 ? i + 1 : j - 1);LL d = f[i][j][k][l] + Dis(cur, a[p]);int k0 = k + (d <= b[p]);auto f0 = (!l0 ? f[i + 1][j] : f[i][j - 1]);f0[k0][l0] = min(f0[k0][l0], d);}}} }}int ans = 0;for (int i = 0; i <= n; ++i) {for (int k = 0; k <= n; ++k) {for (int l = 0; l < 2; ++l) {if (f[i][i + 1][k][l] < Inf) {ans = max(ans, k);}}}}cout << ans << '\n';return 0;
}
D - オリンピックバス (Olympic Bus)
给定一张 \(n\) 个结点 \(m\) 条边的图,你可以进行如下操作至多一次:
- 选择第 \(i\) 条边将其翻转 (\(u \rightarrow v\) 变成 \(v \rightarrow u\)),代价为 \(d_i\)。
然后你在图上从结点 \(1\) 开始行走,走第 \(i\) 条边的代价为 \(w_i\),求走出一条路径 \(1 \rightarrow n \rightarrow 1\) 的最小代价。
\(1 \le n \le 200, 1 \le m \le 5 \times 10^4\)。
首先对原图求一遍最短路(可使用 \(\text{Floyd}\)),若不进行翻转边的操作,则总代价为 \(dis(1, n) + dis(n, 1)\)。
接下来考虑可以翻转边的情况,容易想到枚举一条边,然后重跑 \(O(n^2)\) 的 \(\text{Dijkstra}\),但这样时间复杂度是 \(O(mn^2)\),无法接受。
但是显然并不是所有边都需要重跑最短路。我们拉出以 \(1/n\) 为根,正图/反图的最短路树,则至少在一棵最短路树上的边只有 \(O(n)\) 条。
具体而言,我们对所有边分情况讨论:
- 若它在某一棵最短路树上,则这样的边只有 \(O(n)\) 条,我们沿用前面的算法,重跑 \(\text{Dijkstra}\) 即可。
- 若它不在任意一棵最短路树上,则关于 \(1/n\) 的所有最短路不会改变,此时大力分类讨论即可求出 \(dis(1, n)\) 和 \(dis(n, 1)\)。
时间复杂度 \(O(n^3)\)。
Code
#include <iostream>
#include <vector>
#include <set>
#include <numeric>
#include <algorithm>using namespace std;
using ui = unsigned int;
using Pii = pair<int, int>; const int N = 205;
const ui Inf = 1.3e9;int n, m, p[N];
ui e[N][N], e1[N][N], e2[N][N], e3[N][N], g[N][N];
ui dis[N][N];
set<Pii> s;namespace Dijkstra {ui Solve (ui e[N][N]) {vector<ui> dis(n + 1, Inf);vector<bool> vis(n + 1);dis[1] = 0; for (int t = 1; t <= n; ++t) {int u = 0; for (int i = 1; i <= n; ++i) {if (!vis[i] && (!u || dis[i] < dis[u])) {u = i; }}vis[u] = 1; for (int v = 1; v <= n; ++v) {dis[v] = min(dis[v], dis[u] + e[u][v]);}}ui ans = dis[n];fill(dis.begin(), dis.end(), Inf);fill(vis.begin(), vis.end(), 0);dis[n] = 0; for (int t = 1; t <= n; ++t) {int u = 0; for (int i = 1; i <= n; ++i) {if (!vis[i] && (!u || dis[i] < dis[u])) {u = i; }}vis[u] = 1; for (int v = 1; v <= n; ++v) {dis[v] = min(dis[v], dis[u] + e[u][v]);}}ans += dis[1];return min(ans, Inf);}
}void Build (int o) {auto add = [&](int x, int y) -> void {if (o & 1) swap(x, y);s.insert({x, y});};auto ev = [&](int x, int y) -> ui {if (o & 1) swap(x, y);return e[x][y]; };int s = (o <= 1 ? 1 : n);vector<bool> vis(n + 1);vector<int> pre(n + 1);vector<ui> dis(n + 1, Inf);dis[s] = 0;for (int t = 1; t <= n; ++t) {int u = 0; for (int i = 1; i <= n; ++i) {if (!vis[i] && (!u || dis[i] < dis[u])) {u = i; }}vis[u] = 1; if (pre[u]) {add(pre[u], u);}for (int v = 1; v <= n; ++v) {ui d = dis[u] + ev(u, v);if (d < dis[v]) {dis[v] = d, pre[v] = u; }} }
}int main () {// freopen("01-13.in", "r", stdin);// freopen("01-13.out", "w", stdout);cin.tie(0)->sync_with_stdio(0);cin >> n >> m;fill(e[0], e[n] + n + 1, Inf);fill(e1[0], e1[n] + n + 1, Inf);fill(e2[0], e2[n] + n + 1, Inf);fill(e3[0], e3[n] + n + 1, Inf);for (int i = 1, u, v, w, d; i <= m; ++i) {cin >> u >> v >> w >> d;e2[u][v] = min(e2[u][v], ui(w) + e1[u][v]);e2[u][v] = min(e2[u][v], ui(w) + d + e[u][v]);e[u][v] = min(e[u][v], ui(w));e1[u][v] = min(e1[u][v], ui(w) + d);e3[u][v] = min(e3[u][v], ui(w) * 2 + d);}fill(dis[0], dis[n] + n + 1, Inf);for (int i = 1; i <= n; ++i) {dis[i][i] = 0; }for (int i = 1; i <= n; ++i) {for (int j = 1; j <= n; ++j) {dis[i][j] = min(dis[i][j], e[i][j]);g[i][j] = e[i][j];}}for (int k = 1; k <= n; ++k) {for (int i = 1; i <= n; ++i) { for (int j = 1; j <= n; ++j) {dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);}} }for (int o = 0; o < 4; ++o) {Build(o);}ui ans = min(Inf, dis[1][n] + dis[n][1]);for (int i = 1; i <= n; ++i) {for (int j = 1; j <= n; ++j) {if (i == j) continue;if (s.count({i, j})) {g[i][j] = Inf, g[j][i] = 0; ans = min(ans, Dijkstra::Solve(g) + e1[i][j]);g[i][j] = 0;ans = min(ans, Dijkstra::Solve(g) + e2[i][j]);g[i][j] = e[i][j], g[j][i] = e[j][i];}else {auto s = [&](ui a, ui b, ui c) -> ui {return min(Inf, a + b + c);}; ans = min(ans, min(s(dis[1][j], dis[i][n], dis[n][1]), s(dis[1][n], dis[n][j], dis[i][1])) + e1[i][j]);for (int o = 0; o < 2; ++o) {int u = 1, v = n;if (o) swap(u, v);ui l0 = min(dis[u][i] + dis[i][u], Inf), l1 = min(dis[v][j] + dis[j][v], Inf);ans = min(ans, l0 + l1 + e2[i][j]);}ans = min(ans, min(Inf, dis[1][j] + dis[i][n]) + min(Inf, dis[n][j] + dis[i][1]) + e3[i][j]);}}}cout << int(ans == Inf ? -1 : ans) << '\n';return 0;
}
E - 火事 (Fire)
有一个序列,给定它在 \(0\) 时刻的形态 \(a_1, a_2, \ldots, a_n\),之后每过一单位时间,这个序列会同时发生 \(\forall i \in [1, n], a_i \gets \max(a_{i - 1}, a_i)\) 的变化。
有 \(q\) 次询问,每次询问给定 \(t, l, r\),求在 \(t\) 时刻末的 \(\sum\limits_{i = l}^{r} a_i\)。
显然由于是类似 \(\text{checkmax}\) 的操作,所以序列中的所有值只会不断变大,所以我们考虑维护元素相对与初始值的增量。而原序列的贡献可以前缀和简单维护。
考虑如何刻画增量形式。一个位置 \(i\) 在 \(t\) 时刻相当于给 \([i, i + t]\) 施加了一个 \(\text{checkmax}\)。那么在这个区间中的数就可能会变大。
那么对于一个位置 \(i\),它第一次会被 \(L_i\) 覆盖。其中 \(L_i\) 表示 \(i\) 左边第一个比 \(i\) 大的数,可以单调栈线性求出。被覆盖后可以认为 \(i\) 所在连续段合并到了 \(L_i\) 所在的连续段。
于是考虑 \(i\) 和 \(L_i\) 合并的过程。开始时间显然是 \(L_i - i\),此时对位置 \(i\) 的值有 \(a_{L_i} - a_i\) 的增量,然后第 \(L_i - i + 1\) 秒会对 \(i + 1\) 的值也有 \(a_{L_i} - a_{i}\) 的增量(此处钦定了 \(a_{i + 1} < a_i\),那么 \(i + 1\) 已经被 \(i\) 合并),一直重复这个过程,直到 \(R_i\) 为止,其中 \(R_i\) 表示 \(i\) 右边第一个比 \(i\) 大的数。
我们将上面过程写成形式化的语言:\((l, r, t_0, v) = (i, R_i - 1, i - L_i, a_{L_i} - a_i)\),在 \(t_0\) 时刻将 \(a_l \gets a_l + v\),在 \(t_0 + 1\) 时刻将 \(a_{l + 1} \gets a_{l + 1} + v\),以此类推。
然后询问的形式可以描述为三元组 \((l, r, t)\) 的形式,表示查询 \(t\) 时刻 \(\sum\limits_{i = l}^{r} a_i\) 的值。那么我们就将原问题抽象成了一个较为形式化的数据结构问题。
再做一些转化,容斥一下,将前面修改的四元组 \((l, r, t_0, v)\) 改为 \((l, t_0, v)\) 的形式,即没有 \(r\) 的限制,这样会好看一点。具体的,\((l, r, t_0, v)\) 可以拆成 \((l, t_0, v)\) 和 \((r + 1, t_0 + r - l, -v)\)。
对询问也拆一下,变成 \((p, t)\) 的形式,表示查询 \(t\) 时刻 \(\sum\limits_{i = 1}^{n} a_p\) 的值。
在对序列做个前缀和,修改可重写为在 \(t_0\) 时刻 \([l, n]\) 一段后缀 \(+v\),\(t_0 + 1\) 时刻 \([l + 1, n]\) 一段后缀 \(+v\),以此类推。查询重写为 \(t\) 时刻 \(a_p\) 的值。
直接模拟这个操作相当于二维平面上的三角形 \(+v\),也不好做。你那你考虑算一次修改 \((l, t_0, v)\) 对一次询问 \((p, t)\) 给贡献,我们发现这个贡献是 \(\max(0, \min(p - l + 1, t - t_0 + 1)) v\)。
这个看起来清新很多,我们令 \(p \gets p + 1, t \gets t + 1\),然后把 \((l, t_0)\) 和 \((p, t)\) 分别看成平面直角坐标系上的点。对于一个修改点 \((x_u, y_u, v)\) 和一个查询点 \((x_q, y_q)\),贡献为 \(\max(0, \min(x_q - x_u, y_q - y_u))v\)。
这个很想三维偏序,但稍微想想就知道其实是二维偏序,因为只有以下两种情况有贡献:
- \(0 \le x_q - x_u \le y_q - y_u\),贡献为 \((x_q - x_u)v\)。
- \(0 \le y_q - y_u < x_q - x_u\),贡献为 \((y_q - y_u)v\)。
这个扫描线 + 树状数组或者 \(\text{CDQ}\) 即可。我懒得讨论负数下标和值域范围,所以选了 \(\text{CDQ}\)。
时间复杂度 \(O(n \log n)\)。
Code
#include <iostream>
#include <stack>
#include <vector>
#include <algorithm>using namespace std;
using LL = long long;const int N = 2e5 + 5; int n, q, m;
int a[N], L[N], R[N];
LL ans[N], pre[N];struct Node {int x, y, val, id;
} v[N * 4], tmp[N * 4];void Solve1 (int l, int r) {if (l == r) return; int mid = (l + r) >> 1;Solve1(l, mid);Solve1(mid + 1, r);LL cnt = 0, sum = 0; for (int i = l, j = l, k = mid + 1; i <= r; ++i) {if (j != mid + 1 && (k == r + 1 || v[j].x - v[j].y >= v[k].x - v[k].y)) {tmp[i] = v[j++];if (!tmp[i].id) {cnt += tmp[i].val, sum += 1ll * tmp[i].x * tmp[i].val;}}else {tmp[i] = v[k++];if (tmp[i].id) {ans[tmp[i].id] += tmp[i].val * (tmp[i].x * cnt - sum);}}}copy(tmp + l, tmp + r + 1, v + l);
}void Solve2 (int l, int r) {if (l == r) return;int mid = (l + r) >> 1;Solve2(l, mid);Solve2(mid + 1, r);LL cnt = 0, sum = 0; for (int i = l, j = l, k = mid + 1; i <= r; ++i) {if (j != mid + 1 && (k == r + 1 || v[j].x - v[j].y < v[k].x - v[k].y)) {tmp[i] = v[j++];if (!tmp[i].id) {cnt += tmp[i].val, sum += 1ll * tmp[i].y * tmp[i].val;}}else {tmp[i] = v[k++];if (tmp[i].id) {ans[tmp[i].id] += tmp[i].val * (tmp[i].y * cnt - sum);}}} copy(tmp + l, tmp + r + 1, v + l);
} int main () {cin.tie(0)->sync_with_stdio(0);cin >> n >> q; for (int i = 1; i <= n; ++i) {cin >> a[i], pre[i] = pre[i - 1] + a[i];}stack<int> s;for (int i = 1; i <= n; ++i) {while (!s.empty() && a[i] > a[s.top()]) {R[s.top()] = i;s.pop();}if (!s.empty()) L[i] = s.top();s.push(i);}while (!s.empty()) {R[s.top()] = n + 1;s.pop();}for (int i = 1; i <= n; ++i) {if (L[i]) {v[++m] = Node({i, i - L[i], a[L[i]] - a[i], 0});v[++m] = Node({R[i], R[i] - L[i], a[i] - a[L[i]], 0});}}for (int i = 1, t, l, r; i <= q; ++i) {cin >> t >> l >> r;ans[i] = pre[r] - pre[l - 1];v[++m] = Node({r + 1, t + 1, 1, i});v[++m] = Node({l, t + 1, -1, i});} sort(v + 1, v + m + 1, [&](Node a, Node b) -> bool {return a.x < b.x;});Solve1(1, m);sort(v + 1, v + m + 1, [&](Node a, Node b) -> bool {return a.y < b.y;}); Solve2(1, m);for (int i = 1; i <= q; ++i) {cout << ans[i] << '\n';}return 0;
}