LeetCode 17 - 前缀和
437 路径总和 III
给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
方法一:DFS
我们利用 DFS 穷举所有可能,访问每一个结点 node:
- 以 node 为起点,检测以它为根向下延伸的满足条件的路径条数。在此过程中,首先看结点 node 的值是否已经等于 targetSum,是则合法路径加一;然后以 node 为路径起点继续向下搜索。
- 计算 node 的左右子树中满足条件的路径数。(对左右子树递归调用)
- 将每个结点的合法路径条数相加即为答案。
这个 DFS 涉及到了两层递归:
- 在主方法里面有递归调用:对当前结点计算后,需要对左右孩子递归计算。
- 在 DFS 方法里面也有递归调用:确定了以当前节点为路径起点后,需要向下延伸,对左右孩子递归计算。
int pathSum(TreeNode root, int targetSum) {
if(root == null) return 0;
// 计算 **以当前节点为起点** 且满足条件的路径条数
int result = findPathNum(root, targetSum);
// 递归计算 **左右子树** 中满足条件的路径条数
result += pathSum(root.left, targetSum);
result += pathSum(root.right, targetSum);
return result;
}
// 以 node **为起点** 的满足条件的路径条数
int findPathNum(TreeNode node, int targetSum) {
if(root == null) return 0;
int result = 0;
int curVal = node.val;
// 当前节点可以自成一条路径
if(curVal == targetSum) result++;
// 以当前节点为起点,向下延伸的满足条件的路径条数
result += findPathSum(root.left, targetSum - curVal);
result += findPathSum(root.right, targetSum - curVal);
return result;
}
方法二:前缀和
方法一中存在很多重复计算,我们可以将已计算过的路径和缓存起来,这就用到了前缀和的概念:结点的前缀和定义为 从根结点到当前节点的路径上所有结点的和(包括当前结点)。
注意前缀和的一个特性:在一条路径上,如果两个点的前缀和相同,则这两个点之间的元素总和为 0。进一步,如果 A 和 B 在同一条路径上,A 在 B 之前,且 A 和 B 的前缀和分别是 sum - target 和 sum,那么从 A.next 到 B 的路径和为 target。

// key 是前缀和,value 是对应前缀和出现的次数
HashMap<Integer, Integer> sumCount = new HashMap<>();
int pathSum(TreeNode root, int targetSum) {
sumCount.put(0, 1);
return dfs(root, targetSum, 0);
}
// 计算从根结点到每一个结点的前缀和
// 对遍历到的每个点计算以它为终点的满足条件的路径数
int dfs(TreeNode node, int target, int curSum) {
if(node == null) return 0;
int result = 0;
// 更新前缀和
curSum += node.val;
// 寻找当前这条路径上前缀和为 curSum-target 的结点数量
result += sumCount.getOrDefault(curSum - target, 0);
// 更新路径上当前前缀和的结点个数
// 更新操作必须在上一行的获取操作之后
sumCount.put(curSum, sumCount.getOrDefault(curSum, 0) + 1);
// 进入下一层
result += dfs(node.left, target, curSum);
result += dfs(node.right, target, curSum);
// 以当前结点为根的子树统计完毕,
// 去除当前节点的前缀和对应节点的数量
sumCount.put(curSum, sumCount.get(curSum) - 1);
return result;
}
560 和为 K 的子数组
给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。
方法:前缀和
定义前缀和数组 sum ,其中 sum[i] 表示子数组 [0...i] 中所有数的和。那么「[j..i] 这个子数组的和为 k 」 这个条件可以转化为:\(sum[i]-sum[j-1]=k\)。那么遍历到当前位置 i 并计算出 sum[i] 后,只需要检查满足前缀和为 sum[i]-k 的数有多少个即可。
int subarraySum(int[] nums, int k) {
int count = 0, sum = 0;
HashMap<Integer, Integer> sumCount = new HashMap<>();
sumCount.put(0, 1); // 这是为 sum[i]==k 这种情况准备的
for(int i = 0; i < nums.length; i++) {
sum += nums[i]; // 计算当前位置前缀和
if(sumCount.containsKey(sum-k))
count += sumCount.get(sum-k);
sumCount.put(sum, sumCount.getOrDefault(sum, 0) + 1);
}
return count;
}
上面的 sumCount.put(0, 1) 是为了统计 sum[i] 刚好等于 k 这种情况,因为此时 sum[i]-k 刚好就等于 0 。
1248 统计优美子数组
给你一个整数数组 nums 和一个整数 k。如果某个连续子数组中 恰好有 k 个奇数数字,我们就认为这个子数组是「优美子数组」。请返回这个数组中 「优美子数组」 的数目。
方法:前缀和
用 oddCount[i] 表示子数组 [0..i] 中奇数的个数,则有:
那么「[j..i] 这个子数组里的奇数个数恰好为 k 」这个条件可以转化为:
即下标 j 需要满足:
所以统计以 i 结尾的优美子数组时只要统计有多少个奇数个数前缀和为 oddCount[i]-k 的位置即可。且因为每个位置之和前一个位置有关,所以不必真地创建一个数组,而只需要使用两个变量即可。
int numOfSubarrays(int[] nums, int k) {
int n = nums.length;
// prefix[oddCount] 表示前缀奇数个数为 oddCount 的位置有几个
int[] prefix = new int[n + 1];
int oddSum = 0, result = 0;
prefix[0] = 1; // 表示 prefix[oddCount] == k 的情况
for (int i = 0; i < n; i++) {
oddSum += nums[i] & 1;
result += (oddSum >= k ? prefix[oddSum - k] : 0);
prefix[oddSum] += 1;
}
return result;
}

浙公网安备 33010602011771号