五個常用演算法(一):動態規劃
1.從01揹包問題說起
有一堆寶石一共n個,現在你身上能裝寶石的就只有一個揹包,揹包的容量為C。把n個寶石排成一排並編上號: 0,1,2,…,n-1。第i個寶石對應的體積和價值分別為V[i]和W[i] 。揹包總共也就只能裝下體積為C的東西,那你要裝下哪些寶石才能獲得最大的利益呢?
我們先來看下在這個問題裡,動態規劃最重要的兩個概念:狀態和狀態轉移方程。
假如現在有3個寶石,體積分別為5,4,3;對應的價值為20,10,12;揹包的體積是10。這個問題解容易看出是第0、2個寶石,總價值是22。先考慮第2個寶石,有放入和不放入兩種選擇,選擇放入的話,揹包剩下空間7,問題變成用空間7的揹包裝前2個寶石(最優子結構
程式碼實現的話是這樣:
解法的時間複雜度、空間複雜度都是O(nC);for(int i=0;i<n;i++){ for(int j=0;j<C;j++){ if(i==0) d[i][j]=0; else{ if(j>W[i]&&V[i]+d[i-1][j-W[i]]>d[i-1][j]) d[i][j]=V[i]+d[i-1][j-W[i]]; else d[i][j]=d[i-1][j]; } } }
時間複雜度沒法優化了,可以看出陣列d的更新只用到了i-1的值,所以我們只有必要儲存陣列d[i-1],空間複雜度可以優化一下。
程式碼如下(注意因為更新j分量需要用到比j小的數值,所以按j遞減的順序更新):
memset(d, 0, sizeof(d));
for(int i=0; i<=n; ++i){
for(int j=C;j>=0; --j){
if(j>=V [i]&& i>0) d[j] >?= d[j-V[i]]+W[i];
}
}
最後貼一個完整程式碼/**0-1 knapsack d(i, j)表示前i個物品裝到剩餘容量為j的揹包中的最大重量**/ #include<cstdio> #include<cstdlib> #include<cstring> using namespace std; int main(){ freopen("data.in", "r", stdin); freopen("data.out", "w", stdout); int n, C, V = 0, W = 0; while(scanf("%d %d", &n, &C) != EOF){ int* d = (int*)malloc((C+1)*sizeof(int)); memset(d, 0, (C+1)*sizeof(int)); for(int i=0; i<=n; ++i){ if(i>0) scanf("%d %d", &V, &W); for(int j=C; j>=0; --j){ if(j>=V && i>0) d[j] >?= d[j-V]+W; } } printf("%d\n", d[C]); free(d); } fclose(stdin); fclose(stdout); return 0; }
2.動態規劃問題思考
能採用動態規劃求解的問題的一般要具有3個性質:
(1) 最優化原理:如果問題的最優解所包含的子問題的解也是最優的,就稱該問題具有最優子結構,即滿足最優化原理。
(2) 無後效性:即某階段狀態一旦確定,就不受這個狀態以後決策的影響。也就是說,某狀態以後的過程不會影響以前的狀態,只與當前狀態有關。
(3)有重疊子問題:即子問題之間是不獨立的,一個子問題在下一階段決策中可能被多次使用到。(這個性質是動態規劃節省時間複雜度的原因)
刷題有感:
首先要確定狀態和狀態轉移方程,難點和關鍵在於找狀態轉移方程,參考部落格:
http://www.360doc.com/content/13/0601/00/8076359_289597587.shtml