LeetCode 560, 437 前缀和变体

72 阅读3分钟

1.前缀和模板

单纯的前缀和模板是好写的,给定一个数组a,要求得到一个数组prefix_sum,记录a的前缀和

a = [1, 2, 3, 4, 5, 6, 7, 8, 9]
s = 0
prefix_sum = []
for num in a:
    s += num
    prefix_sum.append(s)

print(prefix_sum)

image.png

2. LeetCode 560

比如LeetCode 560这道题:

给定一个数组,要求返回该数组中和为k的子数组的个数

image.png

那这道题就不好做,因为和为k的子数组的起点不一定是给定数组的第一个结点,有可能是给定数组的中间某段。

满足sum1 + k = s,其中s指前缀和,sum1是开头一段前缀和,k是中间一段数组的和。

从前往后遍历原数组,每走到一个元素就计算一下当前的前缀和,此时s是固定住的,如果当前段内有和为k的子数组,那么该子数组出现的次数就应该是sum1出现的次数,k不是前缀和,但是sum1是前缀和,这样就可以把中间一段的次数转换为sum1这个子前缀和出现的次数。

另外还有一个值得注意的点就是,有可能出现sum1为0的情况,也就是说和为k的子数组并不是出现在中间,而是正好从原数组的开头开始算,这种情况也是成立的,因此得考虑sum1为0,此时k直接等于s

还可以用defaultdict(int)来初始化prefix_sum,顺便提一嘴,prefix_sum最好用字典存储,而不是用数组存储,因为我们需要统计每个前缀和出现的次数,数组只能记录前缀和,如果要统计某个前缀和出现的次数,那就得

  1. 遍历一遍数组,判断sum1是否在数组中
  2. 如果在数组中,还得prefix_sum.count(sum1) 这两步做下来首先时间复杂度高,遍历一遍数组是O(n);其次代码写起来也比较麻烦。

但是如果用字典实现,那么if sum1 in prefix_sum可以直接判断sum1是否在前缀和中出现;字典的value也可以记录sum1出现的次数。

以nums = [1,2,3], k = 3为例,[1, 2], [3],出现两次,答案应该是2

from collections import defaultdict

nums = [1, 2, 3]
k, ans = 3, 0
s = 0

# 从collections把defaultdict引进来,相比于直接用{0:1}定义数组,
# defaultdict的方式可以自动初始化不存在的key的value为0,从而省去if else的逻辑
prefix = defaultdict(int)
prefix[0] = 1
for num in nums:
    s += num
    if s - k in prefix:
        ans += prefix[s - k]
    prefix[s] += 1
    
print(ans)

image.png

3. LeetCode 437

image.png

这题可以看成是560的进阶版本,它将数组中找一截和为k改成了在二叉树中找一截满足和为k。

二叉树本身就要比数组复杂一些,所以初步判断应该是递归 + 前缀和

有了560的理解之后,唯一一个不太好理解的地方就是递归遍历完左右子树之后还要prefix_sum[s] -= 1 我对这句代码的理解就是:遍历另一条从上往下的路径的时候,prefix_sum在前一条路径中的值是不能用的, 所以回溯的时候要减1

先定义二叉树

class TreeNode:
    def __init__(self, val, left = None, right = None):
        self.val = val
        self.left = left
        self.right = right
        
root = TreeNode(10)
root.left = TreeNode(5)
root.left.left = TreeNode(3)
root.left.left.left = TreeNode(3)
root.left.left.right = TreeNode(-2)
root.left.right = TreeNode(2)
root.left.right.right = TreeNode(1)

root.right = TreeNode(-3)
root.right.right = TreeNode(11)

算法解题

from collections import defaultdict

k = 8

prefix_sum = defaultdict(int)
prefix_sum[0] = 1
ans, s = 0, 0

''' 
递归到某个点,看的就是从根节点到该点的路径上是否存在某一小段的和为k
'''
def dfs(root, s):
    if root == None: return
    global ans
    
    s += root.val
    ans += prefix_sum[s - k]
    
    prefix_sum[s] += 1
    if root.left: dfs(root.left, s)    
    if root.right: dfs(root.right, s)
    ''' 
    回溯的时候需要剪枝,剪枝出现在递归调用的后面
    '''
    prefix_sum[s] -= 1
    
dfs(root, 0)
print(ans)

image.png