背包问题详解

本文深入解析背包问题,包括部分背包、01背包、完全背包和多重背包。介绍了各种背包的定义、分析及代码实现,强调动态规划和贪心算法在解决这些问题中的应用,并通过实例说明了状态转移方程和代码实现过程。

导言

部分背包问题

0 1 背包问题

完全背包问题

多重背包问题

背包的分类

一级包,二级包和三级包,显然其中三级包的容量是最大的,吃鸡必备装备啊!
这里写图片描述

但是我们需要研究的是这样的问题:
这里写图片描述

各种背包

背包问题大概分下面这几种

  • 部分背包问题
  • 0 1 背包问题
  • 完全背包问题
  • 多重背包问题
问题

为什么要学习背包问题呢?

  • 背包问题已经是一个很经典而且讨论很广泛的算法问题了。
  • 背包问题的解决方法背后其实隐藏了两种我们比较常见的算
    法解决思路,动态规划和贪心算法。

部分背包

定义:

部分背包问题:
给出 n 个物体,第 i 个物体重量为 wi, 价值为 vi。在总重量
不超过 W 的情况下让总价值尽量高,每一个物体都可以只取走
一部分,价值和重量按比例计算。
分析
因为物体既有重量又有价值,所以不能简单的先拿轻的 (轻
的可能价值也小),也能先拿价值大的 (它可能特别重),而因该
综合考虑两个因素。一种直观的贪心策略就是:优先拿“价值除
以重量”最大的,直到重量和正好为 W

那么你作为一个非常优秀的 ACMer,肯定应该知道按照什
么顺序拿物品的把。没错,看着值钱的先抢!

那么问题很简单咯 ,把” 值钱” 的东西排在前面,每次拿抢
的时候,问问看背包君够不够承受得住,承受的了,就全部抢过
来。承受不住,那么只能按照所能承受的重量,取物品的一部分
了。当然价值也得按照比例来哦

部分背包没什么好讲的,主要是一个贪心策略,就是优先选取性价比最高的来装入背包


01背包

0 1 背包问题:

有 n 种重量和价值分别为 wi ,vi 的物品,从这些物品中挑选
总重量不超过 W 的物品,求出挑选物品价值总和的最大值。在
这里,每种物品只可以挑选一件
限制条件
  • 1n1001≤n≤100
  • 1wi;vi1001≤wi;vi≤100
  • 1W100001≤W≤10000
分析一下

01这道题就是各个物品“选”与“不选”的组合,因此被称为0−1背包问题 n如果检查n个物品所有“选”与“不选”的组合
O(2n)算法的复杂度为O(2n) 当物品的大小以及背包的大小均为正数
01O(nW)则0−1背包问题可以用动态规划法以O(nW)的效率解决。

状态转移方程:

i如果我们按照如下的方式来定义递推关系的话,刚刚关于i的循环就能正向进行
dp[i+1][j]:=0ii+1 j 我们定义dp[i+1][j]:=从0到i这i+1个物品中选出总重量不超过 j 的物品时总价值的最大值
dp[0][j]=00很显然dp[0][j]=0,因为你没物品可取的时候背包的价值为0

所以我们可以写出下边的状态转移方程

dp[i+1][j]={dp[i][j]max(dp[i][j],dp[i][jw[i]]+v[i])(j<w[i])dp[i+1][j]={dp[i][j](j<w[i])max(dp[i][j],dp[i][j−w[i]]+v[i])其他

代码实现1

根据这个状态转移方程我们可以写出这个代码,复杂度O(nW)O(nW)

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][j-w[i]]+v[i]);
printf("%d\n",dp[n][W]);
举个栗子

看一下这个例子
这里写图片描述

由于你非常优秀, 所以一眼就能够看出来这个问题的答案是多少
但是我们需要研究一下解决 01 背包的过程中 dp 数组的状态转移过程
所以我们来用这个简单地样例来观察一下 dp 数组的状态是怎么变化的

取第 0 个物品 (w0;v0)=(2;3)(w0;v0)=(2;3)

这里写图片描述

取第 0 个物品 (w0;v0)=(2;3)(w0;v0)=(2;3)

这里写图片描述

取第1个物品(w1,v1)=(1,2)(w1,v1)=(1,2)

这里写图片描述

取第1个物品(w1,v1)=(1,2)(w1,v1)=(1,2)

这里写图片描述

取第1个物品(w1,v1)=(1,2)(w1,v1)=(1,2)

这里写图片描述

取第1个物品(w1,v1)=(1,2)(w1,v1)=(1,2)

这里写图片描述

取第2个物品(w2,v2)=(3,4)(w2,v2)=(3,4)

这里写图片描述

取第2个物品(w2,v2)=(3,4)(w2,v2)=(3,4)

这里写图片描述

取第3个物品(w3,v3)=(2,2)(w3,v3)=(2,2)

这里写图片描述

取第3个物品(w3,v3)=(2,2)(w3,v3)=(2,2)

这里写图片描述

再啰嗦一下

希望大家能够根据上边的例子完全的理解01背包的实现过程,因为01背包非常重要的,它是背包问题的基础,基础牢固对其他动态规划的学习也有帮助的。

01背包代码实现2
  • 此外,01背包的还可以通过两个数组滚动来实现重复利用
int dp[2][MAXN];
for(int i=0;i<n;i++)
    for(int j=0;j<=W;j++)
       if(j<w[i])
          dp[(i+1)&1][j]=dp[i&1][j];
       else
  dp[(i+1)&1][j]=max(dp[i&1][j],dp[i&1][j-w[i]]+v[i]);

printf("%d\n",dp[n&1][W]);
01背包代码实现3
  • 当然,还可以通过不断重复用一个数组来实现
int dp[MAXN];
for(int i=0;i<n;i++)
    for(int j=W;j>=w[i];j--)
        dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
printf("%d\n",dp[W]);

完全背包

问题提出

n有n种重量和价值分别为w[i],v[i]的物品
从这些物品中挑选总重量不超过W的物品
求出挑选物品价值总和的最大值。在这里,每种物品可以挑选任意件

  • 限制条件
    • 1n1001≤n≤100
    • 1wi,vi1001≤wi,vi≤100
    • 1W100001≤W≤10000

递推关系

这次同一类的物品可以挑选任意多件了。我们再试着写出递推关系。
 dp[i+1][j]=i j 令 dp[i+1][j]=从前i种物品中挑选总重量不超过 j 时总价值的最大值。那么递推关系就为:

dp[0][j]=0dp[0][j]=0

dp[i+1][j]=max{dp[i][jkw[i]]+kv[i]|0<=k}dp[i+1][j]=max{dp[i][j−k∗w[i]]+k∗v[i]|0<=k}

如果按照这个递推关系来写程序话,代码是这样子的:

  • 完全背包代码实现1
for(int i=0;i<n;i++)
    for(int j=0;j<=W;j++)
        for(int k=0;k*w[i]<=j;k++)
    dp[i+1][j]=max(dp[i+1][j],dp[i][j-k*w[i]]+k*v[i]);
printf("%d\n",dp[n][W]);

这个代码并不友好,拥有三重循环,时间复杂度是O(nW2)O(nW2)

上边的代码有很多重复计算,将状态转移方程化简一下 得到的是这样子的
这里写图片描述

这样一来就不需要k的循环了,便可以用O(nW)O(nW)时间解决问题

  • 完全背包代码实现2
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-w[i]]+v[i]);
printf("%d\n",dp[n][W]);
举个栗子

我再来举个栗子
如图所示

这里写图片描述

你当然能够一眼看出来背包的最大价值是多少
为了帮助你理解完全背包状态的转移过程
dp我们可以根据这个例子来看一下dp数组的状态是怎么转移的

取第0个物品(w0,v0)=(3,4)(w0,v0)=(3,4)

这里写图片描述

取第0个物品(w0,v0)=(3,4)(w0,v0)=(3,4)

这里写图片描述

取第0个物品(w0,v0)=(3,4)(w0,v0)=(3,4)

这里写图片描述

取第1个物品(w1,v1)=(4,5)(w1,v1)=(4,5)

这里写图片描述

取第1个物品(w1,v1)=(4,5)(w1,v1)=(4,5)

这里写图片描述

取第2个物品(w2,v2)=(2,3)(w2,v2)=(2,3)

这里写图片描述

取第2个物品(w2,v2)=(2,3)(w2,v2)=(2,3)

这里写图片描述

完全背包代码实现2
  • 当然我们也可以像01背包那样只开2个二维数组通过滚动来实现数组的重复利用
int dp[2][MAXN];
for(int i=0;i<n;i++)
    for(int j=0;j<=W;j++)
        if(j<w[i])
            dp[(i+1)&1][j]=dp[i&1][j];
        else
dp[(i+1)&1][j]=max(dp[i&1][j],dp[(i+1)&1][j-w[i]]+v[i]);

printf("%d\n",dp[n][W]);
完全背包代码实现3
  • 只开一维数组也可以的
int dp[MAXN];
for(int i=0;i<n;i++)
    for(int j=w[i];j<=W;j++)
        dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
printf("%d\n",dp[W]);

上边的代码是不是很熟悉?
上边的代码是不是很熟悉?没错,和01背包的代码除了第二个for循环不一样之外,其他的地方一模一样!
为什么会是这个样子呢?

完全背包与01背包关于第二个循环的分析
  • 01背包的是第一个的代码,完全背包的是第二个代码
for(int j=W;j>=w[i];j--)
for(int j=w[i];j<=W;j++)

 01 j首先我们来想一下为什么 01 背包中要按照j递减的次序来循环。
ji dp[i][j]  dp[i1][jw[i]] 让j递减是为了保证第i次循环中的状态 dp[i][j] 是由状态 dp[i−1][j−w[i]] 递推而来的。
i换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,
i dp[i1][jw[i]] .依据的是一个绝无已经选入第i件物品的子结果 dp[i−1][j−w[i]] .

  • 01背包的是第一个的代码,完全背包的是第二个代码
for(int j=W;j>=w[i];j--)
for(int j=w[i];j<=W;j++)

而现在完全背包的特点恰是每种物品可选无限件

i所以在考虑“加一件第i种物品”这种策略时

idp[i][jw[i]]却正需要一个可能已选入第i种物品的子结果dp[i][j−w[i]]

v所以就可以并且必须采用v递增的顺序进行循环

这就是这个简单程序为何成立的道理


多重背包

问题与应用

有n种重量和价值分别为w[i]v[i]w[i],v[i]的物品,从这些物品中挑选总重量不超过WW的物品,求出挑选物品价值总和的最大值。不过在这里,每种物品最多可以挑选mi

  • 限制条件
    • 1n1001≤n≤100
    • 1wi,vi1001≤wi,vi≤100
    • 1mi100001≤mi≤10000
    • 1W100001≤W≤10000

状态转移方程

dp[i][j]:= ijdp[i][j]:= 到第i个物品为止总重量不超过j的所有选法中最有可能的最大值

dp[i+1][j]=max(dp[i][jkw[i]]+kv[i]|0km[i])dp[i+1][j]=max(dp[i][j−k∗w[i]]+k∗v[i]|0≤k≤m[i])

分析分析

对第i种物品有mi+1mi+1件策略:取0件,取1件…….取mimi件,复杂度是O(nmW)O(nmW),无法在规定时间内求解


一种好想好写的基本方法是将其转化为01背包问题求解:

把第ii件物品换成mi件01背包中的物品,也就是说:

我们将mimi件物品,变成0mi0…mi个物品,

则得到的物品数为mi∑mi的01背包问题

如果直接求得话,复杂度仍然是O(nmW)O(nmW)


但是我们希望将它转换成01背包问题之后,能够想完全背包一样降低复杂度。

考虑二进制的思想,把第i件物品换成若干件物品,

使得原问题中的第i件物品可取的每一种策略——取0…mimi

均能够等价于取若干件物品代换之后后的物品,另外,取mimi件的策略必不能够出现


mimi分解为如下形式:

mi=1+2+4++2k+a(0a2k+1)mi=1+2+4+⋯+2k+a(0≤a≤2k+1)

由于1,2,,2k1,2,…,2k 的组合可以表示出 02k+110⋯2k+1−1 的所有整数,因此 1,2,2k,a1,2,⋯2k,a 可以表示出所有 0 mi0 mi 的所有整数

因此,我们把mimi个重量和价值分别为wiwivivi的物品,
看成重量和价值分别为wix,vix(x=1,2,,2k,a)wi∗x,vi∗x(x=1,2,⋯,2k,a)k+2k+2个物品。

这样,物品的总个数就变为O(nlog m)O(nlog m)个,
使用一般的01背包DP可以在O(nWlogm)O(nWlogm)时间内求出答案


举个栗子

比如说给我的第i个物品,重量为2,价值100,数量13;
按照上边二进制的形式将13分解之后13=1+2+4+613=1+2+4+6所以我们就得到了4个物品,这四个物品分别是

系数重量w价值v备注
121001个i
242002个i
484004个i
6126006个i

我们可以通过对这四个物品取或者不取
可以达到对第i个物品取0,1,2130,1,2⋯13件的操作
所以我们的目的就达成了,将其变成01背包的问题!

多重背包代码实现
for(int i=0;i<n;i++){
    int num=m[i];//用来找a 
    for(int k=1;num>0;k<<=1){
        int mul=min(k,num);
        for(int j=W;j>=w[i]*mul;j--){
            dp[j]=max(dp[j],dp[j-w[i]*mul]+v[i]*mul); 
        }
        num-=mul;
    }
}
printf("%d\n",dp[W]);

个人拙见,如果有错误请在留言区指出
emmm,希望对大家学习背包有所帮助

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值