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 isn + 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.
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.
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
andcnt[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
:
-
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
-
Update our answer:
ans += t * k
- We repaired
t
segments, each containingk
potholes
- We repaired
-
Update the budget:
budget -= t * (k + 1)
- Each segment of length
k
costsk + 1
- Each segment of length
-
Handle leftover segments:
cnt[k - 1] += cnt[k] - t
- If we couldn't repair all segments of length
k
, the remainingcnt[k] - t
segments are "broken down" into segments of lengthk-1
- This clever trick allows us to consider partial repairs of longer segments
- If we couldn't repair all segments of length
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 EvaluatorExample 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:
- The first loop iterates through the
road
string once to count consecutive potholes, takingO(n)
time wheren
is the length of the road string. - The second loop iterates through the
cnt
array from indexn-1
down to 1. In the worst case, this isO(n)
iterations. Although there's a redistribution operationcnt[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 byO(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)
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
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
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!