Start learning free

LESSON 331 OF 414 · BINARY TREES

Max Sum BST in Rust: 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,None,None,None,None,None,None,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,None,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,None,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 23
    Ctrl + Enter to check

    RUST · BINARY TREES

    Max Sum Bst in Rust, explained

    Problem overview

    The function max_sum_bst receives the root of a binary tree, an Option<Box<TreeNode>>. Some of the tree's subtrees are binary search trees (BSTs), meaning that every value in a node's left subtree is strictly smaller than the node's value, every value in its right subtree is strictly larger, and both subtrees are BSTs themselves. The function returns the largest sum of values over any subtree that is a BST. The tree has at most 40,000 nodes, with values from -40,000 to 40,000.

    In the tree [1, 4, 3, 2, 4, 2, 5, null, null, null, null, null, null, 4, 6] the whole tree is not a BST, because 4 is on the left of 1. The subtree rooted at 3 has the left child 2 and the right child 5, whose children are 4 and 6. It is a BST with the sum 3 + 2 + 5 + 4 + 6 = 20, and that is the best one. For [-4, -2, -5] every value is negative, so the best choice is the empty BST with sum 0.

    Approach: test each subtree on its own

    The statement defines the answer over any subtree, so the plain solution looks at every node, treats it as the root of a subtree and asks two questions:

    1. Is this subtree a BST?
    2. If so, what is its sum?

    The best sum seen is kept in a running result. It starts at 0, which stands for the empty BST and wins when every candidate is negative.

    Testing whether a subtree is a BST follows the definition directly. A node is the root of a BST when both child subtrees are BSTs, everything in its left subtree is less than the node's value, and everything in its right subtree is greater. The checks "everything is less than" and "everything is greater than" are two small recursive helpers. Note that comparing a node only with its direct children is not enough. In the tree with root 10, left child 5 and a right child 15 under the 5, node 5 is fine on its own, but 15 is on the left side of 10 and breaks the rule for the whole tree. The helpers scan the entire subtree for exactly this reason.

    This approach is easy to get right, but it repeats work: is_bst and sum are recomputed from scratch for every node. The lesson has a second solution that avoids that by having each subtree report its facts to its parent in one pass.

    Step by step

    1. Keep the best answer in result, declared with let mut result = 0 inside max_sum_bst. Solution has no fields to hold it, so dfs will receive it by mutable reference.
    2. Call dfs as Self::dfs(&root, &mut result), then return result. The tree is read-only, so it is borrowed with &.
    3. Declare dfs with the parameters root: &Option<Box<TreeNode>> and result: &mut i32. It returns nothing.
    4. Handle only real nodes with if let Some(node) = root.
    5. Visit both children first: call Self::dfs on &node.left and &node.right with result, so every subtree below is examined.
    6. Test the current subtree: if Self::is_bst(root) is true, update result with the larger of its value and Self::sum(root), written as *result = (*result).max(Self::sum(root)).
    7. Write sum. A match on root: None gives 0, and Some(node) gives node.val plus the sums of the two children.
    8. Write everything_less with the parameters root and val: i32. An empty subtree is fine (true). A node passes when node.val < val and both child subtrees pass with the same val.
    9. Write everything_greater as the mirror image with node.val > val.
    10. Write is_bst. An empty tree is a BST. For a node, combine four checks with &&: the left subtree is a BST, the right subtree is a BST, everything on the left is less than node.val, and everything on the right is greater.

    The solution in Rust

    impl Solution {
        fn sum(root: &Option<Box<TreeNode>>) -> i32 {
            match root {
                None => 0,
                Some(node) => node.val + Self::sum(&node.left) + Self::sum(&node.right),
            }
        }
    
        fn everything_less(root: &Option<Box<TreeNode>>, val: i32) -> bool {
            match root {
                None => true,
                Some(node) => {
                    node.val < val
                        && Self::everything_less(&node.left, val)
                        && Self::everything_less(&node.right, val)
                }
            }
        }
    
        fn everything_greater(root: &Option<Box<TreeNode>>, val: i32) -> bool {
            match root {
                None => true,
                Some(node) => {
                    node.val > val
                        && Self::everything_greater(&node.left, val)
                        && Self::everything_greater(&node.right, val)
                }
            }
        }
    
        fn is_bst(root: &Option<Box<TreeNode>>) -> bool {
            match root {
                None => true,
                Some(node) => {
                    Self::is_bst(&node.left)
                        && Self::is_bst(&node.right)
                        && Self::everything_less(&node.left, node.val)
                        && Self::everything_greater(&node.right, node.val)
                }
            }
        }
    
        fn dfs(root: &Option<Box<TreeNode>>, result: &mut i32) {
            if let Some(node) = root {
                Self::dfs(&node.left, result);
                Self::dfs(&node.right, result);
                if Self::is_bst(root) {
                    *result = (*result).max(Self::sum(root));
                }
            }
        }
    
        fn max_sum_bst(root: Option<Box<TreeNode>>) -> i32 {
            let mut result = 0;
            Self::dfs(&root, &mut result);
            result
        }
    }
    

    Rust details that matter here:

    • Borrowed trees. Every helper takes &Option<Box<TreeNode>>. In match root and if let Some(node) = root, matching a reference gives node as a reference, so nothing is moved out of the tree and the same subtree can be examined again and again. The recursive calls use &node.left and &node.right.
    • (*result).max(...). result is a &mut i32, so the number is read and written through *result. The parentheses are needed because a method call binds tighter than the *: *result.max(...) would call max on result itself and then apply * to the outcome.
    • Passing result on. Inside dfs it is already a mutable reference, so the recursive calls pass it as it is, and it is usable again after each call.
    • Final expressions and &&. The bodies of the match arms have no semicolons, so their values are the values of the functions. && stops at the first false, which skips the remaining scans.

    Complexity

    Time: balanced trees are fast, but the worst case is O(n³). For one subtree of size s, sum costs O(s). is_bst calls everything_less and everything_greater at every node of the subtree, and each scans a part of it, so one is_bst call costs the sum of all subtree sizes inside, which is O(s · h) for height h. dfs runs is_bst once for every node. For a tree shaped like a chain in which each value is larger than the one above it, every subtree is a BST, so nothing is cut short. Timing that case with 500, 1,000 and 2,000 nodes shows the work growing by about eight times each time the size doubles. For a balanced tree the cost is far lower, because h is small.

    Space: O(h). Nothing is stored except the call stack, whose depth is the height of the tree, which is up to n for a chain.

    The second solution for this problem brings the time down to O(n).

    Edge cases and common mistakes

    • All values negative. [-4, -2, -5] returns 0. The initial result = 0 is the empty BST. Starting from the first node's value would give a negative answer.
    • Checking only the direct children. The tree [10, 5, null, null, 15] has root 10 with a left child 5 whose right child is 15. Comparing each node with its immediate children accepts it, which would give the sum 30. The correct answer is 20, the subtree rooted at 5 (5 + 15), because 15 is on the left side of 10 and is not less than it.
    • Equal values. [2, 2] returns 2. The rule is strict, so a node and its left child with the same value is not a BST, and the best BST is a single node.
    • Single node. A leaf is always a BST. A tree with one node 7 gives 7.
    • Forgetting to examine inner subtrees. If only the root is tested, the answer for the first example would be 0 instead of 20. The recursion on the children is what examines every subtree.
    • Overflow. The biggest possible sum is 40,000 × 40,000 = 1,600,000,000, which still fits in an i32. The lessons build with optimizations, where an overflow would wrap silently.

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