Leetcode-前缀和系列

mhxin

1. 两数之和
560. 和为K的子数组
1248. 统计「优美子数组」
437. 路径总和 III

1. 两数之和

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。

示例:

给定 nums = [2, 7, 11, 15], target = 9

因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]

题目难度: Easy

对于第一次接触这种类型的题目的人而言, 最能想到的方式就是类似冒泡排序的暴力算法, 时间复杂度为$O(n^2)$, 空间复杂度为$O(1)$.

但题目要求每一个元素只能使用一次, 显然上面的做法并不能满足要求, 那么怎样才可以每个元素只使用一次呢

回到最直观的想法, 我们需要两两元素进行判断: nums[i] + nums[j] = target?, 如果每个元素只能使用一次, 换一种思路, nums[j] = target - nums[i], 可以想到将target - nums[i]提前保存下来 由于本题返回的是索引, 于是自然想到使用字典来保存, 后续只要判断nums[j]是否在字典中就可以了.

顺着思路, 可以写出如下的代码

def twoSum(self, nums: List[int], target: int) -> List[int]:
    if not nums or len(nums) < 2:
        return
        
    # 使用字典保存target - nums[i]
    prefix_sum = {}
    
    n = len(nums)
    
    for i in range(n):
        if nums[i] in prefix_sum:
            return [prefix_sum[nums[i]], i]
            
        prefix[target-nums[i]] = i

时间复杂度$O(n)$, 空间复杂度$O(n)$

560. 和为K的子数组

给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。

示例 1 :

输入:nums = [1,1,1], k = 2
输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。

说明 :

数组的长度为 [1, 20,000]。
数组中元素的范围是 [-1000, 1000] ,且整数 k 的范围是 [-1e7, 1e7]。

一开始看到这个题目, 确实不知如何下手, 主要问题在于:

  1. 子数组中可能包含负数.
  2. 并没有限定子数组长度.

现假设子数组开始位置为$i$, 结束位置为$j$, 即$nums[i]+...+nums[j]=target$

$nums[i]+...+nums[j]=sum(nums_{0,...,j})-sum(nums_{0,...,i-1})$

因此, 我们可以用一个字典保存所有可能的连续子数组(上式中右边部分)和, 字典的值为和等于k的子数组个数.

这样, 可以一次遍历, 即可得到和为k的所有子数组个数.
按照这个思路, 可以写出如下的代码


def subarraySum(nums, k):
    if not nums:
        return 0
        
    n = len(nums)
    res = 0
    
    # 从索引为0的位置到当前位置的数组和
    prefix_sum = 0
    
    # 保存了所有前缀和及其对应的子数组个数
    dicts = {0: 1}
    
    for i in range(n):
        prefix_sum += nums[i]
        
        
        if prefix_sum - k in dicts:
            res += dicts[prefix_sum - k]

        #        
        dicts[prefix_sum] = dicts.get(prefix_sum, 0) + 1
        
    return res
        

时间复杂度$O(n)$, 空间复杂度$O(n)$

1248. 统计「优美子数组」

给你一个整数数组 nums 和一个整数 k。

如果某个 连续 子数组中恰好有 k 个奇数数字,我们就认为这个子数组是「优美子数组」。

请返回这个数组中「优美子数组」的数目。

示例 1:

输入:nums = [1,1,2,1,1], k = 3
输出:2
解释:包含 3 个奇数的子数组是 [1,1,2,1] 和 [1,2,1,1] 。

示例 2:

输入:nums = [2,4,6], k = 1
输出:0
解释:数列中不包含任何奇数,所以不存在优美子数组。

示例 3:

输入:nums = [2,2,2,1,2,2,1,2,2,2], k = 2
输出:16

提示:

  • 1 <= nums.length <= 50000
  • 1 <= nums[i] <= 10^5
  • 1 <= k <= nums.length

该题和上一题(560题)的思路基本一致, 只是由元素和变为了奇数个数, 本质上都是一样的.

def numberOfSubarrays(nums, k):
    if not nums or len(nums) < k:
        return 0
        
    res = 0
    
    # 值表示所有可能的奇数个数, 及其对应的子数组的个数
    dicts = {0: 1}
    
    # 表示从索引为0的位置到当前位置, 奇数的个数
    prefix_sum = 0
    
    n = len(nums)
    
    for i in range(n):
        if nums[i] % 2:
            prefix_sum += 1
            
        if prefix_sum - k in dicts:
            res += dicts[prefix_sum - k]
            
        if prefix_sum not in dicts:
            dicts[prefix_sum] = 0
        
        # 奇数个数为prefix_sum的子数组个数加1
        dicts[prefix_sum] += 1
        
    return res

时间复杂度$O(n)$, 空间复杂度$O(n)$

437. 路径总和 III

给定一个二叉树,它的每个结点都存放着一个整数值。

找出路径和等于给定数值的路径总数。

路径不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

二叉树不超过1000个节点,且节点数值范围是 [-1000000,1000000] 的整数。

示例:

root = [10,5,-3,3,2,null,11,3,-2,null,1], sum = 8

      10
     /  \
    5   -3
   / \    \
  3   2   11
 / \   \
3  -2   1

返回 3。和等于 8 的路径有:

1.  5 -> 3
2.  5 -> 2 -> 1
3.  -3 -> 11

这题可以看作是前缀和在二叉树中的应用, 思路还是一样的

def pathSum(self, root, sum):
    
    self.dicts = {0: 1}
    self.res = 0
    def helper(root, prefix_sum, sum):
        if not root:
            return 0
            
        prefix_sum += root.val
        
        if prefix_sum - sum in self.dicts:
            self.res += self.dicts[prefix_sum - sum]
            
        
        self.dicts[prefix_sum] = self.dicts.get(prefix_sum, 0) + 1
        
        helper(root.left, prefix_sum, sum)
        
        helper(root.right, prefix_sum, sum)
        
        # Note: 回到上一层时, 需要将当前的前缀和对应的路径数目减1      
        self.dicts[prefix_sum] -= 1
    
    helper(root, 0, sum)
    return res    

时间复杂度$O(n)$, 空间复杂度$O(n)$

总结

前缀和的方法通常用于解决序列中满足条件的连续子序列的数目, 对于这类问题, 通常会使用字典来记录每个前缀和及其出现的次数, 这样每次只需要判断(当前前缀和 - target) 是否在字典中, 如果在, 则结果加上该前缀和对应的个数.否则将其添加到字典中.

值得注意的是, 前缀和字典通常会初始化为{0, 1}, 因为如果满足条件的连续子数组是从0开始时, 当前前缀和等于连续子数组和, (当前前缀和 - target) = 0 并不一定存在字典中.

以上内容, 如有错误或不当之处, 可以直接在评论区指出. 恳请赐教, 谢谢.

阅读 2k
82 声望
12 粉丝
0 条评论
82 声望
12 粉丝
文章目录
宣传栏