HARD Lab · Trees · 65

124. Binary Tree Maximum Path Sum

Find the maximum-sum path between any two nodes in a binary tree. A single post-order DFS returns each node's best downward gain (negatives clamped to 0) while a global best tracks the full through-node path — linear time, one pass.

HardDFSPost-orderTypeScript

PROBLEM What we're solving

A path is any sequence of connected nodes (parent↔child edges) with no node repeated; it need not pass through the root. Return the maximum sum of node values over all such paths. For [-10,9,20,null,null,15,7] the answer is 42 — the path 15 → 20 → 7 (the root -10 only drags the total down, so the best path skips it).

KEY IDEA Split "gain to return" from "best through here"

Insight → at every node compute two different things. The value you return to your parent is a one-sided downward gain (val + max(left, right)), because a parent can only attach to you through a single branch. But the best path that peaks at this node may use both children: val + left + right. Record that "through" sum into a global best; never return it upward.

RECIPE Post-order: children first, then decide

  • 0 · Recurse (post-order). Get leftGain and rightGain from the children beforetouching this node — you can't score a node without its subtrees' answers.
  • 1 · Clamp negatives. leftGain = max(0, gain(left)) (same for right). A branch that sums negative is simply not taken — zero is always available by stopping at this node.
  • 2 · Update the global best. best = max(best, val + leftGain + rightGain). This is the path that bends through this node using both sides — its highest possible point.
  • 3 · Return the upward gain. return val + max(leftGain, rightGain). The parent can only continue through one side, so hand back a straight-line gain, never the bent through-path.
Classic confusion → two traps. (1) Forgetting to clamp: if a child returns a negative gain you must use 0 instead — a losing branch should be dropped, not subtracted. (2) Returning the through-sum upward: the answer that uses left + right bends at this node and cannot extend to the parent. So the recursion returns the one-sided value while the global best absorbs the two-sided value.

COST Complexity & alternatives

Path-sum from every node
O(n²)
Re-exploring subtrees per start node.
One post-order DFS
O(n)
Visit each node once; O(h) recursion stack.

Space note

Time is O(n) — each node is entered exactly once. Extra space is O(h) for the call stack, where h is the tree height: O(log n) if balanced, O(n) in the worst case (a degenerate "linked-list" tree).

Pattern transfer →the "return one thing, record another" post-order trick powers Diameter of a Binary Tree (return depth, record longest path), Longest Univalue Path, and House Robber III(return a (rob, skip) pair, record the best). Whenever the global answer can bend at a node but the value handed upward can't, reach for this template.

RUN IT Post-order: clamp child gains, score the through-path

step 0 / 11
STARTPost-order DFS. Each node returns its max downward gain = val + max(0, leftGain, rightGain) to its parent, while we update globalBest with the full through-node path val + leftGain + rightGain.
State
−∞
globalBest
current nodechild gain (clamped ≥ 0)through-path sumglobalBest
slowfast

TYPESCRIPT The solution, annotated

maxPathSum.ts
class TreeNode {
  val: number;
  left: TreeNode | null;
  right: TreeNode | null;
  constructor(val = 0, left: TreeNode | null = null, right: TreeNode | null = null) {
    this.val = val;
    this.left = left;
    this.right = right;
  }
}

function maxPathSum(root: TreeNode | null): number {
  let best = -Infinity;                       // global answer (any node may start it)

  // returns the max DOWNWARD gain of a path that starts at `node`
  function gain(node: TreeNode | null): number {
    if (node === null) return 0;              // missing child contributes nothing

    const left = Math.max(0, gain(node.left));   // clamp: skip negative branches
    const right = Math.max(0, gain(node.right));

    best = Math.max(best, node.val + left + right);  // path THROUGH node (can't go up)

    return node.val + Math.max(left, right);     // extend to parent via ONE side only
  }

  gain(root);
  return best;
}

Reading it block by block

The global accumulator. best starts at -Infinity, not 0 — the optimal path might consist of a single negative node (e.g. a tree of all negatives), and 0 would wrongly beat it.
Base case. A null child returns 0 gain. Combined with the Math.max(0, …)clamp at the parent, this cleanly means "a missing or losing branch adds nothing."
Clamp the children. left and right are each forced to be ≥ 0. This is the single most-missed line: a negative subtree should be excluded from the path, which is exactly what taking max(0, gain) does.
Record the through-path. val + left + right is the best path whose highest node is the current one, using both branches. We fold it into best but never return it.
Return the upward gain. The parent can only attach through one side, so we return val + max(left, right) — a straight downward line, not the bent path. After gain(root) finishes, best holds the answer.
Complexity → O(n) time (each node visited once) and O(h) stack space, where h is the tree height — O(log n) balanced, O(n) worst case.

INTERVIEWFollow-ups they'll ask

  • "Why initialize best to -Infinity and not 0?" A tree of all-negative values has a negative answer; 0 would shadow it. The single largest (least-negative) node must remain a candidate.
  • "Reconstruct the actual path, not just its sum?" Store the best node (and which child branches it used) alongside best when you update it, then walk down from that node.
  • "Why return one side but record two?"A path is a simple line; bending through both children at a node makes a peak that can't continue to the parent without revisiting the node.
  • "How does this relate to tree diameter?" Identical shape: return depth upward, record leftDepth + rightDepthglobally. Same post-order "return one, record another" template.

MNEMONIC The one-liner

"Clamp the kids, record the bend, return one side."

TRIGGERS When you see ___ → reach for ___

max path between any two nodespost-order DFS + global best
answer can bend at a noderecord val+left+right, return val+max(left,right)
negative subtree branchclamp with max(0, gain(child))
all-negative values possibleinit best = -Infinity

SKELETON The reusable shape

skeleton.ts
let best = -Infinity;
function gain(node) {
  if (!node) return 0;
  const l = Math.max(0, gain(node.left));
  const r = Math.max(0, gain(node.right));
  best = Math.max(best, node.val + l + r);  // through node
  return node.val + Math.max(l, r);         // upward: one side
}
gain(root);
return best;

FLASHCARDS Tap to flip

What does the recursion RETURN to the parent?
The one-sided downward gain val + max(leftGain, rightGain) — a parent can only attach through a single branch.
tap to flip
1 / 6
Answer to reveal explanations. No penalty for retries.Score: 0 / 7
QUESTION 01
What value does gain(node) return to its parent?
QUESTION 02
Why are child gains wrapped in Math.max(0, …)?
QUESTION 03
Why must best be initialized to -Infinity rather than 0?
QUESTION 04
For root = [-10,9,20,null,null,15,7], the maximum path sum is:
QUESTION 05
Which traversal order does the algorithm rely on?
QUESTION 06
Optimal time and space complexity?
QUESTION 07
The "through-node" path (val + leftGain + rightGain) is recorded into best but never returned because: