Introduction to Binary Tree Maximum Path Sum
The Binary Tree Maximum Path Sum problem is one of the most classic and challenging tree-based algorithm problems you will encounter. Given a binary tree where each node contains an integer value (which can be positive, negative, or zero), the goal is to find the maximum sum of any path connecting a sequence of nodes. A path in this context must follow parent-child connections, can start and end at any node, and must contain at least one node.
This problem frequently appears in technical interviews at top-tier companies and on platforms like LeetCode (problem #124). It tests your understanding of tree traversal, recursion, and the ability to carefully reason about how local computations contribute to a global result.
What Is a Path in a Binary Tree?
Before diving into the solution, it is essential to clarify what constitutes a valid path. A path is a sequence of nodes where each pair of adjacent nodes has a parent-child relationship. The path does not need to pass through the root, and it does not need to go from a leaf to another leaf. It can be as short as a single node or as long as a full traversal from one leaf down through a common ancestor to another leaf.
For example, consider the following tree:
1
/ \
2 3
The possible paths include: [1], [2], [3], [2, 1], [3, 1], and [2, 1, 3]. The maximum path sum here is 6, achieved by the path [2, 1, 3].
Why This Problem Matters
This problem matters because it forces you to distinguish between two related but distinct concepts:
- Local maximum gain: The best single-branch contribution a subtree can offer to its parent.
- Global maximum path: The best complete path found anywhere in the tree, which may "turn" at a node and use both its left and right branches.
Mastering this distinction will sharpen your recursive thinking and prepare you for many other tree and graph problems, including diameter calculations, subtree sum problems, and dynamic programming on trees.
Understanding the Core Idea
The key insight is that when we process a node during a post-order traversal, we need to compute two things:
- The maximum sum of a path that extends upward through this node to its parent. This path can only include one of the two child branches (left or right), because a path cannot split and then continue upward.
- The maximum sum of a path that turns at this node, using both the left and right branches. This value cannot be passed upward, but it is a candidate for the global answer.
At each node, we update the global maximum with the "turning" path sum, and we return the "extending" path sum to the parent. This separation is what makes the algorithm both correct and efficient.
Step-by-Step Algorithm
Step 1: Define the Tree Node
In Go, we define a binary tree node using a struct with a value and pointers to left and right children.
package main
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
Step 2: Set Up a Global Maximum
We need a variable to track the maximum path sum found so far. Since node values can be negative, we initialize this variable to the smallest possible integer value using math.MinInt64.
import "math"
var maxSum int = math.MinInt64
Step 3: Write the Recursive Helper
The helper function performs a post-order traversal. For each node, it recursively computes the maximum gain from the left and right subtrees. If a subtree's gain is negative, we treat it as zero because including a negative branch would only reduce the path sum.
func maxGain(node *TreeNode) int {
if node == nil {
return 0
}
// Recursively get the max gain from left and right subtrees.
// Negative gains are clamped to 0 because we can choose not to include them.
leftGain := max(maxGain(node.Left), 0)
rightGain := max(maxGain(node.Right), 0)
// The price of the path that "turns" at this node (uses both children).
turningPath := node.Val + leftGain + rightGain
// Update the global maximum if this turning path is better.
if turningPath > maxSum {
maxSum = turningPath
}
// For the parent, we can only extend through one child branch.
return node.Val + max(leftGain, rightGain)
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
Step 4: Expose the Public Function
The public function resets the global maximum and kicks off the recursion from the root.
func maxPathSum(root *TreeNode) int {
maxSum = math.MinInt64
maxGain(root)
return maxSum
}
Complete Working Example
Below is a complete, runnable Go program that constructs a sample tree and prints the maximum path sum.
package main
import (
"fmt"
"math"
)
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
var maxSum int
func maxPathSum(root *TreeNode) int {
maxSum = math.MinInt64
maxGain(root)
return maxSum
}
func maxGain(node *TreeNode) int {
if node == nil {
return 0
}
leftGain := max(maxGain(node.Left), 0)
rightGain := max(maxGain(node.Right), 0)
turningPath := node.Val + leftGain + rightGain
if turningPath > maxSum {
maxSum = turningPath
}
return node.Val + max(leftGain, rightGain)
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func main() {
// Construct the tree:
// -10
// / \
// 9 20
// / \
// 15 7
root := &TreeNode{Val: -10}
root.Left = &TreeNode{Val: 9}
root.Right = &TreeNode{Val: 20}
root.Right.Left = &TreeNode{Val: 15}
root.Right.Right = &TreeNode{Val: 7}
result := maxPathSum(root)
fmt.Println("Maximum Path Sum:", result) // Output: 42
}
In this example, the maximum path is [15, 20, 7], which sums to 42. Notice that the root node -10 is excluded because including it would reduce the total sum.
Tracing Through the Execution
To build intuition, let us trace the recursion on the example tree:
maxGain(9): no children, returns9. Turning path is9. Global max becomes9.maxGain(15): no children, returns15. Turning path is15. Global max becomes15.maxGain(7): no children, returns7. Turning path is7. Global max stays15.maxGain(20): left gain is15, right gain is7. Turning path is20 + 15 + 7 = 42. Global max becomes42. Returns20 + 15 = 35.maxGain(-10): left gain is9, right gain is35. Turning path is-10 + 9 + 35 = 34. Global max stays42. Returns-10 + 35 = 25.
The final answer is 42, which matches our expectation.
Complexity Analysis
The algorithm visits each node exactly once, so the time complexity is O(n), where n is the number of nodes in the tree. The space complexity is O(h), where h is the height of the tree, due to the recursion stack. In the worst case of a skewed tree, this becomes O(n); for a balanced tree, it is O(log n).
Best Practices
Avoid Global Mutable State in Production
Using a package-level variable like maxSum works fine for a coding challenge, but in production code it is fragile and not safe for concurrent use. A cleaner approach is to pass a pointer to the maximum value or to return both the local gain and the best path sum from the recursive function.
func maxPathSumClean(root *TreeNode) int {
_, best := dfs(root)
return best
}
func dfs(node *TreeNode) (gain int, best int) {
if node == nil {
return 0, math.MinInt64
}
leftGain, leftBest := dfs(node.Left)
rightGain, rightBest := dfs(node.Right)
leftGain = max(leftGain, 0)
rightGain = max(rightGain, 0)
turning := node.Val + leftGain + rightGain
best = max(turning, max(leftBest, rightBest))
gain = node.Val + max(leftGain, rightGain)
return gain, best
}
This version avoids global state entirely and is easier to test and reason about.
Handle Edge Cases Explicitly
Always consider edge cases such as a tree with a single node, a tree where all values are negative, and a tree that is completely skewed. The algorithm above handles all of these correctly because we initialize maxSum to the smallest possible integer and always consider the node's own value as a candidate path.
Use Go's Built-in Max in Newer Versions
If you are using Go 1.21 or later, you can use the built-in max function instead of defining your own. This reduces boilerplate and makes the code more idiomatic.
Test Thoroughly
Write unit tests covering various scenarios:
- A single positive node.
- A single negative node.
- A balanced tree with mixed positive and negative values.
- A tree where the best path does not include the root.
- A tree where the best path is a single leaf.
func TestMaxPathSum(t *testing.T) {
tests := []struct {
name string
root *TreeNode
want int
}{
{"single positive", &TreeNode{Val: 5}, 5},
{"single negative", &TreeNode{Val: -3}, -3},
{"simple tree", buildSimpleTree(), 6},
{"complex tree", buildComplexTree(), 42},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := maxPathSumClean(tt.root)
if got != tt.want {
t.Errorf("got %d, want %d", got, tt.want)
}
})
}
}
Common Mistakes to Avoid
- Returning the turning path to the parent: A common error is to return
node.Val + leftGain + rightGainfrom the helper. This is wrong because a path that uses both children cannot continue upward; it would create a branching path, which violates the definition of a path. - Forgetting to clamp negative gains: If you do not clamp negative subtree gains to zero, you may include branches that reduce the total sum, leading to incorrect results.
- Initializing maxSum to zero: When all node values are negative, initializing to zero will incorrectly report zero as the answer instead of the largest (least negative) single node.
- Not resetting global state between calls: If you reuse the same global variable across multiple test cases without resetting it, results will leak between runs.
Variations and Follow-Up Problems
Once you understand this problem, you can apply the same dual-return technique to related problems:
- Diameter of Binary Tree: Instead of sums, track the length (number of edges) of the longest path.
- Longest Univalue Path: Track the longest path where all nodes have the same value.
- Maximum Sum BST in Binary Tree: Combine path-sum logic with BST validation.
- Path Sum III: Count paths that sum to a target, often solved with prefix sums.
Conclusion
The Binary Tree Maximum Path Sum problem elegantly demonstrates how a single recursive traversal can solve a seemingly complex problem by carefully separating local contributions from global candidates. By clamping negative gains, computing turning paths at each node, and returning only single-branch extensions to parents, you arrive at an efficient O(n) solution. Whether you are preparing for interviews or writing production Go code, mastering this pattern of dual-purpose recursion will serve you well across a wide range of tree and graph challenges. Remember to avoid global mutable state in real applications, test edge cases thoroughly, and leverage Go's modern features to keep your code clean and idiomatic.