2188. Minimum Time to Finish the Race
Problem Description
You're managing a race car that needs to complete a certain number of laps. You have access to different types of tires, and each tire type has specific performance characteristics.
For each tire type, you're given two values:
f
: the time (in seconds) it takes to complete the first lap with this tirer
: a degradation factor that makes each subsequent lap slower
The time to complete lap x
with a tire is calculated as: f * r^(x-1)
seconds.
For example, if a tire has f = 3
and r = 2
:
- Lap 1:
3 * 2^0 = 3
seconds - Lap 2:
3 * 2^1 = 6
seconds - Lap 3:
3 * 2^2 = 12
seconds - And so on...
You can change tires between laps, but changing tires costs changeTime
seconds. You have unlimited supplies of each tire type, and you can even change to the same tire type (essentially getting a fresh tire).
Given:
- A 2D array
tires
wheretires[i] = [f_i, r_i]
represents the characteristics of the i-th tire type changeTime
: the time penalty for changing tiresnumLaps
: the total number of laps to complete
Find the minimum total time needed to complete all numLaps
laps. You can start with any tire and strategically change tires during the race to minimize the total time.
The key insight is that as tires degrade (making laps progressively slower), there's a trade-off between continuing with a degraded tire versus spending time to change to a fresh tire.
Intuition
The key observation is that as we use a tire continuously, each lap becomes exponentially more expensive due to the degradation factor r
. At some point, it becomes more efficient to change to a fresh tire (paying the changeTime
penalty) rather than continuing with the degraded one.
First, let's think about when we should stop using a tire. If the next lap with the current tire takes longer than changeTime + f
(the cost of changing plus one lap with a fresh tire), then we should definitely change. This gives us a natural upper bound on how many consecutive laps we should consider with any single tire.
Since the lap times grow exponentially with factor r
, and the minimum r
is 2, the number of consecutive laps we'd ever want to use a single tire is quite limited. In practice, we'll rarely want to use the same tire for more than 17-18 consecutive laps before the exponential growth makes it inefficient.
This leads us to a dynamic programming approach:
-
Precompute the minimum time to complete exactly
j
consecutive laps using any single tire (trying all tire types and taking the minimum). We only need to compute this for small values ofj
(up to about 18) because beyond that, the exponential growth makes it always better to change tires. -
Build up the solution using dynamic programming: For each number of laps from 1 to
numLaps
, we consider all possible ways to complete those laps. We could:- Use a single tire for all laps (if the number is small enough)
- Complete some laps with one tire, change, then complete the rest (recursively using our previous computations)
The formula becomes: f[i] = min(f[i-j] + cost[j] + changeTime)
for all valid j
, where:
f[i]
is the minimum time to completei
lapscost[j]
is the precomputed minimum time to doj
consecutive laps with a single tire- We add
changeTime
for each tire change
By building up from smaller subproblems to larger ones, we ensure we always have the optimal solution for completing any number of laps up to numLaps
.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution implements a two-phase dynamic programming approach:
Phase 1: Precompute minimum costs for consecutive laps
cost = [inf] * 18
for f, r in tires:
i, s, t = 1, 0, f
while t <= changeTime + f:
s += t
cost[i] = min(cost[i], s)
t *= r
i += 1
For each tire type, we calculate the total time to complete consecutive laps:
t
tracks the time for the current lap (starts atf
, then multiplies byr
each iteration)s
accumulates the total time for all laps so far- We continue while
t <= changeTime + f
, meaning the current lap is still faster than changing to a fresh tire cost[i]
stores the minimum time across all tire types to complete exactlyi
consecutive laps
The array size of 18 is sufficient because with the minimum r = 2
, lap times grow exponentially and quickly exceed the threshold.
Phase 2: Dynamic programming to find minimum time for numLaps
f = [inf] * (numLaps + 1)
f[0] = -changeTime
for i in range(1, numLaps + 1):
for j in range(1, min(18, i + 1)):
f[i] = min(f[i], f[i - j] + cost[j])
f[i] += changeTime
Here we build up the solution:
f[i]
represents the minimum time to completei
lapsf[0] = -changeTime
is a clever initialization that cancels out the first tire change (since we start with a fresh tire, there's no initial change time)- For each number of laps
i
, we try all possible ways to complete them:- Complete the last
j
laps with a single tire (usingcost[j]
) - The remaining
i - j
laps were completed optimally before (usingf[i - j]
)
- Complete the last
- After finding the minimum, we add
changeTime
to account for the tire change before these laps
The recurrence relation is: f[i] = min(f[i - j] + cost[j] + changeTime)
for all valid j
.
The final answer f[numLaps]
gives us the minimum time to complete all required laps, optimally balancing between tire degradation and change time penalties.
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 the solution approach.
Input:
tires = [[2, 3], [3, 4]]
(two tire types)changeTime = 5
numLaps = 4
Phase 1: Precompute minimum costs for consecutive laps
For tire type [2, 3]:
- Lap 1: time = 2 × 3^0 = 2, cumulative = 2
- Lap 2: time = 2 × 3^1 = 6, cumulative = 8
- Lap 3: time = 2 × 3^2 = 18, cumulative = 26
- Stop here since 18 > changeTime + f (5 + 2 = 7)
For tire type [3, 4]:
- Lap 1: time = 3 × 4^0 = 3, cumulative = 3
- Lap 2: time = 3 × 4^1 = 12, cumulative = 15
- Stop here since 12 > changeTime + f (5 + 3 = 8)
Our cost
array becomes:
cost[1] = min(2, 3) = 2
(best single lap)cost[2] = min(8, 15) = 8
(best two consecutive laps)cost[3] = 26
(only first tire can do 3 consecutive)cost[4+] = infinity
(no tire can efficiently do 4+ consecutive laps)
Phase 2: Build up solution using dynamic programming
Initialize: f[0] = -5
(negates first change time)
For i = 1
(complete 1 lap):
- Option: use last 1 lap with single tire:
f[0] + cost[1] = -5 + 2 = -3
- Add change time:
f[1] = -3 + 5 = 2
For i = 2
(complete 2 laps):
- Option 1: use last 1 lap:
f[1] + cost[1] = 2 + 2 = 4
- Option 2: use last 2 laps:
f[0] + cost[2] = -5 + 8 = 3
- Minimum: 3, add change time:
f[2] = 3 + 5 = 8
For i = 3
(complete 3 laps):
- Option 1: use last 1 lap:
f[2] + cost[1] = 8 + 2 = 10
- Option 2: use last 2 laps:
f[1] + cost[2] = 2 + 8 = 10
- Option 3: use last 3 laps:
f[0] + cost[3] = -5 + 26 = 21
- Minimum: 10, add change time:
f[3] = 10 + 5 = 15
For i = 4
(complete 4 laps):
- Option 1: use last 1 lap:
f[3] + cost[1] = 15 + 2 = 17
- Option 2: use last 2 laps:
f[2] + cost[2] = 8 + 8 = 16
- Option 3: use last 3 laps:
f[1] + cost[3] = 2 + 26 = 28
- Minimum: 16, add change time:
f[4] = 16 + 5 = 21
Result: The minimum time to complete 4 laps is 21 seconds.
This corresponds to the strategy: Use first tire for 2 laps (8 seconds), change tires (5 seconds), use first tire for 2 more laps (8 seconds) = 21 total seconds.
Solution Implementation
1class Solution:
2 def minimumFinishTime(
3 self, tires: List[List[int]], changeTime: int, numLaps: int
4 ) -> int:
5 # Maximum consecutive laps to consider with same tire (18 is sufficient
6 # because lap time grows exponentially and will exceed changeTime + first_lap)
7 MAX_CONSECUTIVE_LAPS = 18
8
9 # min_cost_consecutive[i] = minimum time to complete i consecutive laps with any single tire
10 min_cost_consecutive = [float('inf')] * MAX_CONSECUTIVE_LAPS
11
12 # Calculate minimum cost for each possible number of consecutive laps
13 for first_lap_time, ratio in tires:
14 consecutive_laps = 1
15 total_time = 0
16 current_lap_time = first_lap_time
17
18 # Keep adding laps until it's no longer beneficial to continue with same tire
19 # (when current lap time exceeds the cost of changing tire + starting fresh)
20 while current_lap_time <= changeTime + first_lap_time:
21 total_time += current_lap_time
22 min_cost_consecutive[consecutive_laps] = min(
23 min_cost_consecutive[consecutive_laps],
24 total_time
25 )
26 current_lap_time *= ratio
27 consecutive_laps += 1
28
29 # dp[i] = minimum time to complete i laps (including tire changes)
30 dp = [float('inf')] * (numLaps + 1)
31 dp[0] = -changeTime # Base case: compensate for the extra changeTime added later
32
33 # Dynamic programming to find minimum time for each lap count
34 for lap in range(1, numLaps + 1):
35 # Try all possible ways to complete the last segment of consecutive laps
36 for last_segment_laps in range(1, min(MAX_CONSECUTIVE_LAPS, lap + 1)):
37 dp[lap] = min(
38 dp[lap],
39 dp[lap - last_segment_laps] + min_cost_consecutive[last_segment_laps]
40 )
41 # Add tire change time (for transitioning to this segment)
42 dp[lap] += changeTime
43
44 return dp[numLaps]
45
1class Solution {
2 public int minimumFinishTime(int[][] tires, int changeTime, int numLaps) {
3 // Define a large value to represent infinity
4 final int INFINITY = 1 << 30;
5
6 // Array to store minimum cost for completing i consecutive laps without changing tires
7 // We only need up to 17 laps because after that, it's always better to change tires
8 int[] minCostWithoutChange = new int[18];
9 Arrays.fill(minCostWithoutChange, INFINITY);
10
11 // Calculate minimum cost for each possible number of consecutive laps with each tire
12 for (int[] tire : tires) {
13 int lapTime = tire[0]; // Time for first lap with this tire
14 int degradationRate = tire[1]; // Rate at which lap time increases
15
16 int totalTime = 0; // Total time accumulated
17 int currentLapTime = lapTime; // Current lap's time
18
19 // Calculate cost for consecutive laps until it's no longer beneficial
20 // Stop when single lap time exceeds changeTime + first lap time
21 for (int consecutiveLaps = 1; currentLapTime <= changeTime + lapTime; consecutiveLaps++) {
22 totalTime += currentLapTime;
23 minCostWithoutChange[consecutiveLaps] = Math.min(minCostWithoutChange[consecutiveLaps], totalTime);
24 currentLapTime *= degradationRate;
25 }
26 }
27
28 // Dynamic programming array where dp[i] represents minimum time to complete i laps
29 int[] dp = new int[numLaps + 1];
30 Arrays.fill(dp, INFINITY);
31
32 // Base case: 0 laps takes negative changeTime (to offset the first change)
33 dp[0] = -changeTime;
34
35 // Fill DP table for each number of laps
36 for (int currentLaps = 1; currentLaps <= numLaps; currentLaps++) {
37 // Try all possible numbers of consecutive laps before changing tire
38 for (int consecutiveLaps = 1; consecutiveLaps < Math.min(18, currentLaps + 1); consecutiveLaps++) {
39 // Update minimum time by considering:
40 // time for (currentLaps - consecutiveLaps) laps + time for consecutiveLaps without change
41 dp[currentLaps] = Math.min(dp[currentLaps],
42 dp[currentLaps - consecutiveLaps] + minCostWithoutChange[consecutiveLaps]);
43 }
44 // Add tire change time
45 dp[currentLaps] += changeTime;
46 }
47
48 return dp[numLaps];
49 }
50}
51
1class Solution {
2public:
3 int minimumFinishTime(vector<vector<int>>& tires, int changeTime, int numLaps) {
4 // minCost[i] stores the minimum time to complete exactly i laps without changing tires
5 // We only need to consider up to 17 laps with the same tire
6 // (since after that, it's always better to change tires)
7 int minCost[18];
8 memset(minCost, 0x3f, sizeof(minCost)); // Initialize with large values
9
10 // Calculate the minimum cost for completing 1 to 17 laps with each tire type
11 for (auto& tire : tires) {
12 int firstLapTime = tire[0];
13 int ratio = tire[1];
14 int totalTime = 0;
15 long long currentLapTime = firstLapTime;
16
17 // Keep using the same tire while it's still beneficial
18 // Stop when lap time exceeds changeTime + firstLapTime
19 for (int lapCount = 1; currentLapTime <= changeTime + firstLapTime; ++lapCount) {
20 totalTime += currentLapTime;
21 minCost[lapCount] = min(minCost[lapCount], totalTime);
22 currentLapTime *= ratio; // Next lap takes ratio times longer
23 }
24 }
25
26 // dp[i] represents the minimum time to complete exactly i laps
27 int dp[numLaps + 1];
28 memset(dp, 0x3f, sizeof(dp)); // Initialize with large values
29 dp[0] = -changeTime; // Base case: compensate for the extra changeTime added later
30
31 // Dynamic programming to find minimum time for each lap count
32 for (int currentLaps = 1; currentLaps <= numLaps; ++currentLaps) {
33 // Try all possible last tire usage counts (1 to min(17, currentLaps))
34 for (int lastTireLaps = 1; lastTireLaps < min(18, currentLaps + 1); ++lastTireLaps) {
35 // Complete currentLaps by using:
36 // - dp[currentLaps - lastTireLaps]: time for previous laps
37 // - minCost[lastTireLaps]: time for last tire's laps
38 dp[currentLaps] = min(dp[currentLaps],
39 dp[currentLaps - lastTireLaps] + minCost[lastTireLaps]);
40 }
41 // Add changeTime for switching to this tire
42 dp[currentLaps] += changeTime;
43 }
44
45 return dp[numLaps];
46 }
47};
48
1/**
2 * Calculates the minimum time to complete a given number of laps in a race
3 * @param tires - Array of tire options, each with [baseTime, ratioMultiplier]
4 * @param changeTime - Time required to change tires
5 * @param numLaps - Total number of laps to complete
6 * @returns Minimum time to complete all laps
7 */
8function minimumFinishTime(tires: number[][], changeTime: number, numLaps: number): number {
9 // Maximum consecutive laps to consider using the same tire (empirically determined limit)
10 const MAX_CONSECUTIVE_LAPS: number = 18;
11
12 // minCostForConsecutiveLaps[i] stores the minimum time to complete i consecutive laps without changing tires
13 const minCostForConsecutiveLaps: number[] = Array(MAX_CONSECUTIVE_LAPS).fill(Infinity);
14
15 // Calculate minimum cost for each possible number of consecutive laps with each tire
16 for (const [baseTime, ratioMultiplier] of tires) {
17 let cumulativeTime: number = 0;
18 let currentLapTime: number = baseTime;
19
20 // Continue while using the same tire is still potentially beneficial
21 // Stop when lap time exceeds the cost of changing tire plus starting fresh
22 for (let lapCount: number = 1; currentLapTime <= changeTime + baseTime; lapCount++) {
23 cumulativeTime += currentLapTime;
24 minCostForConsecutiveLaps[lapCount] = Math.min(
25 minCostForConsecutiveLaps[lapCount],
26 cumulativeTime
27 );
28 // Each subsequent lap takes longer by the ratio multiplier
29 currentLapTime *= ratioMultiplier;
30 }
31 }
32
33 // dp[i] represents minimum time to complete i laps
34 const dp: number[] = Array(numLaps + 1).fill(Infinity);
35 // Base case: 0 laps takes negative change time (to offset first tire change)
36 dp[0] = -changeTime;
37
38 // Build up solution using dynamic programming
39 for (let currentLaps: number = 1; currentLaps <= numLaps; currentLaps++) {
40 // Try all possible ways to complete the last set of consecutive laps
41 const maxConsecutive: number = Math.min(MAX_CONSECUTIVE_LAPS, currentLaps + 1);
42
43 for (let consecutiveLaps: number = 1; consecutiveLaps < maxConsecutive; consecutiveLaps++) {
44 // Complete currentLaps by doing (currentLaps - consecutiveLaps) first,
45 // then doing consecutiveLaps with a fresh tire
46 dp[currentLaps] = Math.min(
47 dp[currentLaps],
48 dp[currentLaps - consecutiveLaps] + minCostForConsecutiveLaps[consecutiveLaps]
49 );
50 }
51 // Add tire change time (each segment requires changing to a fresh tire)
52 dp[currentLaps] += changeTime;
53 }
54
55 return dp[numLaps];
56}
57
Time and Space Complexity
Time Complexity: O(n + m²)
where n
is the number of tires and m
is the number of laps (numLaps
).
-
The first loop iterates through each tire and for each tire, it runs a while loop. The while loop is bounded by the condition
t <= changeTime + f
. Sincet
grows exponentially (t *= r
), this loop runs at most 17 times (hence the array size of 18). So this part isO(17n) = O(n)
. -
The second part uses dynamic programming with nested loops:
- Outer loop runs
numLaps
times - Inner loop runs at most
min(18, i+1)
times, which is bounded by 17 - However, when considering all iterations, the inner loop effectively runs
O(numLaps)
times in total across all outer loop iterations
This gives us
O(numLaps × min(17, numLaps)) = O(numLaps²)
for large values ofnumLaps
, orO(17 × numLaps) = O(numLaps)
whennumLaps
is small. - Outer loop runs
Overall: O(n + m²)
where m = numLaps
.
Space Complexity: O(m)
where m
is the number of laps.
- The
cost
array has a fixed size of 18, so it takesO(1)
space. - The
f
array has sizenumLaps + 1
, takingO(numLaps)
space.
Overall space complexity: O(numLaps)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Boundary for Consecutive Laps Calculation
One of the most common mistakes is using the wrong condition to determine when to stop using the same tire. Many developers incorrectly compare the current lap time with just changeTime
:
# WRONG: Missing the first lap time in comparison while current_lap_time <= changeTime: # ... rest of the code
The correct condition should be current_lap_time <= changeTime + first_lap_time
. This is because when deciding whether to continue with a degraded tire or change to a fresh one, we need to compare:
- Continuing with degraded tire:
current_lap_time
seconds - Changing to fresh tire:
changeTime + first_lap_time
seconds (change time plus the time for a fresh lap)
2. Overflow Issues with Exponential Growth
When calculating consecutive lap times, the values can grow exponentially and cause integer overflow:
# POTENTIAL ISSUE: No bounds checking
for first_lap_time, ratio in tires:
current_lap_time = first_lap_time
for i in range(1, 20): # Arbitrary large number
total_time += current_lap_time
current_lap_time *= ratio # Can overflow!
Solution: Always include the boundary check current_lap_time <= changeTime + first_lap_time
before multiplication to prevent overflow and unnecessary calculations.
3. Off-by-One Errors in DP Initialization
A subtle but critical pitfall is incorrectly initializing the DP base case:
# WRONG: Doesn't account for no initial tire change dp[0] = 0 # CORRECT: Compensates for the extra changeTime added in the loop dp[0] = -changeTime
Since the DP recurrence adds changeTime
for every segment (including the first one), we need to initialize dp[0] = -changeTime
to cancel out the unnecessary tire change time at the very beginning of the race.
4. Incorrect Array Sizing
Using a fixed array size without proper justification can lead to either:
- Too small: Missing optimal solutions for certain inputs
- Too large: Wasting memory and computation time
# WRONG: Arbitrary size without justification
min_cost_consecutive = [float('inf')] * 50
# CORRECT: Size 18 is sufficient based on problem constraints
min_cost_consecutive = [float('inf')] * 18
The size 18 is chosen because with minimum ratio = 2 and maximum changeTime = 10^5, the lap time will exceed changeTime + first_lap_time
well before reaching 18 consecutive laps.
5. Not Handling Edge Cases in Range
When iterating through possible segment sizes in the DP phase:
# WRONG: May go out of bounds
for last_segment_laps in range(1, 18):
dp[lap] = min(dp[lap], dp[lap - last_segment_laps] + ...)
# CORRECT: Ensures we don't exceed available laps
for last_segment_laps in range(1, min(18, lap + 1)):
dp[lap] = min(dp[lap], dp[lap - last_segment_laps] + ...)
Always use min(MAX_CONSECUTIVE_LAPS, lap + 1)
to ensure you don't try to use more consecutive laps than the total laps remaining.
Which of the following uses divide and conquer strategy?
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!