做题记录 #4
A. P11364 树上查询 (7)
挑战一年前的外星人惨败而归。
一开始想着直接转 dfn 好处理一个区间的 LCA,因为编号区间可以直接变成区间最大最小 dfn 中最小 dep。但是若是想要维护,很难不上一些笛卡尔树和单调栈。玩一玩想一想发现根本想不到一个性质优秀的刻画方式,也想不到 dfn 怎么很好的跟编号对应。不过由于 dfn 求 LCA 的性质,可以发现可以把区间内 dfn 相邻 / 有交 的两个区间并起来,并起来的答案是小区间 dep 最小值取 min。所以考虑拆分极值对应大区间,比如可以把所有 dfn 相邻的中间区间 最小 dep 取 min。虽然还是没法直接维护。
但是反思一下,为什么要转 dfn。既然可以并起来,那我是不是直接拿编号相邻的并起来也可以?毕竟可以把区间内对应 dfn 当成一个点,然后如此拆分相当于用一条链串起来,很明显只要我能串到极值点就一定可以算出真正答案。因此,令 d[i] = dep[LCA(i, i + 1)],就有区间 \([l,r]\) 的 LCA 深度为 \(\min\limits_{i\in[l,r)} d_i\)。有了这个,再考虑对于每个 \(i\) 求出极长区间 \([L_i,R_i]\) 使得 dep[LCA] = d[i]。考虑如何求解:
此时相当于每次查询一个 \([l,r]\),求满足 \(|[L_i,R_i]\cap [l,r]|\geq k\) 的最大 \(d_i\)。此时相当于要求 \(R_i\geq l+k-1,L_i\leq r-k+1, R_i-L_i+1\geq k\)。首先已经可以 三维偏序 俩 log 做(没想到,qjy 教的,跑的很快。)但是如果想要直接维护,会发现 \(L_i\) 和 \(R_i\) 的互相限制很令人头疼,不难发现其实这三个限制对应的是边界相交的三种情况,考虑怎么拆分一下。可以改成,若 \(R_i\geq r\),则需要 \(L_i\leq r-k+1\);若 \(R_i< r\),则需要 \(R_i\geq l+k-1\),且 \(R_i-L_i+1\geq k\)。前者可以 按 \(r\) 扫描线,后者按区间长扫描线即可。
在 dfn 的超级大弯路上走了若干小时,最后一步也想了两三个假的做法,花费近四个小时惨败而归。这个故事告诉我们,有的时候直观好想但有代价的转化应当谨慎,至少不应该完全不想其他的直接 all in。
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 5e5 + 1;
int n, q;
vector<int> e[kN];
int dfn[kN], dfc, dep[kN], st[20][kN];
void Init(int x, int fa) {
st[0][dfn[x] = ++dfc] = fa;
dep[x] = dep[fa] + 1;
for (auto v : e[x])
(v != fa) && (Init(v, x), 0);
}
inline int High(int x, int y) { return dep[x] < dep[y] ? x : y; }
inline int LCA(int x, int y) {
if (x == y)
return x;
x = dfn[x], y = dfn[y];
x > y && (swap(x, y), 0);
int k = __lg(y - x);
return High(st[k][x + 1], st[k][y - (1 << k) + 1]);
}
int t[4 * kN];
void Update(int p, int w, int x = 1, int l = 1, int r = n) {
if (l == r)
return t[x] = max(t[x], w), void();
int mid = (l + r) / 2;
if (p <= mid)
Update(p, w, 2 * x, l, mid);
else
Update(p, w, 2 * x + 1, mid + 1, r);
t[x] = max(t[2 * x], t[2 * x + 1]);
}
int Max(int L, int R, int x = 1, int l = 1, int r = n) {
if (R < l || r < L)
return 0;
else if (L <= l && r <= R)
return t[x];
int mid = (l + r) / 2;
return max(Max(L, R, 2 * x, l, mid), Max(L, R, 2 * x + 1, mid + 1, r));
}
int f[kN];
struct kItv { int l, r, w; };
vector<kItv> krg[kN];
struct rItv { int l, w; };
vector<rItv> rrg[kN];
inline void GetItv() {
vector<int> v = {0};
for (int i = 1; i < n; i++) {
f[i] = dep[LCA(i, i + 1)];
for (; v.size() > 1 && f[v.back()] > f[i]; v.pop_back()) {
int l = v[v.size() - 2] + 1, r = i;
krg[r - l + 1].push_back({l, r, f[v.back()]});
rrg[r].push_back({l, f[v.back()]});
}
v.push_back(i);
}
for (; v.size() > 1; v.pop_back()) {
int l = v[v.size() - 2] + 1, r = n;
krg[r - l + 1].push_back({l, r, f[v.back()]});
rrg[r].push_back({l, f[v.back()]});
}
for (int i = 1; i <= n; i++) {
krg[1].push_back({i, i, dep[i]});
rrg[i].push_back({i, dep[i]});
}
}
int ans[kN];
struct kQuery { int l, r, i; };
vector<kQuery> kq[kN];
struct rQuery { int l, k, i; };
vector<rQuery> rq[kN];
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n;
for (int i = 1, u, v; i < n; i++) {
cin >> u >> v;
e[u].push_back(v), e[v].push_back(u);
}
Init(1, 0);
for (int d = 1; d < 20; d++) {
for (int i = 1; i + (1 << d) - 1 <= n; i++)
st[d][i] = High(st[d - 1][i], st[d - 1][i + (1 << d - 1)]);
}
GetItv();
cin >> q;
for (int i = 1, l, r, k; i <= q; i++) {
cin >> l >> r >> k;
kq[k].push_back({l, r, i});
rq[r].push_back({l, k, i});
}
for (int len = n; len >= 1; len--) {
for (auto [l, r, w] : krg[len])
Update(r, w);
for (auto [l, r, i] : kq[len])
ans[i] = Max(l + len - 1, r - 1);
}
fill_n(t + 1, 4 * n, 0);
for (int r = n; r >= 1; r--) {
for (auto [l, w] : rrg[r])
Update(l, w);
for (auto [l, k, i] : rq[r])
ans[i] = max(ans[i], Max(1, r - k + 1));
}
for (int i = 1; i <= q; i++)
cout << ans[i] << '\n';
return 0;
}
Day24A. 矩阵 (4)
容斥数数题。考场上想到了可以排序,但是选择的填数顺序是从大到小填,性质很差,但是没有想过换顺序,无效思考良久后选择了弃掉。
考虑从小到大填,发现由于受到左上角前 \(a\) 行 \(b\) 列 的最大值限制 影响,每次必须填的位置是一个倒 L 形,也保持着一个 \(n\times m\) 矩形的子问题。此时就可以很轻松的考虑容斥:当这 \(a\) 行 \(b\) 列 有 \(i\) 行 \(j\) 列没能顶到最大值,那么明显容斥系数为 \((-1)^{i+j}\),方案数是 $${a\choose i} {b\choose j} k^{am+bn-ab} (\frac{k-1}{k})^{im+jn-ij}$$ 容易做到平方 log。考虑拆式子,枚举一个 \(i\),把与 \(j\) 无关的扔到外面去,或是合起来。令 \(d = \frac{k-1}k\),则有答案为:
里面很明显是一个二项式定理的形式,故可以简单快速幂计算。
其实如果能想到从小到大的优秀性质并进行有效思考的话,应该是可以快速做掉的。
把这题发给翔哥 花费 9 min 被秒了/xk
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 2e6 + 1, kP = 1e9 + 7;
inline int Pow(int a, LL b) {
int ret = 1;
for (; b > 0; b /= 2) {
if (b % 2 == 1)
ret = 1ll * ret * a % kP;
a = 1ll * a * a % kP;
}
return ret;
}
int N, a[kN], b[kN];
vector<int> V;
int fac[kN], ifc[kN], inv[kN];
inline int C(int n, int x) { return 1ll * fac[n] * ifc[x] % kP * ifc[n - x] % kP; }
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> N;
fac[0] = fac[1] = inv[1] = ifc[0] = ifc[1] = 1;
for (int i = 2; i <= N; i++) {
fac[i] = 1ll * fac[i - 1] * i % kP;
inv[i] = 1ll * (kP - kP / i) * inv[kP % i] % kP;
ifc[i] = 1ll * ifc[i - 1] * inv[i] % kP;
}
for (int i = 1; i <= N; i++)
cin >> a[i], V.push_back(++a[i]);
for (int i = 1; i <= N; i++)
cin >> b[i], V.push_back(++b[i]);
sort(a + 1, a + N + 1), sort(b + 1, b + N + 1);
sort(V.begin(), V.end());
V.erase(unique(V.begin(), V.end()), V.end());
int i = 0, j = 0, ans = 1;
for (int w : V) {
int p = 0, q = 0, n = N - i, m = N - j;
for (; a[i + 1] == w; i++, p++);
for (; b[j + 1] == w; j++, q++);
int sum = 0, x = (w - 1ll) * Pow(w, kP - 2) % kP;
for (int i = 0; i <= p; i++) {
int k = 1ll * Pow(kP - Pow(x, m), i) * C(p, i) % kP;
sum = (sum + 1ll * k * Pow(kP + 1 - Pow(x, n - i), q)) % kP;
}
ans = 1ll * ans * sum % kP * Pow(w, 1ll * p * m + 1ll * q * n - 1ll * p * q) % kP;
}
cout << ans << '\n';
return 0;
}
Day24B. 树 (3)
ok……其实这题进行的有效思考比较多,然后在 DP 状态只考虑子树内邻域的时候没意识到只需要对于 d-1, d, d+1 中的一个转移即可,意识到的时候已经改掉转移状态了,所以也没有意识到这个转移有着极强的优美性质...它帮助 \(f_{i,d}\) 的状态暗含了一个 \(i\) 向子树外的邻域大小也不小于 \(d\) 的性质。因此直接转移就可以了。
fzx 说这题跟 Day21D 的最后做法极其相似,但是这题进行了一些弱化。强化后应该是不保证 \(b\) 递增。由于没有进行 Day21 的补题,所以进行了加强版的做。由于 \(b\) 不递增,会发现一个很深刻的问题,就是其实我 \(f_{i,d}\) 转移出去之后实际的邻域大小其实已然超过了 \(d\)。但是此时我还是按照 \(d\) 的去算,递增的时候一定更劣所以没有影响,但是不递增就炸掉了。怎么办呢?
此时发现,\(i\) 的邻域刚好为 \(d\) 意味着存在一个子树使得其邻域大小为 \(d-1\)(如果不是整棵树全染掉了)。那么只需要在状态中加上一维 0/1,表示是否存在一个子树满足 \(d-1\),如果没有则要求父亲的邻域为 \(d-1\)。然后正常转移即可。会新增一些 corner,在 fzx 的教授下改对了/hsh
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 5e3 + 1;
int n, a[kN], b[kN];
vector<int> e[kN];
LL f[kN][kN][2], g[kN][2];
void DP(int x, int fa) {
fill(f[x][0], f[x][n], -1e18);
f[x][0][1] = 0;
for (int i = 1; i < n; i++)
f[x][i][i == 1] = b[i] - a[x];
for (auto v : e[x]) {
if (v == fa)
continue;
DP(v, x);
fill(g[0], g[n], -1e18);
for (int i = 0; i < n; i++)
for (bool p : {0, 1}) {
if (i + 1 < n)
g[i][p] = max(g[i][p], f[v][i + 1][0] + f[x][i][p]);
for (int j = max(i - 1, 0); j <= min(i + 1, n - 1); j++) {
bool o = (p || j == i - 1);
g[i][o] = max(g[i][o], f[x][i][p] + f[v][j][1]);
}
}
copy(g[0], g[n], f[x][0]);
}
}
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1; i <= n; i++)
cin >> b[i];
for (int i = 1, u, v; i < n; i++) {
cin >> u >> v;
e[u].push_back(v), e[v].push_back(u);
}
DP(1, 0);
LL ans = 1ll * n * b[n] - accumulate(a + 1, a + n + 1, 0ll);
for (int i = 0; i < n; i++)
ans = max(ans, f[1][i][1]);
cout << ans << '\n';
return 0;
}
Day24C. 树锯解构 (5)
比较美丽的一个题。首先需要发现,区间内所有子集的按位或 的按位异或 等价于 按位与。因为如果一个位上存在一个数是 \(0\),那么是否选它的按位或结果相同,会因为异或偶数次而这一位结果必然是 \(0\);否则,可以全部不选,恰有 \(1\) 种方案是 \(0\),由于总方案数也是偶数,所以这一位就会是 \(1\)。或许也可以用类似 minmax 容斥的方法思考,总之这个结论其实是不难发现的。
所以此时的问题变成了区间加(只保留前 \(M\) 位二进制),区间求按位与。查询可以按位考虑,看这一位是否全 \(1\),那么实际上是维护这一位的 0/1。而某一位上的 0/1 其实性质非常美好。可以维护当前位上的数,然后考虑第 \(i\) 位什么时候取 \(1\):一定是在 \(\mod 2^{i+1}\) 意义下的一个占一半的区间!修改相当于对区间进行平移,区间按位与只需要维护“成 1”区间 的交即可。
当然,由于模数卡到了 \(2^{32}\),而且这个区间是环上区间,求交自带不少 corner,赛场上写其实感觉压力难度会很大。听说封装 32 个线段树很慢,所以写了单个节点维护 32 个区间/hsh
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using uint = unsigned;
using PII = pair<int, int>;
constexpr int kN = 5e5 + 1;
int n, q, M;
uint m, a[kN];
inline uint Mod(uint x) { return m == 32 ? x : (x ^ (x >> m << m)); }
struct Itv {
uint l, r;
bool empty;
inline void operator-=(uint w) { l = Mod(l - w), r = Mod(r - w); }
};
inline Itv operator*(Itv x, Itv y) {
if (x.empty || y.empty)
return {0, 0, 1};
if (x.l <= x.r && y.l <= y.r) {
static uint L, R;
L = max(x.l, y.l), R = min(x.r, y.r);
return {L, R, L > R};
}
if (x.l > x.r && y.l > y.r)
return {max(x.l, y.l), min(x.r, y.r), 0};
(x.l > x.r) && (swap(x, y), 0);
if (x.r <= y.r || x.l >= y.l)
return x;
if (x.l <= y.r)
return {x.l, y.r, 0};
else if (y.l <= x.r)
return {y.l, x.r, 0};
return {0, 0, 1};
}
struct Node {
Itv ans[32];
uint tag;
} t[1 << 20], P;
inline void PushUp(int x) {
for (int d = 0; d < M; d++)
t[x].ans[d] = t[2 * x].ans[d] * t[2 * x + 1].ans[d];
}
inline void PushTag(int x, uint w) {
for (int d = 0; (m = d + 1) && d < M; d++)
t[x].ans[d] -= w;
t[x].tag += w;
}
inline void PushDown(int x) {
if (!t[x].tag)
return;
PushTag(2 * x, t[x].tag);
PushTag(2 * x + 1, t[x].tag), t[x].tag = 0;
}
void Build(int x, int l, int r) {
if (l == r) {
for (int d = 0; (m = d + 1) && d < M; d++)
t[x].ans[d] = P.ans[d], t[x].ans[d] -= a[l];
return;
}
int mid = (l + r) / 2;
Build(2 * x, l, mid);
Build(2 * x + 1, mid + 1, r);
PushUp(x);
}
void Update(int L, int R, uint w, int x = 1, int l = 0, int r = n - 1) {
if (R < l || r < L)
return;
else if (L <= l && r <= R)
return PushTag(x, w);
int mid = (l + r) / 2;
PushDown(x);
Update(L, R, w, 2 * x, l, mid);
Update(L, R, w, 2 * x + 1, mid + 1, r);
PushUp(x);
}
uint Query(int L, int R, int x = 1, int l = 0, int r = n - 1) {
if (R < l || r < L)
return (1ll << M) - 1;
else if (L <= l && r <= R) {
uint ret = 0;
for (int d = 0; d < M; d++) {
auto [tl, tr, e] = t[x].ans[d];
ret |= uint(!e && (tl > tr || tl == 0)) << d;
}
return ret;
}
int mid = (l + r) / 2;
PushDown(x);
return Query(L, R, 2 * x, l, mid) & Query(L, R, 2 * x + 1, mid + 1, r);
}
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n >> q >> M;
for (uint i = 0; i < n; i++)
cin >> a[i];
for (int d = 0; d < M; d++)
P.ans[d] = {1u << d, uint((1ll << d + 1) - 1), 0};
P.tag = 0, Build(1, 0, n - 1);
for (uint o, l, r, k; q--;) {
cin >> o >> l >> r;
if (o == 0)
cin >> k, Update(l, r - 1, k);
else
cout << Query(l, r - 1) << '\n';
}
return 0;
}
Day24D. 序列 (7)
结论维护数据结构细节题。令人难以评价。
中位数性质极差,经典做法是二分转成 01。从左往右考虑,首先 000 合掉纯优,然后想到其他的特判,但其实都没什么意义……一个很没道理的“注意到”结论是 01 可以直接扔掉。证明是直接分讨,首先 如果 01 一起操作则毫无影响,否则是单独去做 1。很明显如果下一个数是 0 则操作也毫无意义,如果后两个数是 11 则纯劣,此时只可能是 0110 的状态,做完操作变 01。但是靠边的 1 明显比其他位置上更加厉害,所以 01 可以直接扔掉。这样按顺序操作后发现如果剩下了两个靠边的 1 就结束了,只需要把某一边的 数全部合一起即可。
此时考虑怎样的 1 的位置可以让最后剩下两个 1。我想要 1 靠边,如果有那是最好,如果没有则需要有两个连续的 1,且其左侧的数是奇数个。因为如果左侧是偶数个,则消掉两个后还需要一个 1 在边上,所以就需要三个 1 连在一起,这样也会形成一个左侧奇数个数的对 1。但只有一个靠边不够,需要两个,所以一定需要两个 起始位置按顺序分别 偶、奇 的对 1。
维护是静态的,询问特别多,可以考虑猫树维护。但是一般的猫树写法要么在线空间 \(\mathcal{O}(n\log n)\),要么离线摁分治询问 时间 \(\mathcal{O}(q\log n)\)。我进行了一些思考,发现可以离线下来,然后仿照 QOJ9857 Interval Mex 的做法,把当前层的所有区间存进 vector 中,每次到下一层就分一下。这样可以让空间和时间的 \(q\) 上都不带 log。比较有意思。
但是我写的很丑!!!!!脑子的问题使我针对奇偶性给每个东西都开了 0/1,糖丸了。然后猫树的深度预测一直写错,所以贺了一个很丑的东西,有时间改一下。
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using uint = unsigned;
using PII = pair<int, int>;
constexpr int kN = 1e6 + 1;
int n, q;
uint seed, a[kN];
inline uint _rnd(uint& _SEED) {
_SEED ^= _SEED << 7;
_SEED ^= _SEED >> 15;
_SEED ^= _SEED << 9;
return _SEED;
}
inline void generate_interval(const int& _N, uint& _SEED, int& l, int& r) {
int len = _rnd(_SEED) % ((_N + 1) / 2) * 2;
l = _rnd(_SEED) % (_N - len) + 1;
r = l + len;
}
int id[kN];
struct Query { int l, r, i; };
vector<Query> vq[kN];
uint val[kN], ans;
uint suf[kN][2], pre[kN][2], L[kN][2], R[kN][2];
inline void Solve() {
vector<PII> cur = {{1, (1 << __lg(n - 1) + 1)}}, tmp;
int d = __lg(id[1]) - __lg(id[1] ^ id[n - 1]);
for (; !cur.empty(); d++) {
fill(suf[1], suf[n], 0), fill(pre[1], pre[n], 0);
fill(L[1], L[n], 0), fill(R[1], R[n], 0);
for (auto [l, r] : cur) {
if (l == r || l > n - 1)
continue;
int mid = (l + r) / 2;
tmp.emplace_back(l, mid);
tmp.emplace_back(mid + 1, r);
if (mid > n - 1)
continue;
suf[mid][1] = val[mid];
for (int i = mid - 1; i >= l; i--) {
suf[i][0] = suf[i + 1][1];
suf[i][1] = max(suf[i + 1][0], val[i]);
L[i][0] = max(L[i + 1][1], min(val[i + 1], suf[i + 1][0]));
L[i][1] = L[i + 1][0];
}
if (mid == n - 1)
continue;
pre[mid + 1][1] = val[mid + 1];
for (int i = mid + 2; i <= min(n - 1, r); i++) {
pre[i][0] = pre[i - 1][1];
pre[i][1] = max(pre[i - 1][0], val[i]);
R[i][0] = max(R[i - 1][1], min(val[i], pre[i][0]));
R[i][1] = R[i - 1][0];
}
}
cur.swap(tmp), tmp.clear();
for (auto [l, r, i] : vq[d]) {
bool o = (r - l + 1) & 1;
uint tl = max(a[l], suf[l][0]);
uint tr = max(!o ? a[r + 1] : 0, pre[r][o]);
uint ret = min(tl, tr);
ret = max({ret, L[l][0], R[r][!o]});
ret = max(ret, min(a[l], suf[l][1]));
!o && (ret = max(ret, min(a[r + 1], pre[r][!o])));
ans += ret * uint(i);
}
}
}
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n >> q;
for (int i = 1; i <= n; i++)
cin >> a[i], val[i - 1] = min(a[i - 1], a[i]);
cin >> seed;
for (int i = 1, l, r; i <= q; i++) {
generate_interval(n, seed, l, r);
if (l == r)
ans += a[l] * uint(i);
else
r--, vq[__lg(id[l]) - __lg(id[l] ^ id[r])].push_back({l, r, i});
}
Solve();
cout << ans << '\n';
return 0;
}
[CSP-S 2025 T3] 谐音替换 / replace (3)
2025.11.02 (upd on 11.03)
一开始看到是字符串,是我不那么擅长的算法,而且我没有秒掉,因此先保证能看到比赛中的每一道题,暂时跳过了。回来之后花费 15 min 想出 AC 自动机做法,但是我挺久没写这玩意儿不是很记得,考前也没有去复习,我并没有信心将其写对。应试技巧告诉我 CSP-S / NOIP 并不会考 > 7级 的算法,因此一定存在更低级算法的做法,已经会一个做法的我非常自信,但是到最后我也没有想出来,最后写了一个最坏复杂度 \(\mathcal{O}(nq+L)\) 的做法,与暴力无异,因为 \(|t1|\neq |t2|\) 一分难得,令人难过。赛后短暂复习了一下 AC 自动机,其实比较简单,比较遗憾吧。
ACAM
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <queue>
#include <iostream>
#include <map>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using i128 = __int128;
using PII = pair<int, int>;
constexpr int kL = 5e6 + 1, kN = 2e5 + 1;
constexpr LL kP = LL(1e18) + 9, kB = 1e5 + 3;
int n, q;
string s1, s2;
int L, R;
inline LL Diff(string &s1, string &s2) {
L = R = -1;
for (int i = 0; i < s1.size(); i++) {
if (s1[i] != s2[i])
L == -1 && (L = i), R = i;
}
i128 dif = 0;
for (int i = L; i <= R; i++)
dif = (dif * kB + s1[i] * 133 + s2[i]) % kP;
return dif;
}
int idx = -1;
map<LL, int> M;
struct Node {
int nxt[27], fail, cnt;
};
struct ACAM {
int tot = 1;
vector<Node> t = {{}, {}};
inline void Insert(string s) {
int x = 1;
for (char ch : s) {
int &nxt = t[x].nxt[ch - 'a'];
!nxt && (nxt = ++tot);
x = nxt;
if (x == t.size())
t.push_back({});
}
t[x].cnt++;
}
inline void GetFail() {
fill_n(t[0].nxt, 27, 1);
queue<int> q;
for (q.push(1); !q.empty(); q.pop()) {
int x = q.front(), fx = t[x].fail;
for (int ch = 0; ch < 27; ch++) {
int v = t[x].nxt[ch];
if (v != 0) {
t[v].fail = t[fx].nxt[ch];
t[v].cnt += t[t[v].fail].cnt, q.push(v);
} else
t[x].nxt[ch] = t[fx].nxt[ch];
}
}
}
inline LL Query(const string &s) const {
int x = 1;
LL ret = 0;
for (auto ch : s)
x = t[x].nxt[ch - 'a'], ret += t[x].cnt;
return ret;
}
};
vector<ACAM> AC;
inline LL Query(string &s1, string &s2) {
if (s1.size() != s2.size())
return 0;
LL w = Diff(s1, s2);
if (!M.count(w))
return 0;
return AC[M[w]].Query(s1.substr(0, L) + char('z' + 1) + s1.substr(L));
}
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n >> q;
for (int i = 1; i <= n; i++) {
cin >> s1 >> s2;
if (s1 == s2)
continue;
assert(s1 != s2 && s1.size() == s2.size());
LL w = Diff(s1, s2);
!M.count(w) && (AC.push_back({}), M[w] = ++idx);
AC[M[w]].Insert(s1.substr(0, L) + char('z' + 1) + s1.substr(L));
}
for (auto &T : AC)
T.GetFail();
for (; q--;) {
cin >> s1 >> s2;
cout << Query(s1, s2) << '\n';
}
return 0;
}
为了考场上那个傻逼,还是改一下 7 级做法。
此时发现要做的事情本质上是在中间定位,向两边判断能否对上。哈希很难做这种事情,哈希根本不擅长计数,除非它对于位置有强单调性,否则哈希在计数,信息的维护方面根本就是废物,因为完全做不到动态更新哈希值然后 check 几个对上之类的事情。不可能纯字符串哈希,考虑一些字符串结构:发现可以向两边分别建 Trie,然后对于一个替换规则,意味着 Trie 上的两个点,用询问串跑,如果这两个点均被经过,则存在该子串,该替换规则合法。比较弱智的想法是可以对 Trie 重链剖分,单点修改链查询。聪明一点的话,可以考虑经典转化:把 单点加链查询 改为 子树加单点查询。于是不难二维数点,做到 \(\mathcal{O}(VL + (n+q)\log L)\),维护用的是树状数组常数会小一些,但是这不在瓶颈。\(VL\) 可以用 umap 做到 \(L\),或者提前排序做到 \(L\log n\),但是都只是小优化,umap 甚至更慢。
写挂了,为了尊重那个傻逼,花大时间写了个暴力 gen checker 对拍,没用 ACAM 的代码。然后发现往左建 Trie 忘记 reverse 了。最后花费 2h 写出来跑的被 ACAM 做法三维严格偏序,绷不住一点。
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <map>
#include <numeric>
#include <vector>
#include <chrono>
#include <random>
using namespace std;
using LL = long long;
using i128 = __int128;
using PII = pair<int, int>;
constexpr int kN = 2e5 + 1, kL = 5e6 + 1;
constexpr LL kP = LL(1e18) + 9;
mt19937_64 Rand(chrono::steady_clock().now().time_since_epoch().count());
LL Base = Rand() % kP;
int n, q;
string s1, s2;
int L, R;
inline LL Diff(string &s1, string &s2) {
L = R = -1;
for (int i = 0; i < s1.size(); i++) {
if (s1[i] != s2[i])
L == -1 && (L = i), R = i;
}
i128 dif = 0;
for (int i = L; i <= R; i++)
dif = (dif * Base + s1[i] * 133 + s2[i]) % kP;
return dif;
}
struct Node {
int nxt[26], dfn, R;
};
LL ans[kN];
struct Queries { int dl, dr, id; };
struct Trie {
int cntl = 0, cntr = 0, dfc;
vector<Node> L = {{}}, R = {{}};
vector<PII> vu;
vector<Queries> vq;
inline void Insert(string sl, string sr) {
reverse(sl.begin(), sl.end());
int xl, xr;
for (int o : {0, 1}) {
int x = 0;
auto &t = (!o ? L : R);
for (char ch : (!o ? sl : sr)) {
int &nxt = t[x].nxt[ch - 'a'];
!nxt && (nxt = ++(!o ? cntl : cntr)), x = nxt;
(t.size() == x) && (t.push_back({}), 0);
}
(!o ? xl : xr) = x;
}
vu.emplace_back(xl, xr);
}
void Dfs(int x, int o) {
auto &t = (!o ? L : R);
t[x].dfn = ++dfc;
for (auto v : t[x].nxt)
v && (Dfs(v, o), 0);
t[x].R = dfc;
}
inline void Query(string sl, string sr, int id) {
int dl = 0, dr = 0;
reverse(sl.begin(), sl.end());
for (int o : {0, 1}) {
int x = 0;
auto &t = (!o ? L : R);
for (char ch : (!o ? sl : sr)) {
if (!t[x].nxt[ch - 'a'])
break;
x = t[x].nxt[ch - 'a'];
}
(!o ? dl : dr) = t[x].dfn;
}
vq.push_back({dl, dr, id});
}
};
int idx;
map<LL, int> M;
vector<Trie> TR;
struct Update { int l, r, w; };
vector<Update> upd[kL];
vector<PII> vq[kL];
int t[kL], V;
inline void Add(int x, int w) {
for (; x <= V; x += x & -x)
t[x] += w;
}
inline int Ask(int x) {
int ret = 0;
for (; x > 0; x -= x & -x)
ret += t[x];
return ret;
}
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n >> q;
for (int i = 1; i <= n; i++) {
cin >> s1 >> s2;
if (s1 == s2)
continue;
assert(s1.size() == s2.size());
LL w = Diff(s1, s2);
if (!M.count(w))
M[w] = idx++, TR.push_back({});
TR[M[w]].Insert(s1.substr(0, L), s1.substr(R + 1));
}
for (auto &t : TR) {
t.Dfs(0, 1);
assert(t.dfc == t.cntr + 1);
t.dfc = 0, t.Dfs(0, 0);
assert(t.dfc == t.cntl + 1);
}
for (int i = 1; i <= q; i++) {
cin >> s1 >> s2;
if (s1.size() != s2.size())
continue;
assert(s1 != s2);
LL w = Diff(s1, s2);
if (!M.count(w))
continue;
TR[M[w]].Query(s1.substr(0, L), s1.substr(R + 1), i);
}
for (auto &t : TR) {
if (t.vq.empty())
continue;
for (auto &P : t.vu) {
int xl = P.first, xr = P.second, L = t.R[xr].dfn, R = t.R[xr].R;
upd[t.L[xl].dfn].push_back({L, R, 1});
if (t.L[xl].R < t.dfc)
upd[t.L[xl].R + 1].push_back({L, R, -1});
}
for (auto &Q : t.vq)
vq[Q.dl].emplace_back(Q.dr, Q.id);
V = t.cntr + 1;
for (int i = 1; i <= t.dfc; i++) {
for (auto &U : upd[i]) {
Add(U.l, U.w);
U.r < V && (Add(U.r + 1, -U.w), 0);
}
for (auto &Q : vq[i])
ans[Q.second] = Ask(Q.first);
upd[i].clear(), vq[i].clear();
}
fill_n(::t, V + 1, 0);
}
for (int i = 1; i <= q; i++)
cout << ans[i] << '\n';
return 0;
}
[CSP-S 2025 T4] 员工招聘 / employ (5.5)
2025.11.03
考场上只想了二十多分钟,口胡了个 \(\mathcal{O}(2^{cnt_1}n)\) 的暴力枚举,看了下只能拿 36pts,然后发现全 1 按值域填数极难考虑,于是跳过了,也没有留时间回来打这个简单好写的暴力。
还是考虑这个暴力枚举。实际上我想要枚举的是一个,对 \(c_i\) 限制的一个序列,然后按顺序枚举,做组合数合起来,差分后本质上就是哪些位置被聘用,只有 \(s_i=1\) 的位置能选,因此可以做到 \(\mathcal{O}(2^{cnt_1})\) 的枚举。既然难以按值域填数,更难以枚举聘用位置,那么只能对于每个 \(1\) 的位置按 \(s\) 顺序考虑此处是否聘用。考虑 DP,若 \(s_i=0\),那么 \([0,n]\) 的数它都能填,放到最后直接阶乘考虑;若 \(s_i=1\),且不打算聘用,那么只能填 \([0, cur]\),其中 \(cur\) 表示当前有多少人被淘汰;若 \(s_i=1\),且打算聘用,那么只能填 \((cur, n]\)。
思考一下,发现 \([0,cur]\) 很简单,因为我知道 这里面有多少个数能用,随便挑一个就可以了;但 \((cur,n]\) 很难:如果我真的在中间随便选一个,我如何在 cur 增长后知道我能选的数变多了多少?无法知道。怎么办呢?考虑改容斥,既然 \([0,n]\) 和 \([0,cur]\) 都好考虑,那么就作个差就好了。考虑钦定有 \(k\) 个 \(s_i=1\) 被聘用 值域为 \([0,cur]\),就有 \(f_{i,j,k}\) 表示当前考虑到了第 \(i\) 个数,有 \(j\) 个人被淘汰了。此时可以随便转移,最后记得同样 \([0,n]\) 的数一起阶乘。
现在看来其实并不算难做,但是当时没有很好的思路,在发现 \(n\leq 500\) 后直接考虑设计状态,并没有想到很好的状态。这题细节考虑还是比发明 ACAM 难的,加上思考写代码补了 2h,在 T3 思路极强的情况下跳掉也无可厚非。
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 500 + 1, kP = 998244353;
inline void Add(int &x, LL w) { x = (x + w) % kP; }
string s;
int n, m, c[kN];
int fac[kN], sum[kN], f[kN][kN][kN];
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m;
cin >> s, s = '#' + s;
for (int i = 1; i <= n; i++)
cin >> c[i], sum[c[i]]++;
fac[0] = 1;
for (int i = 1; i <= n; i++) {
sum[i] += sum[i - 1];
fac[i] = 1ll * fac[i - 1] * i % kP;
}
f[0][0][0] = 1;
int cnt = 0;
for (int i = 1; i <= n; i++) {
for (int j = cnt; j <= i - 1; j++)
for (int k = 0; k <= i - 1 - j && j - cnt + k <= sum[j]; k++) {
if (s[i] == '1') {
int use = j - cnt + k;
if (use + 1 <= sum[j]) {
Add(f[i][j + 1][k], 1ll * (sum[j] - use) * f[i - 1][j][k]);
Add(f[i][j][k + 1], 1ll * (sum[j] - use) * f[i - 1][j][k]);
}
Add(f[i][j][k], f[i - 1][j][k]);
} else
Add(f[i][j + 1][k], f[i - 1][j][k]);
}
cnt += (s[i] == '0');
}
int ans = 0;
for (int i = cnt; i <= n - m; i++) {
int cur = 0, o = 1;
for (int j = 0; j <= n - i && i - cnt + j <= sum[i]; j++, o *= -1)
Add(cur, 1ll * o * fac[n - i - j + cnt] * f[n][i][j] % kP + kP);
Add(ans, cur);
}
cout << ans << '\n';
return 0;
}
B. CF2147F Queries Exchange (3.5)
2025.11.03
很简单的一道题。首先打 CF2151 的时候想到了这种双排列题,发现可以按 \(p_i\) 重新置换,这样可以把两个排列变成下标和一个排列 \(s_i\)。此时题目相当于,\(i\) 可以向 \([1,i-1]\cup \{x|s_x<s_i\}\) 的点连边,求每个点可达的点数之和。这样会形成一些强连通分量,让一些 \(i\) 小的点跟 \(i\) 大的能走到的点数相同。贪心地思考,由于用 \(s\) 或者用编号跳都是值越大能跳到的点越多,且有包含关系,因此要中转,一定是用编号跳到 \(s\) 最大的,用 \(s\) 跳到编号最大的。如此往复,一定会停在 \(j\geq i\) 满足 \((\max s[1,j])=j\)。因此答案就是这些位置呈现的阶梯。
这个东西的维护想了我挺久,其实可以直接线段树维护 \((\max s[1,j])-j\),交换相当于区间加减 \(1\)。于是考虑维护达到对应区间 \(\min\) 的最左最右点位,到全局时 \(\min=0\),即只有 \(\min=0\) 的区间答案会有贡献。就这个玩意儿想了我 半个多小时,体现出我对这种基础的线段树应用反而敏感度低,糊做法的时候还有想到单边递归,但是其实并不适合。
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 1e5 + 1;
int T, n, q;
PII P[kN];
int p[kN], s[kN], buc[kN], init[kN];
struct Node {
int mn, lp, rp, tag;
LL ans;
inline Node operator+(const Node &x) const {
Node ret = {min(mn, x.mn)};
bool lo = (mn == ret.mn), ro = (x.mn == ret.mn);
ret.lp = (lo ? lp : x.lp), ret.rp = (ro ? x.rp : rp);
ret.ans = lo * ans + ro * x.ans - lo * ro * (1ll * rp * x.lp);
ret.tag = 0;
return ret;
}
} t[4 * kN];
void Build(int x, int l, int r) {
if (l == r) {
t[x] = {l - init[l], l, l, 0, 1ll * l * l};
return;
}
int mid = (l + r) / 2;
Build(2 * x, l, mid);
Build(2 * x + 1, mid + 1, r);
t[x] = t[2 * x] + t[2 * x + 1];
}
inline void PushTag(int x, int w) { t[x].mn += w, t[x].tag += w; }
inline void PushDown(int x) {
PushTag(2 * x, t[x].tag);
PushTag(2 * x + 1, t[x].tag), t[x].tag = 0;
}
void Update(int L, int R, int w, int x = 1, int l = 1, int r = n) {
if (R < L || r < L || R < l)
return;
else if (L <= l && r <= R)
return PushTag(x, w);
PushDown(x);
int mid = (l + r) / 2;
Update(L, R, w, 2 * x, l, mid);
Update(L, R, w, 2 * x + 1, mid + 1, r);
t[x] = t[2 * x] + t[2 * x + 1];
}
int main() {
cin.tie(0)->sync_with_stdio(0);
for (cin >> T; T--;) {
cin >> n >> q;
for (int i = 1; i <= n; i++)
cin >> p[i];
for (int i = 1, x; i <= n; i++)
cin >> x, P[i] = {p[i], x};
sort(P + 1, P + n + 1);
for (int i = 1; i <= n; i++) {
s[i] = P[i].second, buc[s[i]]++;
init[i] = init[i - 1] + buc[i] + (s[i] < i);
}
fill_n(buc + 1, n, 0);
Build(1, 1, n);
for (int o, i, j; q--;) {
cin >> o >> i >> j;
int l = p[i], r = p[j];
l > r && (swap(l, r), 0);
Update(max(s[l], l), r - 1, 1);
Update(max(s[r], l), r - 1, -1);
swap(s[l], s[r]);
o == 1 && (swap(p[i], p[j]), 0);
cout << t[1].ans << '\n';
}
}
return 0;
}
NOIP Day1A. G75 兰海高速 (1.5)
2025.11.04
极简单题,但是我一开始并没有意识到这个边的方案是唯一确定的,小猜了点没意义的合法性判定结论。遇到边染色相关的题目应该尽快思考自叶子到根的考虑顺序。理应把做题时间优化到 10min 以内。
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
#include <set>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 5e5 + 1;
int T, n;
vector<int> e[kN];
multiset<int> s[kN];
bool ans;
vector<PII> tr[kN];
int Dfs(int x, int fa) {
for (auto v : e[x]) {
if (v == fa)
continue;
int col = Dfs(v, x);
if (col == -1 || s[x].find(col) == s[x].end())
return ans = 0, -1;
s[x].erase(s[x].find(col));
tr[col].emplace_back(x, v);
}
assert(s[x].size() <= 1);
return s[x].empty() ? -1 : (*s[x].begin());
}
int d[kN], cnt1, cnt2;
inline void Add(int col, int w) {
cnt1 -= (d[col] == 1), cnt2 -= (d[col] == 2);
d[col] += w;
cnt1 += (d[col] == 1), cnt2 += (d[col] == 2);
}
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
for (cin >> T; T--;) {
cin >> n, ans = 1;
for (int i = 1, u, v; i < n; i++) {
cin >> u >> v;
e[u].push_back(v), e[v].push_back(u);
}
for (int i = 1, deg; i <= n; i++) {
cin >> deg;
for (int j = 1, x; j <= deg; j++)
cin >> x, s[i].insert(x);
}
assert(Dfs(1, 0) == -1);
for (int i = 1; i <= n && ans; i++) {
if (tr[i].empty())
continue;
for (auto &P : tr[i])
Add(P.first, 1), Add(P.second, 1);
ans &= (cnt1 == 2 && cnt2 == tr[i].size() - 1);
for (auto &P : tr[i])
Add(P.first, -1), Add(P.second, -1);
}
cout << (ans ? "Yes" : "No") << '\n';
for (int i = 1; i <= n; i++)
e[i].clear(), s[i].clear(), tr[i].clear();
}
return 0;
}
NOIP Day1B. Phigros (4)
2025.11.04
非常简单的一道题。首先看完题就可以想到如果矩阵确定,那么答案就是 max(有 1 的行数,有 0 的列数)。因此可以糊出一个 \(\mathcal{O}(3^nm^2)\) 的按行枚举二进制,子集枚举小优化的暴力,甚至过不了 20。但是反思一下,发现一行如果不是全 \(0\) 就一定有 \(1\),意味着要么行全按要么列全按。我考场上分讨了一下,令一边全按,另一边不全按,然后算下方案数,行列都减一下算出都全按。不妨设当前行全按,列不全按,则直接考虑 DP,求出 \(f_i\) 即要按 \(i\) 列的方案数,转移是简单的。此时复杂度为 \(\mathcal{O}(n^2+m^2)\)。
再考虑优化,发现要统计的是一个 \(\sum\limits_{i=0}^{n-1} f_i\max(i,m)\) 这样的东西。发现做短的那一边的时候,\(f_i\) 上的系数总是 \(m\),可以写简洁一些。另外,做长的那一边时,\(f_i\) 上的系数有 \(i\) 和 \(m\) 两种,但是只有 \(i\leq m\) 的时候才有 \(m\) 作系数。于是我们此处 \(f_i\) 可以只记到 \(m\),与 CSP-S Day16C 连通块 的美丽 DP 相同,维护 \(\sum\limits_{i>m}f_i\) 和 \(\sum\limits_{i>m} if_i\),用方案数加到贡献和上,即可实现下标的偏移。简单考虑下边界即可。
考场上想的太慢了,因为一直都没深刻意识到 '?' 要全部填成 0/1 的意味,所以在想一些容斥贡献的事情。有效思考时间占总时间 40%,想到全行全列花了 2h 多。写平方暴力的时候写的转置忘记 0/1 互换了,以为是 DP 挂了才出了问题,调了快半个小时。期间发现的小 bug 改了,但是没把转置版的代码交上去,因此挂了 7 分还是多少,第一次被自己罚圈/hsh。最后还剩 10min 的时候想到正解全部,只需要做少量修改,但是快静校了所以回了家。还是精力不足,以后尽量回家不写东西了,争取在第二天之前睡觉/qiang。
代码写的很丑,因为考场上的分讨得很细,实际上是没有必要的。
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 1e6 + 1, kM = 1e7 + 1, kP = 1e9 + 7;
int n, m;
string s[kN], S[kN];
int p2[kM], f[kN], w;
int ans = 0, cnt = 0;
inline void Main() { // 列全
f[0] = 1;
for (int i = 1; i <= m; i++) {
int cnt = 0, flg = 0;
for (int j = 1; j <= m; j++) {
flg |= s[i][j] == '1';
cnt += s[i][j] == '?';
}
if (flg) {
for (int j = i - 1; j >= 0; j--)
f[j + 1] = 1ll * f[j] * p2[cnt] % kP, f[j] = 0;
continue;
}
for (int j = i - 1; j >= 0; j--)
f[j + 1] = (f[j + 1] + f[j] * (p2[cnt] - 1ll)) % kP;
}
int F = 0, C = 0;
for (int i = m + 1; i <= n; i++) {
int cnt = 0, flg = 0;
for (int j = 1; j <= m; j++) {
flg |= s[i][j] == '1';
cnt += s[i][j] == '?';
}
if (flg) {
F = (F + C + (m + 1ll) * f[m]) % kP * p2[cnt] % kP;
C = 1ll * (C + f[m]) * p2[cnt] % kP, f[m] = 0;
for (int j = m - 1; j >= 0; j--)
f[j + 1] = 1ll * f[j] * p2[cnt] % kP, f[j] = 0;
continue;
}
F = (F + (F + C + (m + 1ll) * f[m] % kP) * (p2[cnt] - 1)) % kP;
C = (C + (C + f[m]) * (p2[cnt] - 1ll)) % kP;
for (int j = m - 1; j >= 0; j--)
f[j + 1] = (f[j + 1] + f[j] * (p2[cnt] - 1ll)) % kP;
}
for (int i = 0; i <= m; i++) {
cnt = (cnt + f[i]) % kP;
ans = (ans + 1ll * f[i] * m) % kP;
}
cnt = (cnt + C) % kP;
ans = (ans + F) % kP;
fill_n(f, m + 2, 0);
}
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m;
for (int i = p2[0] = 1; i <= n * m; i++)
p2[i] = 2 * p2[i - 1] % kP;
for (int i = 1; i <= n; i++)
cin >> s[i], s[i] = '#' + s[i];
if (n < m) {
for (int j = 1; j <= m; j++) {
S[j] = '#';
for (int i = 1; i <= n; i++)
S[j] += (s[i][j] == '0' ? '1' : (s[i][j] == '1' ? '0' : '?'));
}
for (int i = 1; i <= m; i++)
s[i] = S[i];
swap(n, m);
}
Main();
int f = 1, que = 0, tag = 1;
for (int j = 1; j <= m; j++) { // 行全
int cnt = 0, flg = 0;
for (int i = 1; i <= n; i++) {
flg |= s[i][j] == '0';
cnt += s[i][j] == '?';
}
que += cnt;
f = 1ll * f * p2[cnt] % kP;
tag &= !flg;
}
f = (f - tag + kP) % kP;
cnt = (cnt + f) % kP;
ans = (ans + 1ll * f * n) % kP;
ans = (ans + 1ll * (p2[que] - cnt + kP) * n) % kP;
cout << ans << '\n';
return 0;
}
NOIP Day1C. Z 函数 (?)
2025.11.04
考场上看完题并没有开,实际上也不像是很可做的题,策略上没问题。但是我当时甚至没能看出来 \(f\) 相等 等价于 \(Z\) 函数相等,反应比较慢。
首先考虑这个计数,对 \(Z\) 函数做一些转化,把抽象的 LCP 限制转化成相等和不等关系,将这样的关系进行连边。相等边会形成一些连通块,形成一些等价类,把这样的等价类缩起来,此时剩下的边只有不等边。此时的图染色方案数就是最后的方案数。但是很明显并不能真的直接建图跑,边数太大了。反思做 Z 函数的过程,每次把拿当前与 \([1, r - l + 1]\) 相等的 \([l, r]\),可得 \([i, r] = [i - l + 1, r - l + 1]\),于是直接把 \(Z_{i-l+1}\) 拿过来当 \(Z_i\)。但是其实这样直接拿来的相等关系并不影响连通性,因为有 \(j\in [l,r], j\rightarrow j - l + 1\rightarrow j - i + 1\),所以只需要在暴力扩展的时候加边即可(边并不有向,只是推导过程)。此时的一个等价类,即一个连通块,理应形如一棵树。因为只要存在有一条联通的链,就不会再连边。
但是此时不同边并不具有好的性质,而任意图的染色方案数是极难做的。有一个很没道理的观察,就是对于最后的不等图,每一个连通块的编号设定为其最小位置的下标,按编号从小到大考虑。若当前考虑到的这个点和前面的一个点集 \(p\) 有连边,则这个 \(p\) 的点导出子图是一个完全图,即 \(p\) 的颜色两两不同。有了这个性质,就可以按编号从小到大考虑,然后依次染色,第 \(i\) 个点的染色方案数就是 \(k-deg_i\),其中 \(deg_i\) 只包括向前的边。
证明题解放了八个 lemma,但是其实在说的是五个事情(此时并不考虑简化连通关系,就是暴力连接的 LCP 边,边无向):
- 若 \(i< j < k\),且有 \(i\leftarrow j\leftarrow k\) 的等价边,则一定存在 \(i\leftarrow k\) 的等价边。证明方法就是,考虑到这是 LCP,因此 \(i\leftarrow j\) 这样的等价边可以往前推到 \(1\leftarrow j-i+1\) 和 \(j-i+1\leftarrow k-i+1\),边端点有 \(1\),因此也是一个 LCP 边,因此可以右移到 \(i\leftarrow k\)。同理,\(i\leftarrow k\leftarrow j\) 也有 \(i\leftarrow j\),但是另一种情况不可行。
- 由 1 有,一个连通块的最小值一定会和其他所有点有边。因为间接的联通一定可以连到最小点上。
- 与 1 类似,不等边也可以如此传递。因为不等边意味着前面正好就是等价边,同样具有传递的性质。
- 与 13 类似,不等边可以用等价边传递到一个等价类的更小的点上。因此,一个等价类的最小点一定包含了该等价类所有的不等边。
- 由 34 有,\(i\) 到比它小的点集 \(p\) 的不等边一定可以传递到前面,因此 \(p\) 的导出子图是完全图。
所以证完了。其实不是很懂怎么猜到的结论,但是 fzx 赛时都有想到,可能是我没开的缘故 (?)
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 1e6 + 1;
int n, mod, V, T;
int a[kN], z[kN];
vector<int> sm[kN], dif[kN];
inline void GetZ() {
z[1] = n;
for (int i = 2, l = 0, r = 0; i <= n; i++) {
z[i] = (i > r ? 0 : min(r - i + 1, z[i - l + 1]));
for (; i + z[i] <= n && a[z[i] + 1] == a[i + z[i]]; z[i]++)
sm[z[i] + 1].push_back(i + z[i]);
if (i + z[i] <= n)
dif[z[i] + 1].push_back(i + z[i]);
if (i + z[i] - 1 > r)
r = i + z[i] - 1, l = i;
}
}
int mn[kN], f[kN];
void Dfs(int x, int w) {
mn[x] = w;
for (auto v : sm[x])
Dfs(v, w);
}
inline void Solve() {
GetZ();
for (int i = 1; i <= n; i++)
!mn[i] && (Dfs(i, i), f[i] = V);
int ans = 1;
for (int i = 1; i <= n; i++) {
if (mn[i] != i)
continue;
ans = 1ll * ans * f[i] % mod;
for (auto v : dif[i])
f[v]--;
}
cout << ans << '\n';
for (int i = 1; i <= n; i++)
sm[i].clear(), dif[i].clear(), mn[i] = f[i] = 0;
}
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n >> mod >> V >> T;
for (int i = 1; i <= n; i++)
cin >> a[i];
Solve();
for (int p; T--;) {
cin >> p;
for (int x; p--;)
cin >> x, swap(a[x], a[x + 1]);
Solve();
}
return 0;
}
NOIP Day1D. Genshin Impact (7.5)
2025.11.05
赛时看完了题面,但是没怎么想。实际上 \(\mathcal{O}(q\sqrt{n}\log n)\) 是好想的,赛后跟 fzx 聊了 3min 即得出/hsh 但是并不能通过,送了 52 分,赛时没有写是比较亏的。不过问题来源于 T2 的过度用时,虽然 52' 换潜在的 100' 也不算太亏?
比较 大 的根号题。看不懂做法二,故写了做法一。
首先此时肯定不能直接用线段树维护,带 \(\log\) 的复杂度完全无法接受。不难发现此时修改和查询的复杂度均为 \(\log\),尝试根号平衡,于是硬套值域分块,此时的修改操作是做一些后缀加。对于整块直接打 tag,对于散块并不能暴力加,因为需要一个比 \(\log\) 更优的复杂度。整块直接维护 tag 的差分以及总和,散快需要发现,如果只有加法(不减),那么靠后的一个数如果比前面的数更大,这个偏序关系将永远不发生改变,且位置更靠后查询上也更优。于是尝试维护后缀 max 的点,每个块都类似一个单调栈的合并,但是单调栈不好弹中间,所以考虑用链表,再维护 \(cnt\) 贡献的差分,这样就可以 \(\mathcal{O}(1)\) 修改了...吗?如果修改的点并不是后缀 max 的点,还要找到最近的后缀 max 点,这个事情是比较需要并查集的,并没有更好的选择,只能吃下这个 \(\mathcal{O}(\alpha(n))\) 的复杂度代价。
所以我们想要尽量没有减法,考虑回滚莫队,只需要做左端点移动的撤销即可。回顾我们做法,发现只是散块不适合撤销操作。因此再把当前区间内所有的值都新建一个分界点,再在中间过长的位置加分界点,就可以保证全部减法都只有整块。这样就能做到 \(\mathcal{O}(1)\) 的修改了。根号查询是简单的,枚举一下即可。
这个做法有点卡常,第一次 12' 修改不小心根号了,忘记整块也不能暴力加;第二次 52' 被卡常,发现这个左端点可以撤销但是不一定要完全撤销掉,因为现在左端点是可以任意移动的,可以写一个类似不奇偶分组的普通莫队,于是就卡线通过了 /hsh (2800ms / 3s)
爽 +2
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <cmath>
#include <ctime>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 2e5 + 1, kB = 1e3 + 1;
int n, q, B;
int a[kN], b[kN];
LL c[kN], ans[kN];
struct Queries { int l, r, id; };
vector<Queries> vq[kB];
inline void Init() {
B = ceil(sqrt(n)) + 0.01;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1; i <= n; i++)
cin >> b[i];
for (int i = 1; i <= n; i++)
cin >> c[i];
for (int i = 1, l, r; i <= q; i++) {
cin >> l >> r;
vq[(l - 1) / B + 1].push_back({l, r, i});
}
}
LL cnt[kN];
vector<int> pos;
int bel[kN];
int pre[kN], nxt[kN], fa[kN];
int Find(int x) { return fa[x] == x ? x : (fa[x] = Find(fa[x])); }
inline void Erase(int x) {
pre[x] && (nxt[pre[x]] = nxt[x]);
nxt[x] && (pre[nxt[x]] = pre[x]);
}
LL sumtag;
LL f[kN], tag[kB], dif[kN];
inline void Build(int L, int R) {
pos = {1, n + 1};
for (int i = L; i <= R; i++)
pos.push_back(a[i]);
sort(pos.begin(), pos.end());
for (int i = 1, sz = pos.size(); i < sz; i++) {
int l = pos[i - 1], r = pos[i] - 1;
if (r - l + 1 >= 2 * B) {
int len = r - l + 1, bl = len / (len / B);
for (l += bl; l < r; l += bl)
pos.push_back(l);
}
}
sort(pos.begin(), pos.end());
pos.erase(unique(pos.begin(), pos.end()), pos.end());
assert(pos.size() < kB);
for (int i = 1; i <= n; i++)
cnt[i] = 0;
for (int b = 1; b < pos.size(); b++) {
for (int i = pos[b - 1]; i < pos[b]; i++) {
bel[i] = b;
nxt[i] = i + 1, pre[i] = i - 1;
fa[i] = i, dif[i] = 0;
}
pre[pos[b - 1]] = nxt[pos[b] - 1] = 0;
for (int i = pos[b] - 2, rp = pos[b] - 1; i >= pos[b - 1]; i--) {
if (-c[i] <= -c[rp])
fa[i] = rp, Erase(i);
else
rp = i;
}
tag[b] = 0;
}
sumtag = 0;
}
LL tmp;
inline void Add(int p, LL w) {
{
w *= b[p], p = a[p];
tmp = cnt[p] * cnt[p];
cnt[p] += w;
w = cnt[p] * cnt[p] - tmp;
}
int bp = bel[p];
if (p > pos[bel[p] - 1]) {
p = Find(p);
dif[p] += w, assert(w >= 0);
for (; pre[p] && -c[pre[p]] <= dif[p] - c[p];) {
dif[p] += dif[pre[p]];
fa[pre[p]] = p, Erase(pre[p]);
}
bp++;
}
if (bp < pos.size())
tag[bp] += w, sumtag += w;
}
inline int Ask() {
LL sum = sumtag;
int b = pos.size() - 1;
for (; b >= 1; b--) {
int fa = Find(pos[b - 1]);
if (dif[fa] - c[fa] + sum >= 0)
break;
sum -= tag[b];
}
if (!b)
return -1;
int p = Find(pos[b - 1]);
for (; nxt[p]; p = nxt[p]) {
sum += dif[p];
if (sum + dif[nxt[p]] - c[nxt[p]] < 0)
break;
}
return p;
}
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n >> q;
Init();
for (int b = 1; b <= B; b++) {
sort(vq[b].begin(), vq[b].end(), [&](Queries &x, Queries &y) { return x.r < y.r; });
int L = (b - 1) * B + 1, R = min(b * B, n);
Build(L, R);
int cur = R, pl = R + 1;
for (auto &P : vq[b]) {
int l = P.l, r = P.r;
if (r <= R) {
for (int i = l; i <= r; i++)
Add(i, 1);
ans[P.id] = Ask();
for (int i = l; i <= r; i++)
Add(i, -1);
} else {
for (; pl > l; Add(--pl, 1));
for (; pl < l; Add(pl++, -1));
for (; cur < r; Add(++cur, 1));
ans[P.id] = Ask();
}
}
}
for (int i = 1; i <= q; i++)
cout << ans[i] << '\n';
return 0;
}
NOIP Day2A. 日文看不懂 (1000000)
2025.11.06
妈的,考场上猜了个结论,说是区间最小的状态理应是,左右两边分别最靠外的 相邻两个颜色相同的 两个对,靠内的两个位置作为左右端点。然后拿这个去扩展,直到当前区间合法。然后我发现,这个区间的出现最多次数的颜色不应发生改变,因为一旦改变,在改变之前一定有一个更小的区间直接满足条件,所以我直接拿当前的 出现次数最多的颜色,算左边每个后缀的次数变化量,枚举右边然后往左边找。看上去有那么一点对,赛时瞎调了 3h 过了他妈大样例,确实心里也觉得真的测过不了,果不其然挂 91 分借暗广。赛后重构又调 2h,思路极为清晰,结果 sub1 都过不去,我按照这个结论写暴力(忘记还又搜索硬配上去的暴力),结果暴力都不对。然后发现我他妈忘了这个最大值可以不只一个,我不好评。虽然可能还是可以做的,但是我并不想浪费第三个 2h 了。
换个思路的话,因为可以任意重排,所以这个重排长度是有单调性的,然后前缀和一下随便做带 \(\log\)。呵呵
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 2e6 + 2;
int n, s[kN], L, R;
int sum[4][kN], buc[4];
inline bool Check(int len) {
for (int l = 1, r = len; r <= n; l++, r++) {
if (l > L || r < R)
continue;
int mx = 0;
for (int o : {1, 2, 3}) {
buc[o] = sum[o][r] - sum[o][l - 1];
mx = max(mx, buc[o]);
}
bool tag = 1;
for (int o : {1, 2, 3})
if (buc[o] == mx) {
int sb = (s[l - 1] == o) + (s[r + 1] == o);
tag &= (mx + sb <= (r - l + 1 + sb + 1) / 2);
}
if (tag)
return 1;
}
return 0;
}
int T, mp[256];
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
mp['C'] = 1, mp['W'] = 2, mp['P'] = 3;
char ch;
for (cin >> T; T--;) {
cin >> n, s[n + 1] = 0;
for (int i = 1; i <= n; i++) {
cin >> ch, s[i] = mp[ch];
for (int o : {1, 2, 3})
sum[o][i] = sum[o][i - 1];
sum[s[i]][i]++;
}
L = -1, R = -1;
for (int i = 1; i < n; i++) {
if (s[i] == s[i + 1])
L == -1 && (L = i + 1), R = i;
}
if (L == -1) {
cout << "0\n";
continue;
}
int l = 1, r = n, ans = -1;
for (int mid; l <= r;) {
mid = (l + r) / 2;
if (Check(mid))
r = mid - 1, ans = mid;
else
l = mid + 1;
}
cout << ans << '\n';
}
return 0;
}
NOIP Day2B 叽里咕噜 (3)
2025.11.06
赛时写 T1 的时候为了清醒一点,拿出了昨天准备的冰箱中的咖啡,喝完之后成功感冒,头晕至极,眩晕了半个小时。然后叽里咕噜十几二十分钟猜出了判定结论,花了几秒钟觉得自己是对的,然后想了想写了个平方的暴力验证正确性,发现是对的,心里踏实了一点。然后觉得可能是什么神秘分治,没时间了但是还有两题没看,就先看后两题题面了。打后两题暴力倒闭之后,没时间了,随机想题,猜这个 T2 其实可以暴力双指针删除,赛后走在路上想了下,发现是对的,亏完。
首先无论如何,这个抓奶龙过程过于诡异,肯定要转化判定过程的。考虑手玩一些特殊图,链肯定合法;菊花似乎也合法,可以 check 一个叶子 再 check 根,一直这样,那么奶龙一定是只能在某一个叶子不敢出来的。再玩毛毛虫,发现菊花的性质很好,因为它会阻止奶龙通过菊花的根走到其他的叶子上,所以可以在链上的每个点上都挂点叶子,按顺序做也是合法的。尝试证其他的图不可行,考虑到如果一个点挂了达到三个大小达到 2 的子树,那么当你往某一个子树里 check 的时候,奶龙可以跑出来到根部,然后再进入你 check 过的点,这样你这辈子也抓不到奶龙;反之,就可以设定一个顺序,这样至少在这个局部不会让奶龙用到。
因此现在可以看出,合法即为链上挂一堆叶子。不好处理,考虑把叶子全部剥掉,即删去所有 \(deg_i=1\) 的点,则剩下的 \(\max deg_i =2\),很明显是充分必要的。所以,可以按照这个定义,维护两个图,一个图是正常的图,第二个图是删去叶子,即 \(deg_i>1\) 的点形成的点导出子图。枚举一个左端点 \(l\),从 \(l\) 不断加点推右端点 \(r\),直到 \([l,r]\) 不合法。第一张图怎么搞都不可能有问题,第二张图对于每个 \(l\),每个点只会被枚举一次,复杂度是 \(\mathcal{O}(n^2 + nm)\)。性质不错,有分治前途/qiang
但是其实不需要分治,怀疑一下是不是可以暴力删,发现真的可以!第一张图肯定没问题,第二张图考虑到右端点扫过的 比 \(i\) 编号大的点跟 \(i\) 的连边,会永远存在于 \(deg_i\) 中,直到 \(i\) 被 \(l=i\) 删掉。此外,只有经过 \(deg_i=2\) 这个界才会删增,考虑到左端点推的过程,\(i\) 在加入后,与跟编号小于它的点的边数是单调递减的,因此对于 编号大于它的边数为 \(0/1/2\) 三种状态,分别会 删一次/删增一次/加一次。加上 \(l,r\) 扫,总共只会删增三次,因此总复杂度最劣是 \(\mathcal{O}(6m+n)\) 的。非常简单。
Code
#include <algorithm>
#include <cassert>
#include <numeric>
#include <iostream>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 1e6 + 1;
int n, m, q;
vector<int> e[kN];
int deg[kN], R[kN];
bool sta[kN];
int aft[kN], cnt[kN], mx;
inline void Add(int x, int l, int r) {
if (deg[x] < 2 || sta[x])
return;
sta[x] = 1;
for (auto v : e[x])
if (l <= v && v < r && sta[v]) {
cnt[aft[v]]--;
mx = max(mx, ++aft[v]), cnt[aft[v]]++;
++aft[x];
}
cnt[aft[x]]++;
mx = max(mx, aft[x]);
}
inline void Del(int x, int l, int r) {
if (!sta[x] || deg[x] >= 2)
return;
sta[x] = 0, cnt[aft[x]]--, aft[x] = 0;
for (auto v : e[x]) {
if (l <= v && v <= r && sta[v])
cnt[aft[v]]--, cnt[--aft[v]]++;
}
}
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m >> q;
for (int i = 1, u, v; i <= m; i++) {
cin >> u >> v;
e[u].push_back(v), e[v].push_back(u);
}
for (int l = 1, r = 1; l <= n; l++) {
for (r += (mx <= 2); r <= n && mx <= 2; r++) {
int x = r;
for (auto v : e[x]) {
if (l <= v && v <= r)
deg[x]++, deg[v]++, Add(v, l, r);
}
Add(x, l, r);
if (mx > 2)
break;
}
R[l] = r;
deg[l] = 0, Del(l, l, r);
for (auto v : e[l]) {
if (l < v && v <= r)
deg[v]--, Del(v, l + 1, r);
}
for (; mx > 0 && cnt[mx] <= 0; mx--);
}
for (int i = 1, l, r; i <= q; i++) {
cin >> l >> r;
cout << (r < R[l] ? "Yes" : "No") << '\n';
}
return 0;
}
NOIP Day2C 嘎拉瓜拉 (5)
考场上看完题就没怎么想了,后续在冲 D 的暴力。赛后思考的时候,思路距正解甚远,令人难过,其实套路都很典,理应能自己做出来的 /ng
受 CF1895E 的深刻影响,我一直试图对可击杀关系进行连边,比如 \(a_u\geq b_v\),就 \(v\) 到 \(u\) 连边,每次可以删除一个有出度的点,求最多删几个.... 然后想可能的选择方案的时候,或许是注意力过于分散,我始终没有尝试去观察这样的图的形态。其实我真的不太擅长分析它给定的东西/xk 看到这样的东西就会一定程度上让我的大脑宕机,以后记住遇到特殊给定图形一定要观察图的形态/xk
观察图的形态,或者最开始就贪心地思考的话,会发现所有有出度的点一定会连向 \(a\) 最大的点 \(mx\),即用 \(mx\) 打掉所有其他怪兽。那么此时其实只需要考虑 \(mx\) 本身能不能被干掉。首先可以用 \(mx\) 干不掉的怪兽来打 \(mx\),即求 \(Atk = \max\limits_{v\in S} a_v, S=\{x|b_x>a_{mx}\}\),若 \(Atk\geq b_{mx}\),那么 \(mx\) 可以被干掉。
否则,一定需要拿 \(b_x \leq a_{mx}\) 的点来尝试杀掉 \(mx\),在 \(mx\) 杀掉其他点之后再依次操作这些点,最后剩下的点的防御值更小,再看 \(Atk\) 够不够用。图上来看,我将会选择一条指向 \(mx\) 的链,因为分叉毫无意义,我的目的只有在保证能杀掉下一个点的情况下这一个点的防御值尽量小。看这条链的链头是否防御值不大于 \(Atk\) 即可判定。那怎么做呢?考虑到我接链的过程,我一定要下一个点的 \(b_v < b_u\) 且 \(a_v\geq b_u\),才能让链头防御值更小,如此贪心选。那么把这些数值抽象到数轴上,可以发现,其实一条边 \(u\rightarrow v\) 本质上是在数轴上覆盖了一个 \((a_v,b_v]\) 的段(因为是段覆盖而不是点覆盖,所以做了个开区间),而最后的需求本质是从 \(b_{mx}\) 覆盖到 \(Atk\),即覆盖 \((Atk,b_{mx}]\) 这一段。
没错,静态这么做已经做完了,套个线段树啥得没啥难度。那么带修怎么做呢?其实是一样的,线段覆盖改为区间加减求 \(\min\),于是现在要维护的是 \(a_{mx}, b_{mx}, Atk, |S|\)。大概维护一下防御值的值域后缀的 \(\max a\),求出 \(a_{mx}\) 再求 \(Atk\) 和 \(|S|\)。由于可以同防御值,因此需要用 multiset 存一下每个防御值对应的攻击值集合,方便删除;存在 \(b_{mx}> a_{mx}\) 的可能,因此 \(a_{mx}\) 可能被算进 \(Atk\) 里了,但是这种情况一定是删不掉 \(mx\) 的,答案就是 \(n-|S|\)。当然,\(\max a\) 也可以有多个,很明显取 \(b\) 最小的做 \(mx\) 更优。
思路很平滑,并不像官方题解里写得那么惊悚/qiang 这场循环左移一下可能对我更友好一些/hsh 这个 T1 真是硬实力克到我了/ll
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
#include <set>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 2e5 + 1, kV = 4 * kN;
int T, n, q;
int atk[kN], def[kN];
vector<int> V;
struct Compile { int x, atk, def; } que[kN];
namespace Chain {
struct Node {
int mn, add;
} t[4 * kV];
inline void PushTag(int x, int w) { t[x].mn += w, t[x].add += w; }
inline void PushDown(int x) {
PushTag(2 * x, t[x].add);
PushTag(2 * x + 1, t[x].add), t[x].add = 0;
}
void Add(int L, int R, int w, int x = 1, int l = 1, int r = V.size()) {
if (R < L || r < L || R < l)
return;
else if (L <= l && r <= R)
return PushTag(x, w);
PushDown(x);
int mid = (l + r) / 2;
Add(L, R, w, 2 * x, l, mid);
Add(L, R, w, 2 * x + 1, mid + 1, r);
t[x].mn = min(t[2 * x].mn, t[2 * x + 1].mn);
}
int Min(int L, int R, int x = 1, int l = 1, int r = V.size()) {
if (r < L || R < l)
return 1e9;
else if (L <= l && r <= R)
return t[x].mn;
PushDown(x);
int mid = (l + r) / 2;
return min(Min(L, R, 2 * x, l, mid), Min(L, R, 2 * x + 1, mid + 1, r));
}
}
struct Info {
int atk, def, cnt;
inline Info operator+(const Info &x) const {
Info ret = {max(atk, x.atk), int(1e9), cnt + x.cnt};
atk == ret.atk && (ret.def = min(ret.def, def));
x.atk == ret.atk && (ret.def = min(ret.def, x.def));
return ret;
}
} Z = {0, int(1e9), 0};
Info t[4 * kV];
void Update(int def, Info F, int x = 1, int l = 1, int r = V.size()) {
if (l == r)
return t[x] = F, void();
int mid = (l + r) / 2;
if (def <= mid)
Update(def, F, 2 * x, l, mid);
else
Update(def, F, 2 * x + 1, mid + 1, r);
t[x] = t[2 * x] + t[2 * x + 1];
}
Info Ask(int L, int R = V.size(), int x = 1, int l = 1, int r = V.size()) {
if (R < l || r < L)
return Z;
else if (L <= l && r <= R)
return t[x];
int mid = (l + r) / 2;
return Ask(L, R, 2 * x, l, mid) + Ask(L, R, 2 * x + 1, mid + 1, r);
}
struct Node {
int def, atk;
inline bool operator<(const Node &x) const { return def < x.def; }
};
multiset<int> val[kV];
inline int Get(int w) { return lower_bound(V.begin(), V.end(), w) - V.begin() + 1; }
inline int Solve() {
Info alv = Ask(t[1].atk + 1);
int cnt = n - alv.cnt;
if (t[1].def <= t[1].atk && alv.atk < t[1].def)
cnt -= (Chain::Min(alv.atk + 1, t[1].def) == 0);
return n - cnt;
}
inline void Insert(int x) {
int dx = def[x];
Chain::Add(dx + 1, atk[x], 1);
val[dx].insert(atk[x]);
Update(dx, {*val[dx].rbegin(), dx, int(val[dx].size())});
}
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
for (cin >> T; T--;) {
cin >> n >> q;
for (int i = 1; i <= n; i++) {
cin >> atk[i] >> def[i];
V.push_back(def[i]), V.push_back(atk[i]);
}
for (int i = 1; i <= q; i++) {
cin >> que[i].x >> que[i].atk >> que[i].def;
V.push_back(que[i].def), V.push_back(que[i].atk);
}
cerr << 1;
sort(V.begin(), V.end());
V.erase(unique(V.begin(), V.end()), V.end());
fill_n(Chain::t + 1, 4 * V.size(), Chain::Node{0, 0});
fill_n(t + 1, 4 * V.size(), Z);
cerr << 1;
for (int i = 1; i <= n; i++) {
def[i] = Get(def[i]), atk[i] = Get(atk[i]);
Insert(i);
}
cout << Solve() << '\n';
for (int i = 1; i <= q; i++) {
int x = que[i].x, b = def[x];
Chain::Add(b + 1, atk[x], -1);
val[b].erase(val[b].find(atk[x]));
Update(b, val[b].empty() ? Z : Info{*val[b].rbegin(), b, int(val[b].size())});
atk[x] = Get(que[i].atk), def[x] = Get(que[i].def);
Insert(x);
cout << Solve() << '\n';
}
for (int i = 1; i <= V.size(); i++)
val[i].clear();
V.clear();
}
return 0;
}
NOIP Day2D 地不利多 (9)
赛时没看清楚,没发现这个 答案 是带 \(p_1,p_n\) 的,以为还要删直径,成功浪费自己若干时间/hsh 赛后发现之后,开始糊做法,明显直接边权和两倍及其延申物是完全错误的了。但是看上去非常的典。联想到 Jiry Matchings,直接猜 子树 DP,然后把这个 \(p\) 的构造看成是把节点接成环形链表,且只能有一个大环。那么设 \(f_{i,j}\) 表示处理完点 \(i\) 子树内,剩下 \(j\) 条链,所能贡献的最小答案。直接猜它转移 类似 min+ 卷积,然后有凸性,sz 相关所以直接 启发式合并 / 重链优化 套闵和优化。猜完就回家吃饭了。
回来发现框架还真全搭对了。一开始有一个简化,就是把 \(\text{dis}(i,j)\) 拆为 \(dep_i+dep_j-2dep_{lca}\)。于是答案变成了,\(2\sum dep_i - 2\sum dep_{lca(p_i, p_{i+1})}\),相当于最大化环状链上相邻两点 \(dep\) 和。那么在 \(i\) 这个点合并的信息只有 \(dep_i\) 这一种贡献,非常优美。然后这个转移并不是直接的 min+ 卷积,还要考虑接多少链的事情。比如 \(f_{i,j}+f_{son_i,k}+t dep_i\Rightarrow f_{i,j+k-t}\),其中 \(t\) 表示接了多少对端点,自带 \(0\leq t\leq 2\min(j,k)\) 的限制。这又能怎么做呢?由于我们非常不希望出现一些可以变的东西在转移中,因此考虑去除 \(t\) 在转移中的贡献。发现可以令 \(F_{i,x}=f_{i,x}+xdep_i\),这样转移就只有 \(j,k\) 了。可是这个加等差数列似乎还是不好做?发现我已经猜了这个 \(f\) 是凸的,以后还要闵和,我提前做掉差分是可以接受的,所以这个变成了单独给 \(f_{son_i}\) 全局减边权操作。
此时相当于一个 \(j,k\) 的转移可以对一段做 chkmax。因此,全局的最大值会占其中非常大的一部分,剩下的偏移似乎只能覆盖到一些边边角角一个位置。考虑把最大值拿出来,设这个函数合并是 \(F\Leftarrow G+H\),那么设 \(\arg\max G = x, \arg\max H = y\),设 \(x\leq y\),则 \(F[y-x, y+x]=G[x]+H[y]\)。右侧,因为这个凸函数是上凸的,因此我们希望 \(G[i]+H[j]\) 的 \(i,j\) 都尽量靠近顶点,加上 \(\min(i,j)\) 对 \(t\) 的限制,因此我们希望 \(t=0\),因为 \(t=0\) 是一个转移能贡献到的最右的位置,其他位置全被其他更优的转移覆盖到了。因此 \(F\) 右侧直接就是 \(G,H\) 右侧闵和。相对的,左侧则要求 \(t=\min(i,j)\),因为这是能贡献到的最左位置,即 \(G[i]+H[j]=F[|i-j|]\)。是一个差卷积,对于绝对值的处理,由于 \(x\leq y\),可以发现 \(i\geq x\) 一定不劣。因为如果 \(i<x\),那么会让 \(j=i+d\),太左了,直接令 \(i=x,j=x+d\) 一定更优,因为两个都是直接增长的。\(j>y\) 同理,因此直接拿 \([x,y]\) 间的,\(G\) 右部和 \(H\) 左部做差卷积,reverse 然后闵和即可。
但是我们需要做启发式合并!我们希望我们合并的复杂度是 \(\min(|G|, |H|)\) 的。因此维护这些分割、翻转、闵和操作,似乎是只能用平衡树的。而且有这个限制,还有一些实现细节,实现起来非常麻烦。贺了挺多细节的,感觉对这道题的实现理解还很浅薄,如果有时间可能再来挑战吧。
Code
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <chrono>
#include <random>
#include <vector>
using namespace std;
using LL = long long;
using uint = unsigned;
using PII = pair<int, int>;
constexpr int kN = 2e5 + 1;
mt19937 Rand(chrono::steady_clock().now().time_since_epoch().count());
int T, n;
vector<PII> e[kN];
int dep[kN], sz[kN], hson[kN];
void Init(int x, int fa) {
sz[x] = 1;
for (PII &P : e[x]) {
int v = P.first, w = P.second;
if (v == fa)
continue;
dep[v] = dep[x] + w, Init(v, x);
sz[x] += sz[v];
if (sz[v] > sz[hson[x]])
hson[x] = v;
}
}
struct Node {
uint l, r, prio, sz;
int rev;
LL sum, val, add;
} t[20 * kN];
void PushUp(int x) {
t[x].sz = t[t[x].l].sz + t[t[x].r].sz + 1;
t[x].sum = t[t[x].l].sum + t[t[x].r].sum + t[x].val;
}
void PushRev(int x) {
t[x].val *= -1, t[x].sum *= -1, t[x].rev *= -1, t[x].add *= -1;
swap(t[x].l, t[x].r);
}
void PushAdd(int x, LL w) { t[x].val += w, t[x].add += w, t[x].sum += 1ll * t[x].sz * w; }
void PushDown(int x) {
if (t[x].rev == -1)
PushRev(t[x].l), PushRev(t[x].r);
PushAdd(t[x].l, t[x].add), PushAdd(t[x].r, t[x].add);
t[x].rev = 1, t[x].add = 0;
}
PII SplitVal(int x, LL w) {
if (!x)
return {0, 0};
PushDown(x);
if (t[x].val < w) {
auto P = SplitVal(t[x].l, w);
t[x].l = P.second, PushUp(x);
return {P.first, x};
}
auto P = SplitVal(t[x].r, w);
t[x].r = P.first, PushUp(x);
return {x, P.second};
}
PII SplitSiz(int x, int k) {
if (!x)
return {0, 0};
PushDown(x);
if (k <= t[t[x].l].sz) {
auto P = SplitSiz(t[x].l, k);
t[x].l = P.second, PushUp(x);
return {P.first, x};
}
auto P = SplitSiz(t[x].r, k - t[t[x].l].sz - 1);
t[x].r = P.first, PushUp(x);
return {x, P.second};
}
int Merge(int x, int y) {
if (!x || !y)
return x + y;
PushDown(x), PushDown(y);
if (t[x].prio < t[y].prio) {
t[x].r = Merge(t[x].r, y), PushUp(x);
return x;
}
t[y].l = Merge(x, t[y].l), PushUp(y);
return y;
}
uint tmp[kN], rt[kN], tot;
int New(LL w) {
t[++tot] = {0, 0, Rand(), 1, 1, w, w, 0};
return tot;
}
int Generate(int n) {
if (n == 0)
return 0;
else if (n == 1)
return New(0);
int x = New(0), sz = (n - 1) / 2;
t[x].l = Generate(sz), t[x].r = Generate(n - 1 - sz);
return PushUp(x), x;
}
void Insert(int &x, int y) {
if (!y)
return;
PushDown(y);
Insert(x, t[y].l), Insert(x, t[y].r);
auto P = SplitVal(x, t[y].val);
x = Merge(P.first, Merge(New(t[y].val), P.second));
}
LL f[kN];
int Add(int x, int y) {
auto PX = SplitVal(rt[x], 0), PY = SplitVal(rt[y], 0);
int xL = PX.first, xR = PX.second, yL = PY.first, yR = PY.second;
LL mx = f[x] + t[xL].sum + f[y] + t[yL].sum;
int a = t[xL].sz, b = t[xR].sz, c = t[yL].sz, d = t[yR].sz;
if (a <= c) {
int cut = min(b, c - a);
auto _P = SplitSiz(xR, cut);
PushRev(_P.first);
Insert(yL, _P.first), PushRev(_P.first);
int L = SplitSiz(yL, c + cut - (c - a)).second;
xR = Merge(_P.first, _P.second);
Insert(xR, yR);
f[x] = mx - t[L].sum;
return Merge(L, Merge(Generate(2 * a), xR));
} else {
int cut = min(d, a - c);
auto _P = SplitSiz(yR, cut);
PushRev(_P.first);
Insert(xL, _P.first), PushRev(_P.first);
int L = SplitSiz(xL, a + cut - (a - c)).second;
yR = Merge(_P.first, _P.second);
Insert(xR, yR);
f[x] = mx - t[L].sum;
return Merge(L, Merge(Generate(2 * c), xR));
}
}
void Remove(int x) {
auto P = SplitSiz(rt[x], 1);
int p = P.first;
t[p].sum = t[p].val = f[x] + t[p].val + LL(1e18);
f[x] = -1e18, rt[x] = Merge(p, P.second);
}
void Solve(int x, int fa) {
f[x] = -1e18, rt[x] = New(dep[x] + LL(1e18));
if (!hson[x])
return;
Solve(hson[x], x);
Remove(hson[x]), Remove(x);
PushAdd(rt[hson[x]], dep[x] - dep[hson[x]]);
rt[x] = Add(hson[x], x), f[x] = f[hson[x]];
for (auto &P : e[x]) {
int v = P.first, w = P.second;
if (v == hson[x] || v == fa)
continue;
Solve(v, x);
Remove(v), Remove(x);
PushAdd(rt[v], -w);
rt[x] = Add(x, v);
}
}
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
for (cin >> T; T--;) {
cin >> n;
for (int i = 1, u, v, w; i < n; i++) {
cin >> u >> v >> w;
e[u].emplace_back(v, w), e[v].emplace_back(u, w);
}
Init(1, 0);
LL sum = 0;
for (int i = 1; i <= n; i++)
sum += dep[i];
Solve(1, 0);
auto P = SplitSiz(rt[1], 1);
int p = P.first;
t[p].sum = t[p].val = f[1] + t[p].val;
cout << 2 * (sum - f[1]) << '\n' << '\n';
for (int i = 1; i <= n; i++)
e[i].clear(), sz[i] = hson[i] = dep[i] = 0;
}
return 0;
}
NOIP Day3A. 宇宙 (3.5)
2025.11.8
首先考虑二分,枚举当前的 \(k\),二分 check 一个 \(x\) 是否合法。那么问题实际上就是,每次选 \(n-k\) 个数 -1,选 \(x\) 轮后能不能全正。考虑每个数被选的次数序列 \(a_i\),相当于有 \(\sum a=x(n-k), \forall 0\leq a_i\leq \min(v_i-1,x)\)。考虑这个 \(a_i\) 的取值范围,很明显如果有 \(v_i>x\),那么一定取 \(a_i=x\),因为这样可以减少其他数的负担。那么对于 \(v_i\leq x\) 的 \(a_i\) 是一定小于 \(x\) 的,不必再做约束,也就是只需要比较 \(\sum v_i-1\) 是否能达到 \(x(cnt-k)\) 即可。对 \(a\) 排序,预处理前缀和,二分 \(x\),check 二分算出不受约束的 \(a_i\) 个数,不难做到 \(\mathcal{O}(n\log n\log V)\)。
但是这样有俩 log,而且反思一下发现,这个 \(x\) 并不重要。我们实际上最关心的是我有哪些 \(a\) 是当前可以不受约束考虑的。而且这个事情关于 \(k\) 有单调性的,可以直接推双指针暴力算,复杂度 \(\mathcal{O}(n)\)。
其实这题并不太简单,只能说这场运气还可以,半个小时解决掉了。
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 1e6 + 1;
int C, n, a[kN];
LL sum[kN];
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> C >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
sort(a + 1, a + n + 1);
for (int i = 1; i <= n; i++)
sum[i] = sum[i - 1] + a[i];
for (int k = 1, l = 2; k < n; k++) {
l = max(l, k + 1);
for (; l < n && (sum[l] - l) / (l - k) >= a[l + 1]; l++);
cout << (sum[l] - l) / (l - k) << ' ';
}
return 0;
}
NOIP Day3B. 跳跃 (4)
2025.11.8
好难啊...我操,为什么我想这个 t=0 想了一个多小时啊,一直在想暴力能跳白点就跳,但是一直觉得因为可以跳最远的黑点所以可能有问题,一直在拿 t=1 的限制否 t=0 的做法。非常令人难过啊!!!!
t=0 的做法大概是,首先询问的点顺序是可以交换的,所以不妨设左跳到右;每次向右跳,如果右边有白点就跳到那里,没有就跳到最右边的黑点 \(i+k\)。如果想要最小化总的操作次数的话,每次肯定跳到最右的白点,但是会有一个问题,就是可能我跳到那个白点并不会因此少跳一个黑点,即我一定会跳入这个“黑河”然后以一个相同步数出来到终点,这样我跳到白点那一步是浪费的。但是反思一下我的第二种跳跃方式,跳“黑河”直到最后跳到一个白点,其实就是在说 从 \(i\) 白点 跳到 \(j\) 白点 花费 \(\lceil \frac{j-i}{k} \rceil\) 步,出问题的点就在不论是前面的哪个白点,跳过这里的黑点代价是恒定的。因此可以直接给长度为 \(len\) 的“黑河”砍成 \(len\mod k\) 的,记一下这里必然付出的代价,前缀和一下即可。
怎么算每次跳到最远的白点,\(i\) 到 \(j\) 的代价?连边倍增跳即可。复杂度 \(\mathcal{O}(n\log n)\)。
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 5e5 + 1, kB = 20;
char ch;
int C, T, n, q, k;
int s[kN], ts[kN], m;
int sum[kN], pos[kN], nxt[kN][kB];
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> C >> n >> q >> k >> T;
for (int i = 1, cnt = 0; i <= n; i++) {
cin >> ch;
if (ch == '1') {
int lst = sum[m];
s[++m] = 1, ts[i] = m;
pos[m] = pos[m - 1] + cnt % k + 1;
sum[m] = sum[m - 1] + cnt / k, cnt = 0;
} else
cnt++;
}
for (int i = 1; i <= m; i++) {
nxt[i][0] = lower_bound(pos, pos + m + 1, pos[i] - k) - pos;
for (int d = 1; d < 20; d++)
nxt[i][d] = nxt[nxt[i][d - 1]][d - 1];
}
for (int a, b; q--;) {
cin >> a >> b, a = ts[a], b = ts[b];
a > b && (swap(a, b), 0);
int wht = 1e9, blk = sum[b] - sum[a], x = b, cur = 0;
for (int d = 19; d >= 0; d--) {
if (nxt[x][d] > a)
cur += 1 << d, x = nxt[x][d];
else
wht = min(wht, cur + (1 << d));
}
if (T == 0)
cout << blk << '\n';
else
cout << blk << ' ' << wht + blk << '\n';
}
return 0;
}
NOIP Day3C. 圆环 (4.5)
2025.11.8
非常简单的题目。首先可以秒掉一个平方的暴力,设 \(f_{i,j}\) 表示当前考虑第 \(i\) 个时刻,一只手在 \(a_i\) 上,另一只手在 \(j\) 上的最小体力消耗。转移讨论一下,如果这个时刻只有一个键,有两种转移:
意思大概就是讨论动哪只手。如果有两个键,比较笨的方法是直接枚举上一个 \(j\) 然后讨论两种动的方案;更聪明的方式是,把状态改成 \(i\) 表示考虑第 \(i\) 个限制,那么在考虑这个时刻第二个键的时候,直接禁用转移 \(1\),只留下 \(f_{i,a_{i-1}}\)。接下来有两种优化思路:
- 直接线段树优化。
现在相当于要做全局加 \(\text{dis}(a_i,a_{i-1})\),以及前缀后缀查询 \(f_i-i, f_i+i\) 最小值,最后全局查询 \(f_i\) 最小值。暴力线段树维护即可,不需要动一点脑子。代码难度中低。
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 3e5 + 1;
constexpr LL inf = 1e11;
int C, n, m;
int Dis(int x, int y) {
x > y && (swap(x, y), 0);
return min(y - x, x + n - y);
}
struct Node { // pi = \min(w_i + i), mi = \min(w_i - i);
LL mn, pi, mi, add;
bool clr;
} t[4 * kN];
void PushUp(int x) {
t[x].mn = min(t[2 * x].mn, t[2 * x + 1].mn);
t[x].pi = min(t[2 * x].pi, t[2 * x + 1].pi);
t[x].mi = min(t[2 * x].mi, t[2 * x + 1].mi);
}
void Build(int x, int l, int r) {
t[x] = ((l == 1) ? Node{0, 1, -1} : Node{inf, inf, inf});
if (l == r)
return;
int mid = (l + r) / 2;
Build(2 * x, l, mid);
Build(2 * x + 1, mid + 1, r);
}
void PushAdd(int x, LL w) {
t[x].mn += w, t[x].pi += w, t[x].mi += w, t[x].add += w;
}
void Clear(int x) {
t[x].mn = t[x].pi = t[x].mi = inf;
t[x].add = 0, t[x].clr = 1;
}
void PushDown(int x) {
for (int v : {2 * x, 2 * x + 1}) {
t[x].clr && (Clear(v), 0);
PushAdd(v, t[x].add);
}
t[x].clr = t[x].add = 0;
}
void Chkmax(int p, LL w, int x = 1, int l = 1, int r = n) {
if (l == r) {
w < t[x].mn && (t[x] = {w, w + l, w - l}, 0);
return;
}
int mid = (l + r) / 2;
PushDown(x);
if (p <= mid)
Chkmax(p, w, 2 * x, l, mid);
else
Chkmax(p, w, 2 * x + 1, mid + 1, r);
PushUp(x);
}
struct Info { LL pl, mn; };
Info Min(Info a, Info b) { return {min(a.pl, b.pl), min(a.mn, b.mn)}; }
Info Query(int L, int R, int x = 1, int l = 1, int r = n) {
if (R < l || r < L)
return {inf, inf};
else if (L <= l && r <= R)
return {t[x].pi, t[x].mi};
int mid = (l + r) / 2;
PushDown(x);
return Min(Query(L, R, 2 * x, l, mid), Query(L, R, 2 * x + 1, mid + 1, r));
}
PII P[kN];
int a[kN];
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> C >> n >> m;
for (int i = 1, x, y; i <= m; i++)
cin >> x >> y, P[i] = {x, y};
sort(P + 1, P + m + 1);
for (int i = 1; i <= m; i++)
a[i] = P[i].second;
Build(1, 1, n), a[0] = n;
for (int i = 1; i <= m; i++) {
Info L = Query(1, a[i]), R = Query(a[i], n);
LL val = min({a[i] + L.mn, n - a[i] + L.pl, R.pl - a[i], n + a[i] + R.mn});
if (P[i].first != P[i - 1].first)
PushAdd(1, Dis(a[i], a[i - 1]));
else
Clear(1);
Chkmax(a[i - 1], val);
}
cout << t[1].mn << '\n';
return 0;
}
- cyx 大神场切做法,用 set 维护绝对值函数。
cyx 大神在场上发现,这个第一个转移是极为简单考虑的。第二个转移,相当于每个位置 \(i\) 都有一个环型序列上的绝对值函数(距离函数) \(f_{j}+\text{dis}(j,x)\)。显然我只需要存最底端的点坐标,而且其实我并不需要维护所有的函数,我只需要维护那些没有被其他函数包含的即可。即最初是 \((1,0)\) 一个点,第一种转移打 tag,第二种转移找 \(j\) set 环形序列前驱后继提前算得最优答案,然后重新插入的时候,再找环形前驱后继删无用函数即可。复杂度没有变化,但是我重现后,做到了三维度最优解,比较断层的。代码难度体感其实不比 法1 低/hsh
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
#include <set>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
using PIL = pair<int, LL>;
using Iter = set<PIL>::iterator;
#define x first
#define y second
constexpr int kM = 3e5 + 1;
PII p[kM];
int C, n, m, a[kM];
int Dis(int x, int y) { return min(abs(x - y), n - abs(x - y)); }
set<PIL> f;
LL tag = 0;
Iter Check(Iter it) { return it == f.end() ? f.begin() : it; }
Iter Prev(Iter it) { return prev(it == f.begin() ? f.end() : it); }
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> C >> n >> m;
for (int i = 1; i <= m; i++)
cin >> p[i].first >> p[i].second;
sort(p + 1, p + m + 1);
for (int i = 1; i <= m; i++)
a[i] = p[i].second;
a[0] = 1, f.emplace(n, 0);
for (int i = 1; i <= m; i++) {
assert(!f.empty());
LL val = 1e11;
auto it = f.lower_bound({a[i], 0});
for (auto It : {Check(it), Prev(it)})
val = min(val, It->y + tag + Dis(a[i], It->x));
if (p[i].first != p[i - 1].first)
tag += Dis(a[i], a[i - 1]);
else
f.clear();
val -= tag;
it = f.emplace(a[i - 1], val).first;
for (int o : {0, 1}) {
for (; ;) {
auto It = o ? Check(next(it)) : Prev(it);
if (It == it || It->y < val + Dis(a[i - 1], It->x))
break;
f.erase(It);
}
}
}
LL ans = 1e11;
for (auto it : f)
ans = min(ans, it.y + tag);
cout << ans << '\n';
return 0;
}

浙公网安备 33010602011771号