← Back to DevBytes

Solving Binary Tree Maximum Path Sum in Python: Step-by-Step Guide

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:

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:

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:

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:

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.

  1. Node 9 (leaf): left_gain = 0, right_gain = 0, current_path_sum = 9, max_sum = 9, returns 9.
  2. Node 15 (leaf): current_path_sum = 15, max_sum = 15, returns 15.
  3. Node 7 (leaf): current_path_sum = 7, max_sum = 15, returns 7.
  4. Node 20: left_gain = 15, right_gain = 7, current_path_sum = 20 + 15 + 7 = 42, max_sum = 42, returns 20 + 15 = 35.
  5. 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

Common Pitfalls

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:

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.

— Ad —

Google AdSense will appear here after approval

← Back to all articles