提到动态规划,首先我们了解一下它的历史:
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。
输出:
输出仅一行包含一个整数表示尼克可能获得的最大空暇时间。
样例输入:
遇到这种题目,一开始可能没有思路。那么先明确:
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;
}
#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;
}