✨前缀和数组的应用场景
前缀和数组经常用来求解「子数组之和」相关的问题
💡思考一个很简单的场景:对于原数组 nums 的任意一个子数组,如何在 O(1) 的时间内得到其元素和?
👉用数组 nums 的前缀和数组 preSums 就很方便算出来了
适配上图的公式:sum(nums[p+1...q]) = preSums[q] - preSums[p] (p < q)
💡 进一步思考:公式中的 p 最小是多少?
答案:最小是 -1,你可能会疑问索引 -1 不是越界了嘛
这是因为这样才能通过公式算出 nums[0...x] 的子数组和,我们不妨把 preSums 想象成在它的最左侧有一个索引为-1,值为 0 的一个元素,这样一来, nums[0...x] = preSums[x] - PreSums[-1] = preSums[x] 就也能通过公式算出来了
523.连续的子数组和
1. 题目描述
给你一个整数数组 nums 和一个整数 k ,编写一个函数来判断该数组是否含有同时满足下述条件的连续子数组:
- 子数组大小 至少为 2 ,且
- 子数组元素总和为 k 的倍数。
如果存在,返回 true ;否则,返回 false 。
如果存在一个整数 n ,令整数 x 符合 x = n * k ,则称 x 是 k 的一个倍数。0 始终视为 k 的一个倍数。
示例 1:
输入:nums = [23,2,4,6,7], k = 6
输出:true
解释:[2,4] 是一个大小为 2 的子数组,并且和为 6 。
示例 2:
输入:nums = [23,2,6,4,7], k = 6
输出:true
解释:[23, 2, 6, 4, 7] 是大小为 5 的子数组,并且和为 42 。 42 是 6 的倍数,因为 42 = 7 * 6 且 7 是一个整数。
示例 3:
输入:nums = [23,2,6,4,7], k = 13
输出:false
提示:
- 1 <= nums.length <= 10^5
- 0 <= nums[i] <= 10^9
- 0 <= sum(nums[i]) <= 2^31 - 1
- 1 <= k <= 2^31 - 1
签名函数
def checkSubarraySum(self, nums: List[int], k: int) -> bool:
2. 思路/题解
提示:前缀和数组 preSums 通常搭配哈希表一起使用
根据本题要求
- 如果
preSums[q]−preSums[p]
为k
的倍数,且q−p ≥ 2
,则可以返回True
那如何进行「判断preSums[q]−preSums[p]
是否为 k
的倍数」这一过程 ?
先验知识:当 preSums[q]−preSums[p] 为 k 的倍数时,也就是 (preSums[q] - preSums[p]) % k = 0 时,preSums[q] % k = preSums[p] % k【preSums[q] 和 preSums[q] 除以 k 的余数相同】
👉利用「先验知识」中 preSums[q] % k = preSums[p] % k 这个条件,这道题就可以使用哈希表存储 preSums[i] % k 的每个余数第一次出现的下标
重点步骤
- 定义哈希表为 valToIndex , 先将 key=0, val=-1 存入 valToIndex 中,这样如果遇到 preSums[i] 本身就是 K 的倍数的情况,就不会被忽略了
前面也说了,可以将 preSums 想象成在它的最左侧有一个索引为 -1,值为 0 的元素,题目说 0 始终为 k 的一个倍数,所以 preSums[-1] % k = 0,理应放入 valToIndex 中
- 然后遍历 preSums 中的每个数,当遍历到 preSums[i] 时,
- 如果 preSums[i] % k 在 valToIndex 中已存在,则取出该余数在哈希表中对应的下标 preIndex( preSums[i] % k == preSums[preIndex] % k ),所以 nums 从下标 preIndex + 1 到下标 i 的子数组的元素和一定为 k 的倍数,如果 i - preIndex >=2,则找到了一个大小至少为 2 且元素和为 k 的倍数的子数组,返回 true 就好了
- 如果 preSums[i] % k 在哈希表 valToIndex 中不存在,则将当前余数和当前下标 i 的键值对存入哈希表中,继续后面的遍历
后续思考点:由于哈希表存储的是每个余数第一次出现的下标,因此当遇到重复的余数时,根据当前下标和哈希表中存储的下标计算得到的子数组长度是「以当前下标结尾的所有子数组」中「满足元素和为 k 的倍数的子数组长度」中的最大值。本题只要满足子数组最大长度 >= 2,即存在符合要求的子数组
3. 具体代码
class Solution:
def checkSubarraySum(self, nums: List[int], k: int) -> bool:
n = len(nums)
# 构造前缀和数组
preSumArr = [0] * n
preSumArr[0] = nums[0]
for i in range(1,n):
preSumArr[i] = preSumArr[i-1] + nums[i]
# 遍历前缀和数组,构造哈希表
# key: preSumArr 中元素 %k 得到的余数 , val: 首次出现 key 这个余数所对应的 preSumArr 元素的下标
valToIndex = {}
valToIndex[0] = -1 # 初始化最早出现 0 这个余数的下标,设为 -1,但凡 preSumArr[i] %k = 0 说明 nums[0...i] 这个子数组和即为 k 的倍数
for i in range(n):
val = preSumArr[i] % k
if val not in valToIndex:
valToIndex[val] = i
else:
if i - valToIndex[val] >= 2:
return True
return False
560.和为 K 的子数组
1. 题目描述
给你一个整数数组 nums
和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。
子数组是数组中元素的连续非空序列。
示例 1:
输入:nums = [1,1,1], k = 2
输出:2
示例 2:
输入:nums = [1,2,3], k = 3
输出:2
提示:
- 1 <= nums.length <= 2 * 10^4
- -1000 <= nums[i] <= 1000
- -10^7 <= k <= 10^7
函数签名:
def subarraySum(self, nums: List[int], k: int) -> int:
2. 思路/题解
2.1 基本方法
有了上一题的经验,对「子数组的和为 k 」这几个字就有敏感度了,知道要用前缀和数组去做,通过读题,稍作思考一下,应该就会有如下的思路:
def subarraySum(self, nums: List[int], k: int) -> int:
n = len(nums)
# 构建前缀和数组 preSum
build(preSums[0...n+1]) # preSums[i] 代表数组 nums 从下标 0 到 i-1 的元素和
# 存储结果的变量
res = 0
# 对 preSum 遍历,每次 preSum[left] 为基准,看 preSum[left+1:n+1] 中是否有元素 preSum[j] 与 preSum[left] 的差为 k,
# 有的话就说明 nums[left...j-1] 子数组之和为 k,然后更新结果
for left in range(len(nums)):
for right in range(left, len(nums)):
if preSum[right + 1] - preSum[left] == k:
res += 1
return res
但这个思路的时间复杂度为 O(N²),十分低效
此时,可以想到,而前缀和数组往往可以和哈希表搭配着一起使用,来优化效率...
2.2 哈希表优化
看下面这个例子...
假设输入为:nums=[1, -1, 0] , k = 0
那我们的预期输出应该为:3
因为找到满足元素之和等于 0 的子数组有:[1, -1, 0] , [1, -1], [0]
在思考该如何利用哈希表之前,我们可以先看看前缀和数组 preSums 是什么样子的
对于 nums:[1, -1, 0], preSum : [0, 1, 0, 0] (此处的 preSums[i] 是 nums[-1...i-1] 的子数组和,i >= 0, 假设 nums[-1] 为 0)
下面画一张图来说明怎么从 preSum 中找出所有满足元素之和为 0 的子数组
看上面这张图,以遍历到了 preSums[3],也就是最后一个 0 为例 ,那此时要做的事,无非就是在 preSums 中找这个 0 前面还有几个 0,官方点说就是 preSums[3] 的前面有没有 preSums[i] , 使 preSums[3] - preSums[i] = 0,发现 preSums[0] 和 preSums[2] 这两个都满足,于是 res += 2
那我们又是怎么知道前面满足条件的有多少个呢?上面这个 2 怎么得到的呢?
如果在 hashmap 中存放了前缀和数组中每个不同的值出现的次数,那我们在 preSums 中遍历到 preSums[i] 时,就能在 hashmap 中查到「之前遍历过的 preSums 元素中,值为 preSums[i] - k 的元素出现过几次」,然后把这个次数加到结果变量,再给 hashmap 中 preSums[i] 这个 key 的 value 上加上 1 即可
3. 具体代码 (hashmap 优化后)
class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
# 看到子数组之和,肯定是利用前缀和数组
n , res = len(nums), 0
preSums = [0] * (n + 1)
count = {0:1}
for i in range(1, n+1):
preSums[i] = preSums[i-1] + nums[i-1]
need = preSums[i] - k
if need in count:
res += count[need]
count[preSums[i]] = count.get(preSums[i], 0) + 1
return res
525.连续数组
1. 题目描述
给定一个二进制数组 nums
, 找到含有相同数量的 0
和 1
的最长连续子数组,并返回该子数组的长度。
示例 1:
输入: nums = [0,1] 输出: 2 说明: [0, 1] 是具有相同数量 0 和 1 的最长连续子数组。
示例 2:
输入: nums = [0,1,0] 输出: 2 说明: [0, 1] (或 [1, 0]) 是具有相同数量0和1的最长连续子数组。
提示:
1 <= nums.length <= 10^5
nums[i]
不是0
就是1
2. 思路/题解
这道题的本质和 560 一样,因为题目可以转化为「将原数组中的所有 0 转换成 -1 后,求和为 0 的最长子数组」,但是要注意的是,这里哈希表存储的内容和 523 几乎一样, 这道题哈希表存的是 preSums[i] 第一次出现的下标
求解示意图
重点步骤
- 构造前缀和数组 preSums,长度为 n+1,第一个位置存 0,从第 2 个位置开始存 nums 的前缀和
- 如果 preSums[right] - preSums[left] == 0 , 即 preSums[right] = preSums[left], 那就说明 nums[left...right-1] 的子数组之和为 0 ,子数组长度为 right - left , 然后需要的话就更新结果
3. 具体代码
class Solution:
def findMaxLength(self, nums: List[int]) -> int:
# 将 0 视为 -1,那本题就是找和为 0 的最长子数组
# 构造前缀和数组
n = len(nums)
preSums = [0] * (n+1)
for i in range(1, n+1):
preSums[i] = preSums[i-1] + (-1 if nums[i-1] == 0 else 1)
res = 0
valToIndex = {0: 0}
for i in range(1, n+1):
if preSums[i] in valToIndex:
tmplen = i - valToIndex[preSums[i]]
res = max(tmplen, res)
else:
valToIndex[preSums[i]] = i
return res