背包专题学习
01背包
有N件物品,1个容量为V的背包,每件物品只选择一次并且有各自的价值,要求在有限的背包容量下,装入的物品总价值最大。
思考:假如说这个背包的容量是无限的,那么我们要让总价值最大,只要全部选择就好了,这个思考是显然的,因为只有一个变量,就是物品的价值。那么在01背包中,我们考虑的因素有物品的当前价值和,还有当前物品的总体积是否符合背包的容量,怎么样让问题逐步得到解决呢?那么其实在dp的过程,每一行其实都是在使用控制变量法。
1.状态\(dp[i][j]\)定义:前i个物品,背包容量为j下的最优解
(1)当前的状态依赖于之前的状态,可以理解为\(dp[i][j]\)不断由之前的状态更新而来。
(2)当前背包容量不够(\(j<v[i]\)),没法选择,因此前i个物品最优解即为前i-1个物品的最优解:\(\color{red}{dp[i][j]=dp[i-1][j] }\)
(3)当前背包的容量够,可以选,因此需要决策选或不选这第i个物品:
比较一下\(\color{red}{dp[i-1][j]}\) 与 \(\color{red}{dp[i-1][j- v[i]]+w[i]}\),第二个啥意思呢?就是没放这个物品之前的最优解+当前这个物品的价值,这俩比较取最大值即可。
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
int v[MAXN]; // 体积
int w[MAXN]; // 价值
int f[MAXN][MAXN]; // f[i][j], j体积下前i个物品的最大价值
int main()
{
int n,m; cin>>n>>m;
for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(j<v[i]) f[i][j]=f[i-1][j];
else{
f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);
}
}
}
cout<<f[n][m];
}
01背包的简化版:
如果物品只有一个体积属性,求能放到背包的最多物品,那么只需要把体积看作价值,求最大体积即可。状态转移方程变为
练习题
P1048 [NOIP2005 普及组] 采药
1.这一题是直接套模版
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
int v[MAXN]; // 体积
int w[MAXN]; // 价值
int f[MAXN][MAXN]; // f[i][j], j体积下前i个物品的最大价值
int main()
{
int n,m; cin>>m>>n;
for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(j<v[i]) f[i][j]=f[i-1][j];
else{
f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);
}
}
}
cout<<f[n][m];
}
P1049 [NOIP2001 普及组] 装箱问题
1.这一题就是01背包的简化版,把各自体积当成价值即可
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
typedef pair<int,int> pii;
#define x first
#define y second
#define all(v) v.begin(),v.end()
int c[35];
int w[35];
int dp[20005];
void solve()
{
int v; cin>>v;
int n; cin>>n;
for(int i=1;i<=n;i++) cin>>c[i],w[i]=c[i];
for(int i=1;i<=n;i++)
{
for(int j=v;j>=c[i];j--)
{
dp[j]=max(dp[j],dp[j-c[i]]+w[i]);
}
}
cout<<v-dp[v];
}
signed main()
{
int t=1;
//cin>>t;
while(t--) solve();
}
P1164 小A点菜
1.这题是01背包的变形,我们的\(dp[i][j]\)存的是取前\(i\)种物品总和为\(j\)元的方案数
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
typedef pair<int,int> pii;
#define x first
#define y second
#define all(v) v.begin(),v.end()
int c[105];
int w[105];
int f[105][10005];//表示前i个菜品恰好花费j元的方案数
void solve()
{
int n,m; cin>>n>>m;
for(int i=1;i<=n;i++) cin>>w[i];
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++)
{
if(j==w[i]) f[i][j]=f[i-1][j]+1;//自己单独一种也是可以的
if(j<w[i]) f[i][j]=f[i-1][j];//这道菜太贵了买不起,方案只能是之前的
if(j>w[i]) f[i][j]=f[i-1][j-w[i]]+f[i-1][j];//未点这道菜之前已经凑成j的方案数+点前面的菜能
//够凑出j-w【i】个的数量 那么就是此时j的方案数
}
}
cout<<f[n][m];
}
signed main()
{
int t=1;
//cin>>t;
while(t--) solve();
}
P1060 [NOIP2006 普及组] 开心的金明
1.把总钱数当成背包最大容量,价钱*重要度当成价值,然后就是典型的01背包模版
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
typedef pair<int,int> pii;
#define x first
#define y second
#define all(v) v.begin(),v.end()
int v[105];
int w[105];
int f[30005];//买前i个物品,花j元的最大价值
void solve()
{
int n,m; cin>>n>>m;//n为最大容量,m为物品数量
for(int i=1;i<=m;i++){//这里也写成m
int x;
cin>>v[i]>>x;
w[i]=v[i]*x;
}
for(int i=1;i<=m;i++)//这里是m个物品,别写成n了
{
for(int j=n;j>=v[i];j--)
{
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
}
cout<<f[n];
}
signed main()
{
int t=1;
//cin>>t;
while(t--) solve();
}
滚动数组
滚动数组是DP中最常使用的空间优化技术。用滚动数组可以大大减少空间。
从状态转移方程来看 \(dp[i][j]=max( dp[i-1][j] ,dp[i-1][j-v[i]]+w[i] )\)可以知道\(dp[i][]\)只和\(dp[i-1]\)有关,和前面的\(dp[i-2][ ]\)、\(dp[i-3][ ]\)、.......都没有关系。每一行都是通过自己的上面一行推导出来的,跟更前面的行没有关系。那么干脆就复用这些空间,用新的一行覆盖已经无用的一行(滚动),只需要两行就够了。
1.交替滚动
定义\(dp[2][j]\) ,用\(dp[0][ ]\)和\(dp[1][]\)交替滚动。这种方法的优点是逻辑清晰,编码不易出错。
下面的代码中now始终指向正在计算的最新的一行,old指向已经计算过的旧的一行。按照原代码递推代码,now相当于\(i\),old相当于\(i-1\)。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
int v[MAXN]; // 体积
int w[MAXN]; // 价值
int f[MAXN][MAXN]; // f[i][j], j体积下前i个物品的最大价值
int dp[2][MAXN]; //替换f[][]
void solve()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
int now=0,old=1; //now指向正在计算的一行,old指向旧的一行
for(int i=1;i<=n;i++){
swap(old,now);//交替滚动,now始终指向最新的一行
for(int j=1;j<=m;j++){
if(j<v[i]) dp[now][j]=dp[old][j];
else dp[now][j]=max(dp[old][j],dp[old][j-v[i]]+w[i]);
}
}
cout<<dp[now][m];
}
signed main()
{
ios::sync_with_stdio(0),cin.tie(0);
int t=1;
//cin>>t;
while(t--) solve();
return 0;
}
2.自我滚动
在自我滚动中\(j\) 循环必须是从m-v[i]
如果从小到大循环,会因为重复使用同一个空间导致滚动数组出错。
这个点我的解释并不清晰,详情还可以搜索 https://oi-wiki.org/dp/knapsack/。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
int v[MAXN]; // 体积
int w[MAXN]; // 价值
int f[MAXN][MAXN]; // f[i][j], j体积下前i个物品的最大价值
int dp[MAXN];
void solve()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>v[i]>>w[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]);
}
}
cout<<dp[m];
}
signed main()
{
ios::sync_with_stdio(0),cin.tie(0);
int t=1;
//cin>>t;
while(t--) solve();
return 0;
}
滚动数组也有缺点。它覆盖了中间转移状态,只留下了最后的状态,所以损失了很多信息,导致无法输出具体的方案数。
完全背包
有一个背包的容积为V,有N个物品,每个物品的体积为v[i],权重为w[i],每个物品可以取无限次放入背包中,背包所有物品权重和最大是多少?
完全背包和01背包的区别就在于,01背包每个物品取1次,完全背包每个物品可以取无限次
思考:01背包问题每次取物品时,只能取一次,完全背包可以取无数次,那么我们可以让第\(i\)个物品增加0次,1次,2次....k-1次,然后求所有值的最大值作为\(dp[i][j]\)的最大值。
状态转移
对这个状态转移方程进行优化:
首先展开这个方程
把\(j=j-v[i]\)代入上面这个方程得到
对比观察发现,第一方程可以简化为:
最后对比一下两个01背包和完全背包的状态转移方程
还要注意一点:使用滚动数组在j层的循环当中,应该从\(\color{red}{v[i]}\)开始到\(\color{red}{m}\), 与01背包相反。
模版
#include <bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
typedef pair<int,int> pii;
#define x first
#define y second
#define all(v) v.begin(),v.end()
int v[1005];//体积
int w[1005];//价值
int f[1005];//对应体积下的最大价值
void solve()
{
int n,m; cin>>n>>m;
for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
for(int i=1;i<=n;i++)
{
for(int j=v[i];j<=m;j++)//从小到大是完全背包,从大到小是01背包
{
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
}
cout<<f[m];
}
signed main()
{
int t=1;
//cin>>t;
while(t--) solve();
}
posted on 2024-07-18 16:46 swj2529411658 阅读(26) 评论(0) 收藏 举报
浙公网安备 33010602011771号