开始前,会这些就够了
- 会用列表下标、循环和整数加减;知道第1天对应下标0。
- 先读 复杂度与操作次数 的前缀和部分,理解从左到右累计。
- 先读 单点增加与Fenwick树:读完后比较:单点更新与批量区间更新是不同的操作组合。
遇到不熟悉的词,先看这 3 个解释
打开完整术语小抄 ↗七天的记录是 [2,1,3,0,2,4,1]。现在要给第3到第7天各补5,再给第5到第6天各减2。最直接的做法逐天改值;如果待处理区间很多,同一天会被重复访问。
差分数组先把“从哪里开始多5、到哪里不再多5”记下来。每条更新只记两个端点,全部记完后统一扫一遍,得到每天的新数值。
把例子带到自己的电脑上
下载完整练习包
需要 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- demo.py:完整可运行源码
- README.md:运行步骤与练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和运行验证记录
- SHA256SUMS.txt:文件摘要
在这个文件夹打开 PowerShell,逐行运行
python --version
python -B demo.py解压后,在含 demo.py 的目录打开终端。命令分别回车执行。整数半开区间批量增量;364个初始数组、19956次单更新、1484对更新及反序、8类拒绝输入;有末端哨兵、空区间、重复还原与快照隔离;没有在线区间查询或性能保证。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
一段日期都加同一个数,只在开始和结束做标记
正在准备实验控件…
固定七个整数,采用半开区间 [l,r):包含 l,不包含 r。diff[7] 是末端归零标记,不是第八天。区间增量可以累积,恢复全部结果仍要线性扫描;图中最多保存 8 次操作。
先说清楚:加一个数,以及右端不包含
本文的 add(left,right,delta) 表示给下标 left 到 right-1 的每一项增加 delta。用 [left,right) 写这个左闭右开范围。七天中第3到第7天对应 [2,7),不是 [3,7);后者会漏掉第3天。
增加和赋值是两种操作。原来第3天是3,加5后应为8;不会把它改成5。delta 可以是负整数,表示减去相应数量。空区间 [7,7) 没有任何天,因此增加9也应没有变化。
这个练习面向一批更新后统一取得完整数组的任务。中途当然也能还原一次,但每次还原都要重新扫描;不能把“记更新很快”理解为“随时读取全部结果也很快”。
第二排格子记的是“比前一格多多少”
把数组开头之前的值看成0,令 diff[0]=a[0],后面 diff[i]=a[i]-a[i-1]。默认七天得到 [2,-1,2,-3,2,2,-3]:第2天比第1天少1,第3天比第2天多2。负差值完全正常。
从左到右累计差值,就能重新得到原数组。例如下标2的值是 2+(-1)+2=3。差值中的相邻减法会抵消,累加到哪里,就留下那个位置的原值。
代码多存一个 diff[n],并把数组之后的值约定为0,所以 diff[n]=-a[n-1]。七天例子的最后一格是-1;把八个差值全加起来会回到0。这格只负责结束边界,不是第8天的数据。
一整段一起增加,内部相邻差值为什么没变
若相邻两天都加5,它们的新差值是 (今天+5)-(昨天+5),两个5抵消,所以区间内部的差值不用改。只有进入区间时突然多5,离开区间时突然少5。
于是 add(2,7,5) 只做 diff[2]+=5 和 diff[7]-=5。差值变为 [2,-1,7,-3,2,2,-3,-6];还原前七项得到 [2,1,8,5,7,9,6]。下标7正好是预留的结束格,所以更新到最后一天也不需要越界写入。
若 left=right,两次加减落在同一格,整数结果互相抵消。这既支持数组中间的空区间,也支持空数组唯一合法的 [0,0)。不要为了“至少改一项”而把右端强行加1。
重叠区域会得到两份增量,结束标记也会叠加
再执行 add(4,6,-2),给第5、6天各减2。开始格 diff[4] 加-2,结束格 diff[6] 减-2,也就是加2。最后差值为 [2,-1,7,-3,0,2,-1,-6],每天结果为 [2,1,8,5,5,7,6]。
每次操作都在相同差值账本上加减,因此多个区间的贡献自然相加。把两条更新交换次序,最后结果仍一样;这是整数加法下的增量操作性质,不适用于把区间直接赋成某值。
materialize 只读取差值并新建结果列表。若直接在 diff 上原地累计、又继续把它当差值记录更新,就混淆了两种含义;本实现保留差值账本,重复还原不会把结果越加越大。
拿逐格修改的笨办法当独立账本
保存完整代码为 demo.py,执行 python demo.py。2026-10-23 在 Windows、CPython 3.14.0 实际运行,使用标准库。默认输出列出每次更新后的差值和真实每天值;为便于观察,演示部分每次都会还原一次。
程序穷举长度0到5、值取-1/0/2的364个初始数组,对每个合法半开区间分别加-2、0、3,共19,956次单更新;每次与直接遍历区间的 oracle 比较,再施加反向增量核对恢复。
另外枚举1,484对更新并交换次序再核对,覆盖重叠和边界。测试还确认修改原列表或返回列表不会污染账本,8类非法输入在修改前被拒绝;检查用显式 AssertionError,不依赖 assert 开关。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
"""Batch range increments on integer arrays; half-open ranges [left, right)."""
from itertools import product
def require(condition, message):
if not condition:
raise AssertionError(message)
class RangeAdds:
def __init__(self, values):
if not isinstance(values, (list, tuple)):
raise TypeError("list or tuple required")
if len(values) > 10000 or any(type(x) is not int for x in values):
raise ValueError("at most 10000 integer entries; bool is not accepted")
self.n = len(values)
self.diff = [0] * (self.n + 1)
previous = 0
for i, value in enumerate(values):
self.diff[i] = value - previous
previous = value
self.diff[self.n] = -previous # Return to zero just beyond the array.
def add(self, left, right, delta):
if any(type(x) is not int for x in (left, right, delta)):
raise TypeError("integer bounds and delta required")
if not 0 <= left <= right <= self.n:
raise ValueError("require 0 <= left <= right <= n")
self.diff[left] += delta
self.diff[right] -= delta # Same cell cancels when left == right.
def materialize(self):
total, values = 0, []
for i in range(self.n):
total += self.diff[i]
values.append(total)
return values # Do not overwrite the stored difference array.
def oracle(values, updates):
result = list(values)
for left, right, delta in updates:
for i in range(left, right):
result[i] += delta
return result
def main():
original = [2, 1, 3, 0, 2, 4, 1]
updates = [(2, 7, 5), (4, 6, -2), (7, 7, 9)]
demo = RangeAdds(original)
print(f"initial={original}; diff={demo.diff}")
for update in updates:
demo.add(*update)
print(f"add{update}; diff={demo.diff}; values={demo.materialize()}")
require(demo.materialize() == [2, 1, 8, 5, 5, 7, 6], "demo")
require(original == [2, 1, 3, 0, 2, 4, 1], "input was changed")
arrays, single_updates, paired_updates = 0, 0, 0
for n in range(6):
for values in product((-1, 0, 2), repeat=n):
arrays += 1
tree = RangeAdds(values)
require(tree.materialize() == list(values), "initial reconstruction")
for left in range(n + 1):
for right in range(left, n + 1):
for delta in (-2, 0, 3):
tree.add(left, right, delta)
expected = oracle(values, [(left, right, delta)])
require(tree.materialize() == expected, "single update")
require(sum(tree.diff) == 0, "end sentinel")
tree.add(left, right, -delta)
require(tree.materialize() == list(values), "inverse update")
single_updates += 1
for n in range(5):
base = [i - 2 for i in range(n)]
operations = [(l, r, d) for l in range(n + 1)
for r in range(l, n + 1) for d in (-2, 3)]
for first, second in product(operations, repeat=2):
expected = oracle(base, [first, second])
for ordered in [(first, second), (second, first)]:
tree = RangeAdds(base)
for operation in ordered:
tree.add(*operation)
require(tree.materialize() == expected, "overlap or order")
paired_updates += 1
isolated = [1, 2]
snapshot = RangeAdds(isolated)
isolated[0] = 99
returned = snapshot.materialize()
returned[0] = 88
require(snapshot.materialize() == [1, 2], "snapshot alias")
invalid = [lambda: RangeAdds(iter([1])), lambda: RangeAdds([True]),
lambda: RangeAdds([0] * 10001), lambda: demo.add(-1, 2, 1),
lambda: demo.add(4, 3, 1), lambda: demo.add(0, 8, 1),
lambda: demo.add(0, 2, 1.0), lambda: demo.add(False, 2, 1)]
rejected = 0
before = demo.diff.copy()
for action in invalid:
try:
action()
except (TypeError, ValueError):
rejected += 1
else:
raise AssertionError("invalid input accepted")
require(demo.diff == before, "rejected operation changed state")
print(f"initial_arrays={arrays}; single_updates={single_updates}; paired_updates={paired_updates}")
print(f"reversed_pair_checks={paired_updates}; rejected_inputs={rejected}; snapshot_isolation=PASS")
print("PASS")
if __name__ == "__main__":
main()
怎样运行:在独立练习文件夹中,依次执行下载区的命令。2026-10-23 使用 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查 复跑这份源码并核对完整输出。整数半开区间批量增量;364个初始数组、19956次单更新、1484对更新及反序、8类拒绝输入;有末端哨兵、空区间、重复还原与快照隔离;没有在线区间查询或性能保证。
运行后对照结果
initial=[2, 1, 3, 0, 2, 4, 1]; diff=[2, -1, 2, -3, 2, 2, -3, -1]
add(2, 7, 5); diff=[2, -1, 7, -3, 2, 2, -3, -6]; values=[2, 1, 8, 5, 7, 9, 6]
add(4, 6, -2); diff=[2, -1, 7, -3, 0, 2, -1, -6]; values=[2, 1, 8, 5, 5, 7, 6]
add(7, 7, 9); diff=[2, -1, 7, -3, 0, 2, -1, -6]; values=[2, 1, 8, 5, 5, 7, 6]
initial_arrays=364; single_updates=19956; paired_updates=1484
reversed_pair_checks=1484; rejected_inputs=8; snapshot_isolation=PASS
PASS
省下的是反复遍历区间,最终输出仍要花线性时间
构建 n+1 个差值需要 O(n) 次量级的工作;一次合法更新只做两次格子加减;q 次更新后最后还原一遍,总工作按整数操作次数计为 O(n+q),额外存储为 O(n)。
如果每条更新后都要输出所有 n 项,输出本身就有 n×q 项,不能再沿用“一次统一还原”的总成本。本篇默认轨迹和测试为了可观察性会反复调用 materialize,它们不属于这个只还原一次的成本前提。
Python 整数可以扩展位数,因此没有固定32位回绕,但巨大整数加减仍取决于位数。移植到 Java 或其他固定宽度整数时,要为差值和累计结果选择范围并检查溢出;本例没有给最大输入规模做性能测量。
先看操作形态,再决定要不要更复杂的结构
大量区间增加、最后读取结果,适合这个简单账本。只读区间求和可以用已有前缀和;单点修改和反复区间查询可回读 Fenwick 文章。它们处理的操作组合不同。
若要求每次区间增加后立刻查询某一天,可以进一步把差值的前缀和交给动态结构维护;如果还要快速区间求和,又有另一层设计。这里不把两端标记扩大成对任意在线查询都常数时间的保证。
先写下实际需要的结果出现时机,再数一共发生多少次完整扫描。这样才能看清差分方法到底省去了哪一部分工作。
记住这些要点
- 差分格保存相邻变化,累计后才是每天的实际值。
- [left,right) 在 left 开始增加,在 right 撤销;right=n 使用预留格。
- 多个增量可以叠加;空区间的两次标记互相抵消。
O(n+q)的前提是批量更新后统一还原一次。
别把例子用过头
- 把日历编号和0基下标混用。
- 在 right+1 撤销,误把闭区间公式搬到半开区间。
- 原地还原差值后,继续把它当差值更新。
- 把区间赋值也当成可交换的增量,或忽略每次输出全部结果的成本。
自己试一小步
只看两端标记,能否预测四条区间更新之后每一天的结果?
- 自选8个初始整数,加入一条覆盖到末端的更新、一条负增量、一条重叠更新和一条空区间更新。
- 先手写差值、端点变化和预期结果,再用逐格修改账本核对。
- 交换更新次序,并解释哪些条件让最终结果不变。
- 把需求改成“每次更新后输出所有项”,重新数完整扫描与输出的次数。
做到这些,就算完成
- 日历编号、数组下标和右端是否包含写清楚。
- 包含 diff[n] 的含义和一次完整还原过程。
- oracle 不调用差分构建或两端更新公式。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Competitive Programmer's Handbook:Range updates(作者源文件) (新标签页)↗
相邻差值的定义、前缀累加还原,以及闭区间更新对应开始和结束后一格两个变化。
- Princeton Competitive Programming:Fall 23 Week 4 题解 (新标签页)↗
Problem E 中先记录区间两端增减、再累加恢复的第一层操作;本文不实现二层操作次数查询。
- Python 3.14 range:右端不包含 (新标签页)↗
正步长 range 的元素满足小于 stop,说明本篇半开区间的右端约定;差分算法另由教材与实验支持。