[转载]动态规划之0-1背包问题
出處:http://hawstein.com/posts/dp-knapsack.html
一切都要從一則故事說起。
話說有一哥們?nèi)ド掷锿姘l(fā)現(xiàn)了一堆寶石,他數(shù)了數(shù),一共有n個(gè)。 但他身上能裝寶石的就只有一個(gè)背包,背包的容量為C。這哥們把n個(gè)寶石排成一排并編上號(hào): 0,1,2,…,n-1。第i個(gè)寶石對(duì)應(yīng)的體積和價(jià)值分別為V[i]和W[i] 。排好后這哥們開始思考: 背包總共也就只能裝下體積為C的東西,那我要裝下哪些寶石才能讓我獲得最大的利益呢?
OK,如果是你,你會(huì)怎么做?你斬釘截鐵的說:動(dòng)態(tài)規(guī)劃啊!恭喜你,答對(duì)了。 那么讓我們來看看,動(dòng)態(tài)規(guī)劃中最最最重要的兩個(gè)概念: 狀態(tài)和狀態(tài)轉(zhuǎn)移方程在這個(gè)問題中分別是什么。
我們要怎樣去定義狀態(tài)呢?這個(gè)狀態(tài)總不能是憑空想象或是從天上掉下來的吧。 為了方便說明,讓我們先實(shí)例化上面的問題。一般遇到n,你就果斷地給n賦予一個(gè)很小的數(shù), 比如n=3。然后設(shè)背包容量C=10,三個(gè)寶石的體積為5,4,3,對(duì)應(yīng)的價(jià)值為20,10,12。 對(duì)于這個(gè)例子,我想智商大于0的人都知道正解應(yīng)該是把體積為5和3的寶石裝到背包里, 此時(shí)對(duì)應(yīng)的價(jià)值是20+12=32。接下來,我們把第三個(gè)寶石拿走, 同時(shí)背包容量減去第三個(gè)寶石的體積(因?yàn)樗茄b入背包的寶石之一), 于是問題的各參數(shù)變?yōu)?#xff1a;n=2,C=7,體積{5,4},價(jià)值{20,10}。好了, 現(xiàn)在這個(gè)問題的解是什么?我想智商等于0的也解得出了:把體積為5的寶石放入背包 (然后剩下體積2,裝不下第二個(gè)寶石,只能眼睜睜看著它溜走),此時(shí)價(jià)值為20。 這樣一來,我們發(fā)現(xiàn),n=3時(shí),放入背包的是0號(hào)和2號(hào)寶石;當(dāng)n=2時(shí), 我們放入的是0號(hào)寶石。這并不是一個(gè)偶然,沒錯(cuò), 這就是傳說中的“全局最優(yōu)解包含局部最優(yōu)解”(n=2是n=3情況的一個(gè)局部子問題)。 繞了那么大的圈子,你可能要問,這都哪跟哪啊?說好的狀態(tài)呢?說好的狀態(tài)轉(zhuǎn)移方程呢? 別急,它們已經(jīng)呼之欲出了。
我們?cè)侔焉厦娴睦永硪幌隆.?dāng)n=2時(shí),我們要求的是前2個(gè)寶石, 裝到體積為7的背包里能達(dá)到的最大價(jià)值;當(dāng)n=3時(shí),我們要求的是前3個(gè)寶石, 裝到體積為10的背包里能達(dá)到的最大價(jià)值。有沒有發(fā)現(xiàn)它們其實(shí)是一個(gè)句式!OK, 讓我們形式化地表示一下它們, 定義d(i,j)為前i個(gè)寶石裝到剩余體積為j的背包里能達(dá)到的最大價(jià)值。 那么上面兩句話即為:d(2, 7)和d(3, 10)。這樣看著真是爽多了, 而這兩個(gè)看著很爽的符號(hào)就是我們要找的狀態(tài)了。 即狀態(tài)d(i,j)表示前i個(gè)寶石裝到剩余體積為j的背包里能達(dá)到的最大價(jià)值。 上面那么多的文字,用一句話概括就是:根據(jù)子問題定義狀態(tài)!你找到子問題, 狀態(tài)也就浮出水面了。而我們最終要求解的最大價(jià)值即為d(n, C):前n個(gè)寶石 (0,1,2…,n-1)裝入剩余容量為C的背包中的最大價(jià)值。狀態(tài)好不容易找到了, 狀態(tài)轉(zhuǎn)移方程呢?顧名思義,狀態(tài)轉(zhuǎn)移方程就是描述狀態(tài)是怎么轉(zhuǎn)移的方程(好廢話!)。 那么回到例子,d(2, 7)和d(3, 10)是怎么轉(zhuǎn)移的?來,我們來說說2號(hào)寶石 (記住寶石編號(hào)是從0開始的)。從d(2, 7)到d(3, 10)就隔了這個(gè)2號(hào)寶石。 它有兩種情況,裝或者不裝入背包。如果裝入,在面對(duì)前2個(gè)寶石時(shí), 背包就只剩下體積7來裝它們,而相應(yīng)的要加上2號(hào)寶石的價(jià)值12, d(3, 10)=d(2, 10-3)+12=d(2, 7)+12;如果不裝入,體積仍為10,價(jià)值自然不變了, d(3, 10)=d(2, 10)。記住,d(3, 10)表示的是前3個(gè)寶石裝入到剩余體積為10 的背包里能達(dá)到的最大價(jià)值,既然是最大價(jià)值,就有d(3, 10)=max{ d(2, 10), d(2, 7)+12 }。好了,這條方程描述了狀態(tài)d(i, j)的一些關(guān)系, 沒錯(cuò),它就是狀態(tài)轉(zhuǎn)移方程了。把它形式化一下:d(i, j)=max{ d(i-1, j), d(i-1,j-V[i-1]) + W[i-1] }。注意討論前i個(gè)寶石裝入背包的時(shí)候, 其實(shí)是在考查第i-1個(gè)寶石裝不裝入背包(因?yàn)閷毷菑?開始編號(hào)的)。至此, 狀態(tài)和狀態(tài)轉(zhuǎn)移方程都已經(jīng)有了。接下來,直接上代碼。
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時(shí),d(i, j)為什么為0呢?因?yàn)榍?個(gè)寶石裝入背包就是沒東西裝入,所以最大價(jià)值為0。 if語句里,j>=V[i-1]說明只有當(dāng)背包剩余體積j大于等于i-1號(hào)寶石的體積時(shí), 我才考慮把它裝進(jìn)來的情況,不然d[i][j]就直接等于d[i-1][j]。i>0不用說了吧, 前0個(gè)寶石裝入背包的情況是邊界,直接等于0,只有i>0才有必要討論, 我是裝呢還是不裝呢。簡單吧,核心算法就這么一丁點(diǎn),接下來上完整代碼knapsack.cpp。
/**0-1 knapsack d(i, j)表示前i個(gè)物品裝到剩余容量為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]);//最終求解的最大價(jià)值 } fclose(stdin); fclose(stdout); return 0; }其中freopen函數(shù)將標(biāo)準(zhǔn)輸入流重定向到文件data.in, 這比運(yùn)行程序時(shí)一點(diǎn)點(diǎn)手輸要方便許多,將標(biāo)準(zhǔn)輸出流重定向到data.out。 data.in中每組輸入的第一行為寶石數(shù)量n及背包體積C,接下來會(huì)有n行的數(shù)據(jù), 每行兩個(gè)數(shù)對(duì)應(yīng)的是寶石的體積及價(jià)值。本測(cè)試用例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 6data.out為算法輸出結(jié)果,對(duì)應(yīng)該測(cè)試用例,輸出結(jié)果如下:
19 40 15好,至此我們解決了背包問題中最基本的0/1背包問題。等等,這時(shí)你可能要問, 我現(xiàn)在只知道背包能裝入寶石的最大價(jià)值,但我還不知道要往背包里裝入哪些寶石啊。嗯, 好問題!讓我們先定義一個(gè)數(shù)組x,對(duì)于其中的元素為1時(shí)表示對(duì)應(yīng)編號(hào)的寶石放入背包, 為0則不放入。讓我們回到上面的例子,對(duì)于體積為5,4,3,價(jià)值為20,10,12的3個(gè)寶石 ,如何求得其對(duì)應(yīng)的數(shù)組x呢?(明顯我們目測(cè)一下就知道x={1 0 1}, 但程序可目測(cè)不出來)OK,讓我們還是從狀態(tài)說起。如果我們把2號(hào)寶石放入了背包, 那么是不是也就意味著,前3個(gè)寶石放入背包的最大價(jià)值要比前2個(gè)寶石放入背包的價(jià)值大, 即:d(3, 10)>d(2, 10)。再用字母代替具體的數(shù)字 (不知不覺中我們就用了不完全歸納法哈),當(dāng)d(i, j)>d(i-1, j)時(shí),x(i-1)=1;OK, 上代碼:
//輸出打印方案 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個(gè)寶石后背包能裝入的體積就只剩下j - V[i-1] } } for(int i=0; i<n; ++i) printf("%d ", x[i]);好了,加入這部分內(nèi)容,knapsack.cpp變?yōu)槿缦?#xff1a;
/**0-1 knapsack d(i, j)表示前i個(gè)物品裝到剩余容量為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(轉(zhuǎn)載于:https://www.cnblogs.com/shrimp-can/p/6686076.html
總結(jié)
以上是生活随笔為你收集整理的[转载]动态规划之0-1背包问题的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java之ClassLoader基础知识
- 下一篇: openstack下