
◆ 博主名称: 小此方-CSDN博客 大家好,欢迎来到小此方的博客。
⭐️算法系列个人专栏: 算法实战论
⭐️此方的GitHub: github_此方
⭐️ Re系列专栏:我们思考 (Rethink) · 我们重建 (Rebuild) · 我们记录 (Record)
文章目录
概要&序論
背包问题近年的笔试面试考察比较多。值得一讲。本文是讲解给初学者听的,如果你懂得背包问题的基本内容,请跳过。
一、背包问题概念
背包问题本质是:你有一个背包,地上一堆物品,挑选一些物品放入背包中,问最大能挑选出来的价值是多少。
要解决这个问题,首先需要明确两类核心属性。
- 第一是物品的属性,即每件物品自身的体积和价值。例如图中有三件物品,体积分别为4、5、2,价值分别为1、7、9。

- 第二是背包的属性,即背包的容量。
在实际问题中,背包状态有两种情况,即装满 和不装满,两者的初始化逻辑不同。
此外,根据物品数量的限制,背包问题主要分为两种模型。
- 如果每种物品只有1个,这就是经典的01背包问题。
- 如果每种物品的数量是无限的(无穷多个),这就是完全背包问题。

二、01背包模板
力扣上找不到纯粹的背包,我在牛客上找到了一道。
来看看。这里有两问,先看第一问。
2.1背包问题的状态表示经验
一般的,我们还是喜欢按照我们的经验,以某一个位置为结尾,加上题目的要求。于是自然而然就会想到:
dpi 表示:从前 i 个物品中选,所有选法中,能挑选出来的最大价值
但是,我会告诉你,他是错误的,你需要记忆一个全新的经验。关于背包问题的状态表示:(不是死记硬背,但是和前面讲别的类型时一样,这样的模板题的状态表示十分有参考价值。)
dpij 表示:从前 i 个物品中挑选,总体积不超过 j,所有选法中,能挑选出来的最大价值
2.2基于以往的经验,写出状态转移方程
根据以往的经验,不难想到这么去划分状态:
根据第i个位置的状态进行划分,第i个位置我们选与不选可以作为划分依据。

2.3初始化?依旧是以往的经验
dpij 表示:从前 i 个物品中挑选,总体积不超过 j,所有选法中,能挑选出来的最大价值
如何初始化?还是以往的经验,在dp表外面再包一层,第一行表示第0个物品,那么不论j到底是什么,我没有物品去装,总价值就是0,所以第一行就是0.第一列,j = 0 ,所以背包的容积是0,那么不论我到第几个物品,我能装下的始终是0.

所以不需要初始化。
2.4完整代码
cpp
int UnfilledBag(vector<pair<int,int>>& member , int V , int n){
vector<vector<int>> dp(n+1,vector<int>(V+1,0));
for(int i = 1 ; i <= member.size() ; i ++){
for(int j = 1 ; j <= V ; j ++){
dp[i][j] = dp[i-1][j];
if(j - member[i-1].first >= 0){
dp[i][j] = max(dp[i][j],dp[i-1][j - member[i-1].first] + member[i-1].second);
}
}
}
return dp[member.size()][V];
}
2.5满背包的01背包
来看第二问:
要求最终恰好装满背包,求能获得的最大总价值。若不存在使背包恰好装满的装法,则答案记为 0
和第一问差不太多。 但是我们要约定一件事情:什么事情?如果不能装满,就记为-1;

为什么前者(子问题)不满包,会牵连后者(当前情况)同样为 -1?
一句话解释:如果你决定装入第 i 个物品,那么剩下的空间 j-Vi 必须能够被前 i-1 个物品"完美填满"。如果剩下的空间本身就填不满(即等于 -1),那你加上第 i 个物品后,整个背包依然会有空隙,绝对不可能达到"恰好装满 j"的要求。
2.5完整代码
cpp
int filledBag(vector<pair<int,int>>& member , int V , int n){
vector<vector<int>> dp(n+1,vector<int>(V+1,0));
for(int i = 1 ; i <= V ; i ++)
dp[0][i] = -1;
for(int i = 1 ; i <= member.size() ; i ++){
for(int j = 1 ; j <= V ; j ++){
dp[i][j] = dp[i-1][j];
if(j - member[i-1].first >= 0 && dp[i-1][j - member[i-1].first] != -1){
dp[i][j] = max(dp[i][j],dp[i-1][j - member[i-1].first] + member[i-1].second);
}
}
}
return dp[member.size()][V] == -1 ? 0 : dp[member.size()][V];
}
2.6本题的完整代码Lv1
cpp
#include <iostream>
#include <vector>
using namespace std;
int UnfilledBag(vector<pair<int,int>>& member , int V , int n){
vector<vector<int>> dp(n+1,vector<int>(V+1,0));
for(int i = 1 ; i <= member.size() ; i ++){
for(int j = 1 ; j <= V ; j ++){
dp[i][j] = dp[i-1][j];
if(j - member[i-1].first >= 0){
dp[i][j] = max(dp[i][j],dp[i-1][j - member[i-1].first] + member[i-1].second);
}
}
}
return dp[member.size()][V];
}
int filledBag(vector<pair<int,int>>& member , int V , int n){
vector<vector<int>> dp(n+1,vector<int>(V+1,0));
for(int i = 1 ; i <= V ; i ++)
dp[0][i] = -1;
for(int i = 1 ; i <= member.size() ; i ++){
for(int j = 1 ; j <= V ; j ++){
dp[i][j] = dp[i-1][j];
if(j - member[i-1].first >= 0 && dp[i-1][j - member[i-1].first] != -1){
dp[i][j] = max(dp[i][j],dp[i-1][j - member[i-1].first] + member[i-1].second);
}
}
}
return dp[member.size()][V] == -1 ? 0 : dp[member.size()][V];
}
int main() {
int a, b;
int V = 0 ;
int n = 0 ;
vector<pair<int,int>> member;
if(cin >> a >> b) {
n = a;V = b;
}
while(cin >> a >> b)
member.push_back(make_pair(a,b));
int ret1 = UnfilledBag(member,V,n);
int ret2 = filledBag(member,V,n);
std::cout<<ret1<<'\n'<<ret2;
return 0;
}
三、优化:滚动数组优化空间与时间
3.1优化思路
看看我们上面的代码,挑一个下来:
cpp
for(int i = 1 ; i <= member.size() ; i ++){
for(int j = 1 ; j <= V ; j ++){
dp[i][j] = dp[i-1][j];
if(j - member[i-1].first >= 0){
dp[i][j] = max(dp[i][j],dp[i-1][j - member[i-1].first] + member[i-1].second);
}
}
}
dpij用到了什么?dpi-1j和dpi-1j - member\[i-1.first] 绘制图像,圆圈就是dpij,他依赖两个三角形位置的数值。那么不难看出来,只占用两行。所以我们上面的i-2行全部都可以省略。

于是滚动数组的第一层优化就出来了:只有两行,填写第一行,第二行依赖第一行,第二行填写完毕后第一行当作第三行依赖第二行填写。以此类推。但是!我们想要详细讲解的,不是这个东西。
3.2进一步优化
因为他还可以进一步的优化。只需要一行。

为了避免覆盖,我们还要把填表顺序改为从后往前。
注意事项:
- 这题是模板题,里面的分析思路,可以用到很多题里面
- 不要去强行解释优化后的状态表示以及状态转移方程
状态表示:不要去强行解释它!这没有意义。 怎么来的?因为我们去掉了一维,所以只需要删除i就可以了。
cpp
/ dp[j]
dp[j] <
\ dp[j-v[i]]
3.3完整代码
cpp
#include <iostream>
#include <vector>
using namespace std;
int UnfilledBag(vector<pair<int,int>>& member , int V , int n){
vector<int>dp(V+1,0);
for(int i = 1 ; i <= member.size() ; i ++){
for(int j = V ; j >= 1 ; j --){
if(j - member[i-1].first >= 0){
dp[j] = max(dp[j],dp[j - member[i-1].first] + member[i-1].second);
}
}
}
return dp[V];
}
int filledBag(vector<pair<int,int>>& member , int V , int n){
vector<int>dp(V+1,0);
for(int i = 1 ; i <= V ; i ++)
dp[i] = -1;
for(int i = 1 ; i <= member.size() ; i ++){
for(int j = V ; j >= 1 ; j --){
if(j - member[i-1].first >= 0 && dp[j - member[i-1].first] != -1){
dp[j] = max(dp[j],dp[j - member[i-1].first] + member[i-1].second);
}
}
}
return dp[V] == -1 ? 0 : dp[V];
}
int main() {
int a, b;
int V = 0 ;
int n = 0 ;
vector<pair<int,int>> member;
if(cin >> a >> b) {
n = a;V = b;
}
while(cin >> a >> b)
member.push_back(make_pair(a,b));
int ret1 = UnfilledBag(member,V,n);
int ret2 = filledBag(member,V,n);
std::cout<<ret1<<'\n'<<ret2;
return 0;
}
好的本文内容到此结束,我是此方,我们下期再见。