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 - targetsum,那么从 A.next 到 B 的路径和为 target

image

// 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[j1]=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] 中奇数的个数,则有:

oddCount[i]=oddCount[i1]+(nums[i]&1)

那么「[j..i] 这个子数组里的奇数个数恰好为 k 」这个条件可以转化为:

oddCount[i]oddCount[j1]==k

即下标 j 需要满足:

oddCount[j1]==oddCount[i]k

所以统计以 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;
}
posted @   李志航  阅读(42)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示
主题色彩