Skip to content

Commit 679feae

Browse files
committed
LeetCode 871. Minimum Number of Refueling Stops
1 parent 4629dae commit 679feae

File tree

3 files changed

+262
-1
lines changed

3 files changed

+262
-1
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ Proposed solutions to some LeetCode problems. The first column links to the prob
164164
| [844. Backspace String Compare][lc844] | 🟢 Easy | [![python](res/py.png)][lc844py] |
165165
| [858. Mirror Reflection][lc858] | 🟠 Medium | [![python](res/py.png)][lc858py] |
166166
| [867. Transpose Matrix][lc867] | 🟢 Easy | [![python](res/py.png)][lc867py] |
167+
| [871. Minimum Number of Refueling Stops][lc871] | 🔴 Hard | [![python](res/py.png)][lc871py] |
167168
| [875. Koko Eating Bananas][lc875] | 🟠 Medium | [![python](res/py.png)][lc875py] |
168169
| [876. Middle of the Linked List][lc876] | 🟢 Easy | [![python](res/py.png)][lc876py] |
169170
| [890. Find and Replace Pattern][lc890] | 🟠 Medium | [![python](res/py.png)][lc890py] |
@@ -476,6 +477,8 @@ Proposed solutions to some LeetCode problems. The first column links to the prob
476477
[lc858py]: leetcode/mirror-reflection.py
477478
[lc867]: https://leetcode.com/problems/transpose-matrix/
478479
[lc867py]: leetcode/transpose-matrix.py
480+
[lc871]: https://leetcode.com/problems/minimum-number-of-refueling-stops/
481+
[lc871py]: leetcode/minimum-number-of-refueling-stops.py
479482
[lc875]: https://leetcode.com/problems/koko-eating-bananas/
480483
[lc875py]: leetcode/koko-eating-bananas.py
481484
[lc876]: https://leetcode.com/problems/middle-of-the-linked-list/

leetcode/lists/dp.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ tracking your own progress.
169169
| | 🔴 Hard | [403. Frog Jump][lc403] | |
170170
| | 🔴 Hard | [410. Split Array Largest Sum][lc410] | |
171171
| | 🔴 Hard | [514. Freedom Trail][lc514] | |
172-
| | 🔴 Hard | [871. Minimum Number of Refueling Stops][lc871] | |
172+
| | 🔴 Hard | [871. Minimum Number of Refueling Stops][lc871] | [![python](../../res/py.png)][lc871py] |
173173
| | 🔴 Hard | [920. Number of Music Playlists][lc920] | |
174174
|| 🔴 Hard | [1220. Count Vowels Permutation][lc1220] | [![python](../../res/py.png)][lc1220py] |
175175
| | 🔴 Hard | [1223. Dice Roll Simulation][lc1223] | |
@@ -203,6 +203,7 @@ tracking your own progress.
203203
[lc410]: https://leetcode.com/problems/split-array-largest-sum/
204204
[lc514]: https://leetcode.com/problems/freedom-trail/
205205
[lc871]: https://leetcode.com/problems/minimum-number-of-refueling-stops/
206+
[lc871py]: ../minimum-number-of-refueling-stops.py
206207
[lc920]: https://leetcode.com/problems/number-of-music-playlists/
207208
[lc1220]: https://leetcode.com/problems/count-vowels-permutation/
208209
[lc1220py]: ../count-vowels-permutation.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
# 871. Minimum Number of Refueling Stops
2+
# 🔴 Hard
3+
#
4+
# https://leetcode.com/problems/minimum-number-of-refueling-stops/
5+
#
6+
# Tags: Array - Dynamic Programming - Greedy - Heap (Priority Queue)
7+
8+
import timeit
9+
from heapq import heappop, heappush
10+
from typing import List
11+
12+
13+
# The brute force approach visits every station that can be reached with
14+
# the current amount of fuel and, for each, decides to either refuel
15+
# there or skip it.
16+
#
17+
# Time complexity: O(2^n) - With n being the number of gas stations, we
18+
# choose to either visit or not visit each of them.
19+
# Space complexity: O(n) - We can have n calls in the stack before they
20+
# start returning results.
21+
#
22+
# This solution would fail with Time Limit Exceeded.
23+
# We could easily memoized visitNext using @functools.cache but it would
24+
# not help because most of the redundant work would still be done.
25+
class BruteForce:
26+
def minRefuelStops(
27+
self, target: int, startFuel: int, stations: List[List[int]]
28+
) -> int:
29+
# Define a function that explores the result of stopping and
30+
# skipping the next gas station and returns the one with the
31+
# least number of stops.
32+
# @param position: the current position.
33+
# @param fuel: the amount of fuel left.
34+
# @param idx: the idx of the gas station that we are considering
35+
# visiting or skipping.
36+
def visitNext(position: int, fuel: int, idx: int):
37+
# Base case, we have enough fuel to get to the target.
38+
if position + fuel >= target:
39+
# We don't need to do any more stops.
40+
return 0
41+
# Base case, we don't have enough fuel to get to the next
42+
# gas station we are trying to get to, or we are out of gas
43+
# stations where we can refuel.
44+
if idx == len(stations) or stations[idx][0] - position > fuel:
45+
return float("inf")
46+
# If we stop at this gas station, we start from the position
47+
# where the gas station is. Fuel is the current fuel minus
48+
# the fuel we use to get there plus the fuel at the gas
49+
# station, and the idx is the next gas station index.
50+
visit = (
51+
visitNext(
52+
stations[idx][0],
53+
fuel - (stations[idx][0] - position) + stations[idx][1],
54+
idx + 1,
55+
)
56+
+ 1
57+
)
58+
# If we skip this gas station, we try to get to the next one.
59+
skip = visitNext(position, fuel, idx + 1)
60+
# Return the option that has the least number of stops.
61+
return min(visit, skip)
62+
63+
# Initial call.
64+
res = visitNext(0, startFuel, 0)
65+
# If we couldn't find a way to get to the target, return
66+
if res == float("inf"):
67+
return -1
68+
return res
69+
70+
71+
# Keep a dp array with the index as the number of stops and the value
72+
# being the farthest we can drive with idx number of stops. Visit each
73+
# gas station and update the dp array backwards from its index to 0
74+
# with the best distance for this number of stops between the current
75+
# best and the best distance with one less stop plus the amount of gas
76+
# available at this station.
77+
#
78+
# Time complexity: O(n^2) - With n the number of gas stations. We visit
79+
# each gas station and for each, we visit all the previous ones inside
80+
# a nested loop.
81+
# Space complexity: O(n) - With n the number of gas stations, equal to
82+
# the size of the dp array.
83+
#
84+
# Runtime: 1202 ms, faster than 19.44%
85+
# Memory Usage: 14.3 MB, less than 10.13%
86+
class DP:
87+
def minRefuelStops(
88+
self, target: int, startFuel: int, stations: List[List[int]]
89+
) -> int:
90+
# Base case, we start with enough fuel to get to the target.
91+
if startFuel >= target:
92+
return 0
93+
# dp dictionary stores the furthest we can drive with key stops
94+
# for example, dp[3] will be the furthest we can drive with 3
95+
# refuelling stops. We initialize dp[0] with the initial gas.
96+
dp = [startFuel] + [0] * len(stations)
97+
# Iterate over the stations from closest to furthest away.
98+
for station_idx, (location, gas) in enumerate(stations):
99+
# For every station, iterate over all dp results from 0 to
100+
# the station's index, the maximum number of stops possible
101+
# at this point, in reverse order. [idx..0]
102+
for dp_idx in range(station_idx, -1, -1):
103+
# If we could have reached this gas station by stopping
104+
# at all previous ones along the way.
105+
if dp[dp_idx] >= location:
106+
# Then, the furthest we can drive if we stop at this
107+
# gas station is the maximum between previous
108+
# results and the maximum driving distance before
109+
# this stop plus the fuel stored at this station.
110+
dp[dp_idx + 1] = max(dp[dp_idx + 1], dp[dp_idx] + gas)
111+
# Once we have calculated the maximum distance we can travel by
112+
# refueling n times for n in [0..len(stations) - 1], we have
113+
# to iterate over the dp array to find the first result that
114+
# lets us travel all the way to the target.
115+
for station_idx, d in enumerate(dp):
116+
if d >= target:
117+
return station_idx
118+
return -1
119+
120+
121+
# It seems like there could be an optimization to the DP solution if
122+
# instead of iterating over the dp array after the calculations, we
123+
# stored the current shortest number of stops that lets us reach
124+
# target and returned that after visiting all gas stations, but the
125+
# code actually performs worst, probably because it checks the best
126+
# O(n^2) times instead of iterating over the dp array in O(n) and
127+
# returning the first match.
128+
#
129+
# Runtime: 2508 ms, faster than 5.15%
130+
# Memory Usage: 14.2 MB, less than 74.58%
131+
132+
133+
# A different approach than the evolution from brute force to dynamic
134+
# programming of the previous solutions is based on greedy.
135+
# We can make use of the fact that we don't need to decide if we stop
136+
# or not at a gas station as we visit. We can "drive past" all of them,
137+
# remembering the amount of gas that they had, until we "run out of gas"
138+
# then add the gas at the station that we passed with the most gas, the
139+
# best way of using one stop, to the current, and keep driving.
140+
# The best way to keep track of which gas station, out of the ones that
141+
# we have visited and not stopped at, has the most gas is using a heap.
142+
#
143+
# Time complexity: O(n*log(n)) - We visit each element and push it into
144+
# the heap at O(log(n)), occasionally we pop from the heap to refuel.
145+
# Even if we popped from the heap as often as we pulled the complexity
146+
# would remain the same.
147+
# Space complexity: O(n) - The heap can grow to the size of the input.
148+
#
149+
# Runtime: 214 ms, faster than 51.99%
150+
# Memory Usage: 14.2 MB, less than 74.58%
151+
class Heap:
152+
def minRefuelStops(
153+
self, target: int, startFuel: int, stations: List[List[int]]
154+
) -> int:
155+
# Priority queue with the amount of gas in stations that we have
156+
# passed already and could have stopped at. Since Python only
157+
# has a min heap and we want the max gas, numbers are negated.
158+
skipped = []
159+
# Store the current position and the number of stops that we
160+
# have done already.
161+
position = stops = 0
162+
# Keep track of the current amount of fuel left.
163+
fuel = startFuel
164+
# Adding target to the array of stations to visit simplifies the
165+
# logic of the while loop. The problem guarantees that target is
166+
# greater than the position of any of the gas stations.
167+
stations.append([target, 0])
168+
# Start driving while we have fuel and we have not refuelled at
169+
# all stations.
170+
for location, gas in stations:
171+
# If we don't have enough gas to get to the next station,
172+
# pretend that we stopped at the skipped station with the
173+
# most gas.
174+
while position + fuel < location:
175+
# If there is no station available, we will not be
176+
# able to get to the end.
177+
if not skipped:
178+
return -1
179+
# Else, pretend that we filled up at the gas station
180+
# with the most gas we drove past.
181+
fuel -= heappop(skipped)
182+
stops += 1
183+
# Once we exit the while loop, we have enough gas to drive
184+
# to the next station.
185+
# Subtract the gas we used.
186+
fuel -= location - position
187+
# Update our current position.
188+
position = location
189+
# Add the gas station to the heap in case we need it later.
190+
heappush(skipped, -gas)
191+
# Once we exit the loop, return the number of stops needed to
192+
# reach the target.
193+
return stops
194+
195+
196+
def test():
197+
executors = [
198+
# BruteForce,
199+
DP,
200+
Heap,
201+
]
202+
tests = [
203+
[1, 1, [], 0],
204+
[100, 1, [[10, 100]], -1],
205+
[100, 10, [[10, 60], [20, 30], [30, 30], [60, 40]], 2],
206+
[
207+
1000000,
208+
8663,
209+
[
210+
[31, 195796],
211+
[42904, 164171],
212+
[122849, 139112],
213+
[172890, 121724],
214+
[182747, 90912],
215+
[194124, 112994],
216+
[210182, 101272],
217+
[257242, 73097],
218+
[284733, 108631],
219+
[369026, 25791],
220+
[464270, 14596],
221+
[470557, 59420],
222+
[491647, 192483],
223+
[516972, 123213],
224+
[577532, 184184],
225+
[596589, 143624],
226+
[661564, 154130],
227+
[705234, 100816],
228+
[721453, 122405],
229+
[727874, 6021],
230+
[728786, 19444],
231+
[742866, 2995],
232+
[807420, 87414],
233+
[922999, 7675],
234+
[996060, 32691],
235+
],
236+
6,
237+
],
238+
]
239+
for executor in executors:
240+
start = timeit.default_timer()
241+
for _ in range(1):
242+
for n, t in enumerate(tests):
243+
sol = executor()
244+
result = sol.minRefuelStops(t[0], t[1], t[2])
245+
exp = t[3]
246+
assert result == exp, (
247+
f"\033[93m» {result} <> {exp}\033[91m for "
248+
+ f"test {n} using \033[1m{executor.__name__}"
249+
)
250+
stop = timeit.default_timer()
251+
used = str(round(stop - start, 5))
252+
cols = "{0:20}{1:10}{2:10}"
253+
res = cols.format(executor.__name__, used, "seconds")
254+
print(f"\033[92m» {res}\033[0m")
255+
256+
257+
test()

0 commit comments

Comments
 (0)