leetcode hot100 437. 路径总和 III medium 前缀和 DFS


用前缀和来求区间和

结构中,从根节点到当前节点 的路径也可以看作一个数组


方法:

  • curSum:当前节点前缀和
  • target:中间节点到当前节点的和
  • curSum - target:中间节点前缀和
python 复制代码
                      cursum
   <--------------------------------------------------------------------------------------------------------------------------->
       cursum-target            target
   |<------------------------------------------------------>|<------------------------------------------------------>|
---+--------------------+--------------------+----
 根节点             中间节点             当前节点

问题转换为:检查当前节点之前的节点的前缀和 ,有没有等于cursum-target的?

所以需要一个哈希表,记录:当前路径上所有出现过的前缀和,及其出现的次数


  1. 为什么要记录"当前路径上所有出现过的前缀和,及其出现的次数 ",只记录出没出现过不行吗?
    不行,因为:路径上的前缀和可能会重复出现(尤其是当树中存在节点值为 0 或正负抵消的情况时)
python 复制代码
#  假设 targetSum = 8
根节点
  |
 (A) 值: 3  [前缀和: 3]
  |
 (B) 值: 0  [前缀和: 3]  <-- 重复出现了!
  |
 (C) 值: 5  [前缀和: 8]
  |
 (D) 值: 0  [前缀和: 8]
  |
 (E) 值: 0  [前缀和: 8]  <-- 走到这里时,currSum = 8
 
# 走到节点 E 时, 前缀和 3 出现了 2次(节点 A 和节点 B)
# 这意味着,从节点 A 到 E 是一条和为 8 的路径;从节点 B 到 E 也是一条不同的和为 8 的路径。
# 如果只记录"出现过",就会丢掉其中一条路径,导致计数错误。

如果树中全是正数,前缀和是严格递增的,确实不会重复。但题目没有这个限制:

  • 节点值为 0:会导致前缀和原地踏步。
  • 正负抵消:比如 5 -> -2 -> 2,前缀和会经历 5 -> 3 -> 5。
  1. 为什么要"销账"?在回溯时 "删除当前节点的前缀和"?
    当前节点向下递归时,左右子节点需要当前节点的cursum
    但递归结束,当前节点收到返回值时:
    • 如果当前节点是它的父节点的左子节点,则下次是向右递归,右子节点不需要左子节点的前缀和(题目要求:路径必须是垂直向下,不能跨越左右子树,不能先上再下)
    • 如果当前节点是它的父节点的右子节点,则下次是向上回溯,那么父节点也可能是爷爷节点的左子树,当递归到爷爷节点的右子树时,右子树也不需要左子树的前缀和
    • 即,销账是为了当当前节点去右子树递归时,不影响右子树;当当前节点作为上面某个根节点的左子树时,不影响上面某个根节点的右子树
python 复制代码
# targetSum =8
		5 (root)
       / \
      3   4
     /     \
    10      3 (node_X)
  1. 沿着左边走:5 -> 3 -> 10。前缀和会出现 5, 8, 18
  2. 处理完 10 和 3 后,回溯。此时哈希表里的 18, 8 都被删除了,只剩下根节点的 5
  3. 进入右边:5 -> 4 -> 3
  4. 走到右下角的 3 (node_X) 时,currSum = 12
  5. 查账:找 12 - 8 = 4。哈希表里没有 4,路径数为 0。这是正确的。

哈希表记录:使用一个哈希表,存储当前路径上所有出现过的前缀和,及其出现的次数

python 复制代码
# 哈希表存储:{前缀和: 出现的次数}
# 哈希表的初始值 {0: 1}:这是为了处理从根节点开始且路径和恰好等于 targetSum 的情况。此时 curr_sum - targetSum = 0,正好对应初始值。

DFS 遍历这棵树

python 复制代码
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right

from collections import defaultdict
class Solution:
    def pathSum(self, root: Optional[TreeNode], targetSum: int) -> int:

        # 哈希表存储:{前缀和: 出现的次数}
        cnt = defaultdict(int)
        cnt[0] = 1

        def dfs(node, curr_sum):  # curr_sum 是从根节点到当前节点的值
            if not node:
                return 0

            # # 1. 当前路径的前缀和
            curr_sum += node.val

            # 2.当前路径前缀和为 "curr_sum - targetSum"的中间节点(路径)个数
            # 关键:先定义局部变量 count 并初始化,不然直接 count+=会报错
            count = cnt[curr_sum - targetSum]
            
            # 3. 当前节点前缀和 "curr_sum" 存入字典
            cnt[curr_sum] += 1
            
            # 递归,左右子树继续找
            # 左右子树,都需要"当前节点的前缀和"
            count += dfs(node.left, curr_sum)
            count += dfs(node.right, curr_sum)

            # 当前节点可能是它的父节点的左子节点
            # 当前节点递归结束后,当前节点的父节点,要继续递归它的右子树,右边不需要左边的前缀和
            cnt[curr_sum] -= 1

            return count


        # 直接返回根节点触发的递归结果
        return dfs(root, 0)

时间复杂度:O(N)O(N)O(N),每个节点仅遍历一次。

空间复杂度:O(N)O(N)O(N),哈希表在最坏情况下(树呈链状)需要存储 NNN 个前缀和。

相关推荐
菜鸡儿齐1 小时前
leetcode-和为k的子数组
java·算法·leetcode
im_AMBER2 小时前
Leetcode 122 二叉树的最近公共祖先 | 二叉搜索树迭代器
学习·算法·leetcode·二叉树
52Hz1182 小时前
力扣46.全排列、78.子集、17.电话号码的字母组合
python·leetcode
菜鸡儿齐2 小时前
leetcode-最小覆盖子串
算法·leetcode·职场和发展
Tisfy4 小时前
LeetCode 3714.最长的平衡子串 II:前缀和(一二三分类)
算法·leetcode·前缀和·字符串·题解
WBluuue4 小时前
数据结构与算法:dp优化——树状数组/线段树优化
数据结构·c++·算法·leetcode·动态规划
YGGP4 小时前
【Golang】LeetCode 53. 最大子数组和
leetcode
踩坑记录5 小时前
leetcode hot100 236.二叉树的最近公共祖先 medium dfs 递归
leetcode·深度优先
烟花落o5 小时前
【数据结构系列01】时间复杂度和空间复杂度:消失的数字
数据结构·算法·leetcode·刷题