Minimum Skips to Arrive at Meeting On Time: DP on Skips
You are given an array dist where dist[i] is the length of the i-th road, an integer speed, and an integer hoursBefore. You travel along n roads sequentially. After finishing each road (except the last), you must wait until the next integer hour before continuing, unless you choose to skip the rest. Return the minimum number of skips needed to arrive at the meeting on time, or -1 if it is impossible.
This is LeetCode 1883: Minimum Skips to Arrive at Meeting On Time, a hard DP problem where the key challenge is deciding which rest stops to skip. The brute force tries all combinations, but DP on the skip count collapses the exponential search into a clean O(n^2) solution.
Why this problem matters
This problem teaches a pattern that appears whenever you have a limited number of "exceptions" you can use along a sequence. Instead of tracking which specific positions you skip, you track how many skips you have used so far and what the best outcome is for each count. This "DP on exception count" pattern shows up in problems like Best Time to Buy and Sell Stock with at most k transactions, or any scenario where you have a budget of special moves.
The resting mechanic (ceiling to the next integer hour) also teaches you to be careful with floating point. Multiplying everything by speed to stay in integers is a technique worth having in your toolkit.
The brute force approach
The most direct approach is to try every possible subset of rest stops to skip. With n - 1 rest stops, there are 2^(n-1) subsets.
def minSkips_brute(dist, speed, hoursBefore):
n = len(dist)
best = n
def backtrack(i, time, skips):
nonlocal best
if i == n:
if time <= hoursBefore * speed:
best = min(best, skips)
return
t = time + dist[i]
if i < n - 1:
rest_time = ((t + speed - 1) // speed) * speed
backtrack(i + 1, rest_time, skips)
backtrack(i + 1, t, skips + 1)
else:
backtrack(i + 1, t, skips)
backtrack(0, 0, 0)
return best if best < n else -1
This is O(2^n) time, which is far too slow for n up to 1000. But it reveals the structure: at each rest stop, you make a binary choice (rest or skip), and the total time depends on the sequence of choices. That binary choice at each step is exactly what DP can optimize.
The key insight
Define dp[i][j] as the minimum total travel time (in speed-units, to avoid fractions) after completing road i using exactly j skips. For each road, you have two transitions:
- Rest (no skip): take
dp[i-1][j], adddist[i-1], then round up to the next multiple ofspeed. This costs 0 skips. - Skip: take
dp[i-1][j-1], adddist[i-1], and do not round up. This costs 1 skip.
For the last road, you never need to rest (there is no waiting after the final road), so you just add the distance directly.
Multiply all times by speed to work entirely in integers. Instead of computing ceil(time / speed) * speed, you compute ((time + speed - 1) // speed) * speed. This avoids floating-point precision bugs that can cause wrong answers on edge cases.
Walking through it step by step
Let's trace through dist = [1, 3, 2], speed = 4, hoursBefore = 2. The deadline in speed-units is 2 * 4 = 8. We build the DP table row by row.
Step 1: Initialize the DP table. dp[0][0] = 0 (no roads, no skips, time = 0).
dp[i][j] tracks the minimum time (times speed) after road i using j skips. We work in speed-units (multiply by 4) to avoid fractions.
Step 2: Road 0 (dist=1). Rest: ceil((0+1)/4)*4 = 4. Skip: 0+1 = 1.
j=0 (rest): time rounds up from 1 to 4 (next multiple of speed). j=1 (skip): time stays at 1.
Step 3: Road 1 (dist=3). Fill dp[2][j] from dp[1][j] with rest or skip.
j=0: rest from dp[1][0]=4, (4+3)=7, ceil to 8. j=1: skip from dp[1][0]=4+3=7, or rest from dp[1][1]=1+3=4, min is 4. j=2: skip from dp[1][1]=1+3=4.
Step 4: Road 2 (dist=2, last road). No resting needed after the final road.
Last road: just add dist[2]=2 to each dp[2][j]. dp[3][0]=10, dp[3][1]=6, dp[3][2]=6.
Step 5: Find the answer. First j where dp[3][j] <= hoursBefore * speed = 8.
dp[3][0]=10 > 8, so 0 skips is not enough. dp[3][1]=6 <= 8. Answer: 1 skip.
The answer is 1. With zero skips, you arrive at time 10 (in speed-units), which exceeds the deadline of 8. With one skip (skipping the rest after road 1), you arrive at time 6, which is within the deadline.
The complete solution
def minSkips(dist, speed, hoursBefore):
n = len(dist)
INF = float('inf')
dp = [[INF] * (n + 1) for _ in range(n + 1)]
dp[0][0] = 0
for i in range(1, n + 1):
for j in range(i + 1):
if dp[i - 1][j] < INF:
t = dp[i - 1][j] + dist[i - 1]
if i < n:
rest = ((t + speed - 1) // speed) * speed
else:
rest = t
dp[i][j] = min(dp[i][j], rest)
if j > 0 and dp[i - 1][j - 1] < INF:
t = dp[i - 1][j - 1] + dist[i - 1]
dp[i][j] = min(dp[i][j], t)
for j in range(n + 1):
if dp[n][j] <= hoursBefore * speed:
return j
return -1
Here is what each piece does:
- Initialize
dp[0][0] = 0. Before any road, zero time has elapsed and zero skips have been used. - For each road i (1 through n), for each possible skip count j (0 through i):
- Rest transition: from
dp[i-1][j], adddist[i-1], then ceil to the next multiple ofspeed(unless it is the last road). Updatedp[i][j]. - Skip transition: from
dp[i-1][j-1], adddist[i-1]without ceiling. Updatedp[i][j].
- Rest transition: from
- Find the answer: scan
dp[n][0], dp[n][1], ...and return the firstjwheredp[n][j]is at mosthoursBefore * speed. - Return -1 if no skip count is sufficient.
Complexity analysis
| Metric | Value |
|---|---|
| Time | O(n^2), two nested loops over roads and skip counts |
| Space | O(n^2), the full DP table of size (n+1) x (n+1) |
You can optimize space to O(n) by using a single row and updating it in reverse order (right to left on j), since each dp[i][j] only depends on dp[i-1][j] and dp[i-1][j-1].
Building blocks
This problem is built on two reusable patterns that CodeBricks drills independently.
1. DP on exception count
When you can use a limited number of "special moves" along a sequence, add the move count as a DP dimension. The state becomes dp[position][moves_used], and each transition either uses a move or does not.
dp = [[INF] * (max_moves + 1) for _ in range(n + 1)]
dp[0][0] = base_value
for i in range(1, n + 1):
for j in range(i + 1):
dp[i][j] = transition_normal(dp[i - 1][j], data[i - 1])
if j > 0:
dp[i][j] = min(dp[i][j], transition_special(dp[i - 1][j - 1], data[i - 1]))
This same skeleton solves "Best Time to Buy and Sell Stock with k transactions," "minimum cost with at most k modifications," and many other bounded-exception problems.
2. Integer arithmetic to avoid floating point
When the problem involves division and ceiling operations, multiply all quantities by the divisor so every value stays as an integer. This eliminates precision bugs.
time_in_units = time_raw * speed
deadline_in_units = hours * speed
rounded_up = ((time_in_units + speed - 1) // speed) * speed
This technique is essential in any problem where you compare fractional times or costs to a threshold.
The DP on exception count pattern is one of the most versatile DP extensions. Whenever a problem says "at most k" of some special operation, try adding k as a dimension to your DP state.
Edge cases
Before submitting, make sure your solution handles these:
- All distances fit without any skips: if the total time with all rests is already within the deadline, the answer is 0.
- Impossible even with all skips: if
sum(dist) / speedexceedshoursBefore, return -1. No amount of skipping can help since skipping only removes wait time, not travel time. - Single road (
n = 1): there is no rest stop to skip. Either you arrive in time or you do not. The answer is 0 or -1. - Speed equals 1: every road takes exactly
dist[i]hours, and resting always rounds up to the next integer. The ceiling operation simplifies sincedist[i]is already an integer. - Large values:
nup to 1000 with distances up to 10^5 and speed up to 10^6. The DP values can get large, so use integers (not floats) to avoid precision loss. - Exact deadline match:
dp[n][j]equalshoursBefore * speedexactly. This should return j since arriving exactly on time counts as on time.
Common mistakes
1. Using floating-point division. Computing time / speed as a float and then applying math.ceil leads to precision errors. For example, math.ceil(7.0 / 3.0) might give unexpected results due to floating-point rounding. Multiplying by speed and working in integers avoids this entirely.
2. Resting after the last road. The problem says you rest after each road except the last. If you apply the ceiling operation after the final road, you add unnecessary wait time and get a wrong answer.
3. Forgetting to check dp[i-1][j-1] < INF before the skip transition. If dp[i-1][j-1] is infinity (unreachable state), adding dist[i-1] to it is meaningless. Always guard against processing unreachable states.
4. Scanning for the answer in the wrong row. The answer is in row n (after all roads are completed), not row n-1. If you use 0-indexed roads, make sure you scan dp[n], not dp[n-1].
From understanding to recall
You have traced through the DP table, seen how rest and skip transitions fill in the values, and understood why integer arithmetic matters. But can you set up the state definition, write the two transitions, and handle the last-road special case from scratch in 15 minutes?
The details matter: initializing dp[0][0] = 0 with everything else as infinity, iterating j from 0 to i (not to n), applying the ceiling only for non-final roads, and scanning the last row for the smallest valid j. These are easy to mix up under pressure if you have not practiced writing them from memory.
Spaced repetition closes that gap. You write the DP setup, the rest-vs-skip transitions, and the answer scan from scratch at increasing intervals. After a few rounds, the pattern is automatic. You see "minimize skips" and the two-transition DP structure flows out without hesitation.
The DP on exception count pattern is one of roughly 60 reusable building blocks that cover hundreds of LeetCode problems. Learning them individually and drilling them with spaced repetition is far more effective than grinding random problems and hoping the patterns stick.
Related posts
- Edit Distance - Another 2D DP problem where one dimension tracks the position and the other tracks the cost of operations
- Coin Change - Classic DP with optimization count, finding the minimum number of coins (analogous to minimum skips)
- Dungeon Game - DP with careful boundary handling and the importance of integer arithmetic over floating point
CodeBricks breaks the minimum skips problem into its DP-on-exception-count and integer-arithmetic building blocks, then drills them independently with spaced repetition. You type each piece from scratch until the pattern is automatic. When a bounded-exception DP question shows up in your interview, you do not think about it. You just write it.