背景:leetcode刷題遇到動態規划的題目,做不出來時看別人的code,也可以理解,但還是沒有找到create solution的技巧,單純的comprehend and remeber,直到遇到了下面這篇題解,終於形成了自己的動態規划通用解題方法,拿所有easy難度的題目試了下,結果橫掃
https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/discuss/108870/Most-consistent-ways-of-dealing-with-the-series-of-stock-problems
在動態規划題目中,變形數量最多的差不多就是股票問題了,上述解法也圍繞股票問題展開
根據上述帖子,我總結了動態規划的解題方法如下
Question1:如何識別題目適用於動態規划?
某一過程包含多種狀態(情況),后一種狀態的生成依賴於前面的情況
Quetion2:如何寫動態規划?
1、找出問題中的狀態(state)和選擇(choice)
2、用選擇去表達狀態之間的轉移
like below
for state1 in state1_list: for state2 in state2_list: for state3 in state3_list: dp[state1][state2][state3] = choose(choice1, choice2, choice3)
其中所有的choice都用前面已計算出的狀態來表示
Question3:如何優化動態規划?
在上述解題方案中,經常會有不必要的空間復雜度消耗,我們可以發現並通過適用常數個變量的方式替代適用數組或者矩陣。
Example:
你是一個專業的小偷,計划偷竊沿街的房屋。每間房內都藏有一定的現金,影響你偷竊的唯一制約因素就是相鄰的房屋裝有相互連通的防盜系統,如果兩間相鄰的房屋在同一晚上被小偷闖入,系統會自動報警。
給定一個代表每個房屋存放金額的非負整數數組,計算你在不觸動警報裝置的情況下,能夠偷竊到的最高金額。
來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/house-robber
著作權歸領扣網絡所有。商業轉載請聯系官方授權,非商業轉載請注明出處。
class Solution: """步驟一:判斷是否為動態規划題目;很明顯,下一家的偷法依賴於前面的偷法,不同的偷法導致不同的偷盜總額,屬於動態規划""" def rob(self, nums: List[int]) -> int: if not nums or len(nums)==0: return 0 #步驟二:找到狀態和選擇;這里狀態為當前偷到了第幾家,用i表示,選擇為偷或者不偷 dp = {} #初始化開始情況,當偷第-1家時,可以理解為偷得金額為0 dp[-1] = 0 dp[0] = nums[0] #遍歷的所有情況 for i in range(1,len(nums)): #這里的選擇方法為取最大值,不同的選擇分別代表偷與不偷 dp[i] = max(dp[i-2]+nums[i],dp[i-1]) return dp[len(nums)-1]
步驟三:優化空間復雜度;上述解法用到了長為N的輔助列表,空間復雜度為O(n)。優化思路為用幾個變量替代輔助列表
可以看到當前循環的dp[i],
依賴於當前循環dp[i-1]與dp[i-2],
即依賴於上一循環的dp[i]與dp[i-1],
所以我們用2個變量即可解決該問題,分別cur與pre即可
代碼可優化為
class Solution: def rob(self, nums: List[int]) -> int: if not nums or len(nums)==0: return 0 pre = 0 cur = nums[0] for i in range(1,len(nums)): tmp = cur cur = max(pre+nums[i],cur) pre = tmp return cur
另外,講一下python實現動態規划過程中需要注意的一點,
與java不同,java可以直接int[][]初始化多重數組並賦予默認值,然后直接用索引取訪問對應元素即可,但python的list結構不會初始化大小,需要我們自己初始化,否則直接使用索引會索引越界
如
#初始化長度為10的一維數組並賦予默認值0 dp = [0]*10 #初始化10*3的二維數組並賦予默認值0 dp = [[0]*10 for _ in range(10)]
但問題是,某些情況下需要我們去思考默認值賦予什么才比較合適,這樣會造成困擾,我們更希望不用去思考初始化為何值(keep None即可),如下
#初始化字典(哈希表)代替一維數組(list) dp = {} #初始化10個字典並放到一維數組中 dp = [{} for _ in range(10)]
這樣就解決了不給出默認值用於內存分配導致的角標越界問題
歸納出上述套路之后,leetcode遇到動態規划的題目再也不會毫無頭緒啦~~~
