Codeforces 前缀和与差分三题:区间查询、覆盖与贪心
当许多查询反复询问同一个数组的区间信息时,逐次扫描通常浪费了大量重复计算。前缀和把“多次查询”变成一次预处理,差分则把“多次区间修改”延迟到最后统一还原。
本文选择三道 Codeforces 题,从一维前缀计数开始,走到差分覆盖,最后把覆盖次数与贪心排序结合起来。重点是看清信息流向:题目是在反复读取区间,还是反复影响区间?
1. 前缀和与差分是一对逆操作
设原数组为 a,前缀数组 prefix 记录从开头到当前位置的累计值:
1 | prefix[i] = a[1] + a[2] + ... + a[i] |
那么区间 [l, r] 的和可以用两个前缀相减:
1 | sum(l, r) = prefix[r] - prefix[l - 1] |
差分数组则记录相邻位置的变化。想给整个区间 [l, r] 增加 value,只需要:
1 | difference[l] += value |
最后对差分数组求一次前缀和,就能恢复每个位置受到的总影响。
flowchart LR
A[原数组] -- 累加 --> B[前缀数组]
B -- 相邻相减 --> A
C[多次区间修改] -- 只记起点与终点后一位 --> D[差分数组]
D -- 一次前缀累加 --> E[每个位置的最终值]
可以用一句话区分:前缀和擅长区间查询,差分擅长区间增减。
2. 313B Ilya and Queries:给“相邻关系”做前缀计数
官方题目:313B Ilya and Queries · 难度 1100 · 官方标签:dp、implementation
题意压缩
给定一个字符串,每次查询区间 [l, r] 中有多少对相邻字符相等。查询很多,但字符串不会改变。
最容易写错的地方,是统计对象并不是字符,而是相邻位置之间的关系。可以构造一个关系数组:
1 | equal[i] = (s[i] == s[i - 1]) |
再对 equal 做前缀计数。这样每个查询只需两个前缀值相减。
1 |
|
为什么减去 prefix[left]
查询 [left, right] 中的相邻对,左端最早能使用的是 (left, left + 1)。prefix[right] 包含直到 right 为止的相等关系,减掉 prefix[left] 后,恰好移除左端之前和跨入左端的关系。
可以用最小区间检查公式:当 left == right 时,区间只有一个字符,不存在相邻对,答案应为 0。此时代入公式确实得到 prefix[right] - prefix[left] == 0。
复杂度
预处理 O(n),每次查询 O(1),总复杂度 O(n + q),空间复杂度 O(n)。
3. 816B Karen and Coffee:差分统计区间覆盖
官方题目:816B Karen and Coffee · 难度 1400 · 官方标签:binary search、data structures、implementation
题意压缩
有许多推荐温度区间。某个温度被至少 k 个区间覆盖时,称为可接受温度。接下来多次查询 [a, b] 内有多少个可接受整数温度。
这道题有两层预处理:
- 用差分统计每个温度被多少个区间覆盖;
- 把“覆盖次数至少为
k”转成 0/1,再做前缀和回答查询。
flowchart LR
A[推荐区间] --> B[差分端点]
B --> C[前缀累加得到覆盖次数]
C --> D[是否至少覆盖 k 次]
D --> E[再次前缀累加]
E --> F[O(1) 回答查询]
1 |
|
为什么是 right + 1
区间是闭区间 [left, right]。在 left 处让覆盖数增加,在 right + 1 处让它恢复,前缀累加后 right 仍然包含在覆盖范围内。
这也是差分题最常见的边界错误。数组需要多开一个位置,确保最大右端点的 right + 1 合法。
复杂度
设温度上界为 M。建立差分是 O(n),扫描整个值域是 O(M),每次查询 O(1),总复杂度 O(n + M + q)。
这种写法依赖值域不大。如果坐标可能达到 10^9,但只出现少量端点,就要考虑坐标压缩或有序事件扫描。
4. 276C Little Girl and Maximum Sum:覆盖次数也是资源
官方题目:276C Little Girl and Maximum Sum · 难度 1500 · 官方标签:data structures、greedy、implementation、sortings
题意压缩
给定一个数组和许多区间,目标是在允许重排数组的前提下,让所有区间和的总和尽可能大。
先交换求和顺序。与其逐个计算区间和,不如问每个位置最终被计算多少次:
1 | 总贡献 = Σ 数值[i] × 位置覆盖次数[i] |
区间覆盖次数可以用差分得到。接下来只剩一个配对问题:最大的数应该放在覆盖次数最多的位置,次大的数放在次多的位置。把两个数组同时排序并逐项相乘即可。
为什么同序排序最优
假设 x <= y,覆盖次数 p <= q。比较两种配对:
1 | x·p + y·q |
第一种减去第二种得到 (y - x)(q - p) >= 0,所以大数配大权重不会更差。不断消除逆序配对,就得到同序排序的最优方案。
1 |
|
代码把输入区间转换成左闭右开形式 [left, right):左端从 1 下标转成 0 下标后增加,原输入的 right 恰好就是右端后一位,因此直接在 difference[right] 处减少。
为什么必须使用 long long
单个数组值会被许多区间重复计算,乘积和总答案都可能超过 32 位整数范围。即使原数组和覆盖次数分别能放进 int,相乘前也应提升到 64 位。
复杂度
差分与还原为 O(n + q),排序为 O(n log n),总复杂度 O(n log n + q),空间复杂度 O(n)。
5. 三道题的方法对照
| 题目 | 原始操作 | 预处理结果 | 查询/计算阶段 | 总复杂度 |
|---|---|---|---|---|
| 313B | 多次读取字符串区间 | 相邻关系的前缀计数 | 每次两个前缀相减 | O(n + q) |
| 816B | 多个区间覆盖 + 多次询问 | 差分还原覆盖,再做 0/1 前缀 | 每次两个前缀相减 | O(n + M + q) |
| 276C | 多个区间决定位置权重 | 差分还原覆盖频次 | 排序后同序配对 | O(n log n + q) |
三题的共同点不是都使用某个模板,而是先把大量区间操作压缩成每个位置的一份摘要。得到摘要之后,后续工作才会变得简单。
6. 边界检查清单
- 前缀数组是否保留了代表“空前缀”的 0;
- 查询区间是闭区间、开区间,还是左闭右开;
- 差分数组是否为
right + 1多开了空间; - 统计对象是元素,还是元素之间的关系;
- 是否需要
long long保存累计次数、乘积和答案; - 值域过大时,是否还能直接扫描每一个坐标;
- 排序是否会破坏必须保留的原位置关系。
7. 建议的练习顺序
先完成 313B,练习把“相邻关系”转成 0/1 数组;再完成 816B,体会差分与前缀和如何串联;最后完成 276C,把区间频次看成权重,并证明排序贪心为什么成立。
如果仍不熟悉固定窗口、双指针和二分,可以先回到 Codeforces 序列优化三题。完整进度会继续维护在 算法练习路径。