Binary Tree Maximum Path Sum
Every path has one highest node where it bends, so each node returns its best one-sided gain and tries itself as the bend. Solved in one post-order pass in O(n) time and O(h) space, with a step-through, code in five languages and the Big O worked out from the code.
The problem
Section titled “The problem”You are given the root of a binary tree whose values can be negative. A path is a chain of nodes
where each pair of neighbors in the chain is joined by an edge, and no node appears twice. A path has at
least one node. It does not have to pass through the root, and it may go up and then down again.
Return the largest sum of values along any path.
Trees in the examples are written level by level, from left to right. null marks a missing child.
Example 1
Input: root = [-3, 9, 4, null, null, 5, -2]
Output: 15
The path 9, −3, 4, 5 sums to 15. It passes through the negative root, because 9 on one side and 4 + 5 on the other are worth much more than 3.
Example 2
Input: root = [2, -1, 3]
Output: 5
The path 2, 3 sums to 5. Adding −1 would only lower it.
Example 3
Input: root = [-4]
Output: -4
A path needs at least one node, so the answer is −4 even though it is negative.
Constraints
Section titled “Constraints”- The tree has n nodes, where 1 ≤ n ≤ 30,000.
- -1,000 ≤ Node.val ≤ 1,000
Clarifying questions
Section titled “Clarifying questions”- Must the path go through the root?
- No. It can sit anywhere in the tree.
- Can the path go up and then down?
- Yes, but only once. It climbs to one highest node and goes down from there. It cannot branch.
- Can the answer be negative?
- Yes. If every value is negative, the answer is the largest single value.
- Can the path be empty?
- No. It holds at least one node, so 0 is not a safe starting value for the answer.
Spot the pattern
Section titled “Spot the pattern”Every path has exactly one highest node, where it bends. From there it goes down the left side, the right side, both, or neither. So if each node knew the best downward path on each side, it could score the best path that bends at itself. Asking children first and then combining is a tree traversal in post-order: each subtree hands one number up.
Solutions
Section titled “Solutions”There is only one approach worth showing. Listing every path is far slower, and every node must be seen anyway, which is what this single pass does.
One post-order pass O(n)
The idea
Section titled “The idea”Each call of gain(node) answers one question: what is the best sum of a path that starts at this
node and goes down one side? That is the node’s value plus the better of its two children’s gains.
If a child’s gain is negative, taking that side only lowers the sum. So we cut it to 0, which means “do not go that way”.
Before returning, the node also tries itself as the bend: the left gain, plus the node, plus the right
gain. That is the best path whose highest node is this one. We keep the largest such sum in best.
The return value and the score are different on purpose. A path that goes on up to the parent cannot use both sides, because it would branch. So the node returns only one side, but scores both.
Why it never misses the answer: every path has one highest node, and every node is tried as the highest node once, with the best possible left and right parts below it.
Walkthrough
Section titled “Walkthrough”- best = -inf
- def gain(node):
- if node is None: return 0
- left_gain = max(gain(node.left), 0)
- right_gain = max(gain(node.right), 0)
- best = max(best, node.val + left_gain + right_gain)
- return node.val + max(left_gain, right_gain)
- gain(root)
- return best
Each call answers: what is the best sum of a path that starts here and goes down one side? It also tries this node as the top of a path. best starts at −∞. Under a finished node, ↑ shows the gain it sends up.
class Solution { // At least one node is always taken, so start below any real sum. private int best = Integer.MIN_VALUE;
// One post-order pass. Each node returns its best downward gain, and // tries itself as the top of a path. One call per node, so n steps. public int maxPathSum(TreeNode root) { gain(root); return best; }
// The best sum of a path that starts at node and goes down one side. private int gain(TreeNode node) { if (node == null) return 0; // A side that only lowers the sum is better left out. int leftGain = Math.max(gain(node.left), 0); int rightGain = Math.max(gain(node.right), 0); // A path can bend here once: down the left, up, down the right. best = Math.max(best, node.val + leftGain + rightGain); // A path that goes on upward can only use one side. return node.val + Math.max(leftGain, rightGain); }}import sys
# A tree can be as deep as it has nodes (a chain), and Python stops at 1,000 nested calls by default.# Raise the limit to cover the deepest tree the constraints allow.sys.setrecursionlimit(100_000)
# One post-order pass. Each node returns its best downward gain, and# tries itself as the top of a path. One call per node, so n steps.def max_path_sum(root: TreeNode | None) -> int: # At least one node is always taken, so start below any real sum. best = float("-inf")
# The best sum of a path that starts at node and goes down one side. def gain(node: TreeNode | None) -> int: nonlocal best if node is None: return 0 # A side that only lowers the sum is better left out. left_gain = max(gain(node.left), 0) right_gain = max(gain(node.right), 0) # A path can bend here once: down the left, up, down the right. best = max(best, node.val + left_gain + right_gain) # A path that goes on upward can only use one side. return node.val + max(left_gain, right_gain)
gain(root) return bestpublic class Solution{ // At least one node is always taken, so start below any real sum. private int best = int.MinValue;
// One post-order pass. Each node returns its best downward gain, and // tries itself as the top of a path. One call per node, so n steps. public int MaxPathSum(TreeNode root) { Gain(root); return best; }
// The best sum of a path that starts at node and goes down one side. private int Gain(TreeNode node) { if (node == null) return 0; // A side that only lowers the sum is better left out. int leftGain = System.Math.Max(Gain(node.left), 0); int rightGain = System.Math.Max(Gain(node.right), 0); // A path can bend here once: down the left, up, down the right. best = System.Math.Max(best, node.val + leftGain + rightGain); // A path that goes on upward can only use one side. return node.val + System.Math.Max(leftGain, rightGain); }}// One post-order pass. Each node returns its best downward gain, and// tries itself as the top of a path. One call per node, so n steps.func maxPathSum(root *TreeNode) int { // At least one node is always taken, so start below any real sum. best := math.MinInt // The best sum of a path that starts at node and goes down one side. var gain func(node *TreeNode) int gain = func(node *TreeNode) int { if node == nil { return 0 } // A side that only lowers the sum is better left out. leftGain := max(gain(node.Left), 0) rightGain := max(gain(node.Right), 0) // A path can bend here once: down the left, up, down the right. best = max(best, node.Val+leftGain+rightGain) // A path that goes on upward can only use one side. return node.Val + max(leftGain, rightGain) } gain(root) return best}// One post-order pass. Each node returns its best downward gain, and// tries itself as the top of a path. One call per node, so n steps.function maxPathSum(root: TreeNode | null): number { // At least one node is always taken, so start below any real sum. let best = -Infinity; // The best sum of a path that starts at node and goes down one side. const gain = (node: TreeNode | null): number => { if (node === null) return 0; // A side that only lowers the sum is better left out. const leftGain = Math.max(gain(node.left), 0); const rightGain = Math.max(gain(node.right), 0); // A path can bend here once: down the left, up, down the right. best = Math.max(best, node.val + leftGain + rightGain); // A path that goes on upward can only use one side. return node.val + Math.max(leftGain, rightGain); }; gain(root); return best;}1 Find the work that repeats
There is no loop. The work repeats through recursion, so we count the calls of
gain. Each call does a fixed job: two calls, twomaxwith 0, one sum, one update ofbestand one return. So each call counts as 1 step.- best = -inf
- def gain(node):
- if node is None: return 0
- left_gain = max(gain(node.left), 0)
- right_gain = max(gain(node.right), 0)
- best = max(best, node.val + left_gain + right_gain)
- return node.val + max(left_gain, right_gain)
- gain(root)
- return best
2 Count the runs on Example 1
Example 1 has 5 nodes, and each gets one call. The three leaves 9, 5 and −2 each call their 2 empty children, which return 0 at once.
5 node calls + 6 empty calls = 11 calls.
total calls5 nodes + 6 empty children = 113 Write the count for any n
Every node gets one call. A binary tree with n nodes has n + 1 empty child spots, and each gets one call:
n + (n + 1) = 2n + 1 callsCheck it with Example 1: 2 × 5 + 1 = 11.
4 Keep only the part that grows fastest
- Drop the + 1. One extra call does not change the growth.
- Drop the 2. When n doubles, 2n doubles too.
What is left is n.
So the time is O(n).
S Space: count the extra memory the same way
The code keeps one number,
best, plusleft_gainandright_gainin each call. The calls that wait on the call stack form one path from the root down, so there are at most h, the height of the tree. A balanced tree has h about log n, and a chain has h = n. So the space is O(h).bestcall stack (one frame per level)
Tested at the limits
Section titled “Tested at the limits”2,000 random trees of 1 to 9 nodes, mostly with negative values, so the best path is often short or a single node. The one post-order pass gave the same answer every time as a slow check that walks out from every node and tries every path.
| Largest inputs | Result |
|---|---|
| 30,000 nodes (the most allowed), balanced. One post-order pass. | 0.006 s, and under 0.01 MB of extra memory: only about 15 calls wait at once. |
| 30,000 nodes in one long line. One post-order pass. | 0.01 s, and about 4 MB of extra memory: 30,000 calls wait at once, one per level, which is the O(h) space. |
The designbeyondcode digest, every Saturday
New deep dives, interview tips and site news in one short email. Free. Leave with one click.
We never sell or share your email. We only use it to send this newsletter.