leetcode55_跳躍遊戲

給定一個非負整數數組,你最初位於數組的第一個位置。數組中的每個元素代表你在該位置可以跳躍的最大長度。判斷你是否能夠到達最後一個位置。

示例 1:

輸入: [2,3,1,1,4]
輸出: true
解釋: 我們可以先跳 1 步,從位置 0 到達 位置 1, 然後再從位置 1 跳 3 步到達最後一個位置。

示例 2:

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

本題只需要返回一個能或不能的結果,並不要求給出路徑,因此也暗示了動態規劃。通常解決並理解一個動態規劃問題需要以下 步驟:
1.利用遞歸回溯解決問題
2.利用記憶表優化(自頂向下的動態規劃)
3.移除遞歸的部分(自底向上的動態規劃)

回溯法,其實是一個模板
1, 以當前位置爲源流往下摸排所有可以跳到的位置
2, 最終遞歸返回源流位置
3, 然後再以下面一個位置作爲源流位置,重複上述操作
回溯法的效率是很低的,以上已經說過了,本題只需要返回一個能或不能的結果,並不要求給出路徑,因此回溯法的作用是讓我們理解這個過程,動態規劃法和回溯法的方向是相反的!!

class Solution:
    # 回溯法複雜度O(2^n),可以理解成搜索一顆樹的各個路徑,每個子節點上都有兩條路可以走
    def canJump(self, nums: List[int]) -> bool:
        return self.canJumpfromposition(nums, 0)
    def canJumpfromposition(self, nums, position):
        # 邊界
        # 當前位置如果爲終點,返回true
        if position == len(nums) - 1:
            return True
        # 從當前位置能跳到的最遠位置
        furtherestjump = min(nums[position]+position, len(nums)-1)
        # 從當前位置的下一個位置開始摸排
        for i in range(position+1, furtherestjump+1):
           # 以此爲源流往下摸排所有可以跳到的位置
           # 最終遞歸返回當前位置,也就是源流
            if self.canJumpfromposition(nums, i):
                return True
            # 然後再以下面一個位置作爲源流位置,重複上述操作
        # 如果當前位置能跳到的範圍內都檢查過了,都不能到達終點,則說明當前位置不可能到達終點
        return False

自頂向下的動態規劃法,其實就是回溯法,只不過用了記憶表保存中間結果,依然遞歸所以np難

class Solution:
    # 回溯法複雜度O(2^n),可以理解成搜索一顆樹的各個路徑,每個子節點上都有兩條路可以走
    def canJump(self, nums: List[int]) -> bool:
        n = len(nums)
        # 記錄對於每個位置,是否能跳到終點
        mem = [None]*n
        mem[n-1] = True
        return self.canJumpfromposition(nums, 0, mem)
    def canJumpfromposition(self, nums, position, mem):
        # 邊界
        # 當前位置如果存在記憶,則直接返回記憶的內容
        if mem[position] != None:
            return mem[position]
        # 從當前位置能跳到的最遠位置
        furtherestjump = min(nums[position]+position, len(nums)-1)
        # 從當前位置的下一個位置開始摸排,直到它能跳到的最遠位置
        for i in range(position+1, furtherestjump+1):
           # 以此爲源流往下摸排所有可以跳到的位置
           # 最終遞歸返回當前位置,也就是源流
            if self.canJumpfromposition(nums, i):
                mem[position] = True
                return True
            # 然後再以下面一個位置作爲源流位置,重複上述操作
        # 如果當前位置能跳到的範圍內都檢查過了,都不能到達終點,則說明當前位置不可能到達終點
        mem[position] = False
        return False

真正的動態規劃是和回溯法方向相反的
爲了探究狀態轉移方程,我們先在上面一種解法的基礎上,簡單反轉一下, O(n2)
值得注意的是,我們發現反轉後,對於每一個位置,它右邊的位置都是有記憶內容存在的

public class Solution {
    def canJump(self, nums: List[int]) -> bool:
        n = len(nums)
        mem = [None]*n
        mem[n-1] = True
        # 從終點左邊一位開始,
        for i in range(n-2, -1, -1):
            # 當前位置能跳到的最遠位置
            furtherestjump = min(nums[i]+i, n-1)
            # 在當前位置的跳動範圍內遍歷
            for j in range(i+1, furtherestjump+1):
                # 如果這個跳動範圍內存在能達終點的位置
	            if mem[j]:
	                # 則當前位置也是可以到達終點的
	                mem[i] = True
	                break
	        # 如果跳動範圍內檢查後,沒有一個能到終點的位置,則當前位置也無法到終點
	        mem[i] = False
        return mem[0]

現在就容易得到我們的狀態轉移方程了,時間複雜度O(n)

class Solution:
    # 最右邊的位置一定是可以到達終點的位置,它也是“目前最左邊的一個可達終點的位置”
    # 從終點左邊一位開始往前遍歷每個位置,判斷其是否可以達到“目前最左邊的一個可達終點的位置”
    # 如果可以的話,將這個位置記錄爲“目前最左邊的一個可達終點的位置”
    # 狀態轉移方程: 
    # i + nums[i] >= leftmost: 對於任意位置 i,判斷它是否能到達“目前最左邊的一個可達終點的位置”leftmost
    def canJump(self, nums: List[int]) -> bool:
        leftmost = len(nums) - 1
        for i in range(leftmost, -1, -1):
            if i + nums[i] >= leftmost:
                leftmost = i
        return leftmost == 0

作爲參考,給出貪心法 O(n)

    # 從左邊開始對於每個位置,記錄歷史上能達到的最遠位置,初始爲0
    def canJump(self, nums: List[int]) -> bool:
        furtherestindex_inhistory = 0
        n = len(nums)
        for i in range(n):
            if i > furtherestindex_inhistory:
                return False
            furtherestindex_inhistory = max(i + nums[i], furtherestindex_inhistory)
            if furtherestindex_inhistory >= n - 1:
                return True
        return furtherestindex_inhistory >= n - 1
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章