题解:P12151 【MX-X11-T5】「蓬莱人形 Round 1」俄罗斯方块

· · 题解

:::warning[写在前面]{open}

本题解十分详细。按照正常思考顺序编写,可以跳转到自己卡壳的地方开始阅读。

只有绿色折叠框的代码 6 能够通过本题。 :::

subtask 1

暴力,枚举每个位置激活与否,若激活,激活了后面的哪个。

:::info[代码 1] ```cpp #include <bits/stdc++.h> using namespace std; using ll = long long; using pii = pair<int, int>; #ifdef ONLINE_JUDGE #define debug(...) 0 #else #define debug(...) fprintf(stderr, __VA_ARGS__), fflush(stderr) #endif const int N = 2e4 + 5; int n, q, k; int h[N], a[N], b[N]; int backup[N]; int dfs(int u, int l, int r, int x) { if (u > r) { return *max_element(h + l, h + 1 + r) <= x; } if (dfs(u + 1, l, r, x)) return true; for (int j = u + 1; j <= min(u + k, r); j++) { h[u] += a[u]; h[j] -= b[u]; if (dfs(u + 1, l, r, x)) return true; h[u] -= a[u]; h[j] += b[u]; } return false; } void subtask1() { while (q--) { int l, r, x; cin >> l >> r >> x; for (int i = l; i <= r; i++) h[i] = backup[i]; cout << (dfs(l, l, r, x) ? "Yes\n" : "No\n"); } } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int c, tt; cin >> c >> tt; while (tt--) { cin >> n >> q >> k; for (int i = 1; i <= n; i++) cin >> h[i], backup[i] = h[i]; for (int i = 1; i <= n; i++) cin >> a[i]; for (int i = 1; i <= n; i++) cin >> b[i]; subtask1(); } return 0; } ``` ::: 代码 1 期望获得 $5$ 分,实际获得 $5$ 分。 ### subtask 2 & 3 $k\le 5$,考虑状压 dp。 设 $f_{i, S}$ 表示:处理至点 $i$,往前 $k$ 个点(**包括点** $i$)的状态为 $S$,此时前 $i$ 个数的最大值 的最小值。 其中 $S$ 是一个二进制数,$S$ 的第 $t(0\le t <k)$ 位为 $1$ 表示点 $i - t$ **已激活**,**但还没有往后贡献**(对应题目中就是,激活了点 $p$,但还没往后选择点 $j$)。 转移的时候,枚举 $S$ 的子集 $T$,让 $T$ 中的点贡献给 $i + 1$,即让 $h_{i + 1}$ 减去 $T$ 中点的 $b$。 而点 $i + 1$ 有激活与不激活两种选项,分别写出转移: - 不激活点 $i + 1$: $$f_{i + 1, (S \oplus T) \ll 1} = \min\left(f_{i + 1, (S \oplus T) \ll 1}, \max\left(f_{i, S}, h_{i + 1} - \sum\limits_{x \in T}b_x\right)\right)$$ - 激活点 $i + 1$: $$f_{i + 1, (S \oplus T) \ll 1 | 1} = \min\left(f_{i + 1, (S \oplus T) \ll 1 | 1}, \max\left(f_{i, S}, h_{i + 1} + a_{i + 1} - \sum\limits_{x \in T} b_x\right)\right)$$ 其中 $S \oplus T$ 表示去掉了子集 $T$(贡献给了 $i + 1$),或上 $1$ 表示激活点 $i + 1$,因为只处理到 $i + 1$,肯定是无法往后贡献的。 边界条件:$f_{l, 0} = h_l,f_{l, 1} = h_l + a_l$。其他状态初始设置为 $+\infty$。 若 $f_{r, 0} \le x$(所有激活了的点都贡献了出去),答案为 `Yes`,否则为 `No`。 $\Theta(qn3^k)$。 :::info[代码 2] ```cpp #include <bits/stdc++.h> using namespace std; using ll = long long; using pii = pair<int, int>; #ifdef ONLINE_JUDGE #define debug(...) 0 #else #define debug(...) fprintf(stderr, __VA_ARGS__), fflush(stderr) #endif const int N = 2e4 + 5; const int inf = 0x3f3f3f3f; int n, q, k; int h[N], a[N], b[N]; int f[N][32]; void chkmin(int &x, int y) {x = min(x, y);} int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int tc, tt; cin >> tc >> tt; while (tt--) { cin >> n >> q >> k; for (int i = 1; i <= n; i++) cin >> h[i]; for (int i = 1; i <= n; i++) cin >> a[i]; for (int i = 1; i <= n; i++) cin >> b[i]; while (q--) { int l, r, x; cin >> l >> r >> x; for (int i = l; i <= r; i++) for (int j = 0; j < (1 << k); j++) f[i][j] = inf; f[l][0] = h[l], f[l][1] = h[l] + a[l]; for (int i = l; i < r; i++) { for (int j = 0; j < (1 << min(k, i - l + 1)); j++) { chkmin(f[i + 1][j << 1 & 31], max(f[i][j], h[i + 1])); chkmin(f[i + 1][(j << 1 | 1) & 31], max(f[i][j], h[i + 1] + a[i + 1])); for (int s = j; s; s = (s - 1) & j) { int d = 0; for (int t = 0; t < min(k, i - l + 1); t++) if (s >> t & 1) if (i - t >= l) d += b[i - t]; chkmin(f[i + 1][(j ^ s) << 1 & 31], max(f[i][j], h[i + 1] - d)); chkmin(f[i + 1][((j ^ s) << 1 | 1) & 31], max(f[i][j], h[i + 1] + a[i + 1] - d)); } } } cout << (f[r][0] <= x ? "Yes\n" : "No\n"); } } return 0; } ``` ::: 代码 2 期望获得 $25$ 分,实际获得 $15$ 分(这份代码没有特殊处理特殊性质 A,A 的话只要跑一遍全局的 dp 就行)。 ### subtask 4 ~ 7 如何加速区间询问? 观察转移方程: $$f_{i + 1, (S \oplus T) \ll 1} = \min\left(f_{i + 1, (S \oplus T) \ll 1}, \max\left(f_{i, S}, h_{i + 1} - \sum\limits_{x \in T}b_x\right)\right)$$ $$f_{i + 1, (S \oplus T) \ll 1 | 1} = \min\left(f_{i + 1, (S \oplus T) \ll 1 | 1}, \max\left(f_{i, S}, h_{i + 1} + a_{i + 1} - \sum\limits_{x \in T} b_x\right)\right)$$ 可以发现它们的形式都类似这样: $$f_{i + 1, U} = \min\left(f_{i + 1, U}, \max\left(f_{i, S}, v\right)\right)$$ 由于 $\min$ 运算对 $\max$ 运算有分配律(反过来也成立哦),上式可以矩阵乘法加速,定义转移矩阵 $A_{S, U} = v$,那么 $f_{i + 1} = f_i \times A$。 进一步发现,每个 $i$ 对应的转移矩阵 $A$ 是一样的,问题转化成了求静态的区间矩阵乘积,使用线段树维护。 注意询问的时候,查到线段树上对应的某个区间的矩阵乘积,直接用答案向量 $f$ 去乘它,不要把一堆这些矩阵乘在一次,不然时间复杂度会多乘上一个 $2^k$。 $\Theta(n8^k + q4^k\log n)$。 :::info[代码 3] ``` #include <bits/stdc++.h> using namespace std; using ll = long long; using pii = pair<int, int>; #ifdef ONLINE_JUDGE #define debug(...) 0 #else #define debug(...) fprintf(stderr, __VA_ARGS__), fflush(stderr) #endif const int N = 2e4 + 5; const int inf = 0x3f3f3f3f; int n, q, k; int h[N], a[N], b[N]; // int f[N][32]; void chkmin(int &x, int y) {x = min(x, y);} using Matrix = vector<vector<int>>; Matrix I() {Matrix I(1 << k, vector<int>(1 << k, inf)); for (int i = 0; i < I.size(); i++) I[i][i] = -inf; return I;} Matrix operator*(const Matrix &a, const Matrix &b) { Matrix c(1 << k, vector<int>(1 << k, inf)); for (int i = 0; i < a.size(); i++) for (int j = 0; j < b[0].size(); j++) for (int k = 0; k < b.size(); k++) c[i][j] = min(c[i][j], max(a[i][k], b[k][j])); return c; } Matrix tr[N << 2]; void build(int p, int l, int r) { if (l == r) { tr[p].assign(1 << k, vector<int>(1 << k, inf)); for (int j = 0; j < (1 << k); j++) { chkmin(tr[p][j][j << 1 & (1 << k) - 1], h[l + 1]); chkmin(tr[p][j][(j << 1 | 1) & (1 << k) - 1], h[l + 1] + a[l + 1]); for (int s = j; s; s = (s - 1) & j) { int d = 0; bool valid = true; for (int t = 0; t < k; t++) if (s >> t & 1) { if (l - t < 1) { valid = false; break; } d += b[l - t]; } if (!valid) continue; chkmin(tr[p][j][(j ^ s) << 1 & (1 << k) - 1], h[l + 1] - d); chkmin(tr[p][j][((j ^ s) << 1 | 1) & (1 << k) - 1], h[l + 1] + a[l + 1] - d); } } return; } int mid = l + r >> 1; build(p << 1, l, mid); build(p << 1 | 1, mid + 1, r); tr[p] = tr[p << 1] * tr[p << 1 | 1]; } void query(int p, int l, int r, int L, int R, Matrix &f) { if (L > R || r < L || l > R) return; if (L <= l && r <= R) {f = f * tr[p]; return;} int mid = l + r >> 1; query(p << 1, l, mid, L, R, f); query(p << 1 | 1, mid + 1, r, L, R, f); } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int tc, tt; cin >> tc >> tt; while (tt--) { cin >> n >> q >> k; for (int i = 1; i <= n; i++) cin >> h[i]; for (int i = 1; i <= n; i++) cin >> a[i]; for (int i = 1; i <= n; i++) cin >> b[i]; build(1, 1, n - 1); while (q--) { int l, r, x; cin >> l >> r >> x; Matrix f(1, vector<int>(1 << k, inf)); f[0][0] = h[l], f[0][1] = h[l] + a[l]; // auto A = query(1, 1, n, l, r - 1); query(1, 1, n - 1, l, r - 1, f); cout << (f[0][0] <= x ? "Yes\n" : "No\n"); } } return 0; } ``` ::: 代码 3 期望得分 $70$ 分,实际得分 $55$ 分(常数太大)。 ### subtask 8 观察特殊性质 B:所有 $x$ 均相等。 实际上我们只关心每个数相对 $x$ 的大小关系,不妨令 $\le x$ 的数为 $0$,$>x$ 的数为 $1$,把 $2^k \times 2^k$ 的矩阵改成 $2^k$ 个 `bitset`,每个 `bitset` $2^k$ 位。其他同上。 用 `bitset` 加速矩阵乘法,具体地,原本的矩阵乘法形如 $$c_{i, j} = \min_{k}\max(a_{i, k}, b_{k, j})$$ 改成 $0/1$ 后可以用位运算 $$c_{i, j} = \text{and}_k(a_{i, k} \ \text{or} \ b_{k, j})$$ $j$ 这一维可以省略了,仅需枚举 $i$ 和 $k$。 - 若 $a_{i, k} = 1$,后面 $\max(a_{i, k}, b_{k, j})$,也就是 $a_{i, k}\ \text{or} \ b_{k, j}$ 一定为 $1$,前面对 $1$ 取 $\min$ 显然不会影响 $c_{i, j}$。 - 若 $a_{i, k} = 0$,它不会影响 $\max$,可以直接拿掉, $$c_{i, j} = \text{and}_k b_{k, j}$$ 既然用了 `bitset`,就直接 $c_i = c_i \ \text{and} \ b_i$ 即可,复杂度除以 $w$。 $\Theta\left(\dfrac{n8^k + q4^k \log n}{w}\right)$。 :::info[代码 4] ```cpp #include <bits/stdc++.h> using namespace std; using ll = long long; using pii = pair<int, int>; #ifdef ONLINE_JUDGE #define debug(...) 0 #else #define debug(...) fprintf(stderr, __VA_ARGS__), fflush(stderr) #endif const int N = 2e4 + 5; const ll inf = (1ll << 32) - 1; int n, q, k, x; int h[N], a[N], b[N]; // int f[N][32]; using Matrix = vector<bitset<32>>; ostream& operator<<(ostream &os, const Matrix &b) { for (int i = 0; i < b.size(); i++) { os << b[i]; if (i != b.size() - 1) os << "\n"; } return os; } Matrix operator*(const Matrix &a, const Matrix &b) { Matrix c(32, bitset<32>(inf)); for (int i = 0; i < a.size(); i++) for (int k = 0; k < b.size(); k++) if (!a[i][k]) c[i] &= b[k]; // for (int i = 0; i < a.size(); i++) // for (int j = 0; j < b[0].size(); j++) // for (int k = 0; k < b.size(); k++) // c[i][j] = c[i][j] & (a[i][k] | b[k][j]); return c; } Matrix tr[N << 2]; void build(int p, int l, int r) { if (l == r) { tr[p].assign(32, bitset<32>(inf)); for (int j = 0; j < (1 << k); j++) { tr[p][j][j << 1 & 31] = (h[l + 1] > x); tr[p][j][(j << 1 | 1) & 31] = (h[l + 1] + a[l + 1] > x); for (int s = j; s; s = (s - 1) & j) { int d = 0; bool valid = true; for (int t = 0; t < k; t++) if (s >> t & 1) { if (l - t < 1) { valid = false; break; } d += b[l - t]; } if (!valid) continue; tr[p][j][(j ^ s) << 1 & 31] = tr[p][j][(j ^ s) << 1 & 31] & (h[l + 1] - d > x); tr[p][j][((j ^ s) << 1 | 1) & 31] = tr[p][j][((j ^ s) << 1 | 1) & 31] & (h[l + 1] + a[l + 1] - d > x); } } return; } int mid = l + r >> 1; build(p << 1, l, mid); build(p << 1 | 1, mid + 1, r); tr[p] = tr[p << 1] * tr[p << 1 | 1]; } void query(int p, int l, int r, int L, int R, Matrix &f) { if (L > R || r < L || l > R) return; if (L <= l && r <= R) {f = f * tr[p]; return;} int mid = l + r >> 1; query(p << 1, l, mid, L, R, f); query(p << 1 | 1, mid + 1, r, L, R, f); } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int tc, tt; cin >> tc >> tt; while (tt--) { cin >> n >> q >> k; for (int i = 1; i <= n; i++) cin >> h[i]; for (int i = 1; i <= n; i++) cin >> a[i]; for (int i = 1; i <= n; i++) cin >> b[i]; int l, r; cin >> l >> r >> x; build(1, 1, n - 1); while (q--) { Matrix f(32, bitset<32>(inf)); f[0][0] = (h[l] > x), f[0][1] = (h[l] + a[l] > x); query(1, 1, n - 1, l, r - 1, f); cout << (!f[0][0] ? "Yes\n" : "No\n"); if (q) cin >> l >> r >> x; } } return 0; } ``` ::: 代码 4 期望得分 $15$ 分,实际得分 $10$ 分(常数太大)。 ### subtask 9 & 10 考虑推广特殊性质 B。将询问离线,将所有询问和 $A_{S, U} = v$ 这样的修改,按照 $x$(对于修改,则是 $v$)从小到大排序,优先修改后查询。 初始时从小到大遍历询问和修改,如果是询问,正常查询即可。 如果是修改,发现修改仅仅是将线段树某个叶子节点的矩阵的第 $S$ 行 $U$ 列置 $0$,如果 pushup 的时候直接左儿子矩阵乘右儿子矩阵,复杂度爆炸。 我们只考虑哪些位置要重新更新。叶子节点直接更改。 接下来,令左儿子的矩阵是 $a$,右儿子的矩阵是 $b$,该节点的矩阵是 $c$。 $$c_{i, j} = \min_k\max(a_{i, k}, b_{k, j})$$ $$c_{i, j} = \text{and}_k(a_{i, k} \ \text{or} \ b_{k, j})$$ 依次往上看,先看叶子节点的父亲 $u$,发现儿子在左子树还是右子树的情况不同,分类讨论: - 若修改来自 $u$ 的左儿子,不妨假设修改是把 $a_{i, k}$ 置 $0$。 - 若 $b_{k, j} = 1$,$a_{i, k} \ \text{or} \ b_{k, j}$ 一定为 $1$,不会影响前面的 $\text{and}$。 - 也就是只有 $b_{k, j} = 0$ 的位置可能会影响 $c_{i, j}$,只要找 $b_k$ 所有 $0$ 的位置,把 $c_{i, j}$ 置 $0$ 即可。 - 若修改来自 $u$ 的右儿子,不妨假设修改是把 $b_{k, j}$ 置 $0$。 - 若 $a_{i, k} = 1$,$a_{i, k} \ \text{or} \ b_{k, j}$ 一定为 $1$,不会影响前面的 $\text{and}$。 - 也就是只有 $a_{i, k} = 0$ 的位置会影响 $c_{i, j}$,只要找所有第 $k$ 位是 $0$ 的 $a_{i}$,把 $c_{i, j}$ 置 $0$ 即可。 对于左儿子,我们知道 libstdc++ 为 bitset 提供了方便的 `_Find_first()` 和 `_Find_next(pos)` 成员函数,但这是找 $1$ 的,把 $b_k$ 取反找 $1$ 就等价于找 $0$ 了。 对于右儿子,发现跨了 `bitset`,有点棘手?记录转置矩阵 $A^T_{j, i} = A_{i, j}$,修改是同步的,只有在这一步,要找所有第 $k$ 位是 $0$ 的 $a_i$,只需要找 $a^T_{k}$ 所有的 $0$ 即可,同上取反找 $1$ 即可。 注意,一个 $c_{i, j}$ 被置 $0$ 后就不会再变回 $1$,要保证每次找的 $1$ 一共只被找 $1$ 次,可以使用一些位运算技巧。 :::info[技巧] 因为是实现细节,所以放在了折叠框里。 左右儿子情况类似,不妨只讨论左儿子。 我们要找到所有位置 $j$,使得取反后的 $b_{k, j} = 0$,且这个位置没被我们找过,这意味着 $c_{i, j}=1$。我们的处理把 $b$ 取反得到 $b'$,现在要找 $b_{k, j} = 1$ 且 $c_{i, j} = 1$ 的位置,直接令 $b_k \leftarrow b_k \ \text{and} \ c_i$ 即可。 右儿子就是把 $b_k$ 换成了 $a^T_k$。 ::: 发现现在 $c_{i, j}$ 又有若干个置 $0$ 的修改,用 vector 存下这些修改,在父亲处继续修改即可。 每个点我们保证了只有一次 $1$ 变成 $0$,一共有 $\Theta(n4^k)$ 个这样的操作。复杂度可接受。 $\Theta\left(\dfrac{n8^k + q4^k \log n}{w}\right)$。 :::info[代码 5] ```cpp #include <bits/stdc++.h> using namespace std; using ll = long long; using pii = pair<int, int>; #ifdef ONLINE_JUDGE #define debug(...) 0 #else #define debug(...) fprintf(stderr, __VA_ARGS__), fflush(stderr) #endif const int N = 2e4 + 5, M = 1e5 + 5; const unsigned inf = ~0; int n, m, k; int h[N], a[N], b[N]; using Matrix = vector<bitset<32>>; int ans[M]; struct Query { int l, r, x, id; int typ; Query(int l, int r, int x, int id, int typ) : l(l), r(r), x(x), id(id), typ(typ) {} bool operator<(const Query &b) const {return x != b.x ? x < b.x : typ < b.typ;} }; vector<Query> q; ostream& operator<<(ostream &os, const Matrix &b) { for (int i = 0; i < b.size(); i++) { os << b[i]; if (i != b.size() - 1) os << "\n"; } return os; } bitset<32> operator*(const bitset<32> &a, const Matrix &b) { bitset<32> c(inf); for (int k = 0; k < b.size(); k++) if (!a[k]) c &= b[k]; return c; } // Matrix operator*(const Matrix &a, const Matrix &b) { // Matrix c(32, bitset<32>(inf)); // for (int i = 0; i < a.size(); i++) // for (int k = 0; k < b.size(); k++) // if (!a[i][k]) // c[i] &= b[k]; // return c; // } Matrix tr[N << 2], tr2[N << 2]; vector<pii> add[N << 2]; void build(int p, int l, int r) { tr[p].assign(32, bitset<32>(inf)); tr2[p].assign(32, bitset<32>(inf)); if (l == r) return; int mid = l + r >> 1; build(p << 1, l, mid); build(p << 1 | 1, mid + 1, r); } void modify(int p, int l, int r, int x, int s, int t) { if (l == r) { tr[p][s][t] = 0; tr2[p][t][s] = 0; add[p].emplace_back(s, t); return; } int mid = l + r >> 1; if (x <= mid) { modify(p << 1, l, mid, x, s, t); for (auto [s, t]: add[p << 1]) { auto rev = ~tr[p << 1 | 1][t]; rev &= tr[p][s]; for (int k = rev._Find_first(); k < 32; k = rev._Find_next(k)) { tr[p][s][k] = tr2[p][k][s] = 0; add[p].emplace_back(s, k); } } add[p << 1].clear(); add[p << 1].shrink_to_fit(); } else { modify(p << 1 | 1, mid + 1, r, x, s, t); for (auto [s, t]: add[p << 1 | 1]) { auto rev = ~tr2[p << 1][s]; rev &= tr2[p][t]; for (int i = rev._Find_first(); i < 32; i = rev._Find_next(i)) { tr[p][i][t] = tr2[p][t][i] = 0; add[p].emplace_back(i, t); } } add[p << 1 | 1].clear(); add[p << 1 | 1].shrink_to_fit(); } // if (corp != tr[p]) { // debug("Let's check out the mergement of matrix %d\n", p); // debug("the modify comes from %s, and s = %d, t = %d\n", lef ? "left" : "right", s, t); // cerr << "left matrix: \n" << tr[p << 1] << "\n"; // cerr << "right matrix: \n" << tr[p << 1 | 1] << "\n"; // cerr << "correct matrix " << p << ":\n" << corp << "\n"; // cerr << "present matrix " << p << ": \n" << tr[p] << "\n\n"; // } } void query(int p, int l, int r, int L, int R, bitset<32> &f) { if (L > R || r < L || l > R) return; if (L <= l && r <= R) {f = f * tr[p]; return;} int mid = l + r >> 1; query(p << 1, l, mid, L, R, f); query(p << 1 | 1, mid + 1, r, L, R, f); } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int tc, tt; cin >> tc >> tt; while (tt--) { cin >> n >> m >> k; for (int i = 1; i <= n; i++) cin >> h[i]; for (int i = 1; i <= n; i++) cin >> a[i]; for (int i = 1; i <= n; i++) cin >> b[i]; q.clear(); for (int i = 1, l, r, x; i <= m; i++) { cin >> l >> r >> x; q.emplace_back(l, r, x, i, 1); } for (int i = 1; i < n; i++) for (int j = 0; j < (1 << k); j++) { q.emplace_back(j, j << 1 & 31, h[i + 1], i, 0); q.emplace_back(j, (j << 1 | 1) & 31, h[i + 1] + a[i + 1], i, 0); for (int s = j; s; s = (s - 1) & j) { int d = 0; bool valid = true; for (int t = 0; t < k; t++) if (s >> t & 1) { if (i - t < 1) { valid = false; break; } d += b[i - t]; } if (!valid) continue; q.emplace_back(j, (j ^ s) << 1 & 31, h[i + 1] - d, i, 0); q.emplace_back(j, ((j ^ s) << 1 | 1) & 31, h[i + 1] + a[i + 1] - d, i, 0); } } build(1, 1, n - 1); sort(q.begin(), q.end()); for (auto [l, r, x, id, typ]: q) { if (typ) { // Matrix f(32, bitset<32>(inf)); // f[0][0] = h[l] > x, f[0][1] = h[l] + a[l] > x; bitset<32> f(inf); f[0] = h[l] > x, f[1] = h[l] + a[l] > x; query(1, 1, n - 1, l, r - 1, f); ans[id] = !f[0]; } else { modify(1, 1, n - 1, id, l, r); } } for (int i = 1; i <= m; i++) cout << (ans[i] ? "Yes\n" : "No\n"); } return 0; } ``` ::: 代码 5 期望得分 $100$,实际得分 $5$。 ### 卡常 :::warning{open} 本题卡常严重。 ::: 这部分更多算是我的一些碎碎念,感觉偏离了题解本身教授题目做法的宗旨,所以我放在了折叠框里。 如果你觉得阅读完上面的题解后,认为自己能够写出复杂度正确且常数较小的优秀做法,你可以不用打开折叠框。但如果你认为自己可能会被卡常,可以阅读一下,了解一些注意事项。 :::info[卡常] 从代码 3 开始就埋下伏笔了。 代码 5 的复杂度正确,但因为常数巨大只能获得 $5$ 分。仔细分析不难发现,代码 5 的常数巨大主要由两方面导致: #### 实现本身的不够优。 ##### 排序 排序是很难被注意到的一点。 我们将所有的询问和修改放在了一起,可以发现,极端情况下询问和修改的总数是 $Q = q + 2n3^k = 9.82 \times 10^6$,直接排序乘上 $20$ 多的 $\log$,不优。 降低排序的复杂度就考虑计数排序,$x_i$ 的值域上限是 $h + a \le 10^6 + 10^6 = 2\times 10^6$,下限是 $0$ 减去 $k$ 个最大的 $b$ 就是 $-5\times 10^6$。值域大小 $7 \times 10^6$,可以接受。 计数排序的时间复杂度是 $\Theta(Q)$,$Q$ 是询问和修改的总数。 ##### 构造转移矩阵 如果你看了我的代码 5 的实现,会发现那是我直接在 dp 的基础上改的。 ```cpp q.emplace_back(j, (j ^ s) << 1 & 31, h[i + 1] - d, i, 0); q.emplace_back(j, ((j ^ s) << 1 | 1) & 31, h[i + 1] + a[i + 1] - d, i, 0); ``` 我直接把所有的修改拿出来排序。但修改有很多是打在了同一个位置的,因为一个 $j$ 对应了最多 $32$ 个状态,这里跟 dp 的时候不一样,dp 的时候没有什么 $\Theta\left(\dfrac{q4^k\log n}{w}\right)$ 的修改,只有 $\Theta(1)$ 的取 $\min$。这是非常严重的失误,显然只有最小的 $x$ 有用。$1$ 改成 $0$ 后不会再修改。这可以显著减少修改。 上面这两个是导致常数巨大的主要原因。但还是在一些数据上跑得慢,剩下的就是常规的卡常技巧。 #### 常规卡常 我做了包括但不限于下面的常规卡常: - 快读快写; - 少用 vector; - 修改的时候,记录一下序列的第 $i$ 个对应线段树哪个节点,直接从底往上修改,就不用再递归去找叶子,没有修改直接退出; - 全局 `bitset`。 ::: :::success[代码 6] ``` #include <bits/stdc++.h> using namespace std; using ll = long long; using pii = pair<int, int>; #ifdef ONLINE_JUDGE #define debug(...) 0 #else #define debug(...) fprintf(stderr, __VA_ARGS__), fflush(stderr) #endif namespace fastio{ // from MiniLong #ifdef ONLINE_JUDGE char ibuf[1 << 20],*p1 = ibuf, *p2 = ibuf; #define get() p1 == p2 && (p2 = (p1 = ibuf) + fread(ibuf, 1, 1 << 20, stdin), p1 == p2) ? EOF : *p1++ #else #define get() getchar() #endif template<typename T> inline void read(T &t){ T x = 0, f = 1; char c = getchar(); while(!isdigit(c)){ if(c == '-') f = -f; c = getchar(); } while(isdigit(c)) x = x * 10 + c - '0', c = getchar(); t = x * f; } template<typename T, typename ... Args> inline void read(T &t, Args&... args){ read(t); read(args...); } template<typename T> void write(T t){ if(t < 0) putchar('-'), t = -t; if(t >= 10) write(t / 10); putchar(t % 10 + '0'); } template<typename T, typename ... Args> void write(T t, Args... args){ write(t), putchar(' '), write(args...); } template<typename T> void writeln(T t){ write(t); puts(""); } template<typename T> void writes(T t){ write(t), putchar(' '); } #undef get }; using namespace fastio; const int N = 2e4 + 5, M = 1e5 + 5, Q = 5e6; const unsigned inf = ~0; int n, m, k; int h[N], a[N], b[N]; int ans[M]; struct Query { int l, r, x, id; int typ; Query() {} Query(int l, int r, int x, int id, int typ) : l(l), r(r), x(x), id(id), typ(typ) {} bool operator<(const Query &b) const {return x != b.x ? x < b.x : typ < b.typ;} } q[M + N * 486], qq[M + N * 486]; bitset<32> tr[N << 2][32], tr2[N << 2][32]; vector<pii> add[N << 2]; int reff[N]; void build(int p, int l, int r) { for (int i = 0; i < 32; i++) { tr[p][i].set(); tr2[p][i].set(); } if (l == r) return reff[l] = p, void(); int mid = l + r >> 1; build(p << 1, l, mid); build(p << 1 | 1, mid + 1, r); } void modify(int x, int s, int t) { int p = reff[x]; if (!tr[p][s][t]) return; tr[p][s][t] = tr2[p][t][s] = 0; add[p].emplace_back(s, t); p >>= 1; while (p) { if (add[p << 1].empty() && add[p << 1 | 1].empty()) break; for (auto [s, t]: add[p << 1]) { auto rev = ~tr[p << 1 | 1][t] & tr[p][s]; for (int i = rev._Find_first(); i < (1 << k); i = rev._Find_next(i)) { tr[p][s][i] = tr2[p][i][s] = 0; add[p].emplace_back(s, i); } } add[p << 1].clear(); for (auto [s, t]: add[p << 1 | 1]) { auto rev = ~tr2[p << 1][s] & tr2[p][t]; for (int i = rev._Find_first(); i < (1 << k); i = rev._Find_next(i)) { tr[p][i][t] = tr2[p][t][i] = 0; add[p].emplace_back(i, t); } } add[p << 1 | 1].clear(); p >>= 1; } } bitset<32> f, c; void query(int p, int l, int r, int L, int R) { if (L > R || r < L || l > R) return; if (L <= l && r <= R) { c.set(); for (int i = 0; i < (1 << k); i++) if (!f[i]) c &= tr[p][i]; f = c; return; } int mid = l + r >> 1; query(p << 1, l, mid, L, R); query(p << 1 | 1, mid + 1, r, L, R); } int cnt[7000005]; int g[32]; int main() { int tc, tt; read(tc, tt); int maxx = 0; while (tt--) { read(n, m, k); for (int i = 1; i <= n; i++) read(h[i]); for (int i = 1; i <= n; i++) read(a[i]); for (int i = 1; i <= n; i++) read(b[i]); for (int i = 1; i <= maxx; i++) cnt[i] = 0; int lenq = 0; for (int i = 1; i < n; i++) for (int j = 0; j < (1 << k); j++) { for (int t = 0; t < 32; t++) g[t] = 2e9; for (int s = j; ; s = (s - 1) & j) { int d = 0; bool valid = true; for (int t = 0; t < k; t++) if (s >> t & 1) { if (i - t < 1) { valid = false; break; } d += b[i - t]; } if (!valid) continue; g[(j ^ s) << 1 & 31] = min(g[(j ^ s) << 1 & 31], h[i + 1] - d); g[((j ^ s) << 1 | 1) & 31] = min(g[((j ^ s) << 1 | 1) & 31], h[i + 1] + a[i + 1] - d); // q[++lenq] = {j, (j ^ s) << 1 & 31, h[i + 1] - d, i, 0}, maxx = max(maxx, h[i + 1] - d + Q); // debug("[%d][%d] = %d\n", j, (j ^ s) << 1 & 31, h[i + 1] - d); // q[++lenq] = {j, ((j ^ s) << 1 | 1) & 31, h[i + 1] + a[i + 1] - d, i, 0}, maxx = max(maxx, h[i + 1] + a[i + 1] - d + Q); // debug("[%d][%d] = %d\n", j, ((j ^ s) << 1 | 1) & 31, h[i + 1] + a[i + 1] - d); if (!s) break; } for (int t = 0; t < (1 << k); t++) { if (g[t] == 2e9) continue; q[++lenq] = {j, t, g[t], i, 0}; maxx = max(maxx, g[t] + Q); } } for (int i = 1, l, r, x; i <= m; i++) { read(l, r, x); maxx = max(maxx, x + Q); q[++lenq] = {l, r, x, i, 1}; } build(1, 1, n - 1); for (int i = 1; i <= lenq; i++) cnt[q[i].x + Q]++; for (int i = 1; i <= maxx; i++) cnt[i] += cnt[i - 1]; for (int i = lenq; i >= 1; i--) qq[cnt[q[i].x + Q]--] = q[i]; for (int i = 1; i <= lenq; i++) { int l = qq[i].l, r = qq[i].r, x = qq[i].x, id = qq[i].id, typ = qq[i].typ; if (typ) { // Matrix f(32, bitset<32>(inf)); // f[0][0] = h[l] > x, f[0][1] = h[l] + a[l] > x; f.set(); f[0] = h[l] > x, f[1] = h[l] + a[l] > x; query(1, 1, n - 1, l, r - 1); ans[id] = !f[0]; } else { modify(id, l, r); } } for (int i = 1; i <= m; i++) puts(ans[i] ? "Yes" : "No"); } return 0; } ``` ::: 代码 6 期望得分 $100$,实际得分 $100$。 代码 6 和正常编写的代码 5 相比重构太多,可读性比较差。 ### 后记 & 闲话 :::info[后记 & 闲话] #### 为什么我要写这么详细的题解? 我个人是理解能力比较一般的人,每次看到模拟赛后贴出来的两三行题解,都得仔细研读,乃至去网上搜索更加详细的题解。这让我的改题十分痛苦,因为不能准确捕捉到写题解的人的意思。 我没有权利要求其他选手写出详细的题解,毕竟题解是选手花自己的时间分享自己的心得,而不是老师教导学生。但我想写详细的题解,真正能帮别人的学会这道题的题解,而不仅是自己的做题心得,而且详细的题解也能让我更好地梳理自己的思路,对题目有更深的认识。 #### 居然每个部分都有代码? 这几个部分大致和出题人题解一样,我就是按着这几个部分一个部分一份代码写过来的。这也是正常、合理、科学的思考链条,按部就班有利于更好把握题目。 #### 吐槽 这题花了我很多时间,毕竟每个部分我都去实现。但卡常花了我尤其久,有一天半左右了。总共提交了九十多发。卡常是真的恶心。题很好,就是卡常太恶心。 #### 致谢 本题的出题人 @[MiniLong](https://www.luogu.com.cn/user/573341) 给予了我很多帮助,是一位实力强大且思路清晰的选手。非常感谢。 #### 闲话 洛谷更新的这个折叠框我非常喜欢。 这篇题解花了我很多时间,虽然这题是冷门题,看到的人也不多,但希望我的题解帮到了你。 这是我写过最久最长最详细的题解,27k+ 字。 :::