Skip to content

Commit 3b9a58e

Browse files
committed
LeetCode 55. Jump Game revamp
1 parent e7149e4 commit 3b9a58e

File tree

1 file changed

+100
-90
lines changed

1 file changed

+100
-90
lines changed

leetcode/jump-game.py

+100-90
Original file line numberDiff line numberDiff line change
@@ -1,137 +1,147 @@
1+
# 55. Jump Game
2+
# 🟠 Medium
3+
#
14
# https://leetcode.com/problems/jump-game/
5+
#
6+
# Tags: Array - Dynamic Programming - Greedy
27

38
import timeit
49
from typing import List
510

6-
# Intuition: Mark the end of the array as our point to reach (goal) and start checking the
7-
# positions before it.
8-
# For each position, if we can reach the current goal from there i + nums[i] >= goal
9-
# mark that position as the new goal and check if we can reach it from any of the previous positions.
10-
#
11-
# Runtime: 477 ms, faster than 97.37% of Python3 online submissions for Jump Game.
12-
# Memory Usage: 15.4 MB, less than 18.06 % of Python3 online submissions for Jump Game.
13-
1411

15-
class Linear:
12+
# Start at n=0 and explore the furthest position you can reach.
13+
# Recursively explore the furthest position you can reach from there.
14+
# If at any point you can reach the end of the array or further, return
15+
# true, once all the possibilities have been explored, return false.
16+
#
17+
# Time complexity: O(n^2) - If the array contains small values and they
18+
# fail towards the end, we will explore all combinations of values.
19+
# Space complexity: O(n^2) - The call stack.
20+
class BruteForce:
1621
def canJump(self, nums: List[int]) -> bool:
17-
# Initial goal, reaching the last position of the array.
18-
goal = len(nums) - 1
19-
# Iterate over all the positions except the last one
20-
for i in range(len(nums) - 2, -1, -1):
21-
# If from the current position we can reach the current goal
22-
if i + nums[i] >= goal:
23-
# Reaching this position becomes our current goal
24-
goal = i
25-
# If our last goal is to reach the start position, we can jump to the end
26-
return goal == 0
22+
def explore(n: int):
23+
if nums[n] + n >= len(nums) - 1:
24+
return True
25+
for i in range(nums[n], 0, -1):
26+
if explore(n + i):
27+
return True
28+
return False
2729

30+
return explore(0)
2831

29-
# Similar to the brute force algorithm but memorize positions that we have explored but do not lead
30-
# to a solution.
31-
# There is no need to memoize positions that return True because the True value propagates and
32-
# terminates execution, returning True, as soon as the first one is found.
32+
33+
# Similar to the brute force algorithm but memorize positions that we
34+
# have explored but do not lead to a solution. There is no need to
35+
# memoize positions that return True because the True value propagates
36+
# and terminates execution, returning True, as soon as the first one is
37+
# found.
38+
#
39+
# Time complexity: O(n^2) - For each position, we may end up visiting
40+
# each position after itself.
41+
# Space complexity: O(n) - The dictionary could hold an entry for each
42+
# element in nums.
3343
#
34-
# Runtime: 8078 ms, faster than 5.00% of Python3 online submissions for Jump Game.
35-
# Memory Usage: 27.8 MB, less than 5.11 % of Python3 online submissions for Jump Game.
44+
# Runtime: 8078 ms, faster than 5.00%
45+
# Memory Usage: 27.8 MB, less than 5.11%
3646
class Memoization:
3747
def canJump(self, nums: List[int]) -> bool:
3848
memo = {}
3949

4050
def explore(n: int):
4151
if n in memo:
4252
return memo[n]
43-
if nums[n] + n >= len(nums)-1:
53+
if nums[n] + n >= len(nums) - 1:
4454
return True
4555
for i in range(nums[n], 0, -1):
46-
if explore(n+i):
56+
if explore(n + i):
4757
return True
4858
memo[n] = False
4959
return False
50-
return explore(0)
5160

52-
53-
# Start at n=0 and explore the furthest position you can reach.
54-
# Recursively explore the furthest position you can reach from there.
55-
# If at any point you can reach the end of the array or further, return True
56-
# Once all the possibilities have been explored, return False
57-
# Worst case would be O(n^2) if all n values are small and they fail towards the end.
58-
class BruteForce:
59-
def canJump(self, nums: List[int]) -> bool:
60-
def explore(n: int):
61-
if nums[n] + n >= len(nums)-1:
62-
return True
63-
for i in range(nums[n], 0, -1):
64-
if explore(n+i):
65-
return True
66-
return False
6761
return explore(0)
6862

6963

70-
# Reasoning; for each element i, including i = len(nums)-1, we can jump there if there is any element
71-
# at position j with val = i-j
64+
# Intuition: Mark the end of the array as our point to reach (goal) and
65+
# start checking the positions before it. For each position, if we can
66+
# reach the current goal from there i + nums[i] >= goal mark that
67+
# position as the new goal and check if we can reach it from any of the
68+
# previous positions.
7269
#
73-
# The worst case scenario for this solution is when num[i] for larger is are large values.
74-
# In LeetCode it fails with Time Limit Exceeded.
75-
class BackwardTabulation:
70+
# Time complexity: O(n) - We visit each position once.
71+
# Space complexity: O(1) - Constant extra memory used.
72+
#
73+
# Runtime: 477 ms, faster than 97.37%
74+
# Memory Usage: 15.2 MB, less than 82.53%
75+
class Linear:
7676
def canJump(self, nums: List[int]) -> bool:
77-
if len(nums) == 1:
78-
return True
79-
for i in range(len(nums)-2, -1, -1):
80-
# If we can reach the last element of the current array from this position
81-
if i+nums[i] >= len(nums)-1:
82-
if self.canJump(nums[:i+1]):
83-
return True
84-
return False
85-
86-
# The worst case scenario would be having large values for num[i]
87-
# In that case the solution does not pass on LeetCode, instead it fails with Time Limit Exceeded.
77+
# Initial goal, reaching the last position of the array.
78+
goal = len(nums) - 1
79+
# Iterate over all the positions except the last one.
80+
for i in range(len(nums) - 2, -1, -1):
81+
# If from the current position we can reach the current goal.
82+
if i + nums[i] >= goal:
83+
# Reaching this position becomes our current goal.
84+
goal = i
85+
# If our last goal is to reach the start position, we can jump
86+
# to the end.
87+
return goal == 0
8888

8989

90-
class Tabulation:
90+
# Front to back, store the index of the furthest position we can reach
91+
# at any point, iterate over the input array positions, check if the
92+
# current position could be reached, if it could not, return False, if
93+
# it could, compare the best reach up to that point with the new reach
94+
# we have from the current position and update it if better.
95+
#
96+
# Time complexity: O(n) - We visit each position once.
97+
# Space complexity: O(1) - Constant extra memory used.
98+
#
99+
# Runtime: 477 ms Beats 95.93%
100+
# Memory: 15.1 MB Beats 97.51%
101+
class Greedy:
91102
def canJump(self, nums: List[int]) -> bool:
92-
can_reach = [False for _ in range(len(nums))]
93-
can_reach[0] = True
103+
# Store the maximum position we can reach from any index.
104+
reach, goal = 0, len(nums) - 1
94105
for i in range(len(nums)):
95-
# If we can reach this position
96-
if can_reach[i]:
97-
for j in range(nums[i]):
98-
landing = i + j + 1
99-
if landing < len(can_reach) - 1:
100-
# Mark all the positions we can jump to ahead of this one as reachable
101-
can_reach[landing] = True
102-
elif landing == len(can_reach) - 1:
103-
# Quick return if we are marking the last element as True
104-
return True
105-
return can_reach[len(nums) - 1]
106+
# If we could not reach this index.
107+
if reach < i:
108+
return False
109+
# If we were able to reach this index.
110+
if (reach := max(reach, i + nums[i])) >= goal:
111+
return True
106112

107113

108114
def test():
109-
executor = [
110-
{'executor': Linear, 'title': 'Linear', },
111-
{'executor': Memoization, 'title': 'Memoization', },
112-
{'executor': BruteForce, 'title': 'BruteForce', },
113-
{'executor': BackwardTabulation, 'title': 'BackwardTabulation', },
114-
{'executor': Tabulation, 'title': 'Tabulation', },
115+
executors = [
116+
BruteForce,
117+
Memoization,
118+
Linear,
119+
Greedy,
115120
]
116121
tests = [
117-
[[10, 3, 1, 1, 4], True],
118-
[[2, 3, 1, 1, 4], True],
119-
[[3, 2, 1, 0, 4], False],
120122
[[0], True],
121123
[[1, 0], True],
122124
[[0, 1], False],
125+
[[2, 3, 1, 1, 4], True],
126+
[[10, 3, 1, 1, 4], True],
127+
[[3, 2, 1, 0, 4], False],
123128
]
124-
for e in executor:
129+
for executor in executors:
125130
start = timeit.default_timer()
126-
for _ in range(int(float('1e5'))):
127-
for t in tests:
128-
sol = e['executor']()
129-
result = sol.canJump([*t[0]])
130-
expected = t[1]
131-
assert result == expected, f'{result} != {expected}'
131+
for _ in range(1):
132+
for col, t in enumerate(tests):
133+
sol = executor()
134+
result = sol.canJump(t[0])
135+
exp = t[1]
136+
assert result == exp, (
137+
f"\033[93m» {result} <> {exp}\033[91m for"
138+
+ f" test {col} using \033[1m{executor.__name__}"
139+
)
132140
stop = timeit.default_timer()
133141
used = str(round(stop - start, 5))
134-
print("{0:20}{1:10}{2:10}".format(e['title'], used, "seconds"))
142+
cols = "{0:20}{1:10}{2:10}"
143+
res = cols.format(executor.__name__, used, "seconds")
144+
print(f"\033[92m» {res}\033[0m")
135145

136146

137147
test()

0 commit comments

Comments
 (0)