1. 差分数组是什么?为什么它能高效解题
第一次听说差分数组这个概念时,我也是一头雾水。直到在LeetCode上被一道区间修改的题目卡住,才真正体会到它的威力。差分数组本质上是一种预处理技巧,它能在O(1)时间内完成区间增减操作,将原本O(n)的时间复杂度降维打击。
举个生活中的例子:假设你管理着10个水桶排成一排,每天要给第3到第7个水桶各加1升水。传统做法是遍历每个桶操作,而差分数组就像在水管网络中安装控制阀——只需在3号位打开进水阀,8号位关闭阀门,系统会自动完成区间内的均匀分配。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 差分数组的核心原理拆解
2.1 差分与原数组的转换关系
给定原数组arr = [a1, a2, ..., an],其差分数组diff定义为:
- diff[0] = arr[0]
- diff[i] = arr[i] - arr[i-1] (i > 0)
这个定义看似简单,却暗藏玄机。当我第一次手动推导时发现:对差分数组求前缀和,竟然能完美还原原数组!比如:
code复制原数组: [3, 7, 1, 4, 2]
差分数组: [3, 4, -6, 3, -2]
验证:
sum[0] = 3 = arr[0]
sum[1] = 3+4 = 7 = arr[1]
sum[2] = 3+4-6 = 1 = arr[2]
...
2.2 区间修改的魔法操作
假设要让arr[L..R]区间每个元素加val,传统做法需要循环R-L+1次。而用差分数组只需两步:
- diff[L] += val
- diff[R+1] -= val (当R+1 < n时)
这个操作之所以成立,是因为在后续求前缀和时:
- L位置开始的累加都会多val
- R+1位置开始的累加会抵消这个val
3. 从理论到实战:三道经典题型详解
3.1 LeetCode 1109. 航班预订统计
题目要求处理n个航班的预订记录,每条记录表示在[i,j]区间内预订k个座位。这正是差分数组的典型场景:
python复制def corpFlightBookings(bookings, n):
diff = [0] * (n + 2) # 多留一位防止越界
for i, j, k in bookings:
diff[i] += k
if j + 1 <= n:
diff[j+1] -= k
res = []
curr = 0
for i in range(1, n+1):
curr += diff[i]
res.append(curr)
return res
注意点:
- 差分数组通常要比原数组多开1-2位空间
- 边界检查必不可少(j+1 <= n)
- 前缀和计算从1开始还是0开始容易混淆
3.2 洛谷P2367 语文成绩
这道题要求对指定区间学生加分后查询最低分。用差分数组处理区间修改后,求前缀和时同步维护最小值:
python复制n, p = map(int, input().split())
scores = list(map(int, input().split()))
diff = [0] * (n + 2)
# 初始化差分数组
diff[0] = scores[0]
for i in range(1, n):
diff[i] = scores[i] - scores[i-1]
# 处理修改
for _ in range(p):
x, y, z = map(int, input().split())
diff[x-1] += z # 题目编号从1开始
if y < n:
diff[y] -= z
# 计算最终成绩并找最小值
min_score = float('inf')
current = 0
for i in range(n):
current += diff[i]
if current < min_score:
min_score = current
print(min_score)
3.3 二维差分:POJ 2155 Matrix
差分数组还能扩展到二维。给定N×N矩阵,需要处理子矩阵翻转(0变1,1变0)操作。二维差分的核心操作:
cpp复制// 对(x1,y1)到(x2,y2)的矩形区域进行翻转
void update(int x1, int y1, int x2, int y2) {
diff[x1][y1] ^= 1;
diff[x1][y2+1] ^= 1;
diff[x2+1][y1] ^= 1;
diff[x2+1][y2+1] ^= 1;
}
计算前缀和时需要两次累加:
cpp复制int query(int x, int y) {
int res = 0;
for(int i = x; i > 0; i -= i&-i)
for(int j = y; j > 0; j -= j&-j)
res += diff[i][j];
return res % 2;
}
4. 差分数组的六大实战技巧
4.1 初始化时的避坑指南
很多人在初始化差分数组时会犯两种错误:
- 忘记处理diff[0] = arr[0]
- 错误计算差分值
正确的初始化模板:
python复制def init_diff(arr):
n = len(arr)
diff = [0] * (n + 2) # 通常多开空间
diff[0] = arr[0]
for i in range(1, n):
diff[i] = arr[i] - arr[i-1]
return diff
4.2 处理边界条件的三种策略
- 多开空间法:如diff = [0]*(n+2)
- 条件判断法:if R+1 < n: diff[R+1] -= val
- 虚拟头节点:在索引0位置放置哑元
4.3 差分数组的逆操作
有时需要从差分数组还原原数组:
python复制def restore(diff):
arr = []
current = 0
for i in range(len(diff)):
current += diff[i]
arr.append(current)
return arr
4.4 结合树状数组的高阶用法
当需要频繁查询和更新时,可以用树状数组维护差分数组:
python复制class BIT:
def __init__(self, n):
self.size = n
self.tree = [0] * (n + 2)
def update(self, index, delta):
while index <= self.size:
self.tree[index] += delta
index += index & -index
def query(self, index):
res = 0
while index > 0:
res += self.tree[index]
index -= index & -index
return res
4.5 差分数组的变种应用
- 乘性差分:处理区间乘积问题
- 异或差分:处理开关灯问题
- 分数差分:处理非整数增量
4.6 调试差分数组的实用技巧
- 打印中间差分数组状态
- 手动计算小样例验证
- 对比暴力解法结果
- 使用assert检查边界条件
5. 性能对比:差分数组 vs 线段树
虽然线段树也能处理区间更新,但差分数组在特定场景优势明显:
| 特性 | 差分数组 | 线段树 |
|---|---|---|
| 时间复杂度 | O(1)更新 | O(logn)更新 |
| 空间复杂度 | O(n) | O(4n) |
| 编码难度 | 简单 | 中等 |
| 适用场景 | 离线批量更新 | 实时混合操作 |
| 查询效率 | O(n)前缀和 | O(logn)单点 |
实际刷题时,如果题目符合:
- 先处理所有更新操作
- 最后统一查询
- 不需要单点查询
优先考虑差分数组解法。
6. 从差分数组到前缀和的思维跃迁
掌握差分数组后,我发现很多问题可以转化为前缀和问题。比如「和可被K整除的子数组」:
python复制def subarraysDivByK(nums, k):
prefix = 0
count = [0] * k
count[0] = 1
res = 0
for num in nums:
prefix = (prefix + num) % k
res += count[prefix]
count[prefix] += 1
return res
这种「模前缀和」的思路,本质上是对差分概念的延伸应用。当我在周赛遇到这类题目时,差分思维往往能带来降维打击的效果。
