目录
124.二叉树中的最大路径和Hard
以为想到了却又没想到,最后还是屎山代码憋出来了。这道hard说难也不难。分析题目,主要是路径,这个路径可以是跨越根节点的,也就是可以左根右这种,之前做的遍历题目都是从根节点出发不会出现横跨根节点的路径。
对于二叉树类的复杂题目,好像还是先考虑最简单的二叉树情况然后递归,因此如果是最简单的三个节点的二叉树,它的最大路径和都有6种,三个节点和三个组合(根+左,根+右,左+根+右),我们需要分类讨论,这也是DP(dynamic programing)的一个重要思想。
根据之前的经验,我们分类成从根节点向下的路径(不跨越根节点)和跨越根节点的路径,说个废话,这里是一定要包含根节点的,因为递归中每个点都是被看做根节点进行遍历的。如果我们知道了左右子树的MRL(max route length最大路径和),那么自然而然的就会去思考以根节点的二叉树的MRL;
也就是,把遍历到的根节点代表的子树的作用抽象成一个节点,官方题解里为什么要和0比较也是这个意思,那么就分类讨论吧,自己的方法中对于跨越根节点的路径和单独存放在集合,后面再去比较,也是没有进化完全。
class Solution {
List<Integer> maybe_res = new ArrayList<>();
public int maxPathSum(TreeNode root) {
int max1 = curr_node_start_Max(root);
int max2 = Collections.max(maybe_res);
return Math.max(max1, max2);
}
//以当前节点为头的最大路径和,向下
private int curr_node_start_Max(TreeNode root) {
if(root == null)return 0;
int leftMax = curr_node_start_Max(root.left);
int rightMax = curr_node_start_Max(root.right);
int x = Math.max(leftMax, rightMax);
if(root.val < 0) {
//针对只有-3的样例
if(root.left != null)maybe_res.add(leftMax);
if(root.right != null)maybe_res.add(rightMax);
//maybe_res.add(x);
maybe_res.add(root.val + leftMax + rightMax);
//原来这个地方是直接返回root.val的
//后来[8,9,-6,null,null,5,9]这个样例没过才改的
return Math.max(root.val, root.val + x);
}
//if(root.val >= 0)
else{
if(leftMax>=0&&rightMax>=0){
maybe_res.add(root.val+leftMax+rightMax);
return root.val + x;
} else if(x <0){
//如果左右子树的MRL都小于0,不如一个只要根节点
return root.val;
}else{
return root.val + x;
}
}
}
}
像一坨,但是思路是有点模样的
官方题解:
class Solution {
int maxSum = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
maxGain(root);
return maxSum;
}
public int maxGain(TreeNode node) {
if (node == null) {
return 0;
}
// 递归计算左右子节点的最大贡献值
// 只有在最大贡献值大于 0 时,才会选取对应子节点
int leftGain = Math.max(maxGain(node.left), 0);
int rightGain = Math.max(maxGain(node.right), 0);
// 节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值
int priceNewpath = node.val + leftGain + rightGain;
// 更新答案
maxSum = Math.max(maxSum, priceNewpath);
// 返回节点的最大贡献值
return node.val + Math.max(leftGain, rightGain);
}
}
很巧妙,官方的这个priceNewpath是在左右子树都遍历完的时候计算的,想想你前面写的代码在右子树遍历完之后的弹栈操作,都是有相通的点,而且也是根左右相加,解决了跨根节点的问题,其实这里就是和自己的把跨根节点的MRL存到集合里一样,人家是及时更新,你是最原始的方法选出最大值。
返回时也只能返回不跨根节点的路径,因为跨的都更新比较过了。就写到这吧,越写越感觉自己的解法和官方有很多相同之处。
236.二叉树的最近公共祖先
分别记录p,q节点的遍历路径然后比较,怎么记录?不会。。好像会了,最后没写出来
看题解了,有两种方法
存储节点父子关系
class Solution {
Map<Integer, TreeNode> parent = new HashMap<Integer, TreeNode>();
Set<Integer> visited = new HashSet<Integer>();
public void dfs(TreeNode root) {
if (root.left != null) {
parent.put(root.left.val, root);
dfs(root.left);
}
if (root.right != null) {
parent.put(root.right.val, root);
dfs(root.right);
}
}
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
dfs(root);
while (p != null) {
visited.add(p.val);
p = parent.get(p.val);
}
while (q != null) {
if (visited.contains(q.val)) {
return q;
}
q = parent.get(q.val);
}
return null;
}
}
这种方法就是先遍历所有节点,可以将所有的父子关系存储在hashmap中,然后直接从其中一个节点p出发找到它的所有父节点并加入到集合中,再一个个查找另一个节点的父节点是否在其中。这种方法还是比较好理解的,之前学的时候好像也接触过这种方法。
递归,分类讨论
咱也不知道这个方法为啥叫递归,所有的遍历方法也用到了递归啊。
一,对于结果节点的判断条件还是能理解,两种情况,分别是p和q没有父子节点关系,p和q其中一个是另一个的父节点。
二,对fx函数的定义,以x为根节点的子树中是否包含p和q节点。
三,对于dfs返回条件的理解,首先触发的一定是遍历到的节点值是p或者q,从lson,rson是递归的返回值也能说明这一点。
class Solution {
private TreeNode ans;
public Solution() {
this.ans = null;
}
private boolean dfs(TreeNode root, TreeNode p, TreeNode q) {
if (root == null) return false;
boolean lson = dfs(root.left, p, q);
boolean rson = dfs(root.right, p, q);
if ((lson && rson) || ((root.val == p.val || root.val == q.val) && (lson || rson))) {
ans = root;
}
return lson || rson || (root.val == p.val || root.val == q.val);
}
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
this.dfs(root, p, q);
return this.ans;
}
}
可以再理解一下dfs返回值的写法含义,自己再写的时候dfs里root.left能写成root真是服了。
这题拖了挺久,从上周四下午,中间就没leetcode。。。