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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 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

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 length n, where filled[i] represents the state of row i
  • If the j-th bit of filled[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.

  1. Base Cases:

    • If j == m: We've reached the end of a row, move to the next row by calling dfs(i + 1, 0, t)
    • If i == n: All positions are filled, update the answer with t
  2. Skip Filled Positions:

    • If position (i, j) is already filled (filled[i] >> j & 1), recurse to the next position
  3. 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)
  4. Place and Recurse:

    • For each possible square size w from 1 to mx:
      • 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)
  5. 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

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 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example 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 << 2filled = [7, 6] (binary: [111, 110])
    • Visual state:
      [X X X] [X X .]
  • 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 << 2filled = [7, 7] (binary: [111, 111])
    • Visual state:
      [X X X] [X X X]
  • 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.

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 from 1×1 up to min(n-i, m-j)
  • The recursion tree has a maximum depth of n*m (if we place all 1×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 the filled array which uses n integers to represent the filled state of the grid using bitmasks
  • O(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 like ans, 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
Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which technique can we use to find the middle of a linked list?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More