类型题Ⅳ:前缀和

文章目录

前缀和

前缀和 就是数组 第 0 项当前项 的总和。比如数组 nums,那么它的前缀和 prefixSum[x] 就表示 nums 从第 0 项到第 x 项的总和。
p r e f i x S u m [ x ] = n u m s [ 0 ] + n u m s [ 1 ] + . . . + n u m s [ x ] prefixSum[x] = nums[0] + nums[1] + ...+ nums[x] prefixSum[x]=nums[0]+nums[1]+...+nums[x]
数组 nums 中任意一项就是两个相邻前缀和之差
n u m s [ x ] = p r e f i x S u m [ x ] − p r e f i x S u m [ x − 1 ] nums[x] = prefixSum[x] - prefixSum[x-1] nums[x]=prefixSum[x]prefixSum[x1]
数组 nums 中第 i 项到第 j 项之和:
n u m [ i ] + . . . + n u m [ j ] = p r e f i x S u m [ j ] − p r e f i x S u m [ i − 1 ] num[i] + ... + num[j] = prefixSum[j] - prefixSum[i-1] num[i]+...+num[j]=prefixSum[j]prefixSum[i1]
特别注意,由于 i 可以为 0,i-1 会出现数组越界,所以前缀和数组一般需要在 0 位置扩充一个 0,便于计算数组 nums 中第 0 项到第 k 项之和。

前缀和数组每一项 dp[i] 都是原数组从第 0 项到第 i 项的总和,所以如果 j 大于 i,那么 dp[j] 就一定是包含 dp[i] 的。

相关题目

560. 和为K的子数组

思路一暴力解法,双重循环,每次固定一个 i,用 j 循环往前走,每到一个位置判断子数组和是否为 target,是则 count + 1。时间复杂度为 O ( n 2 ) O(n^2) O(n2),会超时。

class Solution:
    def subarraySum(self, nums: List[int], k: int) -> int:
        if not nums: return 0
        n = len(nums)
        count = 0
        for i in range(n):
            sum = 0
            for j in range(i, -1, -1):
                sum += nums[j]
                if sum == k: count += 1
        return count

思路二前缀和解法。假设目标值为 k,对于任意子数组的和(假设为 x),为了使得这个子数组和能满足目标值 k,就要找另一个子数组的和(假设为 y),使得 y - x = k。

可以创建一个与原数组大小相同的新数组 dp,即前缀和数组。该数组每个位置上都保存了原数组从 0 到对应位置的所有元素的和。从后向前遍历前缀和数组,每到一个位置,如果不等于目标值 target,则向前寻找是否有 dp[j] - target,找到一个 count 数就加 1。

class Solution:
    def subarraySum(self, nums: List[int], k: int) -> int:
        n = len(nums)
        prefix = [0]  # 前缀和数组要额外加一个0
        # 创建前缀和数组
        for i in range(n):
            prefix.append(prefix[i] + nums[i])
        # 遍历计数
        count = 0
        for i in range(n, 0, -1):
            target = prefix[i] - k
            for j in range(i-1, -1, -1):
                if prefix[j] == target:
                    count += 1
        return count

这样遍历其实仍然是双重循环,时间复杂度并没有降低,运行一下依然超时。

思路二改进:考虑用空间换时间,使用 前缀和 + 哈希表。用哈希表存储前缀和,键为 “前缀和的值”,值为 “该前缀和的出现次数”。从左向右遍历前缀和数组,将当前前缀和的值存入哈希表中,如果已经存在则直接加 1,并在哈希表中查找是否有键等于 “当前前缀和 - target”,若存在则给计数器 count 加上该键对应的值。

j > i,则我们的目标是 prefix[j] - prefix[i] = target

  • 如果从左向右遍历前缀和数组,每次来到的位置都是 j,在哈希表里的查找目标就是 pre[i] = pre[j] - target
  • 如果从右向左遍历前缀和数组,每次来到的位置都是 i,在哈希表里的查找目标就是 pre[j] = target + pre[i]
class Solution:
    def subarraySum(self, nums: List[int], k: int) -> int:
        n = len(nums)
        prefix = [0]
        count, dict = 0, {0:1}
        # 从左向右遍历
        for i in range(n):
            pre = prefix[i] + nums[i]
            prefix.append(pre)
            # 查找目标值,若无则返回-1
            c = dict.get(pre - k, -1) # 查找目标pre[i] = pre[j]-target
            if c != -1:
                count += c
            # 当前前缀和添加到哈希表(注意先找目标值,再添加到哈希表)
            if pre not in dict:
                dict[pre] = 1
            else:
                dict[pre] += 1
        return count

只需要一层循环,时间复杂度为 O(n)。用哈希表加速运算的思路适合 不关心具体的解,仅关心解的个数 的情况。


1248. 统计「优美子数组」

思路:前缀和 + 差分。约定数组 prepre[i] 表示以第 i 个元素为结尾的子数组 [nums[0]...nums[i]] 中奇数的个数,那么 pre[i] 可以由前置状态 pre[i-1] 计算得到,即:pre[i] = pre[i-1] + 1 if nums[i] 是奇数 else pre[i-1]

那么题目要求 [j...i] 这个子数组的奇数个数恰好为 k,可以将这个条件转化为 pre[i] - pre[j-1] == k

在实际求解时,从左向右计算 pre 数组中各个位置的状态(即奇数个数),然后将这个个数登记在哈希表里,生成键值对:“奇数个数-出现次数”,每次登记时,顺便在哈希表看一看有没有目标值 pre[j-1],如果有就可以将个数累加在最终的答案 sum 上。pre 数组并不需要真的建立,只需要维护前一个值即可。

时间复杂度 O ( n ) O(n) O(n),遍历长度为 n 的数组
空间复杂度 O ( n ) O(n) O(n),需要哈希表记录奇数出现频次,最坏情况下哈希表长度等于原数组

class Solution:
    def numberOfSubarrays(self, nums: List[int], k: int) -> int:
        n = len(nums)
        pre = 0
        res, dict = 0, {0:1}
        for i in range(n):
            pre = pre + 1 if nums[i] % 2 != 0 else pre
            dict[pre] = dict.get(pre, 0) + 1
            res += dict.get(pre - k, 0)
        return res

长度最小的子数组:给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的连续子数组,并返回其长度。如果不存在符合条件的连续子数组,返回 0.

思路:前缀和 + 二分查找。额外创建一个数组用于存储原数组的前缀和,由于题目规定原数组均为正整数,所以该前缀和数组可保证是递增的。前缀和数组 sums[i] 表示从 nums[0]nums[i-1] 的元素和。对于每个元素下标,可通过二分查找得到一个大于或等于 i 的最小下标 j,使得 sums[j] - sums[i-1] >= s,此时更新新子数组的最小长度。

时间复杂度 O ( n l o g n ) O(nlog_n) O(nlogn)
空间复杂度 O ( n ) O(n) O(n)

class Solution:
    def minSubArrayLen(self, s: int, nums: List[int]) -> int:
        if not nums:
            return 0
        
        n = len(nums)
        ans = n + 1
        sums = [0]
        for i in range(n):
            sums.append(sums[-1] + nums[i])
        
        for i in range(1, n + 1):
            target = s + sums[i - 1]
            bound = bisect.bisect_left(sums, target)  # 二分查找
            if bound != len(sums):
                ans = min(ans, bound - (i - 1))
        
        return 0 if ans == n + 1 else ans

另外,最开始写的暴力法肯定会超时:

class Solution:
    def minSubArrayLen(self, s: int, nums: List[int]) -> int:
        n = len(nums)
        dp = [n+1 for _ in range(n)]
        res = n+1
        for i in range(n):
            cur = 0
            for j in range(i,-1,-1):
                cur += nums[j]
                if cur >= s:
                    res = min(res, i-j+1)
        if res < n+1:
            return res
        else:
            return 0
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不吃饭就会放大招

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值