1240. Tiling a Rectangle with the Fewest Squares
Problem Description
You are given a rectangle with dimensions n x m
. Your task is to completely cover this rectangle using square tiles, where each square tile must have integer side lengths. The goal is to find the minimum number of square tiles needed to tile the entire rectangle.
For example, if you have a 2x3 rectangle, you could tile it with 6 squares of size 1x1, but a better solution would be to use 2 squares of size 2x2 and 2 squares of size 1x1, for a total of 3 squares.
The squares can be of different sizes, and you need to arrange them in such a way that:
- The entire rectangle is covered
- There are no overlapping squares
- There are no gaps between squares
- The total number of squares used is minimized
The function should return the minimum number of integer-sided squares required to tile the given n x m
rectangle.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- No: The problem involves tiling a rectangle with squares, not nodes and edges as in graph problems.
Need to solve for kth smallest/largest?
- No: We're looking for the minimum number of squares needed, not the kth smallest or largest element.
Involves Linked Lists?
- No: The problem deals with a 2D rectangle grid, not linked lists.
Does the problem have small constraints?
- Yes: The problem typically has small constraints (n, m ≤ 13) because we need to explore different ways to tile the rectangle. The search space grows exponentially, making it impractical for large inputs.
Brute force / Backtracking?
- Yes: We need to try different square placements and backtrack when a configuration doesn't lead to an optimal solution. The solution involves:
- Recursively trying to place squares of different sizes at each empty position
- Tracking which positions have been filled using state compression
- Backtracking by removing placed squares to try alternative configurations
- Pruning branches that exceed the current minimum number of squares
Conclusion: The flowchart correctly identifies that this is a backtracking problem due to the small constraints and the need to explore all possible ways to tile the rectangle optimally.
Intuition
When faced with tiling a rectangle with the minimum number of squares, we need to think about how to systematically explore all possible ways to place squares.
The key insight is that we can process the rectangle position by position, from left to right and top to bottom. At each empty position, we have choices - we can place squares of different sizes starting from that position. For instance, if we're at position (i, j)
and have a 3x3 empty space available, we could place a 1x1, 2x2, or 3x3 square there.
Why backtracking? Because we don't know which choice will lead to the optimal solution until we try them all. After placing a square, we move to the next empty position and repeat the process. If we reach a configuration that uses more squares than our current best solution, we can stop exploring that branch (pruning). When we finish one path, we backtrack by removing the last placed square and trying a different size.
The state compression technique comes from realizing that we only need to know which cells are filled or empty. Since each cell has only two states (filled/empty), we can use bits to represent them. For each row, we use an integer where the j
-th bit indicates whether position (i, j)
is filled. This makes checking and updating the state very efficient using bitwise operations.
The recursive approach naturally handles the exploration:
- Start from position
(0, 0)
- For each empty position, try all possible square sizes that fit
- Mark the covered cells, recurse to handle the rest
- When backtracking, unmark those cells to try other options
- Keep track of the minimum number of squares needed
This exhaustive search with pruning ensures we find the optimal solution despite the exponential search space, which is manageable due to the small constraints.
Learn more about Backtracking patterns.
Solution Approach
The implementation uses recursive backtracking with state compression to efficiently track which cells have been filled.
State Representation:
- We use an integer array
filled
of lengthn
, wherefilled[i]
represents the state of rowi
- If the
j
-th bit offilled[i]
is 1, it means position(i, j)
is filled - This allows us to check if a cell is filled using:
filled[i] >> j & 1
Main DFS Function:
The dfs(i, j, t)
function explores all possible tilings starting from position (i, j)
with t
squares already placed.
-
Base Cases:
- If
j == m
: We've reached the end of a row, move to the next row by callingdfs(i + 1, 0, t)
- If
i == n
: All positions are filled, update the answer witht
- If
-
Skip Filled Positions:
- If position
(i, j)
is already filled (filled[i] >> j & 1
), recurse to the next position
- If position
-
Try Different Square Sizes:
- First, find the maximum square size possible from position
(i, j)
:- Count consecutive empty cells to the right (
c
) - Count consecutive empty cells downward (
r
) - Maximum square size is
mx = min(r, c)
- Count consecutive empty cells to the right (
- First, find the maximum square size possible from position
-
Place and Recurse:
- For each possible square size
w
from 1 tomx
:- Mark all cells in the
w × w
square as filled using bitwise OR operations:for k in range(w): filled[i + w - 1] |= 1 << (j + k) # Fill bottom row of square filled[i + k] |= 1 << (j + w - 1) # Fill right column of square
- Recurse to the next position:
dfs(i, j + w, t + 1)
- Mark all cells in the
- For each possible square size
-
Backtrack:
- After exploring, restore the state by unmarking all cells in the square:
for x in range(i, i + mx): for y in range(j, j + mx): filled[x] ^= 1 << y # XOR to flip bits back
- After exploring, restore the state by unmarking all cells in the square:
Pruning:
The condition t + 1 < ans
ensures we only explore paths that could potentially improve our answer, pruning branches that already use too many squares.
Initialization:
- Start with
ans = n * m
(worst case: all 1×1 squares) - Initialize
filled = [0] * n
(all cells empty) - Call
dfs(0, 0, 0)
to start from the top-left corner with 0 squares placed
The algorithm systematically tries all valid square placements, using bit manipulation for efficient state tracking and pruning to avoid unnecessary exploration.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through tiling a 2×3 rectangle to understand the solution approach.
Initial Setup:
- Rectangle dimensions: n=2, m=3
filled = [0, 0]
(binary:[000, 000]
- all cells empty)- Start with
ans = 6
(worst case: six 1×1 squares) - Call
dfs(0, 0, 0)
- start at position (0,0) with 0 squares placed
Step 1: First Square Placement At position (0,0):
- Check maximum square size:
- Consecutive empty cells right: 3
- Consecutive empty cells down: 2
- Maximum square size: min(2,3) = 2
Try placing a 2×2 square:
- Mark cells:
filled = [6, 6]
(binary:[110, 110]
) - Visual state:
[X X .] [X X .]
- Recurse to
dfs(0, 2, 1)
- move to position (0,2) with 1 square placed
Step 2: Second Square at (0,2) At position (0,2):
- Maximum square size possible: 1×1 (only one column left)
- Place 1×1 square at (0,2):
- Update:
filled[0] |= 1 << 2
→filled = [7, 6]
(binary:[111, 110]
) - Visual state:
[X X X] [X X .]
- Update:
- Recurse to
dfs(0, 3, 2)
- reached end of row
Step 3: Move to Next Row
Since j=3 (end of row), move to dfs(1, 0, 2)
Step 4: Process Row 1 At position (1,0):
- Cell already filled (bit check:
filled[1] >> 0 & 1 = 0
), skip to (1,1)
At position (1,1):
- Cell already filled, skip to (1,2)
At position (1,2):
- Empty cell found, place 1×1 square:
- Update:
filled[1] |= 1 << 2
→filled = [7, 7]
(binary:[111, 111]
) - Visual state:
[X X X] [X X X]
- Update:
- Recurse to
dfs(1, 3, 3)
Step 5: Complete First Solution
- Reached i=2 (all rows processed)
- Update
ans = min(6, 3) = 3
- Backtrack to try other configurations
Step 6: Backtrack and Try Alternative Back at position (0,0), now try a 1×1 square instead:
- Mark only (0,0):
filled = [1, 0]
(binary:[001, 000]
) - Visual state:
[X . .] [. . .]
- Continue exploring...
This process would continue, trying different square sizes at each position. For example:
- 1×1 at (0,0), then 2×2 at (0,1), then 1×1 at (1,0) → 3 squares total
- Six 1×1 squares → 6 squares total
The algorithm explores all possibilities while pruning paths that exceed the current best answer. The bit manipulation (filled[i] >> j & 1
) efficiently checks if cells are occupied, and the backtracking ensures we try all valid configurations to find the minimum of 3 squares.
Solution Implementation
1class Solution:
2 def tilingRectangle(self, n: int, m: int) -> int:
3 def backtrack(row: int, col: int, num_squares: int) -> None:
4 """
5 Recursively try to fill the rectangle with squares using backtracking.
6
7 Args:
8 row: Current row position
9 col: Current column position
10 num_squares: Number of squares used so far
11 """
12 nonlocal min_squares
13
14 # Move to next row if we've reached the end of current row
15 if col == m:
16 row += 1
17 col = 0
18
19 # If we've filled all rows, update the minimum answer
20 if row == n:
21 min_squares = num_squares
22 return
23
24 # If current cell is already filled, move to next cell
25 if filled_cells[row] >> col & 1:
26 backtrack(row, col + 1, num_squares)
27 # Only continue if we can potentially get a better solution
28 elif num_squares + 1 < min_squares:
29 # Find maximum possible square size from current position
30 max_rows = 0
31 max_cols = 0
32
33 # Count empty rows from current position
34 for k in range(row, n):
35 if filled_cells[k] >> col & 1:
36 break
37 max_rows += 1
38
39 # Count empty columns from current position
40 for k in range(col, m):
41 if filled_cells[row] >> k & 1:
42 break
43 max_cols += 1
44
45 # Maximum square size is limited by available rows and columns
46 max_square_size = min(max_rows, max_cols)
47
48 # Try all possible square sizes from 1 to max_square_size
49 for square_size in range(1, max_square_size + 1):
50 # Mark cells as filled for current square
51 for k in range(square_size):
52 # Fill bottom row of the square
53 filled_cells[row + square_size - 1] |= 1 << (col + k)
54 # Fill rightmost column of the square
55 filled_cells[row + k] |= 1 << (col + square_size - 1)
56
57 # Recursively try to fill remaining cells
58 backtrack(row, col + square_size, num_squares + 1)
59
60 # Backtrack: unmark all cells that were filled
61 for x in range(row, row + max_square_size):
62 for y in range(col, col + max_square_size):
63 filled_cells[x] ^= 1 << y
64
65 # Initialize minimum squares to worst case (all 1x1 squares)
66 min_squares = n * m
67
68 # Track filled cells using bit manipulation (each row is a bitmask)
69 filled_cells = [0] * n
70
71 # Start backtracking from top-left corner
72 backtrack(0, 0, 0)
73
74 return min_squares
75
1class Solution {
2 private int rows;
3 private int cols;
4 private int[] rowMask; // Bitmask array to track filled cells in each row
5 private int minSquares;
6
7 /**
8 * Find the minimum number of squares needed to tile an n x m rectangle
9 * @param n height of the rectangle
10 * @param m width of the rectangle
11 * @return minimum number of squares needed
12 */
13 public int tilingRectangle(int n, int m) {
14 this.rows = n;
15 this.cols = m;
16 this.minSquares = n * m; // Initialize with worst case (all 1x1 squares)
17 this.rowMask = new int[n]; // Each int represents filled cells in a row using bits
18
19 dfs(0, 0, 0);
20 return minSquares;
21 }
22
23 /**
24 * Depth-first search to try all possible square placements
25 * @param row current row position
26 * @param col current column position
27 * @param squareCount number of squares placed so far
28 */
29 private void dfs(int row, int col, int squareCount) {
30 // Move to next row if we've reached the end of current row
31 if (col == cols) {
32 row++;
33 col = 0;
34 }
35
36 // All cells filled - we found a complete tiling
37 if (row == rows) {
38 minSquares = squareCount;
39 return;
40 }
41
42 // Current cell is already filled, move to next cell
43 if ((rowMask[row] >> col & 1) == 1) {
44 dfs(row, col + 1, squareCount);
45 }
46 // Current cell is empty and we haven't exceeded minimum squares yet
47 else if (squareCount + 1 < minSquares) {
48 // Calculate maximum possible square size from current position
49 int maxRows = 0;
50 int maxCols = 0;
51
52 // Count consecutive empty cells downward
53 for (int r = row; r < rows; r++) {
54 if ((rowMask[r] >> col & 1) == 1) {
55 break;
56 }
57 maxRows++;
58 }
59
60 // Count consecutive empty cells rightward
61 for (int c = col; c < cols; c++) {
62 if ((rowMask[row] >> c & 1) == 1) {
63 break;
64 }
65 maxCols++;
66 }
67
68 // Maximum square size is limited by available space
69 int maxSquareSize = Math.min(maxRows, maxCols);
70
71 // Try placing squares of different sizes (1x1 to maxSize x maxSize)
72 for (int size = 1; size <= maxSquareSize; size++) {
73 // Mark cells as filled for current square
74 for (int offset = 0; offset < size; offset++) {
75 rowMask[row + size - 1] |= 1 << (col + offset); // Bottom edge
76 rowMask[row + offset] |= 1 << (col + size - 1); // Right edge
77 }
78
79 // Continue search with this square placed
80 dfs(row, col + size, squareCount + 1);
81 }
82
83 // Backtrack: unmark all cells that were filled
84 for (int r = row; r < row + maxSquareSize; r++) {
85 for (int c = col; c < col + maxSquareSize; c++) {
86 rowMask[r] ^= 1 << c;
87 }
88 }
89 }
90 }
91}
92
1class Solution {
2public:
3 int tilingRectangle(int n, int m) {
4 // Initialize the filled state array (using bit manipulation for each row)
5 memset(filledState, 0, sizeof(filledState));
6
7 // Store dimensions
8 this->height = n;
9 this->width = m;
10
11 // Initialize minimum squares needed to worst case (1x1 squares)
12 minSquares = n * m;
13
14 // Start DFS from top-left corner with 0 squares used
15 dfs(0, 0, 0);
16
17 return minSquares;
18 }
19
20private:
21 int filledState[13]; // Bit representation of filled cells for each row (max 13 rows)
22 int height, width; // Rectangle dimensions
23 int minSquares; // Minimum number of squares needed
24
25 /**
26 * DFS to find minimum squares needed to tile the rectangle
27 * @param row: current row position
28 * @param col: current column position
29 * @param squaresUsed: number of squares placed so far
30 */
31 void dfs(int row, int col, int squaresUsed) {
32 // Move to next row if we've reached the end of current row
33 if (col == width) {
34 ++row;
35 col = 0;
36 }
37
38 // If we've filled all rows, update the minimum squares needed
39 if (row == height) {
40 minSquares = squaresUsed;
41 return;
42 }
43
44 // If current cell is already filled, skip to next cell
45 if (filledState[row] >> col & 1) {
46 dfs(row, col + 1, squaresUsed);
47 }
48 // Only continue if we can potentially get a better solution
49 else if (squaresUsed + 1 < minSquares) {
50 // Find maximum possible square size from current position
51 int maxRowExtent = 0, maxColExtent = 0;
52
53 // Check how many rows we can extend downward
54 for (int r = row; r < height; ++r) {
55 if (filledState[r] >> col & 1) {
56 break;
57 }
58 ++maxRowExtent;
59 }
60
61 // Check how many columns we can extend rightward
62 for (int c = col; c < width; ++c) {
63 if (filledState[row] >> c & 1) {
64 break;
65 }
66 ++maxColExtent;
67 }
68
69 // Maximum square size is limited by both row and column extent
70 int maxSquareSize = min(maxRowExtent, maxColExtent);
71
72 // Try all possible square sizes from 1x1 to maxSquareSize x maxSquareSize
73 for (int size = 1; size <= maxSquareSize; ++size) {
74 // Mark the square area as filled
75 for (int offset = 0; offset < size; ++offset) {
76 // Fill bottom edge of the square
77 filledState[row + size - 1] |= 1 << (col + offset);
78 // Fill right edge of the square
79 filledState[row + offset] |= 1 << (col + size - 1);
80 }
81
82 // Continue DFS with this square placed
83 dfs(row, col + size, squaresUsed + 1);
84 }
85
86 // Backtrack: unmark all cells in the maximum possible square area
87 for (int r = row; r < row + maxSquareSize; ++r) {
88 for (int c = col; c < col + maxSquareSize; ++c) {
89 filledState[r] ^= 1 << c;
90 }
91 }
92 }
93 }
94};
95
1/**
2 * Finds the minimum number of squares needed to tile an n x m rectangle
3 * @param n - Height of the rectangle
4 * @param m - Width of the rectangle
5 * @returns Minimum number of square tiles needed
6 */
7function tilingRectangle(n: number, m: number): number {
8 // Initialize the answer with worst case (all 1x1 tiles)
9 let minTiles: number = n * m;
10
11 // Bitmask array to track filled cells in each row
12 // Each element represents a row, bits represent columns
13 const filledCells: number[] = new Array(n).fill(0);
14
15 /**
16 * Depth-first search to try all possible square placements
17 * @param row - Current row position
18 * @param col - Current column position
19 * @param tilesUsed - Number of tiles used so far
20 */
21 const searchTiling = (row: number, col: number, tilesUsed: number): void => {
22 // Move to next row if we've reached the end of current row
23 if (col === m) {
24 row++;
25 col = 0;
26 }
27
28 // If we've filled the entire rectangle, update the minimum
29 if (row === n) {
30 minTiles = tilesUsed;
31 return;
32 }
33
34 // If current cell is already filled, move to next cell
35 if ((filledCells[row] >> col) & 1) {
36 searchTiling(row, col + 1, tilesUsed);
37 }
38 // Only continue if we can potentially improve the current best solution
39 else if (tilesUsed + 1 < minTiles) {
40 // Calculate maximum square size that can fit from current position
41 let maxRows: number = 0;
42 let maxCols: number = 0;
43
44 // Check how many rows are available
45 for (let checkRow: number = row; checkRow < n; checkRow++) {
46 if ((filledCells[checkRow] >> col) & 1) {
47 break;
48 }
49 maxRows++;
50 }
51
52 // Check how many columns are available
53 for (let checkCol: number = col; checkCol < m; checkCol++) {
54 if ((filledCells[row] >> checkCol) & 1) {
55 break;
56 }
57 maxCols++;
58 }
59
60 // Maximum square size is limited by available rows and columns
61 const maxSquareSize: number = Math.min(maxRows, maxCols);
62
63 // Try placing squares of different sizes
64 for (let squareSize: number = 1; squareSize <= maxSquareSize; squareSize++) {
65 // Mark cells as filled for current square
66 for (let offset: number = 0; offset < squareSize; offset++) {
67 // Fill bottom row of the square
68 filledCells[row + squareSize - 1] |= 1 << (col + offset);
69 // Fill right column of the square
70 filledCells[row + offset] |= 1 << (col + squareSize - 1);
71 }
72
73 // Recursively try to fill remaining cells
74 searchTiling(row, col + squareSize, tilesUsed + 1);
75 }
76
77 // Backtrack: unmark all cells in the maximum possible square area
78 for (let r: number = row; r < row + maxSquareSize; r++) {
79 for (let c: number = col; c < col + maxSquareSize; c++) {
80 filledCells[r] ^= 1 << c;
81 }
82 }
83 }
84 };
85
86 // Start the search from top-left corner with 0 tiles used
87 searchTiling(0, 0, 0);
88
89 return minTiles;
90}
91
Time and Space Complexity
Time Complexity: O((n*m)^(n*m))
The algorithm uses backtracking with pruning to explore all possible ways to tile an n×m
rectangle with square tiles. In the worst case:
- Each cell in the
n×m
grid can be the starting point of a square tile - For each position
(i,j)
, we can place squares of sizes from1×1
up tomin(n-i, m-j)
- The recursion tree has a maximum depth of
n*m
(if we place all1×1
tiles) - At each level, we potentially have multiple branching factors based on the square sizes we can place
- The state space is exponential as we explore different combinations of square placements
The pruning condition t + 1 < ans
helps reduce the search space, but the worst-case remains exponential.
Space Complexity: O(n + n*m)
The space complexity consists of:
O(n)
for thefilled
array which usesn
integers to represent the filled state of the grid using bitmasksO(n*m)
for the recursion call stack in the worst case when the maximum recursion depth equals the number of cells- Additional
O(1)
space for variables likeans
,r
,c
,mx
, etc.
The dominant factor is the recursion depth, giving us O(n + n*m)
which simplifies to O(n*m)
space complexity.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Bit Manipulation During Square Placement
The most critical pitfall in this solution is the incorrect way of marking cells as filled when placing a square. The current code only marks the bottom row and rightmost column of the square, leaving the interior cells unmarked.
Problem in Original Code:
# This only marks the edges, not the entire square!
for k in range(square_size):
filled_cells[row + square_size - 1] |= 1 << (col + k) # Bottom row only
filled_cells[row + k] |= 1 << (col + square_size - 1) # Right column only
For a 3×3 square, this would only mark:
. . X . . X X X X
Instead of the entire square.
Correct Solution:
# Mark ALL cells in the square as filled
for x in range(row, row + square_size):
for y in range(col, col + square_size):
filled_cells[x] |= 1 << y
2. Inconsistent Backtracking Logic
The backtracking section uses max_square_size
instead of the actual square_size
that was placed, leading to incorrect restoration of state.
Problem:
# Uses max_square_size for ALL squares, regardless of which size was actually placed
for x in range(row, row + max_square_size):
for y in range(col, col + max_square_size):
filled_cells[x] ^= 1 << y
Correct Solution:
# Backtrack should be inside the loop, specific to each square_size
for square_size in range(1, max_square_size + 1):
# Mark cells
for x in range(row, row + square_size):
for y in range(col, col + square_size):
filled_cells[x] |= 1 << y
# Recurse
backtrack(row, col + square_size, num_squares + 1)
# Backtrack for THIS specific square_size
for x in range(row, row + square_size):
for y in range(col, col + square_size):
filled_cells[x] ^= 1 << y
3. Integer Overflow with Bit Operations
For larger values of m
, using bit shifting can cause issues since Python integers have practical limits for bit operations in certain contexts.
Problem:
When m
is large (e.g., > 30), 1 << col
might behave unexpectedly in some Python implementations or be inefficient.
Solution: Add validation or use alternative state representation for very large rectangles:
def tilingRectangle(self, n: int, m: int) -> int:
# Swap dimensions if needed to minimize bit operations
if m > n:
n, m = m, n
if m > 30: # Consider alternative approach for very wide rectangles
# Use different state representation (e.g., 2D boolean array)
pass
Complete Corrected Code:
class Solution:
def tilingRectangle(self, n: int, m: int) -> int:
def backtrack(row: int, col: int, num_squares: int) -> None:
nonlocal min_squares
if col == m:
backtrack(row + 1, 0, num_squares)
return
if row == n:
min_squares = min(min_squares, num_squares)
return
if filled_cells[row] >> col & 1:
backtrack(row, col + 1, num_squares)
return
if num_squares + 1 >= min_squares:
return
# Find maximum square size
max_size = min(n - row, m - col)
for r in range(row, min(n, row + max_size)):
for c in range(col, min(m, col + max_size)):
if filled_cells[r] >> c & 1:
max_size = min(max_size, max(r - row, c - col))
# Try each square size
for size in range(1, max_size + 1):
# Mark all cells in the square
for r in range(row, row + size):
for c in range(col, col + size):
filled_cells[r] |= 1 << c
backtrack(row, col + size, num_squares + 1)
# Unmark all cells in the square
for r in range(row, row + size):
for c in range(col, col + size):
filled_cells[r] ^= 1 << c
min_squares = n * m
filled_cells = [0] * n
backtrack(0, 0, 0)
return min_squares
Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?
Recommended Readings
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!