【每日一题】差分数组

差分数组(Difference Array)是算法竞赛和面试中的高频技巧,核心思想是化区间操作为单点操作,将 O(n) 的区间修改优化到 O(1)。本文从原理出发,结合 5 道经典例题,带你彻底掌握差分数组。


一、核心原理:为什么差分数组这么高效?

1.1 定义与性质

对于一个数组 a[],其差分数组 diff[] 定义为:

复制代码
diff[i] = a[i] - a[i-1]  (i ≥ 2)
diff[1] = a[1]

核心性质

  • 前缀和还原a[i] = diff[1] + diff[2] + ... + diff[i]

  • 区间加法 O(1) :将 [l, r] 全部加上 x,只需修改两个点:

    python 复制代码
    diff[l]   += x
    diff[r+1] -= x

1.2 本质理解

操作 含义
diff[l] += x 从位置 l 开始,后面所有 元素都加了 x
diff[r+1] -= x 从位置 r+1 开始,后面所有元素都减回 x

两者配合,恰好让 [l, r] 区间内净增加 x,区间外不受影响。这就是容斥思想在一维上的体现。

1.3 关键限制

⚠️ 只能"先修改,后查询",无法边修改边查询中间结果。如果需要动态查询,请使用线段树或树状数组。


二、例题 1:区间更新

📌 题目描述

给定长度为 n 的数组 a[1..n],执行 m 次操作,每次将 [x, y] 区间内所有元素加上 z。最后输出整个数组。

输入格式

复制代码
n m
a[1] a[2] ... a[n]
x1 y1 z1
x2 y2 z2
...
xm ym zm

🔍 思路分析

这是差分数组最直白的模板题。暴力做法每次修改 O(n),总复杂度 O(m×n),会超时。使用差分数组可将每次修改降到 O(1),最后统一 O(n) 还原。

💻 代码实现(Python)

python 复制代码
while True:
    try:
        n, m = map(int, input().split())
        a = list(map(int, input().split()))
        
        # 1. 构建差分数组(0-based)
        diff = [0] * (n + 1)
        diff[0] = a[0]
        for i in range(1, n):
            diff[i] = a[i] - a[i - 1]
        
        # 2. 区间修改 → 差分数组单点修改
        for _ in range(m):
            x, y, z = map(int, input().split())
            x, y = x - 1, y - 1  # 转为0-based
            diff[x] += z
            diff[y + 1] -= z     # 注意越界检查
        
        # 3. 前缀和还原原数组
        a[0] = diff[0]
        for i in range(1, n):
            a[i] = a[i - 1] + diff[i]
        
        print(' '.join(map(str, a)))
    except:
        break

✅ 运行示例

输入

复制代码
5 2
1 3 5 6 7
2 4 5
1 3 2

执行过程

步骤 diff 数组 说明
初始 [1, 2, 2, 1, 1, 0] 原数组 [1,3,5,6,7] 的差分
操作1 [2,4]+5 [1, 7, 2, 1, -4, 0] diff[1]+=5, diff[4]-=5
操作2 [1,3]+2 [3, 7, 2, 1, -4, -2] diff[0]+=2, diff[3]-=2
前缀和还原 [3, 10, 12, 13, 9, 7] 最终数组

输出

复制代码
3 10 12 13 9

三、例题 2:航班预订统计

📌 题目描述

n 个航班,编号 1n。给定 bookings[i] = [first, last, seats],表示从航班 firstlast(包含)每个航班预订 seats 个座位。返回每个航班最终的预订总数。

示例

复制代码
输入:bookings = [[1,2,10],[2,3,20],[2,5,25]], n = 5
输出:[10, 55, 45, 25, 25]

🔍 思路分析

这是差分数组的经典变形题。题目绕了个弯,本质就是:初始全0数组,对多个闭区间执行加法,求最终结果。

注意航班编号从 1 开始,而数组下标从 0 开始,需要转换。

💻 代码实现

python 复制代码
class Solution:
    def corpFlightBookings(self, bookings: List[List[int]], n: int) -> List[int]:
        diff = [0] * (n + 1)  # 多开一位防止 r+1 越界
        
        for first, last, seats in bookings:
            diff[first - 1] += seats      # 左端点(转0-based)
            diff[last] -= seats           # 右端点的下一位
        
        # 前缀和还原,diff[n] 是哨兵,不参与结果
        res = [0] * n
        res[0] = diff[0]
        for i in range(1, n):
            res[i] = res[i - 1] + diff[i]
        
        return res

✅ 详细推演

以示例数据为例:

航班 1 2 3 4 5
预订1 [1,2]+10 +10 +10
预订2 [2,3]+20 +20 +20
预订3 [2,5]+25 +25 +25 +25 +25
总计 10 55 45 25 25

差分数组变化过程

复制代码
初始: [0,  0,  0,  0,  0,  0]
操作1: [10, 0, -10, 0,  0,  0]   # diff[0]+=10, diff[2]-=10
操作2: [10, 20, -10, -20, 0,  0] # diff[1]+=20, diff[3]-=20
操作3: [10, 45, -10, -20, 0, -25]# diff[1]+=25, diff[5]-=25

前缀和: 10 → 55 → 45 → 25 → 25

🎯 关键技巧

  • 多开一位数组diff 长度设为 n+1,避免 diff[last] 越界判断
  • 闭区间转差分[first, last] 对应 diff[first-1] += seats, diff[last] -= seats

四、例题 3:拼车

📌 题目描述

车上有 capacity 个空座位,只能单向行驶。给定 trips[i] = [num, from, to],表示在 from 接上 num 人,在 to 放下。判断能否完成所有行程。

示例

复制代码
输入:trips = [[2,1,5],[3,3,7]], capacity = 4
输出:false
解释:在位置3时,车上有 2+3=5 人,超过容量4

🔍 思路分析

这题是差分数组的判断型应用 。注意区间是左闭右开 [from, to)------在 to 位置乘客已经下车了。

我们不需要完全还原数组,只需在求前缀和的过程中实时检查是否超载。

💻 代码实现

python 复制代码
class Solution:
    def carPooling(self, trips: List[List[int]], capacity: int) -> bool:
        # 根据数据范围,最大位置是1000
        diff = [0] * 1001
        
        for num, from_i, to_i in trips:
            diff[from_i] += num   # 上车
            diff[to_i] -= num     # 下车(开区间,到to时已经下车)
        
        # 求前缀和过程中检查容量
        current = 0
        for i in range(1001):
            current += diff[i]
            if current > capacity:
                return False
        
        return True

✅ 推演过程

位置 0 1 2 3 4 5 6 7
行程1 [2,1,5) +2 -2
行程2 [3,3,7) +3 -3
diff 0 2 0 3 0 -2 0 -3
前缀和(车上人数) 0 2 2 5 5 3 3 0

位置3时车上5人 > capacity=4,返回 false

🎯 关键技巧

  • 开区间处理diff[to] -= num,不是 diff[to+1]
  • 边还原边判断:不需要存储完整结果数组,节省空间

五、例题 4:二维差分------子矩阵加法

📌 题目描述

输入 n×m 矩阵,执行 q 次操作,每次将子矩阵 (x1,y1)(x2,y2) 内所有元素加上 c。输出最终矩阵。

输入样例

复制代码
3 4 3
1 2 2 1
3 2 2 1
1 1 1 1
1 1 2 2 1
1 3 2 3 2
3 1 3 4 1

🔍 思路分析

二维差分是一维的扩展。核心公式:

复制代码
diff[i][j] = a[i][j] - a[i-1][j] - a[i][j-1] + a[i-1][j-1]

子矩阵 (x1,y1)(x2,y2) 加上 c

python 复制代码
diff[x1][y1]       += c
diff[x1][y2+1]     -= c
diff[x2+1][y1]     -= c
diff[x2+1][y2+1]   += c

记忆口诀左上角加,右上角减,左下角减,右下角加(容斥原理,防止重复计算)

💻 代码实现

python 复制代码
n, m, q = map(int, input().split())

# 下标从1开始,多开一圈
a = [[0] * (m + 2) for _ in range(n + 2)]
diff = [[0] * (m + 2) for _ in range(n + 2)]

# 读入矩阵
for i in range(1, n + 1):
    row = list(map(int, input().split()))
    for j in range(1, m + 1):
        a[i][j] = row[j - 1]

# 构建二维差分数组
for i in range(1, n + 1):
    for j in range(1, m + 1):
        diff[i][j] = a[i][j] - a[i-1][j] - a[i][j-1] + a[i-1][j-1]

# q次区间修改
for _ in range(q):
    x1, y1, x2, y2, c = map(int, input().split())
    diff[x1][y1]       += c
    diff[x1][y2 + 1]   -= c
    diff[x2 + 1][y1]   -= c
    diff[x2 + 1][y2 + 1] += c

# 二维前缀和还原
for i in range(1, n + 1):
    for j in range(1, m + 1):
        diff[i][j] += diff[i-1][j] + diff[i][j-1] - diff[i-1][j-1]
        print(diff[i][j], end=' ')
    print()

✅ 详细推演

初始矩阵 a

复制代码
1  2  2  1
3  2  2  1
1  1  1  1

操作1(1,1)(2,2) 加 1
操作2(1,3)(2,3) 加 2
操作3(3,1)(3,4) 加 1

diff 数组变化(仅展示关键位置):

复制代码
操作1后 diff:
 0  0  0  0  0
 0  1  0 -1  0
 0  0  0  0  0
 0 -1  0  1  0
 0  0  0  0  0

操作2后 diff:
 0  0  0  0  0
 0  1  0  1  -2
 0  0  0  0  0
 0 -1  0  1  0
 0  0  0  0  0

操作3后 diff:
 0  0  0  0  0
 0  1  0  1  -2
 0  0  0  0  0
 0 -1  0  1  0
 0  1  1  1  1  <- 最后一行+1,但y2+1=5越界不显示

前缀和还原后输出

复制代码
2  3  4  1
4  3  4  1
2  2  2  2

🎯 关键技巧

  • 下标从1开始 :方便处理边界,避免 i-1 越界
  • 多开一圈数组diff 大小设为 (n+2)×(m+2),防止 x2+1y2+1 越界
  • 容斥公式 :右下角 +c 是为了补回被多减的部分

六、例题 5:字母移位 II

📌 题目描述

给定字符串 s(下标从0开始)和 shifts[i] = [start, end, direction]

  • direction = 1:将 [start, end] 区间内字母向后移1位(a→b, z→a
  • direction = 0:将 [start, end] 区间内字母向前移1位(b→a, a→z

返回所有操作后的字符串。

示例

复制代码
输入:s = "abc", shifts = [[0,1,0],[1,2,1],[0,2,1]]
输出:"ace"
解释:
- [0,1,0]: "abc" → "aac" (ab后移→aa)
- [1,2,1]: "aac" → "abd" (ac前移→bd) 不对,重新算...
实际上 [0,1,0] 表示 a,b 向前移: a→z? 不对,direction=0是向前

🔍 思路分析

这题是差分数组的字符版本 。每个操作对区间进行 +1-1,最后统一计算每个位置的累计偏移量,再对26取模。

💻 代码实现

python 复制代码
class Solution:
    def shiftingLetters(self, s: str, shifts: List[List[int]]) -> str:
        n = len(s)
        diff = [0] * (n + 1)  # 差分数组
        
        for start, end, direction in shifts:
            delta = 1 if direction == 1 else -1
            diff[start] += delta
            diff[end + 1] -= delta
        
        # 前缀和计算每个位置的累计偏移
        res = []
        offset = 0
        for i in range(n):
            offset += diff[i]
            # 计算新字符:先转数字(0-25),加偏移,再转回字符
            num = (ord(s[i]) - ord('a') + offset) % 26
            res.append(chr(num + ord('a')))
        
        return ''.join(res)

✅ 推演过程

复制代码
s = "abc", shifts = [[0,1,0],[1,2,1],[0,2,1]]

初始:     a    b    c
diff:    [0,   0,   0,   0]

操作1 [0,1,0](-1): 
diff:    [-1,  0,   1,   0]   # diff[0]-=1, diff[2]+=1

操作2 [1,2,1](+1):
diff:    [-1,  1,   1,  -1]   # diff[1]+=1, diff[3]-=1

操作3 [0,2,1](+1):
diff:    [0,   1,   1,  -1]   # diff[0]+=1, diff[3]-=1

前缀和还原偏移量:
i=0: offset=0,  'a' + 0 = 'a'
i=1: offset=1,  'b' + 1 = 'c'  
i=2: offset=2,  'c' + 2 = 'e'  (c→d→e)

结果: "ace"

🎯 关键技巧

  • 差分存储偏移量:不是直接修改字符,而是记录每个位置的"净偏移"
  • 取模运算% 26 处理循环移位,+26%26 防止负数
  • 统一转换:所有操作完成后,一次性计算最终字符

七、差分数组模板总结

7.1 一维模板

python 复制代码
def difference_array(n, operations):
    """
    operations: List of [l, r, val] (0-based, closed interval)
    returns: modified array
    """
    diff = [0] * (n + 1)  # 多开一位
    
    for l, r, val in operations:
        diff[l] += val
        diff[r + 1] -= val
    
    # 前缀和还原
    res = [0] * n
    res[0] = diff[0]
    for i in range(1, n):
        res[i] = res[i - 1] + diff[i]
    
    return res

7.2 二维模板

python 复制代码
def diff2d(n, m, operations):
    """
    operations: List of [x1, y1, x2, y2, val] (1-based, closed)
    """
    diff = [[0] * (m + 2) for _ in range(n + 2)]
    
    for x1, y1, x2, y2, val in operations:
        diff[x1][y1]       += val
        diff[x1][y2 + 1]   -= val
        diff[x2 + 1][y1]   -= val
        diff[x2 + 1][y2 + 1] += val
    
    # 前缀和还原
    for i in range(1, n + 1):
        for j in range(1, m + 1):
            diff[i][j] += diff[i-1][j] + diff[i][j-1] - diff[i-1][j-1]
    
    return diff

八、适用场景与对比

场景 推荐算法 时间复杂度
多次区间修改 + 最终查询 差分数组 O(n + m)
单点修改 + 区间查询 前缀和 O(1)查询
区间修改 + 区间查询(在线) 树状数组/线段树 O(log n)
二维子矩阵修改 + 最终查询 二维差分 O(n×m + q)

九、学习心得

差分数组的精髓在于"记录变化,而非状态"。就像记账时不记余额,只记每笔进出,最后统一算总账。

三句话记住差分

  1. 一维两点:diff[l] += x, diff[r+1] -= x
  2. 二维四角:左上+,右上-,左下-,右下+
  3. 先改后查:所有修改完成后,统一前缀和还原

掌握差分数组后,你会发现很多看似复杂的区间问题,本质上都是"套路题"。

相关推荐
罗超驿1 小时前
2.LeetCode 1089. 复写零——双指针解法学习笔记
java·算法·leetcode
Struggle_97552 小时前
算法知识-数学
算法
khalil10202 小时前
代码随想录算法训练营Day-41动态规划08 | 121. 买卖股票的最佳时机、122.买卖股票的最佳时机II、123.买卖股票的最佳时机III
数据结构·c++·算法·leetcode·动态规划
量子炒饭大师2 小时前
【优化算法:双指针算法刷题宝典】—— 三数之和
算法·优化算法·双指针·三数之和
1104.北光c°2 小时前
Leetcode215 三种写法完成数组中的第K个最大元素 【hot100算法个人笔记】【java写法】
java·笔记·程序人生·算法·leetcode·排序算法·快速选择
AIpanda8882 小时前
当数字员工与熊猫智汇协作,如何实现销售潜力的全面提升?
算法
无限进步_2 小时前
【C++】AVL树完全解析:从平衡因子到四种旋转
c语言·开发语言·数据结构·c++·后端·算法·github
zubylon3 小时前
前端 RAG:把文档检索接到聊天页
前端·人工智能·算法