|
| 1 | +# Jump Game |
| 2 | + |
| 3 | +## The Problem |
| 4 | + |
| 5 | +Given an array of non-negative integers, you are initially positioned at |
| 6 | +the first index of the array. Each element in the array represents your maximum |
| 7 | +jump length at that position. |
| 8 | + |
| 9 | +Determine if you are able to reach the last index. |
| 10 | + |
| 11 | +**Example #1** |
| 12 | + |
| 13 | +``` |
| 14 | +Input: [2,3,1,1,4] |
| 15 | +Output: true |
| 16 | +Explanation: Jump 1 step from index 0 to 1, then 3 steps to the last index. |
| 17 | +``` |
| 18 | + |
| 19 | +**Example #2** |
| 20 | + |
| 21 | +``` |
| 22 | +Input: [3,2,1,0,4] |
| 23 | +Output: false |
| 24 | +Explanation: You will always arrive at index 3 no matter what. Its maximum |
| 25 | + jump length is 0, which makes it impossible to reach the last index. |
| 26 | +``` |
| 27 | + |
| 28 | +## Naming |
| 29 | + |
| 30 | +We call a position in the array a **"good index"** if starting at that position, |
| 31 | +we can reach the last index. Otherwise, that index is called a **"bad index"**. |
| 32 | +The problem then reduces to whether or not index 0 is a "good index". |
| 33 | + |
| 34 | +## Solutions |
| 35 | + |
| 36 | +### Approach 1: Backtracking |
| 37 | + |
| 38 | +This is the inefficient solution where we try every single jump pattern that |
| 39 | +takes us from the first position to the last. We start from the first position |
| 40 | +and jump to every index that is reachable. We repeat the process until last |
| 41 | +index is reached. When stuck, backtrack. |
| 42 | + |
| 43 | +> See [backtrackingJumpGame.js](backtrackingJumpGame.js) file |
| 44 | +
|
| 45 | +**Time complexity:**: `O(2^n)`. |
| 46 | +There are 2<sup>n</sup> (upper bound) ways of jumping from |
| 47 | +the first position to the last, where `n` is the length of |
| 48 | +array `nums`. |
| 49 | + |
| 50 | +**Auxiliary Space Complexity**: `O(n)`. |
| 51 | +Recursion requires additional memory for the stack frames. |
| 52 | + |
| 53 | +### Approach 2: Dynamic Programming Top-down |
| 54 | + |
| 55 | +Top-down Dynamic Programming can be thought of as optimized |
| 56 | +backtracking. It relies on the observation that once we determine |
| 57 | +that a certain index is good / bad, this result will never change. |
| 58 | +This means that we can store the result and not need to recompute |
| 59 | +it every time. |
| 60 | + |
| 61 | +Therefore, for each position in the array, we remember whether the |
| 62 | +index is good or bad. Let's call this array memo and let its values |
| 63 | +be either one of: GOOD, BAD, UNKNOWN. This technique is |
| 64 | +called memoization. |
| 65 | + |
| 66 | +> See [dpTopDownJumpGame.js](dpTopDownJumpGame.js) file |
| 67 | +
|
| 68 | +**Time complexity:**: `O(n^2)`. |
| 69 | +For every element in the array, say `i`, we are looking at the |
| 70 | +next `nums[i]` elements to its right aiming to find a GOOD |
| 71 | +index. `nums[i]` can be at most `n`, where `n` is the length |
| 72 | +of array `nums`. |
| 73 | + |
| 74 | +**Auxiliary Space Complexity**: `O(2 * n) = O(n)`. |
| 75 | +First `n` originates from recursion. Second `n` comes from the |
| 76 | +usage of the memo table. |
| 77 | + |
| 78 | +### Approach 3: Dynamic Programming Bottom-up |
| 79 | + |
| 80 | +Top-down to bottom-up conversion is done by eliminating recursion. |
| 81 | +In practice, this achieves better performance as we no longer have the |
| 82 | +method stack overhead and might even benefit from some caching. More |
| 83 | +importantly, this step opens up possibilities for future optimization. |
| 84 | +The recursion is usually eliminated by trying to reverse the order of |
| 85 | +the steps from the top-down approach. |
| 86 | + |
| 87 | +The observation to make here is that we only ever jump to the right. |
| 88 | +This means that if we start from the right of the array, every time |
| 89 | +we will query a position to our right, that position has already be |
| 90 | +determined as being GOOD or BAD. This means we don't need to recurse |
| 91 | +anymore, as we will always hit the memo table. |
| 92 | + |
| 93 | +> See [dpBottomUpJumpGame.js](dpBottomUpJumpGame.js) file |
| 94 | +
|
| 95 | +**Time complexity:**: `O(n^2)`. |
| 96 | +For every element in the array, say `i`, we are looking at the |
| 97 | +next `nums[i]` elements to its right aiming to find a GOOD |
| 98 | +index. `nums[i]` can be at most `n`, where `n` is the length |
| 99 | +of array `nums`. |
| 100 | + |
| 101 | +**Auxiliary Space Complexity**: `O(n)`. |
| 102 | +This comes from the usage of the memo table. |
| 103 | + |
| 104 | +### Approach 4: Greedy |
| 105 | + |
| 106 | +Once we have our code in the bottom-up state, we can make one final, |
| 107 | +important observation. From a given position, when we try to see if |
| 108 | +we can jump to a GOOD position, we only ever use one - the first one. |
| 109 | +In other words, the left-most one. If we keep track of this left-most |
| 110 | +GOOD position as a separate variable, we can avoid searching for it in |
| 111 | +the array. Not only that, but we can stop using the array altogether. |
| 112 | + |
| 113 | +> See [greedyJumpGame.js](greedyJumpGame.js) file |
| 114 | +
|
| 115 | +**Time complexity:**: `O(n)`. |
| 116 | +We are doing a single pass through the `nums` array, hence `n` steps, |
| 117 | +where `n` is the length of array `nums`. |
| 118 | + |
| 119 | +**Auxiliary Space Complexity**: `O(1)`. |
| 120 | +We are not using any extra memory. |
| 121 | + |
| 122 | +## References |
| 123 | + |
| 124 | +- [Jump Game Fully Explained on LeetCode](https://leetcode.com/articles/jump-game/) |
| 125 | +- [Dynamic Programming vs Divide and Conquer](https://itnext.io/dynamic-programming-vs-divide-and-conquer-2fea680becbe) |
| 126 | +- [Dynamic Programming](https://en.wikipedia.org/wiki/Dynamic_programming) |
| 127 | +- [Memoization on Wikipedia](https://en.wikipedia.org/wiki/Memoization) |
| 128 | +- [Top-Down and Bottom-Up Design on Wikipedia](https://en.wikipedia.org/wiki/Top-down_and_bottom-up_design) |
0 commit comments