LeetCode - 55. Jump Game、45. Jump Game II、403. Frog Jump - 一维数组跳跃问题 (多种方法)

55 - Jump Game  -  Medium

45 - Jump Game II  -  Hard

403 - Frog Jump - Hard

上边三题都是一维数组中的DP算法题,一起总结一下:

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.

Input: [2,3,1,1,4]
Output: true
Explanation: Jump 1 step from index 0 to 1, then 3 steps to the last index.

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.

解:

    这道题第一反应就是动态规划,不过还是想用dfs先试一下哈哈。Time complexity : O(2^{n}) (因为从0跳到最后一位,共有2^{n}种方式), Space complexity : O(n)。

bool dfs(const vector<int>& nums, int cur)
{
    if(cur >= nums.size() - 1)
        return true;
    for(int i = nums[cur]; i >= 1; i--)    // 时间复杂度没有变,但理论上比 i++ 快
        if(dfs(nums, cur + i))
            return true;
    return false;
}

bool canJump(vector<int>& nums)
{
    return dfs(nums, 0);
}

    过了74个测试用例,最后一个实在太多了没有过(虽然已经加速了,按最大的步长向前走,只要超过了nums的长度,就return true)。

    想了想其实没有必要dfs遍历所有情况,每次进入下一个dfs的时候,只需要for循环找到在cur 这个位置以及后边 nums[cur] 这些个位置中 nums[cur + i] + i 最大的值 的索引 i,进入这个分支继续递归即可,果然AC。当然子这个过程中,每次都是先检查是不是已经能够达到最后的位置了。这样的算法可能不叫深度优先遍历了,因为没有挨个遍历,而是每次找到下次遍历的位置向下遍历,不过思想是从dfs来的,所以就没改名字。

bool dfs(const vector<int>& nums, int cur)
{
    if (cur + nums[cur] >= nums.size() - 1)
        return true;
    int imax = -1, tmpmax = -1;
    for (int i = 1; i <= nums[cur]; i++)
    {
        if (nums[cur + i] + i > tmpmax)
        {
            tmpmax = nums[cur + i] + i;
            imax = i;
        }
    }
    if (nums[cur + imax] == 0)
        return false;
    return dfs(nums, cur + imax);
}

bool canJump(vector<int>& nums)
{
    if(nums.size() == 1)
        return true;
    return dfs(nums, 0);
}

    *****

    其实上面的递归还可以加速,就比如 5 6 1 1 1 1 3 ..... 这种情况,从 5 的位置遍历(6 1 1 1 1),发现跳到 6 之后能跳的最远,所以跳到了 6 位置,然后从 6 开始遍历 (1 1 1 1 3),发现跳到 3 最合适,但是在这个遍历过程中,其实前边的 1 1 1 1是没有必要遍历的,因为在遍历 5 这个位置的时候就已经知道 1 1 1 1 这三个位置肯定不如 6 了,在遍历 6 的时候还遍历 1 1 1 1 是没有意义的,因为如果从 6 跳过去,那为什么不直接从 5 跳过去呢?不过这样加速需要记录上一次的位置,加速效果可能也不明显,所以就没写,只是总结一下。

    *****

    按照dfs的思想进行优化,就是按最大步长走,每次刷新能走到最远的地方,只要这个能走到的最远的超过了nums.size() 就对了,如果刷新到最后还是没有到,就false。按照这个思路,很容易写出不需要递归的直接从左往右挨个遍历代码如下,且 beat 98.81%

bool canJump(vector<int>& nums)
{
    int maximum = nums[0];
    for(int i = 0; i <= maximum; i++)
    {
        if(nums[i] + i > maximum)
            maximum = nums[i] + i;
        if(maximum >= nums.size() - 1)
            return true;
    }
    return false;
}

    我的方法是从左往右,每次刷新能够走到的最远位置,只要超过nums的长度就return true。LeetCode Solution里边也提供了另一种,从右往左的,每次刷新能够达到最后一个位置的最小的位置,最后判断这个最小的位置是不是0,也就是从0能不能达到最后一个位置。

bool canJump(vector<int>& nums)
{
    int len = nums.size(), lastPos = len - 1;
    for(int i = len - 1; i >= 0; i--)
    {
        if(nums[i] + i >= lastPos)
            lastPos = i;
    }
    return lastPos == 0;
}

 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

    递归的 backtracking solution 其实就是上边的dfs,然后用 Top-down Dynamic Programming可以认为是对 backtracking solution 进行了优化。Top-down to bottom-up conversion is done by eliminating recursion。

bool canJump(vector<int>& nums)
{
    enum state{UNKNOWN, GOOD, BAD};
    int size = nums.size();
    int next_good = size - 1;
    vector<state> dp (size, UNKNOWN);
    dp[size-1] = GOOD;
    for (int i = size-2; i >= 0; i--){
        if (i + nums[i] >= next_good){
            dp[i] = GOOD;
            next_good = i;
        }
        else dp[i] = BAD;
    }
    return dp[0] == GOOD;
}

45 - Jump Game II

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.

Your goal is to reach the last index in the minimum number of jumps.

Input: [2,3,1,1,4]
Output: 2
Explanation: The minimum number of jumps to reach the last index is 2.
             Jump 1 step from index 0 to 1, then 3 steps to the last index(数字 2 -> 3 -> 4).

Note:    You can assume that you can always reach the last index.

解:

    上一题是问能否走到最后一个位置,这道题是,确保肯定能走到最后一个位置,问怎么走步数最少。有了第一题的经验,这道 HARD 难度的题就不是很难了。我的第一反应还是递归。AC代码如下:

void dfs(const vector<int>& nums, int cur, int cur_steps, int& min_steps)
{
    if(cur + nums[cur] >= nums.size() - 1)
    {
        min_steps = min(min_steps, cur_steps + 1);
        return;
    }
    int tmpmax = -1, imax = -1;
    for(int i = 1; i <= nums[cur]; i++)
    {
        if(nums[cur + i] != 0 && nums[cur + i] + i > tmpmax)   // 后边这些位置里哪个能走最远
        {
            tmpmax = nums[cur + i] + i;
            imax = i;
        }
    }
    dfs(nums, cur + imax, cur_steps + 1, min_steps);
}

int jump(vector<int>& nums)
{
    if(nums.size() == 1)
        return 0;
    int min_steps = INT_MAX;
    dfs(nums, 0, 0, min_steps);
    return min_steps;
}

    利用递归的思想,但是不递归的话,就是 i 从左往右,每次 i 移动到后边 nums[i] 这些位置中 nums[i + t] + t 最大的位置上,也就是 i + t,这样一直往后走,直到走到最后的位置(或者更后边),记录走了几次就行了,因为每次都是走的最greedy的步长(每一步不一定是最长的,但是是最合理的,比如 23114)。

int jump(vector<int>& nums)
{
    if(nums.size() == 1)
        return 0;
    int i = 0, cnt = 0, endPos = nums.size() - 1;
    while(i <= endPos)
    {
        if(i + nums[i] >= endPos)
            return cnt + 1; 
        int tmpmax = -1, imax = -1;
        for(int t = 0; t <= nums[i]; t++)
        {
            if(nums[i + t] != 0 && nums[i + t] + t > tmpmax)
            {
                tmpmax = nums[i + t] + t;
                imax = i + t;
            }
        }
        i = imax;
        ++cnt;
    }
    return cnt;
}

403 - Frog Jump

A frog is crossing a river. The river is divided into x units and at each unit there may or may not exist a stone. The frog can jump on a stone, but it must not jump into the water.

Given a list of stones' positions (in units) in sorted ascending order, determine if the frog is able to cross the river by landing on the last stone. Initially, the frog is on the first stone and assume the first jump must be 1 unit.

If the frog's last jump was k units, then its next jump must be either k - 1, k, or k + 1 units. Note that the frog can only jump in the forward direction.

Note:

  • The number of stones is ≥ 2 and is < 1,100.
  • Each stone's position will be a non-negative integer < 231.
  • The first stone's position is always 0.

Example 1:

[0,1,3,5,6,8,12,17]
There are a total of 8 stones.
The first stone at the 0th unit, second stone at the 1st unit, third stone at the 3rd unit, and so on...
The last stone at the 17th unit.

Return true. The frog can jump to the last stone by jumping 
1 unit to the 2nd stone, then 2 units to the 3rd stone, then 
2 units to the 4th stone, then 3 units to the 6th stone, 
4 units to the 7th stone, and 5 units to the 8th stone.

Example 2:

[0,1,2,3,4,8,9,11]
Return false. There is no way to jump to the last stone as the gap between the 5th and 6th stone is too large.

解:

    这道题目有点类似刚刚的 Jump Game II 加了一个限制条件,就是每次跳的距离是上一次距离k + 1,或者 k - 1,或者k。 

    第一反应是dfs,因为这道题目的跳跃限制,相当于一个三叉树的深度优先遍历,理论上不难,就是容易超时,进行了一次尝试,结果果然39个test过了16个,TLE了。

    bool dfs(int k, int curPos, const vector<int>& stones, const int& len)
    {
        if(curPos + k == stones[len - 1]) return true;
        if(k <= 0) return false;
        if(find(stones.begin(), stones.end(), curPos + k) == stones.end()) return false;
        return dfs(k - 1, curPos + k, stones, len)
            || dfs(k, curPos + k, stones, len)
            || dfs(k + 1, curPos + k, stones, len);
    }
    bool canCross(vector<int>& stones) {
        int len = stones.size();
        if(stones[1] != 1) return false;        // for test case: [0, 2]
        if(stones.size() == 2) return true;     // for test case: [0, 1]
        return dfs(1, 1, stones, len) || dfs(2, 1, stones, len);
    }

    DFS不行,那肯定就是要用dp了,刚刚dfs的思路就是每一种可能性都走一遍,看看可不可行,但是在这之间有很多不必要的要判断,其实我们不需要判断那么多,我们只需要用一个set记录每个位置能跳的步长,map<int, unordered_set<int>> mp; 来记录,mp[i] 就表示在 i 这个位置的石头上,可以进行跳跃的长度有多少,最开始所有石头上都是0,我们只知道 mp[0] 是 { 1 },也就是在最开始的石头上,我们只能跳1这个长度,如果有石头在1这个位置,那么mp[1] 就应该是 { 1,  2 },因为0这个长度是没有意义的,所以 1 和 1 + 1 被 insert 到 mp[1] 中,依次类推,如果遍历到能够达到的位置正好的最后一块石头的位置,就返回true。

bool canCross(vector<int>& stones) {
    int n = stones.size();
    map<int, unordered_set<int>> mp;
    mp[0].insert(1);	//初始化,0号石头的步数集合为{ 1 }
    for (int i = 0; i < n - 1; i++)	// 遍历到倒数第二个石头即可
    {
        for (auto step : mp[stones[i]])
        {
            int reach = stones[i] + step;
            if (reach == stones[n - 1])	return true; // 达到最后一块石头
            //每到达一块石头上,更新对应位置的可跳跃步长set
            if (find(stones.begin(), stones.end(), reach) != stones.end())	
            {
                if (step - 1 > 0) mp[reach].insert(step - 1);
                mp[reach].insert(step);
                mp[reach].insert(step + 1);
            }
        }
    }
    return false;
}

猜你喜欢

转载自blog.csdn.net/Bob__yuan/article/details/82656530