Facebook Pixel

2209. Minimum White Tiles After Covering With Carpets

Problem Description

You have a floor represented as a binary string floor where:

  • floor[i] = '0' means the tile at position i is black
  • floor[i] = '1' means the tile at position i is white

You are given numCarpets black carpets, each with length carpetLen tiles. Your goal is to place these carpets on the floor to cover as many white tiles as possible. The carpets can overlap with each other.

The task is to find the minimum number of white tiles that remain visible after optimally placing all available carpets.

For example, if you have a floor "10110" with 2 carpets of length 2, you could:

  • Place one carpet covering positions [1, 2] to hide the white tile at position 1
  • Place another carpet covering positions [2, 3] to hide the white tile at position 3
  • This leaves only the white tile at position 4 visible, giving a minimum of 1 visible white tile

The solution uses dynamic programming with memoization. The function dfs(i, j) represents the minimum number of uncovered white tiles starting from position i with j carpets remaining. At each white tile position, you decide whether to place a carpet starting there (covering the next carpetLen tiles) or skip it and leave it uncovered. The algorithm uses a prefix sum array to efficiently calculate the number of white tiles in any range.

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

Intuition

When we look at this problem, we need to make decisions about where to place our carpets to minimize visible white tiles. At each position in the floor, we face a choice: should we place a carpet here or not?

The key insight is that this is an optimization problem with choices - a perfect candidate for dynamic programming. We can think of walking through the floor from left to right, and at each position, we need to decide what to do based on:

  1. What tile we're currently looking at (black or white)
  2. How many carpets we still have available
  3. What happens if we make different choices

If we encounter a black tile, there's no need to cover it - we can simply move to the next position without using any carpet. This is optimal since we want to save our carpets for white tiles.

When we encounter a white tile, we have two options:

  • Skip it: Leave this white tile uncovered (adding 1 to our count of visible white tiles) and move to the next position
  • Cover it: Place a carpet starting at this position, which will cover the next carpetLen tiles, using up one carpet

The recursive nature becomes clear: after making a choice at position i with j carpets remaining, we need to solve a smaller subproblem - either at position i+1 with the same number of carpets (if we skipped), or at position i+carpetLen with j-1 carpets (if we placed a carpet).

To handle the base cases efficiently, we use a prefix sum array. When we run out of carpets (j = 0), we can quickly calculate how many white tiles remain from the current position to the end using s[n] - s[i], where s stores the cumulative count of white tiles.

The overlapping subproblems (we might reach the same position with the same number of carpets through different paths) make memoization essential to avoid redundant calculations, transforming this from an exponential to a polynomial time solution.

Learn more about Dynamic Programming and Prefix Sum patterns.

Solution Approach

The solution implements a top-down dynamic programming approach with memoization using Python's @cache decorator.

Data Structures Used:

  1. Prefix Sum Array s: We build an array where s[i+1] stores the count of white tiles from index 0 to i. This allows us to quickly calculate the number of white tiles in any range in O(1) time.
  2. Memoization Cache: The @cache decorator automatically stores results of dfs(i, j) to avoid recomputing the same subproblems.

Implementation Details:

First, we build the prefix sum array:

s = [0] * (n + 1)
for i, c in enumerate(floor):
    s[i + 1] = s[i] + int(c == "1")

This means s[i+1] - s[j] gives us the count of white tiles from index j to i (inclusive).

The core recursive function dfs(i, j) represents the minimum number of uncovered white tiles starting from position i with j carpets available:

  1. Base Case - Out of bounds: If i >= n, we've processed all tiles, return 0.

  2. Black Tile Case: If floor[i] == "0", we don't need to cover this tile, so we skip to the next position: return dfs(i + 1, j).

  3. No Carpets Left: If j == 0, we can't cover any more tiles. The remaining white tiles from position i to the end is s[-1] - s[i] (where s[-1] equals s[n], the total count up to position n-1).

  4. White Tile with Carpets Available: We have two choices:

    • Don't use a carpet: Skip this white tile (count it as visible) and move to the next position: 1 + dfs(i + 1, j)
    • Use a carpet: Place a carpet starting at position i, covering positions [i, i+carpetLen), and jump to position i + carpetLen with one less carpet: dfs(i + carpetLen, j - 1)

    We take the minimum of these two options: min(1 + dfs(i + 1, j), dfs(i + carpetLen, j - 1))

Time Complexity: O(n × numCarpets) since we have at most n positions and numCarpets different values for j, and each state is computed only once due to memoization.

Space Complexity: O(n × numCarpets) for the memoization cache plus O(n) for the prefix sum array.

The final answer is obtained by calling dfs(0, numCarpets), which gives the minimum number of white tiles visible when starting from position 0 with all carpets available.

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 a concrete example to understand how the solution works.

Example: floor = "10110", numCarpets = 2, carpetLen = 2

Step 1: Build Prefix Sum Array First, we create the prefix sum array s to count white tiles:

  • floor: "10110"
  • s: [0, 1, 1, 2, 3, 3]
    • s[0] = 0 (no tiles counted yet)
    • s[1] = 1 (floor[0]='1' is white)
    • s[2] = 1 (floor[1]='0' is black, no increment)
    • s[3] = 2 (floor[2]='1' is white)
    • s[4] = 3 (floor[3]='1' is white)
    • s[5] = 3 (floor[4]='0' is black)

Step 2: Recursive Exploration with dfs(0, 2)

Starting at position 0 with 2 carpets:

dfs(0, 2): floor[0]='1' (white), we have carpets available

  • Option 1: Skip this white tile → 1 + dfs(1, 2)

    • dfs(1, 2): floor[1]='0' (black) → skip to dfs(2, 2)
    • dfs(2, 2): floor[2]='1' (white)
      • Option 1a: Skip → 1 + dfs(3, 2)
        • dfs(3, 2): floor[3]='1' (white)
          • Skip: 1 + dfs(4, 2) = 1 + 0 = 1
          • Cover: dfs(5, 1) = 0 (out of bounds)
          • Min = 0
        • Result: 1 + 0 = 1
      • Option 1b: Cover positions [2,3] → dfs(4, 1)
        • dfs(4, 1): floor[4]='0' (black) → dfs(5, 1) = 0
        • Result: 0
      • Min(1, 0) = 0
    • Total for Option 1: 1 + 0 = 1
  • Option 2: Cover positions [0,1] with carpet → dfs(2, 1)

    • dfs(2, 1): floor[2]='1' (white), 1 carpet left
      • Skip: 1 + dfs(3, 1)
        • dfs(3, 1): floor[3]='1' (white)
          • Skip: 1 + dfs(4, 1) = 1 + 0 = 1
          • Cover: dfs(5, 0) = 0
          • Min = 0
        • Result: 1 + 0 = 1
      • Cover: dfs(4, 0) (no carpets left)
        • Returns s[5] - s[4] = 3 - 3 = 0 white tiles remain
      • Min(1, 0) = 0
    • Total for Option 2: 0

Final result: min(1, 0) = 0

However, let me recalculate more carefully. When we place carpet at position 0 covering [0,1], we hide the white tile at position 0. Then at position 2, if we place another carpet covering [2,3], we hide both white tiles at positions 2 and 3. This leaves 0 white tiles visible.

Actually, let's trace through with the correct logic:

dfs(0, 2): floor[0]='1', carpets=2

  • Skip: 1 + dfs(1, 2) = 1 + dfs(2, 2) = 1 + min(1 + dfs(3, 2), dfs(4, 1))
    • dfs(3, 2): floor[3]='1' → min(1 + dfs(4, 2), dfs(5, 1)) = min(1, 0) = 0
    • dfs(4, 1): floor[4]='0' → dfs(5, 1) = 0
    • So: 1 + min(1 + 0, 0) = 1 + 0 = 1
  • Cover: dfs(2, 1) = min(1 + dfs(3, 1), dfs(4, 0))
    • dfs(3, 1): floor[3]='1' → min(1 + 0, 0) = 0
    • dfs(4, 0): s[5] - s[4] = 0
    • So: min(1 + 0, 0) = 0

Result: min(1, 0) = 0

The optimal strategy is to place one carpet at position 0 (covering tiles 0-1) and another at position 2 (covering tiles 2-3), leaving 0 white tiles visible.

Solution Implementation

1class Solution:
2    def minimumWhiteTiles(self, floor: str, numCarpets: int, carpetLen: int) -> int:
3        from functools import cache
4      
5        # Helper function to count minimum uncovered white tiles
6        # starting from position 'pos' with 'carpets_left' carpets remaining
7        @cache
8        def dp(pos: int, carpets_left: int) -> int:
9            # Base case: if we've gone past the floor, no white tiles remain
10            if pos >= floor_length:
11                return 0
12          
13            # If current position is black tile, skip it (no need to cover)
14            if floor[pos] == "0":
15                return dp(pos + 1, carpets_left)
16          
17            # If no carpets left, count all remaining white tiles from current position
18            if carpets_left == 0:
19                return prefix_sum[-1] - prefix_sum[pos]
20          
21            # Two choices at a white tile:
22            # Option 1: Don't place carpet here, count this white tile and continue
23            leave_uncovered = 1 + dp(pos + 1, carpets_left)
24          
25            # Option 2: Place carpet starting at current position, skip carpetLen positions
26            place_carpet = dp(pos + carpetLen, carpets_left - 1)
27          
28            return min(leave_uncovered, place_carpet)
29      
30        # Initialize variables
31        floor_length = len(floor)
32      
33        # Build prefix sum array to quickly count white tiles in any range
34        # prefix_sum[i] = number of white tiles in floor[0:i]
35        prefix_sum = [0] * (floor_length + 1)
36        for i, tile in enumerate(floor):
37            prefix_sum[i + 1] = prefix_sum[i] + (1 if tile == "1" else 0)
38      
39        # Calculate minimum uncovered white tiles
40        result = dp(0, numCarpets)
41      
42        # Clear cache to free memory
43        dp.cache_clear()
44      
45        return result
46
1class Solution {
2    // Memoization table: dp[position][carpetsRemaining]
3    private Integer[][] memo;
4    // Prefix sum array to count white tiles
5    private int[] prefixSum;
6    // Total length of the floor
7    private int floorLength;
8    // Length of each carpet
9    private int carpetLength;
10
11    public int minimumWhiteTiles(String floor, int numCarpets, int carpetLen) {
12        // Initialize floor length
13        floorLength = floor.length();
14      
15        // Initialize memoization table for dynamic programming
16        // memo[i][j] represents minimum white tiles visible starting from position i with j carpets available
17        memo = new Integer[floorLength][numCarpets + 1];
18      
19        // Build prefix sum array to quickly calculate white tiles in any range
20        // prefixSum[i] = count of white tiles from index 0 to i-1
21        prefixSum = new int[floorLength + 1];
22        for (int i = 0; i < floorLength; ++i) {
23            prefixSum[i + 1] = prefixSum[i] + (floor.charAt(i) == '1' ? 1 : 0);
24        }
25      
26        // Store carpet length for use in recursive function
27        carpetLength = carpetLen;
28      
29        // Start DFS from position 0 with all carpets available
30        return dfs(0, numCarpets);
31    }
32
33    private int dfs(int position, int carpetsRemaining) {
34        // Base case: reached or exceeded the end of floor
35        if (position >= floorLength) {
36            return 0;
37        }
38      
39        // Base case: no carpets left, return all remaining white tiles
40        if (carpetsRemaining == 0) {
41            return prefixSum[floorLength] - prefixSum[position];
42        }
43      
44        // Check memoization table to avoid redundant calculations
45        if (memo[position][carpetsRemaining] != null) {
46            return memo[position][carpetsRemaining];
47        }
48      
49        // Optimization: if current tile is black (0), skip to next position
50        // No need to make a decision since it doesn't contribute to white tile count
51        if (prefixSum[position + 1] == prefixSum[position]) {
52            return dfs(position + 1, carpetsRemaining);
53        }
54      
55        // Two choices at current white tile:
56        // Option 1: Don't place carpet here, count this white tile and move to next position
57        int skipCarpet = 1 + dfs(position + 1, carpetsRemaining);
58      
59        // Option 2: Place carpet starting at current position, skip carpetLength positions
60        int placeCarpet = dfs(position + carpetLength, carpetsRemaining - 1);
61      
62        // Choose the option that results in minimum white tiles visible
63        int result = Math.min(skipCarpet, placeCarpet);
64      
65        // Store result in memoization table
66        memo[position][carpetsRemaining] = result;
67      
68        return result;
69    }
70}
71
1class Solution {
2public:
3    int minimumWhiteTiles(string floor, int numCarpets, int carpetLen) {
4        int n = floor.size();
5      
6        // dp[i][j] = minimum white tiles visible starting from index i with j carpets remaining
7        // -1 indicates uncomputed state
8        vector<vector<int>> dp(n, vector<int>(numCarpets + 1, -1));
9      
10        // prefixSum[i] = count of white tiles ('1') in floor[0...i-1]
11        vector<int> prefixSum(n + 1);
12        for (int i = 0; i < n; ++i) {
13            prefixSum[i + 1] = prefixSum[i] + (floor[i] == '1');
14        }
15      
16        // Recursive function with memoization to find minimum white tiles
17        auto dfs = [&](this auto&& dfs, int pos, int carpetsLeft) -> int {
18            // Base case: reached end of floor
19            if (pos >= n) {
20                return 0;
21            }
22          
23            // Base case: no carpets left, count remaining white tiles
24            if (carpetsLeft == 0) {
25                return prefixSum[n] - prefixSum[pos];
26            }
27          
28            // Return memoized result if already computed
29            if (dp[pos][carpetsLeft] != -1) {
30                return dp[pos][carpetsLeft];
31            }
32          
33            // Optimization: if current tile is black, skip to next position
34            if (floor[pos] == '0') {
35                return dfs(pos + 1, carpetsLeft);
36            }
37          
38            // Two choices at white tile:
39            // 1. Don't place carpet: count current white tile + recurse
40            int skipCarpet = 1 + dfs(pos + 1, carpetsLeft);
41          
42            // 2. Place carpet: skip carpetLen tiles and use one carpet
43            int placeCarpet = dfs(pos + carpetLen, carpetsLeft - 1);
44          
45            // Store and return the minimum of both choices
46            int result = min(skipCarpet, placeCarpet);
47            dp[pos][carpetsLeft] = result;
48            return result;
49        };
50      
51        // Start from position 0 with all carpets available
52        return dfs(0, numCarpets);
53    }
54};
55
1/**
2 * Finds the minimum number of white tiles visible after placing carpets optimally
3 * @param floor - String representing the floor where '0' is black tile and '1' is white tile
4 * @param numCarpets - Number of carpets available to cover tiles
5 * @param carpetLen - Length of each carpet
6 * @returns Minimum number of white tiles that remain visible
7 */
8function minimumWhiteTiles(floor: string, numCarpets: number, carpetLen: number): number {
9    const floorLength: number = floor.length;
10  
11    // Memoization table: dp[i][j] represents minimum white tiles from position i with j carpets remaining
12    const memoTable: number[][] = Array.from(
13        { length: floorLength }, 
14        () => Array(numCarpets + 1).fill(-1)
15    );
16  
17    // Prefix sum array: prefixSum[i] stores count of white tiles from index 0 to i-1
18    const prefixSum: number[] = Array(floorLength + 1).fill(0);
19    for (let i = 0; i < floorLength; i++) {
20        prefixSum[i + 1] = prefixSum[i] + (floor[i] === '1' ? 1 : 0);
21    }
22  
23    /**
24     * Recursive function to find minimum white tiles visible
25     * @param currentIndex - Current position in the floor string
26     * @param carpetsRemaining - Number of carpets still available to use
27     * @returns Minimum white tiles visible from current position onwards
28     */
29    const findMinWhiteTiles = (currentIndex: number, carpetsRemaining: number): number => {
30        // Base case: reached end of floor
31        if (currentIndex >= floorLength) {
32            return 0;
33        }
34      
35        // No carpets left: return count of remaining white tiles
36        if (carpetsRemaining === 0) {
37            return prefixSum[floorLength] - prefixSum[currentIndex];
38        }
39      
40        // Return memoized result if already computed
41        if (memoTable[currentIndex][carpetsRemaining] !== -1) {
42            return memoTable[currentIndex][carpetsRemaining];
43        }
44      
45        // Optimization: if current tile is black, skip to next position
46        if (prefixSum[currentIndex + 1] === prefixSum[currentIndex]) {
47            return findMinWhiteTiles(currentIndex + 1, carpetsRemaining);
48        }
49      
50        // Choice 1: Don't place carpet at current position (count current white tile)
51        const skipCarpet: number = 1 + findMinWhiteTiles(currentIndex + 1, carpetsRemaining);
52      
53        // Choice 2: Place carpet starting at current position
54        const placeCarpet: number = findMinWhiteTiles(
55            currentIndex + carpetLen, 
56            carpetsRemaining - 1
57        );
58      
59        // Take minimum of both choices
60        const result: number = Math.min(skipCarpet, placeCarpet);
61      
62        // Memoize and return result
63        memoTable[currentIndex][carpetsRemaining] = result;
64        return result;
65    };
66  
67    // Start recursion from position 0 with all carpets available
68    return findMinWhiteTiles(0, numCarpets);
69}
70

Time and Space Complexity

The time complexity is O(n × m), where n is the length of the string floor and m is the value of numCarpets.

The recursive function dfs(i, j) uses memoization with @cache. The function has two parameters: i representing the current position in the floor (ranging from 0 to n-1), and j representing the number of carpets remaining (ranging from 0 to numCarpets). This creates at most n × (numCarpets + 1) unique states. Each state is computed only once due to memoization, and each computation takes O(1) time (comparing values, arithmetic operations, and looking up precomputed prefix sums). Therefore, the overall time complexity is O(n × m).

The space complexity is O(n × m). This comes from two main sources:

  1. The memoization cache stores up to n × (numCarpets + 1) states, which is O(n × m).
  2. The recursion stack depth can go up to O(n) in the worst case when we don't place any carpets and traverse tile by tile.
  3. The prefix sum array s uses O(n) space.

Since O(n × m) dominates, the overall space complexity is O(n × m).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Handling of Carpet Placement Boundaries

One of the most common mistakes is not properly handling what happens when a carpet extends beyond the floor's end. When placing a carpet at position i, it covers positions [i, i+carpetLen). If i + carpetLen > floor_length, the carpet extends past the floor boundary.

Incorrect approach:

# This might cause issues if not handled properly
place_carpet = dp(pos + carpetLen, carpets_left - 1)

Why it's problematic:

  • If pos + carpetLen > floor_length, we jump beyond the floor, which is actually valid (the base case handles it)
  • However, we might miss counting white tiles between pos and floor_length if we don't think about this carefully

Correct approach: The solution handles this correctly by:

  1. Allowing the jump to pos + carpetLen even if it exceeds floor_length
  2. The base case if pos >= floor_length: return 0 properly handles positions beyond the floor
  3. When a carpet partially extends beyond the floor, it still covers all tiles from its starting position to the floor's end

2. Off-by-One Errors in Prefix Sum Calculation

Common mistake:

# Incorrect: might mix up indices
prefix_sum[i] = prefix_sum[i-1] + (1 if floor[i] == "1" else 0)

Correct approach:

# Maintain clear indexing: prefix_sum[i+1] represents sum up to and including floor[i]
prefix_sum[i + 1] = prefix_sum[i] + (1 if floor[i] == "1" else 0)

This ensures:

  • prefix_sum[0] = 0 (no tiles counted)
  • prefix_sum[i+1] = count of white tiles in floor[0:i+1]
  • Range sum from index a to b (inclusive) = prefix_sum[b+1] - prefix_sum[a]

3. Forgetting to Skip Black Tiles

Incorrect approach:

def dp(pos, carpets_left):
    if pos >= floor_length:
        return 0
    if carpets_left == 0:
        return prefix_sum[-1] - prefix_sum[pos]
  
    # Missing the check for black tiles!
    leave_uncovered = 1 + dp(pos + 1, carpets_left)  # Wrong: counts black tiles as 1
    place_carpet = dp(pos + carpetLen, carpets_left - 1)
    return min(leave_uncovered, place_carpet)

Correct approach: Always check if the current tile is black before making decisions:

if floor[pos] == "0":
    return dp(pos + 1, carpets_left)  # Skip black tiles without counting

4. Memory Limit Issues with Large Inputs

For very large inputs, the cache might consume excessive memory. While the provided solution includes dp.cache_clear(), forgetting this step in repeated test cases could cause memory issues.

Best practice:

# Always clear cache after getting the result
result = dp(0, numCarpets)
dp.cache_clear()  # Important for multiple test cases
return result

5. Suboptimal Choice Logic

Common mistake: Placing carpets greedily at every white tile:

# Wrong: Always try to place carpet at first white tile encountered
if floor[pos] == "1" and carpets_left > 0:
    return dp(pos + carpetLen, carpets_left - 1)

Correct approach: Consider both options (place or skip) and choose the minimum:

leave_uncovered = 1 + dp(pos + 1, carpets_left)
place_carpet = dp(pos + carpetLen, carpets_left - 1)
return min(leave_uncovered, place_carpet)

This ensures we find the globally optimal solution rather than a locally optimal one.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What are the most two important steps in writing a depth first search function? (Select 2)


Recommended Readings

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

Load More