背包问题
01背包
一个空间为V的背包,N件物品可装,cost[i]表第i个物品的消耗,val[i]表示第i个物品的价值,求能装下的最大价值
状态:dp[i][j]表示前i个物品用了j空间的最大价值
简单看几组样例我们可以发现最大价值一定与空间有关
V:100
cost val
90 100
20 20 * 5 显然在这组数据中我们应取下面一种方案
V:90
cost val
90 100
20 20 * 5 但在这组数据中我们应取上面一种方案
状态转移方程
if(j>cost[i])
//可取可不取
else
//取不了这个物品
板子
二维代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=35,maxm=205;
int w[maxn],c[maxn];
int dp[maxn][maxm];
int main()
{
int m,n;
scanf("%d%d",&m,&n);
for(int i=1;i<=n;i++)scanf("%d%d",&w[i],&c[i]);
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
if(j>=w[i])
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+c[i]);
else
dp[i][j]=dp[i-1][j];
}
}
printf("%d",dp[n][m]);
return 0;
}
那为毛我们平常看到的代码都是这样的呢
#include<bits/stdc++.h>
using namespace std;
const int maxn=35,maxm=205;
int w[maxn],c[maxn];
int dp[maxm];
int main()
{
int m,n;
scanf("%d%d",&m,&n);
for(int i=1;i<=n;i++)scanf("%d%d",&w[i],&c[i]);
for(int i=1;i<=n;i++){
for(int j=m;j>=w[i];j--){//注意
dp[j]=max(dp[j],dp[j-w[i]]+c[i]);
}
}
printf("%d",dp[m]);
return 0;
}
状态变成一维数组了?真是震惊
让我们来仔细分析分析
看到二维的代码,我们可以发现
dp[i][j]只跟上一层有关也就是dp[i-1][]
那么我们存下的每一层是不是只用过几次呢,还费空间
真是一大堆累赘呢
那么我们可以将dp[i][j]->dp[j]
将物品这个维度给去掉,做一个滚动数组
那么为什么要倒序呢(m->w[i])重点
这是因为我们的状态转移方程dp[j]=max(dp[j],dp[j-w[i]]+c[i]);
我们要的dp[j-w[i]]是上一层的但如果我们从w[i]->m循环的话
我们等到j再去找j-w[i]时dp[j-w[i]]已经更新成这一层的了(j>j-w[i])
例题 精卫填海
什么是这题的背包呢
体力
简直震惊
#include<bits/stdc++.h>
using namespace std;
const int maxv=20005,maxn=10005;
int k[maxn],w[maxn],dp[maxv];
int main()
{
int v,n,c;
scanf("%d%d%d",&v,&n,&c);
for(int i=1;i<=n;i++)scanf("%d%d",&k[i],&w[i]);
for(int i=1;i<=n;i++){
for(int j=c;j>=w[i];j--){
dp[j]=max(dp[j],dp[j-w[i]]+k[i]);
}
}
int ans=-1;
for(int i=0;i<=c;i++){
if(dp[i]>=v){
printf("%d",c-i);
return 0;
}
}
printf("Impossible");
return 0;
}
练习 通天之潜水
这道题的路径输出特有意思
可以参考学习
#include<bits/stdc++.h>
using namespace std;
const int maxn=105,maxm=205,maxv=205;
int a[maxn],b[maxn],c[maxn];
int dp[maxm][maxv];
bool vis[maxn][maxm][maxv];
void write(int x,int y,int z){
if(x==1){
if(vis[x][y][z]==1)
printf("%d ",x);
return ;
}
if(vis[x][y][z]==1){
write(x-1,y-a[x],z-b[x]);
printf("%d ",x);
}
else
write(x-1,y,z);
}
int main()
{
int m,v,n;
scanf("%d%d%d",&m,&v,&n);
for(int i=1;i<=n;i++)scanf("%d%d%d",&a[i],&b[i],&c[i]);
for(int i=1;i<=n;i++){
for(int j=m;j>=a[i];j--){
for(int k=v;k>=b[i];k--){
if(dp[j][k]<dp[j-a[i]][k-b[i]]+c[i]){
dp[j][k]=dp[j-a[i]][k-b[i]]+c[i];
vis[i][j][k]=1;
}
}
}
}
printf("%d\n",dp[m][v]);
write(n,m,v);
return 0;
}
完全背包
一个空间为V的背包,N种物品可装,每种物品无限量,cost[i]表第i个物品的消耗,val[i]表示第i个物品的价值,求能装下的最大价值
二维代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=35,maxm=205;
int w[maxn],c[maxn];
int dp[maxn][maxm];
int main()
{
int m,n;
scanf("%d%d",&m,&n);
for(int i=1;i<=n;i++)scanf("%d%d",&w[i],&c[i]);
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
if(j>=w[i])
dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+c[i]);//本期之谜
else
dp[i][j]=dp[i-1][j];
}
}
printf("max=%d",dp[n][m]);
return 0;
}
注释部分解释如下
本题与01背包的最大区别就是每种可以取无限个物品
为什么只将dp[i-1][j-w[i]]改成dp[i][j-w[i]]就行呢
简单的逻辑就是我们可以在前i种物品中继续取至多j-w[i]空间的物品
而不是之前我们不能取第i种物品了
同理它的一维代码如下
#include<bits/stdc++.h>
using namespace std;
const int maxn=10005,maxm=100005;
int w[maxn],c[maxn];
int dp[maxm];
int main()
{
int m,n;
scanf("%d%d",&m,&n);
for(int i=1;i<=n;i++)scanf("%d%d",&w[i],&c[i]);
for(int i=1;i<=n;i++){
for(int j=w[i];j<=m;j++){
dp[j]=max(dp[j],dp[j-w[i]]+c[i]);
}
}
printf("%d",dp[m]);
return 0;
}
为什么完全背包又是顺序呢
同之前01背包解释
这次我们就是要取更新过的dp值
当然就顺着来啊
例题 A+B Problem(再升级)
这跟a+b有什么关系~
一道完全背包
我们的物品应是小于n的所有质数
背包就是n
这次我们要求的是装满n的方案数
我才不会告诉你long long卡了我好久
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1005;
int prime[maxn];
unsigned long long dp[maxn];
int main()
{
int n,len=0;
scanf("%d",&n);
if(n==0||n==1){
printf("0");
return 0;
}
for(int i=2;i<=n;i++){
bool bj=0;
for(int j=2;j<=sqrt(i);j++){
if(i%j==0){
bj=1;
break;
}
}
if(bj==0)prime[++len]=i;
}
dp[0]=1;
for(int i=1;i<=len;i++){
for(int j=prime[i];j<=n;j++){
dp[j]+=dp[j-prime[i]];
//凑满j的方案数应是凑满j减比j小的质数的方案之和
}
}
printf("%lld",dp[n]);
return 0;
}
练习 [USACO08MAR]跨河River Crossing
这道题我们可以将我们要送过去的奶牛数量当作背包
dp[i]记录送完i头奶牛所需要的最小花费
那么这个值应该等于送第1 ~ k头牛和k+1 ~ i头牛的总和(0<=k<i)
注意每次送完要回来,输出答案时应把我们每次加进dp数组的最后一次回来减去
因为送完n头牛后我们已经不需要回来了
dp[k]和dp[i-k]一定都在dp[i]之前完成赋值,这也证实了这个算法的可能
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=2505;
int w[maxn];
int dp[maxn];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
int x;
scanf("%d",&x);
w[i]=w[i-1]+x;
}
dp[1]=m+w[1]+m;
for(int i=2;i<=n;i++){
dp[i]=m+w[i]+m;
for(int j=1;j<=i/2;j++){
dp[i]=min(dp[i],dp[j]+dp[i-j]);
}
}
printf("%d",dp[n]-m);
return 0;
}