關於動態規劃的個人理解與感悟

簡單來說,就是把大問題化爲小問題,找到小問題的解決方案,合成大問題的解決方案,記錄小問題的最優解決方案,以便於回溯尋找,降低時間複雜度。

通過例子來理解:

揹包問題:

有n個物品,它們有各自的體積和價值,現有給定容量的揹包,如何讓揹包裏裝入的物品具有最大的價值總和?

爲方便講解和理解,下面講述的例子均先用具體的數字代入,即:eg:number=4,capacity=8

i(物品編號) 1 2 3 4
w(體積) 2 3 4 5
v(價值) 3 4 5 6

思路:

根據動態規劃解題步驟(問題抽象化、建立模型、尋找約束條件、判斷是否滿足最優性原理、找大問題與小問題的遞推關係式、填表、尋找解組成)找出01揹包問題的最優解以及解組成,然後編寫代碼實現。

動態規劃的含義:

動態規劃與分治法類似,都是把大問題拆分成小問題,通過尋找大問題與小問題的遞推關係,解決一個個小問題,最終達到解決原問題的效果。但不同的是,分治法在子問題和子子問題等上被重複計算了很多次,而動態規劃則具有記憶性,通過填寫表把所有已經解決的子問題答案紀錄下來,在新問題裏需要用到的子問題可以直接提取,避免了重複計算,從而節約了時間,所以在問題滿足最優性原理之後,用動態規劃解決問題的核心就在於填表,表填寫完畢,最優解也就找到。

最優性原理是動態規劃的基礎,最優性原理是指“多階段決策過程的最優決策序列具有這樣的性質:不論初始狀態和初始決策如何,對於前面決策所造成的某一狀態而言,其後各階段的決策序列必須構成最優策略”。
 

解決過程:

在解決問題之前,爲描述方便,首先定義一些變量:Vi表示第 i 個物品的價值,Wi表示第 i 個物品的體積,定義V(i,j):當前揹包容量 j,前 i 個物品最佳組合對應的價值,同時揹包問題抽象化(X1,X2,…,Xn,其中 Xi 取0或1,表示第 i 個物品選或不選)。

1、建立模型,即求max(V1X1+V2X2+…+VnXn);

2、尋找約束條件,W1X1+W2X2+…+WnXn<capacity;

3、尋找遞推關係式,面對當前商品有兩種可能性:

包的容量比該商品體積小,裝不下,此時的價值與前i-1個的價值是一樣的,即V(i,j)=V(i-1,j);
還有足夠的容量可以裝該商品,但裝了也不一定達到當前最優價值,所以在裝與不裝之間選擇最優的一個,即V(i,j)=max{V(i-1,j),V(i-1,j-w(i))+v(i)}。
其中V(i-1,j)表示不裝,V(i-1,j-w(i))+v(i) 表示裝了第i個商品,揹包容量減少w(i),但價值增加了v(i);

由此可以得出遞推關係式:

j<w(i)      V(i,j)=V(i-1,j)
j>=w(i)     V(i,j)=max{V(i-1,j),V(i-1,j-w(i))+v(i)}

每次都有兩種狀態,第一種是第i件商品沒有裝進去,第二種是第i件商品裝進去了。沒裝進去就是沒用理會這個物品了,但裝進去了爲什麼還要和前一個狀態比較呢?這個就是關鍵了,裝進了第i個物品,揹包的剩餘容量就改變了,所以之前的狀態也要重新更新,得到的價值不一定比沒裝進去的大。正所謂牽一髮而動全身!!正因爲有子問題的最優解記錄,所以可以一步一步更新!

4、填表,首先初始化邊界條件,V(0,j)=V(i,0)=0;

然後一行一行的填表:

如,i=1,j=1,w(1)=2,v(1)=3,有j<w(1),故V(1,1)=V(1-1,1)=0;
又如i=1,j=2,w(1)=2,v(1)=3,有j=w(1),故V(1,2)=max{ V(1-1,2),V(1-1,2-w(1))+v(1) }=max{0,0+3}=3;
如此下去,填到最後一個,i=4,j=8,w(4)=5,v(4)=6,有j>w(4),故V(4,8)=max{ V(4-1,8),V(4-1,8-w(4))+v(4) }=max{9,4+6}=10……

#include<iostream>
using namespace std;
#include <algorithm>

int main()
{
	int w[5] = { 0 , 2 , 3 , 4 , 5 };			//商品的體積2、3、4、5
	int v[5] = { 0 , 3 , 4 , 5 , 6 };			//商品的價值3、4、5、6
	int bagV = 8;					        //揹包大小
	int dp[5][9] = { { 0 } };			        //動態規劃表
	//memset(dp,0,sizeof(dp));可以換成這個語句
	for (int i = 1; i <= 4; i++) {
		for (int j = 1; j <= bagV; j++) {
			if (j < w[i])
				dp[i][j] = dp[i - 1][j];//裝不下這個物品的時候
			else
				dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);//裝的下的時候
		}
	}

	//動態規劃表的輸出
	for (int i = 0; i < 5; i++) {
		for (int j = 0; j < 9; j++) {
			cout << dp[i][j] << ' ';
		}
		cout << endl;
	}
	system("pause");
	return 0;
}

揹包問題最優解回溯

V(i,j)=V(i-1,j)時,說明沒有選擇第i 個商品,則回到V(i-1,j);
V(i,j)=V(i-1,j-w(i))+v(i)時,說明裝了第i個商品,該商品是最優解組成的一部分,隨後我們得回到裝該商品之前,即回到V(i-1,j-w(i));
一直遍歷到i=0結束爲止,所有解的組成都會找到。

最優解爲V(4,8)=10,而V(4,8)!=V(3,8)卻有V(4,8)=V(3,8-w(4))+v(4)=V(3,3)+6=4+6=10,所以第4件商品被選中,並且回到V(3,8-w(4))=V(3,3);
有V(3,3)=V(2,3)=4,所以第3件商品沒被選擇,回到V(2,3);
而V(2,3)!=V(1,3)卻有V(2,3)=V(1,3-w(2))+v(2)=V(1,0)+4=0+4=4,所以第2件商品被選中,並且回到V(1,3-w(2))=V(1,0);
有V(1,0)=V(0,0)=0,所以第1件商品沒被選擇。

#include<iostream>
using namespace std;
#include <algorithm>

int w[5] = { 0 , 2 , 3 , 4 , 5 };			//商品的體積2、3、4、5
int v[5] = { 0 , 3 , 4 , 5 , 6 };			//商品的價值3、4、5、6
int bagV = 8;					        //揹包大小
int dp[5][9] = { { 0 } };			        //動態規劃表
int item[5];					        //最優解情況

void findMax() {					//動態規劃
	for (int i = 1; i <= 4; i++) {
		for (int j = 1; j <= bagV; j++) {
			if (j < w[i])
				dp[i][j] = dp[i - 1][j];
			else
				dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
		}
	}
}

void findWhat(int i, int j) {				//最優解情況
	if (i >= 0) {
		if (dp[i][j] == dp[i - 1][j])//沒選中的情況
		{
			item[i] = 0;
			findWhat(i - 1, j);
		}
		else if (j - w[i] >= 0 && dp[i][j] == dp[i - 1][j - w[i]] + v[i]) 
		{
			item[i] = 1;
			findWhat(i - 1, j - w[i]);
		}
	}
}

void print() {
	for (int i = 0; i < 5; i++) {			//動態規劃表輸出
		for (int j = 0; j < 9; j++) {
			cout << dp[i][j] << ' ';
		}
		cout << endl;
	}
	cout << endl;

	for (int i = 0; i < 5; i++)			//最優解輸出
		cout << item[i] << ' ';
	cout << endl;
}

int main()
{
	findMax();
	findWhat(4, 8);
	print();
	system("pause");
	return 0;
}

接下來再來用一到金字塔的題來理解動態規劃:

數字三角形(POJ1163)    

在上面的數字三角形中尋找一條從頂部到底邊的路徑,使得路徑上所經過的數字之和最大。路徑上的每一步都只能往左下或 右下走。只需要求出這個最大和即可,不必給出具體路徑。 三角形的行數大於1小於等於100,數字爲 0 - 99

    輸入格式:

    5      //表示三角形的行數    接下來輸入三角形

    7

    3   8

    8   1   0

    2   7   4   4

    4   5   2   6   5

    要求輸出最大和

思路:

   首先,肯定得用二維數組來存放數字三角形,

    然後我們用D( r, j) 來表示第r行第 j 個數字(r,j從1開始算)

    我們用MaxSum(r, j)表示從D(r,j)到底邊的各條路徑中,最佳路徑的數字之和。

    因此,此題的最終問題就變成了求 MaxSum(1,1)

    當我們看到這個題目的時候,首先想到的就是可以用簡單的遞歸來解題:

    D(r, j)出發,下一步只能走D(r+1,j)或者D(r+1, j+1)。故對於N行的三角形,我們可以寫出如下的遞歸式:   

if ( r == N)                
	MaxSum(r,j) = D(r,j)  
else      
	MaxSum( r, j) = Max{ MaxSum(r+1,j), MaxSum(r+1,j+1) } + D(r,j) 

完整代碼:

#include <iostream>  
#include <algorithm> 
#define MAX 101  
using namespace std; 
int D[MAX][MAX];  
int n;  
int MaxSum(int i, int j){    
	if(i==n)  
		return D[i][j];    
	int x = MaxSum(i+1,j);    
	int y = MaxSum(i+1,j+1);    
	return max(x,y)+D[i][j];  
}
int main(){    
	int i,j;    
	cin >> n;    
	for(i=1;i<=n;i++)   
		for(j=1;j<=i;j++)        
			cin >> D[i][j];    
	cout << MaxSum(1,1) << endl;  
}      

 但這樣肯定會超時,因爲大量重複計算了子問題,所以我們要把子問題的答案記錄下來。

就拿第三行數字1來說,當我們計算從第2行的數字3開始的MaxSum時會計算出從1開始的MaxSum,當我們計算從第二行的數字8開始的MaxSum的時候又會計算一次從1開始的MaxSum,也就是說有重複計算。這樣就浪費了大量的時間。也就是說如果採用遞規的方法,深度遍歷每條路徑,存在大量重複計算。則時間複雜度爲 2的n次方,對於 n = 100 行,肯定超時。 

    接下來,我們就要考慮如何進行改進,我們自然而然就可以想到如果每算出一個MaxSum(r,j)就保存起來,下次用到其值的時候直接取用,則可免去重複計算。
 

#include <iostream>  
#include <algorithm> 
using namespace std;

#define MAX 101

int D[MAX][MAX];
int n;
int maxSum[MAX][MAX];

int MaxSum(int i, int j)
{
	if (maxSum[i][j] != -1)//已經記錄過的子問題答案就直接輸出,不用再計算一次
		return maxSum[i][j];
	if (i == n)
		maxSum[i][j] = D[i][j];//找到了就賦值記錄
	else {
		int x = MaxSum(i + 1, j);
		int y = MaxSum(i + 1, j + 1);
		maxSum[i][j] = max(x, y) + D[i][j];
	}
	return maxSum[i][j];
}
int main() {
	int i, j;
	cin >> n;
	for (i = 1; i <= n; i++)
		for (j = 1; j <= i; j++) {
			cin >> D[i][j];
			maxSum[i][j] = -1;
		}
	cout << MaxSum(1, 1) << endl;
	system("pause");
	return 0;
}

這樣就開始運用到了動態規劃的思想了,就是記錄子問題的答案!

答案就可以通過測試了。

 

但真正的動態規劃更加簡潔,由小到大,由子問題到大問題,由底層到頂層,運用遞推的方式而不是遞歸的方式

我們首先需要計算的是最後一行,因此可以把最後一行直接寫出,如下圖:

    

    現在開始分析倒數第二行的每一個數,現分析數字2,2可以和最後一行4相加,也可以和最後一行的5相加,但是很顯然和5相加要更大一點,結果爲7,我們此時就可以將7保存起來,然後分析數字7,7可以和最後一行的5相加,也可以和最後一行的2相加,很顯然和5相加更大,結果爲12,因此我們將12保存起來。以此類推。。我們可以得到下面這張圖:

    

    然後按同樣的道理分析倒數第三行和倒數第四行,最後分析第一行,我們可以依次得到如下結果:

    

    

 

    上面的推導過程相信大家不難理解,理解之後我們就可以寫出如下的遞推型動態規劃程序: 

#include <iostream>  
#include <algorithm> 
using namespace std;

#define MAX 101  

int D[MAX][MAX];
int n;
int maxSum[MAX][MAX];
int main() {
	int i, j;
	cin >> n;
	for (i = 1; i <= n; i++)
		for (j = 1; j <= i; j++)
			cin >> D[i][j];
	for (int i = 1; i <= n; ++i)
		maxSum[n][i] = D[n][i];//最後一行的值
	for (int i = n - 1; i >= 1; --i)//從倒數第二行開始
		for (int j = 1; j <= i; ++j)
			maxSum[i][j] = max(maxSum[i + 1][j], maxSum[i + 1][j + 1]) + D[i][j];//自底而上
	cout << maxSum[1][1] << endl;
	system("pause");
	return 0;
}

我們再來優化一下,減少空間複雜度,時間複雜度不變

進一步優化空間,用一行來記錄即可

進一步考慮,我們甚至可以連maxSum數組都可以不要,直接用D的第n行直接替代maxSum即可。但是這裏需要強調的是:雖然節省空間,但是時間複雜度還是不變的。

    依照上面的方式,我們可以寫出如下代碼:

    

#include <iostream>  
#include <algorithm> 
using namespace std; 
 
#define MAX 101  
 
int D[MAX][MAX];  
int n; 
int * maxSum; 
 
int main(){    
	int i,j;    
	cin >> n;    
	for(i=1;i<=n;i++)   
		for(j=1;j<=i;j++)        
			cin >> D[i][j];   
	maxSum = D[n]; //maxSum指向第n行    
	for( int i = n-1; i>= 1;  --i )     
		for( int j = 1; j <= i; ++j )       
			maxSum[j] = max(maxSum[j],maxSum[j+1]) + D[i][j];    
	cout << maxSum[1] << endl;  
}

僅用來幫助自己學習和理解~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章