1. 程式人生 > >經典動態規劃問題總結

經典動態規劃問題總結

動態規劃引入

首先我們以一個最基本的例子來分析——菲波那切數列。

我們都知道,菲波那切數列的遞推公式f(n) = f(n-1)+f(n-2) (這裡我就說明一般情況,不列舉邊界條件了),很簡單,如果我們用遞迴的方法來求解f(n),兩三行程式碼就出來了。那麼我們深入分析一下這樣有什麼問題?

f(2) = f(1) + f(0);

f(3) = f(2) + f(1);

f(4) = f(3) + f(2);

f(5) = f(4) + f(3);

......

計算一個f(5)我們需要計算一個f(4)和一個f(3),而一個f(4)又需要一個f(3)和一個f(2),這其中就有了一個重複的f(3),那麼在繼續往下推導,會發現有越來越多的重複。當我們在計算機中計算f(40)並輸出時,我們會發現已經有相當長時間的延時了,為什麼?因為這樣的遞迴重複計算太多了,導致整個演算法效率非常低。

由於上述過程存在著大量的重複計算,我們可以用一個數組儲存所有已經計算過的項,這樣便可以達到用空間換時間的目的,在這種情況下,時間複雜度為O(N),而空間複雜度也為O(N)。事實上,我們所述的這種演算法就是利用了動態規劃的思想。

動態規劃演算法的思想與分治法類似,也是通過組合子問題的解而解決整個問題。其基本思路是利用一個表來記錄所有已解的子問題的答案,不管該子問題以後是否被用到,只要它被計算過,就將結果填入表中,這樣就可以避免重複計算問題。

動態規劃演算法的設計可以分為如下幾個步驟:

1)描述最優解的結構;

2)遞迴定義最優解的值;

3)按自底向上的方式計算最優解的值;

4)由計算出的結果構造一個最優解;

其中第1~3步構成問題的動態規劃解的基礎。

適合動態規劃方法的最優化問題的兩個要素:最優子結構,重疊子問題

例1:求用1*2的瓷磚覆蓋2*M的地板有幾種方式?

分析:假設所求問題的解為F(M),有下面兩種情況:


當第一塊瓷磚豎著放的時候,問題轉換成求用1*2的瓷磚覆蓋剩下的2*(M-1)的方式,即F(M-1)。

當第一塊瓷磚橫著放的時候,則必有另一塊瓷磚橫著放在其下面,問題轉換成求用1*2的瓷磚覆蓋 剩下的2*(M-2)的方式,即F(M-2)。

在求F(M-1)和F(M-2)時,由於第一列地板的覆蓋方式已經不同,故F(M-1)種覆蓋方式和F(M-2)中覆蓋方式沒有重疊,故:

F(M) = F(M-1)+F(M-2)

其中,F(1) = 1,F(2) = 2。可見我們能夠將問題規模縮小。

仔細看遞推式,其實就和菲波那切數列是一樣的,既然這樣,我們就不要用上面的遞迴方式進行求解F(M)了,而是用動態規劃的方式,建立一個表格,儲存每一步驟的F(i).

//...
int a[100];
int func(int M){}
	a[1] = 1;
	a[2] = 2;
	for(int i = 3; i <= M; ++i)
		a[i] = a[i-1] + a[i-2];
	return a[M];
}
//...

例2:LCS(最長公共子序列問題)

注:LCS問題不要求所求得的字元在所給的字串中是連續的。

分析:假設X={x1,x2,...,xm}和Y={y1,y2,...,yn}的一個最長公共子序列為Z={z1,z2,...,zk},

1)若x(m) = y(n),則必然有z(k) = x(m) = y(n),且Z(k-1)是X(m-1)和Y(n-1)的最長公共子序列;

2)若x(m) != y(n)且z(k) != x(m),則Z是X(m-1)和Y的最長公共子序列;

3)若x(m) != y(n) 且z(k) != y(n),則Z是X和Y(n-1)的最長公共子序列;

也就是說,

當x(m) = y(n)時,LCS(X(m),Y(n)) = LCS(X(m-1),Y(n-1)) + 1;

x(m) != y(n)時,LCS(X(m),Y(n)) = max{LCS(X(m-1),Y(n)), LCS(X(m),Y(n-1))};

若用一個二維表格c來儲存LCS,則c[i][j]表示X和Y長度分別為i和j時的LCS,顯然,當X或Y為空時,LCS為0.


下面給出動態規劃演算法的程式碼:

/* 動態規劃:最長公共子序列問題LCS */
const int INF = 99999;
int c[100][100];
int LCS_Memo(string A, string B, int i, int j){
	if(c[i][j] < INF)
		return c[i][j];
	if(i == 0 || j == 0)
		c[i][j] = 0;
	else if(A[i-1] == B[j-1])
		c[i][j] = c[i-1][j-1] + 1;
	else{
		int p = LCS_Memo(A, B, i-1, j);
		int q = LCS_Memo(A, B, i, j-1);
		if(p >= q)
			c[i][j] = p;
		else
			c[i][j] = q;
	}
	return c[i][j];
}

int LCS_Length(string A, string B){
	int m = A.length();
	int n = B.length();
	memset(c, INF, sizeof(c));
	return LCS_Memo(A, B, m, n);
}
下面再給出一種非遞迴的方法。
//dp[i][j]存放的是長度分別為i、j的字串A、B的LCS
int LCS(string A, string B, int m, int n){
	int dp[300][300];
	memset(dp, 0, sizeof(dp));
	
	for(int i = 1; i <= m; ++i){
		for(int j = 1; j <= n; ++j){
			if(A[i-1] == B[j-1])
				dp[i][j] = dp[i-1][j-1] + 1;
			else
				dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
		}
	}
	return dp[m][n];
}
例3: 01揹包問題:一個揹包有一定的承重cap,有n件物品,每件都有自己的價值,記錄在陣列v中,也都有自己的重量,記錄在陣列w中,每件物品只能選擇要裝入揹包還是不裝入揹包,要求在不超過揹包承重的前提下,選出物品的總價值最大,給定物品的重量w價值v及物品數n和承重cap。請返回最大總價值。

分析:這裡以行n,列cap建立二維表格dp[n+1][cap+1],其中dp[i][j]表示重量不超過j時的最大價值。那麼這裡就有兩種情況:

1)選擇第i件物品,則前i-1件物品的重量不能超過j-w[i];

2)不選擇第i件物品,則前i-1件物品的重量不能超過j;

即dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);

int getMaxValue(vector<int> w, vector<int> v, int n, int cap){
	int dp[n+1][cap+1];
	for(int i = 0; i <= n; ++i)
		dp[i][0] = 0;
	for(int j = 0; j <= cap; ++j)
		dp[0][j] = 0;
	
	for(int i = 1; i <= n; ++i){
		for(int j = 1;j <= cap; ++j){
			if(j >= w[i-1])
				dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i-1]]+v[i-1]);
			else
				dp[i][j] = dp[i-1][j];
		}
	}
}