Facebook Pixel

3119. Maximum Number of Potholes That Can Be Fixed πŸ”’

Problem Description

You have a string road that represents a road condition where:

  • Each "x" character represents a pothole
  • Each "." character represents smooth road

You're given a budget to repair potholes. The repair cost works as follows:

  • You can repair n consecutive potholes in one operation
  • The cost for repairing n consecutive potholes is n + 1

For example:

  • Repairing 1 pothole costs 2
  • Repairing 3 consecutive potholes costs 4
  • Repairing 5 consecutive potholes costs 6

Your goal is to find the maximum number of potholes you can repair without exceeding the given budget.

The key insight is that you need to choose which groups of consecutive potholes to repair to maximize the total number of potholes fixed while staying within budget. You don't have to repair all potholes in a group - you can choose to repair them partially or skip them entirely based on your budget constraints.

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

Intuition

Let's think about the cost efficiency of repairing potholes. When we repair n consecutive potholes, we pay n + 1, which means the cost per pothole is (n + 1) / n.

For different lengths:

  • 1 pothole: cost = 2, cost per pothole = 2/1 = 2.00
  • 2 potholes: cost = 3, cost per pothole = 3/2 = 1.50
  • 3 potholes: cost = 4, cost per pothole = 4/3 = 1.33
  • 4 potholes: cost = 5, cost per pothole = 5/4 = 1.25
  • 5 potholes: cost = 6, cost per pothole = 6/5 = 1.20

Notice that the cost per pothole decreases as we repair longer consecutive segments. This means repairing longer stretches of potholes is more cost-efficient.

Given this observation, our strategy should be to prioritize repairing the longest consecutive pothole segments first, as they give us the best "bang for our buck."

However, there's a twist: if we can't afford to repair a complete long segment (say length 5), we might consider breaking it down. For instance, if we have a segment of 5 potholes but can only afford to repair 3, we could treat it as a segment of 3 potholes (which we repair) and a segment of 2 potholes (which we leave for later consideration).

This leads us to the approach: first count all consecutive pothole segments by their lengths, then greedily repair from the longest segments to the shortest, breaking down segments when our budget doesn't allow full repairs. This ensures we maximize the number of potholes repaired within our budget constraint.

Learn more about Greedy and Sorting patterns.

Solution Approach

The implementation follows our greedy strategy of prioritizing longer pothole segments:

Step 1: Count consecutive pothole segments

First, we append a "." to the road string to ensure we properly count the last segment if it ends with potholes. We create an array cnt where cnt[k] stores the count of segments that have exactly k consecutive potholes.

We iterate through the road string, counting consecutive "x" characters. When we encounter a ".", we record the count in our cnt array. For example, if the road is "xx.xxx.", we'd have:

  • One segment of length 2
  • One segment of length 3
  • So cnt[2] = 1 and cnt[3] = 1

Step 2: Greedily repair from longest to shortest

We iterate from the longest possible segment length n-1 down to 1. For each length k:

  1. Calculate how many complete segments of length k we can afford: t = min(budget // (k + 1), cnt[k])

    • budget // (k + 1) tells us how many segments we can afford
    • We take the minimum with cnt[k] since we can't repair more segments than we have
  2. Update our answer: ans += t * k

    • We repaired t segments, each containing k potholes
  3. Update the budget: budget -= t * (k + 1)

    • Each segment of length k costs k + 1
  4. Handle leftover segments: cnt[k - 1] += cnt[k] - t

    • If we couldn't repair all segments of length k, the remaining cnt[k] - t segments are "broken down" into segments of length k-1
    • This clever trick allows us to consider partial repairs of longer segments

The algorithm continues until we've either exhausted our budget or considered all segment lengths. This greedy approach ensures we maximize the number of potholes repaired by always choosing the most cost-efficient repairs first.

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 with road = "xx.xxx.x" and budget = 7.

Step 1: Identify consecutive pothole segments

First, we append a "." to get "xx.xxx.x." and scan for consecutive potholes:

  • "xx" β†’ segment of length 2
  • "xxx" β†’ segment of length 3
  • "x" β†’ segment of length 1

Our count array becomes:

  • cnt[1] = 1 (one segment of length 1)
  • cnt[2] = 1 (one segment of length 2)
  • cnt[3] = 1 (one segment of length 3)

Step 2: Process segments from longest to shortest

Round 1: Length 3

  • We have cnt[3] = 1 segment of length 3
  • Cost to repair one segment: 3 + 1 = 4
  • Can we afford it? budget // 4 = 7 // 4 = 1 segment
  • We can repair: min(1, 1) = 1 segment
  • Potholes repaired: 1 Γ— 3 = 3
  • Budget remaining: 7 - 4 = 3
  • Running total: 3 potholes repaired

Round 2: Length 2

  • We have cnt[2] = 1 segment of length 2
  • Cost to repair one segment: 2 + 1 = 3
  • Can we afford it? budget // 3 = 3 // 3 = 1 segment
  • We can repair: min(1, 1) = 1 segment
  • Potholes repaired: 1 Γ— 2 = 2
  • Budget remaining: 3 - 3 = 0
  • Running total: 3 + 2 = 5 potholes repaired

Round 3: Length 1

  • We have cnt[1] = 1 segment of length 1
  • Cost to repair one segment: 1 + 1 = 2
  • Can we afford it? budget // 2 = 0 // 2 = 0 segments
  • Budget exhausted, can't repair any more

Final Result: We repaired 5 potholes total (the segments of length 3 and 2), leaving only the single pothole unrepaired. This demonstrates how the greedy approach prioritizes longer segments for better cost efficiency.

Solution Implementation

1class Solution:
2    def maxPotholes(self, road: str, budget: int) -> int:
3        # Add a sentinel to ensure the last sequence of potholes is processed
4        road += "."
5        road_length = len(road)
6      
7        # Array to store count of pothole sequences by their length
8        # sequence_count[i] = number of sequences with length i
9        sequence_count = [0] * road_length
10      
11        # Current sequence length being tracked
12        current_sequence_length = 0
13      
14        # Count all consecutive pothole sequences
15        for char in road:
16            if char == "x":
17                # Found a pothole, increment current sequence length
18                current_sequence_length += 1
19            elif current_sequence_length > 0:
20                # End of a pothole sequence, record its length
21                sequence_count[current_sequence_length] += 1
22                current_sequence_length = 0
23      
24        # Track total potholes fixed
25        fixed_potholes = 0
26      
27        # Greedily fix longest sequences first (more cost-effective)
28        for length in range(road_length - 1, 0, -1):
29            if sequence_count[length] == 0:
30                continue
31          
32            # Calculate how many sequences of this length we can afford to fix
33            # Cost to fix a sequence of length k is k + 1
34            sequences_to_fix = min(budget // (length + 1), sequence_count[length])
35          
36            # Add fixed potholes to result
37            fixed_potholes += sequences_to_fix * length
38          
39            # Deduct cost from budget
40            budget -= sequences_to_fix * (length + 1)
41          
42            if budget == 0:
43                break
44          
45            # Remaining unfixed sequences become sequences of length - 1
46            # (partial fixing strategy)
47            sequence_count[length - 1] += sequence_count[length] - sequences_to_fix
48      
49        return fixed_potholes
50
1class Solution {
2    public int maxPotholes(String road, int budget) {
3        // Append a dot to handle the last segment of potholes
4        road += ".";
5        int roadLength = road.length();
6      
7        // Array to count segments of consecutive potholes by their length
8        // segmentCount[i] represents the number of segments with exactly i consecutive potholes
9        int[] segmentCount = new int[roadLength];
10      
11        // Counter for current consecutive pothole length
12        int currentPotholeLength = 0;
13      
14        // Traverse the road to count consecutive pothole segments
15        for (char currentChar : road.toCharArray()) {
16            if (currentChar == 'x') {
17                // Found a pothole, increment current segment length
18                currentPotholeLength++;
19            } else if (currentPotholeLength > 0) {
20                // End of a pothole segment, record its length
21                segmentCount[currentPotholeLength]++;
22                currentPotholeLength = 0;
23            }
24        }
25      
26        // Maximum number of potholes that can be fixed
27        int maxFixedPotholes = 0;
28      
29        // Greedy approach: Fix longer segments first (more efficient use of budget)
30        // Start from longest possible segments and work downward
31        for (int segmentLength = roadLength - 1; segmentLength > 0 && budget > 0; segmentLength--) {
32            // Cost to fix a segment of length k is (k + 1)
33            // Calculate how many segments of this length we can afford to fix
34            int segmentsToFix = Math.min(budget / (segmentLength + 1), segmentCount[segmentLength]);
35          
36            // Add fixed potholes to result
37            maxFixedPotholes += segmentsToFix * segmentLength;
38          
39            // Deduct cost from budget
40            budget -= segmentsToFix * (segmentLength + 1);
41          
42            // Unfixed segments of length k become segments of length (k-1)
43            // Add them to the count of shorter segments
44            segmentCount[segmentLength - 1] += segmentCount[segmentLength] - segmentsToFix;
45        }
46      
47        return maxFixedPotholes;
48    }
49}
50
1class Solution {
2public:
3    int maxPotholes(string road, int budget) {
4        // Add a sentinel '.' at the end to handle the last segment of potholes
5        road.push_back('.');
6        int n = road.size();
7      
8        // count[i] stores the number of pothole segments of length i
9        vector<int> count(n);
10      
11        // Track the current consecutive pothole length
12        int currentPotholeLength = 0;
13      
14        // Count all consecutive pothole segments
15        for (char& ch : road) {
16            if (ch == 'x') {
17                // Found a pothole, increment current segment length
18                ++currentPotholeLength;
19            } else if (currentPotholeLength > 0) {
20                // End of a pothole segment, record its length
21                ++count[currentPotholeLength];
22                currentPotholeLength = 0;
23            }
24        }
25      
26        int maxFixed = 0;
27      
28        // Greedily fix longer segments first (more efficient use of budget)
29        // Cost to fix a segment of length k is (k + 1)
30        for (int segmentLength = n - 1; segmentLength > 0 && budget > 0; --segmentLength) {
31            // Calculate how many segments of this length we can afford to fix
32            int segmentsToFix = min(budget / (segmentLength + 1), count[segmentLength]);
33          
34            // Add the number of potholes fixed
35            maxFixed += segmentsToFix * segmentLength;
36          
37            // Deduct the cost from budget
38            budget -= segmentsToFix * (segmentLength + 1);
39          
40            // Unfixed segments of length k become available as segments of length (k-1)
41            // for potential partial fixing in next iteration
42            count[segmentLength - 1] += count[segmentLength] - segmentsToFix;
43        }
44      
45        return maxFixed;
46    }
47};
48
1/**
2 * Calculates the maximum number of potholes that can be fixed within budget.
3 * Each consecutive sequence of 'x' characters represents potholes that need fixing.
4 * Fixing a sequence of k potholes costs k+1 units.
5 * 
6 * @param road - String representing the road where 'x' is a pothole and '.' is normal road
7 * @param budget - Available budget for fixing potholes
8 * @returns Maximum number of potholes that can be fixed
9 */
10function maxPotholes(road: string, budget: number): number {
11    // Add a delimiter to ensure the last sequence is processed
12    road += '.';
13    const roadLength: number = road.length;
14  
15    // Array to count sequences of each length (index represents sequence length)
16    const sequenceCount: number[] = Array(roadLength).fill(0);
17  
18    // Track current sequence length
19    let currentSequenceLength: number = 0;
20  
21    // Count all pothole sequences by their lengths
22    for (const char of road) {
23        if (char === 'x') {
24            // Continue counting current pothole sequence
25            currentSequenceLength++;
26        } else if (currentSequenceLength > 0) {
27            // End of pothole sequence, record its length
28            sequenceCount[currentSequenceLength]++;
29            currentSequenceLength = 0;
30        }
31    }
32  
33    // Track total potholes fixed
34    let totalPotholesFixed: number = 0;
35  
36    // Process sequences from longest to shortest (greedy approach for maximum efficiency)
37    for (let sequenceLength = roadLength - 1; sequenceLength > 0 && budget > 0; sequenceLength--) {
38        // Calculate how many sequences of this length we can afford to fix
39        const costPerSequence: number = sequenceLength + 1;
40        const sequencesToFix: number = Math.min(
41            Math.floor(budget / costPerSequence), 
42            sequenceCount[sequenceLength]
43        );
44      
45        // Update total potholes fixed
46        totalPotholesFixed += sequencesToFix * sequenceLength;
47      
48        // Deduct cost from budget
49        budget -= sequencesToFix * costPerSequence;
50      
51        // Split remaining unfixed sequences into smaller sequences
52        // (sequences of length k become sequences of length k-1)
53        sequenceCount[sequenceLength - 1] += sequenceCount[sequenceLength] - sequencesToFix;
54    }
55  
56    return totalPotholesFixed;
57}
58

Time and Space Complexity

Time Complexity: O(n)

The algorithm consists of two main passes:

  1. The first loop iterates through the road string once to count consecutive potholes, taking O(n) time where n is the length of the road string.
  2. The second loop iterates through the cnt array from index n-1 down to 1. In the worst case, this is O(n) iterations. Although there's a redistribution operation cnt[k - 1] += cnt[k] - t within the loop, each pothole segment is processed at most once and potentially redistributed, but the total work across all iterations remains bounded by O(n).

Therefore, the overall time complexity is O(n) + O(n) = O(n).

Space Complexity: O(n)

The algorithm uses an array cnt of size n to store the count of pothole segments of each length. This array requires O(n) space. The other variables (k, ans, budget, t) use constant space O(1).

Therefore, the overall space complexity is O(n).

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

Common Pitfalls

1. Forgetting to Handle the Last Pothole Sequence

One of the most common mistakes is failing to process the last sequence of potholes when the road string ends with "x" characters. Without proper handling, the algorithm would miss counting these final potholes.

Why it happens: When iterating through the string and counting consecutive potholes, we only record a sequence when we encounter a ".". If the string ends with potholes, there's no final "." to trigger the recording.

Example of the bug:

# Buggy version - misses last sequence
for char in road:
    if char == "x":
        current_sequence_length += 1
    elif current_sequence_length > 0:
        sequence_count[current_sequence_length] += 1
        current_sequence_length = 0
# If road = "xx.xxx", only the first sequence (length 2) gets counted

Solution: Add a sentinel character (like ".") to the end of the road string to ensure all sequences are processed:

road += "."  # Ensures last sequence is counted

2. Incorrect Partial Repair Logic

Another critical pitfall is mishandling the "leftover" sequences when we can't afford to repair all sequences of a given length. The algorithm cleverly breaks down unrepaired sequences into shorter ones for future consideration.

Why it happens: It's tempting to simply skip sequences we can't fully afford, but this misses optimization opportunities where we could repair them as shorter sequences.

Example of the bug:

# Buggy version - doesn't handle partial repairs
for length in range(road_length - 1, 0, -1):
    sequences_to_fix = min(budget // (length + 1), sequence_count[length])
    fixed_potholes += sequences_to_fix * length
    budget -= sequences_to_fix * (length + 1)
    # Missing: sequence_count[length - 1] += sequence_count[length] - sequences_to_fix

Impact: If we have a sequence of 5 potholes but can only afford to repair sequences of length 3, the buggy version would skip it entirely instead of considering it as a sequence of length 4 in the next iteration.

Solution: Always transfer unrepaired sequences to the next shorter length:

sequence_count[length - 1] += sequence_count[length] - sequences_to_fix

3. Off-by-One Error in Cost Calculation

A subtle but impactful mistake is using the wrong cost formula, such as using length instead of length + 1 for the repair cost.

Example of the bug:

# Wrong: using length instead of length + 1
sequences_to_fix = min(budget // length, sequence_count[length])  # Bug!
budget -= sequences_to_fix * length  # Bug!

Solution: Always remember the cost formula is n + 1 for repairing n consecutive potholes:

sequences_to_fix = min(budget // (length + 1), sequence_count[length])
budget -= sequences_to_fix * (length + 1)
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What is the best way of checking if an element exists in an unsorted array once in terms of time complexity? Select the best that applies.


Recommended Readings

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

Load More