Start learning free

LESSON 410 OF 540 · BINARY TREES

Binary Tree Maximum Path Sum in Kotlin: One DFS in O(n) Time

Practice O(n) Read the full explanation

A path in a binary tree is a sequence of nodes where each node is connected by an edge to the next one, and no node appears twice. A path has at least one node, and it doesn't have to go through the root.

The path sum is the sum of the values of the nodes on the path.

Given the root of a binary tree, return the largest path sum of any path in the tree.

The trees below are written in level order (top to bottom, left to right), with null for a missing child.

 

Example 1:

    1
   / \
  2   3

Input: root = [1,2,3]
Output: 6
Explanation: The best path is 2 → 1 → 3, with the sum 2 + 1 + 3 = 6.

Example 2:

   -10
   /  \
  9    20
      /  \
     15   7

Input: root = [-10,9,20,null,null,15,7]
Output: 42
Explanation: The best path is 15 → 20 → 7, with the sum 15 + 20 + 7 = 42. Going on to the root would add -10 and lower the sum.

Example 3:

   -2
   /  \
 -1    -3

Input: root = [-2,-1,-3]
Output: -1
Explanation: Every value is negative, so every longer path has a smaller sum. The best path is the single node -1.

 

Constraints:

  • The number of nodes in the tree is in the range [1, 3 * 104].
  • Each node value is between -1000 and 1000.
  • The tree is at most 400 levels deep.

Note:

The solution should run in O(n) time, where n is the number of nodes: visit each node once.

STEP 1 OF 10
    Ctrl + Enter to check

    KOTLIN · BINARY TREES

    Binary Tree Maximum Path Sum in Kotlin, explained

    Problem overview

    The function maxPathSum receives the root of a binary tree and returns the largest path sum of any path in it. A path is a sequence of nodes joined by edges, with no node repeated. It needs at least one node, and it doesn't have to pass through the root. The lesson provides the TreeNode class: a node has an Int field value and nullable left and right children.

    For the tree with root 1 and children 2 and 3, the best path is 2, 1, 3 with sum 6. For the tree with root -10, left child 9 and right child 20, where 20 has children 15 and 7, the best path is 15, 20, 7 with sum 42: adding the root would only subtract 10. When every value is negative, as in the tree -2 with children -1 and -3, the best path is the single node -1.

    Approach: every path has a highest node

    Each path in a tree goes up from some node to a highest node and then down again. It can bend at most once, at that highest node. So the answer is the best value, over all nodes, of "this node as the highest point of a path".

    For a node, that best path takes the node's own value plus the best downward path into the left child plus the best downward path into the right child. Each of those downward gains is allowed to be empty, because a side with a negative total only lowers the sum, so it is replaced by 0.

    The helper maxGain(node) computes the best sum of a path that starts at node and goes down through one child at a time, or stops at node. It does two jobs at once:

    • it updates best with the bent path through node, using both sides;
    • it returns only the one-sided gain, since the parent can extend a single side. A path that took both sides of this node can't also go up to the parent without visiting the node twice.

    Each node is visited once, so the time is O(n).

    Step by step

    1. Keep the best sum in a property of the class, var best = 0, so both methods can use it.
    2. In maxPathSum, start best at the root's value, root!!.value. The !! says the root isn't null, which the lesson guarantees. Starting at 0 would be wrong: a tree of negative values has no path with a sum of 0, and the answer must be a real path.
    3. Call maxGain(root). Its return value isn't needed here; the call exists for its effect on best.
    4. Return best.
    5. Write maxGain(node: TreeNode?): Int. For a null node, return 0: an empty subtree adds nothing.
    6. Get the left and right gains, each as maxOf(0, maxGain(child)). Both children are done before the node itself.
    7. Update best with maxOf(best, node.value + left + right). This is where a path can bend.
    8. Return node.value + maxOf(left, right), the best path that continues upward through one side only.

    The solution in Kotlin

    class Solution {
        var best = 0
    
        fun maxGain(node: TreeNode?): Int {
            if (node == null) {
                return 0
            }
            val left = maxOf(0, maxGain(node.left))
            val right = maxOf(0, maxGain(node.right))
            best = maxOf(best, node.value + left + right)
            return node.value + maxOf(left, right)
        }
    
        fun maxPathSum(root: TreeNode?): Int {
            best = root!!.value
            maxGain(root)
            return best
        }
    }
    

    The Kotlin details worth a closer look:

    • A property as shared state. var best = 0 belongs to the Solution object, so maxGain can update it while recursing. Because maxPathSum assigns best before every search, a second call on the same object starts fresh: calling it on the tree [1, 2, 3] and then on a single node -3 returns 6 and then -3.
    • maxOf is a standard function that returns the larger of two values. It works on Int and many other types.
    • Smart cast. After if (node == null) return 0, Kotlin treats node as TreeNode in the rest of the function, so node.left and node.value need no ?. or !!.
    • root!!.value asserts non-null. It would throw a NullPointerException for a null root, which the constraints exclude.
    • Int is enough. At most 30,000 nodes with values up to 1,000 give sums of at most 30,000,000, far below the 32-bit limit.

    Complexity

    Time: O(n). maxGain runs once per node, plus once per missing child, and each call does constant work.

    Space: O(h), where h is the height of the tree, for the recursion stack. The tree is at most 400 levels deep, so the recursion is shallow enough. A path of 400 nodes, each with value 1, gives 400.

    Edge cases and common mistakes

    • All negative values. The tree [-2, -1, -3] gives -1. Starting best at 0 would return 0, a sum no path has.
    • A single node. [5] gives 5 and [-3] gives -3.
    • Not clamping at 0. Without maxOf(0, ...), a negative subtree gets added to the path even though leaving it out is better.
    • Returning both sides to the parent. The return value of maxGain uses maxOf(left, right). Adding both would build a path that forks, which isn't a path.
    • Assuming the path goes through the root. In [-10, 9, 20, null, null, 15, 7] the best path, 42, skips the root.
    • A path that doesn't reach the leaves. A path may stop anywhere. In [2, -1] the answer is 2, not 1.

    Testing

    Trees are written in level order, with null for a missing child.

    • [1, 2, 3] gives 6.
    • [-10, 9, 20, null, null, 15, 7] gives 42.
    • [-2, -1, -3] gives -1.
    • [5] gives 5.
    • [-3] gives -3.
    • [2, -1] gives 2.
    • [1, -2, 3] gives 4.
    • [-1, 5, null, 4, null, 3] gives 12.
    Keep going with Kotlin500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free