2167. Minimum Time to Remove All Cars Containing Illegal Goods
Problem Description
You have a train represented as a binary string s
where each character represents a train car. A '0'
means the car is clean (no illegal goods), and a '1'
means the car contains illegal goods.
Your goal is to remove all cars containing illegal goods ('1'
s) from the train using the minimum total time. You have three operations available:
- Remove from left end: Remove the leftmost car (
s[0]
) - costs 1 unit of time - Remove from right end: Remove the rightmost car (
s[s.length - 1]
) - costs 1 unit of time - Remove from middle: Remove any car from anywhere in the sequence - costs 2 units of time
You can perform any of these operations any number of times. The task is to find the minimum time needed to remove all cars with illegal goods.
For example, if s = "1100101"
, you need to remove all the '1'
s. You could:
- Remove 2 cars from the left (removes "11"), taking 2 units of time
- Remove 3 cars from the right (removes "101"), taking 3 units of time
- Total: 5 units of time
The solution uses dynamic programming with prefix and suffix arrays. The pre[i]
array tracks the minimum cost to remove all illegal goods from the first i
cars. The suf[i]
array tracks the minimum cost to remove all illegal goods from car i
to the end. For each position, we can either remove the car individually (cost 2) or remove all cars from that end (cost equals the position). The final answer is the minimum sum of pre[i] + suf[i]
across all possible split points.
Intuition
The key insight is that we want to find an optimal "splitting point" in the train. We can remove all illegal goods from the left side of this split point using some strategy, and all illegal goods from the right side using another strategy.
For any segment of the train, we have two choices:
- Remove the entire segment from one end (costs equal to the segment length)
- Keep some cars and remove individual illegal cars from the middle (each costs 2)
This leads us to think about the problem as: at each position i
, what's the minimum cost to clear all illegal goods from [0, i]
and from [i, n-1]
?
For the prefix part (left side), at each position with an illegal good, we can either:
- Remove all cars from the left up to this point (cost =
i + 1
) - Keep the previous cars and remove this one individually (cost =
pre[i] + 2
)
We choose the minimum of these two options.
Similarly, for the suffix part (right side), at each position with an illegal good, we can either:
- Remove all cars from this point to the right end (cost =
n - i
) - Remove this car individually and handle the rest (cost =
suf[i + 1] + 2
)
The beauty of this approach is that it considers all possible strategies: removing everything from the left, removing everything from the right, or some combination where we remove from both ends and handle middle cars individually.
The final answer is the minimum sum of pre[i] + suf[i]
across all positions, which represents the optimal split point where we've minimized the total cost of clearing illegal goods from both sides.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution uses dynamic programming with two arrays to track the minimum cost from both directions.
Data Structures:
pre[i+1]
: Stores the minimum cost to remove all illegal goods from the firsti
carssuf[i]
: Stores the minimum cost to remove all illegal goods from cari
to the end- Both arrays have size
n+1
to handle boundary cases
Algorithm Implementation:
-
Build the prefix array (
pre
):for i, c in enumerate(s): pre[i + 1] = pre[i] if c == '0' else min(pre[i] + 2, i + 1)
- If car
i
is clean (c == '0'
), the cost remains the same aspre[i]
- If car
i
has illegal goods (c == '1'
), we choose the minimum between:pre[i] + 2
: Keep previous arrangement and remove this car individuallyi + 1
: Remove all cars from the left up to and including this position
- If car
-
Build the suffix array (
suf
):for i in range(n - 1, -1, -1): suf[i] = suf[i + 1] if s[i] == '0' else min(suf[i + 1] + 2, n - i)
- Process from right to left
- If car
i
is clean, cost remainssuf[i + 1]
- If car
i
has illegal goods, choose minimum between:suf[i + 1] + 2
: Remove this car individually and handle the restn - i
: Remove all cars from this position to the right end
-
Find the optimal split:
return min(a + b for a, b in zip(pre[1:], suf[1:]))
- For each possible split position, calculate
pre[i] + suf[i]
- The minimum sum represents the optimal strategy
- We use
pre[1:]
andsuf[1:]
to align the arrays properly (avoiding the boundary indices)
- For each possible split position, calculate
Time Complexity: O(n)
- Single pass for each array construction and one pass to find minimum
Space Complexity: O(n)
- Two arrays of size n+1
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with s = "0110"
to illustrate the solution approach.
Step 1: Initialize arrays
n = 4
pre = [0, 0, 0, 0, 0]
(size n+1)suf = [0, 0, 0, 0, 0]
(size n+1)
Step 2: Build prefix array (left to right)
Processing each character:
i=0, s[0]='0'
: Clean car, sopre[1] = pre[0] = 0
i=1, s[1]='1'
: Illegal goods!- Option 1: Remove individually:
pre[1] + 2 = 0 + 2 = 2
- Option 2: Remove all from left:
i + 1 = 1 + 1 = 2
pre[2] = min(2, 2) = 2
- Option 1: Remove individually:
i=2, s[2]='1'
: Illegal goods!- Option 1: Remove individually:
pre[2] + 2 = 2 + 2 = 4
- Option 2: Remove all from left:
i + 1 = 2 + 1 = 3
pre[3] = min(4, 3) = 3
- Option 1: Remove individually:
i=3, s[3]='0'
: Clean car, sopre[4] = pre[3] = 3
Result: pre = [0, 0, 2, 3, 3]
Step 3: Build suffix array (right to left)
Processing from the end:
i=3, s[3]='0'
: Clean car, sosuf[3] = suf[4] = 0
i=2, s[2]='1'
: Illegal goods!- Option 1: Remove individually:
suf[3] + 2 = 0 + 2 = 2
- Option 2: Remove all from right:
n - i = 4 - 2 = 2
suf[2] = min(2, 2) = 2
- Option 1: Remove individually:
i=1, s[1]='1'
: Illegal goods!- Option 1: Remove individually:
suf[2] + 2 = 2 + 2 = 4
- Option 2: Remove all from right:
n - i = 4 - 1 = 3
suf[1] = min(4, 3) = 3
- Option 1: Remove individually:
i=0, s[0]='0'
: Clean car, sosuf[0] = suf[1] = 3
Result: suf = [3, 3, 2, 0, 0]
Step 4: Find optimal split
We need to find the minimum of pre[i] + suf[i]
for valid split points:
- Split at position 1:
pre[1] + suf[1] = 0 + 3 = 3
- Split at position 2:
pre[2] + suf[2] = 2 + 2 = 4
- Split at position 3:
pre[3] + suf[3] = 3 + 0 = 3
- Split at position 4:
pre[4] + suf[4] = 3 + 0 = 3
Answer: 3
This means the optimal strategy costs 3 units of time. One optimal approach would be to remove the 3 rightmost cars (positions 1, 2, 3), which removes both '1's and costs 3 units total.
Solution Implementation
1class Solution:
2 def minimumTime(self, s: str) -> int:
3 """
4 Find minimum time to remove all '1's from string s.
5 Operations allowed:
6 - Remove from left end: cost 1
7 - Remove from right end: cost 1
8 - Remove from middle: cost 2
9
10 Args:
11 s: Binary string containing '0's and '1's
12
13 Returns:
14 Minimum time to remove all '1's
15 """
16 n = len(s)
17
18 # prefix_cost[i] = minimum cost to remove all '1's in s[0:i]
19 prefix_cost = [0] * (n + 1)
20
21 # suffix_cost[i] = minimum cost to remove all '1's in s[i:n]
22 suffix_cost = [0] * (n + 1)
23
24 # Build prefix costs: for each position, decide whether to:
25 # 1. Remove all elements from left (cost = i + 1)
26 # 2. Keep previous strategy and remove current element from middle if it's '1' (cost = previous + 2)
27 for i, char in enumerate(s):
28 if char == '0':
29 # No '1' at current position, carry forward previous cost
30 prefix_cost[i + 1] = prefix_cost[i]
31 else:
32 # '1' at current position, choose minimum between:
33 # - Remove as middle element (previous cost + 2)
34 # - Remove all from left up to current position (i + 1)
35 prefix_cost[i + 1] = min(prefix_cost[i] + 2, i + 1)
36
37 # Build suffix costs: for each position from right, decide whether to:
38 # 1. Remove all elements from right (cost = n - i)
39 # 2. Keep previous strategy and remove current element from middle if it's '1' (cost = previous + 2)
40 for i in range(n - 1, -1, -1):
41 if s[i] == '0':
42 # No '1' at current position, carry forward previous cost
43 suffix_cost[i] = suffix_cost[i + 1]
44 else:
45 # '1' at current position, choose minimum between:
46 # - Remove as middle element (previous cost + 2)
47 # - Remove all from right starting from current position (n - i)
48 suffix_cost[i] = min(suffix_cost[i + 1] + 2, n - i)
49
50 # Find optimal split point: remove all '1's in left part and right part
51 # The total cost at split point i is prefix_cost[i] + suffix_cost[i]
52 return min(left_cost + right_cost
53 for left_cost, right_cost in zip(prefix_cost[1:], suffix_cost[1:]))
54
1class Solution {
2 public int minimumTime(String s) {
3 int length = s.length();
4
5 // prefixCost[i] represents the minimum cost to remove all '1's from s[0...i-1]
6 int[] prefixCost = new int[length + 1];
7
8 // suffixCost[i] represents the minimum cost to remove all '1's from s[i...n-1]
9 int[] suffixCost = new int[length + 1];
10
11 // Calculate prefix costs from left to right
12 // For each position, we have two choices:
13 // 1. Remove the current element (cost = 2)
14 // 2. Remove all elements from the beginning up to current position (cost = i + 1)
15 for (int i = 0; i < length; i++) {
16 if (s.charAt(i) == '0') {
17 // No cost needed for '0', carry forward the previous cost
18 prefixCost[i + 1] = prefixCost[i];
19 } else {
20 // For '1', choose minimum between:
21 // - Remove this single element (previous cost + 2)
22 // - Remove all from start to current position (i + 1)
23 prefixCost[i + 1] = Math.min(prefixCost[i] + 2, i + 1);
24 }
25 }
26
27 // Calculate suffix costs from right to left
28 // For each position, we have two choices:
29 // 1. Remove the current element (cost = 2)
30 // 2. Remove all elements from current position to the end (cost = n - i)
31 for (int i = length - 1; i >= 0; i--) {
32 if (s.charAt(i) == '0') {
33 // No cost needed for '0', carry forward the previous cost
34 suffixCost[i] = suffixCost[i + 1];
35 } else {
36 // For '1', choose minimum between:
37 // - Remove this single element (previous cost + 2)
38 // - Remove all from current position to end (length - i)
39 suffixCost[i] = Math.min(suffixCost[i + 1] + 2, length - i);
40 }
41 }
42
43 // Find the minimum total cost by trying all possible split points
44 // At each split point i, the total cost is:
45 // prefixCost[i] (handle left part) + suffixCost[i] (handle right part)
46 int minimumCost = Integer.MAX_VALUE;
47 for (int splitPoint = 1; splitPoint <= length; splitPoint++) {
48 int totalCost = prefixCost[splitPoint] + suffixCost[splitPoint];
49 minimumCost = Math.min(minimumCost, totalCost);
50 }
51
52 return minimumCost;
53 }
54}
55
1class Solution {
2public:
3 int minimumTime(string s) {
4 int n = s.size();
5
6 // prefixCost[i] = minimum cost to remove all '1's from s[0...i-1]
7 vector<int> prefixCost(n + 1);
8
9 // suffixCost[i] = minimum cost to remove all '1's from s[i...n-1]
10 vector<int> suffixCost(n + 1);
11
12 // Calculate prefix costs
13 // For each position, we can either:
14 // 1. Remove internally (cost +2 from previous)
15 // 2. Remove all from left (cost = i+1)
16 for (int i = 0; i < n; ++i) {
17 if (s[i] == '0') {
18 // No '1' at this position, carry forward the previous cost
19 prefixCost[i + 1] = prefixCost[i];
20 } else {
21 // Found a '1', choose minimum between:
22 // - Remove it internally: prefixCost[i] + 2
23 // - Remove everything from left: i + 1
24 prefixCost[i + 1] = min(prefixCost[i] + 2, i + 1);
25 }
26 }
27
28 // Calculate suffix costs
29 // For each position from right, we can either:
30 // 1. Remove internally (cost +2 from next)
31 // 2. Remove all from right (cost = n-i)
32 for (int i = n - 1; i >= 0; --i) {
33 if (s[i] == '0') {
34 // No '1' at this position, carry forward the next cost
35 suffixCost[i] = suffixCost[i + 1];
36 } else {
37 // Found a '1', choose minimum between:
38 // - Remove it internally: suffixCost[i + 1] + 2
39 // - Remove everything from right: n - i
40 suffixCost[i] = min(suffixCost[i + 1] + 2, n - i);
41 }
42 }
43
44 // Find the minimum total cost by trying all split points
45 // At position i, we handle [0...i-1] from left and [i...n-1] from right
46 int minTotalCost = INT_MAX;
47 for (int splitPoint = 0; splitPoint <= n; ++splitPoint) {
48 int totalCost = prefixCost[splitPoint] + suffixCost[splitPoint];
49 minTotalCost = min(minTotalCost, totalCost);
50 }
51
52 return minTotalCost;
53 }
54};
55
1function minimumTime(s: string): number {
2 const n = s.length;
3
4 // prefixCost[i] represents the minimum cost to remove all '1's from s[0...i-1]
5 const prefixCost: number[] = new Array(n + 1).fill(0);
6
7 // suffixCost[i] represents the minimum cost to remove all '1's from s[i...n-1]
8 const suffixCost: number[] = new Array(n + 1).fill(0);
9
10 // Build prefix costs by scanning from left to right
11 // For each position, we have two choices:
12 // 1. Remove the car internally (adds cost of 2)
13 // 2. Remove all cars from the left end (costs i+1)
14 for (let i = 0; i < n; i++) {
15 if (s[i] === '0') {
16 // No illegal car at this position, inherit the previous cost
17 prefixCost[i + 1] = prefixCost[i];
18 } else {
19 // Found an illegal car ('1'), choose the minimum cost between:
20 // - Removing it internally: prefixCost[i] + 2
21 // - Removing everything from the left: i + 1
22 prefixCost[i + 1] = Math.min(prefixCost[i] + 2, i + 1);
23 }
24 }
25
26 // Build suffix costs by scanning from right to left
27 // For each position, we have two choices:
28 // 1. Remove the car internally (adds cost of 2)
29 // 2. Remove all cars from the right end (costs n-i)
30 for (let i = n - 1; i >= 0; i--) {
31 if (s[i] === '0') {
32 // No illegal car at this position, inherit the next cost
33 suffixCost[i] = suffixCost[i + 1];
34 } else {
35 // Found an illegal car ('1'), choose the minimum cost between:
36 // - Removing it internally: suffixCost[i + 1] + 2
37 // - Removing everything from the right: n - i
38 suffixCost[i] = Math.min(suffixCost[i + 1] + 2, n - i);
39 }
40 }
41
42 // Find the optimal split point to minimize total cost
43 // At each split point i, we handle:
44 // - Cars in range [0...i-1] using prefix strategy
45 // - Cars in range [i...n-1] using suffix strategy
46 let minTotalCost = Number.MAX_SAFE_INTEGER;
47 for (let splitPoint = 0; splitPoint <= n; splitPoint++) {
48 const totalCost = prefixCost[splitPoint] + suffixCost[splitPoint];
49 minTotalCost = Math.min(minTotalCost, totalCost);
50 }
51
52 return minTotalCost;
53}
54
Time and Space Complexity
Time Complexity: O(n)
where n
is the length of the string s
.
The algorithm consists of three main parts:
- First loop: Iterates through the string once to build the
pre
array, performingO(1)
operations for each element -O(n)
time - Second loop: Iterates through the string once in reverse to build the
suf
array, performingO(1)
operations for each element -O(n)
time - Final computation: Uses
zip
andmin
to iterate throughn
elements once, performingO(1)
addition for each pair -O(n)
time
Total time complexity: O(n) + O(n) + O(n) = O(n)
Space Complexity: O(n)
where n
is the length of the string s
.
The algorithm uses:
pre
array: storesn + 1
elements -O(n)
spacesuf
array: storesn + 1
elements -O(n)
space- The
zip
operation creates an iterator (not a new list) -O(1)
space - Other variables (
i
,c
,n
) use constant space -O(1)
space
Total space complexity: O(n) + O(n) + O(1) = O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Off-by-One Errors in Array Indexing
The most common mistake is misaligning the prefix and suffix arrays when calculating the final answer. Since pre[i]
represents the cost for the first i
cars (0-indexed), and suf[i]
represents the cost from car i
to the end, developers often incorrectly combine them.
Incorrect Implementation:
# Wrong: This double-counts or misses elements at boundaries
return min(pre[i] + suf[i] for i in range(n))
Correct Implementation:
# Correct: pre[i] covers [0, i) and suf[i] covers [i, n)
return min(pre[i] + suf[i] for i in range(n + 1))
2. Forgetting to Handle Empty String or All-Zero Cases
If the string contains no '1's or is empty, the algorithm should return 0, but improper initialization can cause issues.
Problem Example:
# If s = "000", without proper handling: # Some implementations might still calculate non-zero costs
Solution: Initialize both arrays with zeros and ensure the logic handles '0' characters by carrying forward the previous cost without modification.
3. Incorrect Cost Calculation for Removing from Ends
A frequent error is using the wrong formula for the cost of removing all cars from an end.
Incorrect:
# Wrong: Using i instead of i+1 for left removal
pre[i + 1] = min(pre[i] + 2, i) # Should be i+1
# Wrong: Using n-i-1 instead of n-i for right removal
suf[i] = min(suf[i + 1] + 2, n - i - 1) # Should be n-i
Correct:
# Removing first i+1 cars costs i+1 units
pre[i + 1] = min(pre[i] + 2, i + 1)
# Removing cars from position i to end costs n-i units
suf[i] = min(suf[i + 1] + 2, n - i)
4. Not Considering the Option to Remove Everything from One Side
Some implementations forget that removing all cars from one side might be optimal, especially for strings like "00001" or "10000".
Solution: Always include the boundary cases in the final comparison:
pre[n] + suf[n]
represents removing all from the left (cost = n)pre[0] + suf[0]
represents removing all from the right (cost = n)
These should be naturally included when iterating through all split points from 0 to n.
Which two pointer techniques do you use to check if a string is a palindrome?
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
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
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!