動態規劃之0-1揹包問題

題目:

  有 N 件物品和一個容量爲 V 的揹包。第 i 件物品的費用是 w[i],價值是 p[i]。求解將哪些物品裝入揹包可使這些物品的費用總和不超過揹包容量,且價值總和最大。

  本文按照動態規劃的標準模式解析:http://blog.csdn.net/hearthougan/article/details/53749841

 0-1揹包問題,表示的是每個物品只有一件,每件物品不能分割,在不超過揹包容量的同時,如何選取物品,使得揹包所裝的價值最大(揹包可以裝不滿)。這是一個經典的動態規劃問題,有《揹包九講》珠玉在前,我所能做的也只是按自己的理解,加以分析這個問題。如果需要《揹包九講》請點擊:

http://download.csdn.net/detail/hearthougan/5891267

  不妨設集合表示n個物品,該問題的某一個最優解集合爲

1、最優子結構

  我們已經假設該問題的最優解爲A,那麼對於某個物品,令,表示原問題除去物品後的一個子問題,那麼,就是該子問題的一個最優解。可反證此問題,即存在一個是子問題的最優解解,那麼,所得的集合一定比原問題最優解集合S所得的最大價值要大,這與假設矛盾。因此原問題的最優解一定包含子問題的最優解,這就證明了最優子結構性質。

2、遞歸地定義最優解的值

  對於每個物品我們可以有兩個選擇,放入揹包,或者不放入,有n個物品,故而我們需要做出n個選擇,於是我們設f[i][v]表示做出第i次選擇後,所選物品放入一個容量爲v的揹包獲得的最大價值。現在我們來找出遞推公式,對於第i件物品,有兩種選擇,放或者不放。

  <1>:如果放入第i件物品,則f[i][v] = f[i-1][v-w[i]]+p[i],表示,前i-1次選擇後所選物品放入容量爲v-w[i]的揹包所獲得最大價值爲f[i-1][v-w[i]],加上當前所選的第i個物品的價值p[i]即爲f[i][v]。

  <2>:如果不放入第i件物品,則有f[i][v] = f[i-1][v],表示當不選第i件物品時,f[i][v]就轉化爲前i-1次選擇後所選物品佔容量爲v時的最大價值f[i-1][v]。則:

f[i][v] = max{f[i-1][v], f[i-1][v-w[i]]+p[i]}

3、求解最優值

我們根據2的遞推公式,可以實現代碼如下:

  1. #include <iostream>
  2. #include <cstdio>
  3. #include <cstdlib>
  4. using namespace std;
  5. const int MAXN = 100;
  6. int main()
  7. {
  8. int n, V;
  9. int f[MAXN][MAXN];
  10. int w[MAXN], p[MAXN];
  11. while(cin>>n>>V)
  12. {
  13. if(n == 0 && V == 0)
  14. break;
  15. for(int i = 0; i <= n; ++i)
  16. {
  17. w[i] = 0, p[i] = 0;
  18. for(int j = 0; j <= n; ++j)
  19. {
  20. f[i][j] = 0;
  21. }
  22. }
  23. for(int i = 1; i <= n; ++i)
  24. cin>>w[i]>>p[i];
  25. for(int i = 1; i <= n; ++i)
  26. {
  27. for(int v = w[i]; v <= V; ++v)
  28. {
  29. f[i][v] = max(f[i-1][v], f[i-1][v-w[i]]+p[i]);
  30. }
  31. }
  32. cout<<f[n][V]<<endl;
  33. }
  34. return 0;
  35. }
  由此我們可以知道,我們必須要做出n次選擇,所以外層n次循環是必不可少的,對於上面代碼的內層循環,表示當第i個商品要放入容量爲v(v = w[i]....V)的揹包時所獲得的價值,即先對子問題求解,這個也是必不可少的,所以時間複雜度爲O(nV),這個已不能進一步優化,但是我們可以對空間進行優化。

  由於我們用f[n][V]表示最大價值,但是當物品和揹包容量比較大時,這種用法會佔用大量的空間,那麼我們是不是對此可以進一步優化呢?

  現在考慮如果我們只用一位數組f[v]來表示f[i][v],是不是同樣可以達到效果?我們由上述可知f[i][v]是由f[i-1][v]和f[i-1][v-w[i]]這兩個子問題推得,事實上第i次選擇時,我們雖用到前i-1次的最優結果,但是前i-1次選擇的最優結果,已經保存在做出第i-1次選擇後的結果中,即第i次的結果只用到了第i-1次選擇後的狀態因此我們可以只用一維數組來維持每次選擇的結果,怎麼維持?也就是當第i次選擇時,我們怎麼得到f[i-1][v]和f[i-1][v-w[j]]這兩種狀態,即第i次求f[v]時,此時f[v]和f[v-w[i]]表示的是不是f[i-1][v]和f[i-1][v-w[j]]事實上我們只需要將內層循環變爲從V到w[j]的逆向循環即可滿足要求。這句話不是很好理解,我們先給出優化後的代碼,然後由圖表來慢慢分析。

  1. #include <iostream>
  2. #include <cstdio>
  3. #include <cstdlib>
  4. #include <cstring>
  5. using namespace std;
  6. const int MAXN = 100;
  7. int main()
  8. {
  9. int n, V;
  10. int f[MAXN];
  11. int w[MAXN], p[MAXN];
  12. while(cin>>n>>V)
  13. {
  14. if(n == 0 && V == 0)
  15. break;
  16. memset(f, 0, sizeof(f));
  17. memset(w, 0, sizeof(w));
  18. memset(p, 0, sizeof(p));
  19. for(int i = 1; i <= n; ++i)
  20. cin>>w[i]>>p[i];
  21. for(int i = 1; i <= n; ++i)
  22. {
  23. for(int v = V; v >= w[i]; --v)
  24. {
  25. f[v] = max(f[v], f[v-w[i]]+p[i]);
  26. }
  27. }
  28. cout<<f[V]<<endl;
  29. }
  30. return 0;
  31. }
  例,有3個物品,揹包容量爲10,如下:

  初始時:我們初始化f[]全部爲0


  第1次主循環,即當i = 1時,我們只對物品1進行選擇,對於內層循環,即當v = 10....3時,我們有:

  f[10] = max{f[10], f[10-3]+p[1]} = max{f[10], f[7]+4} = max{0, 0+4} = 4;

  f[9] = max{f[9], f[9-3]+p[1]} = max{f[9], f[6]+4} = max{0, 0+4} = 4;

  f[8] =max{f[8], f[8-3]+p[1]} = max{f[8], f[5]+4} = max{0, 0+4} = 4;

  f[7] = max{f[7], f[7-3]+p[1]} = max{f[7], f[4]+4} = max{0, 0+4} = 4;

  f[6] = max{f[6], f[6-3]+p[1]} = max{f[6], f[3]+4} = max{0, 0+4} = 4;

  f[5] = max{f[5], f[5-3]+p[1]} = max{f[5], f[2]+4} = max{0, 0+4} = 4;

  f[4] = max{f[4], f[4-3]+p[1]} = max{f[4], f[1]+4} = max{0, 0+4} = 4;

  f[3] = max{f[3], f[3-3]+p[1]} = max{f[3], f[0]+4} = max{0, 0+4} = 4;

  f[2] = f[1] = f[0] = 0;

  其中f[2] = f[1] = f[0] = 0,是因爲體積爲3的物品,根本不會影響當揹包容量爲2、1、0時的狀態。所以他們依舊保持原來的狀態。對應於:


  表中橫軸的藍色區域,表示當容量爲v時,對第1個商品做出選擇時所依賴的上一層的狀態,如當v=10時,所依賴的就是f[0][10]和f[0][7]兩個狀態。所以當計算f[1][v](v = 10....3)時,f[v]和[v-3]保存的就是f[0][v]和f[0][v-3]。

  第2次循環,即i = 2時,我們對物品2做出選擇:

  f[10] = max{f[10], f[10-4]+p[2]} = max{f[10], f[6]+p[2]} = max{4, 4+5} = 9

  f[9] = max{f[9], f[9-4]+p[2]} = max{f[9], f[5]+p[2]} = max{4, 4+5} = 9

  f[8] = max{f[8], f[8-4]+p[2]} = max{f[8], f[4]+p[2]} = max{4, 4+5} = 9

  f[7] = max{f[7], f[7-4]+p[2]} = max{f[7], f[3]+p[2]} = max{4, 4+5} = 9

  f[6] = max{f[6], f[6-4]+p[2]} = max{f[6], f[2]+p[2]} = max{4, 0+5} = 5

  f[5] = max{f[5], f[5-4]+p[2]} = max{f[5], f[1]+p[2]} = max{4, 0+5} = 5

  f[4] = max{f[4], f[4-4]+p[2]} = max{f[4], f[0]+p[2]} = max{4, 0+5} = 5

  f[3] = 4

  f[2] = f[1] = f[0] = 0;


  第3次循環,即當i = 3時

  f[10] = max{f[10], f[10-5]+p[3]} = max{f[10], f[5]+3} = max{9, 5+6} = 11

  f[9] = max{f[9], f[9-5]+p[3]} = max{f[9], f[4]+3} = max{9, 5+6} = 11

  f[8] = max{f[8], f[8-5]+p[3]} = max{f[8], f[3]+3} = max{9, 4+6} = 10

  f[7] = max{f[7], f[7-5]+p[3]} = max{f[7], f[2]+3} = max{9, 0+6} = 9

  f[6] = max{f[6], f[6-5]+p[3]} = max{f[6], f[1]+3} = max{5, 0+6} = 6

  f[5] = max{f[5], f[5-5]+p[3]} = max{f[5], f[0]+3} = max{5, 0+6} = 6

  f[4] = 5

  f[3] = 4

  f[2] = f[1] = f[0]

  對於應表:


  綜上的步驟我們可以很清楚的知道v從大到小循環可以滿足題意,從表中我們也可以知道,我們如用f[i][v]存儲最優結果,有很多沒用的求解結果也被保存下來,從而浪費了大量的空間。如果我們用f[v],那麼保存的就是最終結果,且很好地利用了空間。

  如果還是疑問爲什麼v從小到大的順序不可以(即內循環爲:for(int v = w[i]; v <= V; ++v)),我們依舊可以按着代碼,試着畫一下表,一次,就很清楚了,比如同樣的問題,正着走一次爲:

  當i=1時:

  f[0] = f[1] = f[2] = 0

  f[3] = max{f[3], f[3-3]+p[1]} = max{0, 0+4} = 4

  f[4] = max{f[4], f[4-3]+p[1]} = max{0, 0+4} = 4

  f[5] = max{f[5], f[5-3]+p[1]} = max{0, 0+4} = 4

  以上結果貌似是對的,但是意思已經完全不是我們當初設計代碼時的思想了,注意下面:

  f[6] = max{f[6], f[6-3]+p[1]} = max{0. 4+4} = 8//此時我們用的f[6]和f[3]相當於是f[1][6]和f[1][3],而不是f[0][6]和f[0][3]!

  f[7] = max{f[7], f[7-3]+p[1]} = max{0, 4+4} = 8

  f[8] = max{f[8], f[8-3]+p[1]} = max{0, 4+4} = 8

  f[9] = max{f[9], f[9-3]+p[1]} = max{0, 8+4} = 12

  f[10] = max{f[10], f[10-3]+p[1]} = max{0, 8+4} = 12

  到此,我們清楚了爲什麼正向循環爲何不可,因爲此時f[v]保存的相當於是f[i][v]和f[i][v-w[i]],這已經違背了我們意圖!

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