Hard#1247 min readTreesDFSPostorderLeetCode

Binary Tree Maximum Path Sum

DFS returning best-single-path, tracks any-elbow as side effect

Like diameter but with values. Return one-side max to parent, track global two-side max.

Published Apr 16, 2026

Problem

Given a non-empty binary tree, return the maximum path sum of any path. A path is any sequence of nodes connected by parent-child edges; it does not need to start or end at the root, and it cannot visit a node twice.

Input
root = [1, 2, 3]
Output
6
Explanation
Path 2 → 1 → 3 sums to 6.
Input
root = [-10, 9, 20, null, null, 15, 7]
Output
42
Explanation
Path 15 → 20 → 7 sums to 42; the negative root is excluded.
Input
root = [-3]
Output
-3
Explanation
Single node — must include it.

Intuition

This is the diameter problem (#543) with two extra wrinkles:

  1. Sum, not edge count. The contribution of each subtree is its best path sum, not its depth.
  2. Negative paths are optional. If a subtree's best-down sum is negative, don't include it — the path can stop at the current node.

The "elbow" frame still applies: every path has a topmost node where the path enters and exits, possibly going down both sides. So at each node, ask: what's the best sum if my path elbows here? That's node.val + max(0, left_down) + max(0, right_down).

But what we return to our parent is different: the parent can only continue down one side (the path can't fork). So we return node.val + max(0, max(left_down, right_down)).

The clamp max(0, x) is the only nontrivial line in the algorithm.

Interactive concept

Tree Path Sum Split

Return one-sided gain to the parent, but score two-sided paths locally.

Children return usable gains

Negative child gains are clipped to zero because a path can choose not to include them.

-10
9gain 9
20
15gain 15
7gain 7

Approach

DFS, side-effect global max

TimeO(n)
SpaceO(h)
Recommended

A helper returns the best path sum that ends at this node and continues upward through one child. Inside the helper, it also updates a closure-captured best with the elbow-here case (both children + node).

Initialize best to i32::MIN so that even all-negative trees produce the correct answer.

use std::cell::RefCell;
use std::rc::Rc;

impl Solution {
    pub fn max_path_sum(root: Option<Rc<RefCell<TreeNode>>>) -> i32 {
        fn dfs(node: Option<Rc<RefCell<TreeNode>>>, best: &mut i32) -> i32 {
            match node {
                None => 0,
                Some(n) => {
                    let nb = n.borrow();
                    let l = dfs(nb.left.clone(), best).max(0);
                    let r = dfs(nb.right.clone(), best).max(0);
                    *best = (*best).max(nb.val + l + r);
                    nb.val + l.max(r)
                }
            }
        }
        let mut best = i32::MIN;
        dfs(root, &mut best);
        best
    }
}
Rust · runnable

Pure recursion returning (best_down, best_anywhere)

TimeO(n)
SpaceO(h)

Same algorithm, no shared mutable state. Each call returns the pair (best path that goes down through this node clamped at 0, best path seen anywhere in this subtree). The merge step combines child pairs into the parent pair.

Slightly more verbose, but the dataflow is fully explicit — useful for teaching or for when a side effect would be awkward (e.g. parallelizing subtree work).

use std::cell::RefCell;
use std::rc::Rc;

impl Solution {
    pub fn max_path_sum(root: Option<Rc<RefCell<TreeNode>>>) -> i32 {
        fn dfs(node: Option<Rc<RefCell<TreeNode>>>) -> (i32, i32) {
            match node {
                None => (0, i32::MIN),
                Some(n) => {
                    let nb = n.borrow();
                    let (ld, lb) = dfs(nb.left.clone());
                    let (rd, rb) = dfs(nb.right.clone());
                    let l = ld.max(0);
                    let r = rd.max(0);
                    let here = nb.val + l + r;
                    (nb.val + l.max(r), here.max(lb).max(rb))
                }
            }
        }
        dfs(root).1
    }
}
Rust · runnable

Trace

DFS on root = [-10, 9, 20, null, null, 15, 7]:

        -10
        /  \
       9    20
            / \
           15  7
Tree DP#124

Binary Tree Maximum Path Sum walkthrough

tree = [-10, 9, 20, null, null, 15, 7]

Step 1 of 30%
-10
9
20
15
7
-10-9-10-2020-1520-7
best
15
returns
9, 15, 7

9, 15, and 7 have no useful children.

Leaves return their own value

Decision

Clamp absent children to zero.

Update

best becomes 15 after visiting leaf 15.

Why it works

A path may start and end at any node.

Invariant

Return only the best one-sided path to the parent; update the global answer with the two-sided elbow.

Finish

The best path is 15 → 20 → 7 with sum 42.

Returning a fork upward lets a parent reuse two branches illegally.Initializing best to 0 fails for all-negative trees.

The 42 is captured at node 20 where the path elbows (15 20 7). The negative root contributes nothing useful — its returned value 25 is dominated by the previously seen 42.

Edge cases

  • Single nodebest initialized to MIN; the single node still gets compared and wins.
  • All negative values — clamping the children to 0 is what makes "skip this child" possible. Without it, even all-negative trees would force you to add child sums.
  • Single-side path — returning val + max(l, r) (not val + l + r) is what enforces "no fork on the way up".

Takeaway

Tree DP with two outputs per node is the recurring shape. What you return upward is constrained by what your parent can use; what you record in the global is constrained by the full answer at this subtree. Master that split — you've also handled diameter (#543), longest univalue path, balanced-tree-check, and house-robber-III.

More in Binary Trees