背包问题总结

背包问题主要是分为三种:0-1背包,完全背包,多重背包

1:0-1背包

定义:

何谓0-1背包,可以这样想,那里有一堆值钱的东西,每一样东西只有一件,他们的价值和体积都不一样,现在要你从这n件里面挑选一些放到一个容量一定的背包里面,使得你的背包里的东西总价值最大。对于这些东西的每一件,你可以选择放进你的背包或者是不放进去。(这里放与不放就对应着两种状态0,1),所以称之为0-1背包。

解法:

为什么我们要选用动态规划的思想呢?

考虑到运用dp(动态规划)的几个步骤

 

1. 描述一个最优解的结构; 

2. 递归地定义最优解的值; 

3. 以“自底向上”的方式计算最优解的值;

4. 从已计算的信息中构建出最优解的路径

其中第1-3点是动态规划求解问题的基础,若题目只求最优解,则第四点可以省略。(以上几点都是算法导论上面的原话)

为了下面讲解方便,先我们规定f[i][v]来表示前i件物品放入到容量为v的背包里所获得的最大总价值,c[i]为第i件物品的体积,w[i]表示第i件物品的价值

现在对于这个问题,第i件物品不放或是放,则有两种状态,

第一种是不放,则f[i][v] = f[i-1][v] ;

第二种是放,则f[i][v]=f[i-1][v-c[i]]+w[i] ;

那么我们取哪个呢?当然是这样的 f[i][v] = max(f[i-1][v],f[i-1][v-c[i]]+w[i]) ;(dd大牛的背包九讲上说这个方程很重要,几乎所有的背包问题都是从这个方程衍生出来的)

于是从第1件物品开始,一件一件决定放还是不放。

我们可以写下伪代码

for i=1:n
for j=0:v
{
    if(j>=c[i])
    {
       f[i][j] = max{f[i-1][j],f[i-1][j-c[i]]+w[i]};
    }
    else
    {
      f[i][j] = f[i-1][j];//第i件物品放不进来
    }
}
当然我们得初始化f[0][0]-----f[0][v]都为0(以上伪代码省略)

滚动数组:

注意到我们的动态转移方程f[i][j] = max{f[i-1][j] , f[i-1][j-c[i]]+w[i]};

当前状态只与二维数组的前一行有关,那么之前的那些行都没用了啊,何必在那里浪费空间呢?这就引申了滚动数组的一个概念(其实就是一维数组啦)为何叫滚动呢?因为是一行一行,要求的当前行只与上一行有关。到此我们的0-1背包就可以用一维数组来求解了。看完下面,我们就可以彻底理解滚动数组了。

我们定义f[v]为前i件物品放进容量为v的背包里所获得的最大价值

于是写下动态转移方程:   f[v]=max{f[v] , f[v-c[i]]+w[i]}

那么这次写程序还是j从0开始循环么?(也就是上表从左到右一行一行遍历)显然不对,因为这样我们就没有利用到前一行所求的结果。只有j从v开始才能保证是从利用到前一行的值(仔细想想这里),因为我们在更新f[v]之前f[v-c[i]]的值还在嘛。再想想滚动数组,是不是有点体会了?滚动滚动,从右到左,既更新了当前的值,也利用到了上一行的值。大大节省了内存空间。

现在我们可以写下程序了

for i=1..n
   for j=v..0
        f[j]=max{f[j],f[j-c[i]]+w[i]};

二、完全背包

定义:

何谓完全背包呢?其实和0-1背包一样,只是现在每一样物品都有无限件可以选。只是一个量的不同。

 

与0-1背包的联系:

那么他又和0-1背包有什么联系呢?在此引用一下dd大牛讲的。我们可以这样想:第i件物品有v/c[i]件(因为背包最多放v/c[i]件编号为i的物品嘛)然后把他们都看成是体积相同,价值相同的不同物品,这不就成了0-1背包问题吗?当然这里只是讲一下二者的联系,如果这么去做。显然耗时是很多的。

 

求解:

稍微想一下我们就可以写下动态转移方程f[i+1][j] = max{f[i][j-k*c[i]]+k*w[i]} 0<=k<=j/c[i]

用白话讲就是第i件物品选k件放入背包所能获得的最大价值

显然我们可以写一个三重循环的程序出来,不过效率很低。稍微分析一下。

在f[i+1][j]的计算中选择k个的情况与在f[i+1][j-c[i]]的计算中选择k-1个的情况是一样的,所以f[i+1][j]的递推中k>=1的部分的计算已经在f[i+1][j-c[i]]的计算中完成了。那么可以根据如下方式进行变换。

f[i+1][j] = max{f[i][j-k*c[i]]+k*v[i]   k>=0}

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

            = max{f[i][j]   ,  max{f[i][(j-c[i])-k*c[i]]+v[i]+k*v[i]}    k>=0}  (注意联系k的取值范围)

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

于是可以写出下面的两重循环的程序

void  solve()
{
    for(int i=0;i<n;i++)
        for(int j=0;j<=W;j++)
    {
        if(j<w[i])
            dp[i+1][j] = dp[i][j];
        else
            dp[i+1][j] = max(dp[i][j],dp[i+1][j-c[i]+v[i]);
    }
}

同样根据0-1背包,我们也可以用一维数组优化空间

 

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

 

那么这次内层循环是从for j=0:v   还是for j=v:0呢?

 

先给出答案再解释

 

 
  1. for i=1..n

  2. for j=c[i]..v

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

初学者可能会感到诧异,为什么这里没看到取k个中的最大价值呢?

 

其实内层循环就隐含了这个意思了啊:先放第一件物品,内层循环就是试探当前放几个第i件物品,容量为j时取最大价值啊。

比如第一件体积是2 价值是3 背包总容量是7 一次循环之后f[1]=0,f[2]=3,f[3]=3,f[4]=6 f[5]=6 f[6]=9 f[7]=9

那么为什么这里j是从0...v呢?

先来对比一下0-1背包和完全背包的动态转移方程

0-1背包:     f[j]=max{ f[v] , f[j-c[i]]+w[i]}
完全背包:   f[j]=max{ f[j-k*c[i]] + k*w[i]  }    |0<=k*c[i]<=v

对于0-1背包,前面已经讲了要求f[v] ,则必须f[v-c[i]]的值还没更新,因为他是在两者之中取大值嘛。所以j从v倒退着走。

而对于完全背包,我们注意他的动态转移方程。要更新f[j]则必然先更新了f[j-k*c[i]],换句话说,一旦第i件物品加入之后,则势必影响之后的最优解,还是拿上面那个例子来说

第一件体积是2 价值是3 背包总容量是7 一次循环之后f[1]=0,f[2]=3,f[3]=3,f[4]=6 f[5]=6 f[6]=9 f[7]=9

加入第二件物品(体积是3,价值是5)之后 f[1]=0,f[2]=3,f[3]=5...从此之后f[3]就已经影响后面的解了。可以动手写写。

完全背包到此结束。。

 

三:多重背包

定义:

其实也是和0-1背包差不多,只不过是对于第i件物品,有有限个个数为m[i],这三种背包问题其实就只有一个区别嘛,那就是某件物品的量的差别。

 

解法:

有了前面的基础,写下这个动态转移方程还不是轻而易举么?

 

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

优化的方法:
运用神奇的二进制,进行物品拆分,转化成01背包
物品拆分,把13个相同的物品分成4组(1,2,4,6)
用这4组可以组成任意一个1~13之间的数!
原理:一个数总可以用2^k表示
而且总和等于13,所以不会组成超过13的数

所以可将一种有C个的物品拆分成1,2,4,...,2^(k-1),C-(2^k-1)
然后进行01背包

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值