宝宝也能看懂的 leetcode 周赛 - 174 - 3

1339. 分裂二叉树的最大乘积

Hi 大家好,我是张小猪。欢迎来到『宝宝也能看懂』系列之 leetcode 周赛题解。

这里是第 174 期的第 3 题,也是题目列表中的第 1339 题 -- 『分裂二叉树的最大乘积』

题目描述

给你一棵二叉树,它的根为 root。请你删除 1 条边,使二叉树分裂成两棵子树,且它们子树和的乘积尽可能大。

由于答案可能会很大,请你将结果对 10^9 + 7 取模后再返回。

示例 1:

输入:root = [1,2,3,4,5,6]
输出:110
解释:删除红色的边,得到 2 棵子树,和分别为 11 和 10 。它们的乘积是 110 (11*10)

示例 2:

输入:root = [1,null,2,3,4,null,null,5,6]
输出:90
解释:移除红色的边,得到 2 棵子树,和分别是 15 和 6 。它们的乘积为 90 (15*6)

示例 3:

输入:root = [2,3,9,10,7,8,6,5,4,11,1]
输出:1025

示例 4:

输入:root = [1,1]
输出:1

提示:

  • 每棵树最多有 50000 个节点,且至少有 2 个节点。

  • 每个节点的值在 [1, 10000] 之间。

官方难度

MEDIUM

解决思路

题目提供了一棵二叉树的根节点,需要我们删除一条边使得这棵二叉树变成两棵二叉树。而要求就是对这两棵二叉树里各个节点分别求和之后,这两个和的乘积最小。

不知道小伙伴们是什么想法,不过小猪看完题目之后第一反应是,先遍历起来。毕竟,只对着一个根节点的话,再好的戏也出不来。而提到遍历二叉树的话,又想少写一点代码,那小猪的第一反应就是用递归来实现深度优先遍历啦。

接下来回到题目的需求上,我们需要找到拆分后两个和的乘积最大。由于这棵树给定后就不会变了,所以所有节点求和的值其实是一定的。而对于这个确定的值,我们把它拆成两个加数后要使得它们的乘积最大,那么这两个数应该尽可能的靠近总和的二分之一。相信到这里,很多小伙伴都是能想到哒。不过再往后,如果直接用数学的方式来处理这个问题,小猪就想不明白了。小猪苯苯的,希望小伙们能帮帮我。

那没有了数学的方法,小猪就只能用计算机的方法啦。我们可以想象一下,在去掉一条边之后,拆分成的两棵二叉树,其中必然有一棵的顶点位于我们刚才断掉的那条边的下游。也就是说,从当前位置拆开之后,其实分成的两部分,就是以当前节点为顶点的二叉树,以及其他的节点。

想通了这一点之后,再结合前面提到的总和是固定不变的。那么我们的思路也就大概清晰啦。

直接方案

我们可以先计算出所有节点的总和。然后对于每一个节点,我们也能算出以它为顶点的子二叉树的总和。那么剩下的那一部分也就知道啦。在对每个可能的节点都进行这样的计算之后,我们就能找到那个最大的乘积了。

基于这个思路,我们具体的流程如下:

  1. 遍历二叉树,得到所有节点值的总和。

  2. 遍历二叉树,针对每一个可能的节点,假设从这里拆分,然后计算可能的乘积,并记录最大值。

  3. 返回最大的乘积。

基于这个流程,我们可以实现类似下面的代码:

const maxProduct = root => {
  let sum = max = 0;
  sum = helper(root);
  helper(root);
  return max % (10 ** 9 + 7);

  function helper(node) {
    const subSum = node.val + (node.left ? helper(node.left) : 0) + (node.right ? helper(node.right) : 0);
    sum && subSum * (sum - subSum) > max && (max = subSum * (sum - subSum));
    return subSum;
  }
};

优化

上面的代码我们对整个二叉树进行了两次遍历,其中第一次是为了计算总和,而第二次则是进行每个节点的子二叉树的计算和判断。

不知道小伙伴们有没有发现,其实我们第二次遍历中所计算的东西,在第一次遍历的时候也是可以得到的。而唯一最初无法计算的,就是与总和的差值做乘积的结果。那么我们其实可以把前面的内容都记录下来,在得到了总和之后直接进行计算即可。

基于这个优化思路,我们具体的流程如下:

  1. 遍历二叉树,得到所有节点值的总和,并记录以每个节点为首的子二叉树的总和。

  2. 尝试这些子二叉树的和,并计算出最大的乘积。

基于这个流程,我们可以实现类似下面的代码:

const maxProduct = root => {
  const subSums = new Uint32Array(50000);
  let max = idx = 0;
  const sum = helper(root);
  for (let i = 0; i < idx; ++i) {
    const val = subSums[i];
    val * (sum - val) > max && (max = val * (sum - val));
  }
  return max % (10 ** 9 + 7);

  function helper(node) {
    const subSum = node.val + (node.left ? helper(node.left) : 0) + (node.right ? helper(node.right) : 0);
    subSums[idx++] = subSum;
    return subSum;
  }
};

总结

这道题的逻辑并不复杂,还是非常套路的内容。关键点就是在与要相通我们拆分后的情况究竟是什么,也就是分析中提到的拆分之后其中一部分必然是以当前节点为首的子二叉树。而剩下的就是套用深度优先遍历去实现即可。

在我们这么多期的周赛题解之后,相信小伙伴们已经逐渐的开始熟悉这些套路了吧。突然觉得小猪做的事情还是有意义的呢,开心的摇了摇猪尾巴 >.<

1、写给大忙人看的进程和线程

2、一个HTTP数据包的奇幻之旅

3、一个函数解决【LeetCode 买卖股票的最佳时机】系列所有题目!

4、原来状态机也可以用来刷LeetCode?

5、【算法提高班】《我的日程安排表》系列

6、用好这几个工具,能大幅提升你的 Git/GitHub 操作效率!

如果觉得文章不错,帮忙点个在看呗

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值