Files
codetutor/backend/data/questions/all-possible-full-binary-trees.yaml

238 lines
10 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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:
- slug: backtracking
is_optimal: false
- slug: dynamic-programming
is_optimal: true
function_signature: "def all_possible_fbt(n: int) -> list[TreeNode]:"
test_cases:
visible:
- input: { n: 7 }
expected: 5
- input: { n: 3 }
expected: 1
hidden:
- input: { n: 1 }
expected: 1
- input: { n: 2 }
expected: 0
- input: { n: 5 }
expected: 2
- input: { n: 9 }
expected: 14
- input: { n: 4 }
expected: 0
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
&nbsp;
**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)
&nbsp;
**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
&nbsp;
**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
&nbsp;
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.