数据结构——背包问题

折月煮酒 提交于 2020-02-23 14:22:26

引言

算法的经典问题之一背包问题,背包问题是动态规划(运筹学)的一个典型的例子,它的问题描述即规定背包所能容纳的最大重量,然后此时有一批物品对应不同的质量和价值,那么如何放置物品进入背包使得背包中的价值最大。

通常情况下,背包问题会分为两种情形:

  • 物品可以切开放置到背包中,即物品的重量和价值可以按分数的形式放置
  • 物品只能以整数的形式放置

前一种情况,只能用贪心算法解决去解决问题。后一种情况,则需要通过动态规划。

背包问题(整数)

首先,我们先来认识一下动态规划解决问题的步骤:

  • 定义子问题
  • 实现要反复执行来解决子问题的部分
  • 识别并求解出基线条件

然后假设此时我们有一个这样的背包问题:

此时有一个背包可容纳的重量为 7,分别有重量为 2、3、4 对应价值为 3、4、5 的三个物品 ,并分别命名为物品 1、物品 2、物品 3。要如何往背包中放置物品,才能使得背包中的价值最高?

对于从事编程的同学而言,遇到略显复杂的问题,我们应该先建立数学模型。而对于这个背包问题,它的数学模型是这样的:

背包所能容纳的重量 w:5
物品	重量	价值
1		2		3
2		3		4
3		4		5

(矩阵的行数为物品数量+1,列为背包所能容纳重量+1,初始化默认 00 行为 0)
第一阶段
i/w	0 	1	2	3	4	5
0	0	0	0	0	0	0
1	0   0   3	3	3	3
2	0
3	0

第二阶段
i/w	0 	1	2	3	4	5
0	0	0	0	0	0	0
1	0   0   3	3	3	3
2	0	0	3	4	4	7
3	0	0	3	4	5	7(最高价值)	

可以看出对于我们这个背包问题,只需要两个阶段就可以放入背包的物品最高价值。但是,我们还需要找出对应价值的物品(这个过程就不建模了,比较简单,直接看代码)。

/*
	capacity 背包所能容纳的最大重量
	n		 物品的数量
	kS		 最优方案对应的矩阵
	weights  物品对应的重量数组
	values   物品对应的价值数组
*/
function findGoods(n, capacity, kS, weights, values) {
	let i = n
	let k = capacity
	while(i > 0 && k > 0) {
		debugger
		if (kS[i][k] !== kS[i - 1][k]) {
			console.log(`物品${i}可以是解的一部分, 重量:${weights[i - 1]}, 价值:${values[i - 1]}`)
			i--
			k -= kS[i][k]
		} else {
			i--
		}
	}
}

既然,我们已经写好背包问题的数学模型,以及准备好寻找对应价值的物品的函数。接下来,就把背包问题对应数学模型的算法实现:

/*
	capacity 背包所能容纳的最大重量
	weights  物品对应的重量数组
	values   物品对应的价值数组
	n		 物品的数量
*/
function knapSack(capacity, weights, values, n) {
		// 初始化用于寻找解决方案的矩阵
		const kS = []
		for (let i = 0; i <= n; i++) {
			kS[i] = []
		}
		for (let i = 0; i <= n; i++) {
			for (let w = 0; w <= capacity; w++) {
				// 令矩阵中索引值为0的项值为0
				if (i === 0 || w === 0) {
					kS[i][w] = 0
				} else if (weights[i - 1] <= w) { // 物品的重量需要小于等于约束条件
					const a = values[i - 1] + kS[i - 1][w - weights[i - 1]]
					const b = kS[i - 1][w]
					kS[i][w] = a > b ? a: b
				} else { // 超出背包的重量
					// 使用之前的值
					kS[i][w] = kS[i - 1][w]
				}
			}
		}
		findValues(n, capacity, kS, weights, values)
		return kS[n][capacity]
	}

这个算法其实非常简单,正如我们前面描述的数学模型一样,矩阵中满足约束条件 weights[i - 1] <= w 的进行相应的计算,然后对比矩阵此时索引中上一行对应列的值,谁大此时对应索引值就为谁。反正,如果不满足约束条件,就取矩阵此时索引中上一行对应列的值。

背包问题(分数)

对于背包问题的分数版,动态规划就无能为力了。这个时候,就需要用贪心算法解决,先来看看贪心算法的定义。贪心算法是一种近似解决问题的技术,通过每个阶段的局部最优解,从而达到全局最优解的效果。简单地理解,就是类似于归纳法推出符合大多数情况下公式,然后通过这个公式求解。

function knapSack(capacity, weights, values) {
	const n = values.length
	let load = 0
	let val = 0
	for (let i = 0; i < n && load < capacity; i++) {
		if (weights[i] <= capacity - load) {
			val += values[i]
			load += weights[i]
		} else {
			const r = (capacity -load) / weights[i]
			val += r * values[i]
			load += weights[i]
		}
		return val
	}
}

对比动态规划的背包问题,贪心算法不仅贪心(简洁)而且更易理解,只要我此时背包剩余的空间能容纳下这个物体我就放进去,不能那我就放这个物体的一部分。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!