
个人主页:小则又沐风
个人专栏:<数据结构>
<竞赛专栏>
<Linux>
座右铭
路虽远,行则将至;事虽难,做则必成
目录
前言
蓝桥杯的国赛马上就要开始了,停更已久的算法专栏,为了在国赛的时候不情愿干瞪眼,今天我们来学习动态规划中的背包问题.
在这里祝愿大家在国赛中取得理想的成绩
01背包问题
现在我们来了解一下背包问题的种类
- 01背包问题
这一类的问题就是在一组的物品中选择选择出最符合题意的选法,但是这一类的问题就是对于一个物品只有两种状态就是选或者不选,也就是我们用二进制表示的话就是01了
这一类的问题也是最基础的背包问题
就用一道题目来展开01背包问题的讲解
这个题目就是经典的01背包了
我们就拿他来开刀

通过阅读题目我们知道的是这个题目的第一个问题
要求就是我们需要在n件物品中选择出不超过最大容量V价值最高的那一个
下面是我们解题的步骤:
- 进行状态表示
在通常的情况下背包问题就是这样设置状态的
fiv表示的就是在前i个物品中选择不超过v的最大价值
那么在fnm就是我们的答案了
完成状态表示之后我们需要的就是我们要对状态转移方程进行推算了
下面是我们思考的通解(对于01背包问题来说)
- 状态转移方程
首先我们对于最后一步来说也就是对最后一个物品来说
我们有两种的情况就是选或者不选
那么这两种的情况状态转移的表示是什么???
当我们不选的时候就是fiv的值就是fi-1v从前i-1个物品选择的结果了
当我们选的时候就是
fiv=fi-1v-v\[i]+选择物品的价值
这个表示的是什么意思就是我们选择了这个物品那么我们需要在前i-1中选择的总物重需要减去我们刚才选的物品的重量,那么这个状态表示的前提就是我们要选择的物品的重量必须小于我们当前可以容纳的重量
那么然后就是这两个区一个最大值了
现在我们解决了状态转移方程了
- 初始化
下面就是我们初始化表的时候了
我们可以根据这个表的实际的意义来初始化
那么我们来看这个表的第一行就是i等于0,那么从0个物品中选出不超过总重的最大价值是多少??
不就是0吗?
然后我们的第一列在循环的时候自己初始化就行了
我们就做完了初始化的过程了
下面就是我们遍历顺序了
- 填表的顺序
确定填表的顺序是需要查看我们的状态转移方程的,在我们的状态转移方程中我们需要的数值就是
i-1\]\[j-一个未知数
那么填表我们需要的数据就是前一行的数据
所以就是从上往下就可以了
下面就是我们的代码了
cpp
#include<iostream>
using namespace std;
const int N=1e3+10;
int f[N][N];
int p[N];
int w[N];
int main()
{
int n,v;
cin>>n>>v;
for(int i=1;i<=n;i++)
{
cin>>w[i]>>p[i];
}
for(int i=1;i<=n;i++)
{
for(int j=0;j<=v;j++)
{
f[i][j]=f[i-1][j];
if(j>=w[i])
{
f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+p[i]);
}
}
}
cout<<f[n][v]<<endl;
return 0;
}
那么现在就是解决另一个问题了
下面我们来看这个问题是需要我们解决一个怎么样的问题呢?

这个问题和我们上面我们解决的问题有一点的相似但是这个问题的要求是一个需要我们刚好把这个背包给装满,那么我们还是上面的解决思路
- 状态表示
在这个题型中我们的状态的表示还是那一个方式但是我们的实际的意义改变了
fiv现在这个表示就是在前i个中选择出刚好是v重量的最大的价值
- 状态转移方程
我们发现这类题目的状态转移方程和上面的是一样的
所以我们还是拿上面的状态转移方程
- 初始化
在上面我们发现到目前为止我们的写的代码和上述的题目是一模一样的,现在我们的初始化就是不同的地方了
我们来看我们定义的状态的实际含义
我们从0个物品中选择物品的总重是j(现在不是0)我们可以发现这是不合理的,
所以我们就需要删除这些不合理的状态,至于删除的方法有两种(在我看来,当然亦可以有其他的方法)
对于不合理的表格进行标记,标记为-1在后面的填表的时候进行条件判断就可以了
当然还有一种方法就是把不合理的表格设计成一个无穷小的数值,那么我们知道我们的填表的时候我们会拿着前一行的数据进行填表,我们是会在两个选和不选中拿到一个max的,一个无穷小是不会被拿到的.
所以我们就可以尽情的写了,最后需要把判断一下最后我们需要的状态时否是一个正数就行了
因为如果是一个正数的话,说明有一种的方法可以刚好填满,相反就是没有方案了
- 填表顺序
从上往下
代码实现
cpp
#include<iostream>
#include<stdlib.h>
#include<cstring>
using namespace std;
const int N=1e3+10;
int f[N][N];
int p[N];
int w[N];
int main()
{
int n,v;
cin>>n>>v;
for(int i=1;i<=n;i++)
{
cin>>w[i]>>p[i];
}
for(int i=1;i<=n;i++)
{
for(int j=0;j<=v;j++)
{
f[i][j]=f[i-1][j];
if(j>=w[i])
{
f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+p[i]);
}
}
}
cout<<f[n][v]<<endl;
//next
memset(f,-0x3f,sizeof(f));
f[0][0]=0;
for(int i=1;i<=n;i++)
{
for(int j=0;j<=v;j++)
{
f[i][j]=f[i-1][j];
if(j>=w[i])
{
f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+p[i]);
}
}
}
if(f[n][v]<0)
{
cout<<0<<endl;
}else
{
cout<<f[n][v]<<endl;
}
return 0;
}
进阶部分:
在上面我们是在依靠二维数组进行解题的,现在我们需要进行的是空间优化,因为再有的题目上我们必须进行空间优化的,那么我们怎么才能把这个二维的数组变成一个一维的数组呢?
很简单以只需要跟着我的步骤做就行了
- 在你的代码上直接删除状态表示数组的第一维
- 然后我们需要改变我们的填表的顺序了
怎么确定我们这个版本的填表的顺序呢?
我们可以看到的是我在一维数组中填表的时候我们会拿着当前表格的之前的数据进行填写,但是我们如果在填写之前这个数值就改变了话我们的答案就会是错误的,所以我们需要的是倒着填表了
当然这时候就会有一个时间的小优化了我们可以直接遍历到j=wi的时候就停止了
下面是我们的空间优化的版本了
cpp
//#include<iostream>
//using namespace std;
//const int N=1e3+10;
//int f[N][N];
//int p[N];
//int w[N];
//int main()
//{
// int n,v;
// cin>>n>>v;
// for(int i=1;i<=n;i++)
// {
// cin>>w[i]>>p[i];
// }
// for(int i=1;i<=n;i++)
// {
// for(int j=0;j<=v;j++)
// {
// f[i][j]=f[i-1][j];
// if(j>=w[i])
// {
// f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+p[i]);
// }
// }
// }
// cout<<f[n][v]<<endl;
// //next
// memset(f,-0x3f,sizeof(f));
// f[0][0]=0;
// for(int i=1;i<=n;i++)
// {
// for(int j=0;j<=v;j++)
// {
// f[i][j]=f[i-1][j];
// if(j>=w[i])
// {
// f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+p[i]);
// }
// }
// }
// if(f[n][v]<0)
// {
// cout<<0<<endl;
// }else
// {
// cout<<f[n][v]<<endl;
// }
// return 0;
//}
#include<iostream>
#include<cstring>
using namespace std;
const int N=1e3+10;
int f[N];
int p[N];
int w[N];
int main()
{
int n,v;
cin>>n>>v;
for(int i=1;i<=n;i++)
{
cin>>w[i]>>p[i];
}
for(int i=1;i<=n;i++)
{
for(int j=v;j>=w[i];j--)
{
f[j]=max(f[j],f[j-w[i]]+p[i]);
}
}
cout<<f[v]<<endl;
//next
memset(f,-0x3f,sizeof(f));
f[0]=0;
for(int i=1;i<=n;i++)
{
for(int j=v;j>=w[i];j--)
{
f[j]=max(f[j],f[j-w[i]]+p[i]);
}
}
if(f[v]<0)
{
cout<<0<<endl;
}else
{
cout<<f[v]<<endl;
}
return 0;
}
上述就是01背包的问题了
下面我们进入完全背包问题
完全背包问题
延续上面我们讲解的方式我们还是来看一个模板的题目

这个和我们的01背包问题最大的区别就是我们的物品是无限制的,想选多少个就选多少个,那么我们直接来开始学习把
- 状态表示
还是和之前的一样不变
fiv在前i个物品中选择不超过v的最大价值
那么我们这一问的最后的答案就是fnv
- 状态转移方程
在上面的01背包中的状态转移方程算是很简单的,但是在完全背包问题中,我们最难的就是这个步骤了
怎么去找一个状态转移呢?
我们还是上面的思路从最后来看
对于最后的一个物品来说我们的选择有不选和选两个大类,但是对于这个选的问题,我们又有一个问题了,你要选几个?
我们来研究一下
首先最简单的就是不选了
fiv=fi-1v;
然后就是选一个
fiv=fi-1v;
选择两个,三个.....n个
fi-1v-2\*w\[i]+pi*2;
fi-1v-3\*w\[i]+pi*3;
fi-1v-3\*w\[i]+pi*3;
所以我们的状态转移方程就是
fiv=max(fi-1v,fi-1v-w\[i]+pi,fi-1v-2\*w\[i]+pi*2;fi-1v-3\*w\[i]+pi*3,......fi-1v-n\*w\[i]+pi*n)
这里的>n*wi
所以我们的这个状态转移的方程真是太难表示了
我们需要找到一个好的方法
我们来看一下
fiv-w\[i]=???
我们带入到上面的方程中就是一个
fiv-w\[i]=max(fi-1v-w\[i],fi-1v-2\*w\[i)+pi,...fi-1v-(n-1)w\[i]+(n-1)pi);
我们惊奇的发现这个居然是上面蓝色减去一个pi
那么我们是不是就可以这样来表示状态转移方程
fiv=max(fi-1v,fiv-w\[i]+pi);
可以的
下面就是我们的初始化了
- 初始化
我们还是来看一下这个状态表示的实际含义
从0个物品中选出不超过j的最大价值,
好了和我们之前讲的一样初始化为0
- 填表顺序
在我们的状态转移方程中我们的需要的还是上一行的数据,所以还是从上往下
下面就是我们的代码实现了
cpp
#include<iostream>
#include<cstring>
using namespace std;
const int N=1010;
int f[N][N];
int p[N];
int w[N];
int main()
{
int n,v;
cin>>n>>v;
for(int i=1;i<=n;i++)
{
cin>>w[i]>>p[i];
}
for(int i=1;i<=n;i++)
{
for(int j=0;j<=v;j++)
{
f[i][j]=f[i-1][j];
if(j>=w[i])
{
f[i][j]=max(f[i-1][j],f[i][j-w[i]]+p[i]);
}
}
}
cout<<f[n][v]<<endl;
memset(f,-0x3f,sizeof(f));
f[0][0]=0;
for(int i=1;i<=n;i++)
{
for(int j=0;j<=v;j++)
{
f[i][j]=f[i-1][j];
if(j>=w[i])
{
f[i][j]=max(f[i-1][j],f[i][j-w[i]]+p[i]);
}
}
}
if(f[n][v]<0)
{
cout<<0<<endl;
}else
{
cout<<f[n][v]<<endl;
}
return 0;
}
现在我们浅浅的做一个总结吧
我们现在遇到的题型有了两种了
一种题型就是我们需要挑选的总重量不超过j也就是说我们的背包可以有空余的空间
一种的题型就是我们需要挑选的重量必须是一个数值,也就是说我们的背包的必须刚好装满
第一中的题型我们最好解决了
第二种的就是把我们的不合理的表格填写成一个无穷小,最后判断答案的正负就可以了
下面就是我们的第三种的题型

在这一种的题目中我们的要求就是需要我们的总价值是>=一个数值的,和我们之前遇到的都不一样
我们又该怎么解决呢???
下面还是之前的老步骤
- 状态表示
fij表示的是从前i个中选择总价值至少为j的最少的花费
- 状态转移方程
在这个题目中还是我们的完全背包问题了,我们还是之前的转移方程
那么下面就是我们需要重要介绍的地方了
- 初始化
初始化是解决这个问题的最大的难题,我们来看实际的含义
在0个物品中选出至少价值为j的最小的开销
这是一个不合理的,所以我们还是需要设置一个特殊的值.
那么我们在这里怎么设置呢??
我们在这里不是设置一个无穷小,因为我们的需要得到的就是一个最小的值,这样的会影响我们的答案的,所以我们需要一个无穷大的数据来标记这些不合理的情况
但是当我们的第二维的数组的数字是一个负数的时候我们又是该怎么处理的呢?
我们来思考一下实际的含义是至少就是一个负数的话,是一个符合逻辑的啊,但是一个负数的话,我们直接去fi0中去取数据就可以了,从某种意义上来看这是等价的
所以我们的二维的需要取0和j-wi的最大值
这就是我们的解决方案
- 填表顺序
从上往下
代码实现
cpp
#include<iostream>
#include<cstring>
using namespace std;
const int N=110;
const int H=50010;
int p[N];
int c[N];
int f[N][H];
int main()
{
int n,h;
cin>>n>>h;
for(int i=1;i<=n;i++)
{
cin>>p[i]>>c[i];
}
memset(f,0x3f,sizeof(f));
f[0][0]=0;
for(int i=1;i<=n;i++)
{
for(int j=0;j<=h;j++)
{
f[i][j]=min(f[i-1][j],f[i][max(0,j-p[i])]+c[i]);
}
}
cout<<f[n][h]<<endl;
return 0;
}
上面就是我们的完全背包的问题了,下面我们最后一个就是多重背包了
话不多说直接上例题:
多重背包问题

这类问题和我们的多重背包有一点相似,但是不同就是物品的数量是有限的.
所以我们还怎么解决呢??
老规矩直接开始吧
- 状态表示
还是老旧的状态表示
fij表示在前i个物品中总重不超过j最大的价值
- 状态转移方程
和之前改变的就是我们的物品有多个但是有限
所以我们就需要用三层循环来逐个列举一下就可以解决了
- 初始化
根据我们的实际意义我们可以全部初始化为0
- 填表顺序
从上往下,从左往右
代码实现
cpp
#include<iostream>
using namespace std;
const int N=110;
int f[N][N];
int x[N],w[N],v[N];
int main()
{
int n,t;
cin>>n>>t;
for(int i=1;i<=n;i++)
{
cin>>x[i]>>w[i]>>v[i];
}
for(int i=1;i<=n;i++)
{
for(int j=0;j<=t;j++)
{
f[i][j]=f[i-1][j];
for(int k=1;k<=x[i];k++)
{
if(j>=k*w[i])
{
f[i][j]=max(f[i][j],f[i-1][j-k*w[i]]+k*v[i]);
}
}
}
}
cout<<f[n][t];
return 0;
}
总结
上述的是经典的背包问题,还有不同的题型,还需要我们通过刷题来理解
希望可以帮助到你
谢谢观看!!!