Facebook Pixel

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

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 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
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?


Recommended Readings

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

Load More