Start learning free

LESSON 332 OF 415 · BINARY TREES

Max Sum BST in Binary Tree in Swift: 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,nil,nil,nil,nil,nil,nil,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,nil,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,nil,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

    SWIFT · BINARY TREES

    Max Sum Bst in Swift, explained

    Problem overview

    The method maxSumBST receives the root of a binary tree as a TreeNode? and returns the largest sum of values over all subtrees that are binary search trees. In a BST every value in a node's left subtree is smaller than the node, every value in its right subtree is larger, and both subtrees are BSTs. If no BST has a positive sum, the answer is 0, which stands for an empty BST.

    For [2, 1, 3] the whole tree is a BST and the answer is 6. For [-4, -2, -5] every sum is negative and the answer is 0. For [5, 4, 8, 3, null, 6, 3] the best BST is node 4 with its child 3, and the answer is 7.

    Approach: each subtree reports four facts

    The simple approach tests every subtree from scratch by scanning it, which repeats a lot of work. This lesson computes everything in one post-order pass: handle both children first, then the node, and build the node's facts from its children's facts.

    Each call returns a tuple for its subtree:

    • whether the subtree is a BST;
    • its minimum value;
    • its maximum value;
    • the sum of its values.

    With those four numbers, a node doesn't need to rescan its children. It is the root of a BST when both children are BSTs, its value is greater than the left subtree's maximum and smaller than the right subtree's minimum. Comparing with only the maximum on one side and the minimum on the other is enough, because the maximum is the largest value on the left and the minimum is the smallest on the right.

    An empty subtree is a BST with sum 0. Its minimum is Int.max and its maximum is Int.min, which looks backwards but is deliberate: any value is less than the "minimum" of an empty right side, and greater than the "maximum" of an empty left side, so an empty side never blocks the node. When the node's own minimum and maximum are computed, these sentinels also never win against a real value.

    Whenever a node is the root of a BST, its sum is a candidate for the answer, and the best one is kept in a property named result.

    Take [2, 1, 3]. The leaf 1 returns (true, 1, 1, 1), because its empty children leave its minimum and maximum untouched. The leaf 3 returns (true, 3, 3, 3). At the root, 2 is greater than the left maximum 1 and smaller than the right minimum 3, so it is a BST with minimum 1, maximum 3 and sum 6.

    Step by step

    1. Add the property var result and set it to 0, above maxSumBST. Swift infers Int.
    2. Plan maxSumBST. Call dfs(root) and ignore the returned tuple with _ =, then return result. Only the effect on result matters at the top.
    3. Declare dfs with one unlabeled parameter, _ root: TreeNode?, returning (Bool, Int, Int, Int).
    4. Handle an empty subtree with guard let node = root else { ... }, returning (true, Int.max, Int.min, 0). After the guard, use node.
    5. Get the left facts: call dfs(node.left) and unpack the tuple into isLeftBST, leftMin, leftMax and leftSum.
    6. Get the right facts the same way.
    7. Start with the children's verdict: var isRootBST = isLeftBST && isRightBST. It is a var because the next check may turn it off.
    8. Compute the sum: the node's value plus both child sums. It is needed even when the subtree is not a BST, because the parent uses it.
    9. Compute the minimum as the smallest of leftMin, rightMin and node.val.
    10. Compute the maximum as the largest of leftMax, rightMax and node.val.
    11. Check the node against its sides. If node.val <= leftMax or node.val >= rightMin, set isRootBST to false. Both comparisons are strict, so equal values fail.
    12. Update the answer with max(result, rootSum) if isRootBST is still true.
    13. Return the four facts in the same order the parent unpacks them.

    The solution in Swift

    class Solution {
        var result = 0
    
        func dfs(_ root: TreeNode?) -> (Bool, Int, Int, Int) {
            guard let node = root else {
                return (true, Int.max, Int.min, 0)
            }
            let (isLeftBST, leftMin, leftMax, leftSum) = dfs(node.left)
            let (isRightBST, rightMin, rightMax, rightSum) = dfs(node.right)
            var isRootBST = isLeftBST && isRightBST
            let rootSum = node.val + leftSum + rightSum
            let rootMin = min(min(leftMin, rightMin), node.val)
            let rootMax = max(max(leftMax, rightMax), node.val)
            if node.val <= leftMax || node.val >= rightMin {
                isRootBST = false
            }
            if isRootBST {
                result = max(result, rootSum)
            }
            return (isRootBST, rootMin, rootMax, rootSum)
        }
    
        func maxSumBST(_ root: TreeNode?) -> Int {
            _ = dfs(root)
            return result
        }
    }
    

    Swift details worth a closer look:

    • Tuples. (Bool, Int, Int, Int) is a tuple type. A tuple can be returned from a function and unpacked in one statement: let (isLeftBST, leftMin, leftMax, leftSum) = dfs(node.left) declares four constants at once, in order.
    • guard let. The else branch of a guard must leave the function, here with return. If root has a value, the constant node is available for the rest of the function, with no ! anywhere.
    • Int.max and Int.min. They are the largest and smallest values of the 64-bit Int. The code never adds to them, so overflow can't happen; only comparisons use them. Arithmetic overflow in Swift crashes the program, which is a reason to keep sentinels out of sums.
    • _ = dfs(root). The function result is a tuple that the caller doesn't need. Assigning it to _ says that the value is ignored on purpose, and avoids an unused-result warning.
    • Mutating a property. result is changed inside a method of a class, which Swift allows without mutating. The value stays on the object between calls, so use a new Solution for each tree.
    • min and max nesting. The global functions compare two values here, so three values need two calls.

    Complexity

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

    Time: O(n). Each node is visited once, plus one cheap call per missing child. Each visit does constant work: a few additions and comparisons on values it already received. Nothing rescans a subtree.

    Space: O(h). Only the recursion uses memory, as deep as the tree is tall, which is about log n for a balanced tree and n for a chain. Compared with the first lesson, the running time drops from roughly O(n log² n) for a balanced tree, and O(n³) in the worst case, to linear.

    Edge cases and common mistakes

    • All negative values. [-4, -2, -5] returns 0: result starts at 0 and only a larger sum replaces it.
    • A non-BST subtree still reports real numbers. Its minimum, maximum and sum are correct even though the flag is false, which is why they are computed unconditionally. Its ancestors will fail the check anyway because the flag is passed upward.
    • Equal values. [2, 2] returns 2: the root fails the strict check against its left child, so only the single-node subtree counts.
    • Using the wrong side's extreme. The node must be compared with the maximum of the left side and the minimum of the right side. Comparing with the children's values is not enough, because a node deeper in a subtree can violate the rule.
    • Swapped sentinels. If an empty subtree returned Int.min as the minimum and Int.max as the maximum, a leaf would fail the check against its own empty children.
    • Tuple order. The caller unpacks by position. Returning the sum before the minimum would silently mix up the values.
    • Reusing the object. A second call on the same Solution instance starts with the old result.

    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.
    • [2, 2] gives 2.
    Keep going with SwiftAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free