Introduction to Binary Tree Maximum Path Sum
The Binary Tree Maximum Path Sum problem is one of the most classic and challenging tree-based problems you'll encounter in coding interviews and competitive programming. Given a binary tree where each node contains an integer value (which can be positive, negative, or zero), the task is to find the maximum sum of any path connecting nodes in the tree. A path in this context is defined as a sequence of nodes where each pair of adjacent nodes has an edge connecting them, and no node appears more than once in the sequence.
This problem is deceptively simple to state but requires a deep understanding of tree traversal, recursion, and dynamic programming on trees. In this tutorial, we'll break down the problem, understand its nuances, and build a complete Python solution step by step.
What Is a Path in a Binary Tree?
Before diving into the solution, it's crucial to understand what constitutes a valid path. A path in a binary tree:
- Starts at any node in the tree
- Ends at any node in the tree
- Follows parent-child connections between adjacent nodes
- Does not revisit any node (no cycles)
- Can pass through the root, but doesn't have to
The path does not need to pass through the root, and it does not need to start or end at a leaf node. This flexibility is what makes the problem interesting — the optimal path could be entirely contained within one subtree.
Visual Example
Consider the following binary tree:
-10
/ \
9 20
/ \
15 7
The maximum path sum here is 42, achieved by the path 15 -> 20 -> 7. Notice that this path does not include the root node (-10), which would only reduce the sum. This example illustrates why a naive approach that only considers root-to-leaf paths would fail.
Why This Problem Matters
The Binary Tree Maximum Path Sum problem tests several fundamental skills simultaneously:
- Tree traversal: You must navigate the tree efficiently, typically using post-order traversal.
- Recursive thinking: The optimal substructure of the problem lends itself naturally to recursion.
- State management: You need to distinguish between local (subtree-level) and global (tree-level) maximums.
- Edge case handling: Negative values, single-node trees, and skewed trees all require careful consideration.
Companies like Amazon, Microsoft, Google, and Facebook frequently ask this problem because it effectively separates candidates who can write basic tree traversals from those who can reason about complex recursive state. It also appears in real-world scenarios such as network routing optimization, decision tree analysis, and game theory computations on hierarchical structures.
Breaking Down the Problem
The key insight is that at any given node, there are two types of information we need to track:
1. The Maximum Path Sum Through a Node (Global Candidate)
At any node, the maximum path that passes through that node could include:
- Just the node itself (if both children contribute negative sums)
- The node plus one of its children (forming an L-shaped or straight path)
- The node plus both children (forming an inverted-V shape)
This value is a candidate for the global maximum path sum, but it cannot be returned to the parent node because a path cannot branch in two directions and then continue upward.
2. The Maximum Path Sum Extending Upward (Return Value)
When returning a value to the parent node, we can only extend the path in one direction. Therefore, we return the maximum of:
- The node's value alone
- The node's value plus the maximum upward path from the left child
- The node's value plus the maximum upward path from the right child
If this value is negative, we can choose to return 0 (effectively not including this subtree in any parent path), since including a negative contribution would only reduce the sum.
Step-by-Step Solution in Python
Now let's build the complete solution. We'll use a post-order traversal approach where we process both children before the current node, allowing us to make decisions based on complete subtree information.
Defining the Tree Node
First, let's define the structure of a binary tree node:
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
The Complete Solution
Here is the full implementation with detailed comments:
class Solution:
def maxPathSum(self, root: TreeNode) -> int:
# Initialize the global maximum to negative infinity
# to handle trees with all negative values
self.max_sum = float('-inf')
def max_gain(node):
"""
Returns the maximum path sum that can be extended
upward from this node to its parent.
"""
if not node:
return 0
# Recursively get the maximum gain from left and right subtrees.
# If the gain is negative, we discard it (return 0) because
# including a negative path would only reduce the total sum.
left_gain = max(max_gain(node.left), 0)
right_gain = max(max_gain(node.right), 0)
# The maximum path sum that passes through this node
# (this is a candidate for the global maximum).
# This path can include both children, forming an inverted-V.
current_path_sum = node.val + left_gain + right_gain
# Update the global maximum if this path is better
self.max_sum = max(self.max_sum, current_path_sum)
# For the return value, we can only choose ONE branch
# to extend upward, because a path cannot split.
return node.val + max(left_gain, right_gain)
max_gain(root)
return self.max_sum
How the Algorithm Works
Let's trace through the algorithm using our earlier example tree:
-10
/ \
9 20
/ \
15 7
The post-order traversal visits nodes in this order: 9, 15, 7, 20, -10.
- Node 9 (leaf):
left_gain = 0,right_gain = 0,current_path_sum = 9,max_sum = 9, returns9. - Node 15 (leaf):
current_path_sum = 15,max_sum = 15, returns15. - Node 7 (leaf):
current_path_sum = 7,max_sum = 15, returns7. - Node 20:
left_gain = 15,right_gain = 7,current_path_sum = 20 + 15 + 7 = 42,max_sum = 42, returns20 + 15 = 35. - Node -10:
left_gain = 9,right_gain = 35,current_path_sum = -10 + 9 + 35 = 34,max_sum = 42(unchanged), returns-10 + 35 = 25.
The final answer is 42, which corresponds to the path 15 -> 20 -> 7.
Testing the Solution
Let's write test cases to verify our solution handles various scenarios:
def build_tree(values):
"""Helper function to build a tree from a list of values (level-order)."""
if not values:
return None
root = TreeNode(values[0])
queue = [root]
i = 1
while queue and i < len(values):
node = queue.pop(0)
if i < len(values) and values[i] is not None:
node.left = TreeNode(values[i])
queue.append(node.left)
i += 1
if i < len(values) and values[i] is not None:
node.right = TreeNode(values[i])
queue.append(node.right)
i += 1
return root
# Test Case 1: Standard example
root1 = build_tree([-10, 9, 20, None, None, 15, 7])
sol = Solution()
print(f"Test 1: {sol.maxPathSum(root1)}") # Expected: 42
# Test Case 2: Single node
root2 = TreeNode(5)
print(f"Test 2: {sol.maxPathSum(root2)}") # Expected: 5
# Test Case 3: All negative values
root3 = build_tree([-3, -1, -2])
print(f"Test 3: {sol.maxPathSum(root3)}") # Expected: -1
# Test Case 4: Linear tree (skewed left)
root4 = build_tree([1, 2, None, 3])
print(f"Test 4: {sol.maxPathSum(root4)}") # Expected: 6 (3 -> 2 -> 1)
# Test Case 5: Path through root
root5 = build_tree([1, 2, 3])
print(f"Test 5: {sol.maxPathSum(root5)}") # Expected: 6 (2 -> 1 -> 3)
Complexity Analysis
Time Complexity
The time complexity is O(n), where n is the number of nodes in the tree. This is because we visit each node exactly once during the post-order traversal. Each node performs a constant amount of work: two recursive calls, a few comparisons, and arithmetic operations.
Space Complexity
The space complexity is O(h), where h is the height of the tree. This accounts for the recursion stack. In the worst case (a completely skewed tree), h = n, giving O(n) space. In the best case (a balanced tree), h = log(n), giving O(log n) space.
Best Practices and Common Pitfalls
Best Practices
- Initialize the global maximum properly: Always use
float('-inf')rather than0orfloat('-inf')as the initial value. Using0would fail for trees where all values are negative. - Use a nested helper function: This keeps the global state encapsulated within the main function and avoids polluting the class or module namespace.
- Handle null nodes gracefully: Return
0for null nodes, which naturally handles base cases without special conditional logic. - Test edge cases: Always test with single-node trees, all-negative trees, skewed trees, and large balanced trees.
Common Pitfalls
- Returning the path sum instead of the gain: A common mistake is returning
node.val + left_gain + right_gainto the parent. This is incorrect because a path cannot branch in two directions and continue upward. You must return only one branch. - Not handling negative gains: If you don't clamp negative subtree gains to zero, you might include negative contributions that reduce the overall path sum. For example, if a subtree's best path is
-5, including it would only hurt. - Forgetting to update the global maximum: Some implementations only return values without ever comparing against a global maximum. The global maximum must be updated at every node, not just at the root or leaves.
- Using mutable default arguments: Avoid using mutable objects as default parameter values in Python, as they persist across function calls.
Alternative Approaches
Iterative Post-Order Traversal
If you want to avoid recursion (for example, to prevent stack overflow on very deep trees), you can implement an iterative version using an explicit stack:
class Solution:
def maxPathSum(self, root: TreeNode) -> int:
if not root:
return 0
max_sum = float('-inf')
# Dictionary to store the max gain for each node
gain = {}
# Stack for post-order traversal
stack = [(root, False)]
while stack:
node, visited = stack.pop()
if not node:
continue
if visited:
# Process the node after both children are processed
left_gain = max(gain.get(node.left, 0), 0)
right_gain = max(gain.get(node.right, 0), 0)
current_path_sum = node.val + left_gain + right_gain
max_sum = max(max_sum, current_path_sum)
gain[node] = node.val + max(left_gain, right_gain)
else:
# Push the node back with visited=True, then push children
stack.append((node, True))
stack.append((node.right, False))
stack.append((node.left, False))
return max_sum
This iterative approach has the same time and space complexity but avoids the overhead of recursive function calls. It's particularly useful in environments with limited stack depth or when dealing with extremely deep trees.
Returning Both Values from the Helper
Instead of using a class-level or closure variable for the global maximum, you can return a tuple of (max_gain, max_path_sum) from the helper function:
class Solution:
def maxPathSum(self, root: TreeNode) -> int:
def helper(node):
if not node:
return (0, float('-inf'))
left_gain, left_max = helper(node.left)
right_gain, right_max = helper(node.right)
left_gain = max(left_gain, 0)
right_gain = max(right_gain, 0)
current_path_sum = node.val + left_gain + right_gain
max_path = max(current_path_sum, left_max, right_max)
return (node.val + max(left_gain, right_gain), max_path)
return helper(root)[1]
This approach is purely functional and avoids mutable state, which some developers prefer for clarity and testability.
Real-World Applications
While the Binary Tree Maximum Path Sum problem might seem purely academic, the underlying techniques have practical applications:
- Network routing: Finding the highest-bandwidth path through a hierarchical network topology.
- Financial analysis: Computing maximum profit paths through decision trees representing investment strategies.
- Game AI: Evaluating optimal move sequences in game trees where each node represents a game state with an associated score.
- Bioinformatics: Analyzing phylogenetic trees to find paths with maximum evolutionary distance or similarity scores.
- Organizational analysis: Finding the most valuable communication chain in an organizational hierarchy.
Conclusion
The Binary Tree Maximum Path Sum problem is a masterclass in recursive problem-solving on trees. By carefully distinguishing between the maximum path that passes through a node (a global candidate) and the maximum path that can be extended upward (a return value), we can solve this problem in a single pass with optimal O(n) time complexity. The key takeaways are: use post-order traversal to process children before parents, clamp negative subtree gains to zero to avoid reducing the path sum, maintain a global maximum that gets updated at every node, and return only one branch to the parent since paths cannot split. Mastering this problem not only prepares you for technical interviews but also deepens your understanding of how to reason about state in recursive algorithms — a skill that transfers to countless other tree and graph problems you'll encounter throughout your career.