分裂二叉树的最大乘积 图解题解
这道题到底在问什么
- 输入
- root=[50,30,80,10,40,60]
- 输出
- 18200(砍 80 上面那条边:140 × 130)
先想最直接的笨办法
记住「先求各子树和与 total,再枚举砍边算 sub ×(total − sub) 取最大,最后才取模」,下面逐帧套它。(动画第 3 步)
最优解:一步一步想明白
- 3记住「先求各子树和与 total,再枚举砍边算 sub ×(total − sub) 取最大,最后才取模」,下面逐帧套它。
- 4total = ?第一步:后序遍历,先算孩子、再算自己,求出每个节点的子树和。后序保证算到某个节点时,它的左右子树和都已就绪。
- 5访问 50走到节点 50(紫色),先把它的孩子都算完,再回头算它自己的子树和。
- 6访问 30走到节点 30(紫色),先把它的孩子都算完,再回头算它自己的子树和。
- 7访问 10走到节点 10(紫色),它是叶子,子树里只有它自己。
- 8子树和[10] = 10节点 10 的子树和 = 10 = 10(变绿,存进列表)。
- 9访问 40走到节点 40(紫色),它是叶子,子树里只有它自己。
- 10子树和[40] = 40节点 40 的子树和 = 40 = 40(变绿,存进列表)。
- 11子树和[30] = 80节点 30 的子树和 = 30 + 10 + 40 = 80(变绿,存进列表)。
- 12访问 80走到节点 80(紫色),先把它的孩子都算完,再回头算它自己的子树和。
- 13访问 60走到节点 60(紫色),它是叶子,子树里只有它自己。
- 14子树和[60] = 60节点 60 的子树和 = 60 = 60(变绿,存进列表)。
- 15子树和[80] = 140节点 80 的子树和 = 80 + 60 = 140(变绿,存进列表)。
- 16子树和[50] = 270节点 50 的子树和 = 50 + 80 + 140 = 270(变绿,存进列表)。它是根,这个和就是整棵树的总和 total。
- 17total = 270所有子树和都算出来了,根的子树和 270 就是总和 total。接下来枚举每一棵子树:砍掉它上面那条边,看 sub ×(total − sub) 有多大。
- 1810 × 260 = 2600砍掉节点 10 上面那条边:红色这棵子树和 = 10,蓝色剩下那半 = 270 − 10 = 260,乘积 = 2600。比之前大,刷新 best = 2600。
- 1940 × 230 = 9200砍掉节点 40 上面那条边:红色这棵子树和 = 40,蓝色剩下那半 = 270 − 40 = 230,乘积 = 9200。比之前大,刷新 best = 9200。
- 2080 × 190 = 15200砍掉节点 30 上面那条边:红色这棵子树和 = 80,蓝色剩下那半 = 270 − 80 = 190,乘积 = 15200。比之前大,刷新 best = 15200。
- 2160 × 210 = 12600砍掉节点 60 上面那条边:红色这棵子树和 = 60,蓝色剩下那半 = 270 − 60 = 210,乘积 = 12600。没超过当前 best = 15200,保持不变。
- 22140 × 130 = 18200砍掉节点 80 上面那条边:红色这棵子树和 = 140,蓝色剩下那半 = 270 − 140 = 130,乘积 = 18200。比之前大,刷新 best = 18200。
- 23270 × 0 = 0代码里会把根的子树和 270 也算进去,但 270 ×(270 − 270)= 270 × 0 = 0,等于没切,自然不会被选为最大,可以放心一起遍历。
- 24best = 18200所有砍法里,砍 80 上面那条边最优:把树分成和为 140 和 130 的两半,两者最接近一半,乘积最大 = 18200。
- 25答案 = 18200最后一步才对最大乘积取模:18200 % (1e9+7) = 18200(本例数不大,取模后不变)。注意取模一定放在取完最大值之后,要是一边算乘积一边取模、再拿取模后的值比大小,就会比错。
⚠️ 容易写错的地方
✗ 错:一边比大小一边取模
✓ 对:先在未取模的真实乘积上取最大,最后只对答案取一次模
取模会打乱数的大小关系,在取模后的值之间比大小会选错那条边
✗ 错:用 int 存乘积
✓ 对:乘积用 long / long long(Python 天然大整数)
sub ×(total − sub) 最大可达 (total/2)²,远超 int 范围,会溢出成负数或错值
✗ 错:只试根的左右两棵子树
✓ 对:枚举每一棵子树(每条边)
最优的那条边可能连在树深处的任意节点上、不一定挨着根,只看根的两个孩子会漏掉真正最优的边,所以要对每个节点(每条边)都算一遍
完整代码(Python / C++ / Java)
Python
from typing import List, Optional
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class Solution:
def maxProduct(self, root: Optional[TreeNode]) -> int:
sums = []
def dfs(node):
if not node: return 0
s = node.val + dfs(node.left) + dfs(node.right)
sums.append(s)
return s
total = dfs(root)
return max(s * (total - s) for s in sums) % (10**9 + 7)C++
#include <algorithm>
#include <climits>
#include <functional>
#include <iterator>
#include <numeric>
#include <vector>
using namespace std;
struct TreeNode{int val;TreeNode* left;TreeNode* right;TreeNode():val(0),left(nullptr),right(nullptr){} TreeNode(int x):val(x),left(nullptr),right(nullptr){} TreeNode(int x,TreeNode*l,TreeNode*r):val(x),left(l),right(r){}};
class Solution{public: int maxProduct(TreeNode* root){ vector<long long>sums; function<long long(TreeNode*)> dfs=[&](TreeNode*n){ if(!n)return 0LL; long long s=n->val+dfs(n->left)+dfs(n->right); sums.push_back(s); return s;}; long long total=dfs(root),best=0; for(long long s:sums)best=max(best,s*(total-s)); return best%1000000007; }};Java
import java.util.*;
class TreeNode{int val;TreeNode left,right;TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val,TreeNode left,TreeNode right){this.val=val;this.left=left;this.right=right;}}
class Solution{ ArrayList<Long>sums; public int maxProduct(TreeNode root){ sums=new ArrayList<>(); long total=dfs(root),best=0; for(long s:sums)best=Math.max(best,s*(total-s)); return (int)(best%1_000_000_007); } long dfs(TreeNode n){ if(n==null)return 0; long s=n.val+dfs(n.left)+dfs(n.right); sums.add(s); return s;} }复杂度
时间
O(n)
一遍后序 DFS 求所有子树和 O(n);再遍历 n 个子树和算乘积 O(n);n 是节点数
空间
O(n)
存所有子树和的列表 O(n),外加递归栈 O(h);最坏 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 分裂二叉树的最大乘积 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
能不能不做第二次树遍历,少扫一遍树?+
可以少做一次「树」DFS,但不能在还不知道 total 的同一次 DFS 里就直接算出最终 best。常见写法是:一遍 DFS 把所有子树和收集进一个列表、同时得到 total(根的子树和),再扫一遍这个列表算 sub ×(total − sub) 取最大。这样树只遍历了一次,但仍得等 total 出来才能算乘积,因为每条边的另一半都是 total 减 sub,total 没拿到就算不了。
为什么乘积最大时两棵子树的和会尽量接近?+
因为两半的和固定为 total,设一半是 x、另一半是 total − x,乘积 x(total − x) 是开口向下的抛物线,在 x = total/2 处取最大。所以越接近把总和对半分,乘积越大;我们枚举所有可行的子树和,挑最接近 total/2 的那条边。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 分裂二叉树的最大乘积 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。