1. 程式人生 > >所思 所學 我寫 我得

所思 所學 我寫 我得

動態規劃(DP)是一種解決複雜問題特別是主問題包含重複子問題的常用演算法思想。它將待求解問題分解成若干子問題,先求解子問題,然後再從子問題中得到原問題的解。不同於分治法,動態規劃的子問題常常不是互相獨立的,下一階段的求解建立在上一階段的解的基礎上進行的。

利用動態規劃求解問題的有效性 依賴於問題本身具有的兩個重要性質,也就是如果待解決問題具有以下兩個性質,就可以考慮使用動態規劃求解。

1 最優子結構:問題的最優解包含了其子問題的最優解。

2 重疊子問題:在求解過程中,如使用遞迴自頂向下求解問題時,每次產生子問題並不總是新的,有的被重複計算多次,故需要將子問題的解儲存下來,供以後直接使用。這種思   想又叫“記表備查

”,將求解過程中的最優值儲存在決策表中。

求斐波那契數列問題。1 2 3 5 8 13.....    f(n) = f(n - 1) + f(n - 2)   f(1) = 1, f(2) = 2

   使用遞迴如下:

int Fbnaci(int n) {
    if(n == 1)
        return 1;
    if(n == 2)
        return 2;
    if(n > 2)
        return Fbnaci(n - 1) + Fbnaci(n - 2);
}
上述程式碼雖然簡潔,但是相當費時,因為有著大量重複問題的計算。如計算f(10)需要計算f(9) 和 f(8) 計算f(9)時又需要計算f(8) .使用動態規劃的實現如下:
int Fbnaci(int n) {
    int i,*a;
    if(n==1)
        return 1;
    if(n==2)
        return 2;
    if(n>2)
    {
        a=(int *)malloc(sizeof(int)*n);
        a[0]=1;
        a[1]=2;
        for(i=2;i<n;i++)
            a[i]=a[i-1]+a[i-2];
        return a[n-1];    
    }
}
以上程式碼雖然比前面遞迴的程式碼量大,但是使用陣列儲存了中間結果,以後呼叫時不必再計算,直接使用陣列值就可以,從而大大減少了運算時間。(空間換時間)


求(LCS)最大公共子序列問題

給定兩個字串,求它們共同的最長子序列,(不一定連續)。

對於字串 X = {x1,x2,...,xm}, Y = {y1,y2,...,yn } 求 Z = {z1,z2,....zk }  其中 zi 同時在字串X和Y中,且順序保持不變。

如 X = “ABCBDAB”  Y="BDCABA "  最長公共子序列為  BCBA 和 BDAB

最優子結構性質:

1 若 x(m) = y(n) 則 z(k) =  x(m) = y(n) 且 {Zk-1} 是 {Xm-1} 和 {Yn-1}的最長公共子序列。

2  若 x(m) != y(n) 且 z(k) != x(m) 則  {Zk} 是{Xm-1}和{Yn} 的最長公共子序列

3 若 x(m) != y(n) 且 z(k) != y(n) 則  {Zk} 是{Yn-1}和{Xm} 的最長公共子序列

簡單來說,如果X和Y的最後一個元素相同,則最後一個元素肯定在Z中且是最長子序列的最後一個元素。那麼Z的長度為{Xm-1} 和 {Yn-1}的最長公共子序列的長度 + 1

否則最長公共子序列的長度為 : max{ length(Xm-1,Yn) ,  length(Xm,Yn-1)}

用 陣列 c[ i ][  j ]記錄X[ 0 ~ i ] 與 Y[ 0 ~ j ] 的最長公共子序列的長度 則:

c[i][j] =  0                                              if  i = 0, j = 0;

 =  c[i-1][j-1] + 1                     else if  xi = yj

 =  max{ c[i][j-1] ,c[i-1][j] }          else if  xi != yj

以  X = “ABCBDAB”  Y="BDCABA " 為例,求得矩陣C如下:(參考演算法導論)


其中箭頭方向表示當前子問題的解(箭尾) 由上一個子問題(箭頭)的最優解得來。

如 c[2][1] = c[1][0] +1 得來;表示i= 2 ,j = 1 時 x2 = y1 = ‘B’ ,則當前子問題最長子序列長度 = 上一子問題最長子序列長度  + 1;其餘類似。

具體程式碼如下:

// Length of LCS
int LCSLength(int m,int n,const char x[],char y[])
{
	int i,j;
	for(i = 1; i <= m; i++)//初始第一行
		c[i][0] = 0;
	for(j = 1; j <= n; j++)//初始第一列
		c[0][j] = 0;
	for(i = 1; i <= m; i++)
	{
		for(j = 1; j <= n; j++)
		{
			if(x[i-1] == y[j-1])  //相等
			{
				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;
			}
		}
	}
	return c[m][n];
} 
//根據陣列b內容列印x和y的最大公共子序列
void LCSprint(int i,int j,char x[])
{
	if(i == 0 || j == 0)
		return;
	if(b[i][j] == 1)
	{
	//	printf("%c",x[i-1]);
		LCSprint(i-1,j-1,x);
		printf("%c",x[i]);
		
	}
	else if(b[i][j] == 2)
		LCSprint(i-1,j,x);
	else
		LCSprint(i,j-1,x);
}

注意以上程式碼中字串下標從1 開始。

數塔問題:

給定數字三角形,計算從頂至底的某一條路徑使該路徑所經過的數字的總和最大。


使用二維陣列儲存上述資料

7

3 8

8 1 0

2 7 4 4

逆向思考,從倒數第二層算起,分別計算每一層所有元素可能達到的最大值,從下往上進行,到第一行時就得到路徑最大值。

狀態轉移方程:C[ i ] [ j ]   + =  max{ C[ i+1] [ j ] , C[ i+1] [ j +1]  }