|
| 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