动态规划+背包问题

动态规划+背包问题

背包问题往往可以抽象成:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。背包问题这个总体概念可以划分出如0/1背包问题 、完全背包问题、多重背包问题等经典题目。

0 / 1背包问题

0表示背包未满,1表示已经放满,因此可以转化为拿或者不拿的问题:在容量有限的情况下,我们是否要拿起物品放入背包中,替换掉以前放入的物品,保证背包中的价值最大。

题目背景在这里插入图片描述题目分析

当每次放入新物品的时候,我们都需要和上一次放入的物品信息进行比对,比较出价值最大的放法。 所以我们需要记录以前已经解决了的问题信息,即使用动态规划的记忆性来进行填表解题。

在这里可以用一个二维dp数组来表示存放的记录,对于dp[i][j]来说,i表示物品的数量,j表示背包的容量,dp[i][j]本身则表示背包中已有的总价值。比如dp[1][0]表示当背包容量为0时,我放入第一个物品,dp[n][m]表示当背包容量为m时,放入第n个物品。

同时,使用w[]数组存放所有的重量信息,v[]存放所有的价值信息,绘制出一个表格。

至于这里为什么在设计表时,行列的开头都为0,会在后面的分析中提到。

w[i]v[i]dp[i][j]
012345678910
0
211
332
453
794

首先看第一行,当待放入的数据是第0件物品时,由于它并不存在,所以价值当然为零,直接填入。

w[i]v[i]dp[i][j]
012345678910
000000000000
211
332
453
794

接下来看第二行,当背包容量为0,1时,j < w[1],无法放入,背包内价值为0;当背包容量为2时,此时恰好可以放入第一件物品,包内价值为v[1]所表示的1。再往右看由于j都大于w[1],所以后面的内容都填1。

w[i]v[i]dp[i][j]
012345678910
000000000000
21100111111111
332
453
794

接下来再看第三行,当背包容量为2时,j < w[2],无法放入,背包内价值为上一次放入物品后的1;当背包容量为3时,可以放入重量分别为1、2的1号与2号物品。由于已经放了重量为2 (w[1])的物品,此时最多只能再放入重量为1的物品了。可是,如果将1号物品取出,放入2号物品的话,此时包内价值为更高的3。因此这里可以做一个分析:

  • 当 j < w[i] 时,待分析的物品比背包容量要重,只能选择不拿。此时背包内的价值就是上一次操作后的背包内价值。

    即: dp[i][j] = dp[i-1][j]

  • 当 j >= w[i]时,待分析的物品比背包容量要轻,可以选择拿或者不拿。如果不拿,背包内的价值就是上一次操作后的背包内价值;如果要拿,此时背包容量就会减少w[i],需要查看上一次操作时容量为 j - w[i]时的最优解。

    即: dp[i][j] = dp[i-1][j-w[i]] + v[i]

按照这个分析来看此时背包容量为 3 的情况,在放入1号物品后,如果不放入2号物品,此时包内的价值为1;如果选择放入2号物品,dp的值为 dp[1][3-3] + 3,结果为3。在 1 和 3 之间经过比较后选择最大值的方案 。

因此可以得出一个状态转移方程式 max { dp[i-1][j], dp[i-1][j-w[i]] + v[i] }

不难看到,在方程式中会出现[i-1]、[j - w[i]]的问题,所以如果我们在填表的时候从0开始计数可能会有报错,所以从1开始计数。

为了验证这个方程式,我们再来看看当容量为5时的最大价值为max { dp[2-1][5], dp[2-1][5-w[2]] + c[2] } = 4,符合逻辑

w[i]v[i]dp[i][j]
012345678910
000000000000
21100111111111
33200133444444
453
794

题解

接下来我们就可以按照这个方程式进行编码解题了。

#include<iostream>
#include<algorithm>
using namespace std;

int main(){
	int m, n; // 背包容量为m 物品个数为n
	int w[10];// 具体使用到到 n+1
	int v[10];// 具体使用到到 n+1
	int dp[20][20] = { 0 }; // 具体使用到到 m+1
	cin >> m >> n;
	for (int i = 1; i <= n; ++i) {
		cin >> w[i] >> v[i];
	}

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

	// 输出动态规划解
	for (int i = 0; i <= n; i++) {
		for (int j = 0; j <= m; j++) {
			cout << dp[i][j] << ' ';
		}
		cout << endl;
	}
	return 0;
}

通过滚动数组进行优化

现在可以考虑一下优化方面的问题,我们回到之前的表格可以看到,最新一行的数据只与他的上一行数据有关,当我们分析到3号物品的时候,只用考虑2号物品的那一行,也就是说我们可以在空间上进行优化,不需要记录所有的数据。因此可以考虑将二维数组压缩至一维数组,当一次计算完成后将最新的数据覆盖掉上一行操作中所产生的数据(滚动数组)。

创建一个dp[j]数组,分析完1号物品后进行填表操作,分析完2号后覆盖这一行的数据。

w[i]v[i]dp[i][j]
012345678910
21100111111111

使用二维数组时的推导方程

if (j < w[i])
	dp[i][j] = dp[i - 1][j];
else
	dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);

使用一维数组时的推导方程

if (j >= w[i])
	dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
w[i]v[i]dp[i][j]
012345678910
33200111111111

需要注意的是,现在改为了一维数组后,dp[j - w[i]]位于dp[j]的前方,每次推dp[j]的时候需要从后往前推。因为如果从前往后推的话上一次计算出来的dp[j]会被覆盖掉,而从后往前推可以恰好用到上一次的数据。可能这句话会有些绕口,结合二维数组再来看看。

w[i]v[i]dp[i][j]
012345678910
000000000000
21100111111111
33200133444444
45300135

在二维数组中进行判断是否要取物品放入背包时当前的值需要和dp[i-1][j-w[i]]进行判断(见黑体部分),可如果放在一维数组中从前往后判断的话,dp[2]所对应的值就会经过计算修改为0,导致后面无法得到正确解。因此内层循环需要从后往前推,简单来说,后面的值需要通过前面的值来进行判断,所以不能先修改前面的值

#include<iostream>
#include<algorithm>
using namespace std;

int main(){
	int m, n; // 背包容量为m 物品个数为n
	int w[10];// 具体使用到到 n+1
	int v[10];// 具体使用到到 n+1
	int dp[20] = { 0 }; // 具体使用到到 m+1
	cin >> m >> n;
	for (int i = 1; i <= n; ++i) {
		cin >> w[i] >> v[i];
	}

	for (int i = 1; i <= n; i++) {
		for (int j = m; j >= 1; j--) {
			if (j >= w[i])
				dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
		}
		for (int k = 0; k <= m; k++) {
			cout << dp[k] << ' ';
		}
		cout << endl;
	}
	return 0;
}

完全背包问题

和0/1背包不同的是,在0/1背包中每个物品只能取一次,你只能选择取或者不取;而在完全背包中,某个物品可以取无数次。

题目背景在这里插入图片描述题目分析

从物品的数量来看,数量的下限为0,数量的上限为背包的最大容量。用当前背包的容量 j / w[i] 时便可以计算出某个物品最多可以取多少个。在0/1背包中我们这样计算dp:

for (int i = 1; i <= n; i++) {
		for (int j = m; j >= 1; j--) {
			if (j >= w[i])
				dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
		}
		for (int k = 0; k <= m; k++) {
			cout << dp[k] << ' ';
		}
		cout << endl;
}

现在在这个二重循环中,可以再嵌套一层循环,用来表示某个物品可以被取的次数,这样写可以解出完全背包题目。

for (int i = 1; i <= n; i++) {
    for (int j = m; j >= 1; j--) {
    	for (int k = 0; k <= j / w[i]; k++){
    		if (j >= w[i])
    			dp[j] = max(dp[j], dp[j - k * w[i]] + k * v[i]);
    	}
    }
    for (int k = 0; k <= m; k++) {
    	cout << dp[k] << ' ';
    }
    cout << endl;
}

优化

虽然说三重循环可以解出正确答案,但是这样时间复杂度太高了,需要找出一个更优解,首先还是进行填表。

w[i]v[i]dp[i][j]
012345678910
000000000000
21100112233445
3320013346
453
794

推出状态转移方程式,由于同一个物品可以选多次,即同一行内选取,状态转移方程式为 max { dp[i-1][j], dp[i][j-w[i]] + v[i] },使用和0/1背包中同样的方法,可以将二维dp压缩为一维数组 max {dp[j], dp[j - w[i]] + v[i]}。这个时候你可能会发现,在完全背包中优化后的方程式和0/1背包中的式子完全相同!没错,在0/1问题中新数据是和上一行的数据进行对照,完全背包是和本行的数据对照,当对i进行了压缩后,其余部分是完全相同的。不过,解题时这两者在循环遍历数组时有区别。

0/1背包解题用到的是上一条数据,用的是旧数据,所以需要从后往前遍历

for (int i = 1; i <= n; i++) {
	for (int j = m; j >= 1; j--) {
		if (j >= w[i])
			dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
	}
}

完全背包解题使用的是新数据,所以需要从前往后遍历

for (int i = 1; i <= n; i++) {
	for (int j = m; j >= 1; j--) {
		if (j >= w[i])
			dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
	}
}

题解

#include<iostream>
#include<algorithm>
using namespace std;

int main(){
	int m, n; // 背包容量为m 物品个数为n
	int w[10];// 具体使用到到 n+1
	int v[10];// 具体使用到到 n+1
	int dp[20] = { 0 }; // 具体使用到到 m+1
	cin >> m >> n;
	for (int i = 1; i <= n; ++i) {
		cin >> w[i] >> v[i];
	}

	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= m ; j++) {	// 正向遍历一维数组
			if (j >= w[i])
				dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
		}
	}
    cout << "max=" << dp[m];
	return 0;
}

其实还可以再简化一下代码,这里进行了一次 j >= w[i] 的判断,其实可以将判断放在循环的初始值中,直接从w[i]开始进行循环遍历即可。

for (int i = 1; i <= n; i++) {
	for (int j = w[i]; j <= m ; j++) {	// 正向遍历一维数组
		dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
	}
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值