1、什麼是動態規劃
動態規劃(dynamic programming)是運籌學的一個分支,是求解決策過程(decision process)最優化的數學方法。把多階段過程轉化爲一系列單階段問題,利用各階段之間的關係,逐個求解,創立了解決這類過程優化問題的新方法——動態規劃。
使用動態規劃特徵:
1. 求一個問題的最優解
2. 大問題可以分解爲子問題,子問題還有重疊的更小的子問題
3. 整體問題最優解取決於子問題的最優解(狀態轉移方程)
4. 從上往下分析問題,從下往上解決問題
5. 討論底層的邊界問題
動態規劃最重要的有三個概念:1、最優子結構 2、邊界 3、狀態轉移方程
2、走樓梯問題
有十個臺階,從上往下走,一次只能走一個或兩個臺階,請問總共有多少種走法?
1、最優子結構:我們來考慮要走到第十個臺階的最後一步,最後一步必須走到第八或者第九。不難得到 f(10) = f(9)+f(8)。f(9) = f(8)+f(7)
2、邊界:f(1) = 1, f(2) = 2
3、狀態轉移:f(n) = f(n-1) + f(n-2)
解法一:遞歸
def get_count(n):
if n == 1:return 1
if n == 2:return 2
else:
return get_count(n-1)+get_count(n-2)
print(get_count(10))
程序簡單,但是非常暴力!
程序複雜度分析:
很顯然這是一個滿二叉樹,高度爲N-1。所以總節點數爲2^N-1,時間複雜度爲O(2^N) 。看着就恐怖。
解法二、備忘錄算法
回顧一下上面的遞歸計算方法,我們不難看出,本來總共只有f(1)-f(N)個節點,硬生生被增加到2^N-1個,這就是產生了大量重複的運算。找到問題的根源,對應的解決方法就應運而生,那就是從下往上算,把以前計算過的數值,保存在一個哈希表中,然後後面計算時先查詢一下,存在就無需計算。時間複雜度爲O(n) ,空間複雜度爲O(n)。但是在仔細一想其實,無需保存所有的f,每個f都只與前兩個值相關,所以空間複雜度可以降低爲O(1).我們來看看相關代碼。
def get_count(n):
if n == 1:return 1
elif n == 2 :return 2
else:
l = [1,2]
for i in range(3,n):
l[0],l[1] = l[1],l[0]+l[1]
return l[0]+l[1]
解法3 動態規劃的狀態轉移
第 i 個狀態的方案數和第 i-1, i-2時候的狀態有關,即:dp[i]=dp[i-1]+dp[i-2],dp表示狀態矩陣。
def climb_stairs(n):
dp=[0]*n
dp[0]=1
dp[1]=2
for i in range(2,n):
dp[i]=dp[i-1]+dp[i-2]
return dp[n-1]
3、整數拆分
def func(n):
l = [1]
for i in range(3,n+1):
m = 0
for j in range(1,i//2+1):
m = max(m,j*(i-j),j*l[i-j-2])
l.append(m)
return l
print(func(10))
4、最大子序和
核心思想:記錄以前一個數結尾的最長子序列的最大值。
def max_subarry(nums):
m = nums[0]
tem_m = nums[0]
pre = nums[0]
for i in range(1,len(nums)):
if pre<=0:
tem_m = nums[i]
pre = nums[i]
else:
pre+=nums[i]
tem_m+=nums[i]
if tem_m>m: m = tem_m
return m
5,取珠寶問題
一條直線上,有n 個房間,每個房間數量不等的財寶,一個盜賊希望從房屋中盜取財寶,由於房屋有警報器,同時從相鄰兩個房間盜取珠寶就會觸發警報,求在不觸發警報的情況下,最多可獲取多少財寶?
如有6個房間,每個房間珠寶數量爲[5,2,6,3,1,7]
狀態轉移:
前1個房間可獲取珠寶的最大數量:5;
前2個房間可獲取珠寶的最大數量:5;
前3個房間可獲取珠寶的最大數量:11;
前4個房間可獲取珠寶的最大數量:11;
前5個房間可獲取珠寶的最大數量:12;
可以發現,前i個房間最大可獲取的數量和前i-1,i-2個房間可獲取的最大珠寶數量,以及第i個房間的珠寶數量有關:第i個房間珠寶有兩種方案,獲取(總最大數量等於第i個房間的珠寶數量+前i-2個房間的珠寶最大數量)和不獲取(總最大數量=前i-1個房間可獲取珠寶的最大數量)。
即:dp[i]=max(dp[i-2]+value[i],dp[i-1]),dp表示狀態矩陣。
def stealJewellery(value):
n=len(value)
dp=[0]*n
dp[0]=5
dp[1]=5
for i in range(2,n):
dp[i]=max(dp[i-2]+value[i],dp[i-1])
return dp[n-1]
value=[5,2,6,3,1,7]
print(stealJewellery(value))
6,最大子序列和問題
給定一個數組,求這個數組的連續子數組中,最大的那一段的和。
如數組 arr= [-2,1,-3,4,-1,2,1,-5,4]
狀態轉移:
如果當前數組的值arr[i]加上第i-1個狀態的值大於數組arr[i],第i個狀態的值就等於arr[i]+dp[i-1],
否則,dp[i]=arr[i]。其中dp[i]記錄的是以第i個數組結尾的最大字段和
記錄當前最大的子序列和。即 dp[i]=max(dp[i-1]+arr[i],arr[i]),最大子序列和爲max(dp)
def subsequenceSum(arr):
n=len(arr)
dp=[0]*n
dp[0] = arr[0]
for i in range(1,n):
dp[i]=max(dp[i-1]+arr[i],arr[i])
return max(dp)
7,找零錢問題
已知不同面值的鈔票,求如何用最少數量的鈔票組成某個金額,求可以使用的最少鈔票數量。如果任意數量的已知面值鈔票都無法組成該金額,則返回-1
假設coins=[1,2,5,7,10],金額:amount=14,dp[i]表示金額 i 的最優解。
金額14的最後一張面額可能由coins裏面的任意一張得到,即:
14=coins[0]+(14-coins[0]) 14=1+13 dp[14]=1+dp[13]
14=coins[0]+(14-coins[1]) 14=2+12 dp[14]=1+dp[12]
14=coins[0]+(14-coins[2]) 14=5+9 dp[14]=1+dp[9]
14=coins[0]+(14-coins[3]) 14=7+7 dp[14]=1+dp[7]
14=coins[0]+(14-coins[4]) 14=10+4 dp[14]=1+dp[4]
具體最後一張選了哪個面額,就要看哪個面額情況下總拼湊數量最少。即:dp[i]=min(dp[i],dp[i-coins[j]]+1),當計算到金額14時候,小於14的金額的最小數量都已經求出並存儲在dp[i]裏面,此時只需要直接取出比較即可。最終函數返回dp[14]即爲所求的最少數量。
求解代碼一共有兩個for循環,一個是金額從0-amount的最少數量(dp[i]),另外一個是面額數量組合(coins[j])。dp[i]的初始值都設置爲amount+1,當所有的coins都無法拼湊當前金額 i 的時候,dp[i] = amount+1 此時dp[i] 大於amount,所以函數返回-1。
def coinChange(coins,amount):
dp=[amount+1]*(amount+1)
dp[0]=0
for i in range(1,amount+1):
for j in range(len(coins)):
if coins[j]<=i:
dp[i]=min(dp[i],dp[i-coins[j]]+1)
if dp[amount]>amount:
return -1
else:
return dp[amount]
coins=[1,2,5,7,10]
print (coinChange(coins,14))
相關參考: