一切都要從一則故事提及。html
話說有一哥們去森林裏玩發現了一堆寶石,他數了數,一共有n個。 但他身上能裝寶石的就只有一個揹包,揹包的容量爲C。這哥們把n個寶石排成一排並編上號: 0,1,2,…,n-1。第i個寶石對應的體積和價值分別爲V[i]和W[i] 。排好後這哥們開始思考: 揹包總共也就只能裝下體積爲C的東西,那我要裝下哪些寶石才能讓我得到最大的利益呢?算法
OK,若是是你,你會怎麼作?你斬釘截鐵的說:動態規劃啊!恭喜你,答對了。 那麼讓咱們來看看,動態規劃中最最最重要的兩個概念: 狀態和狀態轉移方程在這個問題中分別是什麼。數組
咱們要怎樣去定義狀態呢?這個狀態總不能是憑空想象或是從天上掉下來的吧。 爲了方便說明,讓咱們先實例化上面的問題。通常遇到n,你就果斷地給n賦予一個很小的數, 好比n=3。而後設揹包容量C=10,三個寶石的體積爲5,4,3,對應的價值爲20,10,12。 對於這個例子,我想智商大於0的人都知道正解應該是把體積爲5和3的寶石裝到揹包裏, 此時對應的價值是20+12=32。接下來,咱們把第三個寶石拿走, 同時揹包容量減去第三個寶石的體積(由於它是裝入揹包的寶石之一), 因而問題的各參數變爲:n=2,C=7,體積{5,4},價值{20,10}。好了, 如今這個問題的解是什麼?我想智商等於0的也解得出了:把體積爲5的寶石放入揹包 (而後剩下體積2,裝不下第二個寶石,只能眼睜睜看着它溜走),此時價值爲20。 這樣一來,咱們發現,n=3時,放入揹包的是0號和2號寶石;當n=2時, 咱們放入的是0號寶石。這並非一個偶然,沒錯, 這就是傳說中的「全局最優解包含局部最優解」(n=2是n=3狀況的一個局部子問題)。 繞了那麼大的圈子,你可能要問,這都哪跟哪啊?說好的狀態呢?說好的狀態轉移方程呢? 別急,它們已經呼之欲出了。函數
咱們再把上面的例子理一下。當n=2時,咱們要求的是前2個寶石, 裝到體積爲7的揹包裏能達到的最大價值;當n=3時,咱們要求的是前3個寶石, 裝到體積爲10的揹包裏能達到的最大價值。有沒有發現它們實際上是一個句式!OK, 讓咱們形式化地表示一下它們, 定義d(i,j)爲前i個寶石裝到剩餘體積爲j的揹包裏能達到的最大價值。 那麼上面兩句話即爲:d(2, 7)和d(3, 10)。這樣看着真是爽多了, 而這兩個看着很爽的符號就是咱們要找的狀態了。 即狀態d(i,j)表示前i個寶石裝到剩餘體積爲j的揹包裏能達到的最大價值
。 上面那麼多的文字,用一句話歸納就是:根據子問題定義狀態!你找到子問題, 狀態也就浮出水面了。而咱們最終要求解的最大價值即爲d(n, C):前n個寶石 (0,1,2…,n-1)裝入剩餘容量爲C的揹包中的最大價值。狀態好不容易找到了, 狀態轉移方程呢?顧名思義,狀態轉移方程就是描述狀態是怎麼轉移的方程(好廢話!)。 那麼回到例子,d(2, 7)和d(3, 10)是怎麼轉移的?來,咱們來講說2號寶石 (記住寶石編號是從0開始的)。從d(2, 7)到d(3, 10)就隔了這個2號寶石。 它有兩種狀況,裝或者不裝入揹包。若是裝入,在面對前2個寶石時, 揹包就只剩下體積7來裝它們,而相應的要加上2號寶石的價值12, d(3, 10)=d(2, 10-3)+12=d(2, 7)+12;若是不裝入,體積仍爲10,價值天然不變了, d(3, 10)=d(2, 10)。記住,d(3, 10)表示的是前3個寶石裝入到剩餘體積爲10 的揹包裏能達到的最大價值
,既然是最大價值,就有d(3, 10)=max{ d(2, 10), d(2, 7)+12 }。好了,這條方程描述了狀態d(i, j)的一些關係, 沒錯,它就是狀態轉移方程了。把它形式化一下:d(i, j)=max{ d(i-1, j), d(i-1,j-V[i-1]) + W[i-1] }。注意討論前i個寶石裝入揹包的時候, 實際上是在考查第i-1個寶石裝不裝入揹包(由於寶石是從0開始編號的)。至此, 狀態和狀態轉移方程都已經有了。接下來,直接上代碼。post
for(int i=0; i<=n; ++i){ for(int j=0; j<=C; ++j){ d[i][j] = i==0 ? 0 : d[i-1][j]; if(i>0 && j>=V[i-1]) d[i][j] >?= d[i-1][j-V[i-1]]+W[i-1]; } }
i=0時,d(i, j)爲何爲0呢?由於前0個寶石裝入揹包就是沒東西裝入,因此最大價值爲0。 if語句裏,j>=V[i-1]說明只有當揹包剩餘體積j大於等於i-1號寶石的體積時, 我才考慮把它裝進來的狀況,否則d[i][j]就直接等於d[i-1][j]。i>0不用說了吧, 前0個寶石裝入揹包的狀況是邊界,直接等於0,只有i>0纔有必要討論, 我是裝呢仍是不裝呢。簡單吧,核心算法就這麼一丁點,接下來上完整代碼knapsack.cpp。測試
/**0-1 knapsack d(i, j)表示前i個物品裝到剩餘容量爲j的揹包中的最大重量**/ #include<cstdio> using namespace std; #define MAXN 1000 #define MAXC 100000 int V[MAXN], W[MAXN]; int d[MAXN][MAXC]; int main(){ freopen("data.in", "r", stdin);//重定向輸入流 freopen("data.out", "w", stdout);//重定向輸出流 int n, C; while(scanf("%d %d", &n, &C) != EOF){ for(int i=0; i<n; ++i) scanf("%d %d", &V[i], &W[i]); for(int i=0; i<=n; ++i){ for(int j=0; j<=C; ++j){ d[i][j] = i==0 ? 0 : d[i-1][j]; if(i>0 && j>=V[i-1]) d[i][j] >?= d[i-1][j-V[i-1]]+W[i-1]; } } printf("%d\n", d[n][C]);//最終求解的最大價值 } fclose(stdin); fclose(stdout); return 0; }
其中freopen函數將標準輸入流重定向到文件data.in, 這比運行程序時一點點手輸要方便許多,將標準輸出流重定向到data.out。 data.in中每組輸入的第一行爲寶石數量n及揹包體積C,接下來會有n行的數據, 每行兩個數對應的是寶石的體積及價值。本測試用例data.in以下:優化
5 10
4 9
3 6
5 1
2 4
5 1
4 9
4 20
3 6
4 20
2 4
5 10
2 6
2 3
6 5
5 4
4 6
data.out爲算法輸出結果,對應該測試用例,輸出結果以下:spa
19
40
15
好,至此咱們解決了揹包問題中最基本的0/1揹包問題。等等,這時你可能要問, 我如今只知道揹包能裝入寶石的最大價值,但我還不知道要往揹包裏裝入哪些寶石啊。嗯, 好問題!讓咱們先定義一個數組x,對於其中的元素爲1時表示對應編號的寶石放入揹包, 爲0則不放入。讓咱們回到上面的例子,對於體積爲5,4,3,價值爲20,10,12的3個寶石 ,如何求得其對應的數組x呢?(明顯咱們目測一下就知道x={1 0 1}, 但程序可目測不出來)OK,讓咱們仍是從狀態提及。若是咱們把2號寶石放入了揹包, 那麼是否是也就意味着,前3個寶石放入揹包的最大價值要比前2個寶石放入揹包的價值大, 即:d(3, 10)>d(2, 10)。再用字母代替具體的數字 (不知不覺中咱們就用了不徹底概括法哈),當d(i, j)>d(i-1, j)時,x(i-1)=1;OK, 上代碼:code
//輸出打印方案 int j = C; for(int i=n; i>0; --i){ if(d[i][j] > d[i-1][j]){ x[i-1] = 1; j = j - V[i-1];//裝入第i-1個寶石後背包能裝入的體積就只剩下j - V[i-1] } } for(int i=0; i<n; ++i) printf("%d ", x[i]);
好了,加入這部份內容,knapsack.cpp變爲以下:htm
/**0-1 knapsack d(i, j)表示前i個物品裝到剩餘容量爲j的揹包中的最大重量**/ #include<cstdio> using namespace std; #define MAXN 1000 #define MAXC 100000 int V[MAXN], W[MAXN], x[MAXN]; int d[MAXN][MAXC]; int main(){ freopen("data.in", "r", stdin); freopen("data.out", "w", stdout); int n, C; while(scanf("%d %d", &n, &C) != EOF){ for(int i=0; i<n; ++i) scanf("%d %d", &V[i], &W[i]); for(int i=0; i<n; ++i) x[i] = 0; //初始化打印方案 for(int i=0; i<=n; ++i){ for(int j=0; j<=C; ++j){ d[i][j] = i==0 ? 0 : d[i-1][j]; if(i>0 && j>=V[i-1]) d[i][j] >?= d[i-1][j-V[i-1]]+W[i-1]; } } printf("%d\n", d[n][C]); //輸出打印方案 int j = C; for(int i=n; i>0; --i){ if(d[i][j