1. 题目解析与解题思路
这道题目要求我们在二叉树中找到一条边,使得删除这条边后形成的两棵子树的节点值乘积最大化。题目给出的二叉树节点值范围在1到10000之间,节点数量不超过50000。
1.1 问题本质分析
这道题的核心在于理解"分裂"操作的实际含义。当我们删除树中的一条边时,整棵树会被分成两个独立的子树。我们需要计算这两棵子树的节点值之和,然后求它们的乘积。
关键点在于:
- 需要遍历整棵树的所有边
- 对于每条边,快速计算分裂后两部分的节点和
- 找到使乘积最大的那个分割方案
1.2 解题思路选择
最直观的暴力解法是:
- 遍历树的所有边
- 对于每条边,分别计算两部分的节点和
- 计算乘积并记录最大值
但这种方法的效率太低,时间复杂度会达到O(n^2)。我们需要更聪明的办法。
优化思路:
- 首先计算整棵树的总和total_sum
- 对于任意子树,如果其和为subtree_sum,则另一部分的和就是total_sum - subtree_sum
- 乘积就是subtree_sum * (total_sum - subtree_sum)
- 我们只需要找到使这个乘积最大的subtree_sum
这样,问题转化为:在遍历树的过程中,计算每个子树的和,并记录使乘积最大的那个值。
2. 算法实现细节
2.1 数据结构定义
首先定义二叉树节点:
class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right2.2 核心算法实现
采用后序遍历的方式计算子树和:
def maxProduct(root): total_sum = 0 max_product = 0 def tree_sum(node): if not node: return 0 left_sum = tree_sum(node.left) right_sum = tree_sum(node.right) return node.val + left_sum + right_sum total_sum = tree_sum(root) def find_max_product(node): nonlocal max_product if not node: return 0 left_sum = find_max_product(node.left) right_sum = find_max_product(node.right) subtree_sum = node.val + left_sum + right_sum product = subtree_sum * (total_sum - subtree_sum) if product > max_product: max_product = product return subtree_sum find_max_product(root) return max_product % (10**9 + 7)2.3 复杂度分析
时间复杂度:O(n),因为我们只需要两次遍历整棵树(一次计算总和,一次寻找最大乘积) 空间复杂度:O(h),h是树的高度,递归调用栈的深度
3. 优化与注意事项
3.1 模运算处理
题目要求结果对10^9+7取模,这是为了避免大数溢出。注意要在最后返回时取模,而不是在计算过程中,否则可能影响比较结果。
3.2 避免重复计算
在实现中,我们通过分离tree_sum和find_max_product两个函数来避免重复计算。第一次遍历计算总和,第二次遍历计算各个子树和并比较乘积。
3.3 边界条件处理
需要考虑的特殊情况:
- 空树(题目保证至少有一个节点)
- 单节点树(无法分割,但题目保证至少有一个节点)
- 完全不平衡的树(如所有节点都在左子树)
4. 测试用例与验证
4.1 示例测试
# 示例1 # 1 # / \ # 2 3 # / \ # 4 5 root = TreeNode(1) root.left = TreeNode(2) root.right = TreeNode(3) root.left.left = TreeNode(4) root.left.right = TreeNode(5) print(maxProduct(root)) # 应输出110 # 示例2 # 1 # \ # 2 # / \ # 3 4 # / \ # 5 6 root = TreeNode(1) root.right = TreeNode(2) root.right.left = TreeNode(3) root.right.right = TreeNode(4) root.right.left.left = TreeNode(5) root.right.left.right = TreeNode(6) print(maxProduct(root)) # 应输出904.2 极端情况测试
# 单边树 root = TreeNode(1) root.right = TreeNode(2) root.right.right = TreeNode(3) print(maxProduct(root)) # 应输出6 # 完全平衡树 root = TreeNode(1) root.left = TreeNode(2) root.right = TreeNode(3) print(maxProduct(root)) # 应输出65. 算法优化空间
5.1 迭代实现
虽然递归实现简洁,但对于非常深的树可能会导致栈溢出。可以使用迭代方式实现后序遍历:
def maxProduct_iterative(root): total_sum = 0 stack = [] node = root # 第一次遍历计算总和 while stack or node: while node: stack.append(node) node = node.left node = stack.pop() total_sum += node.val node = node.right max_product = 0 stack = [] node = root last_visited = None subtree_sums = {} # 第二次遍历计算子树和 while stack or node: while node: stack.append(node) node = node.left node = stack[-1] if not node.right or node.right == last_visited: node = stack.pop() left_sum = subtree_sums.get(node.left, 0) right_sum = subtree_sums.get(node.right, 0) subtree_sum = node.val + left_sum + right_sum product = subtree_sum * (total_sum - subtree_sum) if product > max_product: max_product = product subtree_sums[node] = subtree_sum last_visited = node node = None else: node = node.right return max_product % (10**9 + 7)5.2 并行计算
理论上可以将总和计算和最大乘积查找合并为一次遍历,但实现起来会更复杂,且实际性能提升有限。
6. 同类问题扩展
这种"分割树使某种度量最优"的问题在算法题中很常见,类似的问题包括:
- 分割二叉树使两子树节点数差最小
- 分割二叉树使两子树高度差最小
- 分割二叉树使两子树某种属性(如平均值)差最小
解决这类问题的通用思路是:
- 首先计算全局总量(总和、节点数、高度等)
- 遍历树,计算各个子树的局部量
- 根据全局量和局部量计算分割后的度量值
- 记录最优解
7. 实际应用场景
虽然这类问题看起来是纯算法题,但实际应用场景包括:
- 网络拓扑分割:将网络分成两部分,使两部分之间的通信量最小
- 数据分片:将数据库表分成两部分,使查询负载均衡
- 资源分配:将任务树分成两部分,使两部分的执行时间相近
理解这类问题的解法有助于解决实际工程中的分割和分配问题。
8. 常见错误与调试技巧
8.1 错误类型
- 错误计算总和:忘记在递归中累加当前节点值
- 过早取模:在比较乘积大小前就进行了模运算
- 忽略整数溢出:虽然Python整数不会溢出,但在其他语言中需要注意
- 错误处理空节点:忘记处理节点为None的情况
8.2 调试建议
- 先在小树上手动计算验证
- 打印中间结果检查子树和计算是否正确
- 使用可视化工具观察树结构
- 对特殊树形(如单边树、完全平衡树)单独测试
9. 性能优化实战
对于非常大的树(接近50000节点),可以考虑以下优化:
- 使用非递归遍历避免栈溢出
- 使用备忘录存储已计算的子树和
- 并行计算左右子树的和
- 使用更高效的数据结构表示树
不过对于LeetCode的测试用例,基本的递归实现已经足够。
10. 代码风格与可读性
好的代码应该:
- 分离关注点:如将总和计算和最大乘积查找分为两个函数
- 使用有意义的变量名:如subtree_sum比简单的s更好理解
- 添加必要注释:解释关键步骤
- 处理边界情况:如空节点处理
- 保持一致的代码风格:如缩进、命名规范等
在面试中,清晰的代码结构和良好的编码习惯比微小的性能优化更重要。