Tiling a Rectangle with the Fewest Squares
HardPrompt
Given a rectangle of size n x m, return the minimum number of integer-sided squares that tile the rectangle.
Example 1:

3Example 2:

Input: n = 5, m = 8
Output: 5Example 3:

Input: n = 11, m = 13
Output: 6
Constraints:
1 <= n, m <= 13
Approaches
2 approaches with complexity analysis and trade-offs.
This approach uses a straightforward backtracking algorithm to explore all possible ways of tiling the rectangle. It uses a 2D grid to keep track of covered cells and recursively tries to place squares in the first available empty spot. While simple to understand, its brute-force nature makes it highly inefficient.
Algorithm
- The state of the tiling is represented by a 2D boolean grid
covered[n][m], wheretrueindicates a cell is covered. - A global variable
ansstores the minimum number of squares found, initialized to a large value (e.g.,n * m). - A recursive function,
dfs(covered, count), explores tiling possibilities. - Pruning: If the current count of squares
countis already greater than or equal toans, the function returns immediately. - Base Case: The function scans the grid to find the first uncovered cell
(r, c). If no such cell is found, the rectangle is fully tiled.ansis updated withmin(ans, count). - Recursive Step:
- For the uncovered cell
(r, c), the algorithm tries to place squares of every possible sizes(from largest to smallest) that can fit. - To place a square, it must fit within the rectangle's boundaries and not overlap with any already covered cells.
- For each valid placement:
- Mark the corresponding cells in the
coveredgrid astrue. - Make a recursive call:
dfs(covered, count + 1). - Backtrack: Revert the changes to the
coveredgrid by marking the cells back tofalse.
- Mark the corresponding cells in the
- For the uncovered cell
Walkthrough
The core idea is to perform a depth-first search (DFS) on the state of the rectangle. The state is defined by which cells are covered. We start with a completely empty rectangle and recursively add one square at a time.
We begin by finding the top-most, left-most empty cell. Then, we try to place the largest possible square that covers this cell. After placing the square, we make a recursive call to solve for the rest of the rectangle. Once the recursive call returns, we backtrack by removing the square and trying the next smaller size. This process continues until all possibilities are exhausted.
To avoid exploring fruitless paths, we use pruning. A global variable ans holds the minimum number of squares found so far. If the current number of squares used in a path (count) meets or exceeds ans, we abandon that path.
class Solution { int ans; public int tilingRectangle(int n, int m) { if (n == m) return 1; ans = n * m; boolean[][] covered = new boolean[n][m]; dfs(n, m, 0, 0, covered, 0); return ans; } private void dfs(int n, int m, int r, int c, boolean[][] covered, int count) { if (count >= ans) { return; } // Find the first uncovered cell, starting from (r, c) while (r < n && covered[r][c]) { c++; if (c == m) { r++; c = 0; } } // If all cells are covered, we found a solution if (r == n) { ans = Math.min(ans, count); return; } // Try to place squares of different sizes, largest first for better pruning for (int size = Math.min(n - r, m - c); size >= 1; size--) { if (canPlace(covered, r, c, size)) { place(covered, r, c, size, true); dfs(n, m, r, c + size, covered, count + 1); place(covered, r, c, size, false); // backtrack } } } private boolean canPlace(boolean[][] covered, int r, int c, int size) { for (int i = 0; i < size; i++) { for (int j = 0; j < size; j++) { if (covered[r + i][c + j]) { return false; } } } return true; } private void place(boolean[][] covered, int r, int c, int size, boolean val) { for (int i = 0; i < size; i++) { for (int j = 0; j < size; j++) { covered[r + i][c + j] = val; } } }}Complexity
Time
Exponential. The search space is vast, related to the number of ways to tile a grid. Without effective pruning, the complexity is prohibitive. This approach is too slow for the given constraints.
Space
O(n * m) to store the `covered` grid. The recursion stack depth also adds to the space, but it's dominated by the grid.
Trade-offs
Pros
Guarantees finding the optimal solution.
Relatively easy to conceptualize and implement.
Cons
Extremely inefficient due to the large state representation (
n*mgrid).The process of finding the next available cell and checking for valid placement is slow.
Very likely to cause a 'Time Limit Exceeded' error for most inputs within the given constraints.
This is a more optimized backtracking approach that uses a 'skyline' heuristic to represent the state of the tiling. Instead of a full 2D grid, we only keep track of the height of the tiled area in each column. This significantly reduces the state space and makes finding the next placement location much more efficient. Combined with aggressive pruning, this method is capable of solving the problem within the given constraints.
Algorithm
- To optimize, we can ensure
n >= mby swapping them if necessary. This reduces the width of our state representation. - The state is represented by a
heightarray of sizem, whereheight[j]is the filled height of columnj. - A global variable
ansstores the minimum squares, initialized ton * m. - A recursive function
dfs(height, count)drives the search. - Pruning: If
count >= ans, the function returns. - Base Case: Find the column
cwith the minimum heighth. Ifh == n, all columns are full, the rectangle is tiled. Updateans = min(ans, count)and return. - Recursive Step:
- The point
(h, c)is the top-left corner of the next available area. - Determine the width
wof the continuous flat region at heighthstarting from columnc. - The largest square we can place here is
s_max = min(n - h, w). - Iterate through possible square sizes
sfroms_maxdown to 1 (trying larger squares first is a good heuristic). - For each
s, create a newheightprofile by updating the heights of columnsctoc + s - 1toh + s. - Make a recursive call:
dfs(new_height, count + 1).
- The point
Walkthrough
The key improvement over the naive approach is the state representation. An array height of size m (the smaller dimension) is used, where height[j] stores the y-coordinate of the highest filled cell in column j. This is often called a 'skyline' representation.
The algorithm works as follows:
- Find the lowest point in the skyline. This corresponds to the first available empty space. Let's say this is at column
cwith heighth = height[c]. - From this point, determine how wide of a square we can place. This is limited by the rectangle's boundary and by any adjacent columns that are taller than
h. - We iterate through all possible square sizes that can be placed at this location, from largest to smallest. Trying larger squares first is a heuristic that helps find a good solution early, which in turn makes the pruning (
count >= ans) more effective. - For each potential square placement, we update the skyline and recurse. We pass a copy of the height array to the recursive call to avoid manual backtracking.
This approach is powerful because it directly targets the next area to be filled and prunes the search space effectively, making it feasible for the given constraints.
class Solution { int ans; public int tilingRectangle(int n, int m) { if (n == m) { return 1; } // Ensure n is the larger dimension to keep the skyline array smaller. if (n < m) { int temp = n; n = m; m = temp; } ans = n * m; // Upper bound: tiling with 1x1 squares int[] height = new int[m]; dfs(n, m, height, 0); return ans; } private void dfs(int n, int m, int[] height, int count) { if (count >= ans) { return; } // Find the first column that is not full (lowest point on the skyline) int minHeight = n; int startCol = -1; for (int i = 0; i < m; i++) { if (height[i] < minHeight) { minHeight = height[i]; startCol = i; } } // If all columns are full, we have a complete tiling if (startCol == -1) { ans = Math.min(ans, count); return; } // Find the width of the flat area at minHeight int endCol = startCol; while (endCol + 1 < m && height[endCol + 1] == minHeight && (endCol - startCol + 1) < (n - minHeight)) { endCol++; } int width = endCol - startCol + 1; // Try placing squares of different sizes, largest first for (int size = Math.min(width, n - minHeight); size >= 1; size--) { int[] nextHeight = height.clone(); for (int i = 0; i < size; i++) { nextHeight[startCol + i] += size; } dfs(n, m, nextHeight, count + 1); } }}Complexity
Time
Exponential, but significantly pruned. The number of reachable states is much smaller than the theoretical maximum of `O((n+1)^m)`. The exact complexity is hard to analyze but is efficient enough to pass the given constraints.
Space
O(k * m), where `k` is the maximum recursion depth (number of squares) and `m` is the smaller dimension of the rectangle. A new `height` array of size `m` is created at each recursion level.
Trade-offs
Pros
Guaranteed to find the optimal solution.
Significantly more efficient than naive backtracking due to a compact state representation and targeted search.
Feasible for the given constraints (
n, m <= 13).
Cons
The time complexity is still exponential, though much better than the naive approach.
The logic is more complex to implement correctly compared to the naive grid-based backtracking.
Solutions
Solution
class Solution {private int n;private int m;private int[] filled;private int ans;public int tilingRectangle(int n, int m) { this.n = n; this.m = m; ans = n * m; filled = new int[n]; dfs(0, 0, 0); return ans; }private void dfs(int i, int j, int t) { if (j == m) { ++i; j = 0; } if (i == n) { ans = t; return; } if ((filled[i] >> j & 1) == 1) { dfs(i, j + 1, t); } else if (t + 1 < ans) { int r = 0, c = 0; for (int k = i; k < n; ++k) { if ((filled[k] >> j & 1) == 1) { break; } ++r; } for (int k = j; k < m; ++k) { if ((filled[i] >> k & 1) == 1) { break; } ++c; } int mx = Math.min(r, c); for (int w = 1; w <= mx; ++w) { for (int k = 0; k < w; ++k) { filled[i + w - 1] |= 1 << (j + k); filled[i + k] |= 1 << (j + w - 1); } dfs(i, j + w, t + 1); } for (int x = i; x < i + mx; ++x) { for (int y = j; y < j + mx; ++y) { filled[x] ^= 1 << y; } } } }}class Solution {public: int tilingRectangle(int n, int m) { memset(filled, 0, sizeof(filled)); this->n = n; this->m = m; ans = n * m; dfs(0, 0, 0); return ans; } private: int filled[13]; int n, m; int ans; void dfs(int i, int j, int t) { if (j == m) { ++i; j = 0; } if (i == n) { ans = t; return; } if (filled[i] >> j & 1) { dfs(i, j + 1, t); } else if (t + 1 < ans) { int r = 0, c = 0; for (int k = i; k < n; ++k) { if (filled[k] >> j & 1) { break; } ++r; } for (int k = j; k < m; ++k) { if (filled[i] >> k & 1) { break; } ++c; } int mx = min(r, c); for (int w = 1; w <= mx; ++w) { for (int k = 0; k < w; ++k) { filled[i + w - 1] |= 1 << (j + k); filled[i + k] |= 1 << (j + w - 1); } dfs(i, j + w, t + 1); } for (int x = i; x < i + mx; ++x) { for (int y = j; y < j + mx; ++y) { filled[x] ^= 1 << y; } } } }};class Solution: def tilingRectangle(self, n: int, m: int) -> int: def dfs(i: int, j: int, t: int): nonlocal ans if j == m: i += 1 j = 0 if i == n: ans = t return if filled[i] >> j & 1: dfs(i, j + 1, t) elif t + 1 < ans: r = c = 0 for k in range(i, n): if filled[k] >> j & 1: break r += 1 for k in range(j, m): if filled[i] >> k & 1: break c += 1 mx = r if r < c else c for w in range(1, mx + 1): for k in range(w): filled[i + w - 1] |= 1 << (j + k) filled[i + k] |= 1 << (j + w - 1) dfs(i, j + w, t + 1) for x in range(i, i + mx): for y in range(j, j + mx): filled[x] ^= 1 << y ans = n * m filled = [0] * n dfs(0, 0, 0) return ansVideo 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.