Tiling a Rectangle with the Fewest Squares

Hard
#1158Time: 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.
Patterns

Prompt

Given a rectangle of size n x m, return the minimum number of integer-sided squares that tile the rectangle.

 

Example 1:

3

Example 2:

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

Example 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], where true indicates a cell is covered.
  • A global variable ans stores 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 count is already greater than or equal to ans, 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. ans is updated with min(ans, count).
  • Recursive Step:
    • For the uncovered cell (r, c), the algorithm tries to place squares of every possible size s (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:
      1. Mark the corresponding cells in the covered grid as true.
      2. Make a recursive call: dfs(covered, count + 1).
      3. Backtrack: Revert the changes to the covered grid by marking the cells back to false.

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*m grid).

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

Solutions

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

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.