一、动态规划简介
动态规划是运筹学的一个分支。它是解决多阶段决策过程最优化问题的一种方法。1951年,美国数学家贝尔曼(R.Bellman)提出了解决这类问题的“最优化原则”,1957年发表了他的名著《动态规划》,该书是动态规划方面的第一本著作。动态规划问世以来,在工农业生产、经济、军事、工程技术等许多方面都得到了广泛的应用,取得了显著的效果。
动态规划运用于信息学竞赛是在90年代初期,它以独特的优点获得了出题者的青睐。此后,它就成为了信息学竞赛中必不可少的一个重要方法,几乎在所有的国内和国际信息学竞赛中,都至少有一道动态规划的题目。所以,掌握好动态规划,是非常重要的。
动态规划是一种方法,是考虑问题的一种途径,而不是一种算法。因此,它不像深度优先和广度优先那样可以提供一套模式。它必须对具体问题进行具体分析。需要丰富的想象力和创造力去建立模型求解。
[例8-1] 拦截导弹
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
输入导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
样例:
INPUT OUTPUT
389 207 155 300 299 170 158 65 6(最多能拦截的导弹数)
2(要拦截所有导弹最少要配备的系统数)
[问题分析]
第一部分是要求输入数据串中的一个最长不上升序列的长度,可使用递推的方法,具体做法是从序列的第一个元素开始,依次求出以第i个元素为最后一个元素时的最长不上升序列的长度len(i),递推公式为len(1)=1,len(i)=max(len(j)+1),其中i>1,j=1,2,…i-1,且j同时要满足条件:序列中第j个元素大于等于第i个元素。
第二部分比较有意思。由于它紧接着第一问,所以很容易受前面的影响,采取多次求最长不上升序列的办法,然后得出总次数,其实这是不对的。要举反例并不难,比如长为7的高度序列“7 5 4 1 6 3 2”, 最长不上升序列为“7 5 4 3 2”,用多次求最长不上升序列的结果为3套系统;但其实只要2套,分别击落“7 5 4 1”与“6 3 2”。那么,正确的做法又是什么呢?
我们的目标是用最少的系统击落所有导弹,至于系统之间怎么分配导弹数目则无关紧要;上面错误的想法正是承袭了“一套系统尽量多拦截导弹”的思维定势,忽视了最优解中各个系统拦截数较为平均的情况,本质上是一种贪心算法。如果从每套系统拦截的导弹方面来想行不通的话,我们就应该换一个思路,从拦截某个导弹所选的系统入手。
题目告诉我们,已有系统目前的瞄准高度必须不低于来犯导弹高度,所以,当已有的系统均无法拦截该导弹时,就不得不启用新系统。如果已有系统中有一个能拦截该导弹,我们是应该继续使用它,还是另起炉灶呢?事实是:无论用哪套系统,只要拦截了这枚导弹,那么系统的瞄准高度就等于导弹高度,这一点对旧的或新的系统都适用。而新系统能拦截的导弹高度最高,即新系统的性能优于任意一套已使用的系统。既然如此,我们当然应该选择已有的系统。如果已有系统中有多个可以拦截该导弹,究竟选哪一个呢?当前瞄准高度较高的系统的“潜力”较大,而瞄准高度较低的系统则不同,它能打下的导弹别的系统也能打下,它够不到的导弹却未必是别的系统所够不到的。所以,当有多个系统供选择时,要选瞄准高度最低的使用,当然瞄准高度同时也要大于等于来犯导弹高度。
解题时,用一个数组记下已有系统的当前瞄准高度,数据个数就是系统数目。
[程序清单]
const max=1000;
var i,j,current,maxlong,minheight,select,tail,total:longint;
height,longest,sys:array [1..max] of longint;
line:string;
begin
write('Input test data:');
readln(line);
i:=1;
while i<=length(line) do
begin
while (i<=length(line)) and (line[i]=' ') do i:=i+1;
current:=0;
while (i<=length(line)) and (line[i]<>' ') do
begin
current:=current*10+ord(line[i])-ord('0');
i:=i+1
end;
total:=total+1;
height[total]:=current
end;
longest[1]:=1;
for i:=2 to total do
begin
maxlong:=1;
for j:=1 to i-1 do
begin
if height[i]<=height[j]
then if longest[j]+1>maxlong
then maxlong:=longest[j]+1;
longest[i]:=maxlong
end;
end;
maxlong:=longest[1];
for i:=2 to total do
if longest[i]>maxlong then maxlong:=longest[i];
writeln(maxlong);
sys[1]:=height[1]; tail:=1;
for i:=2 to total do
begin
minheight:=maxint;
for j:=1 to tail do
if sys[j]>height[i] then
if sys[j]<minheight then
begin minheight:=sys[j]; select:=j end;
if minheight=maxint
then begin tail:=tail+1; sys[tail]:=height[i] end
else sys[select]:=height[i]
end;
writeln(tail)
end.
二、动态规划的几个基本概念
想要掌握好动态规划,首先要明白几个概念:阶段、状态、决策、策略、指标函数。
1.阶段:把所给问题的过程,恰当地分为若干个相互联系的阶段,以便能按一定的次序去求解。描述阶段的变量称为阶段变量。
2.状态:状态表示每个阶段开始所处的自然状况和客观条件,它描述了研究问题过程中的状况,又称不可控因素。
3.决策:决策表示当过程处于某一阶段的某个状态时,可以作出不同的决定(或选择),从而确定下一阶段的状态,这种决定称为决策,在最优控制中也称为控制。描述决策的变量,称为决策变量。
4.策略:由所有阶段的决策组成的决策函数序列称为全过程策略,简称策略。
5.状态转移方程:状态转移方程是确定过程由一个状态到另一个状态的演变过程。
6.指标函数:用来衡量所实现过程优劣的一种数量指标,称为指标函数。指标函数的最优值,称为最优值函数。
三、确定动态规划的思路
1、采用动态规划来解决问题,必须符合两个重要的条件。
(1)“过去的历史只能通过当前状态去影响它未来的发展,当前的状态是对以往历史的一个总结”,这种特性称为无后效性,是多阶段决策最优化问题的特征。
(2)作为整个过程的最优策略具有这样的性质:即无论过去的状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简言之,一个最优策略的子策略总是最优的。这就是最优化原理。
2、如果碰到一个问题,能够满足以上两个条件的话,那么就可以去进一步考虑如何去设计使用动态规划:
(1)划分阶段。把一个问题划分成为许多阶段来思考
(2)设计合适的状态变量(用以递推的角度)
(3)建立状态转移方程(递推公式)
(4)寻找边界条件(已知的起始条件)
如果以上几个步骤都成功完成的话,我们就可以进行编程了。
四、动态规划解题的一些技巧
由于动态规划并没有一个定式,这就需要去开拓我们创造力去构造并且使用它。以下,通过一些具体的竞赛实例谈谈使用动态规划过程中的一些技巧。
[例 8-2] 堆塔问题,设有n个边长为1的正立方体,在一个宽为1的轨道上堆塔,但塔本身不能分离。
例如n=1时,只有1种方案 □
n=2时 有2种方案
堆塔的规则为底层必须有支撑,如下列的堆法是合法的.
下面的堆法是不合法的.
程序要求:输入n(n<=40),求出
① 总共有多少种不同的方案
② 堆成每层的方案数是多少,例如n=6时,堆成1层的方案数为1,……堆成6层的方案数为1……
[问题分析] 问题①的分析见第七讲例7-3,关于问题②,可以这样考虑,将n个立方体的第一列去掉的话,则成为一个比n小的堆塔问题,这样问题②就可以用动态规划法来解。具体方法是从1到n依次求出堆成每层的方案数,设f(n,k)为堆成k层的方案数,则递推公式为:
[算法设计] 由于n最大可达到40,所以堆塔的总方案数超过了长整形数的范围,程序采用了高精度加法运算。
[程序清单]
program ex8_2(input,output);
const maxn=40; maxlen=maxn div 3;
type arraytype=array[0..maxlen] of integer;
var i,j,k,n,nn:longint;
total:arraytype;
f:array [0..maxn,0..maxn] of arraytype;
procedure add(var a:arraytype;b:arraytype);
var i:longint;
begin
for i:=0 to maxlen do a[i]:=a[i]+b[i];
for i:=0 to maxlen-1 do
if a[i]>=10 then
begin
a[i+1]:=a[i+1]+1;
a[i]:=a[i]-10
end
end;
procedure print(a:arraytype);
var i,j:longint;
begin
i:=maxlen;
while (i>0) and (a[i]=0) do i:=i-1;
for j:=i downto 0 do write(a[j])
end;
begin
write('Input n:');
readln(n);
for i:=1 to maxlen do total[i]:=0;
total[0]:=1;
for i:=1 to n-1 do add(total,total);
for i:=1 to maxn do
for j:=1 to maxn do
for k:=0 to maxlen do f[i,j,k]:=0;
for i:=1 to maxn do f[i,1,0]:=1;
for i:=1 to maxn do f[i,i,0]:=1;
for nn:=2 to n do
for k:=2 to nn-1 do
begin
for i:=1 to k-1 do add(f[nn,k],f[nn-i,k]);
for i:=1 to k do
if nn-k>=i then add(f[nn,k],f[nn-k,i])
end;
write('Total=');
print(total);
writeln;
for k:=1 to n do
begin
write('Height=',k:2,'Kind=':10);
print(f[n,k]);
writeln;
if k mod 23=0
then
begin
write('Press <Enter> to continue!');
readln
end
end;
write('Press <Enter> to continue!');
readln
end.
[例 8-3] 投资问题:
有n万元的资金,可投资于m个项目,其中m 和n为小于100的自然数。对第i(1≤i≤m)个项目投资j万元(1≤j≤n,且 j为整数)可获得的回报为Q(i , j),请编程序,求解并输出最佳的投资方案(即获得回报总值最高的投资方案)。
输入数据放在一个文本文件中,格式如下:
m n
Q(1 , 0) Q(1 , 1)······Q(1 , n)
Q(2 , 0) Q(2 , 1)······Q(2 , n)
············
Q(m , 0) Q(m , 1)······Q(m , n)
输出数据格式为:
r(1) r(2) ······ r(m) P
其中r(i)(1≤i≤m)表示对第i个项目的投资万元数,P为总的投资回报值,保留两位有效数字,任意两个数之间空一格。当存在多个并列的最佳投资方案时,只要求输出其中之一即可。如输入数据如下时:
2 3
0 1.1 1.3 1.9
0 2.1 2.5 2.6
屏幕应输出:1 2 3.6
[问题分析] 本题可供考虑的递推角度只有资金和项目两个角度,从项目的角度出发,逐个项目地增加可以看出只要算出了对前k个项目投资j万元最大投资回报值(1≤j≤n),就能推出增加第k+1个项目后对前k+1个项目投资j万元最大投资回报值(1≤j≤n),设P[k,j]为前k个项目投资j万元最大投资回报值,则P[k+1,j]= Max(P[k,i]+Q[k+1,j-i]),0<=i<=j,k=1时,对第一个项目投资j万元最大投资回报值(0≤j≤n)是已知的(边界条件)。
[算法设计] 动态规划的时间复杂度相对于搜索算法是大大降低了,却使空间消耗增加了很多。所以在设计动态规划的时候,需要尽可能节省空间的占用。本题中如果用二维数组存储原始数据和最大投资回报值的话,将造成空间不够,事实上,从前面的问题分析可知:在计算对前k+1个项目投资j万元最大投资回报值时,只要用到矩阵Q的第k+1行数据和对前k个项目投资j万元最大投资回报值,而与前面的数据无关,后者也只需有一个一维数组存储即可,程序中用一维数组Q存储当前项目的投资回报值,用一维数组maxreturn存储对当前项目之前的所有项目投资j万元最大投资回报值(0≤j≤n),用一维数组temp存储对到当前项目为止的所有项目投资j万元最大投资回报值(0≤j≤n)。为了输出投资方案,程序中使用了一个二维数组invest,invest[k,j]记录了对前k个项目投资j万元获得最大投资回报时投资在第k个项目上的资金数。
[程序清单]
program ex8_3(input,output);
const maxm=100; maxn=100;
type arraytype=array [0..maxn] of real;
var i,j,k,m,n,rest:integer;
q,maxreturn,temp:arraytype;
invest:array[1..maxm,0..maxn] of integer;
result:array[1..maxm] of integer;
fname:string;
f:text;
begin
write('Input the name of datafile:');
readln(fname);
assign(f,fname);
reset(f);
readln(f,m,n);
for j:=0 to n do read(f,q[j]);
readln(f);
for i:=1 to m do
for j:=0 to n do invest[i,j]:=0;
maxreturn:=q;
for j:=0 to n do invest[1,j]:=j;
for k:=2 to m do
begin
temp:=maxreturn;
for j:=0 to n do invest[k,j]:=0;
for j:=0 to n do read(f,q[j]);
readln(f);
for j:=0 to n do
for i:=0 to j do
if maxreturn[j-i]+q[i]>temp[j] then
begin
temp[j]:=maxreturn[j-i]+q[i];
invest[k,j]:=i
end;
maxreturn:=temp
end;
close(f);
rest:=n;
for i:=m downto 1 do
begin
result[i]:=invest[i,rest];
rest:=rest-result[i]
end;
for i:=1 to m do write(result[i],' ');
writeln(maxreturn[n]:0:2)
end.
[例 8-4] 花店橱窗布置问题(FLOWER)
(1)问题描述
假设你想以最美观的方式布置花店的橱窗。现在你有F束不同品种的花束,同时你也有至少同样数量的花瓶被按顺序摆成一行。这些花瓶的位置固定于架子上,并从1至V顺序编号,V是花瓶的数目,从左至右排列,则最左边的是花瓶1,最右边的是花瓶V。花束可以移动,并且每束花用1至F间的整数唯一标识。标识花束的整数决定了花束在花瓶中的顺序,如果I<J,则令花束I必须放在花束J左边的花瓶中。
例如,假设一束杜鹃花的标识数为1,一束秋海棠的标识数为2,一束康乃馨的标识数为3,所有的花束在放入花瓶时必须保持其标识数的顺序,即:杜鹃花必须放在秋海棠左边的花瓶中,秋海棠必须放在康乃馨左边的花瓶中。如果花瓶的数目大于花束的数目。则多余的花瓶必须空置,且每个花瓶中只能放一束花。
每一个花瓶都具有各自的特点。因此,当各个花瓶中放入不同的花束时,会产生不同的美学效果,并以美学值(一个整数)来表示,空置花瓶的美学值为零。
在上述例子中,花瓶与花束的不同搭配所具有的美学值,如下表所示。
| 花 瓶 | |||||
1 | 2 | 3 | 4 | 5 | ||
花 束 | 1 (杜鹃花) | 7 | 23 | -5 | -24 | 16 |
2 (秋海棠) | 5 | 21 | -4 | 10 | 23 | |
3 (康乃馨) | -21 | 5 | -4 | -20 | 20 |
例如,根据上表,杜鹃花放在花瓶2中,会显得非常好看;但若放在花瓶4中则显得十分难看。
为取得最佳美学效果,你必须在保持花束顺序的前提下,使花束的摆放取得最大的美学值。如果有不止一种的摆放方式具有最大的美学值,则其中任何一直摆放方式都可以接受,但你只要输出任意一种摆放方式。
(2)假设条件
l 1≤F≤100,其中F为花束的数量,花束编号从1至F。
l F≤V≤100,其中V是花瓶的数量。
l -50≤Aij≤50,其中Aij是花束i在花瓶j中的美学值。
(3)输入
输入文件是正文文件(text file),文件名是flower.inp。
l 第一行包含两个数:F,V。
l 随后的F行中,每行包含V个整数,Aij 即为输入文件中第(i+1 )行中的第j个数。
(4)输出
输出文件必须是名为flower.out的正文文件,文件应包含两行:
第一行是程序所产生摆放方式的美学值。
第二行必须用F个数表示摆放方式,即该行的第K个数表示花束K所在的花瓶的编号。
(5)例子
flower.inp:
3 5 7 23 –5 –24 16 5 21 -4 10 23 -21 5 -4 -20 20 |
flower.out:
53 2 4 5 |
(6)评分
程序必须在2秒中内运行完毕。
在每个测试点中,完全正确者才能得分。
[算法设计] flower一题是IOI99第一天第一题,该题如用组合的方法处理,将会造成超时。正确的方法是用动态规划,考虑角度为一束一束地增加花束,假设用b(i,j)表示1~i束花放在1到j之间的花瓶中的最大美学值,其中i<=j ,则b(i,j)=max(b[i-1,k-1]+A[i,k]),其中i<=k<=j,A(i,k)的含义参见题目。输出结果时,显然使得b[F,k]取得总的最大美观值的第一个k值就是第F束花应该摆放的花瓶位置,将总的最大美观值减去A[i,k]的值即得到前k-1束花放在前k-1个瓶中的最大美观值,依次使用同样的方法就可求出每一束花应该摆放的花瓶号。由于这一过程是倒推出来的,所以程序中用递归程序来实现。
[程序清单]
program ex8_4(input,output);
const max=100;
var f,v,i,j,k,cmax,current,max_val:integer;
table,val:array[1..max,1..max] of integer;
fname:string;
fin:text;
procedure print(current,max_val:integer);
var i:integer;
begin
if current>0 then
begin
i:=current;
while val[current,i]<>max_val do i:=i+1;
print(current-1,max_val-table[current,i]);
write(i,' ')
end
end;
begin
write('Input the filename of test data:');
readln(fname);
assign(fin,fname);
reset(fin);
readln(fin,f,v);
for i:=1 to f do
begin
for j:=1 to v do read(fin,table[i,j]);
readln(fin);
end;
close(fin);
max_val:=-maxint;
for i:=1 to v do
if max_val<table[1,i]
then begin val[1,i]:=table[1,i];max_val:=table[1,i] end
else val[1,i]:=table[1,i];
for i:=2 to f do
for j:=i to v-f+i do
begin
max_val:=-maxint;
for k:=i-1 to j-1 do
begin
cmax:=-maxint;
for current:=k+1 to j do
if table[i,current]>cmax then cmax:=table[i,current];
if cmax+val[i-1,k]>max_val then max_val:=cmax+val[i-1,k]
end;
val[i,j]:=max_val
end;
max_val:=-maxint;
for i:=f to v do
if val[f,i]>max_val then max_val:=val[f,i];
writeln(max_val);
print(f,max_val);
writeln
end.
[运行结果]
参见flower子目录下的标准答案和测试数据。
3 5
7 23 –5 –24 16
5 21 -4 10 23
-21 5 -4 -20 20
53
2 4 5