most optimal :

"""
main idea: Greedy - start from last postion, record the first position that can
jump to last position, then update last position to current idex.  Check if last position can go back to idx = 0.  
start from last position, moving towards back. each time check at current position if one can jump to the last position, update the last position. Condition to determine jump will success: can last position move back to start of array.
"""
class Solution:
    def canJump(self, nums: List[int]) -> bool:
        # time O(N) space O(1)
        last_pos = len(nums)-1
        for i in range(len(nums)-1,-1,-1):
            if i + nums[i] >= last_pos:
                last_pos = i
        return last_pos == 0
# within valid range of right most position,  
#track the right most range current position can go
# when ever the right most reaches last position, means we can reach to the end.
# if by the time the loop ends, we can not reach the last point, return False 
class Solution:
    def canJump(self, A: List[int]) -> bool:
        n = len(A)
        right_most = 0
        for i in range(n):
            if i <= right_most:
                right_most = max(right_most, i+A[i])
                if right_most >= n-1:
                    return True 
        return False 

DP TLE

# time O(N^2) space O(N)
class Solution:
    def canJump(self, nums: List[int]) -> bool:
        n = len(nums)
        dp = [False] * n
        dp[0] = True 
        for i in range(1,n):
            for j in range(i):
                if dp[j] == True and j + nums[j] >= i:
                    dp[i] = True 
                    break
        return dp[n-1]

Backtracking TLE time O(2^N) space O(N)


class Solution:
    def canJump(self, nums: List[int]) -> bool:
        # TLE time O(N) space O(N)  
        return self.btr(0,nums)
    
    def btr(self,start,arr):
        if start == len(arr) -1:
            return True
        farest = min(start+arr[start],len(arr)-1)
        for i in range(start+1,farest+1):
            if self.btr(i,arr): return True
        return False
######### slitly optimize :instead of from start to end, from end to start   (!! here only works with end to start )
class Solution:
    def canJump(self, nums: List[int]) -> bool:
        # TLE time O(N*N) space O(N)  
        return self.btr(0,nums)
    
    def btr(self,start,arr):
        if start == len(arr) -1:
            return True
        farest = min(start+arr[start],len(arr)-1)
        for i in range(farest,start,-1):
            if self.btr(i,arr): return True
        return False

DP- top down TLE

class Solution:
    def canJump(self, nums: List[int]) -> bool:
        # TLE time O(N*N) space O(N)
        self.memo = [0]* len(nums)
        self.memo[-1] = 1
        return self.dp(0,nums)
    
    
    def dp(self,start,arr):
        if self.memo[start] != 0:
            return self.memo[start] == 1
        farest = min(start+arr[start],len(arr)-1)
        for i in range(start+1,farest+1):
            if self.dp(i,arr): 
                self.memo[start] = 1
                return True
        self.memo[start] = -1
        return False

bottom up TLE with python

class Solution:
    def canJump(self, nums: List[int]) -> bool:
        # TLE time O(N*N) space O(N)
        memo = [0]* len(nums)
        memo[-1] = 1
        for i in range(len(nums)-2,-1,-1):
            farest = min(nums[i]+i,len(nums)-1)
            for j in range(i+1,farest+1):
                if memo[j] == 1:
                    memo[i] = 1
                    break
        return memo[0] == 1