背包九讲(一)----01背包问题

背包九讲(一)----01背包问题

参考了著名的背包九讲,可以在这里下载,在这里做一个个人笔记,当然过程中有些不懂的地方也参考了许多其他博客,如果能帮助到你那就更好了^ ^

1.1 问题

N件物品和一个容量为V的背包,放入第i件物品的费用是 C i 1 C_i^1 Ci1,得到的价值是 W i W_i Wi。求解将哪些物品装入背包可使价值综合最大。

1.2 解题思路

最基础的背包问题,每种物品只有一件,可以选择放或者不放。

用子问题来获取状态方程:即 F [ i , j ] F[i, j] F[i,j] 表示前 i i i 件物品恰放入一个容量为 v v v 的背包可以获得的最大价值

d p [ i , v ] = m a x { d p [ i − 1 , v ] , d p [ i − 1 , v − C i ] + W i } dp[i,v] = max\left\{ dp[i-1, v], dp[i-1, v-C_i]+ W_i \right\} dp[i,v]=max{dp[i1,v],dp[i1,vCi]+Wi}

可能刚开始看到这个方程会比较懵,接下来通过一个实例来理解一下,这个栗子参考了这篇博客(帮助很大)。

假设我们有一个可以装下4磅东西的背包,可以装的物品如下:

背包九讲(一)----01背包问题_第1张图片

当然可以选择暴力解法,穷举所有可能的装法(当然会有很多装法超出容量),然后选择价值最大的装法。每个商品都有两种状态(装或不装),所以算法的时间复杂度为 O ( 2 N ) O(2^N) O(2N),显然不合理。

背包九讲(一)----01背包问题_第2张图片

然后就是动态规划解法,将装满一个大背包转化为装满一个小背包的问题。

背包九讲(一)----01背包问题_第3张图片

从上面我们的状态转移方程就能看出,求解的过程就是填写一个网格(二维数组),当将网格填写完整后,我们的问题也就解决了。我们当前问题的网格如下:

背包九讲(一)----01背包问题_第4张图片

我们接下来依次填写各行。

① 首先是吉他行,在这一行中,我们在不同背包容量情况下考虑是否装入吉他,判断的依据就是是否能够获得更高的价值

这种情况十分简单,因为目前只有吉他一种物品,所以最优情况当然是选择装入吉他!

背包九讲(一)----01背包问题_第5张图片

② 其次是音响行,我们需要考虑是否装入音响,这时我们的状态转移方程就派上用场了。

d p [ 音 响 , v ] = m a x { d p [ 音 响 之 前 , v ] , d p [ 音 响 之 前 , v − C 音 响 ] + W 音 响 } dp[音响,v] = max\left\{ dp[音响之前, v], dp[音响之前, v-C_{音响}]+ W_{音响} \right\} dp[,v]=max{dp[,v],dp[,vC]+W}

由于音响为4磅,所以前三格都只能选择 d p [ 音 响 之 前 , v ] dp[音响之前, v] dp[,v],如下图所示:

背包九讲(一)----01背包问题_第6张图片

对于第四格 d p [ 音 响 , 4 ] = m a x { 1500 , 3000 + 0 } = 3000 dp[音响,4] = max\left\{ 1500, 3000+ 0 \right\} = 3000 dp[,4]=max{1500,3000+0}=3000 ,即选择音响(剩余容量为0,什么也装不下)

背包九讲(一)----01背包问题_第7张图片

笔记本电脑行,同样的方法,使用状态方程求解。

由于笔记本为3磅,所以前两个单元格直接继承上一行的结果:

背包九讲(一)----01背包问题_第8张图片

对于第三格 d p [ 笔 记 本 电 脑 , 3 ] = m a x { 1500 , 2000 + 0 } = 2000 dp[笔记本电脑,3] = max\left\{ 1500, 2000+ 0 \right\} = 2000 dp[,3]=max{1500,2000+0}=2000 ,即选择音响(剩余容量为0,什么也装不下)

背包九讲(一)----01背包问题_第9张图片

对于第四格:

d p [ 笔 记 本 电 脑 , 4 ] = m a x { d p [ 笔 记 本 电 脑 之 前 , 4 ] , w 笔 记 本 电 脑 + d p [ 笔 记 本 电 脑 之 前 , 1 ] } dp[笔记本电脑,4] = max\left\{ dp[笔记本电脑之前, 4], w_{笔记本电脑}+ dp[笔记本电脑之前, 1] \right\} dp[,4]=max{dp[,4],w+dp[,1]}

= m a x { 3000 , 2000 + 1500 } = 3500 = max\left\{ 3000, 2000+ 1500 \right\} = 3500 =max{3000,2000+1500}=3500

然后我们就填满了表格,也就得到了最优解。

背包九讲(一)----01背包问题_第10张图片

通过这个栗子就能清楚的看到整个问题解决的过程,每当我们要选择装入一个新的物品时(当前行),我们就参考前面的结果(前一行),由此来做出决策,这样对状态转移方程也总算时理解了。

这样我们就能写出这个过程的伪代码了:

dp[0,0...V]0
for i ← 1 to N  // 依次遍历N件物品
	for v ← 1 to Ci	// 装不下当前物品的背包直接继承前值
		dp[i, v] ← dp[i-1,v]
	for v ← Ci to V // 能装下当前物品的背包使用状态方程
		dp[i, v] ← max{dp[i-1,v], dp[i-1,v-Ci]+Wi}

1.3 优化空间复杂度

以上方法的时间和空间复杂度为 O ( V N ) O(VN) O(VN),相比较于暴力穷举 O ( 2 N ) O(2^N) O(2N) 当然是快很多,但是还能在空间复杂度上做进一步优化。

从上面的栗子可以看出,我们考虑新的物品是否装入背包时,只需要参考前一行的结果,之前保存的结果后面都用不上了,所以我们并不需要一直保存着这样一个二维的矩阵。

并且我们看到这样一个事实,我们只参考了小于等于当前坐标的结果,即 d p [ i − 1 , v ] , d p [ i − 1 , v − C i ] dp[i-1, v], dp[i-1, v-C_i] dp[i1,v],dp[i1,vCi](第 v v v 个格子和第 v − C i v-C_i vCi 个格子)

基于上面事实,我们其实只需要使用一个一维数组就能实现整个过程了,状态方程为:

d p [ v ] = m a x { d p [ v ] , d p [ v − C i ] + W i } dp[v] = max\left\{ dp[v], dp[v-C_i] + W_i \right\} dp[v]=max{dp[v],dp[vCi]+Wi}

但是要求 v v v 按照递减的顺序从 V , V − 1 , . . , 0 V,V-1,..,0 V,V1,..,0 计算 d p [ v ] dp[v] dp[v] ,这样就能保证整个运算过程的正确性。

此时的空间复杂度就下降到了 O ( V ) O(V) O(V) ,但是时间复杂度并没有降低。

下面时这个过程的伪代码:

dp[0...V]0
for i ← 1 to N  // 依次遍历N件物品
	for v ← V to Ci // 能装下当前物品的背包使用状态方程
		dp[v] ← max{dp[v], dp[v-Ci]+Wi}

1.4 初始化的细节问题

在上述01背包的基础上,如果要求恰好装满背包,那应该如何求解?

其实和前面的区别仅仅时初始化的方式不同。之前我们初始化就是将 d p [ 0... V ] dp[0...V] dp[0...V] 都初始化为0, 含义就是无论当前背包容量为多少,此时获取的最大价值为0;

而现在要求恰好装满背包,我们只需要将 d p [ 0 ] dp[0] dp[0] 初始化为0,其余 d p [ 1... V ] dp[1...V] dp[1...V] 初始化为 − ∞ -\infty ,含义就是只有 d p [ 0 ] dp[0] dp[0] 一个合法的解(此时恰好装满),其他情况都不是合法的解(赋值为 − ∞ -\infty )。

这样就能过完美解决这样一个情况了

1.5 一个常数优化

对于不需要恰好转满背包的情况,我们最终的结果是 d p [ V ] dp[V] dp[V] ,而获取这一结果我们只需要前面 d p [ V − C N ] dp[V - C_N] dp[VCN] 的结果,接着向前追随就只需要 d p [ V − C N − C N − 1 ] dp[V - C_N - C_{N-1}] dp[VCNCN1] 的结果,由此我们就能在一定程度上简化计算。

优化后的伪代码如下:

dp[0...V]0
for i ← 1 to N  // 依次遍历N件物品
	bound = max(Ci, sum(Ci+1, Ci+2, ..., CN))
	for v ← V to bound // 能装下当前物品的背包使用状态方程
		dp[v] ← max{dp[v], dp[v-Ci]+Wi}

1.6 示例代码

代码部分:

int knapsack01(vector<int> weight, vector<int> value, int volume){
	vector<int> dp(volume + 1);
	for(int i = 0; i < weight.size(); i++){
		for(int v = volume; v >= weight[i]; v--){
			dp[v] = max(dp[v], dp[v-weight[i]] + value[i]);
		}
	}
	return dp[volume];
} 

输入样例:

vector<int> weight = {4, 3, 1};
vector<int> value = {3000, 2000,1500};
int volume = 4

输出结果:

3500

你可能感兴趣的:(算法,算法,动态规划,数据结构)