Start learning free

LESSON 332 OF 414 · BINARY TREES

Max Sum BST in Rust: O(n) with a 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,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 14
    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>>, and returns the largest sum of values over all subtrees that are binary search trees (BSTs). In a BST, 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. The empty BST counts, with sum 0, so the answer is never negative. The tree has at most 40,000 nodes and the values are between -40,000 and 40,000.

    For [1, 4, 3, 2, 4, 2, 5, null, null, null, null, null, null, 4, 6] the answer is 20: the subtree at 3 (children 2 and 5, with 5's children 4 and 6) is a BST, and 3 + 2 + 5 + 4 + 6 = 20. For [4, 3, null, 1, 2] the answer is 2: the subtree rooted at 3 is not a BST, because its right child 2 is smaller than 3, so the best BST is the single node 2

    Approach: each subtree reports four facts

    The straightforward solution checks every subtree separately, scanning it again and again. This one visits each node only once, bottom up. A post-order DFS handles the children first, and each call returns four facts about its subtree to the parent:

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

    With these four numbers from both children, a node can decide in constant time whether it is the root of a BST. It is, if both children are BSTs, its value is strictly greater than the maximum of its left subtree, and strictly smaller than the minimum of its right subtree. No scan of the subtree is needed, because the minimum and the maximum already summarise it. If the node is a BST root, its subtree sum is a candidate for the answer.

    An empty subtree needs values that never break the test. It is a BST with sum 0, and its minimum is the largest possible number, i32::MAX, while its maximum is the smallest, i32::MIN. Then node.val <= left_max is false for an empty left side, and node.val >= right_min is false for an empty right side, so the missing side never blocks the node.

    The minimum, maximum and sum are computed for every subtree, even one that is not a BST, because the parent needs them. If a subtree isn't a BST, then none of its ancestors can be either, since an ancestor's subtree contains it. The flag carries that fact upwards.

    Step by step

    1. Keep the best answer in a mutable result, starting at 0, and pass it down by mutable reference.
    2. Call the helper Solution::dfs_helper(&root, &mut result), ignore the tuple it returns, and end max_sum_bst with result.
    3. Set up the helper. It takes root: &Option<Box<TreeNode>> and result: &mut i32, and returns (bool, i32, i32, i32): is it a BST, minimum, maximum, sum. Its body is a match on root.
    4. Empty subtree: the None arm returns (true, i32::MAX, i32::MIN, 0).
    5. Left child: in the Some(node) arm, call the helper on &node.left and destructure the result into is_left_bst, left_min, left_max and left_sum.
    6. Right child: do the same on &node.right. Both children are processed before the node, which makes the traversal post-order.
    7. Start the flag: is_root_bst is a let mut set to is_left_bst && is_right_bst.
    8. Sum: root_sum = node.val + left_sum + right_sum. It is needed even if this subtree is not a BST.
    9. Minimum and maximum: root_min is left_min.min(right_min).min(node.val), and root_max is the same with max.
    10. Apply the BST rule: if node.val <= left_max || node.val >= right_min, set is_root_bst to false. The comparisons are strict in the definition, so equal values also break it.
    11. Update the answer: if is_root_bst is still true, set *result to the larger of itself and root_sum.
    12. Report upwards: end the arm with the tuple (is_root_bst, root_min, root_max, root_sum).

    The solution in Rust

    impl Solution {
        fn dfs_helper(root: &Option<Box<TreeNode>>, result: &mut i32) -> (bool, i32, i32, i32) {
            match root {
                None => (true, i32::MAX, i32::MIN, 0),
                Some(node) => {
                    let (is_left_bst, left_min, left_max, left_sum) =
                        Solution::dfs_helper(&node.left, result);
                    let (is_right_bst, right_min, right_max, right_sum) =
                        Solution::dfs_helper(&node.right, result);
                    let mut is_root_bst = is_left_bst && is_right_bst;
                    let root_sum = node.val + left_sum + right_sum;
                    let root_min = left_min.min(right_min).min(node.val);
                    let root_max = left_max.max(right_max).max(node.val);
                    if node.val <= left_max || node.val >= right_min {
                        is_root_bst = false;
                    }
                    if is_root_bst {
                        *result = (*result).max(root_sum);
                    }
                    (is_root_bst, root_min, root_max, root_sum)
                }
            }
        }
    
        fn max_sum_bst(root: Option<Box<TreeNode>>) -> i32 {
            let mut result = 0;
            Solution::dfs_helper(&root, &mut result);
            result
        }
    }
    

    Rust details that matter here:

    • A tuple as a multi-value return. (bool, i32, i32, i32) returns four values at once, and let (a, b, c, d) = ... unpacks them into four variables. The call in max_sum_bst ignores the returned tuple, which is allowed: a function call is a valid statement, and the tuple is simply dropped.
    • Borrowing the tree. The helper takes &Option<Box<TreeNode>>, and the match on a reference gives node as a reference. The recursion uses &node.left and &node.right, so nothing is moved or cloned.
    • Sentinel values. i32::MAX and i32::MIN are associated constants of the integer type. They are the extreme values the type can hold.
    • Chained min and max. left_min.min(right_min).min(node.val) is the smallest of three values, because each call returns an i32 that can call the method again.
    • (*result).max(root_sum). result is a &mut i32, so *result is the number itself. The parentheses make the dereference happen before the method call.
    • Passing result along. It is already a mutable reference, so the recursive calls take it as it is. Writing another &mut there would be a type error.
    • Value of the match. The tuple at the end of the Some arm has no semicolon, so it is the value of the arm, and the whole match is the function's tail expression.

    Complexity

    Time: O(n). Each node is visited once, and the work per node is a few comparisons and additions, not a scan of its subtree. This is a large improvement over the check-every-subtree approach, whose worst case is cubic: on a chain-shaped tree of 2,000 nodes where every subtree is a BST, the first solution takes about two seconds, while this one takes under a millisecond.

    Space: O(h). Only the call stack, as deep as the tree is tall.

    Edge cases and common mistakes

    • All values negative. [-4, -2, -5] returns 0. result starts at 0, the empty BST.
    • Equal values. [2, 2] returns 2. The root equals its left child's maximum, so node.val <= left_max holds and the whole tree is not a BST.
    • A deep value that breaks an ancestor. For [10, 5, null, null, 15], node 5 with its right child 15 is a BST (sum 20), but at node 10 the left maximum is 15, which is not below 10. The root is not a BST, and the answer is 20. This is why the whole subtree's maximum is needed, not just the child's value.
    • Empty sides. A node with no left child sees left_max = i32::MIN, so the first comparison can't fail. If the sentinels were swapped, every leaf would be reported as not a BST.
    • Checking only the child values. In [5, 1, 8, null, null, 4], node 8 has the left child 4, which is fine locally. But 4 is in the right subtree of 5 and is smaller than 5, so the tree rooted at 5 is not a BST. Only the right subtree's minimum reveals this. The answer is 12, the subtree rooted at 8.
    • Overflow. The biggest possible sum is 1,600,000,000, which fits in an i32.

    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