应用
- 当题目要求
子数组和或者连续的子数组,可以考虑用prefix-sum sum(i~j) = preSum[j + 1] - preSum[i](当preSum的首位为0)- 常被应用于:
- 2-sum系列(和/差/余数/0)
- range sum
- 滑动窗口
- 单调队列
- 扫描线(少量)
- rolling hash(略)
2-Sum系列
2-sum 变体:和 / 差 / 余数 / 0
560. 和为 K 的子数组(Medium)
Solu:
- 最优解
O(N):- 2-sum的变种:计算出preSum[i]后,找两数之差
preSum[i] - preSum[j] == k (j < i)
- 2-sum的变种:计算出preSum[i]后,找两数之差
Code:
class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
preSum = 0
dic = {0: 1} # key: sum, value: frequency
res = 0
for num in nums:
preSum += num
res += dic.get(preSum - k, 0)
dic[preSum] = dic.get(preSum, 0) + 1
return res
974. 和可被 K 整除的子数组(Medium)
Solu:
- 当相同的
余数再次出现时,两次出现相同余数的位置i和j之间的subarray必定是可以背整除的
Code:
class Solution:
def subarraysDivByK(self, nums: List[int], k: int) -> int:
preSum, mod = 0, 0
count = 0
d = {0: 1}
for num in nums:
preSum += num
mod = preSum % k
count += d.get(mod, 0) # 相同的余数再次出现
d[mod] = d.get(mod, 0) + 1
return count
523. 连续的子数组和(Medium)
Solu:
modDict只保留每个mod第一次出现的indexmodDict初始化为{0 : -1}:-
index = -1:避免nums[0] = m * k就直接返回true了(invalid case)
-
Code:
class Solution:
def checkSubarraySum(self, nums: List[int], k: int) -> bool:
preSum, mod = 0, 0
modDic = {0: -1} # key: mod, value: preSum
for i, num in enumerate(nums):
preSum += num
mod = preSum % k
if mod in modDic:
if i - modDic[mod] > 1:
return True
else: # 只保留第一次出现的位置
modDic[mod] = i
return False
525. 连续数组(Medium)
Solu:
- 重点:把0翻转成-1
- if 在subarray中满足
#0 == #-1 == #1,then 这个subarray的sum == 0
- if 在subarray中满足
- dict只保留每个preSum第一次出现的index
- dict初始化为
{0 : -1}:为了不错过第一次出现#0 == #1的subarray
- dict初始化为
Code:
class Solution:
def findMaxLength(self, nums: List[int]) -> int:
preSum = 0
dic = {0: -1} # key: sum, value: index
ans = -1
for i, num in enumerate(nums): # 把0当成-1,1还是1,方便计算
preSum += (1 if num == 1 else -1)
if preSum in dic:
ans = max(ans, i - dic[preSum])
else: # 只保留preSum第一次出现的位置
dic[preSum] = i
return ans
Range Sum
「差分数组」 &「Lazy Propagation」(延迟更新):
- 对于一个数组
A[0...n-1],其差分数组B[0...n-1]定义为:- 当
i = 0时,B[i] = A[i] - 当
i > 0时,B[i] = A[i] - A[i - 1]
- 当
- 求差分数组的过程,就是求前缀和数组的逆过程
- 恢复为原数组时,
A[i] = B[0] + ... + B[i]
- 恢复为原数组时,
- 对原数组
A做更新操作[L, R, val],反映到差分数组B上,只用更新区间两端B[L]和B[R+1]的值:B[L]' = B[L] + valandB[R + 1]' = B[R + 1] - val-
if
i = L, thenB[i]' = A[i]' - A[i - 1]' = (A[i] + val) - A[i - 1] = B[i] + val -
if
L < i ≤ R, thenB[i]' = A[i]' - A[i - 1]' = (A[i] + val) - (A[i - 1] + val) = B[i]- 对中间部分的更新操作,由于相邻两项之间相减(求差分)而互相抵消
-
if
i = R + 1, thenB[i]' = A[i]' - A[i - 1]' = A[i] - (A[i - 1] + val) = B[i] - val
-
1D Array
370. 区间加法(Medium)
Solu:
- 差分数组 + lazy propogation,略
Code:
class Solution:
def getModifiedArray(self, length: int, updates: List[List[int]]) -> List[int]:
res = [0] * length
for l, r, val in updates: # 更新差分数组
res[l] += val
if r + 1 < length:
res[r + 1] -= val
# 求差分数组的前缀和 = 恢复为(更新后的)原数组
for i in range(1, length):
res[i] += res[i - 1]
return res
2D Marrix
304. 二维区域和检索 - 矩阵不可变(Medium)
Solu:
对于原矩阵A来说,其2D差分数组B可定义为:
B[i][j] = sum(A[0][0] ~ A[i][j]构成的矩形)B[i][j] = B[i-1][j] + B[i][j-1] + A[i][j] - B[i-1][j-1]sumRegion((a,b)->(c,d)) = B[c][d] - B[c][b-1] - B[a-1][d] + B[a-1][b-1]
PS:可以补dummy的0,避免讨论边界情况
Code:
class NumMatrix:
def __init__(self, matrix: List[List[int]]):
self.sumMatrix = [[0] * (len(matrix[0]) + 1) for _ in range(len(matrix) + 1)]
for i in range(len(matrix)):
for j in range(len(matrix[0])):
self.sumMatrix[i + 1][j + 1] = self.sumMatrix[i + 1][j] + self.sumMatrix[i][j + 1] - self.sumMatrix[i][
j] + matrix[i][j]
def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int:
return self.sumMatrix[row2 + 1][col2 + 1] - self.sumMatrix[row2 + 1][col1] - self.sumMatrix[row1][col2 + 1] + \
self.sumMatrix[row1][col1]
滑动窗口
滑动窗口的“左缩进”是不论subarray sum大小直接缩进:对于全正数是ok的,因为缩进一定会让sum减小
209. 长度最小的子数组(Medium)
Solu:
- 本题核心是『滑动窗口』;preSum作为一个rolling sum表示当前窗口内
nums[L:cur_idx]内部数字的和
Code:
code 1: 一个变量代表当前var preSum = sum(nums[l:i+1])
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
preSum, l = 0, 0
minLen = len(nums) + 1
for i, num in enumerate(nums):
preSum += num
while preSum >= target: # 此时preSum = sum(nums[l:i+1])
minLen = min(minLen, i - l + 1)
preSum -= nums[l]
l += 1
return minLen if minLen <= len(nums) else 0
code 2: 修改原数组,成为preSum Array
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
nums.insert(0, 0)
left = 0
ans = len(nums) + 1
for i in range(1, len(nums)):
nums[i] += nums[i - 1]
if nums[i] - nums[left] >= target:
while nums[i] - nums[left] >= target:
left += 1
ans = min(ans, i - left + 1)
return ans if ans != len(nums) + 1 else 0
单调队列
sliding widow还是monqueue,取决于nums中是否有负数
862. 和至少为 K 的最短子数组(Hard)❤️
Solu:
- 因为
K > 0,所以必定是一个较大的preSum - 一个较小的preSum-> 维护一个monoqueue保证window内sum值的单调性 - 正确性:不用担心被pop掉的
index可能会和未来的某个index'构成符合条件的subarray,因为monoque的单调递增性质,被保留下来的index''更可以和未来的某个index构成符合条件的subarray,且此时len(subarray[index'':index'])更小- if
preSum[i'] - preSum[i] >= k && preSum[i''] < preSum[i], thenpreSum[i'] - preSum[i''] >= k必然成立
- if
Code:
class Solution:
def shortestSubarray(self, nums: List[int], k: int) -> int:
nums = [0] + nums
for i in range(1, len(nums)):
nums[i] += nums[i - 1] # 构建preSum数组
res = len(nums) + 1
deq = collections.deque() # 双端队列存储前缀和的index
for i, num in enumerate(nums):
while deq and nums[deq[-1]] >= num: # 右出queue,保持monoqueue的单调递增性
deq.pop()
while deq and num - nums[deq[0]] >= k: # 左出queue,不断缩小window size
res = min(res, i - deq.popleft())
deq.append(i) # 把index放入monoqueue
return res if res <= len(nums) else -1
最大子序和/子序积
⚠️ 重要结论:最大子序列的和/积使用DP来做的!!而不是前缀和/积!!
- 最长/短子序列:滑动窗口(+前缀和)
- 最大子序列:DP
152. 乘积最大子数组(Medium)
Solu:DP
对于乘法,我们需要注意:负数乘以负数,会变成正数。
- 因此,我们需要维护两个变量:
当前的最大值(以nums[i]结尾)以及当前的最小值(以nums[i]结尾)- 最小值可能为负数,但没准下一步乘以一个负数,当前的最大值就变成最小值,而最小值则变成最大值了
Code:一维滚动数组优化
class Solution:
def maxProduct(self, nums: List[int]) -> int:
maxP, minP = nums[0], nums[0]
dp = nums[0]
for i in range(1, len(nums)):
tmp = maxP
maxP = max(maxP * nums[i], minP * nums[i], nums[i])
minP = min(tmp * nums[i], minP * nums[i], nums[i])
dp = max(dp, maxP)
return dp
Reference: