Facebook Pixel

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 tire
  • r: 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 where tires[i] = [f_i, r_i] represents the characteristics of the i-th tire type
  • changeTime: the time penalty for changing tires
  • numLaps: 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.

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

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:

  1. 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 of j (up to about 18) because beyond that, the exponential growth makes it always better to change tires.

  2. 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 complete i laps
  • cost[j] is the precomputed minimum time to do j 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 at f, then multiplies by r 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 exactly i 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 complete i laps
  • f[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 (using cost[j])
    • The remaining i - j laps were completed optimally before (using f[i - j])
  • 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 Evaluator

Example 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. Since t grows exponentially (t *= r), this loop runs at most 17 times (hence the array size of 18). So this part is O(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 of numLaps, or O(17 × numLaps) = O(numLaps) when numLaps is small.

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 takes O(1) space.
  • The f array has size numLaps + 1, taking O(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.

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

Which of the following uses divide and conquer strategy?


Recommended Readings

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

Load More