背包问题小总结 习题(动态规划01背包(第k优解)完全背包,多重背包)acm杭电HDU2639,HDU2602,HDU1114,HDU2191

1、01背包(每种物品只有一个)

题目 有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。

求解将哪些物 品装入背包可使价值总和最大。

基本思路 这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。 用子问题定义状态:

           即表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。 则其状态转移方程便是:

                                      f[i][v] =Max { f[i−1][v] ,f[i−1][v−c[i]] + w[i] }

              这个方程非常重要

               把第i个物品在考虑加进去的时候,计算一下,加入这个物品后,背包的价值能不能提高,这就是状态方程              的意义。

核心代码:

for (int i=1;i<=n;i++)
	for (int v=V;v>=c[i];v--)
		f[v]=max(f[v],f[v-c[i]]+w[i]); 

代码实现时一般用到三个一维数组:f [V] , v[N] , w[N]。

V表示背包的最大容量,N表示物品个数

数组f存的是状态,比如说,f [ j ] 就表示把背包装到 j 这么重的时候的最大价值

数组v 和 w 分别存价值和重量。

 

例题1:HDU2602

题意:Teddy捡石头往他的背包里放,给出背包容量,石头个数,还有每个石头的重量和价值。求出怎么样选择                 石头,可以获得最大的价值。(经典)

代码:

#include<stdio.h>
#include<string.h>
int max(int a,int b)
{
    return a>b?a:b;
}
int main()
{
    int T,N,V;
    int i,j;
    int bag[1010],v[1010],w[1010];
    scanf("%d",&T);
    while(T--)
    {
        memset(bag,0,sizeof(bag));//把背包各个状态是的价值设为0
        scanf("%d%d",&N,&V);
        for(i=0;i<N;i++)
            scanf("%d",v+i);
        for(i=0;i<N;i++)
            scanf("%d",w+i);
        for(i=0;i<N;i++) //第i个物品
            for(j=V;j>=w[i];j--){ //j状态的背包(计算重量为j是的最大价值)
                bag[j]=max(bag[j],bag[j-w[i]]+v[i]);
            }
        printf("%d\n",bag[V]);
    }
    return 0;
}

例题2:HDU2546

题意:就是像一种购买方案,在最后一次买的时候,买最贵的把卡的余额欠的最大。但是在买最后一个之前,要把

           卡的余额刷到最接近5元(大于等于)。这个过程类似01背包。

思路:该题需要把卡的余额类比成背包重量。需要注意,每种菜的价值和重量相等,都是菜价。也就是背包问题中

           v [ i ] == w [ i ]

代码:

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
int main()
{
    int N,V,m;
    int i,j;
    int bag[1010],v[1010];
    while(~scanf("%d",&N),N)
    {
        memset(bag,0,sizeof(bag));
        for(i=0;i<N;i++)
            scanf("%d",v+i);
        sort(v,v+N);//通过排序把最贵的菜放到最后一个位置
        
        scanf("%d",&m);//把卡余额m看做背包容量
        //饭菜的价格即代表他的价值,也代表他的重量
        V=m-5;//我们的目的是把卡余额尽量刷到5元
        if(m<5){
            printf("%d\n",m);
            continue;
        }
        for(i=0;i<N-1;i++) //第i个菜
            for(j=V;j>=v[i];j--){
                bag[j]=max(bag[j],bag[j-v[i]]+v[i]);
            }
        printf("%d\n",m-v[N-1]-bag[V]);//减掉最贵的菜,和规划好的最大消费
    }
    return 0;
}


总结:状态方程不能死记硬背,要根据题目意思灵活应变。上面的代码中,背包状态都是从V往物品重量递推,且要取等号,恰好装也可以。

2、完全背包(每种物品有无限个)

题目 有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i], 价值是w[i]。求解将哪               些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最 大。
基本思路 这个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就是从每种 物品的角度考虑,与它相                  关的策略已并非取或不取两种,而是有取0件、取1件、取2件……等很 多种。如果仍然按照解01背包时                      的思路,令f[i][v]表示前i种物品恰放入一个容量为v的背包的最 大价值。可以按照每种物品不同的                       策略写出状态转移方程:
                                  f[i][v] = max { f[i−1][ v−k×w[i] ] + k×v[i] }          0 <= k×c[i] <= V

             但是实际代码不是这么直接套用的,而是在当前状态下,继续添加当前物品,直到背包装满。

for(int i=0;i<N;i++)
    for(int j=w[i];j<=V;j++)//V是背包最大容量
        f[j]=max(f[j],f[j-w[i]]+v[i]) //数组w重量,v价值

注意:

和01背包仅有一点区别,就是循环顺序。但是动态规划出来的结果是截然不同的!

             先回忆一下01背包为什么要到这循环,因为每一个物品在装进去的时候,都要用到前一个状态的数据来计算当前物品要不要加入。简单说,就是由前面物品装好的状态推出当前物品装入的状态。

             而内循环为正序时,先更新状态,再用更新了的状态继续更新当前状态,就产生了一个现象:在不超背包容量的状态下,我可以一直用当前物品不断更新背包状态,即不断往背包里填同一个物品。打个比方,我想在正在装一个重m的物品,进入内循环,我先把f [m] 这个状态更新了,想在f [m]就是装入了m后的最大价值,继续更新f [m+1]的状态,我会用到 f [j]=max( f [j], f [j-v[i]]+v[i]); 这个语句,也就是会调用到更小重量时的状态,而我恰好在这之前更新过了,那不就是我可以第二次把物品m装进背包吗。以此类推,我往后更新状态过程中,我可以无限的把同一个物品 i 往里装。

例题3:HDU1114

题意:某人有个存钱罐(存硬币),空着的时候重E,存满钱是重F。现在他把罐子装满钱了,给出硬币种类和重量,计算存钱罐最少能存多少钱。

思路:完全背包。每种硬币可以无限放。但是这里不是求最大价值,而是求最小价值,所以我们先假设所有状态为正无穷,然后不断更新dp,以求得最小价值。这样在动态规划时需要注意一点,状态0始终未0,即f [ 0 ] =0恒成立

代码:

#include<stdio.h>
#include<algorithm>
using namespace std;
const int INF=0x6fffffff;
int f[10010];
int main()
{
    int T,E,F,M,N;
    int p,w; //价值和重量
    scanf("%d",&T);
    while(T--)
    {
        for(int i=1;i<=10002;i++)f[i]=INF;
        f[0]=0;//这一步很重要,给动态规划一个开头
        scanf("%d%d%d",&E,&F,&N);
        M=F-E; //最大钱数M
        for(int i=0;i<N;i++){ //第 i 种硬币
            scanf("%d%d",&p,&w);
            for(int j=w;j<=M;j++) //不断往里放这种硬币,取钱数少的方案
                f[j]=min(f[j],f[j-w]+p);
        }
        if(f[M]==INF)
            puts("This is impossible.");
        else
            printf("The minimum amount of money in the piggy-bank is %d.\n",f[M]);
    }
    return 0;
}

总结:与01背包的代码实现类似,但过程却相差甚大,一定要区分代码的计算思路

3、多重背包(每种物品有多个)

           与01背包类似,只不过每种物品的数量不止一个。最直接的想法,把物品i有n个想象成,有n个物品一模一样,再用01背包求解即可。

51nod(优化代码)

1086 背包问题 V2

基准时间限制:1 秒 空间限制:131072 KB 分值: 40 难度:4级算法题

 收藏

 关注

有N种物品,每种物品的数量为C1,C2......Cn。从中任选若干件放在容量为W的背包里,每种物品的体积为W1,W2......Wn(Wi为整数),与之相对应的价值为P1,P2......Pn(Pi为整数)。求背包能够容纳的最大价值。

Input

第1行,2个整数,N和W中间用空格隔开。N为物品的种类,W为背包的容量。(1 <= N <= 100,1 <= W <= 50000)
第2 - N + 1行,每行3个整数,Wi,Pi和Ci分别是物品体积、价值和数量。(1 <= Wi, Pi <= 10000, 1 <= Ci <= 200)

Output

输出可以容纳的最大价值。

Input示例

3 6
2 2 5
3 3 8
1 4 1

Output示例

9

如Ci  = 14,我们可以把它化成如下4个物品:

重量是Wi,体积是Vi
重量是2 * Wi , 体积是2 * Vi
重量是4 * Wi , 体积是4 * Vi
重量是7 * Wi , 体积是7 * Vi

注意最后我们最后我们不能取,重量是8 * Wi , 体积是8 * Vi 因为那样总的个数是1 + 2 + 4 + 8 = 15个了,我们不能多取对吧?

【代码】:

#include<stdio.h>
typedef long long ll;
template<class T>T max(T a,T b)
{
	return a>b?a:b;
}
ll f[60000];
ll w[2002000];
ll p[2002000];
int main()
{
	int n,W;
	scanf("%d%d",&n,&W);
	int k=0;
	for(int i=0;i<n;i++)
	{
		int w1,p1,c1;
		scanf("%d%d%d",&w1,&p1,&c1);
		int t=1;
		while(c1>0)
		{
			if(c1>t)
			{
				w[k]=t*w1;
				p[k]=t*p1;
			}
			else
			{
				w[k]=c1*w1;
				p[k]=c1*p1;
			}
			c1=c1-t;
			t=t*2;
			k++;
		}
	}
	for(int i=0;i<k;i++)
	for(int j=W;j>=w[i];j--)
	{
		f[j]=max(f[j],f[j-w[i]]+p[i]);
	}
	printf("%lld\n",f[W]);
	return 0;
}

例题:HDU 2191 

代码:

//HDU 2191 多重背包 
#include<stdio.h>
int val[110],wei[110],count[110],dp[110];//价格,重量,袋数。动态背包 
int max(int a,int b)
{
	return a>b?a:b;
}
int main()
{
	int T;
	scanf("%d",&T);
	while(T--)
	{
		int n,m;// 钱数,种类 
		scanf("%d%d",&n,&m);
		for(int i=0;i<m;i++)
		{
			scanf("%d%d%d",val+i,wei+i,count+i);
		}
		for(int i=0;i<=n;i++) dp[i]=0;//初始为0 
		
		for(int i=0;i<m;i++) //第 i 种大米 
			for(int k=1;k<=count[i];k++) //大米 i 放入的次数 
				for(int j=n;j>=val[i];j--) // 动态规划 
					dp[j]=max(dp[j],dp[j-val[i]]+wei[i]);
		printf("%d\n",dp[n]); //数组dp存的是重量 
	}
	return 0;
}

4、01背包第k个最优解

        思想与01背包相同,不过在动态规划过程中,不可以把前一个状态的最优解扔掉,而要保存下来

在01背包中,状态数组是 f [ v ] ,表示容积为v时的最优决策。而现在,我不仅要知道最优决策,我还想知道稍微差一点的决策,即第2决策、第3决策....排个名。f [ v ] 我们可以看做是 f [ v ] [ 1 ] 这样的二维数组,他的第二维只有一个元素,也就是最优决策。现在我们增大第二维,比如 f [ v ] [ 3 ] ,意思是,不仅保留了最优解,次解也保留下来了。

也可以理解为,第二维是一个集合,集合里就存了所有可能的决策,并按大小有序,排在第一的就是最优解。

现在问题是 第k个最优决策是多少。

           在01背包里,我们只保留了最优解,而把不是最优解的解直接舍弃了,即 

f[j]=max(f[j],f[j-w[i]]+v[i])

这时候,较小的那个解直接舍弃了,没保留下来。现在我们要做的就是,借助数组把所有的解都保留下来。

核心代码:

		int i,j,t;
		for(i=0;i<n;i++) //对每个物品扫描
			for(j=v;j>=vol[i];j--) //对每个状态进行更新 
			{
				for(t=1;t<=k;t++)
				{ //把所有可能的解都存起来
					a[t]=f[j][t];
					b[t]=f[j-vol[i]][t]+val[i];
				}
				int m,x,y;
				m=x=y=1;
				a[k+1]=b[k+1]=-1;
				//下面的循环相当于求a和b并集,也就是所有的可能解 
				while(m<=k && (a[x]!=-1 || b[y]!=-1))
				{
					if(a[x]>b[y])
						f[j][m]=a[x++];
					else 
						f[j][m]=b[y++];	
					if(f[j][m]!=f[j][m-1])
						m++;
				}
			}

和01背包的代码比较一下。就是多了里面一层处理比较麻烦,但思想很容易接受。就是把所有可能解先借助a,b两个数组存下来,然后按顺序赋给状态数组f。

例题HDU2639

模板题

代码:

#include <stdio.h>  
#include<string.h>
int f[1010][33];//第一维和普通01背包一样。第二维存多个最优解
int val[110],vol[110];//价值和体积
int a[33],b[33];//用于暂时存储多组最优解 
int n,v,k;
int i,j,t,T;
int main()
{
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d%d%d",&n,&v,&k);
		for(i=0;i<n;i++) scanf("%d",&val[i]);
		for(i=0;i<n;i++) scanf("%d",&vol[i]);
		memset(f,0,sizeof(f));//对状态数组清0 
		
		for(i=0;i<n;i++) //对每个物品扫描
			for(j=v;j>=vol[i];j--) //对每个状态进行更新 
			{
				for(t=1;t<=k;t++)
				{ //把所有可能的解都存起来
					a[t]=f[j][t];
					b[t]=f[j-vol[i]][t]+val[i];
				}
				int m,x,y;
				m=x=y=1;
				a[k+1]=b[k+1]=-1;
				//下面的循环相当于求a和b并集,也就是所有的可能解 
				while(m<=k && (a[x]!=-1 || b[y]!=-1))
				{
					if(a[x]>b[y])
						f[j][m]=a[x++];
					else 
						f[j][m]=b[y++];	
					if(f[j][m]!=f[j][m-1])
						m++;
				}
			}
	printf("%d\n",f[v][k]);
	}
	return 0;
 } 

5、01背包记录路径.

            用二维数组来记录,path[ m ] [ n ] 。其中m表示物品(m<=物品数),n表示背包状态(n<=背包容量)。

比如 path [ i ] [ j ] 表示物品 i 放在了状态 j 的背包中。 前提条件:path数组全部为0,

代码实现记录路径

		for(int i=0;i<n;i++)
			for(int j=V;j>=v[i];j--)
				if(f[j]<f[j-v[i]]+w[i])
				{
					f[j]=f[j-v[i]]+w[i];
					path[i][j]=1; //把装进去的物品标记一下
				}

路径读取代码

		int i=n-1,j=V; //V:背包容量。n个物品 
		while(i>=0&&j>=0)
		{
			if(path[i][j])//物品i在j里 
			{
				printf("%d ",i);//把物品i的编号输出 
				j-=v[i];  //读完了物品i,找下一个背包状态 
			}
			i--; 
		}

 

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

雪的期许

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值