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

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.
