|
| 1 | +# 55. Jump Game |
| 2 | +# 🟠 Medium |
| 3 | +# |
1 | 4 | # https://leetcode.com/problems/jump-game/
|
| 5 | +# |
| 6 | +# Tags: Array - Dynamic Programming - Greedy |
2 | 7 |
|
3 | 8 | import timeit
|
4 | 9 | from typing import List
|
5 | 10 |
|
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 |
| - |
14 | 11 |
|
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: |
16 | 21 | 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 |
27 | 29 |
|
| 30 | + return explore(0) |
28 | 31 |
|
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. |
33 | 43 | #
|
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% |
36 | 46 | class Memoization:
|
37 | 47 | def canJump(self, nums: List[int]) -> bool:
|
38 | 48 | memo = {}
|
39 | 49 |
|
40 | 50 | def explore(n: int):
|
41 | 51 | if n in memo:
|
42 | 52 | return memo[n]
|
43 |
| - if nums[n] + n >= len(nums)-1: |
| 53 | + if nums[n] + n >= len(nums) - 1: |
44 | 54 | return True
|
45 | 55 | for i in range(nums[n], 0, -1):
|
46 |
| - if explore(n+i): |
| 56 | + if explore(n + i): |
47 | 57 | return True
|
48 | 58 | memo[n] = False
|
49 | 59 | return False
|
50 |
| - return explore(0) |
51 | 60 |
|
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 |
67 | 61 | return explore(0)
|
68 | 62 |
|
69 | 63 |
|
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. |
72 | 69 | #
|
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: |
76 | 76 | 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 |
88 | 88 |
|
89 | 89 |
|
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: |
91 | 102 | 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 |
94 | 105 | 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 |
106 | 112 |
|
107 | 113 |
|
108 | 114 | 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, |
115 | 120 | ]
|
116 | 121 | tests = [
|
117 |
| - [[10, 3, 1, 1, 4], True], |
118 |
| - [[2, 3, 1, 1, 4], True], |
119 |
| - [[3, 2, 1, 0, 4], False], |
120 | 122 | [[0], True],
|
121 | 123 | [[1, 0], True],
|
122 | 124 | [[0, 1], False],
|
| 125 | + [[2, 3, 1, 1, 4], True], |
| 126 | + [[10, 3, 1, 1, 4], True], |
| 127 | + [[3, 2, 1, 0, 4], False], |
123 | 128 | ]
|
124 |
| - for e in executor: |
| 129 | + for executor in executors: |
125 | 130 | 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 | + ) |
132 | 140 | stop = timeit.default_timer()
|
133 | 141 | 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") |
135 | 145 |
|
136 | 146 |
|
137 | 147 | test()
|
0 commit comments