1. 程式人生 > >LeetCode - 55. Jump Game、45. Jump Game II、403. Frog Jump - 一維陣列跳躍問題 (多種方法)

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