Binary Tree Maximum Path Sum
A path is any sequence of nodes where each consecutive pair is joined by an edge, and no node appears twice. It does not have to pass through the root, and it does not have to start or end at a leaf. Given the root of a binary tree, return the largest possible sum of the values along such a path. The path must contain at least one node, so when every value is negative the answer is the single largest (least negative) value, never 0.
- The number of nodes in the tree is in the range [1, 3 × 10⁴]
- -1000 <= Node.val <= 1000
- The path must be non-empty — the empty path is not a valid answer
- The path need not include the root or any leaf
root = [1,2,3]6root = [-10,9,20,null,null,15,7]42root = [-3]-3root = [2,-1,-2]2This is Diameter of Binary Tree with values instead of lengths, and it is worth holding that comparison in mind, because the skeleton is identical and every difference comes from one fact: lengths are always positive, and values can be negative.
Start with the same structural observation. A path never repeats a node, and edges only run downward, so any path climbs to a single highest node and then descends — one bend, never two. Group the paths by that bend, ask each node "what is the best path that bends at you?", and take the maximum over all nodes. Every path is counted at exactly one node, so nothing is missed.
Sit on node v and hold two different quantities apart. They get confused constantly, and keeping them separate is the whole problem.
The best path that bends at v. It may descend into the left subtree and into the right subtree, so it is v.val plus the best downward run on the left plus the best downward run on the right. This is a candidate for the final answer, but it is finished — it cannot be extended any further, because v has already spent both of its downward links.
The best path that passes through v on its way up. If v's parent wants to build a path through v, that path arrives from above and can continue into only one of v's subtrees; taking both would create a branch point and stop being a path. So what v can offer its parent is v.val plus the better of its two downward runs — never the sum of both.
Only one of these can be the return value, and it has to be the second, since the recursion cannot proceed without it. The first is therefore recorded into a variable outside the recursion, exactly as the diameter was.
In the diameter problem, a deeper subtree was always at least as good, so we took whatever the children offered. Here a child may offer a negative number — a subtree stuffed with losses — and a path is never obliged to descend into it. Declining costs nothing.
So clamp each child's offer at zero: gain = max(0, child's offer). Read that as "use this side only if it helps." A clamped 0 means the path simply stops at v on that side.
best = float('-inf')
def gain(node):
nonlocal best
if node is None:
return 0 # an absent side contributes nothing
left = max(0, gain(node.left)) # take the side only if it is a gain
right = max(0, gain(node.right))
best = max(best, node.val + left + right) # candidate: the path BENDS here
return node.val + max(left, right) # offer upward: the path PASSES through
gain(root)
return bestnode.val itself is never clamped — only the children's offers are. This is what makes the all-negative case come out right. On a tree holding just -3, both sides clamp to 0 and the candidate is -3 + 0 + 0 = -3, so best becomes -3. Had we clamped the node's own value too, or initialised best to 0, the answer would be 0 — a path containing no nodes, which the problem forbids. Initialise the running maximum to negative infinity, not zero, for the same reason: zero is a claim that the empty path is legal.Also note the deliberate asymmetry between the two lines: the recorded candidate adds both sides, the returned offer takes the better one. That is the same + versus max split as in Diameter, and for the same geometric reason — a path that ends here may use both directions; a path that continues upward may use only one.
The root is the instructive step. It genuinely improved on nothing — its own value is a loss — and the winning path was recorded three calls earlier, deep in the right subtree. Recording a candidate at every node is what lets the answer live anywhere in the tree without any special handling.
Now run the fourth example, root 2 with children -1 and -2. Both children offer negative numbers, so both clamp to 0, and the candidate at the root is 2 + 0 + 0 = 2. The clamping is doing real work here: without it the answer would be 2 + (-1) + (-2) = -1, a path forced to swallow losses it was free to skip.
One visit per node, constant work each: O(N) time, O(H) space for the recursion stack. The brute-force alternative — compute, for every node, the best downward run on each side by re-walking the subtrees — costs O(N × H), and it is the same duplicated-walk waste that Balanced Binary Tree and Diameter both had. The fix is the same fusion.
What generalises here is a way of thinking rather than a formula. When a recursion's local answer and its reportable value differ, write them down as two separate sentences before writing any code: what does my parent need from me? and what is the best answer that peaks right here? Then decide which one is the return value (the one the recursion cannot continue without) and put the other in an accumulator. Add one more question whenever values may be negative — am I allowed to decline this piece? — and the clamp appears exactly where it belongs. Those three questions are enough to reconstruct this solution from scratch, which is far more useful than remembering it.
Optimal Path Explorer
Starting Post-order traversal. Nodes return 'Gain' (Node + Max Child).
Strategy
Focus on the recursive nature of trees: solve for subtrees and combine results at the root.
"Divide and Conquer: Subproblem → Recurrence → Result"