第5章|二叉树与递归

Unit 08 · LeetCode #437 路径总和 III

← Unit 07
仅本次页面有效
U08
Path Sum IIIPrefix Sum · Frequency · Backtracking

当前前缀减去目标值,
定位路径开始之前的前缀。

本题不再返回一条最佳路径,而是统计任意起点的向下路径。前缀差把“重新选择起点”压缩成一次哈希查询;回溯则保证频次表只描述当前根到节点的祖先链,不把兄弟分支拼成不存在的路径。

已恢复上次学习位置环节、轨迹、槽位、迁移代码和编辑器高度均已恢复。
知识桥梁|从“路径最优”转向“路径计数”
Unit 07:子树贡献

每个节点向父节点返回一个可组合状态,当前节点负责结算答案候选。

状态含义改变 →
Unit 08:祖先前缀

递归携带当前前缀和,哈希表保存活动祖先链上各前缀出现次数。

贯穿本单元的审查问题当前哈希表中的每个前缀,是否真的位于当前节点的祖先链上?查询发生时,当前前缀是否尚未登记?
LeetCode #437 原题路径可从中间开始,但只能沿父到子方向

题目要求

给定二叉树根节点 root 和整数 targetSum,返回路径和等于目标值的路径数量。路径不必从根开始,也不必在叶子结束,但必须沿父节点到子节点方向向下。

class Solution: def pathSum( self, root: Optional[TreeNode], targetSum: int ) -> int: pass
  • 树中节点数为 [0,1000];
  • 节点值可为负数,不能使用滑动窗口。

示例与方法

root = [10,5,-3,3,2,null,11,3,-2,null,1]
targetSum = 8
输出:3
三条路径:5→3、5→2→1、-3→11

DFS 维护根到当前节点的前缀和 S。若祖先前缀 S-target 出现过,则对应区间和为目标值。进入节点登记前缀,离开节点撤销。时间 O(n),活动状态与递归栈 O(h)。

当前前缀 S

从虚拟起点到当前节点的累计和。

旧前缀 S−target

该位置之后到当前节点的路径和恰好为目标。

频次而非集合

同一前缀可在活动链上出现多次,每次对应一个不同起点。

回溯作用域

离开节点撤销当前前缀,使哈希表始终只代表一条祖先链。

官方示例树|targetSum = 8观察当前活动祖先链
蓝色:当前祖先链绿色:刚找到的目标路径橙色:当前节点红色:已离开分支的幽灵前缀
前缀差needed = current_sum − targetSum

每个旧前缀 needed 对应一条以当前节点结尾的目标路径。

作用域prefix_count 只含当前祖先链

进入登记,离开撤销,兄弟分支之间不能共享局部前缀。

当前前缀—
−
目标值8
=
需要的旧前缀—

当前祖先链上的前缀

prefix_count 频次表

调用栈

尚未进入代码轨迹
当前 node—
current_sum0
本节点新增路径0
累计路径数0

频次表不变量

prefix_count 只包含虚拟前缀到当前节点父亲之间的活动前缀。

操作顺序

查询旧前缀 → 登记当前前缀 → 递归左右 → 撤销当前前缀。

当前活动路径—
当前前缀0
需要旧前缀—
哈希表有效键1
累计答案0
忘记回溯会虚构跨兄弟分支路径

左子树中的前缀不再是右子树节点的祖先前缀。若离开左分支后仍保留它,哈希查询可能把“先向上再向下”的两段拼成题目不允许的路径。

代码—祖先链—频次表联动1 / 1
当前节点—
current_sum / needed—
本节点新增0
prefix_count{0:1}
累计答案0
关键状态问题

当前节点 11 的 current_sum=18,需要旧前缀10。此时频次表中 10 出现一次,而左分支前缀已经撤销。

七个语义槽位每个槽位对应一个不变量动作
顺序审查顺序错误会产生空路径或状态泄漏
正确:先查询再登记

避免 target=0 时让当前前缀与自己配对,虚构长度为0的路径。

正确:离开前撤销

让频次表恢复到进入当前节点前的祖先链状态。

错误:只用 set

同一旧前缀出现多次时,会丢失多个合法起点。

错误:全树共享频次

兄弟分支前缀会相互污染,形成非向下路径。

迁移目标:LeetCode #560 和为 K 的子数组

前缀差和频次统计完全复用。数组只有一条从左到右的历史链,不存在递归返回兄弟分支,因此不需要回溯撤销。

LeetCode #560|和为 K 的子数组

标签:数组 · 哈希表 · 前缀和
中等

迁移原题

给定整数数组 nums 和整数 k,返回数组中和为 k 的连续子数组数量。

class Solution: def subarraySum(self, nums: List[int], k: int) -> int: pass

打开 LeetCode #560 原题 ↗

示例与变化

nums = [1,1,1], k = 2
输出:2
nums = [1,2,3], k = 3
输出:2

仍然先查询 current_sum-k,再登记当前前缀;但历史前缀永远属于同一条扫描链,无需撤销。

保留前缀差公式与频次含义

#437 树上路径

旧前缀必须位于当前祖先链,递归离开节点后撤销。

线性化 →
#560 数组区间

所有旧前缀都位于当前索引之前,始终是合法历史。

数组扫描到当前前缀 S 时,应增加多少个答案?

为什么数组不需要撤销前缀?

数组:唯一历史链

索引只向右移动,所有已登记前缀都位于当前索引之前。

树:存在兄弟分支

返回父节点后,刚离开的分支不再属于下一分支的祖先链。

数组循环处理完一个元素后,应执行什么?

把递归状态压缩为一次线性循环

真实轨迹:nums = [1,1,1],k = 2

当前 value—
current_sum0
needed—
本轮新增0
累计 answer0
prefix_count{0:1}
参考答案可复制后继续修改
环节 1 / 6
本环节已完成

页面不会自动跳转。请先观察当前状态,再决定是否继续。