Facebook Pixel

865. Smallest Subtree with all the Deepest Nodes

Problem Description

You are given the root of a binary tree. The depth of a node is defined as the shortest distance from that node to the root.

Your task is to find and return the smallest subtree that contains all the deepest nodes in the original tree.

Key concepts to understand:

  • Deepest nodes: These are the nodes that have the largest depth in the entire tree (i.e., they are furthest from the root)
  • Subtree: A subtree of a node consists of that node and all of its descendants
  • Smallest subtree containing all deepest nodes: This is the lowest common ancestor of all the deepest nodes, along with all its descendants

For example, if a tree has multiple nodes at the maximum depth, you need to find their lowest common ancestor. If there's only one deepest node, then the subtree containing just that node is the answer.

The solution uses a recursive approach with DFS (Depth-First Search) that returns two values for each node:

  1. The smallest subtree containing all deepest nodes within that node's subtree
  2. The depth of that subtree

The algorithm works by:

  • Recursively processing left and right subtrees
  • Comparing the depths of left and right subtrees
  • If left subtree is deeper (ld > rd), all deepest nodes are in the left subtree
  • If right subtree is deeper (ld < rd), all deepest nodes are in the right subtree
  • If both subtrees have equal depth (ld == rd), the current node is the lowest common ancestor of all deepest nodes

The function returns the node that represents the smallest subtree containing all the deepest nodes.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: While this is a binary tree problem, a tree is a special type of graph (specifically, a connected acyclic graph). Binary trees have nodes connected by edges, making them a graph structure.

Is it a tree?

  • Yes: The problem explicitly states we're working with a binary tree. We need to traverse the tree structure to find the smallest subtree containing all deepest nodes.

DFS

  • Yes: We arrive at DFS (Depth-First Search) as our solution approach.

Conclusion: The flowchart suggests using DFS for this tree problem.

This makes perfect sense because:

  1. We need to explore the entire tree to find all nodes at the maximum depth
  2. We need to traverse from leaves back up to find the lowest common ancestor
  3. DFS allows us to recursively process subtrees and combine their results
  4. The recursive nature of DFS perfectly matches the recursive structure of trees

The DFS approach in this problem:

  • Recursively traverses to the bottom of the tree
  • Returns depth information from each subtree
  • Compares depths to determine where the deepest nodes are located
  • Identifies the smallest subtree (lowest common ancestor) that contains all deepest nodes
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that we need to find where all the deepest nodes "meet" in the tree. Think of it like finding the common ancestor that's closest to all the deepest leaves.

Let's think about what information we need at each node:

  1. We need to know the depth of the deepest nodes in each subtree
  2. We need to identify which subtree contains the deepest nodes (or if both do)

This naturally leads to a bottom-up approach where we gather information from children before making decisions at the parent level.

Consider these scenarios at any node:

  • If the left subtree has deeper nodes than the right subtree, then all the deepest nodes must be in the left subtree. The answer lies somewhere in the left subtree.
  • If the right subtree has deeper nodes than the left subtree, then all the deepest nodes must be in the right subtree. The answer lies somewhere in the right subtree.
  • If both subtrees have the same depth, then the deepest nodes are spread across both subtrees. The current node becomes the lowest common ancestor of all deepest nodes - this is our answer!

This recursive pattern suggests we should return two pieces of information from each recursive call:

  1. The node that represents the smallest subtree containing all deepest nodes in that branch
  2. The depth of the deepest nodes in that branch

By comparing depths at each level, we can determine whether to "bubble up" the answer from one subtree or declare the current node as the meeting point. The depth comparison naturally guides us to the correct smallest subtree.

The beauty of this approach is that we only need one pass through the tree, computing depths and finding the answer simultaneously through the recursive calls.

Learn more about Tree, Depth-First Search, Breadth-First Search and Binary Tree patterns.

Solution Approach

The solution implements a recursive DFS function that returns a tuple containing two values: the smallest subtree with all deepest nodes, and the depth of the current subtree.

Let's walk through the implementation step by step:

Base Case: When we encounter a null node, we return (None, 0) - there's no subtree and the depth is 0.

Recursive Step: For each non-null node, we:

  1. Recursively call dfs on the left child, getting back (l, ld) where l is the smallest subtree containing deepest nodes in the left branch, and ld is the depth of the left subtree
  2. Recursively call dfs on the right child, getting back (r, rd) where r is the smallest subtree containing deepest nodes in the right branch, and rd is the depth of the right subtree

Decision Logic: After getting results from both children, we compare their depths:

  • If ld > rd: The left subtree is deeper, meaning all the deepest nodes are in the left subtree. We return (l, ld + 1). The +1 accounts for the current node's depth level.

  • If ld < rd: The right subtree is deeper, meaning all the deepest nodes are in the right subtree. We return (r, rd + 1).

  • If ld == rd: Both subtrees have the same depth, which means the deepest nodes are distributed across both subtrees. The current node is therefore the lowest common ancestor of all deepest nodes. We return (root, ld + 1).

Final Result: The main function calls dfs(root) and returns only the first element of the tuple [0], which is the node representing the smallest subtree containing all deepest nodes.

The algorithm runs in O(n) time where n is the number of nodes, as we visit each node exactly once. The space complexity is O(h) where h is the height of the tree, due to the recursive call stack.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example to illustrate how the solution works.

Consider this binary tree:

        1
       / \
      2   3
     / \
    4   5

We want to find the smallest subtree containing all the deepest nodes (nodes 4 and 5).

Step-by-step execution of dfs():

  1. Start with dfs(1) (root)

    • First, we need results from children, so we call dfs(2) and dfs(3)
  2. Process dfs(2)

    • Need results from children, so call dfs(4) and dfs(5)
  3. Process dfs(4) (leaf node)

    • Call dfs(None) for left child → returns (None, 0)
    • Call dfs(None) for right child → returns (None, 0)
    • Both children have depth 0 and are equal
    • Return (node4, 1) - node 4 is the subtree, depth is 1
  4. Process dfs(5) (leaf node)

    • Call dfs(None) for left child → returns (None, 0)
    • Call dfs(None) for right child → returns (None, 0)
    • Both children have depth 0 and are equal
    • Return (node5, 1) - node 5 is the subtree, depth is 1
  5. Back to dfs(2)

    • Left child result: (node4, 1)
    • Right child result: (node5, 1)
    • Compare depths: ld = 1, rd = 1 → they're equal!
    • Since depths are equal, node 2 is the LCA of deepest nodes
    • Return (node2, 2) - node 2 is the subtree, depth is 2
  6. Process dfs(3) (leaf node)

    • Call dfs(None) for left child → returns (None, 0)
    • Call dfs(None) for right child → returns (None, 0)
    • Both children have depth 0 and are equal
    • Return (node3, 1) - node 3 is the subtree, depth is 1
  7. Back to dfs(1) (root)

    • Left child result: (node2, 2)
    • Right child result: (node3, 1)
    • Compare depths: ld = 2, rd = 1 → left is deeper!
    • Since left is deeper, all deepest nodes are in left subtree
    • Return (node2, 3) - pass up node 2, depth is 3
  8. Final result: The function returns node 2, which is the smallest subtree containing all deepest nodes (4 and 5).

Key observations from this walkthrough:

  • At each node, we compare the depths of left and right subtrees
  • When depths are equal (like at node 2), that node becomes the LCA of deepest nodes in its subtree
  • When depths differ (like at node 1), we propagate the result from the deeper side
  • The algorithm correctly identifies node 2 as the smallest subtree containing both deepest nodes 4 and 5

Solution Implementation

1# Definition for a binary tree node.
2# class TreeNode:
3#     def __init__(self, val=0, left=None, right=None):
4#         self.val = val
5#         self.left = left
6#         self.right = right
7
8from typing import Optional, Tuple
9
10class Solution:
11    def subtreeWithAllDeepest(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
12        """
13        Find the smallest subtree that contains all the deepest nodes in the tree.
14      
15        Args:
16            root: The root of the binary tree
17          
18        Returns:
19            The root of the smallest subtree containing all deepest nodes
20        """
21      
22        def dfs(node: Optional[TreeNode]) -> Tuple[Optional[TreeNode], int]:
23            """
24            Perform depth-first search to find the subtree with all deepest nodes.
25          
26            Args:
27                node: Current node being processed
28              
29            Returns:
30                A tuple containing:
31                - The root of the subtree with all deepest nodes in this branch
32                - The depth of the deepest node in this branch
33            """
34            # Base case: if node is None, return None with depth 0
35            if node is None:
36                return None, 0
37          
38            # Recursively process left and right subtrees
39            left_subtree, left_depth = dfs(node.left)
40            right_subtree, right_depth = dfs(node.right)
41          
42            # If left subtree is deeper, all deepest nodes are in the left subtree
43            if left_depth > right_depth:
44                return left_subtree, left_depth + 1
45          
46            # If right subtree is deeper, all deepest nodes are in the right subtree
47            if left_depth < right_depth:
48                return right_subtree, right_depth + 1
49          
50            # If both subtrees have the same depth, current node is the LCA of all deepest nodes
51            return node, left_depth + 1
52      
53        # Return only the node from the tuple (discard the depth)
54        return dfs(root)[0]
55
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 *     int val;
5 *     TreeNode left;
6 *     TreeNode right;
7 *     TreeNode() {}
8 *     TreeNode(int val) { this.val = val; }
9 *     TreeNode(int val, TreeNode left, TreeNode right) {
10 *         this.val = val;
11 *         this.left = left;
12 *         this.right = right;
13 *     }
14 * }
15 */
16class Solution {
17    /**
18     * Finds the smallest subtree containing all deepest nodes.
19     * @param root The root of the binary tree
20     * @return The root of the subtree containing all deepest nodes
21     */
22    public TreeNode subtreeWithAllDeepest(TreeNode root) {
23        return dfs(root).getKey();
24    }
25
26    /**
27     * Performs depth-first search to find the subtree with all deepest nodes.
28     * Returns a pair containing the subtree root and its depth.
29     * 
30     * @param root Current node being processed
31     * @return Pair of (subtree root containing deepest nodes, depth from this node)
32     */
33    private Pair<TreeNode, Integer> dfs(TreeNode root) {
34        // Base case: null node has depth 0
35        if (root == null) {
36            return new Pair<>(null, 0);
37        }
38      
39        // Recursively process left and right subtrees
40        Pair<TreeNode, Integer> leftResult = dfs(root.left);
41        Pair<TreeNode, Integer> rightResult = dfs(root.right);
42      
43        // Extract depths from the results
44        int leftDepth = leftResult.getValue();
45        int rightDepth = rightResult.getValue();
46      
47        // If left subtree is deeper, all deepest nodes are in left subtree
48        if (leftDepth > rightDepth) {
49            return new Pair<>(leftResult.getKey(), leftDepth + 1);
50        }
51      
52        // If right subtree is deeper, all deepest nodes are in right subtree
53        if (leftDepth < rightDepth) {
54            return new Pair<>(rightResult.getKey(), rightDepth + 1);
55        }
56      
57        // If both subtrees have same depth, current node is the LCA of all deepest nodes
58        return new Pair<>(root, leftDepth + 1);
59    }
60}
61
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 *     int val;
5 *     TreeNode *left;
6 *     TreeNode *right;
7 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
8 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
9 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
10 * };
11 */
12class Solution {
13public:
14    TreeNode* subtreeWithAllDeepest(TreeNode* root) {
15        // Define a pair type to store node and its depth
16        using NodeDepthPair = pair<TreeNode*, int>;
17      
18        // Recursive lambda function to find the subtree containing all deepest nodes
19        // Returns a pair of (subtree root, depth from current node)
20        auto findDeepestSubtree = [&](this auto&& findDeepestSubtree, TreeNode* node) -> NodeDepthPair {
21            // Base case: if node is null, return null with depth 0
22            if (!node) {
23                return {nullptr, 0};
24            }
25          
26            // Recursively process left and right subtrees
27            auto [leftSubtree, leftDepth] = findDeepestSubtree(node->left);
28            auto [rightSubtree, rightDepth] = findDeepestSubtree(node->right);
29          
30            // If left subtree is deeper, all deepest nodes are in the left subtree
31            if (leftDepth > rightDepth) {
32                return {leftSubtree, leftDepth + 1};
33            }
34          
35            // If right subtree is deeper, all deepest nodes are in the right subtree
36            if (leftDepth < rightDepth) {
37                return {rightSubtree, rightDepth + 1};
38            }
39          
40            // If both subtrees have the same depth, current node is the LCA of all deepest nodes
41            return {node, leftDepth + 1};
42        };
43      
44        // Start the recursion from root and return the resulting subtree root
45        return findDeepestSubtree(root).first;
46    }
47};
48
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 *     val: number
5 *     left: TreeNode | null
6 *     right: TreeNode | null
7 *     constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 *         this.val = (val===undefined ? 0 : val)
9 *         this.left = (left===undefined ? null : left)
10 *         this.right = (right===undefined ? null : right)
11 *     }
12 * }
13 */
14
15/**
16 * Finds the smallest subtree containing all nodes at the deepest level.
17 * 
18 * @param root - The root of the binary tree
19 * @returns The root node of the smallest subtree containing all deepest nodes
20 */
21function subtreeWithAllDeepest(root: TreeNode | null): TreeNode | null {
22    /**
23     * Performs depth-first search to find the subtree with all deepest nodes.
24     * 
25     * @param node - Current node being processed
26     * @returns A tuple containing:
27     *   - The root of the subtree containing all deepest nodes in this branch
28     *   - The depth of the deepest nodes in this branch
29     */
30    const findDeepestSubtree = (node: TreeNode | null): [TreeNode | null, number] => {
31        // Base case: if node is null, return null with depth 0
32        if (!node) {
33            return [null, 0];
34        }
35      
36        // Recursively process left subtree
37        const [leftSubtree, leftDepth] = findDeepestSubtree(node.left);
38      
39        // Recursively process right subtree
40        const [rightSubtree, rightDepth] = findDeepestSubtree(node.right);
41      
42        // If left subtree has deeper nodes, return the left subtree result
43        if (leftDepth > rightDepth) {
44            return [leftSubtree, leftDepth + 1];
45        }
46      
47        // If right subtree has deeper nodes, return the right subtree result
48        if (leftDepth < rightDepth) {
49            return [rightSubtree, rightDepth + 1];
50        }
51      
52        // If both subtrees have the same depth, current node is the LCA of all deepest nodes
53        return [node, leftDepth + 1];
54    };
55  
56    // Return the subtree root from the DFS result
57    return findDeepestSubtree(root)[0];
58}
59

Time and Space Complexity

Time Complexity: O(n)

The algorithm performs a depth-first search (DFS) traversal of the binary tree. Each node is visited exactly once during the traversal. At each node, the function performs constant time operations:

  • Checking if the node is None
  • Making recursive calls to left and right children
  • Comparing the depths ld and rd
  • Returning the appropriate node and depth

Since we visit all n nodes exactly once and perform O(1) work at each node, the total time complexity is O(n).

Space Complexity: O(n)

The space complexity comes from two sources:

  1. Recursion call stack: In the worst case (a skewed tree), the recursion depth can be O(n), requiring O(n) space on the call stack.
  2. Return values: Each recursive call returns a tuple containing a node reference and an integer, but these don't accumulate as they're used immediately.

In the best case (a balanced tree), the recursion depth would be O(log n), but since we need to consider the worst case, the space complexity is O(n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrectly Returning Child Nodes Instead of Current Node When Depths are Equal

A common mistake is misunderstanding what to return when both subtrees have equal depth. Some might incorrectly think they should return one of the child subtrees or try to merge them somehow.

Incorrect approach:

if left_depth == right_depth:
    # Wrong: returning left or right subtree
    return left_subtree, left_depth + 1  # Misses nodes in the other subtree!

Why it's wrong: When both subtrees have the same depth, it means deepest nodes exist in BOTH left and right subtrees. The current node is the lowest common ancestor that encompasses all these deepest nodes.

Correct approach:

if left_depth == right_depth:
    # Correct: current node is the LCA of all deepest nodes
    return node, left_depth + 1

2. Forgetting to Add 1 to the Depth When Returning

Another frequent error is forgetting to increment the depth when returning from the recursive call, which would result in incorrect depth calculations throughout the tree.

Incorrect approach:

if left_depth > right_depth:
    return left_subtree, left_depth  # Forgot to add 1!

Why it's wrong: Each level up in the recursion represents moving one level closer to the root, so we must increment the depth to accurately track the distance from the current node to its deepest descendant.

Correct approach:

if left_depth > right_depth:
    return left_subtree, left_depth + 1  # Properly accounting for current level

3. Confusing Depth with Height

Some developers might confuse the concept and try to calculate height (distance from node to furthest leaf) when the problem actually uses depth terminology. While in this specific problem the logic works similarly, understanding the distinction prevents confusion.

Clarification:

  • Depth: Distance from root to node (increases as you go down)
  • Height: Distance from node to furthest leaf (what we're actually calculating in the recursive function)

The solution correctly uses height calculation internally (counting up from leaves) even though the problem describes it in terms of depth, which works because we're finding nodes at maximum depth/height.

4. Not Handling Single Node or Skewed Trees Properly

Failing to consider edge cases like a single node tree or completely skewed trees (all nodes in one line) can lead to incorrect assumptions about the tree structure.

Solution verification for edge cases:

  • Single node: Returns (root, 1) from dfs, correctly identifying the single node as containing all deepest nodes
  • Skewed tree: Correctly follows the deeper branch at each step until reaching the deepest node
  • Balanced tree with multiple deepest nodes: Correctly identifies the LCA when depths match

These edge cases are automatically handled by the recursive logic without needing special conditions.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of the tree traversal order can be used to obtain elements in a binary search tree in sorted order?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More