LeetCode 1932 - Merge BSTs to Create Single BST
This problem asks us to take multiple very small binary search trees (BSTs), each with at most three nodes, and attempt to merge them into a single valid BST. Each tree is represented by its root node in the array trees.
Difficulty: 🔴 Hard
Topics: Array, Hash Table, Tree, Depth-First Search, Binary Search Tree, Binary Tree
Solution
Problem Understanding
This problem asks us to take multiple very small binary search trees (BSTs), each with at most three nodes, and attempt to merge them into a single valid BST. Each tree is represented by its root node in the array trees. The only operation allowed is to attach one tree to a leaf of another if the leaf's value matches the root of the second tree. After merging, we remove the attached tree from the list.
The input is an array of n root nodes, where 1 <= n <= 5 * 10^4, and each tree has 1 to 3 nodes. No two root values are the same. The output is either the root of the fully merged BST if possible, or null if no valid BST can be formed.
Important points include that each node in the trees may only have children but no grandchildren, so all trees are extremely small. Leaves are defined as nodes with no children. The BST property requires that all left descendants be strictly smaller than the node, and all right descendants be strictly larger.
Edge cases to consider include trees that cannot merge because no leaf matches another root, trees that merge but violate BST properties, and single-node trees which may simply become the leaf of another tree.
Approaches
Brute Force
A brute-force approach would attempt all possible sequences of merges. For each leaf in every tree, we would check if it matches the root of another tree and perform the merge recursively until either all trees are merged or no valid operations remain. While this guarantees correctness, it is computationally infeasible because the number of merge sequences grows factorially with the number of trees. This would be far too slow for n up to 50,000.
Optimal Approach
The key insight for an optimal solution is to use a hash map to quickly look up which trees could be merged based on leaf values. Instead of trying all sequences, we track which trees are leaves and which are roots. We then attempt to merge any tree whose root is a leaf of another tree. After merging, we ensure that the resulting tree still satisfies BST properties using range checks during a DFS traversal.
By precomputing root-to-tree mappings and leaf-to-parent relationships, we can merge trees in linear time relative to the total number of nodes. We also check that there is exactly one candidate root that is not a leaf in any other tree; this must be the final root if a valid BST exists.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n!) | O(n) | Tries all merge sequences; infeasible for large n |
| Optimal | O(n) | O(n) | Uses hash maps to track roots and leaves; merges in linear time |
Algorithm Walkthrough
- Collect root and leaf information: Iterate over all trees. Map each root value to its tree, and track all leaf values along with their parent trees. Count how many trees contain each leaf.
- Identify the candidate final root: Only the root value that does not appear as any leaf can be the final root. If there is not exactly one such candidate, return
null. - Recursive merge: Starting from the candidate root, recursively attempt to merge any subtree whose root matches a leaf of the current tree. For each merge, remove the merged tree from the root map.
- BST validation: While merging, maintain a valid value range for each node. Use DFS to check that all nodes respect the BST property. If any violation occurs, return
null. - Final check: After merging, ensure that all trees have been consumed. If any tree remains unmerged, return
null. Otherwise, return the fully merged root.
Why it works: Each merge only occurs when a leaf matches a root, ensuring that we never lose tree nodes. The BST validation guarantees the merged tree is valid. By selecting the unique root that is not a leaf in any tree, we ensure a single final tree emerges.
Python Solution
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
from typing import List, Optional
class Solution:
def canMerge(self, trees: List[TreeNode]) -> Optional[TreeNode]:
if not trees:
return None
root_map = {tree.val: tree for tree in trees}
leaves = set()
count_leaves = {}
# Collect all leaves and count occurrences
for tree in trees:
for child in (tree.left, tree.right):
if child:
leaves.add(child.val)
count_leaves[child.val] = count_leaves.get(child.val, 0) + 1
# Candidate root is a root that is not a leaf
candidate_roots = [val for val in root_map if val not in leaves]
if len(candidate_roots) != 1:
return None
root_val = candidate_roots[0]
root = root_map[root_val]
def merge(node: TreeNode, lower: int, upper: int) -> bool:
if not node:
return True
if not (lower < node.val < upper):
return False
if node.val in root_map and node != root:
sub = root_map.pop(node.val)
node.left = sub.left
node.right = sub.right
return merge(node.left, lower, node.val) and merge(node.right, node.val, upper)
if not merge(root, float('-inf'), float('inf')):
return None
return root if len(root_map) == 1 else None
Explanation: We build hash maps to quickly access trees by root value and count leaf occurrences. We select the root that is not anyone's leaf. The recursive merge function attaches subtrees when a leaf matches a root and validates the BST property using a range check. Finally, we ensure that all trees have been merged.
Go Solution
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func canMerge(trees []*TreeNode) *TreeNode {
rootMap := make(map[int]*TreeNode)
leaves := make(map[int]int)
for _, tree := range trees {
rootMap[tree.Val] = tree
if tree.Left != nil {
leaves[tree.Left.Val]++
}
if tree.Right != nil {
leaves[tree.Right.Val]++
}
}
var candidateRoot *TreeNode
for val, tree := range rootMap {
if _, exists := leaves[val]; !exists {
if candidateRoot != nil {
return nil
}
candidateRoot = tree
}
}
if candidateRoot == nil {
return nil
}
var merge func(node *TreeNode, low, high int) bool
merge = func(node *TreeNode, low, high int) bool {
if node == nil {
return true
}
if node.Val <= low || node.Val >= high {
return false
}
if sub, exists := rootMap[node.Val]; exists && node != candidateRoot {
node.Left = sub.Left
node.Right = sub.Right
delete(rootMap, node.Val)
}
return merge(node.Left, low, node.Val) && merge(node.Right, node.Val, high)
}
if !merge(candidateRoot, -1, 50001) {
return nil
}
if len(rootMap) != 1 {
return nil
}
return candidateRoot
}
Explanation: Go requires explicit nil checks and uses maps instead of Python dictionaries. The merge logic mirrors Python, ensuring range validation for BST and merging subtrees only when a leaf matches a root.
Worked Examples
Example 1: trees = [[2,1],[3,2,5],[5,4]]
| Step | Root Map | Leaves | Action |
|---|---|---|---|
| Initial | {2,3,5} | {1,2,4,5} | Identify candidate root 3 |
| Merge 2 into 3 | {3,5} | - | Node 2 attached as left child |
| Merge 5 into 3 | {3} | - | Node 5 attached as right child |
| Final | {3} | - | Valid BST formed |
Example 2: trees = [[5,3,8],[3,2,6]]
| Step | Action | Result |
|---|---|---|
| Candidate root | 5 | Merge 3 into 5 |
| Merge | Violates BST (6 > 5 in left subtree) | Return null |
Example 3: trees = [[5,4],[3]]
| Step | Action | Result |
|---|---|---|
| No leaf matches root | - | Return null |
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each tree and node visited once; merge and validation in linear time |
| Space | O(n) | Hash maps for roots and leaves; recursion stack at most O(log n) |
The complexity is linear because each tree has at most three nodes, so total nodes are O(n). Hash maps allow constant-time lookups for merges.