前言:这个专栏主要讲述动态规划算法,所以下面题目主要也是这些算法做的
我讲述题目会把讲解部分分为3个部分:
1、题目解析
2、算法原理思路讲解
3、代码实现
【模板】01背包
题目链接:【模板】01背包_牛客题霸_牛客网
题目
描述
你有一个背包,最多能容纳的体积是V。
现在有n个物品,第i个物品的体积为vi ,价值为wi。
(1)求这个背包至多能装多大价值的物品?
(2)若背包恰好装满,求至多能装多大价值的物品?
输入描述:
第一行两个整数n和V,表示物品个数和背包体积。
接下来n行,每行两个数vi和wi,表示第i个物品的体积和价值。
1≤10001≤n,V,vi,wi≤1000
输出描述:
输出有两行,第一行输出第一问的答案,第二行输出第二问的答案,如果无解请输出0。
示例1
输入:
3 5 2 10 4 5 1 4
复制输出:
14 9
复制说明:
装第一个和第三个物品时总价值最大,但是装第二个和第三个物品可以使得背包恰好装满且总价值最大。
示例2
输入:
3 8 12 6 11 8 6 8
复制输出:
8 0
复制说明:
装第三个物品时总价值最大但是不满,装满背包无解。
备注:
要求O(nV)的时间复杂度,O(V)空间复杂度
解法
算法原理与解析
我们这题使用动态规划,我们做这类题目可以分为以下五个步骤
- 状态显示
- 状态转移方程
- 初始化(防止填表时不越界)
- 填表顺序
- 返回值
我们先解决第⼀问:求这个背包至多能装多大价值的物品?
- 状态显示
dp[i][j] 表示 :从前 i 个物品中挑选,总体积「不超过」 j ,所有的选法中,能挑选出来的最大价值。
- 状态转移方程
线性 dp 状态转移⽅程分析⽅式,⼀般都是根据「最后⼀步」的状况,来分情况讨论:
- 不选第 i 个物品:相当于就是去前 i - 1 个物品中挑选,并且总体积不超过 j 。此时 dp[i][j] = dp[i - 1][j] 。
- 选择第 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 。
- 填表顺序
根据「状态转移⽅程」,我们仅需「从上往下」填表即可。
- 返回值
根据「状态表⽰」,返回 dp[n][V]
接下来解决第⼆问: 若背包恰好装满,求至多能装多大价值的物品?
- 第⼆问仅需微调⼀下 dp 过程的五步即可。
- 因为有可能凑不⻬ j 体积的物品,因此我们把不合法的状态设置为 -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 。
- 初始化(防止填表时不越界)
我们多加⼀行,方便我们的初始化:
- 第⼀个格⼦为 0 ,因为正好能凑⻬体积为 0 的背包;
- 但是第⼀⾏后⾯的格⼦都是 -1 ,因为没有物品,⽆法满⾜体积⼤于 0 的情况。
- 填表顺序
根据「状态转移⽅程」,我们仅需「从上往下」填表即可。
- 返回值
由于最后可能凑不成体积为 V 的情况,因此返回之前需要「特判」⼀下。
代码实现
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <climits>
#include <cstring>
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;
}
背包问题基本上都是利⽤「滚动数组」来做空间上的优化:
- 利⽤「滚动数组」优化;
- 直接在「原始代码」上修改。
在01背包问题中,优化的结果为:
- 删掉所有的横坐标;
- 修改⼀下 j 的遍历顺序。
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <climits>
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;
}