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 positioni
is blackfloor[i] = '1'
means the tile at positioni
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.
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:
- What tile we're currently looking at (black or white)
- How many carpets we still have available
- 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:
- Prefix Sum Array
s
: We build an array wheres[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. - Memoization Cache: The
@cache
decorator automatically stores results ofdfs(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:
-
Base Case - Out of bounds: If
i >= n
, we've processed all tiles, return 0. -
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)
. -
No Carpets Left: If
j == 0
, we can't cover any more tiles. The remaining white tiles from positioni
to the end iss[-1] - s[i]
(wheres[-1]
equalss[n]
, the total count up to position n-1). -
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 positioni + 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))
- Don't use a carpet: Skip this white tile (count it as visible) and move to the next position:
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 EvaluatorExample 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 todfs(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
- Skip:
- 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
- Option 1a: Skip →
- 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
- Skip:
- Result: 1 + 0 = 1
- Cover:
dfs(4, 0)
(no carpets left)- Returns
s[5] - s[4] = 3 - 3 = 0
white tiles remain
- Returns
- Min(1, 0) = 0
- Skip:
- 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:
- The memoization cache stores up to
n × (numCarpets + 1)
states, which isO(n × m)
. - 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. - The prefix sum array
s
usesO(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
andfloor_length
if we don't think about this carefully
Correct approach: The solution handles this correctly by:
- Allowing the jump to
pos + carpetLen
even if it exceeds floor_length - The base case
if pos >= floor_length: return 0
properly handles positions beyond the floor - 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 infloor[0:i+1]
- Range sum from index
a
tob
(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.
What are the most two important steps in writing a depth first search function? (Select 2)
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Prefix Sum The prefix sum is an incredibly powerful and straightforward technique Its primary goal is to allow for constant time range sum queries on an array What is Prefix Sum The prefix sum of an array at index i is the sum of all numbers from index 0 to i By
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
Want a Structured Path to Master System Design Too? Don’t Miss This!