背包问题

01背包

转载于:点击打开链接

问题描述:

01背包(ZeroOnePack):有N件物品和一个容量为V的背包,每种物品均只有一件。且每i件物品的重量为weight[i],价值为value[i]。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。

 

背包特点:

每种物品仅有一件,可以选择放或不放。

 

基本思路:

用子问题定义状态:即DP[i][v]表示前i件物品恰好放入容量为v的背包可以获得的最大价值。则其状态转移方程是:

             DP[i][v]=max{ DP[i-1][v], DP[i-1][v-weight[i]]+value[i] }

对于以上的方程必须进行详细的解释下:“将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。

第一种情况:如果第i件不放进去,那么问题就转化为“前i-1件物品放入容量为v的背包中”,价值为DP[i-1][v];

第二种情况:如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为v-weight[i]的背包中”,此时能获得的最大价值就是DP[i-1][v-weight[i]]再加上通过放入第i件物品获得的价值value[i],及DP[i-1][v-weight[i]]+value[i];

最后再比较第一种与第二种所得的价值大小,哪种价值大,DP[i][v]的值就是哪种。

 

优化空间复杂度:

以上的时间和空间的复杂度均为O(VN),其中时间复杂度应该已经不能在优化了,但时间复杂度却可以优化到O(V);

我们用一维数组来存储DP值,及用DP[0….v]表示:DP[v]表示把前i件物品放入容量为v的背包里得到的价值。

首先要知道,我们是通过i从1到n的循环来依次表示前i件物品存入的状态。即:for i=1..N
现在思考如何能在是DP[v]表示当前状态是容量为v的背包所得价值,而又使DP[v]和DP[v-weight[i]]+value[i]标签前一状态的价值?

这里我们采用了逆序保存DP[v]的值,这要求在每次主循环中我们以v=V..0的顺序推DP[v],这样才能保证推DP[v]时DP[v-weight[i]]保存的是状态DP[i-1][v-weight[i]]的值(及前一个状态的值)。

伪代码如下:

     for i=1…..N

        for v=V…..0

           DP[v]=max{ DP[v], DP[v-weight[i]]+value[i] };

分析上面的代码:当内循环是逆序时,就可以保证后一个DP[v]和DP[v-weight[i]]+value[i]是前一状态的! 例:

 

代码实现:

     void ZeroOnePack()

     {

         for(int i=1;i<=N;i++)

         {

             for(j=V;j>=weight[i];j--)

                 DP[j]=DP[j]>DP[j-weight[i]]+value[i]?DP[j]:DP[j-weight[i]]+value[i];

         }

     }       

 

初始化的细节问题:

有的问题中有要求“恰好装满背包”,有的则没有要求,这就在初始化DP数组时有所不同。

恰好装满背包:则初始化时,DP[0]=0, 其他的DP[1…..V]均设为负的无穷大。

无须恰好装满背包:则初始化时,DP[1…..V]全部设为0;

 

一个常数优化:

前面的伪代码中有for v=V…..0,可以将这个循环下限进行改进。由于只需要最后DP[V]的值,倒推前一个物品,其实只要知道DP[v-weight[n]]即可。以此类推,对以第j个背包,其实只需要知道到DP[v-sum{weight[j….n]}]即可,即代码如下:

sum[n]=weight[n];

              for(i=n-1;i>=1;i--)

                     sum[i]=sum[i+1]+weight[i];

              for(i=1;i<=v;i++)

                     DP[i]=-(1<<10);  //要求恰好装满背包(无此要求,则memset(DP,0,sizeof(DP));)

              DP[0]=0;

              for(i=1;i<=n;i++)

              {

                     bound=MAX(v-sum[i],weight[i]);

                     for(j=v;j>=bound;j--)

                     {

                            DP[j]=MAX(DP[j],DP[j-weight[i]]+value[i]);

                     }

              }

 

例题:

http://acm.hdu.edu.cn/showproblem.php?pid=2602

http://acm.hdu.edu.cn/showproblem.php?pid=1203

http://acm.hdu.edu.cn/showproblem.php?pid=2955

http://acm.hdu.edu.cn/showproblem.php?pid=3496

 

 

完全背包

问题描述:

完全背包(CompletePack):有N件物品和一个容量为V的背包,每种物品都有无限可用。且每i件物品的重量为weight[i],价值为value[i]。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。

 

背包特点:

每种物品有无限多件。也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……等很多种。

 

基本思路:

如果仍然按照解01背包时的思路,令DP[i][v]表示前i种物品恰好放入一个容量为v的背包的最大权值,仍然可以按照每种物品不同的策略写出状态方程:

    DP[i][v]=max{ DP[i-1][v-k*weight[i]]+k*value[i] | 0<=k*value[i]<=v }

 

优化空间复杂度:

同样我们也可以用一维数组来存储DP值,及用DP[0….v]表示。

伪代码如下:

     for i=1…..N

        for v=0…..V

           DP[v]=max{ DP[v], DP[v-weight[i]]+value[i] };

分析上面的代码:

想必大家看出了和01背包的区别,这里的内循环是顺序的,而01背包是逆序的。
现在关键的是考虑:为何完全背包可以这么写?
再次我们先来回忆下,01背包逆序的原因?是为了是max中的两项是前一状态值,这就对了。那么这里,我们顺序写,这里的max中的两项当然就是当前状态的值了,为何?
因为每种背包都是无限的。当我们把i从1到N循环时,DP[v]表示容量为v在前i种背包时所得的价值,这里我们要添加的不是前一个背包,而是当前背包。所以我们要考虑的当然是当前状态。

 

代码实现:

     void CompletePack()

     {

         for(int i=1;i<=N;i++)

         {

             for(j=weight[i];j<=V;j++)

                 DP[j]=DP[j]>DP[j-weight[i]]+value[i]?DP[j]:DP[j-weight[i]]+value[i];

         }

     }       

 

例题:

http://acm.hdu.edu.cn/showproblem.php?pid=1114

http://acm.hdu.edu.cn/showproblem.php?pid=1248

 

 

多重背包

问题描述:

多重背包(MultiplePack):有N件物品和一个容量为V的背包,第i种物品最多有bag[i]件可用。且每i件物品的重量为weight[i],价值为value[i]。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。

 

背包特点:

这题目和完全背包问题很类似。基本的方程只需将完全背包问题的方程略微一改即可,因为对于第i种物品有bag[i]+1种策略:取0件,取1件……取bag[i]件。

 

基本思路:

令DP[i][v]表示前i种物品恰好放入一个容量为v的背包的最大权值,则其状态方程:    DP[i][v]=max{ DP[i-1][v-k*weight[i]]+k*value[i] | 0<=k<=v }

 

代码实现:

         void CompletePack(int v,int w)

{

               for(int j=v;j<=nValue;j++)

                     DP[j]=DP[j]>DP[j-v]+w?DP[j]:DP[j-v]+w;

}

 

void ZeroOnePack(int v,int w)

{

               for(int j=nValue;j>=v;j--)

                     DP[j]=DP[j]>DP[j-v]+w?DP[j]:DP[j-v]+w;

}

 

void MultiplePack(int v,int w,int b)

{

               if(v*b>=nValue)

               {

                      CompletePack(v,w);

                      return ;

               }

               int k=1;

               while(k<b)

               {

                      ZeroOnePack(k*v,k*w);

                      b=b-k;

                      k=k*2;

               }

               ZeroOnePack(b*v,b*w);

   

for(int i=1;i<=N;i++)

    MultiplePack(value[i],weight[i],bag[i]);

 

 

或:

for(i=1;i<=nKind;i++)

     {

     for(j=1;j<=bag[i];j++)

     {

            for(k=nValue;k>=value[i];k--)

            DP[k]=DP[k]>DP[k-value[i]]+weight[i]?DP[k]:DP[k-value[i]]+weight[i];

     }

}

 

例题:

http://acm.hdu.edu.cn/showproblem.php?pid=2191

http://acm.hdu.edu.cn/showproblem.php?pid=1171

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值