Start learning free

LESSON 306 OF 348 · BINARY TREES

Maximum Sum BST in Kotlin: Check Every Subtree

Practice O(n2) Read the full explanation

Given a binary tree root, the task is to return the maximum sum of all keys of any sub-tree which is also a Binary Search Tree (BST).

Assume a BST is defined as follows:

  • The left subtree of a node contains only nodes with keys less than the node's key.
  • The right subtree of a node contains only nodes with keys greater than the node's key.
  • Both the left and right subtrees must also be binary search trees.

 

Example 1:

Input: root = [1,4,3,2,4,2,5,null,null,null,null,null,null,4,6]
Output: 20
Explanation: Maximum sum in a valid Binary search tree is obtained in root node with key equal to 3.

Example 2:

Input: root = [4,3,null,1,2]
Output: 2
Explanation: Maximum sum in a valid Binary search tree is obtained in a single root node with key equal to 2.

Example 3:

Input: root = [-4,-2,-5]
Output: 0
Explanation: All values are negatives. Return an empty BST.

Example 4:

Input: root = [2,1,3]
Output: 6

Example 5:

Input: root = [5,4,8,3,null,6,3]
Output: 7

 

Constraints:

  • Each tree has at most 40000 nodes..
  • Each node's value is between [-4 * 10^4 , 4 * 10^4].
STEP 1 OF 19
    Ctrl + Enter to check

    KOTLIN · BINARY TREES

    Max Sum Bst in Kotlin, explained

    Problem overview

    The function maxSumBST receives the root of a binary tree, a TreeNode?, and returns the largest sum of keys over all subtrees that are binary search trees (BSTs). A BST here follows a strict rule at every node: all keys in the left subtree are less than the node's key, all keys in the right subtree are greater, and both subtrees are BSTs themselves. A tree has at most 40,000 nodes, with values between -40,000 and 40,000.

    The empty tree counts as a BST with sum 0. That is why the tree [-4, -2, -5] has the answer 0: every non-empty BST in it has a negative sum, and choosing nothing is better.

    In the first example, the best subtree is the one rooted at the node 3. Its left child is 2 and its right child is 5, and 5 has children 4 and 6. The keys 2 < 3 < 5 and 4 < 5 < 6 obey the rule, and 4 is also greater than 3, so the sum 3 + 2 + 5 + 4 + 6 = 20 is a valid BST sum. The whole tree is not a BST, because its root 1 has 4 in its left subtree.

    Approach: test every node as a possible BST root

    Every node is the root of exactly one subtree. So the direct plan has three parts:

    1. visit every node;
    2. ask whether the subtree rooted there is a BST;
    3. if it is, compute its sum and keep the largest sum seen.

    The answer starts at 0, which also covers the empty-BST case.

    Each part is a small recursive function. sum adds up a subtree. isBST checks the rule. A BST check has to compare a node with everything in each subtree, not only with its direct children. Take the tree [10, 5, 15, null, null, 6, 20]: every node is correctly placed relative to its own children (5 < 10 < 15 and 6 < 15 < 20), yet the whole tree is not a BST, because 6 lies in the right subtree of 10 and is smaller than 10. Only the subtree rooted at 15 is a BST, so the answer is 6 + 15 + 20 = 41. Two helpers, everythingLess and everythingGreater, scan a whole subtree against one bound.

    So isBST(root) holds when:

    • both child subtrees are BSTs,
    • everything in the left subtree is less than root.value, and
    • everything in the right subtree is greater than root.value.

    This lesson prefers clarity over speed: it re-scans subtrees at every node. The second lesson on this problem returns the minimum, maximum and sum from a single pass, so nothing is scanned twice.

    Step by step

    1. Keep the best answer in a property. Declare result as a var of type Int in the class, starting at 0. Every function can read and update it.
    2. Write maxSumBST. Call dfs(root) to visit all subtrees, then return result.
    3. Declare dfs with a TreeNode? parameter and no return value.
    4. Stop at null. An empty subtree has nothing to check, so a bare return ends the call.
    5. Visit both children with dfs, so every node below is handled.
    6. Test this node with isBST(root), which is written later.
    7. Update the best answer to the larger of result and sum(root), using maxOf.
    8. Write sum above it: 0 for null, otherwise the node's value plus the sums of the two children.
    9. Write isBST: true for null, otherwise the four conditions joined with &&, one per line.
    10. Write everythingLess: true for null, otherwise the node's value must be below the bound and the same must hold for both children with the same bound.
    11. Write everythingGreater as its mirror, with the comparison reversed. The bound is named value in both, because val is a Kotlin keyword.

    The solution in Kotlin

    class Solution {
        var result: Int = 0
    
        fun sum(root: TreeNode?): Int {
            if (root == null) {
                return 0
            }
            return root.value + sum(root.left) + sum(root.right)
        }
    
        fun everythingLess(root: TreeNode?, value: Int): Boolean {
            if (root == null) {
                return true
            }
            return root.value < value &&
            everythingLess(root.left, value) &&
            everythingLess(root.right, value)
        }
    
        fun everythingGreater(root: TreeNode?, value: Int): Boolean {
            if (root == null) {
                return true
            }
            return root.value > value &&
            everythingGreater(root.left, value) &&
            everythingGreater(root.right, value)
        }
    
        fun isBST(root: TreeNode?): Boolean {
            if (root == null) {
                return true
            }
            return isBST(root.left) &&
            isBST(root.right) &&
            everythingLess(root.left, root.value) &&
            everythingGreater(root.right, root.value)
        }
    
        fun dfs(root: TreeNode?) {
            if (root == null) {
                return
            }
            dfs(root.left)
            dfs(root.right)
            if (isBST(root)) {
                result = maxOf(result, sum(root))
            }
        }
    
        fun maxSumBST(root: TreeNode?): Int {
            dfs(root)
            return result
        }
    }
    

    The Kotlin details worth knowing:

    • Properties. var result: Int = 0 is a property of the class, with a getter and setter generated for it. Functions in the class use it by name. Because it is state on the object, a second call to maxSumBST on the same Solution object starts with the previous call's answer instead of 0.
    • Line breaks inside && chains. Kotlin ends a statement at the end of the line unless the line cannot be complete. A line that ends with && needs something after it, so the expression continues. Putting the operator at the start of the next line instead would not work, because the previous line would already be a complete statement.
    • Short-circuiting. && stops at the first false, so after one failed check the remaining, possibly costly, scans are skipped.
    • Smart casts. After the null check returns, the compiler treats root as non-null, so root.left and root.value need no ?.. Passing root.left, which may be null, to a parameter of type TreeNode? is fine.
    • A function that returns nothing needs no return type. Its type is Unit, and the bare return leaves early.
    • Naming. val is a keyword, so a parameter can't be called that, and value is used instead. value is also the name of the node's field, so inside these functions root.value is the node's key and value alone is the bound.

    Complexity

    Let n be the number of nodes and h the height of the tree.

    Time: it depends on the shape. The scans in everythingLess, everythingGreater and sum each cost the size of the subtree they walk. isBST on a subtree of size s costs up to O(s * h), because every node below does its own scans. dfs runs that for every node. For a balanced tree the total is around O(n log² n). For a chain-shaped tree, where every node has one child, the worst case reaches O(n³). The early exit of && makes many inputs much cheaper than that, but the second lesson's single pass is the one that keeps O(n) in every case.

    Space: O(h). Only the recursion uses memory, and it never goes deeper than the height of the tree.

    Edge cases and common mistakes

    • All values negative. [-4, -2, -5] returns 0, because result starts at 0 and no sum beats it. Starting from the first node's sum or a very small number would return a negative value.
    • Checking only the direct children. The node's children are not enough: every key in the left subtree must be below the node, which is why the helpers scan whole subtrees.
    • Equal keys. The rule is strict. [5, 5] is not a BST, because the left child is not less than the root. The best BST there is the single node on its own, and the result is 5.
    • A BST inside a bigger non-BST. The tree [1, 4, 3, 2, 4, 2, 5, null, null, null, null, null, null, 4, 6] is not a BST, but the subtree at 3 is, and the answer is 20. This is why every node is a candidate and not only the root.
    • A single node. A leaf is a BST by itself. For [2, 1, 3] the whole tree is a BST and the answer is 6.
    • Skipping the recursion on children. Without the two dfs calls, only the root would be checked.

    Testing

    • [1, 4, 3, 2, 4, 2, 5, null, null, null, null, null, null, 4, 6] gives 20.
    • [4, 3, null, 1, 2] gives 2.
    • [-4, -2, -5] gives 0.
    • [2, 1, 3] gives 6.
    • [5, 4, 8, 3, null, 6, 3] gives 7.
    Keep going with KotlinAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free