The problem
You get the root of a binary tree whose values can be negative. A path is any sequence of connected nodes where each node appears at most once. It can start and end anywhere and doesn’t have to pass through the root. Return the largest sum of any non-empty path.
Input: [-10, 9, 20, null, null, 15, 7]
-10
/ \
9 20
/ \
15 7
Output: 42The path 15 → 20 → 7 sums to 42. Including the root would only make it worse, because -10 costs more than 9 adds.
How to think about it
Start with brute force. Every path has a highest node where it bends. For each node, try every downward path on the left and every one on the right, and combine. Enumerating paths per node is O(n) each, so O(n²) overall, and it’s fiddly to write.
The key insight: it’s Diameter with values. At each node, two different quantities matter:
- What the node returns to its parent: the best path that starts here and goes down one side only,
node.val + max(l, r). A parent can’t use a path that forks. - What the node records as a candidate answer: the best path that bends here,
node.val + l + r, kept in a globalbest.
Drop negative branches. If a child’s best downward gain is negative, including it only lowers the sum, so clamp it at 0: l = max(gain(left), 0). That’s the whole difference from Diameter, and it’s what handles negative values.
Which pattern is this? Tree DFS with “return one value, track another”. The tell is “path anywhere in the tree”, where the best path can bend at any node.
Approach
- Keep
best = -infoutside the recursion. Not 0: an all-negative tree’s answer is negative. gain(node)returns 0 for an empty node.- Compute
l = max(gain(left), 0)andr = max(gain(right), 0). - Update
bestwithnode.val + l + r, the path bending at this node. - Return
node.val + max(l, r), the best one-sided path for the parent. - Call
gain(root)and returnbest.
Solution
def max_path_sum(root):
best = float('-inf')
def gain(node):
nonlocal best
if not node: return 0
l = max(gain(node.left), 0); r = max(gain(node.right), 0) # ignore branches that hurt
best = max(best, node.val + l + r) # path that bends at this node
return node.val + max(l, r) # parent can extend only one side
gain(root); return best
# max_path_sum(tree[-10,9,20,null,null,15,7]) -> 42Dry run
On the example, in the order nodes finish:
| Node | l |
r |
Bend val + l + r |
best |
Returns |
|---|---|---|---|---|---|
| 9 | 0 | 0 | 9 | 9 | 9 |
| 15 | 0 | 0 | 15 | 15 | 15 |
| 7 | 0 | 0 | 7 | 15 | 7 |
| 20 | 15 | 7 | 42 | 42 | 35 |
| -10 | 9 | 35 | 34 | 42 | 25 |
The bend at 20 gives 42. The root’s bend (34) is smaller, so it doesn’t win.
Complexity
- Time: O(n). One post-order pass with O(1) work per node.
- Space: O(h). The recursion stack goes as deep as the tree height: O(log n) balanced, O(n) skewed.
Edge cases and pitfalls
- All negative values. For
[-3, -1, -2]the answer is -1, a single node. Startingbestat 0 would wrongly return 0. - Clamp the children, not the node. The node’s own value is always included (the path is non-empty); only optional branches are clamped at 0.
- Returning the bend upward is the classic bug. A parent must get
val + max(l, r), neverval + l + r. - Single node returns its own value.
Related problems
- Diameter of Binary Tree (LeetCode 543): the same shape, counting edges instead of summing values.
- Path Sum (LeetCode 112): the restricted root-to-leaf version.
- Lowest Common Ancestor of a Binary Tree (LeetCode 236): another post-order DFS combining left and right results.
- 1.Maximum Depth of Binary Tree (LeetCode 104)
- 2.Same Tree (LeetCode 100)
- 3.Invert Binary Tree (LeetCode 226)
- 4.Path Sum (LeetCode 112)
- 5.Subtree of Another Tree (LeetCode 572)
- 6.Diameter of Binary Tree (LeetCode 543)
- 7.Lowest Common Ancestor of a BST (LeetCode 235)
- 8.Lowest Common Ancestor of a Binary Tree (LeetCode 236)
- 9.Validate Binary Search Tree (LeetCode 98)
- 10.Kth Smallest Element in a BST (LeetCode 230)
- 11.Construct Binary Tree from Preorder and Inorder (LeetCode 105)
- 12.Serialize and Deserialize Binary Tree (LeetCode 297)
- 13.Binary Tree Maximum Path Sum (LeetCode 124)