背包问题 - 动态规划

1. 背包问题总结

暴力的解法是指数级别的时间复杂度。进而才需要动态规划的解法来进行优化!
背包问题是动态规划(Dynamic Planning) 里的非常重要的一部分,关于几种常见的背包,其关系如下:
在这里插入图片描述

2. 01背包

01背包问题是最基础的背包问题类型,它的特点是每种物品都只有一个,可以选择装入背包或不装入背包。每个物品的体积是vi,价值是wi,背包的体积是V。现在要求在不超过背包体积的情况下,选取物品的价值最大。

为理解此问题的实质,下面我用一个实际例子来进行讲解。假设现在有以下3件物品,以及一个容量为4的背包,现在你想知道,你的背包所能装下的最大价值是多少

物品名称体积价值
airpods11500
iwatch22000
iphone33000

最简单的办法,我们可以将这3件物品的所有组合枚举出来,然后求出每种组合的价值,最终输出最大值即可。高中时大家都学过集合,我们知道,对于某个具有n个元素的集合Φ,其子集个数为2n。也就是说对于有n件物品的集合,其可能的组合方案有2n个。比如对于上面这3件物品,其可能的组合就有23=8个,如下

组合方案总容量总价值
{}00
{airpods}11500
{iwatch}22000
{iphone}33000
{airpods,iwatch}33500
{airpods,iphone}44500
{iwatch,iphone}55000
{airpods,iwatch,iphone}66500

接下来我们将其中满足总容量不大于4的组合方案选出,并将其作为一种合法的选取,于是可以得到此类方案对应的总价值集合:{ 0,1500,2000,3000,4500 },最终取出其中的最大值4500即可(经验证,此为正确答案,即选择组合{ airpods,iphone})。
这样的算法很容易理解,但是弊端非常大:严重耗时。比如当待选物品有100件时,此时总的组合方案数将达到2100个,要枚举这所有的组合必然超时。而实际上,对于100件物品而言,这个数据范围却并不大,所以上面的枚举法是行不通的。
而动态规划算法的基本思想是将待求解问题分解成若干个子问题,然后再将这些子问题分阶段处理以避免重复计算来进行求解的。这里面最关键的一步,在于寻找问题的动态转移方程(即递推式)。接下来,我们依然通过填表来寻找这一规律,先将背包问题的网格绘出,如下:

1234
airpods
iwatch
iphone

其中每列表示容量不同的背包,每行表示当前可以选择的物品。
其中每个格子的含义为(假设当前为第i行、第j列):在当前背包容量为j、可选第i行及其之前的所有物品(按序排列)的前提下,能够选到的最大价值。
接下来我们需要填充其中的每个单元格,当网格填到最后一行最后一列时,即求到了在容量为V、可选所有商品的条件下背包所能容纳的最大价值。

首先是第一行(可选airpods),我们需要尝试把airpods装入背包以使背包的价值最大。在每个单元格,都需要做一个简单的决定:要不要airpods?别忘了,你要找出一个价值最高的商品集合。第一个单元格表示背包的容量为1,耳机的体积也是1,这意味着它能装入背包!因此,这个单元格包含耳机,价值为1500。于是可以填充网格,如下图所示:

1234
airpods1500
iwatch
iphone

与第一个单元格相同,每个单元格都将包含当前可装入背包的所有商品。来看下一个单元格,这个单元格表示背包的容量为2,完全能够装下airpods!并以此类推第一行的所有单元格均可装下airpods

1234
airpods1500150015001500
iwatch
iphone

现在你很可能心存疑惑:原来的问题说的是容量为4的背包,我们为何要考虑容量为1、2、3的背包呢?前面说过,动态规划是从小问题着手,逐步解决大问题。这里解决的子问题将帮助后面我们解决大问题。其实这正是体现动态转移的一个方面。

接下来填充第二行(可选airpods、iwatch)。我们先看第一个单元格,它表示容量为1的背包。在此之前,可装入容量为1的背包的商品最大价值为1500。现在面临一个新问题:该不该拿iwatch呢?当前背包的容量为1,能装下iwatch吗?太大了,装不下!由于容量1的背包装不下iwatch,因此最大价值依然是1500,如下:

1234
airpods1500150015001500
iwatch1500
iphone

接下来第二个单元格的容量为2可以装下iwatch,并且iwatch的价值比airpods大,所以我们装iwatch:

1234
airpods1500150015001500
iwatch15002000
iphone

接下来第三四个单元格的容量为3和4可以同时装下airpods和iwatch了,并且价值肯定是最大的, 所以我们装都装下:

1234
airpods1500150015001500
iwatch1500200035003500
iphone

然后就是第三行了,前两个单元格,我们之前都知道按照之前的计算就能得出:

1234
airpods1500150015001500
iwatch1500200035003500
iphone15002000

当到了第三个单元格,我们就能装下iphone了。但是iphone的价值是3000,而如果放airpods和iwatch价值是3500。当然这里不用再次计算airpods和iwatch的价值了,因为通过第二行的第三个单元格,我们就可以知道,所以我们此时不放iphone,最大价值仍然是3500:

1234
airpods1500150015001500
iwatch1500200035003500
iphone150020003500

到了第四个单元格了,我们可以选择iphone,并且选完后仍然有一个容量可以放airpods他们的价值是4500:

1234
airpods1500150015001500
iwatch1500200035003500
iphone1500200035004500

注:在计算最后一个单元格的最大价值时,我们选择拿iphone,此时还剩下1的容量,于是我们直接加上该行单元格中的第一个单元格内容(即3000+1500)便得到了这种方案下的总价值,最后再与之前的总价值(即3500)比较大小,并将较大者写入其中。这一操作,实际上就体现了动态转移的思想(以递推的方式取代递归求解)。

dp[i][j] = max( 上方单元格的价值,剩余空间的价值 + 当前商品的价值 )
= max( dp[i-1][j],dp[i-1][j-当前商品的体积] + 当前商品的价值 )
= max( dp[i-1][j],dp[i-1][j-w[i]] + v[i] )

看算法:

function maxBagProfit(){
    const weights = [1,2,3]
	const values = [15,20,30]
	const bagWeight = 4

	let dp = new Array(weights.length).fill(0).map(x=> new Array(bagWeight+1).fill(0));
	console.log("创建数组",dp)

	for (let i = bagWeight; i >= weights[0]; i--) {
		dp[0][i] = dp[0][i-weights[0]] + values[0];
	}
	console.log("初始化数组",dp)

	for (var i = 1; i < weights.length; i++) {
		for (var j = 0; j <= bagWeight; j++) {
			if (j < weights[i]) {
				dp[i][j] = dp[i - 1][j];
			}
			else {
				dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i]] + values[i]);
			}
		}
	}
	console.log("背包数组",dp)

	return dp[weights.length - 1][bagWeight]    
}

2. 完全背包

完全背包问题是背包问题的一个变种,其特点在于每种物品有无限个。
和01背包问题不同,01背包每种物品只能选择一次,而完全背包每种物品可以选择多次,甚至可以不选择。每个物品有体积vi和价值wi,背包的容量为V,目标是在不超过背包容量的前提下,选取物品的总价值最高。

我们还是用上面的例子来解释,因为我真的有airpods,iwatch,iphone,除此之外还有ipad和MacBook Pro当然ipencil也有了纯粹题外话过渡下。但是这里我要调整下iwatch和iphone的价格不然全选aipods就是最优解,再进行下面的推演就没有意义了。

物品名称体积·价值
airpods11500
iwatch23200
iphone35000

首先是第一行只能选airpods,所以根据数量无脑填充,如下图所示:

1234
airpods1500300045006000
iwatch
iphone

接着是第二行,我们可选的多了iwatch,对于第一个单元格只能选airpod,而而第二个就可以选iwatch了并且iwath的价值是大于两个aipods:

1234
airpods1500300045006000
iwatch15003200
iphone

到了第三个单元格时这时就是组合价值最高了

1234
airpods1500300045006000
iwatch150032004700
iphone

而第四个单元格就变复杂了,我们有两种选择方案,并且他们的价值都比上一行单元格的价值大,第一种是选一个iwatch和两个airpods他们的价值和是6200,而选两个iwatch是6400,所以就是两个iwatch(但是这里的算法显然就和之前的不一样了呀,应该怎么解决呢?)

1234
airpods1500300045006000
iwatch1500320047006400
iphone

到了第三行,前两个单元格放不下iphone,所以我们还是用上一行的替代:

1234
airpods1500300045006000
iwatch1500320047006400
iphone15003200

到了第三个单元格可以放下iphone并且价值最高:

1234
airpods1500300045006000
iwatch1500320047006400
iphone150032005000

到了最一个单元格,这里的可选方案就有选择iphone和一个airpods价值最大(到这里我已经想明白了上面疑问,其实不是有两种方案,而是始终只有一种方案就是选择两个iwatch,因为如果这个方案的价值小于四个airpod的话,那就说明就算选一个iwatch和两个airpods的价值也是小于四个aippods的价值)所以结果就是

1234
airpods1500300045006000
iwatch1500320047006400
iphone1500320050006500

dp[i][j] = max( 上方单元格的价值,剩余空间的价值 + 当前商品的价值*当前商品的数量 )
dp[i][j] = max( dp[i-1][j] , dp[i-1][ j - kw[i] ] + kv[i] )

看算法:

function maxBagProfit(){
    const weights = [1,2,3]
	const values = [15,32,50]
	const bagWeight = 4

	let dp = new Array(weights.length).fill(0).map(x=> new Array(bagWeight+1).fill(0));
	console.log("创建数组",dp)

	for (let i = bagWeight; i >= weights[0]; i--) {
		dp[0][i] = dp[0][i-weights[0]] + values[0]*Math.floor(i/weights[0]);
	}
	console.log("初始化数组",dp)

	for (var i = 1; i < weights.length; i++) {
		for (var j = 0; j <= bagWeight; j++) {
			if (j < weights[i]) {
				dp[i][j] = dp[i - 1][j];
			}
			else {
				let num = Math.floor(j/weights[i]);
				dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i]*num] + values[i]*num);
			}
		}
	}
	console.log("背包数组",dp)

	return dp[weights.length - 1][bagWeight]   
}

3. 多重背包

多重背包问题是背包问题中更一般化的问题。在多重背包问题中,每种物品不再是只有一个或者无限个,而是有限个,数量为mi
每个物品有体积vi和价值wi,背包的容量为V。现在要求背包装下的物品价值最大。更具体地,我们要确定每种物品应该选取多少个,才能使得选取的物品的体积不超过背包容量,同时使得价值最大。

不嫌麻烦的话可以继续看下面的推演,或者直接看结论。
这里的推演我们还是用01背包的数据,但是要加上数量限制:

物品名称体积价值数量
airpods115003
iwatch220002
iphone330001

这时候由于数量限制第一行的初始数据就变成了:

1234
airpods1500300045004500
iwatch
iphone

接着是第二行由于容量限制第一个单元格和上一个一样,第二个单元格选iwatch的话没有两个airpods的价值高,第三个单元格,选一个iwatch和一个airpods的价值低于三个airpods,所以还是不变:

1234
airpods1500300045004500
iwatch150030004500
iphone

到了第四个单元格,两个iwatch是4000,低于三个aipods,但是一个iwatch和两个airpods就超过了,(这里就要开始矛盾了,因为正常来说按照上面的算法我是不会算到一个iwatch和两个airpods,那就在下个括号中看看我能不能反应过来吧)

1234
airpods1500300045004500
iwatch1500300045005000
iphone

到了第三行,前三个单元格直接可以比较得出和上一行的数据一样

1234
airpods1500300045004500
iwatch1500300045005000
iphone150030004500

到了第四个单元格一个iphone和一个airpods的价值是4500小于5000,所以选5000

1234
airpods1500300045004500
iwatch1500300045005000
iphone1500300045005000

(针对上面提出的问题,我这里想出的方案就是再加上一层循环考虑所有的情况了)

dp[i][j] = max( 上方单元格的价值,剩余空间的价值 + 当前商品的价值*(1-当前商品的数量) )
dp[i][j] = max{dp[i-1][j], dp[i-1][j-vi]+wi, dp[i-1][j-2vi]+2wi, … , dp[i-1][j-ki*vi]+ki*wi},其中ki为 min{j/vi, mi}。

看算法:

function maxBagProfit(){
    const weights = [1,2,3]
	const values = [15,20,30]
	const nums = [3,2,1]
	const bagWeight = 4

	let dp = new Array(weights.length).fill(0).map(x=> new Array(bagWeight+1).fill(0));
	console.log("创建数组",dp)

	for (let i = bagWeight; i >= weights[0]; i--) {
		dp[0][i] = dp[0][i-weights[0]] + values[0]* Math.min(nums[0],Math.floor(i/weights[0]));
	}
	console.log("初始化数组",dp)

	for (var i = 1; i < weights.length; i++) {
		for (var j = 0; j <= bagWeight; j++) {
			if (j < weights[i]) {
				dp[i][j] = dp[i - 1][j];
			}
			else {
				let maxValue = dp[i - 1][j];
				let num = Math.min(nums[i],Math.floor(j/weights[i]));
				for (let k = 1; k <= num; k++) {
					maxValue = Math.max(maxValue, (dp[i - 1][j - weights[i]*k] + values[i]*k));
				}
				dp[i][j] = maxValue;
				
			}
		}
	}
	console.log("背包数组",dp)

	return dp[weights.length - 1][bagWeight]   
}

4.总结

01背包问题与完全背包问题实际上是两种极端,而多重背包问题则正是介于这两者之间的一种情况。基于此,我们可以将多重背包问题转化为01背包或完全背包问题来进行求解。

  • 可以把某种物品中的k个视为k种不同物品,此时再对所有物品按照01背包问题来进行处理。这样的转化当然是成立的,但是仅在数据范围较小时才适用,一旦每种物品的数量稍大一点,在时间上必然有超时的风险。此时,对于某种物品(假设有k个),若我们采用一种更精炼的划分方案,就会使得该物品分类下来的组数大大减少。比如可以采用二进制的拆分将原来的k个物品分为:{
    1、2、4、……、k - 2i + 1 } 这些组,以替代最初的分类:{ 1、1、1、……、1 }
    这些组,这是一个log2(n)级别的数量优化。
  • 若存在某个物品,其数量k乘以其单位体积大于背包总容量(即k*w[i] > V),那么此时对于该物品而言,它与背包之间是完全背包问题。

上述两点分别从01背包和完全背包的角度对多重背包问题进行了转化,而多重背包正好也是介于01背包和完全背包之间的问题。正是这两点,使得我们能设计出一个可以与“单调队列优化”分庭抗衡的算法。下面还是用一个实际例题来练手,以巩固理解。最后对于分组背包知道就好。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

全栈游戏开发

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值