動態規劃案例(python版本)
最近幾天一直在看有關動態規劃的演算法,整理了一些常見案例,主要是求最長公共子序列,最長公共子串,最長遞增子序列,最長迴文子串,硬幣的組合數,硬幣的最少組合方法,最小編輯距離,揹包問題(01揹包,完全揹包,多重揹包)等方面的經典案例求解。
這些案例大部分都是用python實現的動態規劃演算法。
案例一:求最長公共子序列(不一定連續)
Q:給定兩個序列,找出在兩個序列中同時出現的最長子序列的長度。一個子序列是出現在相對順序的序列,但不一定是連續的。
分析:
- 假設str1的長度為M,str2的長度為N,生成的大小為M*N的矩陣dp。dp[i][j]的含義是str[0...i]與str2[0...j]的最長公共子序列的長度。
- 矩陣dp第一列,即dp[i][0],代表str1[0...i]與str2[0]的最長公共子序列長度。str2[0]只有一個字元,所以dp[i][0]最大為1,如果str[i] == str2[0],則令dp[i][0]為1,一旦dp[i][0]被設為1,則令dp[i+1...M][0]全部為1
- 矩陣dp第一行,即dp[0][j],與步驟1同理。如果str1[0]==str[j],則令dp[0][j]為1,一旦dp[0][j]被設為1,則令dp[0][j+1...N]全部為1
- 其他位置,dp[i][j]的值只可能來自一下三種情況,三種可能的值中,選擇最大的值即可:
- 情況一:可能是dp[i-1][j]的值,這代表str1[0....i-1]與str2[0...j]的最長公共子序列長度。 舉例:str1 = "A1BC2", str2 = "AB34C" str1[0..3]為"A1BC",str2[0...4]為"AB34C",這兩部分最長公共子序列為"ABC",即dp[3][4]為3. str1整體和str2整體最長公共子序列也是"ABC",所以dp[4][4]可能來自dp[3][4]
- 情況二:同理可知,dp[i][j]的值也可能是dp[i][j-1]
- 情況三:如果str1[i]==str2[j],還可能是dp[i-1][j-1]+1的值。 舉例:比如str1 ="ABCD", str2 = "ABCD". str1[0...2]即“ABC”與str2[0...2]即“ABC”的最長公共子序列為"ABC",也就是dp[2][2]為3。因為str1和str2的最後一個字元都是"D",所以dp[i][j] = dp[i-1][j-1]+1
def findLongest(self, A, n, B, m): #新建一個m行n列的矩陣 matrix = [0] * m * n #1、矩陣的第一行,即matrix[0][i],代表str1[0]與str2[0...n]的最長公共子串. # str2[0]只有一個字元,所以matrix[i][0]最大為1 for i in range(n): if A[i] == B[0]: for j in range(i,n): matrix[j] = 1 #2、矩陣的第一列,matrix[i][0]最大為1 for i in range(m): if B[i] == A[0]: for j in range(i,m): matrix[j*n] = 1 #3、其他位置,matrix[i][j]有三種情況,matrix[m][n]即為所求的最長公共子序列長度 for i in range(1,m): for j in range(1,n): if B[i] == A[j]: matrix[i*n+j] = max(matrix[(i-1)*n+j-1]+1,matrix[(i-1)*n+j],matrix[i*n+j-1]) else: matrix[i*n+j] = max(matrix[(i-1)*n+j],matrix[i*n+j-1]) return matrix[m*n-1]
案例二:求最長公共子串(連續)
Q:給定兩個序列,找出在兩個序列中同時出現的最長子序列的長度。子串的意思是要求為連續的子序列
分析:
矩陣的第一行,即matrix[0][i],代表str1[0]與str2[0...n]的最長公共子串.
與案例一中的前兩步相同,只是最後一步不同。
程式碼:
def findLongest(self, A, n, B, m):
#新建一個m行n列的矩陣
matrix = [0] * m * n
#1、矩陣的第一行,即matrix[0][i],代表str1[0]與str2[0...n]的最長公共子串.
# str2[0]只有一個字元,所以matrix[i][0]最大為1
for i in range(n):
if A[i] == B[0]:
matrix[i] = 1
#2、矩陣的第一列,matrix[i][0]最大為1
for i in range(m):
if B[i] == A[0]:
matrix[i*n] = 1
#3、其他位置
max = 0
for i in range(1,m):
for j in range(1,n):
if B[i] == A[j]:
matrix[i*n+j] = matrix[(i-1)*n+j-1]+1
if max<matrix[i*n+j]:
max = matrix[i*n+j]
return max
案例三:最長遞增子序列
Q:給定一個序列,找到最長子序列的長度,使得子序列中的所有元素被排序的順序增加。比如arr = [2,1,5,3,6,4,8,9,7], 最長遞增子序列為[1,3,4,8,9],所以返回這個子序列的長度5。給定陣列arr,返回陣列arr,返回arr的最長遞增子序列長度。比如arr =[2,1,5,3,6,4,8,9,7],最長遞增子序列為[1,3,4,8,9],所以返回這個子序列的長度5
分析:
dp[i]表示在必須以arr[i]結尾的情況下,arr[0 ... i]中的最大遞增子序列長度,
dp[i] = max{ dp[j]+1 (0<=j<i , arr[j]<arr[i])}
程式碼:
def findLongest(self, A, n):
dp = [0] * n
dp[0] = 1
for i in range(1, len(A)):
l = [1]
for j in range(0, i):
if A[i] > A[j]:
l.append(dp[j] + 1)
dp[i] = max(l)
return max(dp)
案例四:最長迴文子串
Q:給一個字串,找出它的最長的迴文子序列LPS的長度。例如,如果給定的序列是“BBABCBCAB”,則輸出應該是7,“BABCBAB”是在它的最長迴文子序列。
分析:
dp[i][j] = 1表示字串s從i到j是迴文串 dp[i][j] = 0表示字串s從i到j不是迴文串
如果dp[i][j] = 1, 那麼dp[i+1][j-1] = 1
程式碼:
def manacher(self,s):
#建立一個二維陣列
maxlen = 0
start = 0
dp = [[0 for i in range(len(s))] for i in range(len(s))]
for i in range(len(s)):
dp[i][i] = 1
if i+1<len(s) and s[i] == s[i+1]:
dp[i][i+1] = 1
maxlen = 2
start = i
for i in range(3,len(s)+1): #i表示迴文子串長度,從3開始,最長為len(s)
for j in range(len(s)-i+1): #j表示指標移動的起點
k = i+j-1 #k表示終點
if dp[j+1][k-1]==1 and s[j]==s[k]:
dp[j][k] = 1
if i>maxlen:
start = j
maxlen = i
if maxlen>=2:
return s[start:start+maxlen]
return None
案例五:硬幣最少數量(湊齊n元最少需要幾枚硬幣)
Q:假設有 1 元,3 元,5 元的硬幣若干(無限),現在需要湊出 11 元,問如何組合才能使硬幣的數量最少?
分析:
我們先假設一個函式 d(i) 來表示需要湊出 i 的總價值需要的最少硬幣數量。
- 當 i = 0 時,很顯然我們可以知道 d(0) = 0。因為不要湊錢了嘛,當然也不需要任何硬幣了。注意這是很重要的一步,其後所有的結果都從這一步延伸開來。
- 當 i = 1 時,因為我們有 1 元的硬幣,所以直接在第 1 步的基礎上,加上 1 個 1 元硬幣,得出 d(1) = 1。
- 當 i = 2 時,因為我們並沒有 2 元的硬幣,所以只能拿 1 元的硬幣來湊。在第 2 步的基礎上,加上 1 個 1 元硬幣,得出 d(2) = 2。
- 當 i = 3 時,我們可以在第 3 步的基礎上加上 1 個 1 元硬幣,得到 3 這個結果。但其實我們有 3 元硬幣,所以這一步的最優結果不是建立在第 3 步的結果上得來的,而是應該建立在第 1 步上,加上 1 個 3 元硬幣,得到 d(3) = 1。
- ...
接著就不再舉例了,我們來分析一下。可以看出,除了第 1 步這個看似基本的公理外,其他往後的結果都是建立在它之前得到的某一步的最優解上,加上一個硬幣得到。得出:
d(i) = d(j)+1
這裡j<i。通俗的將,我們需要湊出 i 元,就在湊出 j 的結果上再加上某一個硬幣就行了。那這裡我們加上的是哪個硬幣呢。嗯,其實很簡單,把每個硬幣試一下就行了:
- 假設最後加上的是 1 元硬幣,那 d(i) = d(j) + 1 = d(i - 1) + 1。
- 假設最後加上的是 3 元硬幣,那 d(i) = d(j) + 1 = d(i - 3) + 1。
- 假設最後加上的是 5 元硬幣,那 d(i) = d(j) + 1 = d(i - 5) + 1。
我們分別計算出d(i - 1) + 1,d(i - 3) + 1,d(i - 1) + 1的值,取其中的最小值,即為最優解,也就是d(i)。
最後公式
d(i) = min( d(i - 1) + 1,d(i - 3) + 1,d(i - 5) + 1 )
程式碼:
def findLeast(self, n):
# write code here
l = [0,1,2,1,2,1]
for i in range(6,n+1):
l.append(min(l[i-1]+1,l[i-3]+1,l[i-5]+1))
return l[n]
案例六:硬幣組合種類數(湊齊n分錢有多少種方法)
Q:有數量不限的硬幣,幣值為25分、10分、5分和1分,請編寫程式碼計算n分有幾種表示法。給定一個int n,請返回n分有幾種表示法。保證n小於等於1000,為了防止溢位,請將答案Mod 1000000007。
分析:
- dp[i][sum]表示用前i種硬幣構成sum的所有組合數,本題實際上就是求dp[n][sum]
- coins = [1,5,10,25]
- dp[i][sum] = dp[i-1][sum-0*coins[i]] + dp[i-1][sum-1*coins[i]]+....+dp[i-1][sum-k*coins[i]](k = sum/coins[i])
- 上一步化簡後:dp[i][sum] = dp[i-1][sum-k*coins[i]]求和 (k = 0...sum/coins)
程式碼:
def coinsWays(self, n):
coins = [1,5,10,25]
dp = [[0 for i in range(n+1)] for i in range(5)]
for i in range(5):
dp[i][0] = 1
for i in range(1,5):
for j in range(1,n+1):
for k in range(j/coins[i-1]+1):
dp[i][j] += dp[i-1][j-k*coins[i-1]]
return dp[4][n]
案例七:最小編輯距離
Q:給定一個長度為m和n的兩個字串,設有以下幾種操作:替換(R),插入(I),刪除(D)且都是相同代價的操作。尋找到轉化一個字串插入到另一個需要修改的最小(操作)數量。
分析:
- dp[i][j] 表示長度為i的字串A替換到長度為j的字串B所付出的代價
- 當兩個字串的大小為0,其操作距離為0。
- 當其中一個字串的長度是零,需要的操作距離就是另一個字串的長度.
程式碼:
def editDist(self,s1,s2):
#思路:
#dp[i][j] 表示長度為i的字串A替換到長度為j的字串B所付出的代價
len1 = len(s1)
len2 = len(s2)
dp = [[0 for i in range(len2+1)]for i in range(len1+1)]
for i in range(len1+1):
dp[i][0] = i
for i in range(len2+1):
dp[0][i] = i
for i in range(1,len1+1):
for j in range(1,len2+1):
#如果當前兩個字串指標所指向的字元相等時,
if s1[i-1]==s2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = min(dp[i-1][j],dp[i][j-1],dp[i-1][j-1])+1
return dp[len1][len2]
案例八:揹包問題(01揹包,完全揹包,多重揹包)
這裡只寫出了c++的寫法
首先分別解釋一下三種揹包的含義:
- 01揹包:有n種物品與承重為m的揹包。每種物品只有一件,每個物品都有對應的重量weight[i]與價值value[i],求解如何裝包使得價值最大
- 完全揹包:有n種物品與承重為m的揹包。每種物品有無限件,每個物品都有對應的重量weight[i]與價值value[i],求解如何裝包使得價值最大
- 多重揹包:有n種物品與承重為m的揹包。每種物品有有限件num[i],每個物品都有對應的重量weight[i]與價值value[i],求解如何裝包使得價值最大
關於01揹包:
為什麼叫01揹包,因為裝進去就是1,不裝進去就是0,所以針對每個物品就有兩種狀態,裝?不裝?所以這個揹包只要有足夠大的空間,這個物品都是有可能被裝進去的。
所以有狀態轉移方程
dp[i][m] = max(dp[i-1][m],dp[i-1][m-weight[i]+value[i]])
for (i = 1; i <= n; i++) #從1開始是因為這涉及到dp[i-1][j],從0開始會越界
for (m = v; j >= weight[i]; j--)//在這裡,揹包放入物品後,容量不斷的減少,直到再也放不進了
dp[i][m] = max(dp[i-1][m],dp[i-1][m-weight[i]+value[i]])
仔細分析就會發現,這種二維陣列開銷很大,因此有了下面的滾動陣列,說白了只是把所有的物品都跑一遍,然後到最後一個物品的時候輸出答案,那麼過程值只是計算的時候用一次,沒必要存下來,所以用一個數組去滾動儲存,然後用後一個狀態的值去覆蓋前一個狀態。
for(int i=1; i<=n; i++)//對每個數判斷,可反
{
for(int j=m; j>=weight[i]; j--)//這裡這個迴圈定死,不能反,反了就是完全揹包
{
dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);//其實不斷在判斷最優解,一層一層的
}
}
其實就是規定從m開始迴圈,保證了選擇這個物品時,肯定不會重複使用狀態。關於完全揹包:
完全揹包每個物品都是無限,認死了選價效比最高的,不一定是完全填滿揹包的。(其實就是01揹包一維陣列中把j倒置)
這裡的二維陣列就不如一維陣列了
for(int i=0;i<n;i++){
for(int j=node[i].b;j<=m;j++){//這樣就是完全揹包
dp[j]=max(dp[j],dp[j-node[i].b]+node[i].a)
關於多重揹包:
首先把物品拆開,把相同的num[i]件物品看成價值和重量相同的num[i]件不同的商品,那麼,就轉化成了一個規模稍微大一點的01揹包了。
for(int i=1; i<=n; i++)//每種物品
for(int k=0; k<num[i]; k++)//其實就是把這類物品展開,呼叫num[i]次01揹包程式碼
for(int j=m; j>=weight[i]; j--)//正常的01揹包程式碼
dp[j]=max(dp[j],dp[j-weight[i]]+value[i])
以上八種案例為動態規劃的經典案例,後序還會進行不定期更新!