437. 路径总和 III(Path Sum III)

难度:中等 | 主题:二叉树、DFS、前缀和 ⭐常考

题目

给定一个二叉树的根节点和一个整数 targetSum,求该二叉树里节点值之和等于 targetSum 的路径的数目。路径不需要从根节点开始,也不需要在叶子节点结束,但必须是向下的(只能从父节点到子节点)。

示例:

 
输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
 
输出:3
 

三条路径:

  • 5 → 3

  • 5 → 2 → 1

  • -3 → 11


思路

先讲个故事:记账本上查流水

你在检查这个月的流水,想看有多少笔连续支出加起来正好等于 8 元

你的记账本长这样——从月初到月底逐日记账,但中间可能有几天没花钱(相当于路径不一定要从根开始)。你要找的是:任意连续几天的支出之和等于 8

这个小故事其实就是 前缀和 的思想(560和为K的子数组)。现在把它搬到了树上——“从根到当前节点”的路径相当于”从月初到今天”的账本。


引导式推导:从双重 DFS 到前缀和

第 1 层:双重 DFS(O(n²))

最直接的想法——每个节点都作为起点试一次:

 
def dfs(node):
 
    if not node: return
 
    check_sum(node, targetSum)  # 从当前节点往下找
 
    dfs(node.left)              # 换左子树节点当起点
 
    dfs(node.right)             # 换右子树节点当起点
 

对每个节点都要往下 DFS 一遍。n=5000 时还在可接受范围,再大就慢了。

第 2 层:前缀和优化(O(n))——灵光一现


graph TD

    subgraph 树路径

        R["10<br/>sum=10"] --> A["5<br/>sum=15"]

        A --> B["2<br/>sum=17"]

        B --> C["1<br/>sum=18"]

    end

    R -.-> pre1["prefix={0:1, 10:1, 15:1}"]

    A -.-> pre2["prefix={0:1, 10:1, 15:1}"]

    B -.-> pre3["prefix={0:1, 10:1, 15:1, 17:1}"]

    C -.-> pre4["当前 sum=18<br/>target=8<br/>18-8=10 → 存在!<br/>找到: 5→2→1"]

核心公式:

路径和 = 前缀和之差

curr_sum - prev_sum = targetSum

prev_sum = curr_sum - targetSum

如果 curr_sum - targetSum 在前缀和哈希表中出现过,说明存在一条以当前节点结尾的路径满足条件。

关键细节:

步骤说明
prefix_count[0] = 1处理从根开始的路径(前缀和为 0 的空路径)
遍历时 +1把当前前缀和记入哈希表
回溯时 -1必须恢复现场,避免路径分叉(路径只能从上到下,不能跨分支)

为什么回溯时一定要减 1?


graph TD

    subgraph 正确:回溯恢复

        R["10<br/>prefix: {0:1,10:1}"] --> L["5<br/>prefix: +{15:1}"]

        R --> R2["-3<br/>prefix: {0:1,10:1}<br/>{15:1}已被移除!<br/>不会误算"]

    end

如果离开左子树时不把 prefix[15] 减 1,右子树计算时会把左子树的路径也算进去——路径从上往下不能跨分支。


优化递进

方案时间空间说明
双重 DFSO(n²)O(h)每个节点为起点往下搜
前缀和优化O(n)O(h)哈希表存前缀和,回溯恢复

推荐写法

 
def pathSum(self, root, targetSum):
 
    from collections import defaultdict
 
    prefix_count = defaultdict(int)
 
    prefix_count[0] = 1        # 空路径前缀和为 0
 
    self.count = 0
 
    def dfs(node, curr_sum):
 
        if not node:
 
            return
 
        curr_sum += node.val
 
        # 关键:有多少条已有路径可以和当前路径拼出 target
 
        self.count += prefix_count[curr_sum - targetSum]
 
        prefix_count[curr_sum] += 1
 
        dfs(node.left, curr_sum)
 
        dfs(node.right, curr_sum)
 
        prefix_count[curr_sum] -= 1   # 回溯:离开当前节点,恢复现场
 
    dfs(root, 0)
 
    return self.count
 

复杂度

指标解释
时间O(n)每个节点一次
空间O(h)递归栈 + 哈希表最多存 h 个前缀和
双重 DFSO(n²)n=5000 时 ~2500 万次操作

实战考量

频率分析

出现在:字节/阿里/腾讯 二面或三面中档难度题。关键在于能否把一维数组的前缀和推广到树上,同时考察回溯恢复的意识。

延伸思考

Q:如果要求返回所有路径(不只是计数)呢?

A:哈希表不存 count 而是存节点列表。找到后回溯收集路径上的节点。注意空间会变大。

Q:为什么要用 defaultdict(int)

A:prefix_count[curr_sum - targetSum] 在 key 不存在时返回 0,不会抛 KeyError。

Q:prefix_count[0] = 1 是什么意思?

A:空路径的前缀和是 0。如果某个节点的 curr_sum == targetSum,那么 curr_sum - targetSum = 0,hash 表里有 1 表示”有一条空路径可以和你拼出 target”——即从根到当前节点的路径本身就是一种答案。

Q:空间能优化吗?

A:不能。必须存前缀和才能 O(n) 时间。如果空间敏感可以用双重 DFS 但 O(n²) 时间。

Q:这题和 560 什么关系?

A:560和为K的子数组 是数组版的前缀和,本题是把线性结构推广到了树形结构。核心思想完全一样,只是多了回溯恢复这一步。

易错点

  • prefix_count[0] = 1 忘了初始化

  • 回溯时不减 1 → 路径跨分支,结果偏大

  • curr_sum 用整数不会溢出,但注意 Python 的 int 是无限的

  • 用全局变量 self.count 而不是 nonlocal(在类方法里用 self 更简洁)


生活类比

前缀和找路径 → 记账查连续支出

你每个月记流水账,每天记下到今天的总花销(前缀和)。

想知道某段连续支出是否等于 target → 看”今天总花销 - target”是否等于之前某天的总花销。

在树上的区别是:每条分支单独记账。走完左分支回到分叉口时,要把左分支的账本收起来,不能带到右分支去(回溯恢复)。


相关题目

题目关系
560和为K的子数组一维前缀和,本题的数组基础版
112路径总和简单版:必须从根到叶子
437路径总和III本题

→ 返回题单:LeetCode学习路线图 > 五、二叉树

速记卡(面试闪卡)

Q1:一句话讲清「437. 路径总和 III(Path Sum III)」到底是什么?

A:把数组前缀和推广到树:哈希表记根到当前前缀和,curr-target 在表即一条路径。

Q2:一、题目:向下路径凑 target —— 怎么理解?

A:像数一数账本里连续几天支出恰好等于 8 元:给二叉树与 target,求向下路径(不必根始、不必叶终)和等于 target 的数目(Path Sum III)。

Q3:二、思路:前缀和搬上树 —— 怎么理解?

A:像记账查连续支出:今天总花销 - target = 之前某天总花销,就有一条(Prefix sum)。树上是 curr_sum - targetSum 在前缀哈希表里出现过,即一条以当前节点结尾的路径。

Q4:三、代码:哈希 + 回溯 —— 怎么理解?

A:像分支各自记账:dfs 时把 curr_sum 记入哈希、count += 表里 curr-target 的个数;离开节点必须 -1 恢复现场,否则路径跨分支(Backtracking)。prefix[0]=1 处理从根起的路径。

Q5:四、复杂度与实战:常考题 —— 怎么理解?

A:时间 O(n)、空间 O(h)。字节/阿里/腾讯中档题,考能否把一维前缀和推广到树并记得回溯(Prefix sum on tree)。返回路径本身则存节点列表。

Q6:核心速记主线有哪些?

  • 思路:前缀和 curr_sum - targetSum 在哈希表出现过即一条路径

  • 代码:dfs 记前缀和,count += prefix[curr-target],回溯 -1

  • 细节:prefix[0]=1 处理从根开始;defaultdict 避免 KeyError

  • 复杂度:时间 O(n) 空间 O(h);双重 DFS 为 O(n²)

  • 关联:560 和为K子数组的树形版

口诀

A:路径总和上树算,

前缀和来把账翻;

curr减target存在,

回溯减一别跨栏。

相关链接