做基础动态规划题目的方法

提到动态规划,首先我们了解一下它的历史:

20世纪50年代,Richard Bellman(美)等人首先提出了动态规划。

动态规划的英文为:dynamic programming 简称dp 所以现在大家都以dp作为动态规划的数组(不过我爱用f数组)

一般在各类比赛,如NOIP中,动态规划题非常普遍,那么怎么做动态规划题呢?

首先建议不要套公式,要在理解的基础上自己总结公式。

1.大部分动态规划可以写成记忆化搜索,如分治。如果不会写动规,可以试试记忆化;

2.首先写动规需要明确状态:一个数组f[i][j],明确f[i][j]代表什么;

3.思考转移:如01背包,转移应是:f[j]=f[j-w[i]]+c[i],也就是应该用什么公式;

4.明确起点与目标:起点可以理解成初始化,如f[0]=0;目标就是结果,如f[n]。

这里常用一种方法叫填表,也就是模拟计算机算出每个状态的最优解。非常实用。

接下来展示几道例题:

1.01背包

一个旅行者有一个最多能装m公斤物品的背包,现在有n件物品,它们的重量分别是w1,w2,…,wn,它们的价值分别为c1,c2,…,cn。若每件物品只有一件,求旅行者能获得的最大总价值。
输入:
第一行:两个整数,m(背包容量,m<=200)和n(物品数量,n<=30)。
第二~n+1行:每行两个整数wi,ci,表示每个物品的重量和价值。
输出:
一个数据,表示最大总价值。

样例输入:

10 4

2 1

3 3

4 5

7 9

样例输出:

12

f[i][j]代表前i个物品中,挑取一些放入容量为j的背包,得到的最大价值为多少。

填表:

i/j 0 1 2 3 4 5 6 7 8 9 10
0 0 0 0 0 0 0 0 0 0 0 0
1 0 0 1 1 1 1 1 1 1 1 1
2 0 0 1 3 3 4 4 4 4 4 4
3 0 0 1 3 5 5 6 8 8 9 9
4 0 0 1 3 5 5 6 9 9 10 12

转移:f[i][j]=max(f[i][j],f[i-1][j-w[i]]+c[i]);

寻找规律发现每一个元素转移都是从上一行的左边一部分转移过来的,前面几行在这时就没用了。可以思考,节省空间的机会来了——只用开个一维的数组。每次保留上一行。

转移:f[i][j]=max(f[j],f[j-w[i]]+c[i]);

可是如果从左到右循环,会发现:如果转移后的一行的前半部分已经完成,从下一个没有转移的元素转移,肯定从本行的元素转移了,不对。

为了避免后效性,内循环必倒着循环。这样转移就正确了。

注:通过细致考虑,会发现j-w[i]可能是负数,所以要保证j>=w[i]。

代码:

#include
using namespace std;
int w[31],c[31],n,m,f[201];
int main(){
 int i,j;
 scanf("%d %d",&m,&n);
 for(i=1;i<=n;i++) scanf("%d %d",&w[i],&c[i]);
 for(i=1;i<=n;i++)
  for(j=m;j>=w[i];j--)//倒着循环;保证j>=w[i].
   f[j]=max(f[j],f[j-w[i]]+c[i]);
 printf("%d",f[m]);
 return 0;
}

注:本人爱用万能头文件,在某些编译器中不能用,所以在比赛中慎用

2.尼克的任务

尼克每天上班之前都连接上英特网,接收他的上司发来的邮件,这些邮件包含了尼克主管的部门当天要完成的全部任务,每个任务由一个开始时刻与一个持续时间构成。 尼克的一个工作日为N分钟,从第一分钟开始到第N分钟结束。当尼克到达单位后他就开始干活。如果在同一时刻有多个任务需要完成,尼克可以任选其中的一个来做,而其余的则由他的同事完成,反之如果只有一个任务,则该任务必需由尼克去写成,假如某些任务开始时刻尼克正在工作,则这些任务也由尼克的同事完成。如果某任务于第P分钟开始,持续时间为T分钟,则该任务将在第P+T-1分钟结束。 写一个程序计算尼克应该如何选取任务,才能获得最大的空暇时间。

输入:

输入数据第一行包含两个用空格隔开的整数N和K,1≤N≤10000,1≤K≤10000,N表示尼克的工作时间,单位为分,K表示任务总数。 接下来共有K行,每一行有两个用空格隔开的整数P和T,表示该任务从第P分钟开始,持续时间为T分钟,其中1≤P≤N,1≤P+T-1≤N。

输出:

输出仅一行包含一个整数表示尼克可能获得的最大空暇时间。

样例输入:

15 6
1 2
1 6
4 11
8 5
8 1
1 1 5
样例输出:
4

遇到这种题目,一开始可能没有思路。那么先明确:

f[i]代表前i分钟尼克的空闲时间。

可以试试看填表,明确转移。。。



做着做着你会发现,这样写很麻烦——能不能换一种思路呢?

我们可以在“每个任务结束”这一方面想想看。

f[i]代表第i到第n分钟尼克的空闲时间。

试着转移:

f[i]=f[i+1]+1(当i时没有任务)

      max(f[p[j]+t[j])(有任务,从这个任务结束的时刻转移)

可以自己试试。


代码:

#include
using namespace std;
int n,k,p[10001],t[10001],f[10010];
int main(){
 int i,j;
 scanf("%d %d",&n,&k);
 for(i=1;i<=k;i++) scanf("%d %d",&p[i],&t[i]);
 j=k;
 for(i=n;i>=1;i--){
  if(i!=p[j]) f[i]=f[i+1]+1;
  else
   while(i==p[j]){
    f[i]=max(f[i],f[p[j]+t[j]]);
    j--;
   }
 }
 printf("%d",f[1]);
 return 0;
}
3.FATE
最近xhd正在玩一款叫做FATE的游戏,为了得到极品装备,xhd在不停的杀怪做任务。久而久之xhd开始对杀怪产生的厌恶感,但又不得不通过杀怪来升完这最后一级。现在的问题是,xhd升掉最后一级还需n的经验值,xhd还留有m的忍耐度,每杀一个怪xhd会得到相应的经验,并减掉相应的忍耐度。当忍耐度降到0或者0以下时,xhd就不会玩这游戏。xhd还说了他最多只杀s只怪。请问他能升掉这最后一级吗?
输入数据有多组,对于每组数据第一行输入n,m,k,s(0 < n,m,k,s < 100)四个正整数。分别表示还需的经验值,保留的忍耐度,怪的种数和最多的杀怪数。接下来输入k行数据。每行数据输入两个正整数a,b(0 < a,b < 20);分别表示杀掉一只这种怪xhd会得到的经验值和会减掉的忍耐度。(每种怪都有无数个)
输出升完这级还能保留的最大忍耐度,如果无法升完这级输出-1。
样例输入:
10 10 1 10
1 1
10 10 1 9
1 1
9 10 2 10
1 1
2 2
样例输出:
0
-1
1

f[i][j]表示使用i的忍耐度,杀j只怪得到的最多经验值。
f[i][j]=max(f[i][j],f[i-b[p]][j-1]+a[p]);p代表第p种怪。
在这里,一个外循环枚举怪物种数,也就是每一次增加一种怪,再重新模拟一次杀怪,在先前的基础上取最优解。
目标:f[m][s].
如果f[m][s]仍没达到目标,输出-1(强迫症患者表示崩溃。。。)
否则循环找最优解。
代码:
#include
using namespace std;
int n,m,k,s,a[101],b[101],f[101][101];
int main(){
	int i,j,l;
	while(scanf("%d %d %d %d",&n,&m,&k,&s)!=EOF){
		for(i=1;i<=k;i++) scanf("%d %d",&a[i],&b[i]);
		for(i=0;i<=m;i++)
			for(j=0;j<=s;j++)
				f[i][j]=0;
		for(i=1;i<=k;i++)
			for(j=b[i];j<=m;j++)
				for(l=1;l<=s;l++)
					f[j][l]=max(f[j][l],f[j-b[i]][l-1]+a[i]);
		if(f[m][s]=n){
					printf("%d\n",m-i);
					flag=1;
					break;
				}
			if(flag) break;
		}
	}
	return 0;
}
如果发现有bug,请告诉我哦~
这次就先讲到这了。身为一个小学生,我理解的也一定不够深入,技术也没那么好,请多多指教~
谢谢!

你可能感兴趣的:(原创-dp,动态规划,dp,c语言)