一切都要從一則故事說起。
話說有一哥們去森林裡玩發現了一堆寶石,他數了數,一共有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(i, j)的一些關係,沒錯,它就是狀態轉移方程了。把它形式化一下:d(i, j)=max。注意討論前i個寶石裝入揹包的時候,其實是在考查第i-1個寶石裝不裝入揹包(因為寶石是從0開始編號的)。至此,狀態和狀態轉移方程都已經有了。接下來,直接上**。
for(int i=0; i<=n; ++i)
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的揹包中的最大重量**/
#includeusing namespace std;
#define maxn 1000
#define maxc 100000
int v[maxn], w[maxn];
int d[maxn][maxc];
int main()
} 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 93 6
5 12 4
5 14 9
4 20
3 64 20
2 45 10
2 62 3
6 55 4
4 6data.out為演算法輸出結果,對應該測試用例,輸出結果如下:
好,至此我們解決了揹包問題中最基本的0/1揹包問題。等等,這時你可能要問,我現在只知道揹包能裝入寶石的最大價值,但我還不知道要往揹包裡裝入哪些寶石啊。嗯,好問題!讓我們先定義乙個陣列x,對於其中的元素為1時表示對應編號的寶石放入揹包,為0則不放入。讓我們回到上面的例子,對於體積為5,4,3,價值為20,10,12的3個寶石,如何求得其對應的陣列x呢?(明顯我們目測一下就知道x=,但程式可目測不出來)ok,讓我們還是從狀態說起。如果我們把2號寶石放入了揹包,那麼是不是也就意味著,前3個寶石放入揹包的最大價值要比前2個寶石放入揹包的價值大,即:d(3, 10)>d(2, 10)。再用字母代替具體的數字(不知不覺中我們就用了不完全歸納法哈),當d(i, j)>d(i-1, j)時,x(i-1)=1;ok,上**:
//輸出列印方案
int j = c;
for(int i=n; i>0; --i)
} printf("%d\n", d[n][c]);
//輸出列印方案
int j = c;
for(int i=n; i>0; --i)
} for(int i=0; idata.out輸出結果變為:
1 1 0 1 0
1 0 1 0
1 1 0 0 1
至此,好像該解決的問題都解決了。當乙個問題找到乙個放心可靠的解決方案後,我們往往就要考慮一下是不是有優化方案了。為了保持**的簡潔,我們暫且把寶石裝包方案的求解去掉。該演算法的時間複雜度是o(n*c),即時間都花在兩個for迴圈裡了,這個應該是沒辦法再優化了。再看看空間複雜度,陣列d用來儲存每個狀態的值,空間複雜度為o(n*c);陣列v和w用來儲存每個寶石的體積和價值,空間複雜度為o(n)。程式總的空間複雜度為o(n*c),這個是可以進一步優化的。首先,我們先把陣列v和w去掉,因為它們沒有儲存的必要,改為一邊讀入一邊計算:
int v = 0, w = 0;
for(int i=0; i<=n; ++i),也就是在計算d(i, j)時我們用到了d(i-1,j)和d(i-1, j-v)的值。如果我們只用乙個一維陣列d(0)~d(c)來儲存狀態值可以麼?將i方向的維數去掉,我們可以將原來二維陣列表示為一維資料:d(i-1, j-v)變為d(j-v),d(i-1, j)變為d(j)。當我們要計算d(i, j)時,只需要比較d(j)和d(j-v)+w的大小,用較大的數更新d(j)即可。等等,如果我要計算d(i, j+1),而它恰好要用到d(i-1, j)的值,那麼問題就出來了,因為你剛剛才把它更新為d(i, j)了。那麼,怎麼辦呢?按照j遞減的順序即可避免這種問題。比如,你計算完d(i, j),接下來要計算的是d(i, j-1),而它的狀態轉移方程為d(i, j-1)=max,它不會再用到d(i-1,j)的值!所以,即使該位置的值被更新了也無所謂。好,上**:
memset(d, 0, sizeof(d));
for(int i=0; i<=n; ++i)
} printf("%d\n", d[c]);
free(d);
} fclose(stdin);
fclose(stdout);
return 0;
}ok,揹包問題暫時先講這麼多,以後接著講。
注:以上**均在dev-cpp下編譯執行成功,請大膽放心使用。:-)
網易部落格:
動態規劃之揹包問題
最近刷題遇到好幾道揹包問題,揹包問題是動態規則中的一類體型,在考察演算法的筆試中經常遇到。關於揹包問題,文章 揹包問題九講 中已經做了很多分析,這裡就不再細述,建議好好看看這篇文章。然而文章給了許多案例分析,卻沒有很好的練習。說明 1 本文目的不在於講解揹包問題的分析與講解,而是收集了一些揹包問題。...
動態規劃之揹包問題
一 問題描述 有n 個物品,它們有各自的重量和價值,現有給定容量的揹包,如何讓揹包裡裝入的物品具有最大的價值總和?二 總體思路 根據動態規劃解題步驟 問題抽象化 建立模型 尋找約束條件 判斷是否滿足最優性原理 找大問題與小問題的遞推關係式 填表 尋找解組成 找出01揹包問題的最優解以及解組成,然後編...
動態規劃之 揹包問題
前些天在做動態規劃的題,感覺動態規劃博大精深,沒有一種特定的模式,學起來很費勁。在這裡就動態規劃中的揹包問題談談。三種揹包問題 0 1揹包,完全揹包,多重揹包。0 1揹包 有n件物品和容量為v的揹包,求解將哪些物品放入揹包中可以使獲得的價值最大。f i v 表示將前i件物品恰好放入容量為v的揹包可以...