55. Jump Game

Given an array of non-negative integers, you are initially positioned at the first index of the array.
Each element in the array represents your maximum jump length at that position.
Determine if you are able to reach the last index.
Example 1:
Input: [2,3,1,1,4]
Output: true
Explanation: Jump 1 step from index 0 to 1, then 3 steps to the last index.
Example 2:
Input: [3,2,1,0,4]
Output: false
Explanation: You will always arrive at index 3 no matter what. Its maximum
             jump length is 0, which makes it impossible to reach the last index.




这种题可能知道答案了觉得特别简单, 但是 比 知道答案 更重要的是, 怎么 想到的这个答案
以及, 这一步步优化的过程, 这个掌握了, 对 做 其他的题 才有帮助

detailed  explanation from dfs backtracking to backtracking + memo  to dp 

https://leetcode.com/articles/jump-game/

https://www.youtube.com/watch?v=r3pZd9ghqxk



https://leetcode.com/problems/jump-game/solution/



Naming
* We call a position in the array a "good index" if starting at that position, we can reach the last index. Otherwise, that index is called a "bad index". The problem then reduces to whether or not index 0 is a "good index".
Solution
This is a dynamic programming[1] question. Usually, solving and fully understanding a dynamic programming problem is a 4 step process:
1. Start with the recursive backtracking solution
2. Optimize by using a memoization table (top-down[3] dynamic programming)
3. Remove the need for recursion (bottom-up dynamic programming)
4. Apply final tricks to reduce the time / memory complexity
All solutions presented below produce the correct result, but they differ in run time and memory requirements. 




Approach 1: Backtracking
This is the inefficient solution where we try every single jump pattern that takes us from the first position to the last. We start from the first position and jump to every index that is reachable. We repeat the process until last index is reached. When stuck, backtrack.


// dfs 
class Solution {
    public boolean canJump(int[] nums) {
      return dfs(0, nums);
    }
  
    private boolean dfs(int pos, int[] nums){
      // base case 
      if(pos == nums.length - 1){
        return true;
      }
      
      int furthestJump = Math.min(pos + nums[pos], nums.length - 1);
      for(int nextPos = pos + 1; nextPos <= furthestJump; nextPos++){
        if(dfs(nextPos, nums)) return true;
      }
      return false;
      
    }
}


// solution 2 : 

Approach 2: Dynamic Programming Top-down
Top-down Dynamic Programming can be thought of as optimized backtracking. It relies on the observation that once we determine that a certain index is good / bad, this result will never change. This means that we can store the result and not need to recompute it every time.
Therefore, for each position in the array, we remember whether the index is good or bad. Let's call this array memo and let its values be either one of: GOOD, BAD, UNKNOWN. This technique is called memoization[2].
An example of a memoization table for input array nums = [2, 4, 2, 1, 0, 2, 0] can be seen in the diagram below. We write G for a GOOD position and B for a BAD one. We can see that we cannot start from indices 2, 3 or 4 and eventually reach last index (6), but we can do that from indices 0, 1, 5 and (trivially) 6.
Index    0    1    2    3    4    5    6
nums    2    4    2    1    0    2    0
memo    G    G    B    B    B    G    G
Steps
1. Initially, all elements of the memo table are UNKNOWN, except for the last one, which is (trivially) GOOD (it can reach itself)
2. Modify the backtracking algorithm such that the recursive step first checks if the index is known (GOOD / BAD)
    1. If it is known then return True / False
    2. Otherwise perform the backtracking steps as before
3. Once we determine the value of the current index, we store it in the memo table





Enum , first time using it, remember learned this in OOD class
https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html



// dfs + memo : dp, top down 

enum Index {
    GOOD, BAD, UNKNOWN
}

public class Solution {
    Index[] memo;

    public boolean canJumpFromPosition(int position, int[] nums) {
        if (memo[position] != Index.UNKNOWN) {
            return memo[position] == Index.GOOD ? true : false;
        }

        int furthestJump = Math.min(position + nums[position], nums.length - 1);
        for (int nextPosition = position + 1; nextPosition <= furthestJump; nextPosition++) {
            if (canJumpFromPosition(nextPosition, nums)) {
                memo[position] = Index.GOOD;
                return true;
            }
        }

        memo[position] = Index.BAD;
        return false;
    }

    public boolean canJump(int[] nums) {
        memo = new Index[nums.length];
        for (int i = 0; i < memo.length; i++) {
            memo[i] = Index.UNKNOWN;
        }
        memo[memo.length - 1] = Index.GOOD;
        return canJumpFromPosition(0, nums);
    }
}


Complexity Analysis
* Time complexity : O(n^2) For every element in the array, say i, we are looking at the next nums[i]elements to its right aiming to find a GOOD index. nums[i] can be at most n, where n is the length of array nums.

* Space complexity : O(2n) = O(n)
 First n originates from recursion. Second n comes from the usage of the memo table. 




Approach 3: Dynamic Programming Bottom-up

Top-down to bottom-up conversion is done by eliminating recursion. In practice, this achieves better performance as we no longer have the method stack overhead and might even benefit from some caching. More importantly, this step opens up possibilities for future optimization. The recursion is usually eliminated by trying to reverse the order of the steps from the top-down approach.

The observation to make here is that we only ever jump to the right. This means that if we start from the right of the array, every time we will query a position to our right, that position has already be determined as being GOOD or BAD. This means we don't need to recurse anymore, as we will always hit the memo table.


enum Index {
    GOOD, BAD, UNKNOWN
}

public class Solution {
    public boolean canJump(int[] nums) {
        Index[] memo = new Index[nums.length];
        for (int i = 0; i < memo.length; i++) {
            memo[i] = Index.UNKNOWN;
        }
        memo[memo.length - 1] = Index.GOOD;

        for (int i = nums.length - 2; i >= 0; i--) {
            int furthestJump = Math.min(i + nums[i], nums.length - 1);
            for (int j = i + 1; j <= furthestJump; j++) {
                if (memo[j] == Index.GOOD) {
                    memo[i] = Index.GOOD;
                    break;
                }
            }
        }

        return memo[0] == Index.GOOD;
    }
}


* Time complexity : O(n^2)

O(n^ 2) For every element in the array, say i, we are looking at the next nums[i]elements to its right aiming to find a GOOD index. nums[i] can be at most n  where n  is the length of array nums.

* Space complexity : O(n)

O(n). This comes from the usage of the memo table.