算法----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.

给定一个数组,每个元素数值代表能够移动的最大步数,从0位置出发,是否能够移动到数组最后位置。


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".

如果某一个位置能够跳到最后位置,则称为好位置,否则坏位置。

This is a dynamic programming[1] question. Usually, solving and fully understanding a dynamic programming problem is a 4 step process:

Start with the recursive backtracking solution
Optimize by using a memoization table (top-down[2] dynamic programming)
Remove the need for recursion (bottom-up dynamic programming)
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.

动态规划问题:

第一给出递归回溯解法;

第二利用内存空间自上到下动态规划来优化算法;

第三使用自下到上动态规划消除递归;

第四使用技巧来优化时间和空间。

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.

从第一个位置开始搜索所有可能,如果能够到达最后位置则返回,返回则回溯,返回上一步,继续进行搜索,有点类似于深度优先搜索。

对于某一个位置position,其可以探索的所有可能位置由position以及nums[position]和数组长度决定,遍历从position开始的每一个位置。

public class Solution {
    public boolean canJumpFromPosition(int position, int[] nums) {
        if (position == nums.length - 1) {
            return true;
        }

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

        return false;
    }

    public boolean canJump(int[] nums) {
        return canJumpFromPosition(0, nums);
    }
}

One quick optimization we can do for the code above is to check the nextPositionfrom right to left. The theoretical worst case performance is the same, but in practice, for silly examples, the code might run faster. Intuitively, this means we always try to make the biggest jump such that we reach the end as soon as possible

可以优化的地方,上述过程从position开始向最远位置探索,极端情况下可能需探索所有不必要情况,才在最远位置找到可行解。为了能够尽快到达最后位置,因此可以从position最远位置向最近位置开始探索。

public class Solution {
    public boolean canJumpFromPosition(int position, int[] nums) {
        if (position == nums.length - 1) {
            return true;
        }

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

        return false;
    }

    public boolean canJump(int[] nums) {
        return canJumpFromPosition(0, nums);
    }
}

For instance, in the example below, if we start from index 0, jump as far as possible and reach 1, jump as far as possible and reach 6. By doing so, we determine that 0 is a GOOD index in 3 steps.

Index0123456
nums1521020

To illustrate the worst case, where this optimization has no effect, take the example below. Index 6 cannot be reached from any position, but all combinations will be tried.

Index0123456
nums5432100

The first few steps of the backtracking algorithm for the example above are: 0 -> 4 -> 5 -> 4 -> 0 -> 3 -> 5 -> 3 -> 4 -> 5 -> etc.

Complexity Analysis

  • Time complexity : O(2^n). There are 2^n(upper bound) ways of jumping from the first position to the last, where nn is the length of array nums. For a complete proof, please refer to Appendix A.

  • Space complexity : O(n). Recursion requires additional memory for the stack frames. 


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[3].

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 BADone. 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.

Index0123456
nums2421020
memoGGBBBGG

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 memotable

从初始位置出发,如果能够到达某一个位置是好位置,则可以停止搜索,说明已经找到可行解;如果某个位置为坏位置,则直接回溯即可

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 GOODindex. nums[i] can be at most nn, where nn 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. 


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.

自下到上动态规划可以不使用递归方法,可以考虑从最后出发看是否能够到达初始位置。

初始化 所有位置状态未知,最后一个位置为好位置

从最后一个位置前一个位置i出发,假定从i出发可以到达最后位置则说明i为好位置;i为好位置的判断条件是从i+1出发开始往后搜索能够搜索到好位置,则说明i位置也是好位置;这样探索最后位置前的每一个位置i--,看最终初始位置0是否是好位置。

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;
    }
}

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 GOODindex. nums[i] can be at most nn, where nn is the length of array nums.

  • Space complexity : O(n). This comes from the usage of the memo table. 


4.贪婪算法  Greedy

Once we have our code in the bottom-up state, we can make one final, important observation. From a given position, when we try to see if we can jump to a GOODposition, we only ever use one - the first one (see the break statement). In other words, the left-most one. If we keep track of this left-most GOOD position as a separate variable, we can avoid searching for it in the array. Not only that, but we can stop using the array altogether.

在自下到上动态规划算法中,从最后位置前一个位置i出发,探索每一种可能i--,对于每一种i可能都需要从i+1出发是否能够到达好位置。这里可以用一个变量来记录左边最大好位置坐标,从i开始出发,如果能够到达lastPos位置,说明i也是好位置,则将lastPos=i,继续进行判断。

Iterating right-to-left, for each position we check if there is a potential jump that reaches a GOOD index (currPosition + nums[currPosition] >= leftmostGoodIndex). If we can reach a GOOD index, then our position is itself GOOD. Also, this new GOOD position will be the new leftmost GOOD index. Iteration continues until the beginning of the array. If first position is a GOOD index then we can reach the last index from the first position.

To illustrate this scenario, we will use the diagram below, for input array nums = [9, 4, 2, 1, 0, 2, 0]. We write G for GOODB for BAD and U for UNKNOWN. Let's assume we have iterated all the way to position 0 and we need to decide if index 0 is GOOD. Since index 1 was determined to be GOOD, it is enough to jump there and then be sure we can eventually reach index 6. It does not matter that nums[0] is big enough to jump all the way to the last index. All we need is one way.

Index0123456
nums9421020
memoUGBBBGG
public class Solution {
    public boolean canJump(int[] nums) {
        int lastPos = nums.length - 1;
        for (int i = nums.length - 1; i >= 0; i--) {
            if (i + nums[i] >= lastPos) {
                lastPos = i;
            }
        }
        return lastPos == 0;
    }
}

Complexity Analysis

  • Time complexity : O(n). We are doing a single pass through the nums array, hence nn steps, where nn is the length of array nums.

  • Space complexity : O(1). We are not using any extra memory.

参考地址:https://leetcode.com/problems/jump-game/solution/

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值