動態(tài)規(guī)劃(Dynamic Programming)
本文包括:
- 動態(tài)規(guī)劃定義
- 狀態(tài)轉(zhuǎn)移方程
- 動態(tài)規(guī)劃算法步驟
- 最長非降子序列(LIS)
- 最大乘積子串
- Unique Paths
- Unique Paths II
- Minimum Path Sum
- Triangle
- 最長公共自序列(LCS)
- 編輯距離
- 交替字符串
- 矩陣鏈乘積
前文引自:http://www.hawstein.com/posts/dp-novice-to-advanced.html
1、 什么是動態(tài)規(guī)劃,我們要如何描述它?
動態(tài)規(guī)劃算法通常基于一個遞推公式及一個或多個初始狀態(tài)。 當(dāng)前子問題的解將由上一次子問題的解推出。使用動態(tài)規(guī)劃來解題只需要多項式時間復(fù)雜度, 因此它比回溯法、暴力法等要快許多。
首先,我們要找到某個狀態(tài)的最優(yōu)解,然后在它的幫助下,找到下一個狀態(tài)的最優(yōu)解。
2、“狀態(tài)”代表什么及如何找到它?
“狀態(tài)”用來描述該問題的子問題的解。原文中有兩段作者闡述得不太清楚,跳過直接上例子。
如果我們有面值為1元、3元和5元的硬幣若干枚,如何用最少的硬幣湊夠11元? (表面上這道題可以用貪心算法,但貪心算法無法保證可以求出解,比如1元換成2元的時候)
首先我們思考一個問題,如何用最少的硬幣湊夠i元(i<11)?為什么要這么問呢? 兩個原因:1.當(dāng)我們遇到一個大問題時,總是習(xí)慣把問題的規(guī)模變小,這樣便于分析討論。 2.這個規(guī)模變小后的問題和原來的問題是同質(zhì)的,除了規(guī)模變小,其它的都是一樣的, 本質(zhì)上它還是同一個問題(規(guī)模變小后的問題其實(shí)是原問題的子問題)。
好了,讓我們從最小的i開始吧。當(dāng)i=0,即我們需要多少個硬幣來湊夠0元。 由于1,3,5都大于0,即沒有比0小的幣值,因此湊夠0元我們最少需要0個硬幣。 (這個分析很傻是不是?別著急,這個思路有利于我們理清動態(tài)規(guī)劃究竟在做些什么。) 這時候我們發(fā)現(xiàn)用一個標(biāo)記來表示這句“湊夠0元我們最少需要0個硬幣。”會比較方便, 如果一直用純文字來表述,不出一會兒你就會覺得很繞了。
那么, 我們用d(i)=j來表示湊夠i元最少需要j個硬幣。于是我們已經(jīng)得到了d(0)=0, 表示湊夠0元最小需要0個硬幣。當(dāng)i=1時,只有面值為1元的硬幣可用, 因此我們拿起一個面值為1的硬幣,接下來只需要湊夠0元即可,而這個是已經(jīng)知道答案的, 即d(0)=0。所以,d(1)=d(1-1)+1=d(0)+1=0+1=1。當(dāng)i=2時, 仍然只有面值為1的硬幣可用,于是我拿起一個面值為1的硬幣, 接下來我只需要再湊夠2-1=1元即可(記得要用最小的硬幣數(shù)量),而這個答案也已經(jīng)知道了。 所以d(2)=d(2-1)+1=d(1)+1=1+1=2。
一直到這里,你都可能會覺得,好無聊, 感覺像做小學(xué)生的題目似的。因?yàn)槲覀円恢倍贾荒懿僮髅嬷禐?的硬幣!耐心點(diǎn), 讓我們看看i=3時的情況。當(dāng)i=3時,我們能用的硬幣就有兩種了:1元的和3元的( 5元的仍然沒用,因?yàn)槟阈枰獪惖臄?shù)目是3元!5元太多了親)。 既然能用的硬幣有兩種,我就有兩種方案。如果我拿了一個1元的硬幣,我的目標(biāo)就變?yōu)榱耍?湊夠3-1=2元需要的最少硬幣數(shù)量。即d(3)=d(3-1)+1=d(2)+1=2+1=3。 這個方案說的是,我拿3個1元的硬幣;第二種方案是我拿起一個3元的硬幣, 我的目標(biāo)就變成:湊夠3-3=0元需要的最少硬幣數(shù)量。即d(3)=d(3-3)+1=d(0)+1=0+1=1. 這個方案說的是,我拿1個3元的硬幣。
好了,這兩種方案哪種更優(yōu)呢? 記得我們可是要用最少的硬幣數(shù)量來湊夠3元的。所以, 選擇d(3)=1,怎么來的呢?具體是這樣得到的:d(3)=min{d(3-1)+1, d(3-3)+1}。
從以上的文字中, 我們要抽出動態(tài)規(guī)劃里非常重要的兩個概念:狀態(tài)和狀態(tài)轉(zhuǎn)移方程。
上文中d(i)表示湊夠i元需要的最少硬幣數(shù)量,我們將它定義為該問題的”狀態(tài)”, 這個狀態(tài)是怎么找出來的呢?我在另一篇文章 動態(tài)規(guī)劃之背包問題(一)中寫過: 根據(jù)子問題定義狀態(tài)。你找到子問題,狀態(tài)也就浮出水面了。 最終我們要求解的問題,可以用這個狀態(tài)來表示:d(11),即湊夠11元最少需要多少個硬幣。
-
那狀態(tài)轉(zhuǎn)移方程是什么呢?既然我們用d(i)表示狀態(tài),那么狀態(tài)轉(zhuǎn)移方程自然包含d(i), 上文中包含狀態(tài)d(i)的方程是:d(3)=min{d(3-1)+1, d(3-3)+1}。沒錯, 它就是狀態(tài)轉(zhuǎn)移方程,描述狀態(tài)之間是如何轉(zhuǎn)移的。當(dāng)然,我們要對它抽象一下,
d(i)=min{ d(i-vj)+1 },其中i-vj >=0,vj表示第j個硬幣的面值;
-
偽代碼:
依次計算,可以計算出11元至少要3枚硬幣。
3、DP 算法步驟
動態(tài)規(guī)劃算法一般用來求解最優(yōu)化問題,當(dāng)問題有很多可行解,而題目要求尋找這些解當(dāng)中的“最大值”/“最小值”時,通常可以采用DP。
動態(tài)規(guī)劃算法與分治法相似,都是通過組合子問題的解來求解原問題。所不同的是,動態(tài)規(guī)劃應(yīng)用于子問題重疊的情況,在遞歸求解子問題的時候,一些子子問題可能是相同的,這種情況下,分治法會反復(fù)地計算同樣的子問題,而動態(tài)規(guī)劃對于相同的子問題只計算一次。
-
動態(tài)規(guī)劃算法的設(shè)計步驟:
1、刻畫最優(yōu)解的結(jié)構(gòu)特征(尋找最優(yōu)子結(jié)構(gòu)) 2、遞歸地定義最優(yōu)解的值(確定遞歸公式,動態(tài)規(guī)劃法的重點(diǎn)就是這個) 3、計算最優(yōu)解的值(有兩種方法:帶備忘錄自頂向下法、自底向上法) 4、利用計算出的信息構(gòu)造一個最優(yōu)解(通常是將具體的最優(yōu)解輸出)
最優(yōu)子結(jié)構(gòu):問題的最優(yōu)解包含的子問題的解相對于子問題而言也是最優(yōu)的。
并非所有組合優(yōu)化問題都具有最優(yōu)子結(jié)構(gòu)特性。
4、最長非降子序列(LIS)
-
問題描述:
一個序列有N個數(shù):A[1],A[2],…,A[N],求出最長非降子序列的長度。
(講DP基本都會講到的一個問題LIS:longest increasing subsequence) -
思路:
假如我們考慮求A[1],A[2],…,A[i]的最長非降子序列的長度,其中i<N, 那么上面的問題變成了原問題的一個子問題(問題規(guī)模變小了,你可以讓i=1,2,3等來分析)
然后我們定義d(i),表示前i個數(shù)中以A[i]結(jié)尾的最長非降子序列的長度。
如果我們把d(1)到d(N)都計算出來,那么最終我們要找的答案就是這里面最大的那個。
OK,狀態(tài)找到了,下一步找出狀態(tài)轉(zhuǎn)移方程。
-
可以舉個例子,然后求出前面幾項,找出遞推規(guī)律
d(i)可以用下面的狀態(tài)轉(zhuǎn)移方程得到:
d(i) = max{1, d(j)+1},其中j<i,A[j]<=A[i]
-
Python 代碼:
def LIS(A): d = [0 for i in range(len(A))] for i in range(len(A)): d[i] = 1 # 就算前面所有元素都比當(dāng)前大,那么至少可以包含自身,所以長度默認(rèn)值為1 for j in range(i): if A[i] >= A[j] and d[j]+1 > d[i]: d[i] = d[j] + 1 return max(d) A = [5, 3, 4, 8, 6, 7] print(LIS(A))
另一個解法:利用LCS思想(第9點(diǎn))
構(gòu)造一個新序列,B,它是對A進(jìn)行降序排列而得。
此時求最長非增子序列,調(diào)用LCS-LENGTH即可求出A與B的最長公共子序列,這個序列就是最后的解。
5、最大乘積子串
-
題目描述:
給一個浮點(diǎn)數(shù)序列,取最大乘積連續(xù)子串的值,例如 -2.5,4,0,3,0.5,8,-1,則取出的最大乘積連續(xù)子串為3,0.5,8。
也就是說,上述數(shù)組中,3 0.5 8這3個數(shù)的乘積30.58=12是最大的,而且是連續(xù)的。 -
思路:
因?yàn)橛姓胸?fù),下次要處理的 a[i] 可能是負(fù),所以每次都要存儲最小的子串乘積值,負(fù)負(fù)得正,得出的結(jié)果有可能還更大。
-
遞推方程:
maxend = max(max(maxend * a[i], minend * a[i]), a[i]); minend = min(min(maxend * a[i], minend * a[i]), a[i]);
-
Python 代碼:
def maxProductSubstring(a): maxend, minend, maxresult = a[0], a[0], a[0] for i in range(1,len(A)): maxend = max(max(maxend * a[i], minend * a[i]), a[i]) minend = min(min(maxend * a[i], minend * a[i]), a[i]) maxresult = max(maxend, maxresult) return maxresult A = [-2.5, 4, 0, 3, 0.5, 8, -1] print(maxProductSubstring(A))
6、Unique Paths
-
Leetcode:62. Unique Paths
A robot is located at the top-left corner of a m x n grid (marked 'Start' in the diagram below).
The robot can only move either down or right at any point in time. The robot is trying to reach the bottom-right corner of the grid (marked 'Finish' in the diagram below).
How many possible unique paths are there?
-
Solution:
class Solution(object): def uniquePaths(self, m, n): """ :type m: int :type n: int :rtype: int """ c = [[0 for i in range(n+1)] for i in range(m+1)] for i in range(n+1): c[0][i] = 0 for i in range(m+1): c[i][0] = 0 for i in range(1, m+1): for j in range(1, n+1): if i == 1 and j == 1: c[i][j] = 1 else: c[i][j] = c[i-1][j] + c[i][j-1] return c[i][j] sol = Solution() print(sol.uniquePaths(1, 1))
7、Unique Paths II
-
Leetcode:63. Unique Paths II
Follow up for "Unique Paths":
Now consider if some obstacles are added to the grids. How many unique paths would there be?
An obstacle and empty space is marked as 1 and 0 respectively in the grid.
For example,
There is one obstacle in the middle of a 3x3 grid as illustrated below.[
[0,0,0],
[0,1,0],
[0,0,0]
]The total number of unique paths is 2.
Note: m and n will be at most 100.
-
solution:
class Solution(object): def uniquePathsWithObstacles(self, obstacleGrid): """ :type obstacleGrid: List[List[int]] :rtype: int """ m = len(obstacleGrid) n = len(obstacleGrid[0]) dp = [[0 for i in range(n+1)] for i in range(m+1)] dp[1][1] = 1 for i in range(1, m+1): for j in range(1, n+1): if obstacleGrid[i-1][j-1] == 1: dp[i][j] = 0 elif i == 1 and j == 1: dp[1][1] = 1 else: dp[i][j] = dp[i-1][j] + dp[i][j-1] return dp[m][n] sol = Solution obstacleGrid = [ [0, 0, 0], [0, 1, 0], [0, 0, 0] ] print(sol.uniquePathsWithObstacles(sol, obstacleGrid))
8、Minimum Path Sum
-
Leetcode:64. Minimum Path Sum
Given a m x n grid filled with non-negative numbers,
find a path from top left to bottom right which minimizes the sum of all numbers along its path.
Note: You can only move either down or right at any point in time.
-
solution:
class Solution(object): def minPathSum(self, grid): """ :type grid: List[List[int]] :rtype: int """ m = len(grid) if m > 0 and len(grid[0]) > 0: n = len(grid[0]) else: return 0 dp = [[0 for i in range(n+1)] for i in range(m+1)] for i in range(m+1): dp[i][0] = float('inf') for i in range(n+1): dp[0][i] = float('inf') dp[1][1] = grid[0][0] for i in range(1, m+1): for j in range(1, n+1): if i == 1 and j == 1: dp[i][j] = grid[i-1][j-1] elif dp[i-1][j] < dp[i][j-1]: dp[i][j] = dp[i-1][j] + grid[i-1][j-1] else: dp[i][j] = dp[i][j-1] + grid[i-1][j-1] return dp[m][n]
9、Triangle
-
Leetcode:120. Triangle
Given a triangle, find the minimum path sum from top to bottom. Each step you may move to adjacent numbers on the row below.
For example, given the following triangle
[ [2], [3,4], [6,5,7], [4,1,8,3] ]
The minimum path sum from top to bottom is 11 (i.e., 2 + 3 + 5 + 1 = 11).
Note:
Bonus point if you are able to do this using only O(n) extra space, where n is the total number of rows in the triangle. -
第一次嘗試:
class Solution(object): def minimumTotal(self, triangle): """ :type triangle: List[List[int]] :rtype: int """ length = len(triangle) minSum = triangle[0][0] index = 0 for i in range(1, length): if triangle[i][index] < triangle[i][index+1]: minSum += triangle[i][index] else: minSum += triangle[i][index+1] index += 1 return minSum
解釋:
有點(diǎn)類似于貪心,每一步都尋找最優(yōu)的,但是卻沒想到,全局最優(yōu)可能是另外一種情況,比如下面的測試用例:Submission Result: Wrong Answer More Details Input: [[-1],[2,3],[1,-1,-3]] Output: 0 Expected: -1
很明顯,按照我的程序,走法是:-1 2 -1 = 0。
但是其實(shí)最優(yōu)解是 -1 3 -3 = -1。 -
需要輔助空間 O(n^2) 的 DP 解法:
''' extra space: O(n^2) using 'top to bottom' thought ''' def minimumTotal(self, triangle): """ :type triangle: List[List[int]] :rtype: int """ length = len(triangle) dp = [[0 for i in range(j+1)] for j in range(length)] # dp[i][j] means element of row i column j is the last one of the min_path dp[0][0] = triangle[0][0] for i in range(1, length): for j in range(i+1): if j == 0: dp[i][j] = dp[i-1][j] + triangle[i][j] elif j == i: dp[i][j] = dp[i-1][j-1] + triangle[i][j] elif dp[i-1][j] < dp[i-1][j-1]: dp[i][j] = dp[i-1][j] + triangle[i][j] else: dp[i][j] = dp[i-1][j-1] + triangle[i][j] return min(dp[length-1])
理解不難,同樣是構(gòu)建二維數(shù)組,dp[i][j] 的意思是以 triangle[i][j] 結(jié)尾的最小路徑和。這其實(shí)就是帶備忘錄的自頂向下辦法。
-
需要輔助空間 O(n) 的 DP 解法(推薦!):
''' extra space: O(n) using 'bottom to top' thought ''' def minimumTotal_bottomtotop(self, triangle): """ :type triangle: List[List[int]] :rtype: int """ length = len(triangle) dp = triangle[length-1] for i in range(length-2, -1, -1): for j in range(i+1): dp[j] = min(dp[j], dp[j+1]) + triangle[i][j] return dp[0]
這種辦法是從底往上,dp 只是一維數(shù)組,在每層操作時,僅記錄該層的各元素結(jié)尾的最小路徑和,并且路徑是從底往上。在處理不同層時,dp 根據(jù)下一層的計算結(jié)果算出當(dāng)前層的值,所以一直在變。輔助空間只有 O(n)。
10、最長公共子序列(LCS)
最長公共子序列(Longest Common Subsequence)是一個在一個序列集合中(通常為兩個序列)用來查找所有序列中最長子序列的問題。這與查找最長公共子串的問題不同的地方是:子序列不需要在原序列中占用連續(xù)的位置 。最長公共子序列問題是一個經(jīng)典的計算機(jī)科學(xué)問題,也是數(shù)據(jù)比較程序,比如Diff工具,和生物信息學(xué)應(yīng)用的基礎(chǔ)。它也被廣泛地應(yīng)用在版本控制,比如Git用來調(diào)和文件之間的改變。
-
LCS 問題可以用動態(tài)規(guī)劃思想完美解決:
設(shè)二維數(shù)組 f[i][j] 表示:數(shù)組 X 的前 i 位與數(shù)組 Y 的前 j 位的最長公共子序列的長度
-
則有:
f[i][j] = same(1,1) f[i][j] = max{f[i-1][j-1] + same(i,j), f[i-1][j], f[i][j-1]}
其中,same(a,b) 表示:當(dāng) X 的第 a 位與 Y的第 b 位完全相同時為“1”,否則為“0”。
-
綜上,可以總結(jié)出如下的算法,用B[i,j]來作標(biāo)記,
用"↖"表示序列 X 和 Y 的當(dāng)前最后兩項 x[i] 和 y[j] 相等; 用“↑”表示選擇時不考慮 x[i],即 f[i][j] = f[i-1][j]; 用“←”表示選擇時不考慮 y[j],即 f[i][j] = f[i][j-1] LCS_LENGTH(X, Y): for i := 1 to m C[i,0] := 0 for j := 1 to n C[0,j] := 0 for i := 1 to m for j := 1 to n if X[i] = Y[j] C[i,j] := C[i-1,j-1] + 1 B[i,j] := "↖" else if C[i-1,j] >= C[i, j-1] C[i,j] := C[i-1,j] B[i,j] := "↑" else C[i,j] := C[i,j-1] B[i,j] : "←"
-
為了更加直觀,這里用圖表形象給出:
-
Python 代碼(A表示↖,T表示↑,L表示←):
def LCS_LENGTH(X, Y): m = len(X) n = len(Y) c = [[0 for i in range(n+1)] for i in range(m+1)] b = [[0 for i in range(n)] for i in range(m)] for i in range(1, m + 1): for j in range(1, n + 1): if X[i-1] == Y[j-1]: c[i][j] = c[i-1][j-1] + 1 b[i-1][j-1] = "A" elif c[i-1][j] >= c[i][j-1]: c[i][j] = c[i-1][j] b[i-1][j-1] = "T" else: c[i][j] = c[i][j-1] b[i-1][j-1] = "L" return c, b def print_lcs(b, X, i, j): if i == -1 or j == -1: return if b[i][j] == "A": print_lcs(b, X, i-1, j-1) print(X[i]) elif b[i][j] == "T": print_lcs(b, X, i-1, j) else: print_lcs(b, X, i, j-1) return Y = "BDCABA" X = "ABCBDAB" c, b = LCS_LENGTH(X, Y) print_lcs(b, X, len(X)-1, len(Y)-1)
-
代碼細(xì)節(jié):
-
創(chuàng)建二維數(shù)組,并且不會出現(xiàn)引用的問題(引用問題是一改就改了多處)
c = [[0 for i in range(n+1)] for i in range(m+1)]
-
b 是輔助的一個二維數(shù)組(表),用來指示當(dāng)前的路徑選擇(走對角A,還是走上T,還是走左L),實(shí)際上,《算法導(dǎo)論》中也明確指出這里可以省略,進(jìn)而在 print_lcs 過程中做比較,這樣可以節(jié)省輔助空間,但是這樣比較淺顯易懂
2017924-LCSb -
構(gòu)造完表后,就可以調(diào)用 print_lcs 過程來得到路徑了,在過程中使用了遞歸的思想,并且最后輸出的順序是從字符串的左邊到右邊的
"C:\Program Files\Python36\python.exe" D:/PythonProject/DataStructure-Algorithm/DynamicProgramming/LCS.py B C B A Process finished with exit code 0
-
11、編輯距離
-
題目描述:
給定一個源串和目標(biāo)串,能夠?qū)υ创M(jìn)行如下操作:
- 在給定位置上插入一個字符
- 替換任意字符
- 刪除任意字符
寫一個程序,返回最小操作數(shù),使得對源串進(jìn)行這些操作后等于目標(biāo)串,源串和目標(biāo)串的長度都小于2000。
-
思路:
動態(tài)規(guī)劃,構(gòu)建二維數(shù)組,注意二維數(shù)組的第0行和第0列不是全0的。
可以想象,如果source 為空,想要轉(zhuǎn)換為 target,則肯定要執(zhí)行 len(target) = n 次操作,所以dp[i][j]賦初值時要注意這點(diǎn)。
-
遞推方程:
//dp[i,j]表示表示源串S[0…i] 和目標(biāo)串T[0…j] 的最短編輯距離 dp[i,j] = min {dp[i-1,j]+1, dp[i,j-1]+1, dp[i-1,j-1] + (s[i] == t[j] ? 0 : 1) } //分別表示:刪除1個,添加1個,替換1個(相同就不用替換)。
-
解釋:
插入是A在和B的前j-1個比,然后再在A的基礎(chǔ)上進(jìn)行插入一個字符,插入的字符是B的第j位,所以插入的代價是dp[i][j-1]+1
刪除是A的前i-1個和B的j個比,因?yàn)榘袮刪除了一個字符,所以刪除的代價是dp[i-1][j]+1
替換是A的前i-1個和B的j-1個比,然后把A的第i位變成B的第j位。所以編輯的代價是dp[i-1][j-1]+1
-
python 代碼:
def editDistance(source, target): m = len(source) n = len(target) dp = [[0 for i in range(n+1)] for i in range(m+1)] for i in range(n+1): dp[0][i] = i for i in range(m+1): dp[i][0] = i for i in range(1, m+1): for j in range(1, n+1): if source[i-1] == target[j-1]: dp[i][j] = dp[i-1][j-1] else: dp[i][j] = min(min(dp[i][j-1], dp[i-1][j]), dp[i-1][j-1]) + 1 return dp[m][n] source = "abc" target = "axxxbxxxc" print(editDistance(source, target))
12、交替字符串
Leetcode: 97. Interleaving String
-
題目描述:
輸入三個字符串s1、s2和s3,判斷第三個字符串s3是否由前兩個字符串s1和s2交錯而成,即不改變s1和s2中各個字符原有的相對順序,
例如當(dāng)s1 = “aabcc”,s2 = “dbbca”,s3 = “aadbbcbcac”時,則輸出true,但如果s3=“accabdbbca”,則輸出false。
-
思路:
多個字符串做“比較”的問題,大多都可以用DP求解。
構(gòu)建二維數(shù)組,一般其規(guī)模為:(m+1)*(n+1)。
令dp[i][j]代表s3[0...i+j-1]是否由s1[0...i-1]和s2[0...j-1]的字符組成。
自然,我們的想法是遍歷s3中的每個元素,然而要如何找到遞推關(guān)系呢?
因?yàn)橹恍枰敵鰐rue或false,那么我們可以只計算true的情形,其余情況全是false。
假設(shè)dp[i-1][j]為true,那么dp[i][j]為true的條件就是s1[i-1]是否等于s3[i+j-1]。
假設(shè)dp[i][j-1]為true,那么dp[i][j]為true的條件就是s2[j-1]是否等于s3[i+j-1]。 -
由此遞推關(guān)系就可以求出:
dp[i][j]= (dp[i][j-1] && str2[j-1]==str3[i+j-1]) || (dp[i-1][j] && str1[i-1]==str3[i+j-1])
-
Python 代碼:
def isInterleave(s1, s2, s3): m = len(s1) n = len(s2) k = len(s3) if k != m + n: return False dp = [[False for i in range(n + 1)] for i in range(m + 1)] dp[0][0] = True # if s1[0] == s3[0]: # dp[1][0] = True # if s2[0] == s3[0]: # dp[0][1] = True for i in range(m+1): for j in range(n+1): if i != 0 or j != 0: if dp[i-1][j] is True and s1[i-1] == s3[i+j-1]: dp[i][j] = True elif dp[i][j-1] is True and s2[j-1] == s3[i+j-1]: dp[i][j] = True else: dp[i][j] = False return dp[i][j] s1 = "xyz" s2 = "abc" s3 = "xyzabc" print(isInterleave(s1, s2, s3))
13、矩陣鏈乘積
矩陣鏈乘積(英語:Matrix chain multiplication,或Matrix Chain Ordering Problem,MOCP)是可用動態(tài)規(guī)劃解決的最佳化問題。給定一序列矩陣,期望求出相乘這些矩陣的最有效方法。此問題并不是真的去執(zhí)行其乘法,而只是決定執(zhí)行乘法的順序而已。
-
因?yàn)榫仃嚦朔ň哂薪Y(jié)合律,所有其運(yùn)算順序有很多種選擇。換句話說,不論如何括號其乘積,最后結(jié)果都會是一樣的。例如,若有四個矩陣ABCD,將可以有:
ABCD = (AB)(CD) = A(BC)D = AB(CD) ...
但括號其乘積的順序是會影響到需計算乘積所需簡單算術(shù)的數(shù)目,假定各矩陣維數(shù)分別為 10x100 100x5 5x50,如果按 ((AB)C) 的加括號方式,要執(zhí)行7500次標(biāo)量乘法,而按(A(BC))的加括號方式,要執(zhí)行75000次標(biāo)量乘法。
那要如何決定n個矩陣相乘的最佳順序呢?可以比較每一順序的運(yùn)算量(使用蠻力),但這將需要時間O(2^n),是一種非常慢且對大n不實(shí)在的方法。那解決方法,如我們將看到的,是將問題分成一套相關(guān)的子問題。以解答子問題一次而再使用其解答數(shù)次,即可以徹底地得出其所需時間。此一方法稱為動態(tài)規(guī)劃。
-
動態(tài)規(guī)劃算法步驟:
- 首先思考:若只有兩個矩陣相乘,則只會有一種方法去乘它們,所有其最小成本為乘積的成本,那么接下來可以按照如下方法計算。
- 取得矩陣的序列且將其分成兩個子序列。
- 找出乘完每一子序列的最小成本。
- 將成本加起來,并加上兩個結(jié)果矩陣相乘的成本。
- 在每一矩陣序列可分開的位置運(yùn)作,并取其最小值。
-
總結(jié)成狀態(tài)轉(zhuǎn)移方程即為:
m[i,j] = 0 i=j m[i,j] = min{m[i,k]+m[k+1,j]+Pi-1PkPj} i<j
m[i,j] 為 A1A2...Aj 的最優(yōu)完全加括號所需的最少標(biāo)量乘法次數(shù),對于整個問題來說,計算 A1...n 的最節(jié)省方式的代價自然應(yīng)為 m[1,n]
Pi-1PkPj 是計算 Ai...k 和 Ak+1...j 的積所消耗的時間
-
偽代碼:
Matrix-Chain-Order(int p[]) { n = p.length - 1; for (i = 1; i <= n; i++) m[i,i] = 0; for (l=2; l<=n; l++) { // l is chain length for (i=1; i<=n-l+1; i++) { j = i+l-1; m[i,j] = MAXINT; for (k=i; k<=j-1; k++) { q = m[i,k] + m[k+1,j] + p[i-1]*p[k]*p[j];//Matrix Ai has the dimension p[i-1] x p[i]. if (q < m[i,j]) { m[i,j] = q; s[i,j] = k; } } } } }
- 稍加考察,可以知道運(yùn)行時間為 Θ(n^3),輔助空間為 Θ(n^2)
上述過程除了確定最少標(biāo)量乘法數(shù),還計算了用于構(gòu)造最優(yōu)解的表格 s[1..n,1..n] 。每一項s[i,j]記錄了AiAi+1...Aj的最佳加括號的在Ak和Ak+1之間分裂的值k,于是我們知道最終矩陣積A1..n的最佳計算是A1..s[1,n] As[1,n]+1..n。
-
于是可以有打印過程:
PRINT-OPTIMAL-PARENS(s,i,j) if i = j then print "A"i else print "(" PRINT-OPTIMAL-PARENS(s,i,s[i,j]) PRINT-OPTIMAL-PARENS(s,s[i,j]+1,j) print ")"