动态规划(普通dp,01背包,完全背包)

此文为自己复习理解用,本人菜鸡一枚,如有不当之处请指正

首先看一道例题

 这个问题我们可能首先会想到用递归解决,但如果有n种硬币,需要m元(键入),用递归解决就不合适了,这时就需要我们的动态规划算法了,它与递归之间的区别主要在于dp将计算过的结果存了下来,而递归则进行了许多重复计算。

附c++递归代码

#include<iostream>
#include<algorithm>
using namespace std;
int dg(int x)
{
    if(x<=0)    return 0;
    int res=0x3f3f3f3f;
    if(x>=2)    res=min(res,dg(x-2)+1);
    if(x>=5)    res=min(res,dg(x-5)+1);
    if(x>=7)    res=min(res,dg(x-7)+1);
    return res;
}
int main()
{
	cout<<dg(27);
	return 0;
 } 

对于这道题来说,dp其实采取了几乎一样的策略

dp的做题技巧

1.找状态

2.考虑边界和初始条件

3.状态转移方程

4.考虑计算顺序

放到这个题目来看:

1.状态:我们设一个f数组,f[x]表示x枚硬币的最优划分方案

2.边界和初始条件:x>0 , f[0]=0

3.状态转移方程:f[x]=min(f[x-2]+1,f[x-5]+1,f[x-7]+1);

4.计算顺序:因为我们已经知道f[0]的值,故采取0-27的顺序将更加方便

附dp c++代码

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
int f[1010];
int main()
{
	memset(f,0x3f,sizeof f);
	f[0]=0;
	for(int i=1;i<=27;i++)
	{
		if(i>=2)	f[i]=min(f[i],f[i-2]+1);
		if(i>=5)	f[i]=min(f[i],f[i-5]+1);
		if(i>=7)	f[i]=min(f[i],f[i-7]+1);
	}
	cout<<f[27];
	return 0;
}

接下来我们思考一个进阶题目——有n种硬币,硬币面额需键入,有m元钱,思考最少的硬币组合

我们还是先找状态:f[x]为x元时的最佳硬币组合

初始情况:f[0]=0     边界:x>0

状态转移方程:我们设硬币面额存在a数组中,那么 f[x]=min(f[x-a[1]],...,f[x-a[n]])+1

计算顺序:从小到大

#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
int f[1010],a[1010],n,m;
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	memset(f,0x3f,sizeof f);
	f[0]=0;
	for(int i=1;i<=m;i++)
	{
		for(int j=1;j<=n;j++)
		{
			if(i>=a[j])		f[i]=min(f[i],f[i-a[j]]+1);
		}
	}
	printf("%d",f[m]);
	return 0;
}

到这里,dp入门内容基本完成,接下来我们进入背包问题

首先是最基础的0/1背包问题,题目如下,它的特点是每种物品只有一件,这样在选择时只能选择0件(不选)或者选一件

样例数据:     

输入                输出:14

3 6
2 5
3 8        我们用一个表格帮助理解
4 9        每一行开头的数x表示选前x个物品,每一列开头的数y表示物品的总体积不超过y
物品价值0123456
00000000
10055555
2005881313
3005891314

通过表格和对题意的理解,我们得到状态为:f[i][j]表示选前i件物品,总体积不超过j的情况下,背包中物品的最大价值

初始条件:如表格所示,f[x][0]和f[0][x]的值都为0     边界:x,y>0

状态转移方程:通过表格和对题意的理解,我们可知,每一次选择,我们都有选和不选两种选项,不选的话,f[i][j]=f[i-1][j],如果选,f[i][j]=f[i-1][j-w[i]]+p[i]

综上:f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+p[i])

计算顺序:从小到大

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
int f[110][10010],P[110],W[110],n,w;
int main()
{
	scanf("%d%d",&n,&w);
	for(int i=1;i<=n;i++)
	{
		scanf("%d%d",&W[i],&P[i]);
	}
	memset(f,0,sizeof f);
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=w;j++)
		{
			if(j-W[i]>=0)
				f[i][j]=max(f[i-1][j],f[i-1][j-W[i]]+P[i]);
			else
				f[i][j]=f[i-1][j];
		}
	}
	printf("%d",f[n][w]);
	return 0;
}

再思考,现在我们的n只是不超过100而已,如果不超过10000,再采取刚才的策略,空间这第一关都过不了,这该怎么办?

我们通过观察刚才的表格和代码发现,f[i][j]的值都是从f[i-1][j]或f[i-1][j-W[i]]过来的,也就是说每一行的数据都是从上一行的数据经过处理后得到的,我们可以以此为突破点

接下来介绍——滚动数组  顾名思义,这个数组是滚动的,存完一行之后,下一行可以将原来的数据覆盖掉,以此来减少开数组而占用的空间,而这个数组怎么开呢?很简单,只需要开一个一维数组即可,大小为w+1,在本题里为f[101]即可

那具体操作是什么?其实跟原来的二维数组差不多,只需将f[i][j]中表示行的i删去,变成f[j]即可,这样的话,状态转移方程就变成了f[j]=max(f[j],f[j-W[i]]+P[i]),但我们还需要思考一件事

物品价值0123456
00000000
10055555
2005881313--16
3005891314

我们再来观察这个表格,看1、2行,当j=3时,f[j]取f[j-W[i]]+P[i],这样的话f[3]由5变成了8,而当j=6时,f[j]本来应该取5+8=13,现在却变成了8+8=16,出现了错误

再来思考,怎样解决这个问题呢?我们只需让j从后往前循环,就不会出现前面的数将f数组的值覆盖,后面的数出现错误的情况,我们再用表格模拟一下,发现的确是这样的

物品价值0123456
00000000
10055555
2005881313
3005891314
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
int f[10010],P[110],W[110],n,w;
int main()
{
	scanf("%d%d",&n,&w);
	for(int i=1;i<=n;i++)
	{
		scanf("%d%d",&W[i],&P[i]);
	}
	memset(f,0,sizeof f);
	for(int i=1;i<=n;i++)
	{
		for(int j=w;j>=1;j--)
		{
			if(j-W[i]>=0)
				f[j]=max(f[j],f[j-W[i]]+P[i]);
			else
				f[j]=f[j];
		}
	}
	printf("%d",f[w]);
	return 0;
}

好的,现在我们进入完全背包,下附例题:

 完全背包与0/1背包的不同之处在于每一件物品都可以选择无数次,其他事项都和0/1背包一致,所以我们可以延用0/1背包的状态,初始情况,边界,计算顺序,对于状态转移方程稍加变动即可

再把我们的表格拿出来并稍加改变

状态:f[i][j]:在前i个物品中,选总体积不超过j的物品所能达到的最大价值

样例: 输入3 6    2 5   3 8    4 9

物品价值0123456
00000000
10055101015
20058101316
30058913

16

我们再来分析一下:对于每一件物品,我们有这几种选择:选0件,选1件,选2件,...,选n件

其中n*W[i]<=w,那对于状态转移方程的变化,我们也有了头绪

原方程:f[i][j]=max(f[i-1][j],f[i-1][j-V[i]]+C[i])

现方程:f[i][j]=max(f[i-1][j],f[i-1][j-n*V[i]]+n*C[i])

解释一下:跟0/1背包一样, 对于一件物品,我们可以选择不选它,就有了max函数里的第一个参数f[i-1][j],跟0/1背包的区别在于,一件物品,只要我选择的件数×该物品的体积不超过j,在这个范围里我可以任意选择,所以我们可以加一重循环,让循环变量k从1开始,循环条件为k*V[i]<=j,k++

初始条件:如表格所示,f[0][x]=0,f[x][0]=0      边界:j>0

计算顺序:从小到大

#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
int f[110][50010],V[110],C[110],n,v;
int main()
{
	scanf("%d%d",&n,&v);
	for(int i=1;i<=n;i++)
	{
		scanf("%d%d",&V[i],&C[i]);
	}
	memset(f,0,sizeof f);
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=v;j++)
		{	
			for(int k=0;k*V[i]<=j;k++)
			{
				f[i][j]=max(f[i][j],f[i-1][j-k*V[i]]+k*C[i]);
			}
		}
	}
	printf("%d",f[n][v]);
	return 0;
}

因为本题数据范围过大,上面的代码会被卡掉部分分,所以跟0/1背包一样,接下来我们思考如何优化

又是和0/1背包一致,我们根据上面的代码,又一次发现每一行的数据都是从上一行的数据而来,如此我们便可以采取和0/1背包相似的策略——将f数组降成一维,把f[i][j]变成f[j]

如此,状态转移方程变成了:f[j]=max(f[j],f[j-k*V[i]]+k*C[i])

但是我们的优化还没停止,刚才我们只是减小了空间复杂度,对于例题,我们的时间复杂度依然过高,怎么办呢?我们继续来观察表格

物品价值0123456
00000000
第一次0055101015
第二次0058101316
第三次0058913

16

我们可以发现,对于每一个f[j],都可以从上一次的f[j]和本次的f[j-V[i]]变来(从f[j]和f[j-V[i]]+C[i]        中取最大值),那我们是不是可以删去第三重循环k啊!

我们再来思考,这一次,第二重循环还需要倒序进行吗?答案是:不需要了,我们再来观察表格

物品价值0123456
00000000
第一次0055101015
第二次0058101316
第三次0058913

16

之前说过,对于每一个f[j],都是从f[j]和f[j-V[i]]+C[i]中取最大值后得到的,也就是说我们正序进行就可以,如果倒序进行反而会出现错误

#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
int f[50010],V[110],C[110],n,v;
int main()
{
	scanf("%d%d",&n,&v);
	for(int i=1;i<=n;i++)
	{
		scanf("%d%d",&V[i],&C[i]);
	}
	memset(f,0,sizeof f);
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=v;j++)
		{	
			if(j-V[i]>=0)
				f[j]=max(f[j],f[j-V[i]]+C[i]);
			else
				f[j]=f[j];
		}
	}
	printf("%d",f[v]);
	return 0;
}

that's all, thanks for watching!

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值