title: Construct Quad Tree
slug: construct-quad-tree
difficulty: medium
leetcode_id: 427
leetcode_url: https://leetcode.com/problems/construct-quad-tree/
categories:
  - arrays
  - trees
  - recursion
patterns:
  - slug: matrix-traversal
    is_optimal: true
  - slug: dfs
    is_optimal: false

function_signature: "def construct(grid: list[list[int]]) -> Node:"

test_cases:
  visible:
    - input: { grid: [[0, 1], [1, 0]] }
      expected: [[0, 1], [1, 0], [1, 1], [1, 1], [1, 0]]
    - input: { grid: [[1, 1], [1, 1]] }
      expected: [[1, 1]]
  hidden:
    - input: { grid: [[0]] }
      expected: [[1, 0]]
    - input: { grid: [[1]] }
      expected: [[1, 1]]
    - input: { grid: [[0, 0], [0, 0]] }
      expected: [[1, 0]]
    - input: { grid: [[1, 1, 0, 0], [1, 1, 0, 0], [0, 0, 1, 1], [0, 0, 1, 1]] }
      expected: [[0, 1], [1, 1], [1, 0], [1, 0], [1, 1]]

description: |
  Given a `n * n` matrix `grid` of `0`s and `1`s only. We want to represent `grid` with a Quad-Tree.

  Return *the root of the Quad-Tree representing* `grid`.

  A Quad-Tree is a tree data structure in which each internal node has exactly four children. Besides, each node has two attributes:

  - `val`: `True` if the node represents a grid of `1`s or `False` if the node represents a grid of `0`s. Notice that you can assign the `val` to `True` or `False` when `isLeaf` is `False`, and both are accepted in the answer.
  - `isLeaf`: `True` if the node is a leaf node on the tree or `False` if the node has four children.

  We can construct a Quad-Tree from a two-dimensional area using the following steps:

  1. If the current grid has the same value (i.e., all `1`s or all `0`s), set `isLeaf` to `True` and set `val` to the value of the grid and set the four children to `None` and stop.
  2. If the current grid has different values, set `isLeaf` to `False` and set `val` to any value and divide the current grid into four sub-grids.
  3. Recurse for each of the children with the proper sub-grid.

constraints: |
  - `n == grid.length == grid[i].length`
  - `n == 2^x` where `0 <= x <= 6`

examples:
  - input: "grid = [[0,1],[1,0]]"
    output: "[[0,1],[1,0],[1,1],[1,1],[1,0]]"
    explanation: "The grid has mixed values, so we create an internal node with four leaf children. Each quadrant contains a single cell, so each becomes a leaf node with its respective value."
  - input: "grid = [[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0],[1,1,1,1,1,1,1,1],[1,1,1,1,1,1,1,1],[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0]]"
    output: "[[0,1],[1,1],[0,1],[1,1],[1,0],null,null,null,null,[1,0],[1,0],[1,1],[1,1]]"
    explanation: "The grid is divided into four quadrants. The top-left, bottom-left, and bottom-right quadrants each have uniform values and become leaf nodes. The top-right quadrant has mixed values, so it is further subdivided into four leaf nodes."

explanation:
  intuition: |
    Imagine you have a satellite image that you want to compress. If a region of the image is all one colour, you can represent it with a single value instead of storing every pixel. But if a region has mixed colours, you need to look at it more closely by dividing it into smaller sections.

    This is exactly how a **Quad-Tree** works. Think of it like a recursive "zoom in" strategy:

    - Look at the entire grid. Is it all `0`s or all `1`s?
    - If yes, you're done — represent it with a single leaf node
    - If no, divide it into four equal quadrants and ask the same question for each

    The key insight is that this naturally follows a **divide-and-conquer** approach. At each level, we either stop (uniform region → leaf) or recursively process four smaller subproblems. Since the grid size is always a power of 2, we can always divide evenly until we reach individual cells.

    The elegance of this approach is that homogeneous regions get compressed into single nodes regardless of their size, while only heterogeneous regions require the full tree structure.

  approach: |
    We solve this using a **Divide and Conquer** approach with recursion:

    **Step 1: Define the recursive function**

    - Create a helper function that takes the grid and the boundaries of the current region (row start, column start, and size)
    - This function will return a `Node` representing that region

    &nbsp;

    **Step 2: Check if the region is uniform**

    - Scan all cells in the current region
    - If all cells have the same value, create a leaf node with that value
    - Return immediately — no need to recurse further

    &nbsp;

    **Step 3: Divide into four quadrants**

    - If the region has mixed values, calculate the midpoint (`size // 2`)
    - Recursively build nodes for each quadrant:
      - **Top-left**: starts at `(row, col)`
      - **Top-right**: starts at `(row, col + half)`
      - **Bottom-left**: starts at `(row + half, col)`
      - **Bottom-right**: starts at `(row + half, col + half)`

    &nbsp;

    **Step 4: Combine into an internal node**

    - Create a non-leaf node with `isLeaf = False`
    - Set its four children to the nodes returned from the recursive calls
    - The `val` attribute can be any value for non-leaf nodes (we use `True` by convention)

    &nbsp;

    **Step 5: Start the recursion**

    - Call the helper with the full grid boundaries: `(0, 0, n)`
    - Return the resulting root node

  common_pitfalls:
    - title: Checking Uniformity Inefficiently
      description: |
        A naive approach might check uniformity by iterating through all cells every time, even when recursing into smaller regions. This leads to O(n^2 log n) time complexity.

        The standard approach with O(n^2) checks at each level is acceptable since we only do this once per node, and the total work across all levels is bounded.

        However, for a more optimized solution, you could use prefix sums to check uniformity in O(1) time per region.
      wrong_approach: "Re-scanning the entire grid at every recursion level"
      correct_approach: "Only scan the current region being processed"

    - title: Incorrect Quadrant Boundaries
      description: |
        When dividing the grid, it's easy to get the boundaries wrong. The four quadrants for a region starting at `(row, col)` with size `s` are:

        - Top-left: `(row, col)` with size `s/2`
        - Top-right: `(row, col + s/2)` with size `s/2`
        - Bottom-left: `(row + s/2, col)` with size `s/2`
        - Bottom-right: `(row + s/2, col + s/2)` with size `s/2`

        Off-by-one errors here will cause incorrect tree construction or index out of bounds.
      wrong_approach: "Using inconsistent indexing for quadrant boundaries"
      correct_approach: "Use half = size // 2 and apply consistently to both row and column offsets"

    - title: Forgetting the Base Case
      description: |
        The recursion naturally stops when a region is uniform, but you should also handle the case when size is 1. A single cell is always uniform, so it becomes a leaf node.

        This is implicitly handled by the uniformity check, but it's good to be aware of it as the logical base case.

  key_takeaways:
    - "**Divide and Conquer**: When a problem can be broken into identical subproblems on smaller regions, recursion is the natural approach"
    - "**Quad-Trees for 2D data**: This structure is widely used in image compression, spatial indexing, and collision detection in games"
    - "**Power of 2 constraint**: The guarantee that `n = 2^x` ensures we can always divide evenly, simplifying the logic"
    - "**Lazy evaluation principle**: Only subdivide when necessary — uniform regions don't need further processing"

  time_complexity: "O(n^2 log n) in the worst case. Each cell may be visited O(log n) times across different recursion levels. However, for grids with large uniform regions, many cells are only visited once."
  space_complexity: "O(log n) for the recursion stack depth. The output tree size is O(n^2) in the worst case but is not counted as auxiliary space."

solutions:
  - approach_name: Divide and Conquer
    is_optimal: true
    code: |
      class Node:
          def __init__(self, val: bool, isLeaf: bool,
                       topLeft: 'Node' = None, topRight: 'Node' = None,
                       bottomLeft: 'Node' = None, bottomRight: 'Node' = None):
              self.val = val
              self.isLeaf = isLeaf
              self.topLeft = topLeft
              self.topRight = topRight
              self.bottomLeft = bottomLeft
              self.bottomRight = bottomRight


      def construct(grid: list[list[int]]) -> Node:
          def build(row: int, col: int, size: int) -> Node:
              # Check if all cells in this region have the same value
              first_val = grid[row][col]
              is_uniform = True

              for r in range(row, row + size):
                  for c in range(col, col + size):
                      if grid[r][c] != first_val:
                          is_uniform = False
                          break
                  if not is_uniform:
                      break

              # If uniform, create a leaf node
              if is_uniform:
                  return Node(val=bool(first_val), isLeaf=True)

              # Otherwise, divide into four quadrants
              half = size // 2
              return Node(
                  val=True,  # Value doesn't matter for non-leaf
                  isLeaf=False,
                  topLeft=build(row, col, half),
                  topRight=build(row, col + half, half),
                  bottomLeft=build(row + half, col, half),
                  bottomRight=build(row + half, col + half, half)
              )

          return build(0, 0, len(grid))
    explanation: |
      **Time Complexity:** O(n^2 log n) — Each level of recursion processes the entire grid, and there are O(log n) levels.

      **Space Complexity:** O(log n) — Recursion stack depth equals the number of levels in the tree.

      We recursively check each region for uniformity. If uniform, we create a leaf. Otherwise, we split into four quadrants and recurse. The tree structure naturally emerges from the recursion.

  - approach_name: Divide and Conquer with Prefix Sum
    is_optimal: false
    code: |
      class Node:
          def __init__(self, val: bool, isLeaf: bool,
                       topLeft: 'Node' = None, topRight: 'Node' = None,
                       bottomLeft: 'Node' = None, bottomRight: 'Node' = None):
              self.val = val
              self.isLeaf = isLeaf
              self.topLeft = topLeft
              self.topRight = topRight
              self.bottomLeft = bottomLeft
              self.bottomRight = bottomRight


      def construct(grid: list[list[int]]) -> Node:
          n = len(grid)

          # Build prefix sum for O(1) region sum queries
          prefix = [[0] * (n + 1) for _ in range(n + 1)]
          for r in range(n):
              for c in range(n):
                  prefix[r + 1][c + 1] = (grid[r][c] +
                                          prefix[r][c + 1] +
                                          prefix[r + 1][c] -
                                          prefix[r][c])

          def region_sum(row: int, col: int, size: int) -> int:
              # Sum of region from (row, col) to (row+size-1, col+size-1)
              return (prefix[row + size][col + size] -
                      prefix[row][col + size] -
                      prefix[row + size][col] +
                      prefix[row][col])

          def build(row: int, col: int, size: int) -> Node:
              total = region_sum(row, col, size)
              area = size * size

              # Check uniformity: sum is 0 (all 0s) or sum equals area (all 1s)
              if total == 0:
                  return Node(val=False, isLeaf=True)
              if total == area:
                  return Node(val=True, isLeaf=True)

              # Mixed region: divide into quadrants
              half = size // 2
              return Node(
                  val=True,
                  isLeaf=False,
                  topLeft=build(row, col, half),
                  topRight=build(row, col + half, half),
                  bottomLeft=build(row + half, col, half),
                  bottomRight=build(row + half, col + half, half)
              )

          return build(0, 0, n)
    explanation: |
      **Time Complexity:** O(n^2) — Prefix sum construction is O(n^2), and each node creation is O(1) with O(n^2) nodes maximum.

      **Space Complexity:** O(n^2) — For the prefix sum array.

      This optimised version uses a 2D prefix sum to check region uniformity in O(1) time. If the sum of a region is 0, all cells are 0. If the sum equals the area, all cells are 1. Otherwise, the region has mixed values and must be subdivided. While this has better time complexity, the extra space makes it a trade-off.
