LC 124 · Hard · Post-order · return one thing, record another

Binary Tree Maximum Path Sum

The best answer at a node is not the value its parent can use.

Recognise it

Before any code, what in the question tells you this is the pattern?

A path that may bend at its highest point rather than run root to leaf, combined with values that can be negative. The moment the best answer at a node is not the value you can pass to its parent, you are in this pattern.

The brute force

Always have this one ready. It is the honest starting point, and it is what the real solution improves on.

For every node, compute the longest downward path from it into each subtree, then combine to get the best path bending at that node. Repeat for all nodes and take the maximum.

O(n squared), because computing the downward best at every node re-walks its whole subtree.

The recomputation is the problem, and it is the standard signal to fold the work into a single post-order pass. Every node already learns its childrens best downward value while the recursion returns, so computing it again from scratch is pure waste. The fix is not a new idea, it is doing the same arithmetic on the way back up.

The approach

The idea in plain language, before it becomes syntax.

The difficulty in this problem is that two different quantities live at every node and beginners conflate them. Separating them is the whole solution.

The first is the best path that bends at this node. It may descend into the left subtree, come up through this node, and descend into the right. That is a candidate for the global answer, and it is the number you record.

The second is the best path that passes through this node on its way to the parent. A path going through the parent enters this node once and leaves once, so it can use at most one child. That is the number you return. Returning the bent value instead is the classic bug, because it lets the parent build a path that visits this node twice, which is not a path.

Clamping each child at zero expresses the last piece: a subtree whose best contribution is negative is worth skipping entirely, and taking the maximum with zero says exactly that without a branch. The clamp applies only to what children contribute, never to the node itself, because every path contains at least one node.

A path bending at a node against the single arm that can be passed up.Two different numbers live at every node.-10920157bend at 20: 15 + 20 + 7 = 42record this, never return itpass up from 20: 20 + 15 = 35one arm only, because a parententers and leaves onceReturn the bent value and the parent builds a path that visits a node twice.
Binary Tree Maximum Path Sum: the idea in one picture.edit source
A path bending at a node against one passing through it.1046bend: 4 + 10 + 6 = 20, recorded1046pass up: 10 + 6 = 16A parent enters and leaves once, so it can only extend a single arm.
Record the bent one, return the straight one. Confusing them is the whole difficulty.

The algorithm

The same idea again, as steps you could follow with a pencil.

Flowchart separating the recorded value from the returned one.node is null ?yesreturn 0noL = max(gain(left), 0)R = max(gain(right), 0)best = max(best, val + L + R)return val + max(L, R)
Binary Tree Maximum Path Sum: the loop, step by step.edit source
Time O(n)Space O(h)

The solution

Now the code, and why each decision in it is the way it is.

binary_tree_maximum_path_sum
def maxPathSum(root):
    best = float('-inf')          # not 0: an all-negative tree has a negative answer

    def gain(node):
        nonlocal best
        if not node:
            return 0
        left = max(gain(node.left), 0)      # a negative branch is worth skipping
        right = max(gain(node.right), 0)
        best = max(best, node.val + left + right)   # bends here: record only
        return node.val + max(left, right)          # straight: safe to pass up

    gain(root)
    return best

Seed the running best at negative infinity rather than zero. A tree consisting of a single node holding -3 has answer -3, and a zero seed would report 0, which is not a path at all.

The clamp is on the child gains, and the node value is added unclamped. If you clamp the node value too, an all-negative tree returns zero for the same wrong reason.

One pass, constant work per node, so linear time and stack-height space. The quadratic brute force and this share every line of arithmetic; the only difference is that this one keeps what it already computed.

If this problem will not click, solve Diameter of Binary Tree first. It has the identical return-versus-record split with edges instead of sums, and without negative numbers to reason about at the same time.

A negative subtree skipped by clamping its contribution at zero.max(gain(child), 0)a subtree worth less than nothing is skippedbest starts at -infinitya tree of all negatives has a negative answerClamp the children only. Every path contains at least one node, so the node value stands.
The clamp applies to children only, never to the node value.

Where people slip

  • Returning the bent value is the classic bug. It lets a parent build an impossible path that visits a node twice.
  • Clamp each child at zero. A subtree with a negative best is worth skipping entirely, and the clamp expresses that without a branch.
  • Seed the running best at negative infinity, not zero. A tree of all negative values has a negative answer, and zero would wrongly win.
  • The clamp applies to what children contribute, never to the node value itself. Every path contains at least one node.

Your notes

What tripped you up here? Write it in your own words.

Where interviewers go next

Solve this one and the follow-up is usually a variation rather than a new idea. Knowing the connection is worth more than memorising each problem.

  • Diameter of Binary TreeLC 543The same return-versus-record split, counting edges instead of summing values. Solve this one first if 124 will not click.
  • Longest Univalue PathLC 687Same shape again, with the extra condition that a child only counts when its value matches.
  • Path Sum IIILC 437Paths must go downward, which turns it into prefix sums over the current root-to-node path.