最优化原理是动态规划的基础。
任何一个问题,如果失去了这个最优化原理的支持,就不可能用动态规划方法计算。
能采用动态规划求解的问题都需要满足一定的条件:
(1)问题中的状态必须满足最优化原理;
(2)问题中的状态必须满足无后效性。
所谓的无后效性是指:
“下一时刻的状态只与当前状态有关,而和当前状态之前的状态无关,当前的状态是对以往决策的总结”。
问题求解模式
动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。
这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。
如图所示。
动态规划的设计都有着一定的模式,一般要经历以下几个步骤。
初始状态→│决策1│→│决策2│→…→│决策n│→结束状态
图1动态规划决策过程示意图
(1)划分阶段:
按照问题的时间或空间特征,把问题分为若干个阶段。
在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。
(2)确定状态和状态变量:
将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。
当然,状态的选择要满足无后效性。
(3)确定决策并写出状态转移方程:
因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。
所以如果确定了决策,状态转移方程也就可写出。
但事实上常常是反过来做,根据相邻两段各状态之间的关系来确定决策。
(4)寻找边界条件:
给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。
算法实现
动态规划的主要难点在于理论上的设计,也就是上面4个步骤的确定,一旦设计完成,实现部分就会非常简单。
使用动态规划求解问题,最重要的就是确定动态规划三要素:
问题的阶段,每个阶段的状态以及从前一个阶段转化到后一个阶段之间的递推关系。
递推关系必须是从次小的问题开始到较大的问题之间的转化,从这个角度来说,动态规划往往可以用递归程序来实现,不过因为递推可以充分利用前面保存的子问题的解来减少重复计算,所以对于大规模问题来说,有递归不可比拟的优势,这也是动态规划算法的核心之处。
确定了动态规划的这三要素,整个求解过程就可以用一个最优决策表来描述,最优决策表是一个二维表,其中行表示决策的阶段,列表示问题状态,表格需要填写的数据一般对应此问题的在某个阶段某个状态下的最优值(如最短路径,最长公共子序列,最大价值等),填表的过程就是根据递推关系,从1行1列开始,以行或者列优先的顺序,依次填写表格,最后根据整个表格的数据通过简单的取舍或者运算求得问题的最优解。
下面分别以求解最大化投资回报问题和最长公共子序列问题为例阐述用动态规划算法求解问题的一般思路。
1.最长公共子串问题的实现
最长公共子串问题:
一个给定序列的子序列是在该序列中删去若干元素后得到的序列。
给定两个序列X和Y,当另一序列Z既是X的子序列又是Y的子序列时,称Z是序列X和Y的公共子序列。
最长公共子串就是求给定两个序列的一个最长公共子序列。
例如,X=“ABCBDAB”,Y=“BCDB”是X的一个子序列。
问题分析:
给定两个序列A和B,称序列Z是A和B的公共子序列,是指Z同是A和B的子序列。
问题要求已知两序列A和B的最长公共子序列。
如采用列举A的所有子序列,并一一检查其是否又是B的子序列,并随时记录所发现的子序列,最终求出最长公共子序列。
这种方法因耗时太多而不可取。
考虑最长公共子序列问题如何分解成子问题,设A=“a0,a1,…,am-1”,B=“b0,b1,…,bm-1”,并Z=“z0,z1,…,zk-1”为它们的最长公共子序列。
不难证明有以下性质:
(1)如果am-1=bn-1,则zk-1=am-1=bn-1,且“z0,z1,…,zk-2”是“a0,a1,…,am-2”和“b0,b1,…,bn-2”的一个最长公共子序列;
(2)如果am-1!
=bn-1,则若zk-1!
=am-1,蕴涵“z0,z1,…,zk-1”是“a0,a1,…,am-2”和“b0,b1,…,bn-1”的一个最长公共子序列;
(3)如果am-1!
=bn-1,则若zk-1!
=bn-1,蕴涵“z0,z1,…,zk-1”是“a0,a1,…,am-1”和“b0,b1,…,bn-2”的一个最长公共子序列。
这样,在找A和B的公共子序列时,如有am-1=bn-1,则进一步解决一个子问题,找“a0,a1,…,am-2”和“b0,b1,…,bm-2”的一个最长公共子序列;如果am-1!
=bn-1,则要解决两个子问题,找出“a0,a1,…,am-2”和“b0,b1,…,bn-1”的一个最长公共子序列和找出“a0,a1,…,am-1”和“b0,b1,…,bn-2”的一个最长公共子序列,再取两者中较长者作为A和B的最长公共子序列。
为了节约重复求相同子问题的时间,引入一个数组,不管它们是否对最终解有用,把所有子问题的解存于该数组中,这就是动态规划法所采用的基本方法,具体说明如下。
定义c[i][j]为序列“a0,a1,…,ai-2”和“b0,b1,…,bj-1”的最长公共子序列的长度,计算c[i][j]可递归地表述如下:
(1)c[i][j]=0 如果i=0或j=0;
(2)c[i][j]=c[i-1][j-1]+1 如果i,j>0,且a[i-1]=b[j-1];
(3)c[i][j]=max{c[i][j-1],c[i-1][j]}如果i,j>0,且a[i-1]!
=b[j-1]。
按此算式可写出计算两个序列的最长公共子序列的长度函数。
由于c[i][j]的产生仅依赖于c[i-1][j-1]、c[i-1][j]和c[i][j-1],故可以从c[m][n]开始,跟踪c[i][j]的产生过程,逆向构造出最长公共子序列。
细节见程序。
#include
#include
#defineN100
chara[N],b[N],str[N];
intc[N][N];
intlcs_len(char*a,char*b,intc[][N])
{
intm=strlen(a),n=strlen(b),i,j;
for(i=0;i<=m;i++)
c[i][0]=0;
for(i=0;i<=n;i++)
c[0][i]=0;
for(i=1;i<=m;i++)
{
for(j=1;j<=n;j++)
{
if(a[i-1]==b[j-1])
c[i][j]=c[i-1][j-1]+1;
elseif(c[i-1][j]>=c[i][j-1])
c[i][j]=c[i-1][j];
else
c[i][j]=c[i][j-1];
}
}
returnc[m][n];
}
char*build_lcs(chars[],char*a,char*b)
{
inti=strlen(a),j=strlen(b);
intk=lcs_len(a,b,c);
s[k]='\0';
while(k>0)
{
if(c[i][j]==c[i-1][j])
i--;
elseif(c[i][j]==c[i][j-1])
j--;
else
{
s[--k]=a[i-1];
i--;j--;
}
}
returns;
}
voidmain()
{
printf("Entertwostring(length<%d):
\n",N);
scanf("%s%s",a,b);
printf("LCS=%s\n",build_lcs(str,a,b));
}
2.用动态规划实现导弹拦截
某国为了防御敌国的导弹袭击,开发出一种导弹拦截系统。
但是这种导弹拦截系统有一个缺陷:
虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。
某天,雷达捕捉到敌国的导弹来袭。
由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
输入导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数),计算这套系统最多能拦截多少导弹,并依次输出被拦截的导弹飞来时候的高度。
SAMPLEINPUT:
38920715530029917015865
SAMPLEOUTPUT:
6
38930029917015865
因为只有一套导弹拦截系统,并且这套系统除了第一发炮弹能到达任意高度外,以后的每一发炮弹都不能高于前一发炮弹的高度;所以,被拦截的导弹应该按飞来的高度组成一个非递增序列。
题目要求我们计算这套系统最多能拦截的导弹数,并依次输出被拦截导弹的高度,实际上就是要求我们在导弹依次飞来的高度序列中寻找一个最长非递增子序列。
设X={x1,x2,…,xn}为依次飞来的导弹序列,Y={y1,y2,…,yk}为问题的最优解(即X的最长非递增子序列),s为问题的状态(表示导弹拦截系统当前发送炮弹能够到达的最大高度,初值为s=∞——第一发炮弹能够到达任意的高度)。
如果y1=x1,即飞来的第一枚导弹被成功拦截。
那么,根据题意“每一发炮弹都不能高于前一发的高度”,问题的状态将由s=∞变成s≤x1(x1为第一枚导弹的高度);在当前状态下,序列Y1={y2,…,yk}也应该是序列X1={x2,…,xn}的最长非递增子序列(大家用反证法很容易证明)。
也就是说,在当前状态s≤x1下,问题的最优解Y所包含的子问题(序列X1)的解(序列Y1)也是最优的。
这就是拦截导弹问题的最优子结构性质。
设D(i)为第i枚导弹被拦截之后,这套系统最多还能拦截的导弹数(包含被拦截的第i枚)。
我们可以设想,当系统拦截了第k枚导弹xk,而xk又是序列X={x1,x2,…,xn}中的最小值,即第k枚导弹为所有飞来的导弹中高度最低的,则有D(k)=1;当系统拦截了最后一枚导弹xn,那么,系统最多也只能拦截这一枚导弹了,即D(n)=1;其它情况下,也应该有D(i)≥1。
假设系统最多能拦截的导弹数为dmax(即问题的最优值),则dmax=max(D(i))
所以,要计算问题的最优值dmax,需要分别计算出D
(1)、D
(2)、……D(n)的值,然后将它们进行比较,找出其中的最大值。
根据上面分析出来的递归方程,我们完全可以设计一个递归函数,采用自顶向下的方法计算D(i)的值。
然后,对i从1到n分别调用这个递归函数,就可以计算出D
(1)、D
(2)、……D(n)。
但这样将会有大量的子问题被重复计算。
比如在调用递归函数计算D
(1)的时候,可能需要先计算D(5)的值;之后在分别调用递归函数计算D
(2)、D(3)、D(4)的时候,都有可能需要先计算D(5)的值。
如此一来,在整个问题的求解过程中,D(5)可能会被重复计算很多次,从而造成了冗余,降低了程序的效率。
其实,通过以上分析,我们已经知道:
D(n)=1。
如果将n作为阶段对问题进行划分,根据问题的动态规划递归方程,我们可以采用自底向上的方法依次计算出D(n-1)、D(n-2)、……D
(1)的值。
这样,每个D(i)的值只计算一次,并在计算的同时把计算结果保存下来,从而避免了有些子问题被重复计算的情况发生,提高了程序的效率。
算法代码如下:
for(i=n-2;i>=0;i--){ //从后往前计算d[i]值
for(j=i+1;j if((array[j]<=array[i])&&(d[i] d[i]=d[j]+1;
}
}
}
dmax=0;
xh=1;
for(i=0;i if(d[i]>dmax){
dmax=d[i];
xh=i;
}
}
cout< cout< inttemp=xh;
for(i=xh+1;i if(d[i]==d[temp]-1){
temp=i;
cout< }
}
cout<3.最大化投资回报问题的实现
最大化投资回报问题:
某人有一定的资金用来购买不同面额的债卷,不同面额债卷的年收益是不同的,求给定资金,年限以及债卷面额、收益的情况下怎样购买才能使此人获得最大投资回报。
程序输入约定:
第一行第一列表示资金(1000的倍数)总量,第二列表示投资年限;第二行表示债卷面额总数;从第三行开始每行表示一种债卷,占用两列,前一列表示债卷面额,后一列表示其年收益,如下输入实例,
100001
2
4000400
3000250
程序实现如下,注释几乎说明了一切,所以不再另外分析。
///此数组是算法的关键存储结构,用来存储不同阶段各种债卷
///组合下对应可获取的最大利息。
intsaifa[80005];
///此函数用于计算当前债卷在不同购买额下的最优利息情况,
///注意此时的利息情况是基于上一次债卷的情况下计算得到的,
///也就是说当前利息最优是基于上一次利息最优的基础上计算出来的,
///这也正好体现了动态规划中“最优化原则”:
不管前面的策略如何,
///此后的决策必须是基于当前状态(由上一次决策产生)的最优决策。
/*
动态规划的求解过程一般都可以用一个最优决策表来描述,
对于本程序,以示例输入为例,对于第一年,其最优决策表如下:
0123 4 5 6 7 8 9 10(*1000) --
(1)
0000 400400400400800800800 --
(2)
000250400400500650800900900 --(3)
(1)--表示首先选利息为400的债卷在对应资金下的最优利息。
(2)--表示可用来购买债卷的资金。
(3)--表示在已有状态下再选择利息为300的债卷在对应资金下的最优利息。
注意上面表格,在求购买利息为300的债卷获得的最优收益的时候,
参考了以前的最优状态,以3行8列的650为例,7(*1000)可以
在以前购买了0张4000的债卷的基础上再2张3000的,也可以在以前购
买了1张4000的基础上再买1张3000,经比较取其收益大的,这就是典
型的动态规划中的当前最优状态计算。
本程序中把上面的最优决策二维表用一个一维数组表示,值得借鉴。
*/
voidadd(inta,intb)
{cout< for(inti=0;i<=80000;i++)
{
if(i+a>80000)
{
break;
}
if(saifa[i]+b>saifa[i+a])//累计同时购买多种债卷时的利息
{
saifa[i+a]=saifa[i]+b;
}
if(i<200)//fordebug
cout<
}
cout< }
intmain(void)
{
intn,d,money,year,pay,bond;
intii,i;
sca