膜拜 - 题解
标签与难度
标签: 数据结构, 树状数组, 离线算法, 动态规划, 计数问题, 字符串, 子序列
难度: 2500
题目大意喵~
你好呀,指挥官!这道题是关于一个只包含 'o', 'r', 'z' 三种字符的神奇字符串~ 咱们的任务是处理两种操作,喵:
- 插入操作: 在字符串的第 个字符后面,插入一个新的 'o'。字符串会因此变长哦。
- 查询操作: 给定一个区间 ,我们需要数一数,在这个子串里,有多少个子序列是 "orz"。
子序列的意思是,我们可以从字符串里按顺序挑出三个字符,只要它们能组成 "orz" 就行,中间可以隔着其他字符,也可以不隔。比如 "oorz" 里面,第一个 'o'、'r'、'z' 算一个,第二个 'o'、'r'、'z' 也算一个,总共就有两个 "orz" 子序列啦!
题目还会用一个特殊的随机函数来生成初始字符串和操作,不过我们不用太担心这个,只要按部就班地处理生成的输入就好啦,喵~
解题思路分析
这道题看起来好复杂呀,又要在字符串里插入,又要查询区间... 如果每次插入都真的移动字符,那肯定会慢得像猫咪追不上激光笔,喵呜~ (。•́︿•̀。)
所以,我们需要更聪明的办法!这种动态修改序列并且进行复杂查询的问题,通常可以往 离线处理 和 数据结构 的方向思考,呐。
Step 1: 从静态问题开始思考
咱们先不想插入操作,假如字符串是固定不变的,要怎么查询区间 内 "orz" 的数量呢?
一个 "orz" 子序列是由一个 'o'、一个 'r' 和一个 'z' 组成的,它们的下标分别是 ,并且满足 。
一个朴素的想法是,我们遍历区间 中所有的 'r' (假设在位置 ),然后对于每个 'r',我们乘以它左边 'o' 的数量和右边 'z' 的数量。把所有 'r' 的贡献加起来就是答案啦!
这个方法对于单次查询还行,但每次都这么算,还是太慢了。我们可以用一些数学魔法(也就是容斥原理)来优化它!
Step 2: 容斥原理的华丽变身!
我们换个角度看问题。总的 "orz" 数量,可以按第一个字符 'o' 的位置来分类统计。对于区间 里的每一个 'o' (假设在位置 ),它能构成的 "orz" 子序列数量,就是它后面(在 范围内)"rz" 子序列的数量。
所以,N_orz(l, r) (区间 内 "orz" 的数量) 可以表示为:
其中 是指示函数(条件成立为1,否则为0), 表示区间 内 "rz" 子序列的数量。
这个公式看起来还是不好直接算。但是,我们可以利用一个非常巧妙的容斥思想。我们先计算一个更宽松的值,然后减掉多余的部分。
考虑一个 'o' 在位置 。它后面(直到字符串末尾 )的所有 "rz" 子序列数量是 。我们先把这些都加起来:
这个总和里包含了我们不想要的子序列,它们的 'r' 或 'z' 跑到了查询区间 的外面,也就是在 里。我们需要把这些“出界”的情况减掉!
出界的情况有两种:
- 'o' 在 ,'r' 在 ,但 'z' 在 。 这种情况的数量是 。
- 'o' 在 ,但 'r' 和 'z' 都在 。 这种情况的数量是 。
所以,最终的公式就是:
这个公式看起来有点吓人,但别怕!我们一步步拆解它,会发现它非常适合用数据结构来维护,喵~
Step 3: 离线大法好!
现在回到最头疼的插入操作。既然在线处理(来一个操作处理一个)这么麻烦,不如我们先把所有操作都“看”一遍,提前规划好一切,这就是 离线处理 的思想,呐。
预知未来: 我们一共有 个初始字符和 次操作。我们可以先统计出一共有多少次插入操作,记为
num_insertions。那么,字符串的最终长度就是m = n + num_insertions。我们直接创建一个这么大的数组,为最终的字符串形态预留好空间。时光倒流: 接下来是关键一步!我们从第 个操作开始,倒着 往回处理。我们用一个树状数组(或者其他能动态查询排名的数据结构)来管理这 个空位。
- 初始时,这 个位置都是空的。
- 遇到一个查询操作
query(l, r),我们就在当前的空位中找到第 个和第 个空位,记录下它们的物理地址。 - 遇到一个插入操作
insert(x),我们就在当前的空位中找到第 个空位,记录下它的物理地址,然后把这个位置标记为“已占用”(比如在树状数组中减1)。
尘埃落定: 当所有操作都倒着处理完后,树状数组里剩下的空位,就是留给初始 个字符的。我们再把初始字符串的字符一个个填到这些剩下的空位里。
做完这一切,我们就得到了一个长度为 的、包含了所有插入字符的最终形态的字符串!并且,所有的查询和插入操作,都被我们转换成了在这个静态大数组上的物理位置。
Step 4: 数据结构闪亮登场!
现在问题变成了:在一个基本静态的字符串上,处理两种操作:
- 在某个位置
p“激活”一个 'o' (对应原来的插入操作)。 - 查询区间
[l, r]的 "orz" 数量。
注意到,'r' 和 'z' 的位置在离线处理后就完全固定了!只有 'o' 是动态出现(被激活)的。这简直是为我们的公式量身定做的!
我们可以这样维护:
- 静态部分 (
r和z): 用普通的前缀和数组处理。r_prefix_sum[i]:s[1...i]中 'r' 的数量。z_prefix_sum[i]:s[1...i]中 'z' 的数量。rz_helper_prefix_sum[i]: 一个辅助数组,用于快速计算 。
- 动态部分 (
o): 用三个树状数组(BIT)来维护!o_count_bit: 维护区间 'o' 的数量,即 。or_count_bit: 维护 ,用于计算 。orz_count_bit: 维护 ,也就是我们公式里的第一项。
当一个 'o' 在位置 p 被激活时,我们更新这三个树状数组。当查询时,我们就用树状数组和前缀和数组,把上面推导的公式算出来。
这样,通过离线处理把动态问题静态化,再结合树状数组和容斥公式,我们就能高效地解决这个问题啦!是不是很奇妙呢,喵~
代码实现
这是我根据上面的思路,精心重构的一份代码~ 每个部分都有详细的注释,希望能帮助你理解,喵!
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <cmath>
using namespace std;
// 使用 long long 防止计数时溢出
using ll = long long;
const int MAX_LEN = 4000005; // 初始长度 + 查询次数
// --- 树状数组模板 ---
// 功能:单点更新,区间查询
template<typename T>
struct FenwickTree {
int size;
vector<T> tree;
FenwickTree(int n) : size(n), tree(n + 1, 0) {}
void add(int index, T value) {
for (; index <= size; index += index & -index) {
tree[index] += value;
}
}
T query(int index) {
T sum = 0;
for (; index > 0; index -= index & -index) {
sum += tree[index];
}
return sum;
}
T query(int left, int right) {
if (left > right) return 0;
return query(right) - query(left - 1);
}
// 找到第 k 个 1 对应的位置(用于离线处理)
int find_kth(int k) {
int pos = 0;
int current_sum = 0;
// 从高位到低位尝试
for (int bit = log2(size); bit >= 0; --bit) {
int step = 1 << bit;
if (pos + step <= size && current_sum + tree[pos + step] < k) {
pos += step;
current_sum += tree[pos];
}
}
return pos + 1;
}
};
// --- 全局变量 ---
int n_initial, q_queries;
int final_len;
char final_s[MAX_LEN];
// 用于离线处理的结构体
struct Query {
int type;
int l, r;
};
// 静态前缀和数组
ll r_prefix_sum[MAX_LEN];
ll z_prefix_sum[MAX_LEN];
ll rz_helper_prefix_sum[MAX_LEN]; // 辅助计算 N_rz
// --- 辅助函数 ---
// 计算 N_rz(l, r)
ll count_rz(int l, int r) {
if (l > r) return 0;
ll total_r_before_l = r_prefix_sum[l - 1];
ll z_in_range = z_prefix_sum[r] - z_prefix_sum[l - 1];
ll main_part = rz_helper_prefix_sum[r] - rz_helper_prefix_sum[l - 1];
return main_part - total_r_before_l * z_in_range;
}
// --- 输入生成器 (题目提供) ---
unsigned int SA, SB, SC;
unsigned int rnd() {
SA ^= SA << 16;
SA ^= SA >> 5;
SA ^= SA << 1;
unsigned int t = SA;
SA = SB;
SB = SC;
SC ^= t ^ SA;
return SC;
}
char initial_s_gen[2000005];
Query queries_gen[2000005];
void generate_input() {
scanf("%d%d", &n_initial, &q_queries);
final_len = n_initial;
scanf("%u%u%u", &SA, &SB, &SC);
for (int i = 1; i <= n_initial; i++) {
int si = rnd() % 3;
if (si == 0) initial_s_gen[i] = 'o';
if (si == 1) initial_s_gen[i] = 'r';
if (si == 2) initial_s_gen[i] = 'z';
}
for (int i = 1; i <= q_queries; ++i) {
queries_gen[i].type = rnd() % 2 + 1;
if (queries_gen[i].type == 1) {
queries_gen[i].l = rnd() % n_initial + 1;
final_len++; // 插入操作使总长度增加
} else {
int l = rnd() % n_initial + 1;
int r = rnd() % n_initial + 1;
if (l > r) std::swap(l, r);
queries_gen[i].l = l;
queries_gen[i].r = r;
}
}
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
generate_input();
// --- Step 1: 离线处理,确定最终物理位置 ---
FenwickTree<int> pos_tracker_bit(final_len);
for (int i = 1; i <= final_len; ++i) {
pos_tracker_bit.add(i, 1);
}
// 从后往前遍历操作,确定物理位置
for (int i = q_queries; i >= 1; --i) {
if (queries_gen[i].type == 1) { // 插入 'o'
// 插入到第 l 个字符后,即占据第 l+1 个可用位置
int physical_pos = pos_tracker_bit.find_kth(queries_gen[i].l + 1);
queries_gen[i].l = physical_pos;
pos_tracker_bit.add(physical_pos, -1);
} else { // 查询
int physical_l = pos_tracker_bit.find_kth(queries_gen[i].l);
int physical_r = pos_tracker_bit.find_kth(queries_gen[i].r);
queries_gen[i].l = physical_l;
queries_gen[i].r = physical_r;
}
}
// 将初始字符串放入剩余的空位
for (int i = 1; i <= n_initial; ++i) {
int physical_pos = pos_tracker_bit.find_kth(i);
final_s[physical_pos] = initial_s_gen[i];
}
// --- Step 2: 预计算静态部分的 'r' 和 'z' ---
for (int i = 1; i <= final_len; ++i) {
r_prefix_sum[i] = r_prefix_sum[i - 1];
z_prefix_sum[i] = z_prefix_sum[i - 1];
rz_helper_prefix_sum[i] = rz_helper_prefix_sum[i - 1];
if (final_s[i] == 'r') {
r_prefix_sum[i]++;
} else if (final_s[i] == 'z') {
z_prefix_sum[i]++;
rz_helper_prefix_sum[i] += r_prefix_sum[i]; // 注意这里是 r_prefix_sum[i]
}
}
// --- Step 3: 初始化动态部分的树状数组 ---
FenwickTree<ll> o_count_bit(final_len);
FenwickTree<ll> or_count_bit(final_len);
FenwickTree<ll> orz_count_bit(final_len);
for (int i = 1; i <= final_len; ++i) {
if (final_s[i] == 'o') {
o_count_bit.add(i, 1);
// N_r(i+1, m)
ll r_after = r_prefix_sum[final_len] - r_prefix_sum[i];
or_count_bit.add(i, r_after);
// N_rz(i, m)
orz_count_bit.add(i, count_rz(i, final_len));
}
}
// --- Step 4: 正向处理操作,计算答案 ---
ll total_xor_sum = 0;
for (int i = 1; i <= q_queries; ++i) {
if (queries_gen[i].type == 1) { // 插入 'o',即“激活”
int p = queries_gen[i].l;
o_count_bit.add(p, 1);
ll r_after = r_prefix_sum[final_len] - r_prefix_sum[p];
or_count_bit.add(p, r_after);
orz_count_bit.add(p, count_rz(p, final_len));
total_xor_sum ^= (ll)i;
} else { // 查询
int l = queries_gen[i].l;
int r = queries_gen[i].r;
// 应用我们的容斥公式
ll o_in_range = o_count_bit.query(l, r);
ll r_after_r = r_prefix_sum[final_len] - r_prefix_sum[r];
ll z_after_r = z_prefix_sum[final_len] - z_prefix_sum[r];
// 计算 N_or(l, r)
ll or_term = or_count_bit.query(l, r);
ll or_in_range = or_term - o_in_range * r_after_r;
// 计算 N_rz(r+1, m)
ll rz_after_r = count_rz(r + 1, final_len);
// 计算 N_orz(l, r)
ll orz_term = orz_count_bit.query(l, r);
ll ans = orz_term - or_in_range * z_after_r - o_in_range * rz_after_r;
total_xor_sum ^= (ans + (ll)i);
}
}
printf("%lld\n", total_xor_sum);
return 0;
}复杂度分析
时间复杂度:
- 离线处理: 共有 个操作,每次操作需要在大小为 的树状数组上做一次
find_kth,其复杂度为 。所以这部分是 。 - 预计算: 静态前缀和的计算是 。
- 正向处理: 共有 个操作,每次操作(插入或查询)都涉及到常数次树状数组的更新或查询,单次复杂度为 。所以这部分是 。
- 因为 的规模是 ,所以总时间复杂度是 ,喵~
- 离线处理: 共有 个操作,每次操作需要在大小为 的树状数组上做一次
空间复杂度:
- 我们需要存储最终的字符串、所有查询、前缀和数组以及树状数组,它们的大小都和最终的字符串长度 成正比。
知识点总结
这道题是多种算法思想的精彩结合,能学到很多东西哦!
- 离线处理 (Offline Processing): 当在线处理(即时响应)非常困难时,可以考虑将所有输入读入后,进行预处理,再统一计算答案。对于涉及动态增删元素的问题,这是一种非常强大的思想。
- 坐标映射/动态排名: 通过树状数组的
find_kth操作,我们将逻辑上的第k个位置映射到物理数组的实际下标。这是处理序列动态变化的一个常用技巧。 - 树状数组 (Fenwick Tree): 它是我们手中强大的工具,能够以 的代价实现单点更新和前缀和查询,非常适合维护动态变化的计数值。
- 容斥原理 (Inclusion-Exclusion Principle): 这是组合计数的灵魂!通过“先多算,再减掉”的策略,我们可以将一个复杂的、带很多限制条件的计数问题,分解成若干个限制更少的、更容易计算的子问题。
- 前缀和思想: 对于静态数组的区间查询,前缀和是最基础也是最高效的工具。在这里,它完美地处理了固定的 'r' 和 'z' 的信息。
希望这篇题解能帮到你!解出难题的感觉,就像猫咪找到了最舒服的纸箱一样满足,喵~ 继续加油哦!(^• ω •^)