Taro Logo

Construct Quad Tree

Medium
Apple logo
Apple
1 view
Topics:
TreesRecursion

You are given a n * n matrix grid containing only 0s and 1s. Your task is to represent this grid using a Quad-Tree and return the root of the constructed Quad-Tree. A Quad-Tree is a tree data structure where each internal node has exactly four children.

Each node in the Quad-Tree has two attributes:

  • val: A boolean value representing whether the node represents a grid of 1s (True) or 0s (False).
  • isLeaf: A boolean value indicating whether the node is a leaf node (True) or an internal node with four children (False).

The construction of the Quad-Tree follows these steps:

  1. If the current grid has the same value (i.e., all 1s or all 0s), create a leaf node with isLeaf set to True and val set to the value of the grid. Set the four children to Null.
  2. If the current grid has different values, create an internal node with isLeaf set to False and val set to any value (it doesn't matter for internal nodes). Divide the current grid into four equal sub-grids (top-left, top-right, bottom-left, bottom-right).
  3. Recursively construct Quad-Trees for each of the four sub-grids and assign them as the children of the current node.

For example, consider the following input grid:

[[0, 1],
 [1, 0]]

The corresponding Quad-Tree representation would be:

  • Root node: isLeaf = False, val = True
  • Top-left child: isLeaf = True, val = False
  • Top-right child: isLeaf = True, val = True
  • Bottom-left child: isLeaf = True, val = True
  • Bottom-right child: isLeaf = True, val = False

Another example:

[[1, 1, 1, 1],
 [1, 1, 1, 1],
 [1, 1, 1, 1],
 [1, 1, 1, 1]]

In this case, the entire grid has the same value (all 1s), so the Quad-Tree would simply be a single leaf node:

  • Root node: isLeaf = True, val = True

Write a function that takes a n * n grid as input and returns the root of the constructed Quad-Tree.

Solution


Quad-Tree Construction from Grid

Problem Description

Given a n * n matrix grid of 0s and 1s only, the task is to represent the grid with a Quad-Tree and return the root of the Quad-Tree.

A Quad-Tree is a tree data structure in which each internal node has exactly four children. 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.
  • isLeaf: True if the node is a leaf node on the tree or False if the node has four children.

The Quad-Tree is constructed as follows:

  1. If the current grid has the same value (all 1's or all 0's), set isLeaf to True and val to the value of the grid, and set the four children to Null.
  2. If the current grid has different values, set isLeaf to False, 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.

Naive Approach

The naive approach would be to recursively divide the grid into four sub-grids until we reach a sub-grid that contains only one value (either all 0s or all 1s). This approach directly follows the problem's recursive definition.

  1. Base Case: If the grid has only one element, create a leaf node with the value of that element.
  2. Recursive Step: If the grid has more than one element, check if all elements are the same. If they are, create a leaf node with that value. If not, create an internal node, divide the grid into four sub-grids, and recursively create the four child nodes.

Code (Python)

class Node:
    def __init__(self, val, isLeaf, topLeft=None, topRight=None, bottomLeft=None, bottomRight=None):
        self.val = val
        self.isLeaf = isLeaf
        self.topLeft = topLeft
        self.topRight = topRight
        self.bottomLeft = bottomLeft
        self.bottomRight = bottomRight


class Solution:
    def construct(self, grid: List[List[int]]) -> 'Node':
        n = len(grid)
        
        def build_tree(grid, top_row, top_col, size):
            if size == 1:
                return Node(grid[top_row][top_col] == 1, True)
            
            first_val = grid[top_row][top_col]
            all_same = True
            for i in range(top_row, top_row + size):
                for j in range(top_col, top_col + size):
                    if grid[i][j] != first_val:
                        all_same = False
                        break
                if not all_same:
                    break
            
            if all_same:
                return Node(first_val == 1, True)
            
            half_size = size // 2
            top_left = build_tree(grid, top_row, top_col, half_size)
            top_right = build_tree(grid, top_row, top_col + half_size, half_size)
            bottom_left = build_tree(grid, top_row + half_size, top_col, half_size)
            bottom_right = build_tree(grid, top_row + half_size, top_col + half_size, half_size)
            
            return Node(True, False, top_left, top_right, bottom_left, bottom_right)

        return build_tree(grid, 0, 0, n)

Time Complexity

O(N^2 log N), where N is the dimension of the grid. In the worst case, where the grid has alternating values, we might have to traverse each cell multiple times.

Space Complexity

O(N^2) in the worst case, due to the recursive calls and the space required to store the Quad-Tree nodes. The maximum height of the Quad-Tree is log4(N^2) = log2(N).

Optimal Approach

The optimal approach improves upon the naive approach by minimizing redundant calculations. Since the core operation is checking if a region has the same value, caching/memoizing these results can significantly reduce computations.

  1. Use the same recursive structure as the naive approach.
  2. Instead of repeatedly checking if all elements in a sub-grid are the same, precompute the sums of all sub-grids of different sizes. This allows for a quick O(1) check if a sub-grid contains all 0s or all 1s.

Optimized Code (Python)

class Node:
    def __init__(self, val, isLeaf, topLeft=None, topRight=None, bottomLeft=None, bottomRight=None):
        self.val = val
        self.isLeaf = isLeaf
        self.topLeft = topLeft
        self.topRight = topRight
        self.bottomLeft = bottomLeft
        self.bottomRight = bottomRight

class Solution:
    def construct(self, grid: List[List[int]]) -> 'Node':
        n = len(grid)

        # Precompute prefix sums for efficient region sum calculation
        prefix_sum = [[0] * (n + 1) for _ in range(n + 1)]
        for i in range(1, n + 1):
            for j in range(1, n + 1):
                prefix_sum[i][j] = prefix_sum[i-1][j] + prefix_sum[i][j-1] - prefix_sum[i-1][j-1] + grid[i-1][j-1]

        def region_sum(row1, col1, row2, col2):
            # Calculate the sum of the region bounded by (row1, col1) and (row2, col2)
            return prefix_sum[row2][col2] - prefix_sum[row1][col2] - prefix_sum[row2][col1] + prefix_sum[row1][col1]

        def build_tree(top_row, top_col, size):
            total = region_sum(top_row, top_col, top_row + size, top_col + size)

            if total == 0:
                return Node(False, True)
            if total == size * size:
                return Node(True, True)

            half_size = size // 2
            top_left = build_tree(top_row, top_col, half_size)
            top_right = build_tree(top_row, top_col + half_size, half_size)
            bottom_left = build_tree(top_row + half_size, top_col, half_size)
            bottom_right = build_tree(top_row + half_size, top_col + half_size, half_size)

            return Node(True, False, top_left, top_right, bottom_left, bottom_right)

        return build_tree(0, 0, n)

Time Complexity

O(N^2), where N is the dimension of the grid. Precomputing the prefix sum takes O(N^2) time. The tree construction then also takes O(N^2) time because each node is visited once.

Space Complexity

O(N^2), primarily due to the prefix sum matrix. The Quad-Tree itself could also take up to O(N^2) space in the worst case.

Edge Cases

  1. Empty Grid: The grid could be empty (n = 0). The code should handle this gracefully, likely by returning None.
  2. Single-Element Grid: The grid could contain only one element. The code should correctly create a leaf node with the value of that element.
  3. Grid with All Zeros or All Ones: The code should efficiently identify these cases and create a single leaf node.

Summary

The optimal solution involves precomputing prefix sums to quickly determine if a sub-grid contains all 0s or all 1s. This reduces the time complexity from O(N^2 log N) to O(N^2), while maintaining the space complexity at O(N^2).