#0414Time: O(N^2 * log N). For each node in the Quad Tree, we iterate over its corresponding sub-grid. The recurrence relation is `T(N) = 4 * T(N/2) + O(N^2)`. At each level of recursion, the total work for the homogeneity check across all nodes at that level is `O(N^2)`. Since the depth of the tree is `log N`, the total time complexity is `O(N^2 * log N)`.Space: O(N^2). The space is dominated by the storage for the Quad Tree itself, which can have `O(N^2)` nodes in the worst-case scenario (e.g., a checkerboard pattern). The recursion stack depth contributes `O(log N)`.1 company
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.
class Node {
public boolean val;
public boolean isLeaf;
public Node topLeft;
public Node topRight;
public Node bottomLeft;
public Node bottomRight;
}
We can construct a Quad-Tree from a two-dimensional area using the following steps:
If the current grid has the same value (i.e all 1's or all 0's) set isLeaf True and set val to the value of the grid and set the four children to Null and stop.
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 as shown in the photo.
Recurse for each of the children with the proper sub-grid.
If you want to know more about the Quad-Tree, you can refer to the wiki.
Quad-Tree format:
You don't need to read this section for solving the problem. This is only if you want to understand the output format here. The output represents the serialized format of a Quad-Tree using level order traversal, where null signifies a path terminator where no node exists below.
It is very similar to the serialization of the binary tree. The only difference is that the node is represented as a list [isLeaf, val].
If the value of isLeaf or val is True we represent it as 1 in the list [isLeaf, val] and if the value of isLeaf or val is False we represent it as 0.
Example 1:
Input: grid = [[0,1],[1,0]]
Output: [[0,1],[1,0],[1,1],[1,1],[1,0]]
Explanation: The explanation of this example is shown below:
Notice that 0 represents False and 1 represents True in the photo representing the Quad-Tree.
Example 2:
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: All values in the grid are not the same. We divide the grid into four sub-grids.
The topLeft, bottomLeft and bottomRight each has the same value.
The topRight have different values so we divide it into 4 sub-grids where each has the same value.
Explanation is shown in the photo below:
Constraints:
n == grid.length == grid[i].length
n == 2x where 0 <= x <= 6
Approaches
3 approaches with complexity analysis and trade-offs.
This approach directly implements the recursive definition of the Quad Tree construction. A helper function is defined that takes the coordinates and size of the current sub-grid. This function first checks if all values in the sub-grid are the same. If they are, it creates a leaf node. If not, it creates an internal node, divides the grid into four equal quadrants, and makes a recursive call for each quadrant to build the sub-trees.
Algorithm
Create a recursive helper function build(grid, row, col, size).
In the helper function, check if the sub-grid from (row, col) of size x size is homogeneous.
To check for homogeneity, iterate through all size * size cells and compare them to the first cell's value.
If the sub-grid is homogeneous, create and return a leaf node with isLeaf=true and val set to the common value.
If the sub-grid is not homogeneous, create an internal node with isLeaf=false.
bottomRight = build(grid, row + size/2, col + size/2, size/2)
Assign the results to the children of the internal node and return it.
The initial call is construct(grid) which calls build(grid, 0, 0, grid.length).
Walkthrough
The core of this method is a recursive function, let's call it build(row, col, size). This function is responsible for constructing the Quad Tree for the size x size sub-grid starting at (row, col).
The process within build(row, col, size) is as follows:
Homogeneity Check: First, we determine if the sub-grid is homogeneous (contains all 0s or all 1s). We can do this by taking the value of the top-left cell, grid[row][col], and then iterating through all the cells in the size x size sub-grid. If we find any cell with a different value, the grid is not homogeneous.
Leaf Node Creation: If the check passes (the grid is homogeneous), we create a new Node. We set isLeaf to true, val to the common value (true for 1, false for 0), and all four children pointers to null. This node is then returned.
Internal Node Creation: If the check fails (the grid has mixed values), we create an internal node. We set isLeaf to false and val to an arbitrary value (e.g., true). Then, we divide the current size x size grid into four (size/2) x (size/2) sub-grids and make recursive calls to build for each of these four sub-grids, assigning the returned nodes to the topLeft, topRight, bottomLeft, and bottomRight children of the current internal node. This node is then returned.
The initial call to start the process would be build(0, 0, n), where n is the dimension of the input grid.
1class Solution {2 public Node construct(int[][] grid) {3 return build(grid, 0, 0, grid.length);4 }56 private Node build(int[][] grid, int row, int col, int size) {7 // Check if the sub-grid is homogeneous8 boolean isHomogeneous = true;9 int firstVal = grid[row][col];10 for (int i = row; i < row + size; i++) {11 for (int j = col; j < col + size; j++) {12 if (grid[i][j] != firstVal) {13 isHomogeneous = false;14 break;15 }16 }17 if (!isHomogeneous) {18 break;19 }20 }2122 if (isHomogeneous) {23 return new Node(firstVal == 1, true, null, null, null, null);24 } else {25 int newSize = size / 2;26 Node topLeft = build(grid, row, col, newSize);27 Node topRight = build(grid, row, col + newSize, newSize);28 Node bottomLeft = build(grid, row + newSize, col, newSize);29 Node bottomRight = build(grid, row + newSize, col + newSize, newSize);30 // The 'val' for an internal node can be arbitrary, e.g., true.31 return new Node(true, false, topLeft, topRight, bottomLeft, bottomRight);32 }33 }34}
Complexity
Time
O(N^2 * log N). For each node in the Quad Tree, we iterate over its corresponding sub-grid. The recurrence relation is `T(N) = 4 * T(N/2) + O(N^2)`. At each level of recursion, the total work for the homogeneity check across all nodes at that level is `O(N^2)`. Since the depth of the tree is `log N`, the total time complexity is `O(N^2 * log N)`.
Space
O(N^2). The space is dominated by the storage for the Quad Tree itself, which can have `O(N^2)` nodes in the worst-case scenario (e.g., a checkerboard pattern). The recursion stack depth contributes `O(log N)`.
Trade-offs
Pros
Simple to understand and implement as it directly follows the problem definition.
Does not require any extra data structures for pre-computation.
Cons
Inefficient due to redundant computations. The homogeneity check for a sub-grid re-scans cells that were already scanned by its parent's check.
This approach improves upon the naive recursive solution by optimizing the homogeneity check. The key bottleneck is repeatedly scanning sub-grids to see if they contain all the same values. We can pre-compute a 2D prefix sum array (also known as an integral image) of the grid. This allows us to find the sum of any rectangular sub-grid in O(1) time. A sub-grid is homogeneous if its sum is either 0 (all 0s) or equal to its area (all 1s).
Algorithm
Pre-compute a 2D prefix sum array prefixSum for the input grid. This takes O(N^2) time.
Create a recursive helper function build(row, col, size).
In the helper function, calculate the sum of the current size x size sub-grid using the prefixSum array in O(1) time.
Check if the sum is 0 (all zeros) or size * size (all ones).
If the sub-grid is homogeneous, create and return a leaf node.
If not, create an internal node, recursively call build for the four quadrants, assign the children, and return the node.
The initial call is construct(grid) which first builds the prefix sum array and then calls build(0, 0, grid.length).
Walkthrough
The overall structure is still a recursive function build(row, col, size), but the way it checks for homogeneity is different and much faster.
Pre-computation: First, we create a prefix sum array, prefixSum, of size (n+1) x (n+1). prefixSum[i+1][j+1] will store the sum of all elements in the rectangle from (0, 0) to (i, j) in the original grid. This can be computed in O(n^2) time.
Optimized Homogeneity Check: With the prefixSum array, we can calculate the sum of any size x size sub-grid starting at (row, col) in O(1). The sum is given by:
Recursive Construction: The rest of the logic is the same as the naive approach. If the sub-grid is homogeneous, create a leaf node. Otherwise, create an internal node and recurse on the four quadrants. Since the check is now O(1), the performance is significantly better.
1class Solution {2 int[][] prefixSum;3 int[][] grid;45 public Node construct(int[][] grid) {6 int n = grid.length;7 this.grid = grid;8 this.prefixSum = new int[n + 1][n + 1];9 for (int i = 0; i < n; i++) {10 for (int j = 0; j < n; j++) {11 prefixSum[i + 1][j + 1] = grid[i][j] + prefixSum[i][j + 1] + prefixSum[i + 1][j] - prefixSum[i][j];12 }13 }14 return build(0, 0, n);15 }1617 private Node build(int row, int col, int size) {18 int sum = prefixSum[row + size][col + size] - prefixSum[row][col + size] - prefixSum[row + size][col] + prefixSum[row][col];1920 if (sum == 0) { // All 0s21 return new Node(false, true, null, null, null, null);22 } else if (sum == size * size) { // All 1s23 return new Node(true, true, null, null, null, null);24 } else { // Mixed values25 int newSize = size / 2;26 Node topLeft = build(row, col, newSize);27 Node topRight = build(row, col + newSize, newSize);28 Node bottomLeft = build(row + newSize, col, newSize);29 Node bottomRight = build(row + newSize, col + newSize, newSize);30 return new Node(true, false, topLeft, topRight, bottomLeft, bottomRight);31 }32 }33}
Complexity
Time
O(N^2). The pre-computation of the prefix sum array takes `O(N^2)`. The recursive construction involves visiting each node of the Quad Tree once. The work done at each node is `O(1)`. In the worst case, the tree has `O(N^2)` nodes. Thus, the recursion takes `O(N^2)` time. The total time complexity is `O(N^2) + O(N^2) = O(N^2)`.
Space
O(N^2). We need `O(N^2)` space for the prefix sum array. The Quad Tree itself can also take up to `O(N^2)` space. The recursion stack uses `O(log N)` space.
Trade-offs
Pros
Much more efficient than the naive approach, with an optimal time complexity of O(N^2).
The logic remains recursive and relatively easy to follow.
Cons
Requires extra space of O(N^2) for the prefix sum array.
This approach is also recursive but optimizes the process by avoiding both redundant grid scans and the need for an auxiliary prefix sum array. It works in a bottom-up fashion (similar to a post-order traversal). The recursive function builds the tree for the sub-quadrants first and then uses the results to decide whether to create a leaf or an internal node for the current grid.
Algorithm
Create a recursive helper function build(grid, row, col, size).
The base case for the recursion is when size == 1. In this case, return a leaf node representing the single cell.
For size > 1, recursively call build for the four quadrants to get the four children nodes.
After the recursive calls return, check if the four returned nodes are all leaf nodes and if they all share the same val.
If they do, it means the current grid is homogeneous. Return a new single leaf node with that common value.
Otherwise, the current grid is not homogeneous. Return a new internal node, setting its four children to the nodes returned by the recursive calls.
The initial call is construct(grid) which calls build(grid, 0, 0, grid.length).
Walkthrough
The recursive function, let's call it build(row, col, size), will construct and return the node for the sub-grid starting at (row, col) of size x size.
The logic is as follows:
Base Case: If size == 1, the sub-grid is a single cell. We are at the lowest level of recursion. We create a leaf node with isLeaf = true and val corresponding to grid[row][col]. This node is returned.
Recursive Step: If size > 1, we first recursively call build for the four sub-quadrants.
Merge Check: After the recursive calls return, we inspect the four children nodes. If all four children are leaf nodes (isLeaf == true) and they all have the same val, it means the current size x size grid is homogeneous. In this case, we can "merge" them. We create a single new leaf node with the common value and return it. The four sub-trees that were just created are effectively discarded.
Internal Node Creation: If the four children cannot be merged (either because one of them is not a leaf or they don't all have the same value), it means the current grid is not homogeneous. We create a new internal node (isLeaf = false), assign the four children nodes we received from the recursive calls, and return this new internal node.
This post-order traversal style ensures that we only ever create nodes that are necessary for the final tree, and the homogeneity check is implicitly done by checking the returned children.
1class Solution {2 int[][] grid;34 public Node construct(int[][] grid) {5 this.grid = grid;6 return build(0, 0, grid.length);7 }89 private Node build(int row, int col, int size) {10 if (size == 1) {11 return new Node(grid[row][col] == 1, true, null, null, null, null);12 }1314 int newSize = size / 2;15 Node topLeft = build(row, col, newSize);16 Node topRight = build(row, col + newSize, newSize);17 Node bottomLeft = build(row + newSize, col, newSize);18 Node bottomRight = build(row + newSize, col + newSize, newSize);1920 // Check if all children are leaves and have the same value21 if (topLeft.isLeaf && topRight.isLeaf && bottomLeft.isLeaf && bottomRight.isLeaf &&22 topLeft.val == topRight.val && topRight.val == bottomLeft.val && bottomLeft.val == bottomRight.val) {23 // Merge them into a single leaf node24 return new Node(topLeft.val, true, null, null, null, null);25 } else {26 // Otherwise, create an internal node27 return new Node(true, false, topLeft, topRight, bottomLeft, bottomRight);28 }29 }30}
Complexity
Time
O(N^2). The recurrence relation is `T(N) = 4 * T(N/2) + O(1)`, which resolves to `O(N^2)`. We visit each cell of the grid once at the base case of the recursion (`size == 1`), and the merge/check operations at each internal node take constant time.
Space
O(N^2). The space is dominated by the storage for the Quad Tree itself, which can have `O(N^2)` nodes in the worst case. The recursion stack depth is `O(log N)`. This approach does not require any auxiliary `O(N^2)` data structure.
Trade-offs
Pros
Achieves optimal O(N^2) time complexity.
More space-efficient than the prefix sum approach as it avoids the O(N^2) auxiliary array.
Cons
The logic can be slightly more complex to reason about compared to the top-down approaches.
Solutions
Solution
1/* // Definition for a QuadTree node. class Node { public boolean val; public boolean isLeaf; public Node topLeft; public Node topRight; public Node bottomLeft; public Node bottomRight; public Node() { this.val = false; this.isLeaf = false; this.topLeft = null; this.topRight = null; this.bottomLeft = null; this.bottomRight = null; } public Node(boolean val, boolean isLeaf) { this.val = val; this.isLeaf = isLeaf; this.topLeft = null; this.topRight = null; this.bottomLeft = null; this.bottomRight = null; } public Node(boolean val, boolean isLeaf, Node topLeft, Node topRight, Node bottomLeft, Node bottomRight) { this.val = val; this.isLeaf = isLeaf; this.topLeft = topLeft; this.topRight = topRight; this.bottomLeft = bottomLeft; this.bottomRight = bottomRight; } }; */ class Solution { public Node construct ( int [][] grid ) { return dfs ( 0 , 0 , grid . length - 1 , grid [ 0 ]. length - 1 , grid ); } private Node dfs ( int a , int b , int c , int d , int [][] grid ) { int zero = 0 , one = 0 ; for ( int i = a ; i <= c ; ++ i ) { for ( int j = b ; j <= d ; ++ j ) { if ( grid [ i ][ j ] == 0 ) { zero = 1 ; } else { one = 1 ; } } } boolean isLeaf = zero + one == 1 ; boolean val = isLeaf && one == 1 ; Node node = new Node ( val , isLeaf ); if ( isLeaf ) { return node ; } node . topLeft = dfs ( a , b , ( a + c ) / 2 , ( b + d ) / 2 , grid ); node . topRight = dfs ( a , ( b + d ) / 2 + 1 , ( a + c ) / 2 , d , grid ); node . bottomLeft = dfs (( a + c ) / 2 + 1 , b , c , ( b + d ) / 2 , grid ); node . bottomRight = dfs (( a + c ) / 2 + 1 , ( b + d ) / 2 + 1 , c , d , grid ); return node ; } }
1/* // Definition for a QuadTree node. class Node { public: bool val; bool isLeaf; Node* topLeft; Node* topRight; Node* bottomLeft; Node* bottomRight; Node() { val = false; isLeaf = false; topLeft = NULL; topRight = NULL; bottomLeft = NULL; bottomRight = NULL; } Node(bool _val, bool _isLeaf) { val = _val; isLeaf = _isLeaf; topLeft = NULL; topRight = NULL; bottomLeft = NULL; bottomRight = NULL; } Node(bool _val, bool _isLeaf, Node* _topLeft, Node* _topRight, Node* _bottomLeft, Node* _bottomRight) { val = _val; isLeaf = _isLeaf; topLeft = _topLeft; topRight = _topRight; bottomLeft = _bottomLeft; bottomRight = _bottomRight; } }; */ class Solution { public: Node * construct ( vector < vector < int >>& grid ) { return dfs ( 0 , 0 , grid . size () - 1 , grid [ 0 ]. size () - 1 , grid ); } Node * dfs ( int a , int b , int c , int d , vector < vector < int >>& grid ) { int zero = 0 , one = 0 ; for ( int i = a ; i <= c ; ++ i ) { for ( int j = b ; j <= d ; ++ j ) { if ( grid [ i ][ j ]) one = 1 ; else zero = 1 ; } } bool isLeaf = zero + one == 1 ; bool val = isLeaf && one ; Node * node = new Node ( val , isLeaf ); if ( isLeaf ) return node ; node -> topLeft = dfs ( a , b , ( a + c ) / 2 , ( b + d ) / 2 , grid ); node -> topRight = dfs ( a , ( b + d ) / 2 + 1 , ( a + c ) / 2 , d , grid ); node -> bottomLeft = dfs (( a + c ) / 2 + 1 , b , c , ( b + d ) / 2 , grid ); node -> bottomRight = dfs (( a + c ) / 2 + 1 , ( b + d ) / 2 + 1 , c , d , grid ); return node ; } };
1""" # Definition for a QuadTree node. class Node: def __init__(self, val, isLeaf, topLeft, topRight, bottomLeft, bottomRight): 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' : def dfs ( a , b , c , d ): zero = one = 0 for i in range ( a , c + 1 ): for j in range ( b , d + 1 ): if grid [ i ][ j ] == 0 : zero = 1 else : one = 1 isLeaf = zero + one == 1 val = isLeaf and one if isLeaf : return Node ( grid [ a ][ b ], True ) topLeft = dfs ( a , b , ( a + c ) // 2 , ( b + d ) // 2 ) topRight = dfs ( a , ( b + d ) // 2 + 1 , ( a + c ) // 2 , d ) bottomLeft = dfs (( a + c ) // 2 + 1 , b , c , ( b + d ) // 2 ) bottomRight = dfs (( a + c ) // 2 + 1 , ( b + d ) // 2 + 1 , c , d ) return Node ( val , isLeaf , topLeft , topRight , bottomLeft , bottomRight ) return dfs ( 0 , 0 , len ( grid ) - 1 , len ( grid [ 0 ]) - 1 )
Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.