目录
题目解析
这是一道非常有趣的“反套路”题目。虽然题目背景里提到了线段树、分块、平衡树等各种高级数据结构,但通过分析数据范围和操作特性,你会发现这道题其实考察的是差分数组、前缀和以及暴力模拟的结合,同时非常考验I/O优化(读写速度)。
以下是针对洛谷 P3948 的详细解析和 AC 代码。
1. 题目核心分析
题目分为两个阶段:
阶段一:在线修改与少量查询
- 操作:
A L R X:区间加法。Q L R:区间查询满足的元素个数。
- 数据特性:
- 数组长度
。 - 修改次数
较多(高达 )。 - 关键点:查询操作
非常少(不超过 1000 次)。
- 数组长度
阶段二:离线大规模查询
- 操作:
- 不再有修改,只有大量的区间查询。
- 查询次数
Final高达次。
- 关键点:数据量极大,必须要求单次查询
,且必须使用快读快写。
2. 算法设计
针对阶段一:差分数组 (Difference Array)
由于修改多、查询少,且涉及区间加法,差分数组是最佳选择。
- 定义
diff[i] = a[i] - a[i-1]。 - 修改
A L R X:只需操作: diff[L] += X,diff[R+1] -= X。 - 查询
Q L R:由于查询次数极少(),我们可以暴力还原数组。从 遍历到 ,一边累加 diff得到当前的,一边在 范围内统计满足条件的个数。 - 复杂度:
,在 C++ 1秒的时限内是可以接受的。
- 复杂度:
针对阶段二:前缀和 (Prefix Sum)
当所有修改结束后,数组
- 我们可以遍历一次数组,计算出每个位置
是否满足条件(满足记为1,不满足记为0)。 - 构造一个答案的前缀和数组
ans_sum,其中ans_sum[i]表示前个数中有多少个满足条件。 - 对于每个
Final询问,直接输出 ans_sum[R] - ans_sum[L-1]。 - 复杂度:预处理
,查询 。
3. 坑点与注意事项
- 取模规则:题目特别强调遵循 C++ 的负数取模规则(如
)。直接使用 %运算符即可,不需要像数学题那样转为正数。 - 数据类型:
累加后可能很大,乘上下标 更大,必须使用 long long。 - I/O 速度:
Final询问高达, cin/cout甚至普通的scanf/printf都可能超时。必须使用快读(Fast Read)和快写(Fast Write)。 - 数组下标:注意差分数组更新
R+1时不要越界(开大一点数组)。
4. AC 代码
cpp
#include <iostream>
#include <vector>
#include <cstdio>
#include <cctype>
using namespace std;
// 定义最大数组大小
const int MAXN = 100005;
// 核心变量
long long diff[MAXN]; // 差分数组
int ans_sum[MAXN]; // 答案的前缀和数组
int n, opt;
long long mod_val, min_val, max_val; // 题目中的 mod, min, max
// --- 快读快写模板 (必须使用,否则最后部分会TLE) ---
inline long long read() {
long long x = 0, f = 1;
char ch = getchar();
while (!isdigit(ch)) {
if (ch == '-') f = -1;
ch = getchar();
}
while (isdigit(ch)) {
x = x * 10 + (ch - '0');
ch = getchar();
}
return x * f;
}
// 专门用于读取单个字符 (用于区分 A 和 Q)
inline char readChar() {
char ch = getchar();
while (isspace(ch)) ch = getchar();
return ch;
}
// 快写整数
void write(long long x) {
if (x < 0) {
putchar('-');
x = -x;
}
if (x > 9) write(x / 10);
putchar(x % 10 + '0');
}
// 检查是否满足条件的辅助函数
inline bool check(long long val, int idx) {
long long res = (val * idx) % mod_val;
return (res >= min_val && res <= max_val);
}
int main() {
// 1. 读取基础信息
n = read();
opt = read();
mod_val = read();
min_val = read();
max_val = read();
// 2. 处理第一阶段:修改 + 少量查询
for (int k = 1; k <= opt; ++k) {
char op = readChar();
int l = read();
int r = read();
if (op == 'A') {
long long x = read();
// 差分数组修改 O(1)
diff[l] += x;
diff[r + 1] -= x;
} else {
// 暴力查询 O(N)
// 因为查询次数很少,这里即使每次从头扫一遍也是可以通过的
// 为了正确得到 a[i],我们需要从 diff[1] 累加到 diff[r]
long long current_val = 0;
long long count = 0;
for (int i = 1; i <= r; ++i) {
current_val += diff[i];
if (i >= l) { // 只统计在区间 [l, r] 内的
if (check(current_val, i)) {
count++;
}
}
}
write(count);
putchar('\n');
}
}
// 3. 预处理第二阶段:将数组固定,计算答案前缀和
long long current_val = 0;
for (int i = 1; i <= n; ++i) {
current_val += diff[i]; // 还原真实的 a[i]
// 计算前缀和:如果当前位满足条件,则+1
ans_sum[i] = ans_sum[i - 1] + (check(current_val, i) ? 1 : 0);
}
// 4. 处理 Final 询问
int final_qs = read();
while (final_qs--) {
int l = read();
int r = read();
// O(1) 回答
write(ans_sum[r] - ans_sum[l - 1]);
putchar('\n');
}
return 0;
}
5. 代码逻辑解析
- 差分维护:我们没有直接维护数组
,而是维护 diff。当遇到A操作时,只修改diff的两个端点。 Q操作处理:- 因为不知道具体的
值,必须从 diff[1]开始累加current_val直到i,才能还原出。 - 虽然这里看起来效率低(
),但由于操作说明中保证了 操作极少,所以总耗时远小于 次运算。
- 因为不知道具体的
Final阶段:- 此时没有修改了。我们最后一次性遍历整个数组,把动态的
diff转化为静态的ans_sum数组。 ans_sum[i]存储的是区间[1, i]中有多少个数符合要求。- 最后对于数百万次的询问,利用容斥原理
ans_sum[R] - ans_sum[L-1]瞬间得到答案。
- 此时没有修改了。我们最后一次性遍历整个数组,把动态的
这道题是典型的“纸老虎”,只要不被复杂的题目背景吓倒,识别出“少查多改用差分,离线查询用前缀和”的本质,就能轻松解决。