Return the maximum sum of a continuous path in a binary tree. A path is defined by the following characteristics:
Output: 30
New to this one? Start here. Below is the core idea in everyday language, plus a real-world analogy—then the sections that follow build on it with the formal reasoning, code, and step-by-step walkthroughs.
Picture each node as a summit. A path climbs up from one valley, crosses a peak, and descends into another valley — an upside-down “V”. At the peak you may use both descending trails, but when you report a route to the summit above you, you can only offer one trail (a path can’t fork while still going up). And any trail with a negative total is worse than taking none — so you drop it.
Let’s first understand what a path is in a binary tree. An important thing to note is that all paths have a root node. Consider the following binary tree:
Every path that exists in this tree has a corresponding root node, as shown in the three examples below:
Inversely, this means that every node in the tree is the root of some path(s). For example, node 7 in the tree below is the root of four paths. The largest sum rooting from node 7 is 13:
So, to find the maximum path sum, we calculate the maximum sum rooting from each node and return the largest of these sums.
Calculating the maximum path sum at any node
Let’s try adopting a recursive strategy for this. Consider the root node of our example. The maximum sum of a path rooting from node 5 involves the maximum gain we can attain from its left subtree and its right subtree.
Which sums do we expect node 5’s left and right subtrees to return? Consider what happens when the maximum path sums of these subtrees are returned to node 5:
As we can see, when we received the maximum path sum from a recursive call made to node 8, we received the sum of a path with multiple branches. This results in an invalid path formed at node 5.
Below, we can see what we actually want. The sums of the two paths returned here correctly give us the maximum path sum rooting from node 5:
The difference between the valid and invalid paths above lies in the type of path returned by the recursive call to node 8. We can see this difference clearly in the diagram below.
In the left diagram below, an invalid path is formed at node 5 because at node 8, we return node 8’s maximum path sum, which is from a path with two branches.
In the right diagram, a valid path is formed at node 5 because we return the largest sum of a path with a single branch.
This observation highlights that we can’t just return the maximum path sum of a node. So, let’s have a closer look at which path sum we should return, instead.
Identifying the value returned during recursion
Consider node 8 from the above example. The maximum path sum rooting from node 8 is 30. We know from the discussion above that we can’t just return this maximum path sum value:
We know we need to make sure we return a single, continuous path from node 8. This would mean returning a path with node 8 as an end point, which leaves us with two main choices for values we could return:
Between the above two paths, we just return whichever of their sums is larger. Therefore, our return statement is:
return node.val + max(left_sum, right_sum)
Now that we’re getting single continuous path sums from the left and right subtrees, the maximum path sum rooting from a node can be calculated using node.val + left_sum + right_sum. This is done separately from the return statement.
The maximum path sum of the entire tree is found by keeping track of the largest path sum formed at every node.
Handling negative path sums
One final thing to note is that we shouldn’t include the values of left_sum or right_sum if either is negative, as they wouldn’t contribute to a maximum sum. We can do this by setting their values to 0 if they’re negative, which is the same as excluding the left or right path from the sum.
from ds import TreeNode
max_sum = float('-inf')
def max_path_sum(root: TreeNode) -> int:
global max_sum
max_path_sum_helper(root)
return max_sum
def max_path_sum_helper(node: TreeNode) -> int:
global max_sum
# Base case: null nodes have no path sum.
if not node:
return 0
# Collect the maximum gain we can attain from the left and right subtrees, setting
# them to 0 if they’re negative.
left_sum = max(max_path_sum_helper(node.left), 0)
right_sum = max(max_path_sum_helper(node.right), 0)
# Update the overall maximum path sum if the current path sum is larger.
max_sum = max(max_sum, node.val + left_sum + right_sum)
# Return the maximum sum of a single, continuous path with the current node as an
# endpoint.
return node.val + max(left_sum, right_sum)long long g_best;
int gainMP(TreeNode* n) {
if (n == nullptr) {
return 0;
}
int lg = max(gainMP(n->left), 0); // best downward gain on the left (never negative)
int rg = max(gainMP(n->right), 0); // best downward gain on the right
g_best = max(g_best, (long long)n->val + lg + rg); // path that peaks here
return n->val + max(lg, rg); // gain if we extend upward
}
int maxPathSum(TreeNode* root) {
g_best = LLONG_MIN;
gainMP(root);
return (int)g_best;
}long gBest;
int gainMP(TreeNode n) {
if (n == null) return 0;
int lg = Math.max(gainMP(n.left), 0);
int rg = Math.max(gainMP(n.right), 0);
gBest = Math.max(gBest, (long) n.val + lg + rg); // peak here
return n.val + Math.max(lg, rg); // extend upward
}
int maxPathSum(TreeNode root) {
gBest = Long.MIN_VALUE;
gainMP(root);
return (int) gBest;
}Time complexity: The time complexity of max_path_sum is , where denotes the number of nodes in the tree. This is because it traverses each node of the tree once.
Space complexity: The space complexity is due to the space taken up by the recursive call stack, which can grow as large as the height of the binary tree. The largest possible height of a binary tree is .
Two step-by-step walkthroughs of the algorithm below: a valid case (the expected happy path) and an invalid / edge case that exercises the tricky parts. Each step shows the program state as the code runs—the same steps apply to Python, C++ and Java.
| node | left_gain | right_gain | peak = val+lg+rg | best | returns val+max(lg,rg) |
|---|---|---|---|---|---|
| 9 | 0 | 0 | 9 | 9 | 9 |
| 15 | 0 | 0 | 15 | 15 | 15 |
| 7 | 0 | 0 | 7 | 15 | 7 |
| 20 | 15 | 7 | 42 | 42 | 35 |
| −10 | 9 | 35 | 34 | 42 | 25 |
At node 20 the peak path uses both children: 20 + 15 + 7 = 42. The root only offers −10 + 35 = 25 upward and a peak of just 34, so the best stays 42 — the path 15 → 20 → 7. ✓
| node | left_gain | right_gain | peak | best | returns |
|---|---|---|---|---|---|
| −1 | 0 | 0 | −1 | −1 | −1 |
| −3 | 0 | 0 | −3 | −1 | −3 |
| 2 | max(−1,0)=0 | max(−3,0)=0 | 2 | 2 | 2 |
Both children’s gains are negative, so they are clamped to 0 and dropped. The peak at the root is just 2 + 0 + 0 = 2, which beats −1 and −3. answer = 2 (the single node). ✓
best = [float('-inf')] is a one-element list so the nested gain can mutate it. Starting at −∞ means all-negative trees still return their largest single value.
g_best is a 64-bit accumulator seeded with LLONG_MIN so partial sums cannot overflow. Gains are ints (values are bounded).
gBest is a long seeded with Long.MIN_VALUE. The clamp Math.max(gain, 0) and the peak-vs-return split match the other two exactly.