-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathjump-game-ii.py
203 lines (188 loc) · 7.77 KB
/
jump-game-ii.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# 45. Jump Game II
# 🟠 Medium
#
# https://leetcode.com/problems/jump-game-ii/
#
# Tags: Array - Dynamic Programming - Greedy
import timeit
from typing import List
# The brute force solution would explore all possibilities, from each
# position, take all possible jumps, once we get to the end, compare
# that result to the best found so far.
#
# Time complexity: O(2^n) - For each option we explore all possibilities
# it amounts to choosing to take, or not, each element of the array.
# Space complexity: O(n) - The call stack can grow to the depth of the
# input array.
#
# This solution would fail with Time Limit Exceeded.
class BruteForceDFS:
def jump(self, nums: List[int]) -> int:
# Store the current shortest number of jumps found.
self.best = float("inf")
# Define a function that explores jumps from a given position.
# It receives the index of the position that is exploring and
# returns the minimum number of jumps to get from that position
# to the end of the array.
def dfs(idx: int) -> int:
# Base case, we are at the end of the array.
if idx == len(nums) - 1:
return 0
# Base case, if the element at the current index is 0, we
# will not be able to reach the end from here.
if nums[idx] == 0:
return float("inf")
# Explore the result of all options if the jump would keep
# us within the bounds of the array.
jumps = [
dfs(idx + j)
for j in range(1, nums[idx] + 1)
if idx + j < len(nums)
]
# We keep the best option out of all of them and add the
# jump needed to get there to the result.
return min(jumps) + 1
# Initial call.
return dfs(0)
# We can improve the previous solution if we avoid recalculating the
# best result from an index once we have calculated it, we could use
# functools.cache for that, which would be more efficient, or manually
# keep results in a dictionary.
#
# Time complexity: O(n^2) - We calculate all possibilities from each
# index once.
# Space complexity: O(n) - We store an array of length n.
#
# This solution would fail with Time Limit Exceeded because there is a
# better one.
class MemoizedDFS:
def jump(self, nums: List[int]) -> int:
# Store results already calculated in an array, it could also be
# a dictionary, but we can use the fact that the key would be
# the index of an array of known length.
memo = [None] * len(nums)
memo[-1] = 0
# Define a function that explores jumps from a given position.
# It receives the index of the position that is exploring and
# returns the minimum number of jumps to get from that position
# to the end of the array.
# @cache would be more efficient than the dictionary.
def dfs(idx: int) -> int:
# Use the memoized result if existing.
if memo[idx] is not None:
return memo[idx]
# Base case, we are at the end of the array.
if idx == len(nums) - 1:
return 0
# Base case, if the element at the current index is 0, we
# will not be able to reach the end from here.
if nums[idx] == 0:
memo[idx] = float("inf")
return memo[idx]
# Explore the result of all options if the jump would keep
# us within the bounds of the array.
memo[idx] = (
min(
[
dfs(idx + j)
for j in range(1, nums[idx] + 1)
if idx + j < len(nums)
]
)
+ 1
)
# We keep the best option out of all of them and add the
# jump needed to get there to the result.
return memo[idx]
# Initial call.
return dfs(0)
# The bottom-up DP version of the previous solution starts at the first
# index and "jumps" to all the possible positions checking if the result
# of jumping there from the current position is better than the best
# option explored so far.
#
# Time complexity: O(n^2) - This solution is not more effective than the
# memoized solution, we still explore every possible jump from the
# position that we are visiting.
# Space complexity: O(n) - The size of the DP array.
#
# This solution would probably fail with Time Limit Exceeded.
class DP:
def jump(self, nums: List[int]) -> int:
dp = [0] + [float("inf")] * (len(nums) - 1)
# Iterate over the input taking all possible "jumps".
for i in range(len(nums)):
# Calculate the result of all possible jumps.
for j in range(1, nums[i] + 1):
if i + j < len(nums):
# If taking this jump would reduce the number of jumps
# to get to i+j, that is the best jump seen so far.
dp[i + j] = min(dp[i + j], dp[i] + 1)
# Return the best jump to get to the end of the array.
return dp[-1]
# Visit each element of the array but do it in groups, we can see it as
# treating each group as a level and the algorithm as BFS. For each
# level, keep track of the farthest position we could jump to from this
# level. When we get to the end of the level, add one to the number of
# jumps that we have taken, and update the current level by updating the
# last element we can explore to match the farthest element we can
# reach from this level.
# The algorithm repeatedly calculates the farthest point we can reach
# from any of the positions that we can reach given the current number
# of jumps, then "jump" once more and continue calculating. Each element
# is only explored once.
#
# Time complexity: O(n) - Each element is visited once.
# Space complexity: O(1) - Constant space.
#
# Runtime: 155 ms, faster than 84.71%
# Memory Usage: 15.1 MB, less than 59.64%
class Greedy:
def jump(self, nums: List[int]) -> int:
# Store the number of jumps we have taken, the farthest we can
# jump right now and the last index of the window we are
# exploring in this jump.
jumps = current_end = current_farthest = 0
# Iterate over the input except the last element.
for i in range(len(nums) - 1):
# The farthest we can jump is the max of the current
# farthest we have seen and the current jump.
current_farthest = max(current_farthest, i + nums[i])
# If we are at the current end, we need to jump.
if i == current_end:
# We need to jump to keep exploring.
jumps += 1
# Greedily take (up to) the best jump we can make.
current_end = current_farthest
# Return minimum number of jumps to get to the end.
return jumps
def test():
executors = [
BruteForceDFS,
MemoizedDFS,
DP,
Greedy,
]
tests = [
[[0], 0],
[[2, 3, 1, 1, 4], 2],
[[2, 3, 0, 1, 4], 2],
[[2, 3, 0, 1, 4, 0, 0, 0, 2, 8, 7, 3], 5],
]
for executor in executors:
start = timeit.default_timer()
for _ in range(1):
for n, t in enumerate(tests):
sol = executor()
result = sol.jump(t[0])
exp = t[1]
assert result == exp, (
f"\033[93m» {result} <> {exp}\033[91m for "
+ f"test {n} using \033[1m{executor.__name__}"
)
stop = timeit.default_timer()
used = str(round(stop - start, 5))
cols = "{0:20}{1:10}{2:10}"
res = cols.format(executor.__name__, used, "seconds")
print(f"\033[92m» {res}\033[0m")
test()