数据结构

OJ: luogu

题目 ID: P3948

标签: 前缀和

日期: 2026-01-01 10:44

题目解析

这是一道非常有趣的“反套路”题目。虽然题目背景里提到了线段树、分块、平衡树等各种高级数据结构,但通过分析数据范围和操作特性,你会发现这道题其实考察的是差分数组前缀和以及暴力模拟的结合,同时非常考验I/O优化(读写速度)。

以下是针对洛谷 P3948 的详细解析和 AC 代码。

1. 题目核心分析

题目分为两个阶段:

阶段一:在线修改与少量查询

  • 操作
    • A L R X:区间加法。
    • Q L R:区间查询满足 min(a[i]×i)%modmax\min \le (a[i] \times i) \% mod \le \max 的元素个数。
  • 数据特性
    • 数组长度 N80,000N \le 80,000
    • 修改次数 optopt 较多(高达 10510^5)。
    • 关键点:查询操作 QQ 非常少(不超过 1000 次)。

阶段二:离线大规模查询

  • 操作
    • 不再有修改,只有大量的区间查询。
    • 查询次数 Final 高达 1×1071 \times 10^7 次。
  • 关键点:数据量极大,必须要求单次查询 O(1)O(1),且必须使用快读快写。

2. 算法设计

针对阶段一:差分数组 (Difference Array)

由于修改多、查询少,且涉及区间加法,差分数组是最佳选择。

  • 定义 diff[i] = a[i] - a[i-1]
  • 修改 A L R X:只需 O(1)O(1) 操作:diff[L] += Xdiff[R+1] -= X
  • 查询 Q L R:由于查询次数极少(1000\le 1000),我们可以暴力还原数组。从 11 遍历到 RR,一边累加 diff 得到当前的 a[i]a[i],一边在 [L,R][L, R] 范围内统计满足条件的个数。
    • 复杂度:1000×800008×1071000 \times 80000 \approx 8 \times 10^7,在 C++ 1秒的时限内是可以接受的。

针对阶段二:前缀和 (Prefix Sum)

当所有修改结束后,数组 aa 的值固定了。

  • 我们可以遍历一次数组,计算出每个位置 ii 是否满足条件(满足记为1,不满足记为0)。
  • 构造一个答案的前缀和数组 ans_sum,其中 ans_sum[i] 表示前 ii 个数中有多少个满足条件。
  • 对于每个 Final 询问 [L,R][L, R],直接输出 ans_sum[R] - ans_sum[L-1]
  • 复杂度:预处理 O(N)O(N),查询 O(1)O(1)

3. 坑点与注意事项

  1. 取模规则:题目特别强调遵循 C++ 的负数取模规则(如 7%3=1-7 \% 3 = -1)。直接使用 % 运算符即可,不需要像数学题那样转为正数。
  2. 数据类型a[i]a[i] 累加后可能很大,乘上下标 ii 更大,必须使用 long long
  3. I/O 速度Final 询问高达 10710^7cin/cout 甚至普通的 scanf/printf 都可能超时。必须使用快读(Fast Read)和快写(Fast Write)
  4. 数组下标:注意差分数组更新 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. 代码逻辑解析

  1. 差分维护:我们没有直接维护数组 aa,而是维护 diff。当遇到 A 操作时,只修改 diff 的两个端点。
  2. Q 操作处理
    • 因为不知道具体的 a[i]a[i] 值,必须从 diff[1] 开始累加 current_val 直到 i,才能还原出 a[i]a[i]
    • 虽然这里看起来效率低(O(R)O(R)),但由于操作说明中保证了 QQ 操作极少,所以总耗时远小于 10810^8 次运算。
  3. Final 阶段
    • 此时没有修改了。我们最后一次性遍历整个数组,把动态的 diff 转化为静态的 ans_sum 数组。
    • ans_sum[i] 存储的是区间 [1, i] 中有多少个数符合要求。
    • 最后对于数百万次的询问,利用容斥原理 ans_sum[R] - ans_sum[L-1] 瞬间得到答案。

这道题是典型的“纸老虎”,只要不被复杂的题目背景吓倒,识别出“少查多改用差分,离线查询用前缀和”的本质,就能轻松解决。