動態規划
動態規划算法與分治法類似,其基本思想也是將待求解問題分解成若干子問題,先求解子問題,然后從這些子問題的解得到原問題的解。與分治法不同的是,適用於動態規划法求解的問題,經分解得到的子問題往往不是相互獨立的。在用分治法求解的時候,有些子問題被重復計算了許多次。如果能夠保存已解決的子問題的答案,而在需要的時候再找出已求得的答案,就可以避免大量重復計算,從而得到多項式時間的算法。
動態規划算法適用於解最優化問題,通常可以按以下步驟設計的動態規划算法:
- 找出最優解的性質,並刻畫其結果特征。
- 遞歸地定義最優值。
- 以自底向上的方式計算出最優值。
- 根據計算最優值時得到的信息,構造最優解。
最大子段和
先來看一下最大子段和用分治法思想的分析:
如果將所給的序列a[1 : n]分為長度相同的兩段a[1 : n/2]、a[n/2+1 : n],分別求出這兩段的最大子段和,則a[1 : n]的最大子段和分為三種情況:
- a[1 : n]的最大子段和同a[1 : n/2]的最大子段和相同。
- a[1 : n]的最大子段和同a[n/2 : n]的最大子段和相同。
- a[1 : n]的最大子段和等於位於a[1 : n/2]的子段和和位於a[n/2+1 : n]的子段和的和
1和2的情況可以直接遞歸求得,第3種情況,a[n/2]和a[n/2+1]在最優子序列之中,我們只需要計算出左邊a[1 : n/2]從n/2開始的最大子段和s1,右邊a[ n/2+1 : n]從n/2+1開始的最大子段和,然后將兩者相加s=s1+s2;s即為第3種情況的最優值。
算法思想
從上述分治法思想注意到,我們可以記b[ j ]為1 ~ j 中的最大子段和,其中j∈[1,n];
這樣,那么所求的1 ~ n 中的最大子段和就為 i~j 的和可以表示為:
意思就是,j從1到n,依次找到最大子段和。
由b[ j ]的定義易知,當b[ j-1 ]>0 時b[ j ]= b[ j-1 ] +a[j],否則 b[ j ]=a[ j ]。 由此可得計算b[j]的動態規划遞歸式:
b[j] = max {b[j-1] +a[j], a[j] } ,1≤j ≤ n
當前的最優解,就等於前一個最優解加上當前值和當前值中的較大者。
偽代碼
public static int MaxSum(int []a,int n)
{
int sum=0;
int b=0;
for(int i=0;i<n;i++)
{
if(b>0)
b+=a[i];
else
b=a[i];
if(b>sum)
sum=b;
}
return sum;
}
矩陣連乘問題
給定n個矩陣{A1,A2......An} , 其中Ai與Ai+1可乘,i=1,2,3...n-1 。 如何確定計算矩陣連乘積的計算次序,使得依此次序計算矩陣連乘積需要的數乘次數最少。
考察這n個矩陣的連乘積 A1A2...An
由於矩陣乘法滿足結合律,所以計算矩陣的連乘可以有許多不同的計算次序。這種計算次序可以用加括號的方式來確定。
若一個矩陣連乘積的計算次序完全確定,也就是說該連乘積已完全加括號,則可以依此次序反復調用2個矩陣相乘的標准算法計算出矩陣連乘積。
基本思想
將矩陣連乘積 AiAi+1...Aj簡記為A[i:j] ,這里i ≤ j 考察計算A[i:j]的最優計算次序。設這個計算次序在矩陣 Ak和Ak+1之間將矩陣鏈斷開,i≤k<j,則其相應完全加括號方式為
計算量:A[i:k]的計算量加上A[k+1:j]的計算量,再加上 A[i:k]和A[k+1:j]相乘的計算量。
最優解結構:
-
特征:計算A[i:j]的最優次序所包含的計算矩陣子 鏈 A[i:k]和A[k+1:j]的次序也是最優的。
-
矩陣連乘計算次序問題的最優解包含着其子問題的最優解。這種性質稱為最優子結構性質。問題的最優子結構性質是該問題可用動態規划算法求解的顯著特征。
建立遞歸關系:
-
設計算A[i:j],1≤i≤j≤n,所需要的最少數乘次數m[i,j],則原問題的最優值為m[1,n]
-
當i=j時,A[i:j]=Ai,因此,m[i,i]=0,i=1,2,…,n
-
當i<j時,m[i , j]=m[i , k] + m[k+1 , j] + pi-1 * pk * pj ,這里Ai的維數為pi-1 * pi
-
遞歸的定義m[i , j]為:
k的位置只有j-i種可能。
計算的時候是斜着對角進行計算的,這樣再計算下一輪的時候就可以用到前面已經求得的值。
計算過程:
比如m[2] [5]就可以表示為:
偽代碼
public void matrixChain(int []p,int n,int [][]m,int [][]s)
{
//初始化對角線全為0 因為當i=j時,A[i:j]=Ai,因此,m[i,i]=0
for(int i=1;i<=n;i++) m[i][i]=0;
for(int r=2;r<=n;r++)
for(int i=1;i<=n-r+1;i++)
{
int j=i+r-1;
//這是把 m[i][j]分成i->i i+1->j的情形,m[i][i]=0
//m[i , j]=m[i , k] + m[k+1 , j] + pi-1 * pk * pj
m[i][j]= m[i+1][j]+ p[i-1]*p[i]*p[j];
s[i][j] = i;
//這里就是除了i->i i+1->j的情形以外的情況,要找到最小值
for (int k = i+1; k < j; k++) {
int t = m[i][k] + m[k+1][j] + p[i-1]*p[k]*p[j];
if (t < m[i][j])
{
m[i][j] = t;
s[i][j] = k;
}
}
}
}
算法復雜度分析: 算法matrixChain的主要計算量取決於算法中對r,i 和 k 的3重循環。循環體內的計算量為O(1),而3重循環的總次數為O(n^3 )。因此算法的計算時間上界為O(n^3 )。算法所占用的空間顯然為O(n^2 )。
最長公共子序列問題
若給定序列X={x1 ,x2 ,…,xm},則另一序列 Z={z1 ,z2 ,…,zk },是X的子序列是指存在一個嚴格遞增 下標序列{i1 ,i2 ,…,ik }使得對於所有j=1,2,…,k有:zj=xi。 例如,序列Z={B,C,D,B}是序列X={A,B,C,B, D,A,B}的子序列,相應的遞增下標序列為{2,3,5, 7}。
給定2個序列X和Y,當另一序列Z既是X的子序列又是 Y的子序列時,稱Z是序列X和Y的公共子序列。
給定2個序列X={x1,x2,…,xm}和Y={y1,y2,…,yn},找 出X和Y的最長公共子序列。
基本思想
設序列X={x1 ,x2 ,…,xm}和Y={y1 ,y2 ,…,yn }的最長公共子序列為 Z={z1 ,z2 ,…,zk } ,則
- 若xm==yn,則zk=xm=yn。X與Y的最長公共子序列為{x1 ,x2 ,…,x(m-1)},{y1 ,y2 ,…,y(n-1) }的最長公共子序列+xm
- 若xm!=yn且zk!=xm,則X與Y的最長公共子序列為{x1 ,x2 ,…,x(m-1)},{y1 ,y2 ,…,yn }的最長公共子序列
- 若xm!=yn且zk!=yn,則X與Y的最長公共子序列為{x1 ,x2 ,…,xm},{y1 ,y2 ,…,y(n-1) }的最長公共子序列
由此可見,2個序列的最長公共子序列包含了這2個序列的前綴 的最長公共子序列。因此,最長公共子序列問題具有最優子結 構性質。
由最長公共子序列問題的最優子結構性質建立子問題最優值的遞歸關系。用c[i] [j]記錄序列和的最長公共子序列的長度。其中,Xi={x1 ,x2 ,…,xi };Yj={y1 ,y2 ,…,yj }。當i=0或j=0時,空序列是Xi和Yj的最長公共子序列。故此時C[i] [j]=0。其它情況下,由最優子結構性質可建立遞歸關系如下:
Algorithms和alchemist:
i=0和j=0,則c[i] [j]為0;然后,首先X1 A 和Y1 A相同,那么就取左上方c[i-1] [j-1]+1 ;接下來,X1 A和Y2 L不同,則c[i] [j] 就等於左邊c[i] [j-1] 和 上邊 c[i-1] [j] 中的較大者。
偽代碼
public void LCSLength(int m,int n,char []x,char []y,int [][]c,int [][]b)
{
int i=1;
int j=1;
//i=0或者j=0,空序列為最長公共子序列,因此c[i][j]=0
for(i=1;i<=m;i++)
c[i][0]=0;
for(i=1;i<=n;i++)
c[0][i]=0;
for (i = 1; i <= m; i++)
for (j = 1; j <= n; j++)
{
if (x[i]==y[j])
{
c[i][j]=c[i-1][j-1]+1;
//記錄
b[i][j]=1;
}
else if (c[i-1][j]>=c[i][j-1])
{
c[i][j]=c[i-1][j];
b[i][j]=2;
}
else
{
c[i][j]=c[i][j-1];
b[i][j]=3;
}
}
}
//構造最長公共子序列
public void LCS(int i,int j,char []x,int []b)
{
if (i ==0 || j==0)
return;
if (b[i][j]== 1)
{
LCS(i-1,j-1,x,b);
System.Out.print(x[i]+" ");
}
else if (b[i][j]== 2)
LCS(i-1,j,x,b);
else LCS(i,j-1,x,b);
}
算法LCSLength耗時O(m*n),算法LCS的計算時間為O(m+n)。
0-1背包問題
給定n種物品和一背包。物品i的重量是wi,其價值為vi,背包的容量為C。問應如何選擇裝入背包的物品,使得裝入背包中物品的總價值最大?
設所給0-1背包問題的子問題
的最優值為m[i] [j],即m(i,j)是背包容量為j,可選擇物品為i,i+1,...,n時0-1背包問題的最優值。
由0-1背包問題的最優子結構性質,可以建立如下計算m(i,j)的遞歸式:
遞歸式的意思就是:如果當前背包容量小於當前物品的重量,那么就是不能裝下,這樣就等於下一個到m(i+1 , j)。如果當前背包容量能大於當前物品的重量,那么就是能裝下,這樣就只需要比較裝下該物品(如果選擇裝下該物品,那么前面物品的總價值就會被壓縮,因為該物品占了重量所以要去找m(i+1 , j-wi)然后再加上剛裝下物品的價值)或者不裝該物品,哪一個得到的價值更大。
偽代碼
public void knapsack(int []v,int []w,int c,int [][]m)
{
int n=v.length-1;
for(int i=0;i<c;i++)
m[0][i]=0;
for(int i=0;i<n;i++)
m[i][0]=0
for(int i=1;i<n;i++)
for(int j=1;j>=c;j++)
{
if(j>=w[i])
m[i][j]=max(m[i-1][j],m[i-1][j-w[i]]+v[i]);
else
m[i][j]=m[i-1][j];
}
}
這里用m[i][j]=max(m[i-1] [j],m[i-1] [j-w[i]]+v[i]);同上方i+1意思是一樣的,只不過初始化不同。
時間復雜度O(nc);