ABC434G Keyboard
本题是一道单侧递归类线段树问题(这类题比较经典的代表是楼房重建).
本题的信息结构适合用线段树维护.为了方便讲解,做一些说明:
- 下述节点指的是线段树的节点,用小写字母表示,如节点 \(u\).
- 一个节点维护的内容为信息,信息包含多个值.
- 对于节点 \(u\),信息 \(u\) 指的是节点 \(u\) 维护的信息,下用 \(I(u)\) 表示.
- 现在就是要考虑节点存放的信息有哪些值.
首先,任何一段区间操作序列,都应被处理为一段退格加一段数字的组合.
如
B1BB23B45可以化为BB245.
合并一个节点的左右儿子时,只需考虑让右子区间的前导 B 与左子区间的一段数字后缀发生消去.
如
BB123和BB456合并后的结果应为BB1456.
也有可能右子区间的 B 过多,会把左边的数字全消去,
如
BB123和BBBB456合并后的结果是BBB456.
问题:对于一个节点,右子区间有 \(x\) 个 B,我们怎么迅速获取左子区间删掉 \(x\) 个数位后缀后的信息?我们不可以让每个区间节点保存完整的数位信息,否则单个节点保存的信息量就达到了 \(\Theta(n)\) 量级,维护的时间复杂度不可承受;如果我们只存下该区间内数字取模后的信息,又不能从之获知这段数字的后缀信息——取模把它破坏了.
因此这个问题是本题的一大难点,也是本题在赛场上杀伤力相当高的原因之一(仅一人 AC).重新表述此问题:对于任意整数 \(x\),设 \(I(u)\) 后接 \(x\) 个 B 形成的状态为信息 \(f(u, x)\),我们想快速地得到 \(f(u, x)\).
需要明确一点:我们是在合并当前节点 \(p\) 的左右儿子 \(l_p\),\(r_p\) 的信息时,考虑计算的 \(f(l_p, x)\).根据线段树的性质,\(l_p\),\(r_p\) 子树内所有节点的信息已经被计算好,也自然包含 \(I(l_p)\).这一点的明确非常重要,因为我们是用已知的信息得到未知的信息,必须清楚地理解可利用的已知的,被计算好信息是谁;未知的,要计算的信息是谁.因此,当我们计算 \(f(u, x)\) 时,认为 \(I(u)\) 已被计算好.
考虑单侧递归解决这个问题.考虑对 \(I(u)\) 维护下面的值:
B的数量 \(b\).- 数位的数量 \(c\).
- 数字取模后的结果 \(v\).
- 设 \(u\) 的左子节点为 \(l\),右子节点为 \(r\),称信息 \(f(l, b_r)\) 的值 \(v\) 为信息 \(u\) 的值 \(lv\).其含义可以视作 \(v_l\) 被右子节点的
B作用后的结果.
注:只有节点 \(u\) 保存的信息 \(I(u)\) 上,\(lv\) 才有意义.对于计算出的 \(f(u, x)\),其 \(lv\) 值无意义,代码中随便填写即可.
那么可以有如下计算思路:
当 \(x = 0\) 时,直接返回节点 \(u\) 的信息即可.
当 \(x \le c_r\) 时,这 \(x\) 个 B 只会影响右儿子.故只需递归右儿子,计算右儿子后接 \(x\) 个 B 的信息,即 \(f(r, x)\).再将 \(lv_u\) 与 \(f(r, x)\) 拼接即可.
当 \(x > c_r\),但 \(x - c_r + b_r \le c_l\) 时,这 \(x\) 个 B 会把对应右儿子的那一部分右区间数位删空,所以相当于 \(x - c_r + b_r\) 个 B 作用于 \(l\),因此只需递归左儿子,返回 \(f(l, x - c_r + b_r)\) 即可.
若 \(x > c_r\) 且 \(x - c_r + b_r > c_l\),说明 \(x\) 个 B 把整个区间删空了,最终只剩下 \(x + b_r + b_l - c_r - c_l\) 个 B.
注意到上面的计算思路中,由于每次我们只会对单侧进行递归,因此递归量级不会超过这个区间节点在线段树上的高度,也即,我们在 \(\Theta(\log n)\) 的时间复杂度下解决了计算任意 \(f(u, x)\) 的问题.
梳理一下目前的进度.我们是在考虑区间合并.对节点 \(u\) 的左右节点 \(l\),\(r\) 合并时,需要用到一个 \(f(l, b_r)\) 的操作.这个操作被 \(\Theta(\log n)\) 地解决了,那么只需要将 \(f(l, b_r)\) 与 \(I(r)\) 直接拼接即可.
单点修改是 \(\Theta(\log n)\) 次区间合并,一次区间合并复杂度 \(\Theta(\log n)\),故单次单点修改的复杂度为 \(\Theta(\log^2 n)\).
到这里区间查询也不是什么难事了.区间查询只是 \(\Theta(\log n)\) 个节点的信息合并而已.但是需要注意的是,刚才的区间合并必须要求合并的左方信息是一个节点上的信息,而不能是多个节点区间合并而来的信息,因为我们计算 \(f(l, b_r)\) 时,这个 \(l\) 必须是线段树上存在的节点.
因此,如果我们想完全地用刚才的函数 f 维护,就应当区分拆分出的这些节点从右到左地依次合并,而不是传统的分别合并左右,再将左右合并.复杂度依然 \(\Theta(\log^2 n)\).
#include <bits/stdc++.h>
#define int long long
int read() {
int x = 0; bool f = true; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = false;
for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + ch - '0';
return f ? x : (~(x - 1));
}
char rech() {
char ch = getchar();
for (; !isgraph(ch); ch = getchar());
return ch;
}
const int N = (int)8e6 + 5;
const int mod = 998244353;
int pw(int a, int p = mod - 2) {
int ans = 1;
for (; p; p >>= 1, (a *= a) %= mod)
if (p & 1)
(ans *= a) %= mod;
return ans;
}
int bs[N];
#define ls(p) p << 1
#define rs(p) p << 1 | 1
struct info {
int lb, v, cnt;
// leading b; value (modulo mod); digit count
int lv;
// left value with right lb
};
struct node {
int l, r;
info v;
} t[N << 2];
#define v(p) t[p].v
info f(int p, int x) {
if (x == 0) return v(p); //case 1
if (t[p].l == t[p].r) return (info){v(p).lb + x - v(p).cnt, 0, 0, 0}; //case leaf
if (x <= v(rs(p)).cnt) { // case 2
info rgt = f(rs(p), x);
return (info){
v(p).lb,
(v(p).lv * bs[rgt.cnt] + rgt.v) % mod,
v(p).cnt - x,
0 // 对于任意 f(u, x) 的计算,信息中的 lv 值无意义,随便填写即可
};
}
x += v(rs(p)).lb - v(rs(p)).cnt;
if (x <= v(ls(p)).cnt) return f(ls(p), x); //case 3
return (info) { //case 4
x + v(ls(p)).lb - v(ls(p)).cnt,
0, 0, 0
};
}
info con(int p, info rgt) {
info lef = f(p, rgt.lb);
return (info) {
lef.lb,
(lef.v * bs[rgt.cnt] % mod + rgt.v) % mod,
lef.cnt + rgt.cnt,
lef.v
};
}
void up(int p) {
t[p].v = con(ls(p), t[rs(p)].v);
}
void build(int p, int l, int r) {
t[p].l = l; t[p].r = r;
if (l == r) {
char ch = rech();
if (ch == 'B') v(p) = (info){1, 0, 0, 0};
else v(p) = (info){0, ch ^ '0', 1, 0};
return ;
}
int mid = (l + r) >> 1;
build(ls(p), l, mid);
build(rs(p), mid + 1, r);
up(p);
}
void update(int p, int x, char ch) {
int l = t[p].l, r = t[p].r;
if (l == r) {
if (ch == 'B') v(p) = (info){1, 0, 0, 0};
else v(p) = (info){0, ch ^ '0', 1, 0};
return ;
}
int mid = (l + r) >> 1;
if (x <= mid) update(ls(p), x, ch);
else update(rs(p), x, ch);
up(p);
}
std :: vector <int> ps;
void get(int p, int L, int R) {
int l = t[p].l, r = t[p].r;
if (l == L && R == r) return ps.push_back(p);
int mid = (l + r) >> 1;
if (R <= mid) return get(ls(p), L, R);
else if (L > mid) return get(rs(p), L, R);
else {
get(rs(p), mid + 1, R);
get(ls(p), L, mid);
}
}
info query(int l, int r) {
ps.clear();
get(1, l, r);
info ans = (info){0, 0, 0, 0};
for (int p : ps) ans = con(p, ans);
return ans;
}
signed main() {
int n = read(), q = read();
bs[0] = 1;
for (int i = 1; i <= n; ++i) bs[i] = (bs[i - 1] * 10) % mod;
build(1, 1, n);
while (q--) {
int op = read();
if (op == 1) {
int x = read(); char v = rech();
update(1, x, v);
} else {
int l = read(), r = read();
info ans = query(l, r);
if (ans.cnt == 0) puts("-1");
else printf("%lld\n", ans.v);
}
}
return 0;
}

浙公网安备 33010602011771号