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