01 / 本课学习路线
本课学习路线
阅读与推演约 112 分钟,练习约 70 分钟,进阶练习另需约 35 分钟
02 / 学习目标与先修自测
学完本课你能做什么,以及开始前需要会什么
记牢前缀数组的约定:pre 比原数组长一格,pre[0] = 0,0 基闭区间 [l, r] 的和 = pre[r+1] − pre[l]。再用它推导余数配对、差分恢复与区间计数。
| 学完后能做的事 | 正文位置 | 检查方式 |
|---|---|---|
| 不看资料写出 pre 的递推与闭区间公式,并说出为什么右和要用 total − pre[i+1] | 第 03、04 节 | 自查第 2 条、必做任务 1 |
| 用前缀和逐位置比较左和与右和,通过(AC)P4200 | 第 04、08 节 | 必做任务 2 |
| 推导「同余配对 ⇒ 段和被 m 整除」,并说出余数 0 为什么要先进集合,通过 P4202 | 第 05、08 节 | 自查第 3 条、必做任务 3 |
| 写对差分的两端更新与最后一次累加,并说出 r+1 的含义 | 第 06 节 | 自查第 4 条、练习 3 |
| 见到「多次区间询问」先想预处理而不是重扫;含负数计数用前缀和 + 哈希 | 第 07、09 节 | 自查第 5 条、练习 2 |
先修自测:下面 5 题请先自己写答案,再展开对照。答不出的按括号里的位置补看再回来。本课假定你已完成模块 1 的「哈希计数」与上一课。
展开先修自测答案
自测 1:pre = [0] 然后 for x in a: pre.append(pre[-1] + x),得到 [0, 1, 8, 11]。这就是前缀数组,多出来的第一格 0 是全课的约定。
自测 2:sum(a[:3]) = 2 + 5 + (−1) = 6;sum(a[4:]) = 6。两者相等,所以题面示例里编号 3 是关键位置——第 04 节会用前缀和把这两次求和换成两端相减。
自测 3:(-7) % 5 = 3(除数为正时,Python 的取余结果恒为非负,与被除数符号无关);12 % 5 = 2。C++ / Java 里 −7 % 5 会得到 −2,第 05 节的补充里说明归一写法。
自测 4:真、假。集合的 in 平均 O(1),第 05 节用它记录「出现过的余数」。
自测 5:[int(x) for x in input().split(",")]——按逗号拆,不能用不带参数的 split()(那只按空白拆,整行会被当成一个记号)。第 09 节的错误表给了写错时的报错。
03 / 概念与术语
前缀和、下标约定、区间和、差分、恢复
前缀和与差分是一对互逆操作:前缀和把「区间求和」变成两端相减,差分把「区间修改」变成两端更新。它们依据同一组端点关系。
| 术语 | 含义 | 代码写法 |
|---|---|---|
| 前缀和数组 pre | pre[i] = 原数组前 i 项之和;长度 n + 1,pre[0] = 0 | pre[i + 1] = pre[i] + a[i] |
| 下标约定 | 本课统一:0 基原数组、pre 比它长一格 | pre = [0] * (n + 1) |
| 区间和 | 0 基闭区间 [l, r] 的和 | pre[r + 1] - pre[l] |
| 左和 / 右和 | 编号 i 之前 / 之后所有数之和 | pre[i] / total - pre[i + 1] |
| 同余 | 两数除以 m 余数相同;此时它们的差是 m 的倍数 | pre_i % m == pre_j % m |
| 差分数组 diff | diff[i] = a[i] − a[i−1],记录「从这里开始变化了多少」 | diff = [0] * (n + 1) |
| 区间加 | [l, r] 整体加 v,只改两个端点 | diff[l] += v; diff[r + 1] -= v |
| 恢复 | 对 diff 做一次前缀累加得到最终数组 | running += diff[i] |
| 预处理换查询 | 一次 O(n) 预处理,之后每次询问 O(1) | 先建 pre,再回答 Q 次询问 |
为什么 pre 要多开一格
有了 pre[0] = 0,「从头开始的区间」[0, r] 的和就是 pre[r+1] − pre[0],不需要特判;余数配对时空前缀的余数 0 也自然进集合。全课所有公式都建立在这一格之上,换成别的约定公式要整体改写。
04 / 完整手算例:P4200
递推 pre,再逐位置比较左和与右和
P4200「找黄金宝箱」:一行逗号分隔的整数(编号从 0 起),找第一个「之前所有数之和 = 之后所有数之和」的编号;首个位置的左和、末个位置的右和都定义为 0;不存在输出 −1。
递推:题面示例 2,5,-1,8,6
nums = [2, 5, -1, 8, 6] total = 20 pre = [0, 2, 7, 6, 14, 20] pre[i+1] = pre[i] + nums[i]
| 编号 i | nums[i] | 左和 pre[i] | 右和 20 − pre[i+1] | 相等? |
|---|---|---|---|---|
| 0 | 2 | 0 | 20 − 2 = 18 | 否 |
| 1 | 5 | 2 | 20 − 7 = 13 | 否 |
| 2 | −1 | 7 | 20 − 6 = 14 | 否 |
| 3 | 8 | 6 | 20 − 14 = 6 | 是 → 输出 3,停止 |
| 4 | 6 | 14 | 20 − 20 = 0 | (不再检查) |
与题面解释一致:编号 3 之前 2 + 5 + (−1) = 6,之后 6。右和用 total − pre[i+1] 而不是 total − pre[i]:后者把自己也算进右边(第 09 节错误表第一行)。
| 输入 | pre | 逐位置 | 输出 |
|---|---|---|---|
| 8,9 | [0, 8, 17] | i=0:左 0,右 9;i=1:左 8,右 0 | −1 |
| 7 | [0, 7] | i=0:左 0,右 7 − 7 = 0 → 相等 | 0 |
| 1,-1,0 | [0, 1, 0, 0] | i=0:0 与 −1;i=1:1 与 0;i=2:0 与 0 → 相等 | 2 |
只有一个数时左右都是 0,它就是关键位置——约定自然覆盖了题面里「第一个箱子左边为 0、最后一个右边为 0」两句,不需要特判。
前缀和模板(固定下标约定)
Pythonimport sys
def main():
data = sys.stdin.read().split()
# 步骤 1:在这里读入 n 与数组
...
pre = [0] * (n + 1)
for i in range(n):
# 步骤 2:在这里递推 pre[i+1] = pre[i] + a[i]
...
# 步骤 3:用 pre 回答题目的询问(左右平衡 / 区间和 / 余数配对)
...
main()pre 永远比原数组长一格;区间 [l, r] 的和 = pre[r+1] − pre[l]。差分数组同样开 n+1 长度,最后一遍累加恢复。
数据范围的含义:宝箱数不超过 10000、每个数在 [−1000, 1000],总和 total 的绝对值不超过 10⁷,Python 整数不会溢出。建 pre 与逐位置比较各 O(n),远在时限之内;即使不建 pre、每个位置重算两次 sum 也是 O(n²) ≈ 10⁸ 次加法,按量级估算在 Python 里很可能超时——预处理正是为了避免这一点。
05 / 余数配对:P4202
同余配对 ⇒ 段和被 m 整除
P4202「数字游戏」:第一行 n 和 m(m 是小慕手里那张牌的数字),第二行 n 个整数;判断这 n 张牌里是否存在连续的一段,段和是 m 的倍数;存在输出 1,否则输出 0。
推导:按本课的 0 基下标约定,区间 [i, j)(下标 i 到 j−1)的和 = pre[j] − pre[i],其中 i < j。若 pre[i] 与 pre[j] 除以 m 的余数相同,差就是 m 的倍数。于是问题变成「前缀和的余数序列里有没有重复」——用集合记录见过的余数,一次扫描即可。空前缀 pre[0] = 0 的余数 0 要先放进集合,否则「从第一张牌开始的段」配不上对。
| 读入的牌 | pre | pre % 5 | 集合里有它? | 动作 |
|---|---|---|---|---|
| (空前缀) | 0 | 0 | — | 集合 = {0} |
| 3 | 3 | 3 | 否 | 加入 → {0, 3} |
| 4 | 7 | 2 | 否 | 加入 → {0, 3, 2} |
| 1 | 8 | 3 | 是(与 pre=3 同余) | 输出 1,停止 |
同余的两处是 pre[1] = 3 和 pre[3] = 8,中间的段是第 2、3 张牌 4 + 1 = 5,恰是 5 的倍数。
| 读入的牌 | pre | pre % 7 | 集合里有它? | 动作 |
|---|---|---|---|---|
| (空前缀) | 0 | 0 | — | 集合 = {0} |
| 1 | 1 | 1 | 否 | 加入 |
| 2 | 3 | 3 | 否 | 加入 |
| 3 | 6 | 6 | 否 | 加入;扫完 → 输出 0 |
六个连续段的和是 1、2、3、3、5、6,都不是 7 的倍数,与余数全不重复的结论一致。
手算 3:n=2,m=3,牌 6 1(第一张牌就成立)
集合 = {0}
牌 6: pre = 6, 6 % 3 = 0 → 集合里已有 0 → 输出 1
对应的段就是「第 1 张牌」本身:6 是 3 的倍数题面里的「整除」按参考题解的实现理解
题面写「牌上数字之和能够整除小慕手中牌上的数字」,题目页参考题解的实现是「段和 % m == 0」,即段和是 m 的倍数。本课全部按这一实现;提交前以题目页的示例为准核对一次。
补充学习(选学)为什么同余就能配对约 4 分钟pre[j] − pre[i] 被 m 整除的推导
段和 (i, j] = pre[j] − pre[i]。若 pre[i] 和 pre[j] 除以 m 的余数相同,差就是 m 的整数倍——这就是「同余配对」的全部数学。实现时哈希表记每个余数第一次出现的下标,扫到重复余数即得答案;要「最短段」就每次更新,要「存在性」就见到即停。
负数取余要小心:Python 的 % 恒返回非负,C++/Java 的 % 会返回负数,需要 ((x % m) + m) % m 归一。使用 Python 的同学本课不会遇到这个问题,但要知道它存在。
06 / 差分:区间批量加减
两端更新,最后一次累加恢复
把「[l, r] 每个位置加 v」记成 diff[l] += v、diff[r+1] −= v;所有操作记完后,对 diff 做一次前缀累加,就得到最终数组。每次操作 O(1),恢复 O(n)。
| 步骤 | diff(下标 0..6) | 说明 |
|---|---|---|
| 初始 | [0, 0, 0, 0, 0, 0, 0] | 长度 n + 1 = 7 |
| [1, 3] + 2 | [0, +2, 0, 0, −2, 0, 0] | diff[1] += 2,diff[4] −= 2 |
| [2, 5] + 1 | [0, +2, +1, 0, −2, 0, −1] | diff[2] += 1,diff[6] −= 1(下标 6 就是多开的那一格) |
| 累加恢复 | [0, 2, 3, 3, 1, 1] | running 依次 0, 2, 3, 3, 1, 1 |
核对:位置 1 只被第一笔覆盖 → 2;位置 2、3 被两笔覆盖 → 3;位置 4、5 只被第二笔覆盖 → 1;位置 0 没被覆盖 → 0。
为什么在 r+1 处减:累加是「从左往右一直带着」的,diff[l] 加的 v 会一直带到数组末尾;想让效果恰好停在 r,就在 r+1 处减回去。第 09 节错误表给出了「在 r 处减」的具体错误输出。多开的那一格保证 r = n−1 时 r+1 = n 不越界。
07 / 前缀和 + 哈希:计数
数「和恰好为 k 的子数组」——可以含负数
把「和为 k」写成前缀差:子数组 (i, j] 的和 = pre[j] − pre[i],「和为 k」就是 pre[i] = pre[j] − k。扫描到 j 时,只要知道此前出现过多少个值等于 pre[j] − k 的前缀,就知道以 j 结尾的合法子数组有多少个。
用一个哈希表边扫边记每个前缀值出现过几次,一趟 O(n) 就能数完。这条路线不要求元素为正:负数只是让前缀和上下起伏,等式本身照样成立——这正是下一课的滑动窗口做不到的地方。
手推:数组 nums = [1, −1, 5, 2, −3, 3]、k = 5 的计数过程
起点 count = {0: 1} prefix = 0 ans = 0
x=1 prefix=1 查 count[1−5=−4]=0 ans=0 记 count[1]=1
x=−1 prefix=0 查 count[0−5=−5]=0 ans=0 记 count[0]=2
x=5 prefix=5 查 count[5−5=0]=2 ans=2 记 count[5]=1
x=2 prefix=7 查 count[7−5=2]=0 ans=2 记 count[7]=1
x=−3 prefix=4 查 count[4−5=−1]=0 ans=2 记 count[4]=1
x=3 prefix=7 查 count[7−5=2]=0 ans=2 记 count[7]=2
答案 2,对应 [1, −1, 5] 和 [5]和恰好为 k 的子数组计数(含负数)
Python# 和恰好为 k 的子数组个数:前缀和 + 哈希,元素可以是负数
def count_subarrays(nums, k):
count = {0: 1} # 前缀和 0 出现过一次(空前缀),让「从头开始的段」也能配对
prefix = 0
ans = 0
for x in nums:
prefix += x
ans += count.get(prefix - k, 0) # 先查:之前每出现一次 prefix-k,就多一个合法子数组
count[prefix] = count.get(prefix, 0) + 1 # 后记:避免和自己配对
return ans
assert count_subarrays([1, -1, 5, 2, -3, 3], 5) == 2 # [1,-1,5] 与 [5]
assert count_subarrays([1, 1, 1], 2) == 2 # 两个相邻的 [1,1]
assert count_subarrays([0, 0, 0], 0) == 6 # 6 个子数组和都是 0
assert count_subarrays([3], 5) == 0三处细节决定对错:① count 初始化为 {0: 1},否则从下标 0 开始的子数组数不到;② 累加的是出现次数 count.get(prefix − k, 0),不是「找到就加 1」——上面推演里 prefix=5 那一步一次就加了 2;③ 先查后记,同一个前缀不会和自己配成长度为 0 的段。
补充学习(选学)二维前缀和与子矩阵求和约 4 分钟P3282 用到的子矩阵 O(1) 求和
二维版本同一个思想:pre[i][j] = 左上角 (0,0) 到 (i−1,j−1) 的矩形和,子矩阵和 = 大 − 上 − 左 + 左上(容斥补回多减的角)。预处理 O(nm),之后每个候选位置 O(1)。下一课「双指针与滑动窗口」的进阶练习 P3282 数「和 ≥ k 的 c×c 正方形」,就是它的直接应用。
08 / 从步骤到程序
三道题的完整参考程序,每一步落在哪几行
先自己写完并提交一次,再展开对照。P4203 是进阶题,它的程序也在这里,第 10 节练习 5 会先让你手算。
| 题 | 步骤 | 代码 | 说明 |
|---|---|---|---|
| P4200 | 读一行逗号分隔 | sys.stdin.read().strip().split(",") | 不能用不带参数的 split() |
| P4200 | 递推 pre,逐位置比较 | left = pre[i];right = total - pre[i + 1] | 第一个相等就 break |
| P4202 | 余数集合 | seen = {0};r = pre % m;if r in seen | 0 先进集合 |
| P4203 | 总数减去「和 < target」的个数 | total = n * (n + 1) // 2;窗口收缩后 fewer += right - left + 1 | target ≤ 0 直接输出总数 |
展开完整参考程序 1:P4200 找黄金宝箱(先自己写完并提交一次,再展开对照)
完整程序:P4200(标准输入 → 标准输出)
Pythonimport sys
nums = [int(x) for x in sys.stdin.read().strip().split(",")] # 一行,逗号分隔
n = len(nums)
pre = [0] * (n + 1) # pre[i] = 前 i 个数之和;pre[0] = 0
for i in range(n):
pre[i + 1] = pre[i] + nums[i]
total = pre[n]
ans = -1
for i in range(n): # 从左往右,第一个满足的就停
left = pre[i] # 编号 i 之前所有数之和
right = total - pre[i + 1] # 编号 i 之后所有数之和(减去 pre[i+1] 才不含自己)
if left == right:
ans = i
break
print(ans)自测用例:题面两个示例,以及「7」「1,-1,0」「0,0,0」三组边界(第 04 节与第 09 节给了期望输出)。
展开完整参考程序 2:P4202 数字游戏
完整程序:P4202(标准输入 → 标准输出)
Pythonimport sys
data = sys.stdin.read().split()
n, m = int(data[0]), int(data[1])
nums = [int(x) for x in data[2:2 + n]]
seen = {0} # 空前缀的和是 0,余数 0 先进集合
pre = 0
found = 0
for x in nums:
pre += x
r = pre % m # Python 的 % 结果恒为非负
if r in seen: # 之前出现过同样的余数 → 中间这一段的和是 m 的倍数
found = 1
break
seen.add(r)
print(found)自测用例:第 05 节三组手算例;再自己写一个 O(n²) 枚举全部连续段的暴力程序,在几组随机输入(含负数)上对拍。
展开完整参考程序 3:P4203 寻找连续区间(进阶)
完整程序:P4203(标准输入 → 标准输出)
Pythonimport sys
data = sys.stdin.read().split()
n, target = int(data[0]), int(data[1])
nums = [int(x) for x in data[2:2 + n]]
total = n * (n + 1) // 2 # 非空子数组总数
if target <= 0: # 元素非负:每个子数组的和都 ≥ 0 ≥ target,全部满足
print(total)
else:
fewer = 0 # 和 < target 的子数组个数
left = 0
win = 0
for right in range(n):
win += nums[right]
while win >= target: # 收缩到窗口和 < target(元素非负保证窗口和单调)
win -= nums[left]
left += 1
fewer += right - left + 1 # 以 right 结尾、和 < target 的子数组恰有这么多个
print(total - fewer)输入:第一行 n 和目标值 target,第二行 n 个非负整数;输出和 ≥ target 的连续子数组个数。自测用例:第 10 节练习 5 的两组手算与 3 0 / 1 2 3(target = 0 → 6);再用 O(n²) 暴力枚举在几组随机输入上对拍。也可用前缀和 + 二分(第 10 节练习 5 答案里说明)。
09 / 边界、反例与复杂度
错误做法在具体输入上各输出什么
下面每一行都是一个具体的错误程序,给出输入、错误输出与正确输出;把参考程序改成对应写法就能复现。
| 错误做法 | 输入 | 错误输出 | 正确输出 | 判题结果 |
|---|---|---|---|---|
| P4200 右和写成 total − pre[i](把自己算进右边) | 2,5,-1,8,6 | -1 | 3 | 答案错误(WA) |
| P4200 找到后不 break,输出最后一个 | 0,0,0 | 2 | 0 | 答案错误(WA) |
| P4200 用不带参数的 split() 读入 | 2,5,-1,8,6 | 抛出 ValueError: invalid literal for int() | 3 | 运行错误(RE) |
| P4202 余数集合初始为空、漏掉 0 | 2 3 / 6 1 | 0 | 1 | 答案错误(WA) |
| P4202 只判断 pre % m == 0(只看从头开始的段) | 3 5 / 1 3 2 | 0(漏掉 3 + 2 = 5) | 1 | 答案错误(WA) |
| P4203 没有 target ≤ 0 的分支 | 3 0 / 1 2 3 | 收缩循环把 left 推过 right,抛出 IndexError | 6 | 运行错误(RE) |
| P4203 收缩条件写成 win > target(漏掉恰好等于) | 5 10 / 1 2 3 4 5 | 3 | 4 | 答案错误(WA) |
| P4203 双重循环枚举所有子数组 | n = 10⁵ | 结果正确但 5×10⁹ 次加法 | 同左 | 超时(TLE) |
| 差分忘记最后累加,直接输出 diff | 第 06 节的两笔操作 | [0, 2, 1, 0, -2, 0] | [0, 2, 3, 3, 1, 1] | 答案错误(WA) |
| 差分在 r 处减而不是 r+1 处 | 第 06 节的两笔操作 | [0, 2, 3, 1, 1, 0] | [0, 2, 3, 3, 1, 1] | 答案错误(WA) |
第一行的 −1 来自:pre = [0, 2, 7, 6, 14, 20],没有任何 i 满足 pre[i] = 20 − pre[i]。第五行的 3 5 / 1 3 2:从头开始的前缀和 1、4、6 都不是 5 的倍数,但第 2、3 张牌 3 + 2 = 5 是。
| 做法 | 预处理 | 每次询问 | n = 10⁴、Q = 10⁴ 时 |
|---|---|---|---|
| 每次重扫区间 | 无 | O(n) | ≈ 10⁸ 次加法,按量级估算会超时 |
| 前缀和 | O(n) | O(1) | ≈ 10⁴ + 10⁴ |
| 差分(Q 次区间加) | 每次 O(1) | 恢复一次 O(n) | ≈ 2×10⁴ + 10⁴ |
10 / 渐进练习与参考答案
跟做 → 改一个条件 → 独立实现 → 迁移
每题先在纸上或文件里做完,再展开答案。
练习 1(跟做):按第 04 节表的格式,对 P4200 输入 1,-1,1,-1 和 3,0,3 逐位置列出左和、右和,写出输出。
展开练习 1 答案
1,-1,1,-1:pre = [0, 1, 0, 1, 0],total = 0。i=0:左 0,右 0 − 1 = −1;i=1:左 1,右 0 − 0 = 0;i=2:左 0,右 0 − 1 = −1;i=3:左 1,右 0。没有相等 → 输出 -1。3,0,3:pre = [0, 3, 3, 6],total = 6。i=0:左 0,右 6 − 3 = 3;i=1:左 3,右 6 − 3 = 3 → 相等,输出 1。
练习 2(改一个条件):P4202 改成「输出和为 m 的倍数的连续段有多少个」,用第 07 节的计数写法(记录每个余数出现的次数)对 4 5 / 3 4 1 2 手算。
展开练习 2 答案
计数表从 {0: 1} 开始。余数依次 3(表里 0 个 → 加 0,记 3:1)、2(0 个,记 2:1)、3(已有 1 个 → 答案 +1,记 3:2)、10 % 5 = 0(已有 1 个 → 答案 +1,记 0:2)。答案 2:段 [4, 1] 和 [3, 4, 1, 2]。与「存在性」版本的区别只在:集合换成计数表、见到重复不停止而是累加。
练习 3(独立实现):写函数 apply_range_adds(n, ops),ops 是若干 (l, r, v) 三元组(0 基闭区间),返回最终数组;让三条断言通过,其中一条覆盖「区间到最后一格」。
展开练习 3 答案
apply_range_adds 的参考实现(自带断言)
Pythondef apply_range_adds(n, ops):
diff = [0] * (n + 1) # 多开一格,r+1 最大可以是 n
for l, r, v in ops: # 0 基闭区间 [l, r] 整体加 v
diff[l] += v
diff[r + 1] -= v
out = []
running = 0
for i in range(n): # 一次前缀累加恢复最终数组
running += diff[i]
out.append(running)
return out
assert apply_range_adds(6, [(1, 3, 2), (2, 5, 1)]) == [0, 2, 3, 3, 1, 1]
assert apply_range_adds(3, [(0, 2, 5)]) == [5, 5, 5] # 覆盖到最后一格:diff[3] -= 5 不越界
assert apply_range_adds(4, [(0, 0, 1), (3, 3, 1)]) == [1, 0, 0, 1]diff 开 n + 1 长度,r = n − 1 时 diff[n] −= v 落在多开的那一格,恢复时只累加前 n 格。
练习 4(独立实现):完成「必做任务 1」的 build_pre 与 range_sum,再加一条断言:单个元素 [2, 2] 的区间和应为 3。
展开练习 4 答案
build_pre / range_sum 的参考实现(自带断言)
Pythondef build_pre(a):
pre = [0] * (len(a) + 1) # 比原数组长一格,pre[0] = 0
for i, x in enumerate(a):
pre[i + 1] = pre[i] + x # pre[i+1] = 前 i+1 项之和
return pre
def range_sum(pre, l, r):
return pre[r + 1] - pre[l] # 0 基闭区间 [l, r]
pre = build_pre([1, 7, 3, 6, 5, 6])
assert pre == [0, 1, 8, 11, 17, 22, 28]
assert range_sum(pre, 0, 1) == 8 # 1 + 7(含首元素)
assert range_sum(pre, 3, 5) == 17 # 6 + 5 + 6(含尾元素)
assert range_sum(pre, 2, 2) == 3 # 单个元素四条断言分别覆盖:整表、含首元素、含尾元素、单元素。
练习 5(迁移):P4203 输入 5 10 / 1 2 3 4 5,先用双重循环列出所有和 ≥ 10 的子数组,再按第 08 节程序的窗口收缩逐个 right 算出「和 < 10 的子数组个数」,验证两种算法得到同一个答案;再算 3 7 / 3 4 7。
展开练习 5 答案
5 10 / 1 2 3 4 5:和 ≥ 10 的子数组是 [1,2,3,4] = 10、[1,2,3,4,5] = 15、[2,3,4,5] = 14、[3,4,5] = 12,共 4 个。窗口法:总数 15;right=0 窗 [1] 计 1,right=1 窗 [1,2] 计 2,right=2 窗 [1,2,3] 计 3,right=3 和为 10 收缩到 [2,3,4] 计 3,right=4 收缩到 [4,5] 计 2;和 < 10 的共 11 个,15 − 11 = 4。3 7 / 3 4 7:[7]、[3,4]、[4,7]、[3,4,7] 共 4 个。前缀和 + 二分的做法:pre = [0, 3, 7, 14] 单调不减,对每个 i 二分找第一个 pre[j] ≥ pre[i] + target 的 j,贡献 n + 1 − j 个;元素非负是二分成立的前提。
11 / 读题要求与复习自评
三道题的要求对照,以及完成本课之后怎么复习
提交前把下表过一遍;题目页的题面与样例是最终依据。
| 项目 | P4200 找黄金宝箱 | P4202 数字游戏 | P4203 寻找连续区间 |
|---|---|---|---|
| 输入 | 一行,逗号分隔的整数 | 第一行 n m;第二行 n 个整数 | 第一行 n target;第二行 n 个非负整数 |
| 输出 | 第一个关键位置的编号;不存在输出 −1 | 存在输出 1,否则 0 | 和 ≥ target 的连续子数组个数 |
| 核心步骤 | pre 递推 + 逐位置比较左右和 | 前缀和取余 + 集合去重(0 先进) | 总数减去「和 < target」的个数(窗口)或前缀和 + 二分 |
| 数据范围 | 宝箱数 1..10000,值 −1000..1000 | 以题目页为准 | n 可达 10⁵,注意总数约 5×10⁹(Python 无溢出) |
| 样例 | 2,5,-1,8,6 → 3;8,9 → −1 | 题目页为准(自拟 4 5 / 3 4 1 2 → 1) | 题目页为准(自拟 5 10 / 1 2 3 4 5 → 4) |
题解入口:需要对照解法时,先展开本课第 08 节的三份完整参考程序;三道题的题目页另有思路与参考代码,可在题目页查看。复习与自评:本课算完成 = 两道必做题 P4200、P4202 都通过判题,并勾选全部六条「学习完成检查」(含复习题 P2556 那一条);进阶练习不影响完成状态。六条检查是自评,不改变题目的通过(AC)状态。复习时用三个问题自测:① 不看正文,写出 pre 的递推与 [l, r] 的公式,并说出右和为什么用 total − pre[i+1];② 不看表格,重算 4 5 / 3 4 1 2 的余数序列并指出哪两处同余;③ 说出差分为什么在 r+1 处减、多开的一格什么时候用到。答不出哪一条,就回到对应的节重读,再做第 10 节对应的练习。
基础加练(选做)
同一主题的 1 道基础题
这组题目采用标准输入输出(ACM 模式):程序从标准输入读取数据、把结果写到标准输出。它们难度低于必做题,适合在必做题之前热身,或在未通过时回来巩固;不计入本课完成标准。每道题给出练习重点、完成标准与提示;已在本站通过的题目会直接显示为已通过。
12 / 练习
按顺序完成本课的任务
必做题已通过 0/2 道;进阶练习已通过 0/1 道
实现前缀数组构建与区间求和函数(build_pre、range_sum)
必做任务 1:代码自测自主练习练习重点:pre 的递推与闭区间和公式;预计用时:10 分钟
完成标准:能直接写出 [l, r] 的和 = pre[r+1] − pre[l]
需要时查看提示
pre 开 n+1 长度、pre[0]=0;两个断言分别检验「含首元素」和「含尾元素」两个边界。参考实现在第 10 节练习 4 的展开区。
自测代码(复制到你的代码文件中运行,检查输出是否一致)
def build_pre(a):
# 请在这里实现:pre[0]=0,pre[i]=a[0..i-1] 之和
...
def range_sum(pre, l, r):
# 请在这里实现:0 基闭区间 [l, r] 的和
...
pre = build_pre([1, 7, 3, 6, 5, 6])
assert pre == [0, 1, 8, 11, 17, 22, 28]
assert range_sum(pre, 0, 1) == 8 # 1 + 7
assert range_sum(pre, 3, 5) == 17 # 6 + 5 + 6P4200 · 找黄金宝箱(关键位置)
必做任务 2练习重点:枚举每个位置,用 pre 以 O(1) 比较左和与右和;预计用时:25 分钟
完成标准:能说出「第一个满足条件」为什么必须从左往右枚举
需要时查看提示
左和 = pre[i],右和 = 总和 − pre[i+1]。找到第一个相等的位置立即输出;扫完没有输出 −1。首尾位置的「另一侧和为 0」已被约定自然覆盖。输入是一行逗号分隔,第 04 节把题面示例逐位置列出。
P4202 · 数字游戏(整除段)
必做任务 3练习重点:前缀和取余 + 哈希配对,判断是否存在被整除的连续段;预计用时:35 分钟
完成标准:能推导「同余配对 ⇒ 段和整除」并处理余数 0 的情形
需要时查看提示
pre[0]=0 也要进哈希——它让「从头开始的段」也能配对。扫描到某个余数已出现过即可输出存在;注意第一行的 m 是除数不是数组成员。第 05 节有三组逐张牌的手算。
P4203 · 寻找连续区间
进阶练习 1进阶练习练习重点:前缀和配合二分或滑动窗口,把区间查找降到 O(n log n) 或 O(n);预计用时:35 分钟
完成标准:能说出为什么前缀数组单调时才能二分
需要时查看提示
元素全为非负时 pre 单调不减,才可以对 pre 二分;下一课「双指针与滑动窗口」的单调窗口同样以「元素非负」为前提。含负数时这两条路都不成立,要按题目实际问法另选方法:问「和恰好为 k 的子数组个数」用本课的前缀和 + 哈希,问「和 ≥ k 的最短子数组」则要用单调队列维护前缀最小值一类的方法,本课程不展开。先做第 10 节练习 5 的手算,再看第 08 节的程序;target 为 0 要单独处理。
提交结果
提交结果说明与处理方法
- WA
答案错误
多数是差一位:用 pre[r]−pre[l] 求闭区间把第 r 项漏掉了,或右和用
total− pre[i] 把自己算进去了;对照约定「pre[r+1] − pre[l]」逐项检查,第 09 节的表给出了每种错误的具体输出- RE
运行错误
pre 只开了 n 长度,访问 pre[n] 越界;差分数组同样要开 n+1;P4200 按空格拆逗号输入会抛
ValueError- TLE
超时
每次询问重扫区间 O(nQ) 会超时;先建 pre 再回答
- PE
格式错误
输出 −1 的场景不要带多余空格;编号是 0 基还是 1 基按题目说明
- AC
通过
口述一遍差分为什么要在 r+1 减:想让效果恰好停在 r
13 / 学习完成检查
本课学习完成检查
完成本课需要:必做题全部通过,并勾选本课的全部学习完成检查;进阶练习、基础加练与复习题单独统计,不影响完成状态。登录后,勾选记录会保存到账号,并更新课程总览的完成状态。