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