Start learning free

LESSON 307 OF 348 · BINARY TREES

Maximum Sum BST in a Binary Tree in TypeScript: O(n) Post-Order DFS

Practice O(n) 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 13
    Ctrl + Enter to check

    TYPESCRIPT · BINARY TREES

    Max Sum Bst in TypeScript, explained

    Problem overview

    The method maxSumBST receives the root of a binary tree, a TreeNode | null, and returns the largest sum of the keys of any subtree that is also a binary search tree (BST). Keys in the left subtree must be strictly smaller than the node's key, keys in the right subtree strictly larger, and both subtrees must be BSTs. An empty BST is allowed, so the answer is never below 0.

    In [2,1,3] the whole tree is a BST with sum 6. In [5,4,8,3,null,6,3] the whole tree fails, and so does the subtree at 8, whose left child 6 is fine but whose right child 3 is smaller than 8. The best BST is the subtree at 4, whose sum is 4 + 3 = 7. In [-4,-2,-5] all sums are negative, so the answer is 0.

    Approach: ask each subtree for four facts

    A simple solution checks every subtree from scratch, scanning its nodes to test the BST rule and to add them up. That re-reads the same nodes for every ancestor and gets slow on large, unbalanced trees.

    The faster idea is to read each node once and let the children report what the parent needs. A post-order walk visits the children before the node. If each child returns four facts about its subtree, the node can decide in constant time:

    • whether the subtree is a BST;
    • its smallest value;
    • its largest value;
    • its sum.

    A node with value v roots a BST exactly when both child subtrees are BSTs, the left subtree's maximum is below v and the right subtree's minimum is above v. The minimum and maximum make the "everything on the left is smaller" test a single comparison instead of a scan.

    An empty subtree needs special values so that it never blocks a parent. It is a valid BST with sum 0, its minimum is +Infinity and its maximum is -Infinity. Then any value v is above the empty left side's maximum and below the empty right side's minimum, so the checks pass without special cases. The same values also never win a Math.min or Math.max against a real value.

    Whenever a subtree passes, the walk compares its sum with the best so far, which starts at 0 for the empty BST.

    Step by step

    1. Keep the best answer in a field. Declare result as a number and set it to 0 in a constructor.
    2. Call dfs once from maxSumBST, ignore what it returns for the whole tree, and return this.result.
    3. Declare dfs to return a tuple of four values: a boolean, then three numbers (minimum, maximum and sum).
    4. Handle the empty subtree by returning true, positive infinity, negative infinity and 0.
    5. Get the left report by calling dfs on the left child and destructuring into isLeftBST, leftMin, leftMax and leftSum.
    6. Get the right report the same way.
    7. Start with isRootBST as true only when both children are BSTs. It is declared with let because a later check may turn it off.
    8. Compute the sum of this subtree: the node's value plus both child sums. This is needed even when the subtree is not a BST, because the parent receives it.
    9. Compute the minimum and maximum of the subtree by nesting two Math.min or two Math.max calls with the node's own value.
    10. Check the node against its sides: if the value is at most leftMax or at least rightMin, set isRootBST to false.
    11. Update the answer. If the subtree is still a BST, set result to the larger of itself and the sum.
    12. Return the four facts in the order the parent expects: flag, minimum, maximum, sum.

    The solution in TypeScript

    class Solution {
        result: number;
    
        constructor() {
            this.result = 0;
        }
    
        dfs(root: TreeNode | null): [boolean, number, number, number] {
            if (root == null) {
                return [true, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, 0];
            }
            let [isLeftBST, leftMin, leftMax, leftSum] = this.dfs(root.left);
            let [isRightBST, rightMin, rightMax, rightSum] = this.dfs(root.right);
            let isRootBST = isLeftBST && isRightBST;
            let rootSum = root.val + leftSum + rightSum;
            let rootMin = Math.min(Math.min(leftMin, rightMin), root.val);
            let rootMax = Math.max(Math.max(leftMax, rightMax), root.val);
            if (root.val <= leftMax || root.val >= rightMin) {
                isRootBST = false;
            }
            if (isRootBST) {
                this.result = Math.max(this.result, rootSum);
            }
            return [isRootBST, rootMin, rootMax, rootSum];
        }
    
        maxSumBST(root: TreeNode | null): number {
            this.dfs(root);
            return this.result;
        }
    }
    

    Walk through [2,1,3]. The leaves 1 and 3 each have empty children, so for leaf 1 the checks 1 <= -Infinity and 1 >= Infinity are both false. It is a BST with minimum 1, maximum 1 and sum 1, and the same goes for 3. At the root 2, leftMax is 1 and rightMin is 3, so 2 passes both checks. The sum is 6, and result becomes 6.

    The TypeScript details worth a closer look:

    • Tuple return type. [boolean, number, number, number] promises an array of exactly four elements in that order. It is an ordinary array at run time. The destructuring on the receiving side relies on the same order.
    • Number.POSITIVE_INFINITY and Number.NEGATIVE_INFINITY. They are the same values as Infinity and -Infinity. They work in comparisons: any finite number is less than the first and greater than the second.
    • Math.min takes several arguments, so the nested calls could also be written Math.min(leftMin, rightMin, root.val). The nesting is equivalent.
    • Return values and fields. The tuple carries facts upward, while this.result collects the best answer found anywhere. The call in maxSumBST ignores the tuple for the root.
    • State on the object. The field persists as long as the object does, so reusing one Solution object for a second tree would start from the previous answer.

    Complexity

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

    Time: O(n). Each node is processed once, and the work at a node (a few additions and comparisons) does not depend on the size of its subtrees. This is the improvement over checking each subtree from scratch, which can be cubic on a chain-shaped tree.

    Space: O(h). The only extra memory is the recursion, with a four-element array per call. It is O(log n) for a balanced tree and O(n) for a chain.

    Edge cases and common mistakes

    • All negative values. The answer is 0, because result starts there and the empty BST is allowed.
    • A non-BST node still reports its sum. The sum is computed before the BST check and returned in all cases. Otherwise the parents above would get wrong totals.
    • A non-BST node still reports a real minimum and maximum, but its flag is false, which stops every ancestor from counting it as part of a BST.
    • Equal keys. The comparisons use <= and >= to reject a node, so a duplicate on either side breaks the BST. [2,2] gives 2: each single node is a BST, but together they are not.
    • Using the direct children only. Comparing the node with its children's values would miss a deeper violation. The subtree's maximum and minimum carry that information up.
    • Wrong sentinels. If an empty subtree returned 0 for both bounds, a positive leaf would fail root.val <= leftMax. The infinities are what make the empty case harmless.
    • Reordering the tuple. The parent destructures by position. Swapping the minimum and maximum in the return would silently break the checks.

    Testing

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