【演算法】0-1揹包問題

HelloZEX發表於2018-07-26

動態規劃之詳細分析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]],這已經違背了我們意圖!

相關文章