動態規划的三要素:最優子結構,邊界和狀態轉移函數,最優子結構是指每個階段的最優狀態可以從之前某個階段的某個或某些狀態直接得到(子問題的最優解能夠決定這個問題的最優解),邊界指的是問題最小子集的解(初始范圍),狀態轉移函數是指從一個階段向另一個階段過度的具體形式,描述的是兩個相鄰子問題之間的關系(遞推式)
重疊子問題,對每個子問題只計算一次,然后將其計算的結果保存到一個表格中,每一次需要上一個子問題解時,進行調用,只要o(1)時間復雜度,准確的說,動態規划是利用空間去換取時間的算法.
判斷是否可以利用動態規划求解,第一個是判斷是否存在重疊子問題,
例子:
爬樓梯 假設你正在爬樓梯。需要 n 階你才能到達樓頂。 每次你可以爬 1 或 2 個台階。你有多少種不同的方法可以爬到樓頂呢? 注意:給定 n 是一個正整數。 示例 1: 輸入: 2 輸出: 2 解釋: 有兩種方法可以爬到樓頂。 1. 1 階 + 1 階 2. 2 階 示例 2: 輸入: 3 輸出: 3 解釋: 有三種方法可以爬到樓頂。 1. 1 階 + 1 階 + 1 階 2. 1 階 + 2 階 3. 2 階 + 1 階
https://leetcode-cn.com/explore/interview/card/top-interview-questions-easy/23/dynamic-programming/54/
分析: 假定n=10,首先考慮最后一步的情況,要么從第九級台階再走一級到第十級,要么從第八級台階走兩級到第十級,因而,要想到達第十級台階,最后一步一定是從第八級或者第九級台階開始.也就是說已知從地面到第八級台階一共有X種走法,從地面到第九級台階一共有Y種走法,那么從地面到第十級台階一共有X+Y種走法.
即F(10)=F(9)+F(8) 分析到這里,動態規划的三要素出來了. 邊界:F(1)=1,F(2)=2 最優子結構:F(10)的最優子結構即F(9)和F(8) 狀態轉移函數:F(n)=F(n-1)+F(n-2)
求解: class Solution(object): def climbStairs(self, n): """ :type n: int :rtype: int """ if n<=2: return n a=1 #邊界 b=2 #邊界 temp=0 for i in range(3,n+1): temp=a+b #狀態轉移 a=b #最優子結構 b=temp #最優子結構 return temp
礦工挖礦問題:
某一個地區發現了5座金礦,每個金礦的黃金儲量不同,需要挖掘的工人也不同,設參加挖掘的總共10人,且每座金礦要么全挖,要么不挖,不能派出一半人挖取一般金礦
金礦 黃金儲量 工人數
1 400 5
2 500 5
3 200 3
4 300 4
5 350 3
分析: 如果要使用動態規划解決,必須要滿足三個條件; 首先確定最優子結構,解題目標是確定10個工人挖5座金礦時能夠獲得最多的黃金數量,該結果可以從10個工人挖4座金礦的子問題遞歸求解. 10個人挖掘4個金礦的過程中,存在兩種選擇,一種是放棄第5座金礦,把10全放入4座金礦的挖掘中,另一種是挖掘第5座金礦,那么10人中的3人取挖掘5座金礦,因此,最優解為上面兩種方案的其中一個. 為了描述方便,假設金礦的數量為n(1-n),工人的數量為w,當前獲得的黃金數量為G[n],當前所用的礦工數量為P[n],根據上述分析買葯獲得10個礦工挖掘第5座金礦的最優解F(5,10),需要在F(4,10),和F(4,10-P[5])+G[4] 中獲得最大值.即 F(5,10)=max(F(4,10),F(4,10-P[5])+G[5])
之后,我們考慮問題的邊界,對於第一座金礦來說,當前礦工人數不滿足金礦所需人數,則其獲得黃金為0,滿足要求則為G[1],因此該邊界問題為(索引從0開始):
當n=1,w>=P[0]時,F(n,w)=G[0]
當n=1,w<P[0]時,F(n,w)=0
綜上,可以得到該問題的狀態轉移函數:
F(n,w)=0(n<=1,w<p[0])
F(n,w)=0(n==1,w>=p[0])
F(n,w)=F(n-1,w)(n>1,w<p[n-1])
F(n,w)=max(F(n-1,w),F(n-1,w-P[n-1])+G[n-1])(n>1,w>p[n-1])
53. 最大子序和 給定一個整數數組 nums ,找到一個具有最大和的連續子數組(子數組最少包含一個元素),返回其最大和。 示例: 輸入: [-2,1,-3,4,-1,2,1,-5,4], 輸出: 6 解釋: 連續子數組 [4,-1,2,1] 的和最大,為 6。 https://leetcode-cn.com/problems/maximum-subarray/
#分析: 利用動態規划的思路解題: 首先尋找最優子問題,[-2,1,-3,4,-1,2,1,-5,4],第一個最優子問題為-2,那么到下一個1時,其最優為當前值或者當前值加上上一個最優值,因而可以得到其遞推公式
狀態轉移方程
dp[i] = max(nums[i], nums[i] + dp[i - 1])
解釋
- i代表數組中的第i個元素的位置
- dp[i]代表從0到i閉區間內,所有包含第i個元素的連續子數組中,總和最大的值
nums = [-2,1,-3,4,-1,2,1,-5,4]
dp = [-2, 1, -2, 4, 3, 5, 6, 1, 5]
class Solution(object): def maxSubArray(self, nums): """ :type nums: List[int] :rtype: int """ # 判斷邊界
if len(nums)==0: return 0 # 定義一個表格進行存儲上一個子問題的最優解 d=[] d.append(nums[0]) #第一個最優解為第一個元素 max_=nums[0] #返回的最大值 for i in range(1,len(nums)): if nums[i]>nums[i]+d[i-1]: d.append(nums[i]) else: d.append(nums[i]+d[i-1]) if max_<d[i]: max_=d[i] return max_
198. 打家劫舍 你是一個專業的小偷,計划偷竊沿街的房屋。每間房內都藏有一定的現金,影響你偷竊的唯一制約因素就是相鄰的房屋裝有相互連通的防盜系統,如果兩間相鄰的房屋在同一晚上被小偷闖入,系統會自動報警。 給定一個代表每個房屋存放金額的非負整數數組,計算你在不觸動警報裝置的情況下,能夠偷竊到的最高金額。 示例 1: 輸入: [1,2,3,1] 輸出: 4 解釋: 偷竊 1 號房屋 (金額 = 1) ,然后偷竊 3 號房屋 (金額 = 3)。 偷竊到的最高金額 = 1 + 3 = 4 。 示例 2: 輸入: [2,7,9,3,1] 輸出: 12 解釋: 偷竊 1 號房屋 (金額 = 2), 偷竊 3 號房屋 (金額 = 9),接着偷竊 5 號房屋 (金額 = 1)。 偷竊到的最高金額 = 2 + 9 + 1 = 12 。 https://leetcode-cn.com/problems/house-robber/
class Solution(object): def rob(self, nums): """ :type nums: List[int] :rtype: int """ # 思路:由該例子解析:[1,2,3,1],最后一個1,該點有兩種操作,一個是偷取,那么的加上在2處的最優解,不偷取,獲取3處的最優解.因而f(1)的最優子解為max(f(2)+1,f(3))轉移方程:d[i]=max(d[i-1],d[i-2]+nums[i]) , 邊界 d[0]=nums[0],d[1]=max(nums[0],nums[1]) if len(nums)==0: return 0 if len(nums)<=2: return max(nums) dp=[] dp.append(nums[0]) dp.append(max(nums[0],nums[1])) for i in range(2,len(nums)): dp.append(max(dp[i-1],dp[i-2]+nums[i])) return dp[-1]
121. 買賣股票的最佳時機 給定一個數組,它的第 i 個元素是一支給定股票第 i 天的價格。 如果你最多只允許完成一筆交易(即買入和賣出一支股票),設計一個算法來計算你所能獲取的最大利潤。 注意你不能在買入股票前賣出股票。 示例 1: 輸入: [7,1,5,3,6,4] 輸出: 5 解釋: 在第 2 天(股票價格 = 1)的時候買入,在第 5 天(股票價格 = 6)的時候賣出,最大利潤 = 6-1 = 5 。 注意利潤不能是 7-1 = 6, 因為賣出價格需要大於買入價格。 示例 2: 輸入: [7,6,4,3,1] 輸出: 0 解釋: 在這種情況下, 沒有交易完成, 所以最大利潤為 0。 https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/
class Solution(object): def maxProfit(self, prices): """ :type prices: List[int] :rtype: int """ # 思路:動態規划,設置一個變量記錄買入的最小金額 # [7,1,5,3,6,4] 從最后一個4開始分析,比如我從4賣出,那么其獲得的最大利潤為(6)的時候最大利潤與(4)-最小值之間的最大值, # 遞推式為 f(4)=max(f(6),4-最小金額) 邊界: f(0)=0,最優子結構:f(4)的最有子結構為f(6) if len(prices)<=1: return 0 dp=[] dp.append(0) min_value=prices[0] for i in range(1,len(prices)): dp.append(max(dp[i-1],prices[i]-min_value)) if prices[i]<min_value: min_value=prices[i] return dp[-1]
746. 使用最小花費爬樓梯 數組的每個索引做為一個階梯,第 i個階梯對應着一個非負數的體力花費值 cost[i](索引從0開始)。 每當你爬上一個階梯你都要花費對應的體力花費值,然后你可以選擇繼續爬一個階梯或者爬兩個階梯。 您需要找到達到樓層頂部的最低花費。在開始時,你可以選擇從索引為 0 或 1 的元素作為初始階梯。 示例 1: 輸入: cost = [10, 15, 20] 輸出: 15 解釋: 最低花費是從cost[1]開始,然后走兩步即可到階梯頂,一共花費15。 示例 2: 輸入: cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1] 輸出: 6 解釋: 最低花費方式是從cost[0]開始,逐個經過那些1,跳過cost[3],一共花費6。 注意: cost 的長度將會在 [2, 1000]。 每一個 cost[i] 將會是一個Integer類型,范圍為 [0, 999] https://leetcode-cn.com/problems/min-cost-climbing-stairs/
class Solution(object): def minCostClimbingStairs(self, cost): """ :type cost: List[int] :rtype: int """ # 思路: 從樓頂分析,比如說10為樓頂,到達樓頂只有兩種方式,一種從第八層走兩步到達,一種是從第九層走一步到達,因為該10為樓頂其: # 10為樓頂:F(10)最有子結構為: F(9) 和 F(8) # F(10) 遞推式: F(10)=min(F(9)+cost[9],F(8)+cost[8]) # 邊界: F(0)=1 F(1)=100 # if len(cost)<=1: return min(cost) dp=[] dp.append(cost[0]) dp.append(cost[1]) for i in range(2,len(cost)+1): #樓頂不在cost的范圍內,因為對遍歷+1 if i==len(cost): #該層為樓頂,沒有取值 dp.append(min(dp[i-1],dp[i-2])) else: dp.append(min(dp[i-1]+cost[i],dp[i-2]+cost[i])) return dp[-1]
338. 比特位計數 給定一個非負整數 num。對於 0 ≤ i ≤ num 范圍中的每個數字 i ,計算其二進制數中的 1 的數目並將它們作為數組返回。 示例 1: 輸入: 2 輸出: [0,1,1] 示例 2: 輸入: 5 輸出: [0,1,1,2,1,2] 進階: 給出時間復雜度為O(n*sizeof(integer))的解答非常容易。但你可以在線性時間O(n)內用一趟掃描做到嗎? 要求算法的空間復雜度為O(n)。 你能進一步完善解法嗎?要求在C++或任何其他語言中不使用任何內置函數(如 C++ 中的 __builtin_popcount)來執行此操作。
class Solution(object): def countBits(self, num): """ :type num: int :rtype: List[int] """ # 動態規划問題:當數字i未2的指數倍時,其只有一個1,dp[i]=1 if(i==2**k) # 遞推試 : dp[i] = 1+dp[i-2**k] res=[0] k=0 for i in range(1,num+1): if(i == 2**k): res.append(1) k+=1 else: res.append(1+res[i-2**k]) return res
120. 三角形最小路徑和 給定一個三角形,找出自頂向下的最小路徑和。每一步只能移動到下一行中相鄰的結點上。 例如,給定三角形: [ [2], [3,4], [6,5,7], [4,1,8,3] ] 自頂向下的最小路徑和為 11(即,2 + 3 + 5 + 1 = 11)。 說明: 如果你可以只使用 O(n) 的額外空間(n 為三角形的總行數)來解決這個問題,那么你的算法會很加分。
class Solution(object): def minimumTotal(self, triangle): """ :type triangle: List[List[int]] :rtype: int """ ''' dp問題:利用列表進行存儲,每一行每個步驟結束后的最小值,那么在最后一行,其最小值為min(4+dp[0],4+dp[1],1+dp[0],1+dp[1]...) 所以狀態轉移方程為: 如果i==0 or i==len(triangle[row]) 那么其轉移方程為dp[i]=dp[0]triangle[row][i] dp[i]=dp[i-1]+triangle[row][i] dp[i]=min(dp[i-1],dp[i])+triangle[row][i] 初始值為 dp[0]=triangle[0][0] ''' if len(triangle)==1: return triangle[0][0] dp=[[triangle[0][0]]] for i in range(1,len(triangle)): for j in range(len(triangle[i])): dp.append([]) # 邊界只有一個鄰邊 if j==0: dp[i].append(dp[i-1][j]+triangle[i][j]) elif j==len(triangle[i])-1: dp[i].append(dp[i-1][j-1]+triangle[i][j]) else: # 當前取值,在上一層的鄰邊最小值相加 dp[i].append(min(dp[i-1][j-1],dp[i-1][j])+triangle[i][j]) return min(dp[len(triangle)-1])