Problem Walkthrough

Binary Tree Max Path Sum LeetCode 124

Master LeetCode 124 Binary Tree Maximum Path Sum using post-order DFS that tracks the maximum gain each subtree can contribute upward to its parent, while updating a global maximum at every node by considering the full path through both branches — the key to solving this deceptively tricky tree problem.

9 min read|

Max Path Sum

LeetCode 124

Problem Overview

LeetCode 124 — Binary Tree Maximum Path Sum — gives you the root of a binary tree and asks you to find the maximum path sum of any non-empty path. A path in a binary tree is defined as a sequence of nodes where each pair of adjacent nodes has an edge connecting them. The path does not need to pass through the root, but it must be contiguous — you cannot skip nodes or teleport between branches.

The challenge is that node values can be negative. Unlike problems where you simply sum all positive nodes, a negative subtree might drag down an otherwise good path. You must decide, at each node, whether including a given subtree helps or hurts the total sum. The global maximum might be a single node, a path through two subtrees, or anything in between.

This problem is frequently cited as one of the hardest "easy to state, hard to solve" tree problems on LeetCode because the recursive structure that solves it requires you to think about two separate things simultaneously at every node.

  • Constraints: number of nodes is in [1, 30000]
  • Node values are in the range [-1000, 1000]
  • The path must contain at least one node and does not need to pass through the root
  • A path is contiguous — no skipping nodes between any two endpoints
  • Nodes can have negative values — sometimes the best path excludes an entire subtree
  • The answer is a single integer: the maximum sum achievable by any valid path

Why This Is Tricky

The core difficulty is that a path can start and end at any node — it does not have to pass through the root. This means you cannot simply run a DFS and return the sum of the best downward path from the root. The maximum path might be entirely within the left subtree, entirely within the right subtree, or it might cross through some interior node using both its left and right children as arms.

Negative node values compound the difficulty. If both children of a node have negative subtree sums, the best path through that node is just the node itself — both arms are excluded. If only one child has a positive subtree sum, you take just that arm. You cannot decide this statically; it must be evaluated dynamically at each node during the traversal.

A naive approach might try to enumerate all paths, but that is O(n²) or worse. The elegant O(n) solution requires separating two distinct concepts that are computed at the same node but serve different purposes.

💡

Two Separate Concepts at Every Node

At each node, separate two ideas: (1) the max gain this node can contribute upward to its parent — a single-branch value used to build longer paths — and (2) the max path sum passing through this node using both branches, which updates the global answer. Conflating these two is the most common mistake when first attempting this problem.

Post-Order DFS Strategy

The solution uses post-order DFS (left → right → root), which processes children before their parent. For each node, we recursively compute the max gain from the left subtree and the max gain from the right subtree. The max gain from a subtree is the best single-branch path sum starting at that subtree's root and going downward — either through the left child, the right child, or stopping at the node itself.

The critical trick is clamping negative gains to zero using max(gain, 0). If the left subtree's best single-branch sum is negative, we treat it as zero — meaning we simply do not extend the path into that subtree. This handles all negative-value subtree cases cleanly without special-casing.

Once we have the left gain and right gain for a node, we compute the "through" path: node.val + leftGain + rightGain. This is the best path that uses this node as the topmost point (the bend in the path). We update the global maximum with this value. Then we return node.val + max(leftGain, rightGain) to the parent — the single best arm this node can offer upward.

  1. 1Define a global variable maxSum, initialized to -infinity (not 0)
  2. 2Write a recursive helper gain(node) that returns the max single-branch gain from node downward
  3. 3Base case: gain(None) = 0
  4. 4Compute leftGain = max(gain(node.left), 0) to clamp negatives
  5. 5Compute rightGain = max(gain(node.right), 0) to clamp negatives
  6. 6Update maxSum = max(maxSum, node.val + leftGain + rightGain)
  7. 7Return node.val + max(leftGain, rightGain) to the parent

The Two Return Values

Understanding why the function returns one value but computes another is the heart of this problem. The function returns node.val + max(leftGain, rightGain) — the single best arm extending downward. This is what the parent uses to build its own path. A parent can only pick one child arm to extend through; picking both would create a fork that cannot continue upward as a single path.

However, at the current node, we know both arms. The path node.val + leftGain + rightGain uses both arms and treats the current node as the top of an arch. This is a valid path — it starts at the bottom of the left subtree, rises through the current node, and descends into the right subtree. It might be the global maximum. We capture it with the maxSum update before returning.

Every node simultaneously plays two roles: it contributes a single arm upward to its parent (the return value), and it is evaluated as the apex of a complete arch path (the global max update). Both computations happen in the same DFS call, making the O(n) solution possible.

ℹ️

Return vs. Update — The Key Distinction

The function returns the single-branch gain (one arm) for the parent to use, but updates the global max with the full arch path (both arms + node). These are structurally different: the return value must be a path that can be extended further upward, while the global max update considers a complete path that starts and ends somewhere below the current node.

Handling Negative Values

The max(gain, 0) clamping idiom is what makes the algorithm robust against negative values. Without it, a subtree with a net negative sum would drag down the path sum, even if the correct answer is to simply exclude that subtree. By returning 0 instead of a negative gain, we signal to the parent: "there is no benefit in extending the path into this subtree."

This handles the case where both children are negative: leftGain = 0, rightGain = 0, and the best path through the current node is just node.val itself. If node.val is also negative, the global max update still registers it — which matters when every node in the tree is negative and the answer is the single least-negative node.

The edge case of an all-negative tree is why the global max must be initialized to negative infinity, not zero. If initialized to zero, a tree like [-3] would incorrectly return 0 instead of -3. The initialization to -infinity (or to the root value) ensures a correct answer even when every valid path has negative sum.

  1. 1Clamp left subtree gain: leftGain = max(gain(node.left), 0)
  2. 2Clamp right subtree gain: rightGain = max(gain(node.right), 0)
  3. 3If both are zero (both subtrees negative), the path through node is just node.val
  4. 4If one is zero (one subtree negative), the path through node uses only the positive arm
  5. 5If both are positive, the path uses both arms — the arch configuration
  6. 6All five cases are handled by the same two lines of code — no if/else needed

Code Walkthrough: Python and Java

In Python, the global max is stored as a nonlocal variable or as a list of one element to allow mutation inside the nested helper. The helper function gain(node) checks if node is None (returns 0), computes leftGain and rightGain with clamping, updates self.maxSum or the nonlocal variable with node.val + leftGain + rightGain, then returns node.val + max(leftGain, rightGain). The main function initializes maxSum = float('-inf'), calls gain(root), and returns maxSum. Total solution: about 10 lines.

In Java, the global max is stored as an instance variable int maxSum = Integer.MIN_VALUE. The private helper int gain(TreeNode node) returns 0 for null, computes leftGain = Math.max(gain(node.left), 0) and rightGain = Math.max(gain(node.right), 0), updates maxSum = Math.max(maxSum, node.val + leftGain + rightGain), and returns node.val + Math.max(leftGain, rightGain). The public maxPathSum method calls gain(root) and returns maxSum.

Both implementations run in O(n) time since every node is visited exactly once in the DFS. Space complexity is O(h) where h is the height of the tree, due to the recursive call stack. For a balanced tree h = O(log n); for a degenerate (linear) tree h = O(n). There is no iterative version that meaningfully simplifies this problem — the recursive structure maps naturally to the post-order computation.

⚠️

Initialize to -Infinity, Not Zero

Always initialize the global max to float('-inf') in Python or Integer.MIN_VALUE in Java — never to 0. If every node in the tree has a negative value, the best answer is the single least-negative node. Initializing to 0 would incorrectly return 0 for an all-negative tree, since the clamped arch path at every node would be at most 0 from the children, making the update maxSum = max(0, node.val + 0 + 0) = 0 for negative node.val.

Ready to master algorithm patterns?

YeetCode flashcards help you build pattern recognition through active recall and spaced repetition.

Start practicing now