title: All Possible Full Binary Trees slug: all-possible-full-binary-trees difficulty: medium leetcode_id: 894 leetcode_url: https://leetcode.com/problems/all-possible-full-binary-trees/ categories: - trees - recursion - dynamic-programming patterns: - backtracking - dynamic-programming description: | Given an integer `n`, return *a list of all possible **full binary trees** with* `n` *nodes*. Each node of each tree in the answer must have `Node.val == 0`. Each element of the answer is the root node of one possible tree. You may return the final list of trees in **any order**. A **full binary tree** is a binary tree where each node has exactly `0` or `2` children. constraints: | - `1 <= n <= 20` examples: - input: "n = 7" output: "[[0,0,0,null,null,0,0,null,null,0,0],[0,0,0,null,null,0,0,0,0],[0,0,0,0,0,0,0],[0,0,0,0,0,null,null,null,null,0,0],[0,0,0,0,0,null,null,0,0]]" explanation: "With 7 nodes, there are 5 possible full binary trees. Each tree has nodes with exactly 0 or 2 children." - input: "n = 3" output: "[[0,0,0]]" explanation: "With 3 nodes, there is only one possible full binary tree: a root with two children." explanation: intuition: | A **full binary tree** has a special property: every node has either 0 children (leaf) or exactly 2 children. This constraint immediately tells us something crucial — a full binary tree can only exist when `n` is **odd**. Why? Think about it: we start with a root (1 node). Every time we add children, we must add exactly 2 nodes (not 1). So the count goes 1 → 3 → 5 → 7... always odd. If `n` is even, no valid full binary tree exists. Now, how do we construct all possible trees with `n` nodes? Imagine you're the root node. You must have exactly two subtrees: a left and a right. If you use `i` nodes for the left subtree, you have `n - 1 - i` nodes remaining for the right subtree (subtracting 1 for yourself, the root). The key insight is that this is a **divide and conquer** problem. For each valid split of nodes between left and right subtrees, we recursively find all possible left subtrees and all possible right subtrees, then combine every left-right pair with a new root. This naturally leads to a recursive structure where the answer for `n` nodes depends on answers for smaller values of `n`. approach: | We solve this using **Recursion with Memoisation** — breaking the problem into smaller subproblems and caching results. **Step 1: Handle base cases** - If `n` is even, return an empty list (no full binary tree possible) - If `n == 1`, return a list containing a single leaf node   **Step 2: Recursively build trees** - For a tree with `n` nodes, try all ways to split remaining `n - 1` nodes between left and right subtrees - Left subtree gets `i` nodes, right subtree gets `n - 1 - i` nodes - Since both subtrees must be full binary trees, `i` must be odd and between 1 and `n - 2` - Iterate `i` from 1 to `n - 1` in steps of 2 (only odd values)   **Step 3: Combine subtrees** - Recursively get all possible left subtrees with `i` nodes - Recursively get all possible right subtrees with `n - 1 - i` nodes - For each combination of left and right subtree, create a new root node connecting them - Add each complete tree to the result list   **Step 4: Memoise results** - Cache results for each value of `n` to avoid recomputation - When building trees for `n = 7`, we might need trees for `n = 3` multiple times - Memoisation ensures we compute each subproblem only once   The total number of full binary trees with `n` nodes follows the **Catalan number** sequence, which grows exponentially but is well within bounds for `n <= 20`. common_pitfalls: - title: Forgetting the Odd Constraint description: | A full binary tree can only have an odd number of nodes. If you don't check this upfront, you'll waste computation or return incorrect results for even `n`. For `n = 4`, the answer should be an empty list, not an error or an invalid tree. wrong_approach: "Try to build trees for any n without checking parity" correct_approach: "Return empty list immediately when n is even" - title: Skipping Memoisation description: | Without memoisation, the same subproblems get solved repeatedly. For `n = 15`, computing all trees for `n = 7` happens multiple times during different splits. This leads to exponential time complexity instead of the manageable complexity with caching. The difference can be dramatic — solving `n = 20` goes from impractical to instant. wrong_approach: "Pure recursion without caching" correct_approach: "Use a dictionary or array to cache results for each n" - title: Incorrect Node Splitting description: | When splitting `n` nodes between left and right subtrees, remember that the root takes 1 node. So if the total is `n`, and the root takes 1, you have `n - 1` nodes to distribute. Also, both subtrees must have an odd number of nodes. Iterating through all values of `i` instead of just odd values wastes time on impossible cases. wrong_approach: "Split n nodes instead of n-1, or try even values for subtree sizes" correct_approach: "Iterate i from 1 to n-2 in steps of 2 (odd values only)" key_takeaways: - "**Structural recursion**: When a data structure is defined recursively (like trees), solutions often follow the same recursive structure" - "**Divide and conquer with combinatorics**: Generate all combinations by recursively generating sub-solutions and combining them" - "**Catalan numbers**: Full binary tree counts follow the Catalan sequence — this appears in many tree-related combinatorial problems" - "**Memoisation is essential**: Overlapping subproblems make caching crucial for efficiency in tree-generation problems" time_complexity: "O(2^n). The number of full binary trees with n nodes is the Catalan number C((n-1)/2), which grows exponentially. We generate each tree exactly once." space_complexity: "O(n × 2^n). We store all generated trees, and each tree has O(n) nodes. The recursion stack adds O(n) depth." solutions: - approach_name: Recursion with Memoisation is_optimal: true code: | class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def all_possible_fbt(n: int) -> list[TreeNode]: # Cache to store results for each n memo = {} def build(num_nodes: int) -> list[TreeNode]: # Check cache first if num_nodes in memo: return memo[num_nodes] # Full binary trees only exist for odd n if num_nodes % 2 == 0: return [] # Base case: single node is a valid full binary tree if num_nodes == 1: return [TreeNode(0)] result = [] # Try all ways to split n-1 nodes between left and right # Both subtrees need odd number of nodes, so step by 2 for left_count in range(1, num_nodes, 2): right_count = num_nodes - 1 - left_count # Get all possible left and right subtrees left_trees = build(left_count) right_trees = build(right_count) # Combine every left-right pair with a new root for left in left_trees: for right in right_trees: root = TreeNode(0) root.left = left root.right = right result.append(root) # Cache and return memo[num_nodes] = result return result return build(n) explanation: | **Time Complexity:** O(2^n) — We generate all Catalan((n-1)/2) trees, each requiring O(n) construction time. **Space Complexity:** O(n × 2^n) — Storing all trees, each with O(n) nodes, plus O(n) recursion depth. The memoisation dictionary stores all trees for each odd value from 1 to n. This prevents recomputation when the same subtree size is needed for different parent splits. The nested loops combine all valid left-right subtree pairs. - approach_name: Bottom-Up Dynamic Programming is_optimal: false code: | class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def all_possible_fbt(n: int) -> list[TreeNode]: # Full binary trees only exist for odd n if n % 2 == 0: return [] # dp[i] = list of all full binary trees with i nodes dp = {1: [TreeNode(0)]} # Build up from 3 nodes to n nodes (odd values only) for num_nodes in range(3, n + 1, 2): dp[num_nodes] = [] # Try all ways to split num_nodes-1 between left and right for left_count in range(1, num_nodes, 2): right_count = num_nodes - 1 - left_count # Combine all left-right pairs for left in dp[left_count]: for right in dp[right_count]: root = TreeNode(0) root.left = left root.right = right dp[num_nodes].append(root) return dp.get(n, []) explanation: | **Time Complexity:** O(2^n) — Same as recursive, we build all valid trees. **Space Complexity:** O(n × 2^n) — We store trees for all odd values up to n. This iterative approach builds solutions bottom-up, starting from the base case of 1 node and working up to n nodes. It's equivalent to the recursive solution but makes the memoisation explicit as a dictionary indexed by node count. Some prefer this style as it avoids recursion overhead.