【演算法】0-1揹包問題
動態規劃之詳細分析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的遞推公式,可以實現程式碼如下:
#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;
const int MAXN = 100;
int main()
{
int n, V;
int f[MAXN][MAXN];
int w[MAXN], p[MAXN];
while(cin>>n>>V)
{
if(n == 0 && V == 0)
break;
for(int i = 0; i <= n; ++i)
{
w[i] = 0, p[i] = 0;
for(int j = 0; j <= n; ++j)
{
f[i][j] = 0;
}
}
for(int i = 1; i <= n; ++i)
cin>>w[i]>>p[i];
for(int i = 1; i <= n; ++i)
{
for(int v = w[i]; v <= V; ++v)
{
f[i][v] = max(f[i-1][v], f[i-1][v-w[i]]+p[i]);
}
}
cout<<f[n][V]<<endl;
}
return 0;
}
由此我們可以知道,我們必須要做出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]的逆向迴圈即可滿足要求。這句話不是很好理解,我們先給出優化後的程式碼,然後由圖表來慢慢分析。
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
using namespace std;
const int MAXN = 100;
int main()
{
int n, V;
int f[MAXN];
int w[MAXN], p[MAXN];
while(cin>>n>>V)
{
if(n == 0 && V == 0)
break;
memset(f, 0, sizeof(f));
memset(w, 0, sizeof(w));
memset(p, 0, sizeof(p));
for(int i = 1; i <= n; ++i)
cin>>w[i]>>p[i];
for(int i = 1; i <= n; ++i)
{
for(int v = V; v >= w[i]; --v)
{
f[v] = max(f[v], f[v-w[i]]+p[i]);
}
}
cout<<f[V]<<endl;
}
return 0;
}
例,有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]],這已經違背了我們意圖!
相關文章
- 演算法——貪心演算法解0-1揹包問題演算法
- 資料結構與演算法——0-1揹包問題資料結構演算法
- leetcode題解(0-1揹包問題)LeetCode
- 0-1揹包問題(動態規劃)動態規劃
- 動態規劃解0-1揹包問題動態規劃
- 0-1揹包問題 動態規劃法動態規劃
- 動態規劃之 0-1 揹包問題詳解動態規劃
- 揹包問題的演算法演算法
- 揹包問題(01揹包與完全揹包)
- 【動態規劃】0-1揹包問題原理和實現動態規劃
- 揹包問題
- 動態規劃0-1揹包動態規劃
- 0-1 揹包問題詳解(暫告一段落)
- ACM 揹包問題ACM
- 01揹包問題
- acm演算法之三大揹包問題ACM演算法
- 揹包問題大合集
- 從【零錢兌換】問題看01揹包和完全揹包問題
- 位元組面試演算法題-0,1揹包問題面試演算法
- JavaScript中揹包問題(面試題)JavaScript面試題
- 揹包問題例題總結
- JavaScript 揹包問題詳解JavaScript
- 01揹包問題理解動態規劃演算法動態規劃演算法
- 揹包問題解題方法總結
- 揹包問題的遞迴與非遞迴演算法遞迴演算法
- 01揹包問題的解決
- 揹包問題----動態規劃動態規劃
- 【動態規劃】揹包問題動態規劃
- Java實現-揹包問題IJava
- Java實現-揹包問題IIJava
- Java實現-揹包問題VIJava
- chapter12-2-揹包問題APT
- 二維費用揹包問題
- 華為面試題:購物車問題(01揹包演算法升級)面試題演算法
- Facebook 面試題 | Backpack VI 揹包演算法面試題演算法
- javascript演算法基礎之01揹包,完全揹包,多重揹包實現JavaScript演算法
- 揹包問題的一道經典問題
- 【資料結構與演算法】揹包問題總結梳理資料結構演算法