背包笔记-含0/1背包问题、完全背包问题、多重背包问题、二维背包问题、分组背包问题



根据背包⑨讲自己做的一些笔记,做到后面就不太懂了

黏贴过来就看不到图了,提供pdf文档下载。还是建议看PDF文档

http://download.csdn.net/detail/wearenoth/5612079









0-1背包问题

问题描述

         物品数量为N,背包容量为V。第i件物品费用为c[i],价值为w[i],每件物品数量为1。问:放入哪些物品后,背包中物品价值总和达到最大。

基本思路

         使用DP进行求解。首先需要定义状态:

设f[i][v]表示前i件物品放入一个容量为v的背包中获取的最大价值。

假设我们已经知道放入前i-1个物品到容量为v的背包中可以获取的最大价值。则在考虑第i个物品的时候,第i个物品的处理方法只有两种:放、不放。

Ø  不放:如果不放入第i个物品,则背包中物品的总价值为f[i-1][v]。

Ø  放:如果放入第i个物品,则背包中物品的总价值为f[i-1][v-c[i]]+w[i]。即前i-1个物品只能放在容量为v-c[i]的背包中。

这样状态转移方程为:

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

         将这个思路转换成代码就是

表 1 0-1背包问题代码

#definePACKAGESIZE1000

#defineMATERIALSSIZE100

intf[MATERIALSSIZE][PACKAGESIZE];

/**

  @argcost 物品费用

  @argvalue物品价值

  @argnum  物品数量

  @argpackSize背包大小

*/

voidzeroOnePack(intcost[],intvalue[],intnum,intpackSize){

    for(intindex=0;index<num;index++){

        for(v=cost[index];v<=packSize;v++){

            f[index+1][v]=max(f[index][v], f[index][v-cost[index]]+value[index]);

        }

    }

}

         对于这个方法,容易求的其时间复杂度为O(NV),空间复杂度为O(NV)。这个时间复杂度已经不能优化了,但是空间复杂度还可以优化到O(V)。

优化空间复杂度

         我们可以观察下图,箭头位置标识当前求解的位置(绿色部分),即f[i][v]。根据状态转移方程可以知道,它的值只与f[i-1][v]和f[i-1][v-c[i]]相关(黄色部分)。对于所有>v的状态(蓝色部分)是没有使用到的。

图 0-1

         简单一些思考,可以认为这个过程使用2个大小为V的数组就可以解决问题。但是可以有更优的方法,只要在求解f[v]之前的操作不覆盖<=v的左右状态即可,所以可以只使用一个大小为V的数组即可。这种方法的伪代码为:

for i=1..N

    for v=V..c[i]

        f[v]=max{f[v],f[v-c[i]]+w[i]};

         与之对应的状态转移方程为:

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

         需要注意的就是,使用这个状态转移方程的时候,v的遍历规则就必须从高往低,这样才不会发生覆盖。

通用化模板

         这里将求解第i个物品的状态过程抽取出来,伪代码如下:

procedureZeroOnePack(cost,weight)

    for v=V..cost

        f[v]=max{f[v],f[v-cost]+weight}

         求解背包问题的伪代码就是:

for i=1..N

ZeroOnePack(c[i],w[i]);

         这样背包问题的模板代码就如下所示,对于其他背包问题的代码模板也如下所示。

表 2

#definePACKAGESIZE1000

intf[PACKAGESIZE];

/**

  @argcost 物品价值

  @argvalue物品价值

  @argpackSize背包大小

*/

voidzeroOnePack(intcost,intvalue,intpackSize){

    for(intv = packSize;v>=cost;v--){

        f[v]=max(f[v],f[v-cost]+value);

    }

}

intmain(){

    intc[]={2,3,4,5,6};

    intw[]={2,3,4,5,6};

    intN=5;intV=13;

    for(inti=0;i<N;i++){

        zeroOnePack(c[i],w[i],V);

    }

}

初始化细节问题

         背包问题在问的时候可能有两种问法:“恰好装满背包”、“不要求装满背包”。对于这两种问法,初始化过程则存在一些不同。

Ø  如果要求恰好装满背包,那么在初始化时除了f[0]为0其它f[1..V]均设为,这样就可以保证最终得到的f[N]是一种恰好装满背包的最优解。

Ø  如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将f[0..V]全部设为0。

可以这样理解:初始化的f数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可能被价值为0的nothing“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是-∞了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为0了。

         举个例子说明:假设有3个物品,费用为c[]={2,3, 4},价值为w[]={3, 4, 8},背包大小5。如果不要求恰好装满,则最后答案为8(选择物品3);如果要求恰好装满,则要求最后答案为7(选择物品1、2)。

图 0-2

         这个技巧适用于其他背包问题中的初始化过程。

通用化状态转移方程

         0-1背包问题指的是放入背包中物品数量的取值范围为{0、1},所以状态转移方程可以成为:

f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i] |k=0,1}

         后面的很多状态转移方程其实就是这种格式。

 

 


完全背包问题

问题描述

物品数量为N,背包容量为V。第i件物品费用为c[i],价值为w[i],每件物品数量任意。问:放入哪些物品后,背包中物品价值总和达到最大。

基本思路

         因为物品数量可以取任意个,所以容易得到完全背包问题的状态转移方程为:

f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i] |k=0,1,...,V/c[i]}

         这个思路的空间复杂度还是O(NV),时间复杂度为O(NΣ(V/c[i])),大于O(NV)。

该算法的伪代码为:

for i=1..N

         for v=1..V

                  for k=0..V/c[i]

                           f[i][v]=max{f[i-1][v],f[i][v-k*c[i]]+k*w[i]}

简单优化

         优化思路:若两件物品i,j满足c[i]<=c[j]且w[i]>=w[j],则可以将物品j去除。因为j物品费用更高但是价值更小,我们可以倾向选择物品i而不选择物品j。

二进制优化

         二进制优化基于这样一个基本原理:

将一个数字num分解为若干个数的和,即

num=n1+n2+...+nm

         通过组合n1,n2,...,nm中的若干个数(每个数字只能选取1次)进行求和,可以表示1~num之间的任意数字。

         例如:比如13(1101)这个数,可以分解成1(0001)、2(0010)、4(0100)和6(0110)四个数,1~13范围内的数可以由这四个数字唯一的表示如下:

1=1                2=2                   3=1+2                  4=4                   5=1+4

6=6                7=1+6                 8=2+6                  9=1+2+6               10=4+6  

11=1+4+6           12=2+4+6              13=1+2+4+6

对于如何进行二进制数分解,算法的代码如下:

表 3 对一个数字进行二进制分解

void divNum(intnum) {

     int k = 1;

     while (k <num) {

         k = k << 1;  

         num -= k;

         cout << k << endl; //这里输出分解后的数

     }

     cout << num <<endl;  //注意,分解的最后一个数字

}

         通过上述的模板可以得到完全背包问题的代码如下。

表 4 使用二进制优化的完全背包代码

#definePACKAGESIZE1000

intf[PACKAGESIZE];

/**

  @argcost 物品价值

  @argvalue物品价值

  @argpackSize背包大小

*/

voidcompletePac(intcost,intvalue,intpackSize){

    intnum=packSize/cost;//背包可以容纳最多物品的数量

    intk=1;

    while(k<num){

        zeroOnePack(k*cost,k*value,packSize);

        num-=k;

        k=k<<1;

    }

    if(num>0){

        zeroOnePack(num*cost,num*value,packSize);

    }

}

         我们观察二进制优化的完全背包问题的计算过程。背包容量为6,有2个物品,费用为c[]={1, 2},价值为w[]={1,3}。这里忽略第1个物品的求解过程。对于物品2,背包最多容纳6/2=3个物品。数字3可以分解成1、2。在图中第二行(k=2)中,黄色部分表示背包里面装着1个物品2;蓝色部分表示装着1个物品1;橙色部分表示装着1个物品1和2个物品2;绿色部分表示装着3个物品2。

图 0-1

O(NV)的算法

         伪代码如下:

procedureCompletePack(cost,weight)

    for v=cost..V

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

         这个代码与0-1背包问题中的代码区别就在于循环的时候的次序是从低往高。通过观察下图的过程,可以发现物品2的选择是被不断累积的。

图 0-2

         这种方式下得到的时间复杂度就是O(NV),空间复杂度就是O(V),比原来的快了不少,而且代码量非常少。

表 5 完全背包问题代码

#definePACKAGESIZE1000

intf[PACKAGESIZE];

/**

  @argcost 物品价值

  @argvalue物品价值

  @argpackSize背包大小

*/

voidcompletePack(intcost,intvalue,intpackSize){

    for(intv= cost;v<=packSize;v++){

        f[v]=max(f[v],f[v-cost]+value);

    }

}

 


多重背包问题

问题描述

物品数量为N,背包容量为V。第i件物品费用为c[i],价值为w[i],每件物品数量为n[i]。问:放入哪些物品后,背包中物品价值总和达到最大。

基本思路

         如果知道了完全背包问题中的状态转移方程,多重背包问题的状态转移方程就很容易得到了:

f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i] |k=0,1,...,min(V/c[i],n[i])}

如果直接破解,时间复杂度为O(V*Σn[i])。

二进制优化

         如果返回观察完全背包问题中使用二进制优化的算法过程,我们其实发现二进制优化的完全背包问题的求解过程是具有通用性的,它适合于任意的物品数量。这样多重背包问题就可以转换成完全背包问题进行求解了。其伪代码如下:

procedureMultiplePack(cost, weight, amount)

    if cost*amount>=V

        CompletePack(cost, weight)

    else

        integerk=1

        while k<amount

            ZeroOnePack(k*cost, k*weight)

            amount=amount-k

            k=k*2

            ZeroOnePack(amount*cost, amount*weight)

         因为物品数量可能超过背包容纳的最大值,对于这种情况肯定就直接使用完全背包过程中的方法进行处理。

单调队列优化

         先学习单调队列,假设有一个数组A[],给定一个下标i,希望可以在O(1)的时间内求出A[i-j]到A[i]范围内的最大值。对于这个问题,很容易得到表达式:

f[i]=max{A[k]| j<=k<=i}

这个问题可以利用单调队列进行求解,如下图,下方的指针表示要求的区间范围[st, ed],则单调队列中含有的值则是图中标注颜色的部分。

图 0-1

要保持单调队列比较简单,单调队列其实是一个双向队列,可以参考下图。例如,如果保持一个单调递减的队列,则操作为:

Ø  如果当前元素小于(<)队尾元素,则直接入队。

Ø  如果当前元素大于等于(>=)队尾元素,则队尾元素出队。重复这个步骤一直遇到第一个大于当前元素时停止,然后再将当前元素入队。

Ø  每次移动区间都需要将队列首部中不在区间范围内的元素清除。

图 0-2

         使用单调队列判断这个数组的代码如下所示。此外,很容易就知道单调队列的最大长度不会超过区间大小。

表 6 单调队列遍历数组代码

classLocation{

public:

    intkey;

    intindex;

};

#defineQUEUESIZE100

Locationqueue[QUEUESIZE];

inthead=0 inttail=0;

/**

    @arga输入数组

    @argn数组长度

    @argr区间大小

*/

voidsearch(inta[],intn,intr){

    r--;

    for(inti=0;i<n;i++){

        //出队操作

        while(head<tail&&queue[tail-1].key<a[i]){

            tail--;

        }

        //入队操作

        queue[tail].key=a[i];

        queue[tail].index=i;

        tail++;

        //判断队列头部

        while(queue[head].index<=i-r){

            head++;

        }

    }

}

我们先考察下面的这个递归表达式:

dp[i]=max{dp[k-1]-sum[k]+sum[i]| i-r<=k<=i}

直接求解这个递归表达式容易得到时间复杂度为O(NK),其中N是i的取值范围,r是k的取值范围。

         将dp[k-1]-sum[k]看做是一个数组,只要使用一个单调队列维护这个数组在区间范围[i-r, i]内的最大值,则在求解dp[i]时候就可以在O(1)的时间复杂度内求解得到。

单调队列可以用于优化DP问题,但它只能优化一些特定的DP问题而不是所有,所有诸如上述形式的状态转移方程就可以利用单调队列进行优化。

利用单调队列优化多重背包问题的时候,需要先进行一些方程上的转换,适应上述的格式。目标是去除f[v-k*c[i]]中同时出现v与k的情况。

令b=v%c[i],a=v/c[i],则v可以表示为:v=a*c[i]+b。则状态转移方程变成:

f[i][a*c[i]+b]=max{f[i-1][(a-k)*c[i]+b]+k*w[i]| 0<=k<=n [i],n[i]<V/c[i]}

         令k’=a-k,则可以得到新的状态转移方程:

f[i][a*c[i]+b]=max{f[i-1][k’*c[i]+b]+(a-k’)*w[i]| n[i]-a<=k’<=a,n[i]<V/c[i]}

         这时候就可以利用单调队列进行优化。

编程之美中的一个题

题目:在微软亚洲研究院上班,大家早上来的第一件事是干啥呢?查看邮件?No,是去水房拿饮料:酸奶,豆浆,绿茶、王老吉、咖啡、可口可乐……(当然,还是有很多同事把拿饮料当做第二件事)。

管理水房的阿姨们每天都会准备很多的饮料给大家,为了提高服务质量,她们会统计大家对每种饮料的满意度。一段时间后,阿姨们已经有了大批的数据。某天早上,当实习生小飞第一个冲进水房并一次拿了五瓶酸奶、四瓶王老吉、三瓶鲜橙多时,阿姨们逮住了他,要他帮忙。

从阿姨们统计的数据中,小飞可以知道大家对每一种饮料的满意度。阿姨们还告诉小飞,STC(Smart TeaCorp.)负责给研究院供应饮料,每天总量为V。STC很神奇,他们提供的每种饮料之单个容量都是2的方幂,比如王老吉,都是23=8升的,可乐都是25=32升的。当然STC的存货也是有限的,这会是每种饮料购买量的上限。统计数据中用饮料名字、容量、数量、满意度描述每一种饮料。

那么,小飞如何完成这个任务,求出保证最大满意度的购买量呢?

         基本上一看到这个题就可以确定它就是一个多重背包问题。使用上述的做法就可以解决这个问题。

 


混合背包问题

问题描述

         将0-1背包问题、完全背包问题和多重背包问题混合。也就是说,对于第i个物品的数量n[i]的取值范围存在三种情况:

问:放入哪些物品后,背包中物品价值总和达到最大。

基本思路

         只要将上述的三种背包过程进行组合就可以,伪代码如下:

for i=1..N

    if 第i件物品是01背包

        ZeroOnePack(c[i],w[i])

    else if 第i件物品是完全背包

        CompletePack(c[i],w[i])

    else if 第i件物品是多重背包

        MultiplePack(c[i],w[i],n[i])

 

 

 

 

 

 


二维背包问题

问题描述

         之前的问题都是建立在一维费用的背包问题基础上,二维背包就是扩展一个维度,即选择第i个物品会存在2种费用c1[i]和c2[i],物品的价值还是w[i],两种费用各自的上限值为V1、V2,每个物品的数量限制在1个。问:放入哪些物品后,背包中物品价值总和达到最大。

基本思路

         根据之前的方法,假设状态f[i][u][v]表示在容量分别为u,v的情况下第i个物品放入背包中达到的最优解。得到状态转移方程:

f[i][u][v]=max{f[i-1][u][v],f[i-1][u-c1[i]][v-c2[i]]+w[i]}

         时间复杂度为O(NV1V2),空间复杂度O(NV1V2)。根据0-1背包中的方法可以优化空间复杂度到O(V1V2)。

         算法的伪代码为:

procedure ZeroOnePackinTwoDimension(cost1,cost2, weight)

         for u=V1..cost1

                  for v=V2..cost2

                           f[u][v]=max{f[u][v],f[u-cost1][v-cost2]+weight}

举一反三

         上述的是物品只能取1次的情况,其实物品数量也可以不做限制。

Ø  如果物品数量无限多,则就变成一个二维上的完全背包问题,根据完全背包问题的状态转移方程也很容易得出二维情况下的状态转移方程为:

f[i][u][v]=max{f[i-1][u-k*c1[i]][v-k*c2[i]]| 0<=k<=min{u/c1[i],v/c2[i]}}

Ø  如果物品数量有限次,则就编程一个二维上的多重背包问题,根据多重背包问题的状态转移方程也很容易得出二维情况下的状态转移方程为:

f[i][u][v]=max{f[i-1][u-k*c1[i]][v-k*c2[i]]| 0<=k<=n[i]}

         至于如何优化,如何求解,都可以根据之前的方法进行扩展得到。

 

 

 

 

 


分组背包问题

问题描述

         有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。这些物品被划分为若干组K,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

基本思路

         首先,这个问题是一个0-1背包问题的变种,因为物品只能放入1次。另外其加入新的约束条件——同一个组中的物品只能放入一次。

         假设,已经知道前k-1组物品下放入背包内物品的最优情况,在考虑第k组的时候,依次考察k组中每个物品放入背包时的解,最后再从中选取最优的解即可得到前k组的最优解。

         写成状态转移方程就是:

f[k][v]=max{f[k-1][v], f[k-1][v-c[i]]+w[i]| 物品i属于第k组}

         如果写成伪代码如下:

for k=1..K

    GroupPack(cost[k][], weight[k][]);

         需要注意的就是红色标注部分,因为分组中的物品是互斥的,所以每个物品最多只能选择一次,所以在容量为v的背包进行求解的时候,就需要考察组中每个物品,最后将最优的物品放入背包。

procedureGroupPack(cost[], weight[])

for v=V..0

    for i=1..I

        tmp=max{max{f[v],f[v-c[i]]+w[i]},tmp}

    f[v]=tmp

         分组背包问题代码如下

表 7 分组背包问题代码

#definePACKAGESIZE1000

intf[PACKAGESIZE];

/**

    @argcost:第k组物品费用

    @argweight:第k组物品价值

    @argn:第k组物品数量

    @argpackSize:背包最大容量

*/

voidgroupPack(intcost[],intweight[],intn,intpackSize){

    for(intv=packSize;v>0;v--){

        intm=0;

        for(inti=0;i<n;i++){

            if(cost[i]>v){

                continue;

            }

            inttmp=max(f[v],f[v-cost[i]]+weight[i]);

            m=max(m,tmp);

        }

        f[v]=m;

    }

}

 

  


背包问题的其他变化

 

要求输出方案

         如果要求输出最后物品的选择方案,常规一些的思路就是利用一个二维布尔数组g[i][v]记录在求解过程中所有的物品选择情况,最后g[i][V]上的值就是方案内容。

         也可以简化这个数组的维数,因为输出的解是第V列上的情况,所以可以只用一个一维数组g[N]记录。伪代码描述如下:

if v=V&& f[v]<f[v-c[i]]

g[i]=1

         上述的是对于0-1背包问题下的输出方案。

         如果是完全背包问题和多重背包问题,使用一个二维数组g[i][v]记录在求解过程中的物品选择情况,伪代码如下:

iff[v]<f[v-c[i]]

g[i][v]=g[i][v-c[i]]+1

         如果是使用二维状态表示,则可以不使用二维数组,因为根据状态数组就可以推导出物品的选择情况,伪代码为:

iff[i][V]>f[i-1][V]

count= 0, v = V

while(f[i][v]!=f[i-1][v])

        count = count + 1

        v = v - cost


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值