Loading

ABC434G Keyboard

本题是一道单侧递归类线段树问题(这类题比较经典的代表是楼房重建).

本题的信息结构适合用线段树维护.为了方便讲解,做一些说明:

  • 下述节点指的是线段树的节点,用小写字母表示,如节点 \(u\)
  • 一个节点维护的内容为信息,信息包含多个
  • 对于节点 \(u\),信息 \(u\) 指的是节点 \(u\) 维护的信息,下用 \(I(u)\) 表示.
  • 现在就是要考虑节点存放的信息有哪些值.

首先,任何一段区间操作序列,都应被处理为一段退格一段数字的组合.

B1BB23B45 可以化为 BB245

合并一个节点的左右儿子时,只需考虑让右子区间的前导 B 与左子区间的一段数字后缀发生消去.

BB123BB456 合并后的结果应为 BB1456

也有可能右子区间的 B 过多,会把左边的数字全消去,

BB123BBBB456 合并后的结果是 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;
}
posted @ 2025-11-30 15:37  dbxxx  阅读(3)  评论(0)    收藏  举报