【1】【經典回溯、動態規划、貪心】【leetcode-55】跳躍游戲


給定一個非負整數數組,你最初位於數組的第一個位置。

數組中的每個元素代表你在該位置可以跳躍的最大長度。

判斷你是否能夠到達最后一個位置。

示例 1:

輸入: [2,3,1,1,4]
輸出: true
解釋: 從位置 0 到 1 跳 1 步, 然后跳 3 步到達最后一個位置。
示例 2:

輸入: [3,2,1,0,4]
輸出: false
解釋: 無論怎樣,你總會到達索引為 3 的位置。但該位置的最大跳躍長度是 0 , 所以你永遠不可能到達最后一個位置。

來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/jump-game

此題很經典,可用回溯、動態規划、貪心求解並對比,我看題第一反應用回溯結果超時

我的代碼(超時):

public class Solution55 {
    boolean res = false;
    public boolean canJump(int[] nums) {
        canJump(nums,0);
        return res;
    }
    public void canJump(int[] nums,int begin) {
        if (nums[begin] >= nums.length-1-begin) {
            res = true;
        }
        for (int i=begin+1;i<=Math.min(nums.length-1,begin + nums[begin]);i++) {
            canJump(nums,i);
            if (res == true) {
                break;
            }
        }
    }
}

 

下面是leetcode官方題解,很詳細!

定義
如果我們可以從數組中的某個位置跳到最后的位置,就稱這個位置是“好坐標”,否則稱為“壞坐標”。問題可以簡化為第 0 個位置是不是“好坐標”。
題解
這是一個動態規划問題,通常解決並理解一個動態規划問題需要以下 4 個步驟:

1.利用遞歸回溯解決問題
2.利用記憶表優化(自頂向下的動態規划)
3.移除遞歸的部分(自底向上的動態規划)
4.使用技巧減少時間和空間復雜度
下面的所有解法都是正確的,但在時間和空間復雜度上有區別。

實際上leetcode的測試用例方法一回溯法和方法二自頂向下的動態規划都超時,只有方法三自底向上的動態規划和方法四貪心可AC

方法 1:回溯

(超時)
這是一個低效的解決方法。我們模擬從第一個位置跳到最后位置的所有方案。從第一個位置開始,模擬所有可以跳到的位置,然后從當前位置重復上述操作,當沒有辦法繼續跳的時候,就回溯。

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

一個快速的優化方法是我們可以從右到左的檢查 nextposition ,理論上最壞的時間復雜度復雜度是一樣的。但實際情況下,對於一些簡單場景,這個代碼可能跑得更快一些。直覺上,就是我們每次選擇最大的步數去跳躍,這樣就可以更快的到達終點。

// Old
for (int nextPosition = position + 1; nextPosition <= furthestJump; nextPosition++)
// New
for (int nextPosition = furthestJump; nextPosition > position; nextPosition--)

比方說,對於下面的例子,我們從下標 0 開始跳,第一次跳到 1,第二次跳到 6。這樣用 3 步就發現坐標 0 是一個“好坐標”。

下面的例子解釋了上述優化沒有辦法解決的情況,坐標 6 是不能從任何地方跳到的,但是所有的方案組合都會被枚舉嘗試。

前幾次回溯訪問節點如下:0 -> 4 -> 5 -> 4 -> 0 -> 3 -> 5 -> 3 -> 4 -> 5 -> 等等。

復雜度分析

時間復雜度:O(2^n),最多有 2^n 種從第一個位置到最后一個位置的跳躍方式,其中 n 是數組 nums 的元素個數

空間復雜度:O(n),回溯法只需要棧的額外空間。

 

方法 2:自頂向下的動態規划

(實際上這個方法也超時)

自頂向下的動態規划可以理解成回溯法的一種優化。我們發現當一個坐標已經被確定為好 / 壞之后,結果就不會改變了,這意味着我們可以記錄這個結果,每次不用重新計算。

因此,對於數組中的每個位置,我們記錄當前坐標是好 / 壞,記錄在數組 memo 中,定義元素取值為 GOOD ,BAD,UNKNOWN。這種方法被稱為記憶化。

例如,對於輸入數組 nums = [2, 4, 2, 1, 0, 2, 0] 的記憶表如下,G 代表 GOOD,B 代表 BAD。我們發現不能從下標 2,3,4 到達最終坐標 6,但可以從 0,1,5 和 6 到達最終坐標 6。

 

步驟

1.初始化 memo 的所有元素為 UNKNOWN,除了最后一個顯然是 GOOD (自己一定可以跳到自己)
2.優化遞歸算法,每步回溯前先檢查這個位置是否計算過(當前值為:GOOD / BAD)
  1.如果已知直接返回結果 True / False
  2.否則按照之前的回溯步驟計算
3.計算完畢后,將結果存入memo表中

enum Index {
    GOOD, BAD, UNKNOWN
}

public class Solution {
    Index[] memo;

    public boolean canJumpFromPosition(int position, int[] nums) {
    //優化遞歸算法,每步回溯前先檢查這個位置是否計算過(當前值為:GOOD / BAD
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); } }

 

 復雜度分析

 時間復雜度:O(n^2),數組中的每個元素,假設為 i,需要搜索右邊相鄰的 nums[i] 個元素查找是否有 GOOD 的坐標。 nums[i] 最多為 nn 是 nums 數組的大小。

空間復雜度:O(2n)=O(n),第一個 n 是棧空間的開銷,第二個 n 是記憶表的開銷

 

 

方法 3:自底向上的動態規划

底向上和自頂向下動態規划的區別就是消除了回溯,在實際使用中,自底向下的方法有更好的時間效率因為我們不再需要棧空間,可以節省很多緩存開銷。更重要的事,這可以讓之后更有優化的空間。回溯通常是通過反轉動態規划的步驟來實現的。

這是由於我們每次只會向右跳動,意味着如果我們從右邊開始動態規划,每次查詢右邊節點的信息,都是已經計算過了的不再需要額外的遞歸開銷,因為我們每次在 memo 表中都可以找到結果。

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

復雜度分析

時間復雜度:O(n^2),數組中的每個元素,假設為 i,需要搜索右邊相鄰的 nums[i] 個元素查找是否有 GOOD 的坐標。 nums[i] 最多為 nn 是 nums 數組的大小。

空間復雜度:O(n),記憶表的存儲開銷。

 

方法 4:貪心

 

當我們把代碼改成自底向上的模式,我們會有一個重要的發現,從某個位置出發,我們只需要找到第一個標記為 GOOD 的坐標(由跳出循環的條件可得),也就是說找到最左邊的那個坐標。如果我們用一個單獨的變量來記錄最左邊的 GOOD 位置,我們就可以避免搜索整個數組,進而可以省略整個 memo 數組。

從右向左迭代,對於每個節點我們檢查是否存在一步跳躍可以到達 GOOD 的位置(currPosition + nums[currPosition] >= leftmostGoodIndex)。如果可以到達,當前位置也標記為 GOOD ,同時,這個位置將成為新的最左邊的 GOOD 位置,一直重復到數組的開頭,如果第一個坐標標記為 GOOD 意味着可以從第一個位置跳到最后的位置。

模擬一下這個操作,對於輸入數組 nums = [9, 4, 2, 1, 0, 2, 0],我們用 G 表示 GOOD,用 B 表示 BAD 和 U 表示 UNKNOWN。我們需要考慮所有從 0 出發的情況並判斷坐標 0 是否是好坐標。由於坐標 1 是 GOOD,我們可以從 0 跳到 1 並且 1 最終可以跳到坐標 6,所以盡管 nums[0] 可以直接跳到最后的位置,我們只需要一種方案就可以知道結果。

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

復雜度分析

  • 時間復雜度:O(n)O(n),只需要訪問 nums 數組一遍,共 nn 個位置,nn 是 nums 數組的長度。
  • 空間復雜度:O(1)O(1),不需要額外的空間開銷。

總結
最后一個問題是,如何在面試場景中想到這個做法。我的建議是“酌情考慮”。最好的解法當然和別的解法相比更簡單也更短,但是不那么容易直接想到。

遞歸回溯的版本最容易想到,所以在思考更復雜解法的時候可以順帶提及一下這個解法,你的面試官實際上可能會想要看到這個解法。但如果沒有,請提及可以使用動態規划的解法,並試想一下如何用記憶表來實現。如果你發現面試官希望你回答自頂向下的方法,那么就不太需要思考自底向上的版本,但我推薦在面試中提及一下自底向下的優點。

很多人會在將自頂向下的動態規划轉成自底向上版本時出現困難,多做一些相關的練習可以對你有所幫助。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM