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:
- The smallest subtree containing all deepest nodes within that node's subtree
- 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:
- We need to explore the entire tree to find all nodes at the maximum depth
- We need to traverse from leaves back up to find the lowest common ancestor
- DFS allows us to recursively process subtrees and combine their results
- 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
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:
- We need to know the depth of the deepest nodes in each subtree
- 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:
- The node that represents the smallest subtree containing all deepest nodes in that branch
- 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:
- Recursively call
dfs
on the left child, getting back(l, ld)
wherel
is the smallest subtree containing deepest nodes in the left branch, andld
is the depth of the left subtree - Recursively call
dfs
on the right child, getting back(r, rd)
wherer
is the smallest subtree containing deepest nodes in the right branch, andrd
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 EvaluatorExample 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()
:
-
Start with
dfs(1)
(root)- First, we need results from children, so we call
dfs(2)
anddfs(3)
- First, we need results from children, so we call
-
Process
dfs(2)
- Need results from children, so call
dfs(4)
anddfs(5)
- Need results from children, so call
-
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
- Call
-
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
- Call
-
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
- Left child result:
-
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
- Call
-
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
- Left child result:
-
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
andrd
- 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:
- Recursion call stack: In the worst case (a skewed tree), the recursion depth can be
O(n)
, requiringO(n)
space on the call stack. - 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.
Which of the tree traversal order can be used to obtain elements in a binary search tree in sorted order?
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Want a Structured Path to Master System Design Too? Don’t Miss This!