彻底理解01背包问题

本人不才,虽然学过运筹学,但早已忘了个干净,最近在看面试题,发现DP问题非常普遍,而我总喜欢用递归搜索的思维方式去解决DP问题,做了几道题才发现自己根本没有理解DP,也没有使用DP。所以在看到01背包问题的解的时候百思不得其解。相信不少人跟我一样也会对01问题的递推公式不理解,下面就通过一道程序的打印结果来分析一下到底DP在程序中是如何运作的。

设总的背包大小是V,申请一个大小是V的数组f[V]来存储中间结果。

下面要开始发挥想象力了:

想象一下,现在你有V个从1到V容量连续变化的V个背包(而不是只有一个背包),

第一步迭代:

  不论目前手边有多少个物件,首先拿起第一个物件1,在手里掂量掂量,看起来可以放进背包V中,于是记录一下:“如果手边只有物件1,那么容量为V的背包能放的最大价值是物件1的价值W[1]”,然后再掂量掂量物件1,发现还可以放在V-1容量的背包里,再记录一下:“如果手边只有物件1,那么容量为V-1的背包能放的最大价值是物件1的价值W[1]”......如此直到拿出容量为V-C[1](C[1]是物件1的体积)的那个背包时,发现这个背包的容积刚好是物件1的体积,那么比这个背包容量再小的背包放不下物件1了,于是记录一下:“矮油,如果手边只有物件1,那么容量小于V-C[1]的那些背包里面什么都放不了,最大价值也只能是0了”。得到的f[V]的结果是:0,0,...,W[1],W[1],W[1]。

第二步迭代:

  现在拿出第二个物品,假设这个物品的价值比第一个物品大,体积也更大。到底要不要把物件2放入背包中呢?如果背包V能同时放下两个物品,那么就都放下最好,价值达到W[1]+W[2],但当某个容积小于V的背包不能同时放下两个物品时,就会出现一个抉择问题,要放物件1还是物件2?那就得比较物件1和物件2的价值,哪个大就放哪个。当容积继续变小,没法放下物件2的时候,那么就只有放物件1了。得到f[V]的结果是:0,0,...,W[1],W[1],...,W[2],w[2],....,W[1]+W[2]。

根据“1生2,2生3,3生无穷的思想”进行

第三步迭代:

  假设物件3的价值大于物件1和物件2,拿出背包V,发现背包同时放不下三个物件,这个时候,到底要不要把物件3放到这个背包里面呢?好了,最关键的时候到了。拿出容积为V的最大的那个背包,再拿出容积和最大的背包相差正好为物件3体积的背包,也就是说,最大的背包里面放着一个小背包,再加上物件3,如果这个小背包装满了的话,则正好可以装满背包V(不过小背包是否装满并不重要)。这个时候,小背包中不放物件3的最大价值是已知的,最大的背包V如果不放物件3的最大价值也是已知的,什么时候要选择放入小背包和物件3呢?只有当小背包加物件3的价值比最大的背包V不放物件3的最大价值还大的时候,才会选择将小背包和物件3放入这个大背包。

  解释到这里,不上代码说不过去了,抄来的代码,自己写的注释:

#define EMPTY
#include <iostream>
using namespace std ; 
const  int V = 10 ;  //总的体积 
const  int T = 5 ;    //物品的种类个数
int f[V+1] ;
int w[T] = {8 , 10 , 9 , 5 , 5};        //价值 
int c[T] = {3 , 6 , 5 , 2 , 3};        //每一个的体积 
const int INF = -66536  ;

int package()
{
#ifdef EMPTY
    for(int i = 0 ; i <= V ;i++) //条件编译,表示背包可以不存储满
        f[i] = 0 ;    
#else
    f[0] = 0 ;
    for(int i = 1 ; i <= V ;i++)//条件编译,表示背包必须全部存储满
        f[i] = INF ;   
#endif

    for(int i = 0 ; i < T ; i++)
    {
        for(int v = V ; v >= c[i] ;v--) //不用非要减到0,本身总容量就放不下物件i的背包就不用进行决策了
        {              
            f[v] = max(f[v-c[i]] + w[i] , f[v])  ; //小背包的容积是大背包v减去要放的物件i的体积v-c[i],物件i的价值w[i]。则没放物件3的小背包的价值为f[v-c[i]],加上物件i的总价值是f[v-c[i]] + w[i],如果这个值比不放物件i的大背包最大价值大,那么就把物件i放入背包v。
        }      
        for (int j=0;j<V;j++)
        {
            printf("%d,",f[j]);
        }
        printf("\n");
    }
    return f[V] ;        
}

int main()
{

    int temp = package() ;   
    cout<<temp<<endl     ;   
    system("pause")      ;
    return 0 ;    
} 

  算法精妙之处在于把背包容积作为了数组的下标,通过背包容积就能找到这个背包的最大价值,然后决定是否要将当前物件放入大背包中。DP的精髓在于一个问题有完整的子结构(而不是子问题!!),求解最终答案的过程中需要用到子结构的决策结果来做下一轮的决策。可以发现,为了求最终容积为V的背包最优容量,把从0到V-1容量背包的问题全都求解了,当然这是必须的,否则无法得出背包V的解。找出了DP问题的状态转移方程,基本都能用这个代码框架求解,状态转移方程相当重要!!

  之前我一直把子结构和子问题混淆,所以总是往递归搜索的思路上走,最后越走越糊涂,弄了两天终于搞明白了,特此记之!!

原文地址:https://www.cnblogs.com/darknightsnow/p/2709360.html