树形递归技巧
Last updated
Last updated
二叉树的三种遍历方式 (前序, 中序, 后序) 代表了三种典型的递归场景;
这三种遍历方式的主要区别在于遍历到当前节点时, 已知节点的位置不同, 以根节点为例:
前序遍历到根节点时, 其他节点都未知;
中序遍历到根节点时, 遍历完了整棵左子树;
后序遍历到根节点时, 已经遍历完了所有子节点;
可见,
前序遍历是自顶向下的递归; 如果当前节点需要父节点的信息时, 就用前序遍历;
后序遍历是自底向上的递归; 如果当前节点需要子节点的信息时, 就用后序遍历;
中序遍历比较特殊, 可以认为它是二叉树特有的, 比如对一棵多叉树, 中序遍历就无从谈起, 所以中序遍历主要用在那些利用了二叉树特殊性质的情况, 比如二叉搜索树;
我们使用递归的一个关键, 就是希望将问题分解为子问题后再逐步解决,
这一点非常契合后序遍历的方式, 即从子树的解, 递推全局的解, 所以后序遍历的问题是最多的;
此时可以把这种在树上进行的递归看作是一种特殊的动态规划, 即树形 dp;
本篇介绍的技巧, 简单来说就是如何结构化处理这些子问题的解, 这个方法可以用于所有需要自底向上进行递归的问题;
自底向上的递归技巧, 树形 dp 等
记 dfs(x)
模板如下:
考虑计算出当前节点的答案需要从左右子树获得哪些信息, 并假设已知, 记 l_info
和 r_info
;
利用 l_info
和 r_info
构造出当前节点应该返回的信息 x_info
;
注意空树返回的信息, 即 Info
的默认值;
为了可读性, 推荐使用结构体 (python 中推荐使用 dataclass
) 来保存需要的信息;
进一步的, 最终生成的 x_info
不一定都会与 x
有关, 此时需要分与 x 有关的答案和与 x 无关的答案进行讨论, 并取其中的最优解 (详见示例 3 和 4);
二叉树 x
的高度等于左右子树高度中的较大值 +1
;
本题比较简单, 故简化了模板;
98. 验证二叉搜索树 - 力扣(LeetCode) 110. 平衡二叉树 - 力扣(LeetCode) 958. 二叉树的完全性检验 - 力扣(LeetCode)
搜索二叉树: 对每个节点, 其值大于左子树的最大值,小于右子树的最小值;
平衡二叉树: 对每个节点, 其左右子树的高度差 <= 1
;
完全二叉树, 这里给出两种判断条件:
条件1:
左右子树都是满二叉树,且高度相同, 即本身是满二叉树;
左右子树都是满二叉树,且左子树的高度+1;
左子树是满二叉树,右子树是完全二叉树,且高度相同;
左子树是完全二叉树,右子树是满二叉树,且左子树的高度+1;
满二叉树: 左右子树的高度相同, 且都是满二叉树;
条件2, 该方法需要前序遍历进行预处理 (详见下一节: 前序遍历的信息传递):
记根节点 id
为 1
;若父节点的 id
为 i
,则其左右子节点分别为 2*i
和 2*i+1
;
如果是完全二叉树则有 max_id == n
,其中 n
为总节点数;
以下为合并实现
根据最大路径和是否经过 x
节点分两种情况;
如果不经过 x
节点, 那么 x
中的最大路径和等于左右子树中最大路径和中的较大值;
如果经过 x
节点, 那么 x
的最大路径和等于左右子树能提供的最大路径之和, 再加上当前节点的值;
取两者中的较大值;
综上, 需要记录的信息包括, 当前节点能提供的最大路径, 当前的最大路径;
所谓 "当前节点能提供的最大路径", 即从该节点向下无回溯遍历能得到的最大值; 假设每个节点能提供的路径都是 1, 那么这个最大路径就是树的高度;
因为节点值存在负数, 这是一个容易出错的点 (详见代码);
从本题可以看到, 模板并不能解决问题, 只是减少了问题以外的思考, 即如何将思路转换为代码.
树形 dp,就是否抢劫当前节点分两种情况讨论;
后序遍历中, 父节点获取子节点的信息很自然, 直接在函数体内接收递归的结果即可;
前序遍历中, 子节点想要获取父节点的信息就不能这么做, 一般的方法是通过参数传递;
思路:
记根节点 id
为 1
;若父节点的 id
为 i
,则其左右子节点分别为 2*i
和 2*i+1
;
如果是完全二叉树则有 max_id == total_cnt
,其中 total_cnt
为总节点数;
这里子节点的 id 就需要通过父节点的 id 来计算;
问题简述:
判断树中是否存在 根节点到叶子节点 的路径和等于 targetSum;
algorithms/树形递归