差分数组(Difference Array)及其应用
一、知识点描述
差分数组是一种用于高效处理区间更新操作的数据结构。它主要用于解决这样的问题:我们需要对原始数组的某个连续区间中的所有元素,都加上或减去一个相同的常数,并且这样的更新操作会频繁执行多次。在完成所有更新后,我们需要查询最终的数组状态或某个元素的值。
如果使用朴素方法,每次对长度为 L 的区间进行更新(如将所有元素加 k),时间复杂度为 O(L)。如果进行 M 次这样的更新,总时间复杂度会达到 O(M * L),在处理大规模数据时效率很低。差分数组可以将单次区间更新的时间复杂度优化到 O(1),而最终的数组重建(查询)可以在 O(N) 时间内完成(N 为数组长度)。
二、核心思想与定义
-
原始数组:假设我们有一个长度为
n的数组arr[0…n-1]。 -
差分数组:我们定义一个新的、同样长度为
n的数组diff[0…n-1]。它的定义如下:diff[0] = arr[0](当i = 0时)diff[i] = arr[i] - arr[i-1](当1 <= i < n时)
简单来说,差分数组的第
i个元素,等于原始数组第i个元素与第i-1个元素的差值。 -
重要性质:从差分数组的定义,我们可以推导出原始数组是差分数组的前缀和。
arr[0] = diff[0]arr[1] = diff[0] + diff[1]arr[2] = diff[0] + diff[1] + diff[2]- ...
- 一般地,
arr[i] = diff[0] + diff[1] + ... + diff[i] = Σ_{j=0}^{i} diff[j]
三、区间更新的操作原理
这是差分数组最精妙和实用的部分。假设我们需要对原始数组 arr 的区间 [L, R](包含左右端点)内的所有元素都加上一个值 k。
朴素方法:遍历 i 从 L 到 R,执行 arr[i] += k。时间复杂度为 O(R-L+1)。
差分数组方法:
- 在
diff[L]上加上k。思考一下这个操作的影响:根据arr[i]是diff前缀和的性质,所有i >= L的arr[i]在计算前缀和时都会包含这个新加的k。也就是说,从位置L开始,一直到数组末尾的所有arr[i]都隐式地增加了k。 - 在
diff[R+1]上减去k(前提是R+1 < n)。这个操作是为了“抵消”上一步操作对区间[R+1, n-1]产生的影响。因为我们的目标只是更新区间[L, R],所以需要在R之后的下一个位置“关闭”这个更新效应。这样,对于所有i > R的arr[i],它们的前缀和里就同时包含了+k和-k,效果抵消,值保持不变。
核心操作总结:
要对 arr[L…R] 区间内所有元素加 k,只需执行:
diff[L] += k
if (R + 1 < n) diff[R+1] -= k
每次区间更新的时间复杂度是 O(1)。
四、解题步骤详解
我们通过一个完整的例子来演示如何构建、使用和重建差分数组。
问题:初始数组 arr = [1, 3, 5, 2, 4]。依次执行以下更新操作:
- 对区间
[1, 3]所有元素加2。 - 对区间
[0, 2]所有元素加-1(即减1)。 - 对区间
[2, 4]所有元素加3。
请输出最终数组。
步骤1:构建初始差分数组 diff
- 根据定义:
diff[i] = arr[i] - arr[i-1](i>0),diff[0] = arr[0]。 - 计算:
diff[0] = arr[0] = 1diff[1] = arr[1] - arr[0] = 3 - 1 = 2diff[2] = arr[2] - arr[1] = 5 - 3 = 2diff[3] = arr[3] - arr[2] = 2 - 5 = -3diff[4] = arr[4] - arr[3] = 4 - 2 = 2
- 初始差分数组:
diff = [1, 2, 2, -3, 2]
步骤2:应用区间更新操作
我们直接在 diff 数组上进行操作。
-
操作1:对
arr[1…3]加2。L=1,R=3,k=2。diff[1] += 2→diff[1]从2变为4。R+1=4,4 < n成立,所以diff[4] -= 2→diff[4]从2变为0。- 更新后
diff = [1, 4, 2, -3, 0]
-
操作2:对
arr[0…2]加-1。L=0,R=2,k=-1。diff[0] += (-1)→diff[0]从1变为0。R+1=3,3 < n成立,所以diff[3] -= (-1)→diff[3] = -3 - (-1) = -2。(注意是-= k,k为-1,所以是- (-1)即+1)- 更新后
diff = [0, 4, 2, -2, 0]
-
操作3:对
arr[2…4]加3。L=2,R=4,k=3。diff[2] += 3→diff[2]从2变为5。R+1=5,5不小于n(n=5),所以不执行diff[5]的操作(因为数组越界)。- 更新后
diff = [0, 4, 5, -2, 0]
步骤3:从最终差分数组重建原始数组 arr_final
根据性质 arr[i] = Σ_{j=0}^{i} diff[j],我们对最终的 diff 数组求前缀和。
arr_final[0] = diff[0] = 0arr_final[1] = diff[0] + diff[1] = 0 + 4 = 4arr_final[2] = diff[0] + diff[1] + diff[2] = 0 + 4 + 5 = 9arr_final[3] = 0 + 4 + 5 + (-2) = 7arr_final[4] = 0 + 4 + 5 + (-2) + 0 = 7
最终结果数组为:[0, 4, 9, 7, 7]。
验证:我们可以用朴素方法手动计算一下。
初始: [1, 3, 5, 2, 4]
操作1后: [1, 5, 7, 4, 4]
操作2后: [0, 4, 6, 4, 4]
操作3后: [0, 4, 9, 7, 7]
结果一致,验证成功。
五、应用场景与总结
-
典型应用:
- 频繁的区间加减更新,最后进行单点或全数组查询。例如,记录航班预订座位、会议室日程安排、像素区域渲染等。
- 是解决“区间修改、单点查询”或“区间修改、区间查询”(结合前缀和)类问题的高效工具。
- 许多更复杂的数据结构(如线段树、树状数组的区间更新模式)其思想根源也类似于差分。
-
时间复杂度分析:
- 构建差分数组:
O(N),一次遍历即可。 - 单次区间更新:
O(1),只修改两个元素。 - 重建最终数组(查询):
O(N),计算一次前缀和。 - 对于
M次更新,总时间复杂度为O(N + M),远优于朴素的O(M * L)。
- 构建差分数组:
-
空间复杂度:
O(N),需要额外存储一个长度为N的差分数组。 -
关键要点:
- 差分是前缀和的逆运算。
- 区间更新的操作是成对出现的 (
diff[L] += k和diff[R+1] -= k)。 - 差分数组本身通常不直接体现最终值,需要通过前缀和还原。
通过掌握差分数组,你就能在常数时间内处理复杂的区间批量更新问题,这是算法竞赛和面试中一个非常实用的技巧。