01背包问题
题目链接:https://www.nowcoder.com/share/jump/3971807151726832163092
题目描述:
你有⼀个背包,最多能容纳的体积是V。
现在有 n 个物品,第 i 个物品的体积为 vi ,价值为 wi。
(1)求这个背包⾄多能装多⼤价值的物品?
(2)若背包恰好装满,求⾄多能装多⼤价值的物品?
输⼊描述:
第⼀⾏两个整数 n 和 V,表⽰物品个数和背包体积。
接下来 n ⾏,每⾏两个数 vi 和 wi,表⽰第i个物品的体积和价值。
输出描述:
输出有两⾏,第⼀⾏输出第⼀问的答案,第⼆⾏输出第⼆问的答案,如果⽆解请输出 0。
⽰例1
输⼊:
3 5
2 10
45
1 4
输出:
14
9
说明:
装第⼀个和第三个物品时总价值最⼤,但是装第⼆个和第三个物品可以使得背包恰好装满且总价
值最⼤。
⽰例2
输⼊:
3 8
12 6
11 8
6 8 输出:
8
0
说明:
装第三个物品时总价值最⼤但是不满,装满背包⽆解。
算法思路:
第一问
状态表示: dp[i][j] 表⽰:从前i 个物品中挑选,总体积「不超过」j ,所有的选法中,能挑选出来 的最⼤价值。
状态转移⽅程: 线性 dp 状态转移⽅程分析⽅式,⼀般都是根据「最后⼀步」的状况,来分情况讨论: i. 不选第 i 个物品:相当于就是去前 i - 1 个物品中挑选,并且总体积不超过 j 。此时 dp[i][j] = dp[i - 1][j] ;
ii. 选择第 i 个物品:那么我就只能去前 i - 1 个物品中,挑选总体积不超过 j - v[i] 的物品。此时dp[i][j] =
dp[i - 1][j - v[i]] + w[i] 。但是这种状态不⼀定存在,因此需要特判⼀下。 综上,状态转移⽅程为: dp[i][j]
= max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]) 。
初始化: 我们多加⼀⾏一列,⽅便我们的初始化,此时仅需将第⼀⾏初始化为 0 即可。因为什么也不选,也能满⾜体积不⼩于 j 的情况,此时的价值为 0 。第一列也初始化为0,因为体积不超过0,即什么都不选,价值自然为0。
填表顺序: 根据「状态转移⽅程」,我们仅需「从上往下」填表即可。
返回值: 根据「状态表⽰」,返回 dp[n][V] 。
第二问
第⼆问仅需微调⼀下 dp 过程的五步即可。
因为有可能凑不⻬ j 体积的物品,因此我们把不合法的状态设置为 -1 。1.状态表⽰: dp[i][j] 表⽰:从前 i 个物品中挑选,总体积「正好」等于 j ,所有的选法中,能挑选出 来的最⼤价值。
- 状态转移⽅程: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]) 。 但是在使⽤ dp[i - 1][j - v[i]] 的时候,不仅要判断 j >= v[i] ,⼜要判断 dp[i -1][j
- v[i]] 表⽰的情况是否存在,也就是 dp[i - 1][j - v[i]] != -1 。
- 初始化: 我们多加⼀⾏一列,⽅便我们的初始化: i. 第⼀个格⼦为0 ,因为正好能凑⻬体积为 0 的背包 ii. 但是第⼀⾏后⾯的格⼦都是 -1 ,因为没有物品,⽆法满⾜体积⼤于 0 的情况。 iii.
第一列初始化都为0,因为要求所选物品的体积等于0,即什么都不选,价值自然为04.填表顺序: 根据「状态转移⽅程」,我们仅需「从上往下」填表即可。
5.返回值: 由于最后可能凑不成体积为 V 的情况,因此返回之前需要「特判」⼀下。
代码
优化前的算法代码
cpp
#include <iostream>
#include <string.h>
using namespace std;
const int N = 1010;
int n, V, v[N], w[N];
int dp[N][N];
int main()
{
// 读⼊数据
cin >> n >> V;
for(int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
// 解决第⼀问
for(int i = 1; i <= n; i++)
for(int j = 0; j <= V; j++) // 修改遍历顺序
{
dp[i][j] = dp[i - 1][j];
if(j >= v[i])
dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
}
cout << dp[n][V] << endl;
// 解决第⼆问
memset(dp, 0, sizeof dp);
for(int j = 1; j <= V; j++) dp[0][j] = -1;
for(int i = 1; i <= n; i++)
for(int j = 0; j <= V; j++) // 修改遍历顺序
{
dp[i][j] = dp[i - 1][j];
if(j >= v[i] && dp[i - 1][j - v[i]] != -1)
dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
}
cout << (dp[n][V] == -1 ? 0 : dp[n][V]) << endl;
return 0;
}
java
class Dy {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int V = in.nextInt();
int[] v = new int[n];
int[] w = new int[n];
for (int i = 0; i < n; i++) {
v[i] = in.nextInt();
w[i] = in.nextInt();
}
//多加一行,为了防止下标越界
int[][] dp = new int[n + 1][V + 1];
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= V; j++) {
dp[i][j] = dp[i - 1][j];
if (j - v[i - 1] >= 0) {
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - v[i - 1]] + w[i - 1]);
}
}
}
System.out.println(dp[n][V]);
int[][] dp1 = new int[n + 1][V + 1];
for (int j = 1; j <= V; j++) {
dp1[0][j] = -1;
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= V; j++) {
dp1[i][j] = dp1[i - 1][j];
if (j - v[i - 1] >= 0 && dp1[i - 1][j - v[i - 1]] != -1) {
dp1[i][j] = Math.max(dp1[i][j], dp1[i - 1][j - v[i - 1]] + w[i - 1]);
}
}
}
if (dp1[n][V] == -1) {
System.out.println(0);
} else {
System.out.println(dp1[n][V]);
}
}
}
}
优化后的算法代码
空间优化: 背包问题基本上都是利⽤「滚动数组」来做空间上的优化: i. 利⽤「滚动数组」优化; ii. 直接在「原始代码」上修改。
在01背包问题中,优化的结果为: i. 删掉所有的横坐标; ii. 修改⼀下j 的遍历顺序。
cpp
#include <iostream>
#include <string.h>
using namespace std;
const int N = 1010;
int n, V, v[N], w[N];
int dp[N];
int main()
{
// 读⼊数据
cin >> n >> V;
for(int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
// 解决第⼀问
for(int i = 1; i <= n; i++)
for(int j = V; j >= v[i]; j--) // 修改遍历顺序
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
cout << dp[V] << endl;
// 解决第⼆问
memset(dp, 0, sizeof dp);
for(int j = 1; j <= V; j++) dp[j] = -1;
for(int i = 1; i <= n; i++)
for(int j = V; j >= v[i]; j--)
if(dp[j - v[i]] != -1)
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
cout << (dp[V] == -1 ? 0 : dp[V]) << endl;
return 0;
}