数组和链表代表着计算机最基本的两种存储形式:顺序存储和链式存储,所以他俩可以算是最基本的数据结构。
数组链表的主要算法技巧是双指针,双指针⼜分为中间向两端扩散的双指针、两端向中间收缩的双指针、快慢指针。
此外,数组还有前缀和和差分数组也属于必知必会的算法技巧。本节主要讲解这两种算法。
1 前缀和与差分数组
前缀和是指某序列的前 n 项和,可以把它理解为数学上的数列的前 n 项和,差分数组是与前缀和数组所对应的一种逆操作,类似于求导和积分,也就是说,对差分数组求前缀和,可以得到原数组,同样的,对前缀和数组求差分,也可以得到原数组。合理的使用前缀和与差分,可以将某些复杂的问题简单化。
1.1 前缀和
假设有一个序列 A,前缀和为 S。根据概念很容易知到公式
S [ i ] = ∑ j = 1 i A [ j ] S[i]=\displaystyle \sum_{j=1}^iA[j] S[i]=j=1∑iA[j]
如何求区间 [ l , r ] [l,r] [l,r] 的和呢?
s u m [ l , r ] = s [ r ] − s [ l − 1 ] sum[l,r]=s[r]-s[l-1] sum[l,r]=s[r]−s[l−1]
1.2 差分数组
设原数组为 A[i],差分数组为 diff[i],则:
d i f f [ i ] = { A [ i ] i = 1 A [ i ] − A [ i − 1 ] i ≥ 2 diff[i]=\begin{cases} A[i]&i=1\\ A[i]-A[i-1]&i\geq2 \end{cases} diff[i]={ A[i]A[i]−A[i−1]i=1i≥2
差分数组的性质是:
- 如果对区间 [ l , r ] [l,r] [l,r] 进行修改,只需修改 d i f f [ l ] , d i f f [ r + 1 ] diff[l], diff[r+1] diff[l],diff[r+1](diff[l]加上修改值,diff[r+1] 减去修改值)
- A [ i ] = ∑ j = 1 i B [ j ] A[i]=\displaystyle \sum_{j=1}^{i}B[j] A[i]=j=1∑iB[j](通过 B [ i ] = A [ i ] − A [ i − 1 ] B[i]=A[i]-A[i-1] B[i]=A[i]−A[i−1] 证明)
- S [ x ] = ∑ i = 1 x A [ i ] = ∑ i = 1 x ∑ j = 1 i d i f f [ j ] = ∑ i = 1 x ( x − i + 1 ) ∗ d i f f [ i ] S[x]=\displaystyle \sum_{i=1}^{x}A[i]=\displaystyle \sum_{i=1}^{x} \displaystyle \sum_{j=1}^{i}diff[j]=\displaystyle \sum _{i=1}^{x}(x-i+1)*diff[i] S[x]=i=1∑xA[i]=i=1∑xj=1∑idiff[j]=i=1∑x(x−i+1)∗diff[i]$
当我们希望对原数组的某一个区间 [ i , j ] [i, j] [i,j] 施加一个增量 inc 时,差分数组 d d d 对应的变化是: d [ i ] 4 d[i] 4 d[i]4 增加 inc, d [ j + 1 ] d[j+1] d[j+1] 减少inc,并且这种操作是可以叠加的。
下面举个例子:
差分数组是一个辅助数组,从侧面来表示给定某一数组的变化,一般用来对数组进行区间修改的操作。
还是上面那个表里的例子,我们需要进行以下操作:
- 将区间[1,4]的数值全部加上3
- 将区间[3,5]的数值全部减去5
很简单对吧,你可以进行枚举。但是如果给你的数据量是 1 × e 5 1\times e^5 1×e5,操作量 1 × e 5 1\times e^5 1×e5,限时1000ms你暴力枚举能莽的过去吗?慢到你怀疑人生直接。这时我们就需要使用到差分数组了。
其实当你将原始数组中元素同时加上或者减掉某个数,那么他们的差分数组其实是不会变化的。
利用这个思想,咱们将区间缩小,缩小的例子中的区间 [1,4] 吧这是你会发现只有 d[1] 和 d[5] 发生了变化,而 d[2], d[3], d[4]却保持着原样,
进行下一个操作,
这时我们就会发现这样一个规律,当对一个区间进行增减某个值的时候,他的差分数组对应的区间左端点的值会同步变化,而他的右端点的后一个值则会相反地变化,其实这个很好理解。
本部分参考自:差分详解+例题
也就是说,当我们需要对原数组的不同区间施加不同的增量,我们只要按规则修改差分数组即可。所以,差分数组的主要适⽤场景是频繁对原始数组的某个区间的元素进⾏增减,但只能是区间元素同时增加或减少相同的数的情况才能用。
有 n n n 个数, m m m 个操作,每一次操作,将 x y x~y x y 区间的所有数增加 z z z;最后有 q q q 个询问,每一次询问求出 x y x~y x y 的区间和。设原数组为 A [ i ] A[i] A[i]。其步骤为:
- 先求出差分数组 B [ i ] = A [ i ] − A [ i − 1 ] B[i]=A[i]−A[i−1] B[i]=A[i]−A[i−1]
- 在根据 m m m 个造作修改 B [ i ] B[i] B[i]
- 求修改后的 A [ i ] = A [ i − 1 ] + B [ i ] A[i]=A[i−1]+B[i] A[i]=A[i−1]+B[i]
- 求前缀和 S [ i ] = S [ i − 1 ] + A [ i ] S[i]=S[i−1]+A[i] S[i]=S[i−1]+A[i]
- 最后输出区间和 s u m [ x , y ] = S [ y ] − S [ x − 1 ] sum[x,y]=S[y]−S[x−1] sum[x,y]=S[y]−S[x−1]
前缀和主要适用的场景是原始数组不会被修改的情况下,适用于快速、频繁地计算一个索引区间内的元素之和以及频繁查询某个区间的累加;差分数组的主要适用场景是频繁对原始数组的某个区间的元素进行增减。
2 常见题型
2.1 题库列表
303. 区域和检索 - 数组不可变:一维前缀和
304. 二维区域和检索 - 矩阵不可变:二维前缀和
370. 区间加法:差分数组
1109. 航班预订统计:差分数组
1094. 拼车:差分数组
303. 区域和检索 - 数组不可变
题目描述:
一维前缀和
class NumArray:
def __init__(self, nums: List[int]):
self.nums_array = [0] # 便于计算累加和
for i in range(len(nums)):
self.nums_array.append(self.nums_array[i] + nums[i]) # 计算nums累加和
def sumRange(self, left: int, right: int) -> int:
return self.nums_array[right+1] - self.nums_array[left]
304. 二维区域和检索 - 矩阵不可变
题目描述:
二维前缀和
class NumMatrix:
def __init__(self, matrix: List[List[int]]):
m, n = len(matrix), len(matrix[0]) # 矩阵的行和列
self.pre_sum = [[0]*(n+1) for _ in range(m+1)] # 构造一维前缀和矩阵
for i in range(m):
for j in range(n):
self.pre_sum[i+1][j+1] = self.pre_sum[i+1][j] + self.pre_sum[i][j+1] - self.pre_sum[i][j] + matrix[i][j]
def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int:
return (self.pre_sum[row2+1][col2+1] - self.pre_sum[row1][col2+1] - self.pre_sum[row2+1][col1] + self.pre_sum[row1][col1])
370. 区间加法
题目描述:假设你有一个长度为n的数组,初始情况下所有的数字均为0,你将会被给出k个更新的操作。其中,每个操作会被表示为一个三元组: [startIndex, endIndex, inc],你需要将子数组 A[startIndex, endIndex](包括startlndex和endIndex)增加 inc。
请你返回 k 次操作后的数组。
class Solution:
def getModifiedArray(self, length: int, updates: List[List[int]]) -> List[int]:
diff = [0] * (length+1) # 末尾多个0,防止越界
for update in updates:
start, end, inc = update[0], update[1], update[2]
diff[start] += inc
diff[end + 1] -= inc
for i in range(1, length):
diff[i] += diff[i - 1] # 对差分数组求前缀和便可得到原数组
return diff[:-1]
1109. 航班预订统计
题目描述:这里有 n 个航班,它们分别从 1 到 n 进行编号。有一份航班预订表 bookings ,表中第 i i i 条预订记录 b o o k i n g s [ i ] = [ f i r s t i , l a s t i , s e a t s i ] bookings[i] = [first_i, last_i, seats_i] bookings[i]=[firsti,lasti,seatsi] 意味着在从 f i r s t i first_i firsti 到 l a s t i last_i lasti(包含 f i r s t i first_i firsti 和 l a s t i last_i lasti)的 每个航班 上预订了 s e a t s i seats_i seatsi 个座位。请你返回一个长度为 n 的数组 answer,里面的元素是每个航班预定的座位总数。
class Solution:
def corpFlightBookings(self, bookings: List[List[int]], n: int) -> List[int]:
diff = [0] * (n+1)
for booking in bookings:
start, end, inc = booking[0], booking[1], booking[2]
diff[start] += inc
if end < n: # 没在末尾添加0,要判断一下边界
diff[end+1] -= inc
for i in range(1, n+1):
diff[i] += diff[i-1]
return diff[1:]
1094. 拼车
题目描述:车上最初有 capacity 个空座位。车只能向一个方向行驶(也就是说,不允许掉头或改变方向),给定整数 capacity 和一个数组 trips , t r i p [ i ] = [ n u m P a s s e n g e r s i , f r o m i , t o i ] trip[i] = [numPassengers_i, from_i, to_i] trip[i]=[numPassengersi,fromi,toi] 表示第 i i i 次旅行有 n u m P a s s e n g e r s i numPassengers_i numPassengersi 乘客,接他们和放他们的位置分别是 f r o m i from_i fromi 和 t o i to_i toi。这些位置是从汽车的初始位置向东的公里数。当且仅当你可以在所有给定的行程中接送所有乘客时,返回 true,否则请返回 false。
class Solution:
def carPooling(self, trips: List[List[int]], capacity: int) -> bool:
diff = [0] * (1001) # 题目中最多有1001个车站
max_station = 0 # 找到车站数
for trip in trips:
inc, start, end = trip[0], trip[1], trip[2]
diff[start] += inc
diff[end] -= inc # 第end站乘客已经下车,这里就不用end+1
max_station = max(max_station, end)
for i in range(1, max_station+1): # 进行区间求和
diff[i] += diff[i-1]
if max(diff[:max_station]) > capacity:
return False
return True
欢迎各位大佬一起来学习前缀和与差分数组,希望本篇可以让你对前缀和和差分的概念及使用技巧有更清晰的理解,欢迎继续补充!
参考
- 数组+常见题型与解题策略:https://blog.csdn.net/qq_42647903/article/details/120594856
- 差分详解+例题:https://blog.csdn.net/qq_44786250/article/details/100056975
- 数组(三)-- LC[370]&[1109]&[1094] 区间加法:https://blog.csdn.net/xq151750111/article/details/129243272?spm=1001.2014.3001.5502