Binary Tree Maximum Path Sum
Finding optimal paths in trees with negative values
This is one of the most challenging tree problems frequently asked in interviews. It requires careful handling of negative values, understanding what constitutes a valid path, and using DFS with global state tracking.
Problem Statement
A path in a binary tree is a sequence of nodes where each pair of adjacent nodes in the sequence has an edge connecting them. A node can only appear in the sequence at most once. Note that the path does not need to pass through the root.
The path sum of a path is the sum of the node's values in the path.
Given the root of a binary tree, return the maximum path sum of any non-empty path.
LeetCode Problem: 124. Binary Tree Maximum Path Sum
Visualization
In this tree, the maximum path sum is 15 + 20 + 7 = 42 (nodes highlighted in green)
Example Cases
Example 1: Example 2: Example 3:
1 -10 -3
/ \ / \
2 3 9 20
/ \
15 7
Max: 6 (2->1->3) Max: 42 (15->20->7) Max: -3 (single node)Key Insight
Two Types of Paths to Consider
For each node, we need to distinguish between:
Path through this node (can bend): The path that includes both left and right subtrees
- This could be the maximum path in the entire tree
- Formula:
node.val + max_gain(left) + max_gain(right)
Path continuing upward (cannot bend): The maximum sum we can contribute to the parent
- Can only include one child (left OR right), not both
- Formula:
node.val + max(max_gain(left), max_gain(right))
Handling Negative Values
- If a subtree's gain is negative, we can choose not to include it (return 0 instead)
- This is why we use
max(0, gain)when computing contributions - A single node might be the maximum path if all other paths are more negative
Solution
def maxPathSum(root) -> int:
max_sum = float('-inf') # Global maximum path sum
def max_gain(node):
nonlocal max_sum
if not node:
return 0
# Recursively get maximum gain from left and right subtrees
# Use max(0, ...) to handle negative subtrees
left_gain = max(0, max_gain(node.left))
right_gain = max(0, max_gain(node.right))
# Path sum through this node (potentially the answer)
path_through_node = node.val + left_gain + right_gain
# Update global maximum
max_sum = max(max_sum, path_through_node)
# Return maximum gain if continuing upward (can only go one direction)
return node.val + max(left_gain, right_gain)
max_gain(root)
return max_sumprivate int maxSum = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
maxSum = Integer.MIN_VALUE;
maxGain(root);
return maxSum;
}
private int maxGain(TreeNode node) {
if (node == null) return 0;
int leftGain = Math.max(0, maxGain(node.left));
int rightGain = Math.max(0, maxGain(node.right));
// Path through this node (potentially the answer)
int pathThroughNode = node.val + leftGain + rightGain;
maxSum = Math.max(maxSum, pathThroughNode);
// Return max gain continuing upward (only one direction)
return node.val + Math.max(leftGain, rightGain);
}Complexity: Time O(n) · Space O(h)
- Time: O(n) because we visit each node exactly once, computing gains bottom-up
- Space: O(h) for recursion stack; global max_sum uses O(1) extra space
Step-by-Step Example
Consider the tree:
-10
/ \
9 20
/ \
15 7Traversal (postorder-like, children before parent):
Node 9 (leaf):
- left_gain = 0, right_gain = 0
- path_through_node = 9 + 0 + 0 = 9
- max_sum = max(-inf, 9) = 9
- returns 9 (to parent)
Node 15 (leaf):
- left_gain = 0, right_gain = 0
- path_through_node = 15
- max_sum = max(9, 15) = 15
- returns 15
Node 7 (leaf):
- path_through_node = 7
- max_sum = max(15, 7) = 15
- returns 7
Node 20:
- left_gain = 15, right_gain = 7
- path_through_node = 20 + 15 + 7 = 42
- max_sum = max(15, 42) = 42
- returns 20 + max(15, 7) = 35
Node -10 (root):
- left_gain = max(0, 9) = 9
- right_gain = max(0, 35) = 35
- path_through_node = -10 + 9 + 35 = 34
- max_sum = max(42, 34) = 42
- returns -10 + 35 = 25
Final answer: 42
Alternative: Without Global Variable
def maxPathSum_no_global(root) -> int:
def dfs(node):
"""Returns (max_path_sum_in_subtree, max_gain_continuing_up)"""
if not node:
return float('-inf'), 0
left_max, left_gain = dfs(node.left)
right_max, right_gain = dfs(node.right)
# Don't take negative gains
left_gain = max(0, left_gain)
right_gain = max(0, right_gain)
# Path through this node
path_through = node.val + left_gain + right_gain
# Maximum in this subtree
subtree_max = max(left_max, right_max, path_through)
# Gain if continuing upward
continuing_gain = node.val + max(left_gain, right_gain)
return subtree_max, continuing_gain
return dfs(root)[0]Complexity: Time O(n) · Space O(h)
- Time: O(n) because each node is visited once; tuple return propagates both values
- Space: O(h) for recursion stack; no global variable needed
Common Mistakes
1. Forgetting to Handle Negative Subtrees
# WRONG: Doesn't handle negative subtrees
def max_gain_wrong(node):
if not node:
return 0
left_gain = max_gain_wrong(node.left)
right_gain = max_gain_wrong(node.right)
return node.val + max(left_gain, right_gain)
# CORRECT: Use max(0, ...) to optionally exclude negative subtrees
left_gain = max(0, max_gain(node.left))2. Returning Path Through Node Instead of Single Direction
# WRONG: Path through node can't be returned upward
return node.val + left_gain + right_gain # Wrong!
# CORRECT: Only one direction when continuing up
return node.val + max(left_gain, right_gain)3. Initializing max_sum to 0
# WRONG: All nodes could be negative
max_sum = 0
# CORRECT: Initialize to negative infinity
max_sum = float('-inf')4. Not Considering Single Node as Valid Path
The problem states the path must be non-empty, so a single node is always valid:
- Even if all values are negative, the answer is the maximum single node value
- Our algorithm handles this because
path_through_node = node.val + 0 + 0 = node.valwhen both subtrees are negative
Complexity Analysis
- Time: O(n) - visit each node exactly once
- Space: O(h) - recursion stack depth, where h is the height of the tree
- O(log n) for balanced trees
- O(n) for skewed trees
Related Problems
Diameter of Binary Tree (LeetCode 543)
Problem: Find the length of the longest path between any two nodes in a tree. The path length is the number of edges.
Key Insight: Same pattern as max path sum, but count edges instead of summing values. Diameter through node = left_height + right_height.
Approach: DFS returning height. At each node, update global diameter with left_height + right_height. Return 1 + max(left_height, right_height).
Complexity: Time O(n), Space O(h)
def diameterOfBinaryTree(root) -> int:
diameter = 0
def height(node):
nonlocal diameter
if not node:
return 0
left_h = height(node.left)
right_h = height(node.right)
# Update diameter (edges = left_h + right_h)
diameter = max(diameter, left_h + right_h)
return 1 + max(left_h, right_h)
height(root)
return diameterComplexity: Time O(n) · Space O(h)
- Time: O(n) because we visit each node once computing heights bottom-up
- Space: O(h) for recursion stack depth
:::
Longest Univalue Path (LeetCode 687)
Problem: Find the longest path where all nodes have the same value. Length is measured in edges.
Key Insight: Extend path only when child value matches current node value. Use same DFS with global state pattern.
Approach: DFS returning path length that can extend upward. Only extend if values match. Update global longest with path through current node.
Complexity: Time O(n), Space O(h)
def longestUnivaluePath(root) -> int:
longest = 0
def dfs(node):
nonlocal longest
if not node:
return 0
left = dfs(node.left)
right = dfs(node.right)
# Extend path only if values match
left_path = left + 1 if node.left and node.left.val == node.val else 0
right_path = right + 1 if node.right and node.right.val == node.val else 0
# Update longest (path through this node)
longest = max(longest, left_path + right_path)
# Return single direction
return max(left_path, right_path)
dfs(root)
return longestComplexity: Time O(n) · Space O(h)
- Time: O(n) because each node is visited once with O(1) value comparisons
- Space: O(h) for recursion stack depth
:::
Pattern: DFS with Global State
This problem exemplifies a common pattern:
def tree_optimization_problem(root):
global_optimum = initial_value
def dfs(node):
nonlocal global_optimum
if not node:
return base_value
left_result = dfs(node.left)
right_result = dfs(node.right)
# Compute value through this node (using both subtrees)
value_through_node = combine(node.val, left_result, right_result)
# Update global optimum
global_optimum = optimize(global_optimum, value_through_node)
# Return value for parent (using at most one subtree)
return extend(node.val, left_result, right_result)
dfs(root)
return global_optimumInterview Applications
Common Variations
- Return the actual path: Track nodes that form the maximum path
- K maximum paths: Find top k path sums
- Path with constraints: Maximum path sum where length <= k
- Negative weight edges: Similar problem on graphs
Follow-up Questions
| Question | Approach |
|---|---|
| What if we need the actual path nodes? | Track path during DFS, update when max changes |
| What if there are millions of nodes? | Consider iterative approach with explicit stack |
| What about N-ary trees? | Extend to consider all children, take top 2 for "through node" |
| What if path must include root? | Simpler: just compute max of (left path + root + right path) |
Interview Tips
- Clarify path definition: Can it be a single node? Does it need to go through root?
- Handle negatives explicitly: State that you're using max(0, gain) to handle negative subtrees
- Distinguish two cases: Make clear the difference between "path through node" and "path continuing up"
- Trace through example: Walk through a small tree to demonstrate your algorithm
Summary
| Aspect | Details |
|---|---|
| Difficulty | Hard |
| Pattern | DFS with global state tracking |
| Key Insight | Distinguish between path through node vs. path continuing up |
| Time Complexity | O(n) |
| Space Complexity | O(h) |
| Common Mistake | Forgetting max(0, gain) for negative subtrees |