Bone Collector(01背包问题)详解

题目连接: Bone Collector

题目:

已知N个糖果的重量和价值. 我们有一个口袋, 最多可以装V重量的糖果. 问口袋最多能放多少价值的糖果进去?

Input
输入的第一行是T, 表示有T组数据.
每组数据由三行组成.
第一行包含两个整数N和V(N <= 1000, V <= 1000). N表示糖果的个数, V表示口袋的载重.
第二行包含N个整数, 表示每一颗糖果的价值.
第三行包含N个整数, 表示每一颗糖果的重量.

Output
对每一组数据, 输出口袋最终可以放进去糖果的价值.

Sample Input
1
5 10
1 2 3 4 5
5 4 3 2 1
Sample Output
14
推荐大家去看看英文题目, 中文方便大家理解, 但是解题思路及代码以英文题目为准.

解题思路:

这个题是动态规划里背包问题中十分基础的01背包问题, 这里不多说废话了, 讲解部分将放在代码部分.
至于怎么想到的dp思路, 在结尾也有一些个人浅显的看法.

AC代码(共三种):

版本1(二维数组无优化版本):

#include <bits/stdc++.h>
#define ll long long
using namespace std;
int w[1005], v[1005]; //分别表示第i件商品的价值与体积
int dp[1005][1005]; //用于存储结果, dp[i][j]. 表示可选购商品有i件, 背包容量为j时的最优解
int main(void)
{
	int t; cin >> t;
	while (t--) {
		int n, m; scanf("%d %d", &n, &m); //n表示商品总数, m表示背包总容量
		memset(dp, 0, sizeof(dp));

		for (int i = 1; i <= n; i++) scanf("%d", &w[i]); //读入价值
		for (int i = 1; i <= n; i++) scanf("%d", &v[i]); //读入体积
		for (int i = 1; i <= n; i++) { //可选购的商品i
			for (int j = 0; j <= m; j++) { //由于可能有体积为0的商品, 所以从0开始循环
				if (j >= v[i]) dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]); //dp思路的核心
				else dp[i][j] = dp[i - 1][j]; /* 如果现在背包的容量比当前的商品体积大了, 才是能买的情况.
												至于是否购买, 则取决于买后的总价值是否高于不买时的总价值. */
			}
		}
		printf("%d\n", dp[n][m]); //输出 可选购的商品有n件, 背包容量为m时的最优解
	}
	return 0;
}

其实我们可以由上述动态规划公式我们可以得出一个结论: 即我们当前所求状态只与前一个状态有关, 那么我们所开的dp数组理论上而言只需要2 * 1005就可以了, 所以我们尝试进行化简.

版本2(二维数组降维->二维滚动数组):

#include <bits/stdc++.h>
#define ll long long
using namespace std;
int w[1005], v[1005]; int dp[2][1005]; //注释请参照版本一
int main(void)
{
	int t; cin >> t;
	while (t--) {
		memset(dp, 0, sizeof(dp));
		int n, m; scanf("%d %d", &n, &m);

		for (int i = 1; i <= n; i++) scanf("%d", &w[i]);
		for (int i = 1; i <= n; i++) scanf("%d", &v[i]);

		for (int i = 1; i <= n; i++) {
			for (int j = 0; j <= m; j++) { 
				if (j >= v[i]) dp[i % 2][j] = max(dp[i % 2 ^ 1][j], dp[i % 2 ^ 1][j - v[i]] + w[i]); 
				else dp[i % 2][j] = dp[i % 2 ^ 1][j];/* 这里是一个滚动数组, 如果 i % 2 ^ 1 你看不懂, 你可以用(i + 1) % 2 代替
											这样每一轮由于i++, 因此我们可以一直用i % 2表示当前状态,  i % 2 ^ 1 表示上一个状态 */
			}
		}
		cout << dp[n % 2][m] << endl;
	}
	return 0;
}

版本3(二维滚动数组->一维数组):

#include <bits/stdc++.h>
#define ll long long
using namespace std;
int w[1005], v[1005]; int dp[1005];
int main(void)
{
	int t; cin >> t;
	while (t--) {
		memset(dp, 0, sizeof(dp));
		int n, m; scanf("%d %d", &n, &m);

		for (int i = 1; i <= n; i++) scanf("%d", &w[i]);
		for (int i = 1; i <= n; i++) scanf("%d", &v[i]);

		for (int i = 1; i <= n; i++) {
			for (int j = m; j >= v[i]; j--) {
				dp[j] = max(dp[j], dp[j - v[i]] + w[i]); //其实第三个版本相较前两个版本变化大一些.
			}											 //个人认为算是01背包问题里的最优写法了吧... (代码讲解放下面)
		}
		cout << dp[m] << endl;
	}
	return 0;
}

其实dp核心思路还是没有变, 我们当前所求的状态还是取决于前一个状态, 而二维->一维时, 我们需要保证当前需要的这个状态是上一个状态留下来的, 而不是当前状态改变后再次使用的.

如果我们直接对于版本2的代码进行修改, 可以发现, 如果购买商品i时能取得优解, 则dp[j1]的状态会被改变, 而在后续计算dp[j2]时, 我们就无法保证dp[j1]为前一个状态留下来的(j1 = j2 - v[i]).
但是如果倒着跑, 那就不会出现这种情况了, 因为即使我们改变了dp[j1]的值, 而在dp[j2]的计算中, 我们只会用到小于等于j2的部分, 而j1是>j2的.

同样我们可以发现, 在版本1和版本2中, 如果当前背包容量比v[i]要小的话, 我们的做法是直接把上一个状态复制到当前状态, 并没有做出改变. 而我们通过将j倒着去跑, 在条件判断部分就让j>=v[i]就很好优化了这一步的判断.

为何想到DP?

如果你刚开始接触, 我并不认为有人能一下子就能想到是动态规划的思路, 但是你做过一次, 再做你就知道了.
如果你和我一样想法有点较真, 那你可以看看我下面的说法:

这个题目已知一定是有最优解的, 无非就是买不买当前这个商品, 如果你把所有的解都列出来, 一定有最优解, 你可以考虑递归函数的思路. 缺点就是数据量一大, 电脑就跑不出来. 而跑不出来了, 我们就要想方法优化代码.

如果只有1件商品, 你可以想到看看背包能不能装的下, 装的下就要.
如果有2件商品, 你会想到看看能不能都装下, 如果不能都装下, 装谁的时候价值最大.
如果有3件甚至更多的商品…
其实不难发现, 你自己模拟着模拟着, 你就找到了01背包的dp公式, 你会想到从最简单的状态开始模拟, 记录简单状态的每一种最优解. 随着商品数量增加, 也要不断改变最优解.

如果你还是想不懂, 我想你或许付出的时间有点少了, 多付出点时间思考思考吧, 毕竟dp问题还是有些难度的.

END

发布了20 篇原创文章 · 获赞 0 · 访问量 517

猜你喜欢

转载自blog.csdn.net/weixin_45799835/article/details/104282670
今日推荐