dp背包(一)

背包问题

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])
d p [ i ] [ j ] = m a x ( d p [ i 1 ] [ j ] , d p [ i 1 ] [ j c o s t [ i ] ] + v a l [ i ] ) ; dp[i][j]=max(dp[i-1][j],dp[i-1][j-cost[i]]+val[i]);
//可取可不取
else
d p [ i ] [ j ] = d p [ i 1 ] [ j ] dp[i][j]=dp[i-1][j]
//取不了这个物品

板子

二维代码

#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;
}
发布了28 篇原创文章 · 获赞 8 · 访问量 607

猜你喜欢

转载自blog.csdn.net/zty_ju/article/details/96350858