Facebook Pixel

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:

  1. Remove from left end: Remove the leftmost car (s[0]) - costs 1 unit of time
  2. Remove from right end: Remove the rightmost car (s[s.length - 1]) - costs 1 unit of time
  3. 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.

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

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:

  1. Remove the entire segment from one end (costs equal to the segment length)
  2. 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 first i cars
  • suf[i]: Stores the minimum cost to remove all illegal goods from car i to the end
  • Both arrays have size n+1 to handle boundary cases

Algorithm Implementation:

  1. 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 as pre[i]
    • If car i has illegal goods (c == '1'), we choose the minimum between:
      • pre[i] + 2: Keep previous arrangement and remove this car individually
      • i + 1: Remove all cars from the left up to and including this position
  2. 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 remains suf[i + 1]
    • If car i has illegal goods, choose minimum between:
      • suf[i + 1] + 2: Remove this car individually and handle the rest
      • n - i: Remove all cars from this position to the right end
  3. 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:] and suf[1:] to align the arrays properly (avoiding the boundary indices)

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 Evaluator

Example 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, so pre[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
  • 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
  • i=3, s[3]='0': Clean car, so pre[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, so suf[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
  • 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
  • i=0, s[0]='0': Clean car, so suf[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, performing O(1) operations for each element - O(n) time
  • Second loop: Iterates through the string once in reverse to build the suf array, performing O(1) operations for each element - O(n) time
  • Final computation: Uses zip and min to iterate through n elements once, performing O(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: stores n + 1 elements - O(n) space
  • suf array: stores n + 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.

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

Which two pointer techniques do you use to check if a string is a palindrome?


Recommended Readings

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

Load More