questions A (01-matrix - avoid-flood)

This commit is contained in:
2025-05-24 21:40:39 +01:00
parent e8898841cf
commit f757e28b24
55 changed files with 10813 additions and 0 deletions

View File

@@ -0,0 +1,215 @@
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
&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.