常用算法设计方法(八) ---动态规划法

经常会遇到复杂问题不能简单地分解成几个子问题,而会分解出一系列的子问题。简单地采用把大问题分解成子问题,并综合子问题的解导出大问题的解的方法,问题求解耗时会按问题规模呈幂级数增加。
   为了节约重复求相同子问题的时间,引入一个数组,不管它们是否对最终解有用,把所有子问题的解存于该数组中,这就是动态规划法所采用的基本方法。以下先用实例说明动态规划方法的使用。
【问题】   求两字符序列的最长公共字符子序列
问题描述:字符序列的子序列是指从给定字符序列中随意地(不一定连续)去掉若干个字符(可能一个也不去掉)后所形成的字符序列。令给定的字符序列X=“x0,x1,…,xm-1”,序列Y=“y0,y1,…,yk-1”是X的子序列,存在X的一个严格递增下标序列<i0,i1,…,ik-1>,使得对所有的j=0,1,…,k-1,有xij=yj。例如,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[j]为序列“a0,a1,…,ai-2”和“b0,b1,…,bj-1”的最长公共子序列的长度,计算c[j]可递归地表述如下:
(1)c[j]=0                     如果i=0或j=0;
(2)c[j]= c[i-1][j-1]+1            如果I,j>0,且a[i-1]=b[j-1];
(3)c[j]=max(c[j-1],c[i-1][j])   如果I,j>0,且a[i-1]!=b[j-1]。
按此算式可写出计算两个序列的最长公共子序列的长度函数。由于c[j]的产生仅依赖于c[i-1][j-1]、c[i-1][j]和c[j-1],故可以从c[m][n]开始,跟踪c[j]的产生过程,逆向构造出最长公共子序列。细节见程序。
# include <stdio.h>
# include <string.h>
# define    N   100
char  a[N],b[N],str[N];

int lcs_len(char  *a,  char  *b, int c[ ][ N])
{   int  m=strlen(a),   n=strlen(b), i,j;
   for (i=0;i<=m;i++)      c[0]=0;
   for (i=0;i<=n;i++)      c[0]=0;
   for (i=1;i<=m;i++)   
      for (j=1;j<=m;j++)
         if (a[i-1]==b[j-1])
            c[j]=c[i-1][j-1]+1;
         else   if (c[i-1][j]>=c[j-1])
               c[j]=c[i-1][j];
            else
               c[j]=c[j-1];
   return c[m][n];
}

char *buile_lcs(char s[ ],char *a, char *b)
{   int  k, i=strlen(a),  j=strlen(b);
   k=lcs_len(a,b,c);
   s[k]=’/0’;
   while (k>0)
      if (c[j]==c[i-1][j])   i--;
      else   if (c[j]==c[j-1])   j--;
         else   {   s[--k]=a[i-1];
               i--;   j--;
            }
   return s;
}

void main()
{   printf (“Enter  two  string(<%d)!/n”,N);
   scanf(“%s%s”,a,b);
   printf(“LCS=%s/n”,build_lcs(str,a,b));
}

 

1、动态规划的适用条件
任何思想方法都有一定的局限性,超出了特定条件,它就失去了作用。同样,动态规划也并不是万能的。适用动态规划的问题必须满足最优化原理和无后效性。
(1)最优化原理(最优子结构性质)
最优化原理可这样阐述:一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简而言之,一个最优化策略的子策略总是最优的。一个问题满足最优化原理又称其具有最优子结构性质。

图2
例如图2中,若路线I和J是A到C的最优路径,则根据最优化原理,路线J必是从B到C的最优路线。这可用反证法证明:假设有另一路径J’是B到C的最优路径,则A到C的路线取I和J’比I和J更优,矛盾。从而证明J’必是B到C的最优路径。
最优化原理是动态规划的基础,任何问题,如果失去了最优化原理的支持,就不可能用动态规划方法计算。根据最优化原理导出的动态规划基本方程是解决一切动态规划问题的基本方法。
(2)无后向性
将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。
(3)子问题的重叠性
动态规划算法的关键在于解决冗余,这是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其它的算法。选择动态规划算法是因为动态规划算法在空间上可以承受,而搜索算法在时间上却无法承受,所以我们舍空间而取时间。
所以,能够用动态规划解决的问题还有一个显著特征:子问题的重叠性。这个性质并不是动态规划适用的必要条件,但是如果该性质无法满足,动态规划算法同其他算法相比就不具备优势。
2、动态规划的基本思想
前文主要介绍了动态规划的一些理论依据,我们将前文所说的具有明显的阶段划分和状态转移方程的动态规划称为标准动态规划,这种标准动态规划是在研究多阶段决策问题时推导出来的,具有严格的数学形式,适合用于理论上的分析。在实际应用中,许多问题的阶段划分并不明显,这时如果刻意地划分阶段法反而麻烦。一般来说,只要该问题可以划分成规模更小的子问题,并且原问题的最优解中包含了子问题的最优解(即满足最优子化原理),则可以考虑用动态规划解决。
动态规划的实质是分治思想和解决冗余,因此,动态规划是一种将问题实例分解为更小的、相似的子问题,并存储子问题的解而避免计算重复的子问题,以解决最优化问题的算法策略。
由此可知,动态规划法与分治法和贪心法类似,它们都是将问题实例归纳为更小的、相似的子问题,并通过求解子问题产生一个全局最优解。其中贪心法的当前选择可能要依赖已经作出的所有选择,但不依赖于有待于做出的选择和子问题。因此贪心法自顶向下,一步一步地作出贪心选择;而分治法中的各个子问题是独立的(即不包含公共的子子问题),因此一旦递归地求出各子问题的解后,便可自下而上地将子问题的解合并成问题的解。但不足的是,如果当前选择可能要依赖子问题的解时,则难以通过局部的贪心策略达到全局最优解;如果各子问题是不独立的,则分治法要做许多不必要的 工作 ,重复地解公共的子问题。
解决上述问题的办法是利用动态规划。该方法主要应用于最优化问题,这类问题会有多种可能的解,每个解都有一个值,而动态规划找出其中最优(最大或最小)值的解。若存在若干个取最优值的解的话,它只取其中的一个。在求解过程中,该方法也是通过求解局部子问题的解达到全局最优解,但与分治法和贪心法不同的是,动态规划允许这些子问题不独立,(亦即各子问题可包含公共的子子问题)也允许其通过自身子问题的解作出选择,该方法对每一个子问题只解一次,并将结果保存起来,避免每次碰到时都要重复计算。
因此,动态规划法所针对的问题有一个显著的特征,即它所对应的子问题树中的子问题呈现大量的重复。动态规划法的关键就在于,对于重复出现的子问题,只在第一次遇到时加以求解,并把答案保存起来,让以后再遇到时直接引用,不必重新求解。
3、动态规划算法的基本步骤
设计一个标准的动态规划算法,通常可按以下几个步骤进行:
(1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。注意这若干个阶段一定要是有序的或者是可排序的(即无后向性),否则问题就无法用动态规划求解。
(2)选择状态:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。
(3)确定决策并写出状态转移方程:之所以把这两步放在一起,是因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以,如果我们确定了决策,状态转移方程也就写出来了。但事实上,我们常常是反过来做,根据相邻两段的各状态之间的关系来确定决策。
(4)写出规划方程(包括边界条件):动态规划的基本方程是规划方程的通用形式化表达式。
 
一般说来,只要阶段、状态、决策和状态转移确定了,这一步还是比较简单的。动态规划的主要难点在于理论上的设计,一旦设计完成,实现部分就会非常简单。根据动态规划的基本方程可以直接递归计算最优值,但是一般将其改为递推计算,实现的大体上的框架如下:
标准动态规划的基本框架
1.  对fn+1(xn+1)初始化;    {边界条件}
for k:=n downto 1 do
for 每一个xk∈Xk do
for 每一个uk∈Uk(xk) do
begin
5.            fk(xk):=一个极值;                 {∞或-∞}
6.            xk+1:=Tk(xk,uk);                  {状态转移方程}
7.            t:=φ(fk+1(xk+1),vk(xk,uk));       {基本方程(9)式}
if  t比fk(xk)更优 then fk(xk):=t; {计算fk(xk)的最优值}
end;
9.  t:=一个极值;                               {∞或-∞}
for 每一个x1∈X1 do
11.     if f1(x1)比t更优 then t:=f1(x1);       {按照10式求出最优指标}
12. 输出t;
但是,实际应用当中经常不显式地按照上面步骤设计动态规划,而是按以下几个步骤进行:
(1)分析最优解的性质,并刻划其结构特征。
(2)递归地定义最优值。
(3)以自底向上的方式或自顶向下的记忆化方法(备忘录法)计算出最优值。
(4)根据计算最优值时得到的信息,构造一个最优解。
步骤(1)~(3)是动态规划算法的基本步骤。在只需要求出最优值的情形,步骤(4)可以省略,若需要求出问题的一个最优解,则必须执行步骤(4)。此时,在步骤(3)中计算最优值时,通常需记录更多的信息,以便在步骤(4)中,根据所记录的信息,快速地构造出一个最优解。

【问题】   凸多边形的最优三角剖分问题
问题描述:多边形是平面上一条分段线性的闭曲线。也就是说,多边形是由一系列首尾相接的直线段组成的。组成多边形的各直线段称为该多边形的边。多边形相接两条边的连接点称为多边形的顶点。若多边形的边之间除了连接顶点外没有别的公共点,则称该多边形为简单多边形。一个简单多边形将平面分为3个部分:被包围在多边形内的所有点构成了多边形的内部;多边形本身构成多边形的边界;而平面上其余的点构成了多边形的外部。当一个简单多边形及其内部构成一个闭凸集时,称该简单多边形为凸多边形。也就是说凸多边形边界上或内部的任意两点所连成的直线段上所有的点均在该凸多边形的内部或边界上。
通常,用多边形顶点的逆时针序列来表示一个凸多边形,即P=<v0,v1,…,vn-1>表示具有n条边v0v1,v1v2,…,vn-1vn的一个凸多边形,其中,约定v0=vn 。
若vi与vj是多边形上不相邻的两个顶点,则线段vivj称为多边形的一条弦。弦将多边形分割成凸的两个子多边形<vi,vi+1,…,vj>和<vj,vj+1,…,vi>。多边形的三角剖分是一个将多边形分割成互不重迭的三角形的弦的集合T。图1是一个凸多边形的两个不同的三角剖分。
 
a)   (b)
图1 一个凸多边形的2个不同的三角剖分
在凸多边形P的一个三角剖分T中,各弦互不相交且弦数已达到最大,即P的任一不在T中的弦必与T中某一弦相交。在一个有n个顶点的凸多边形的三角刮分中,恰好有n-3条弦和n-2个三角形。
凸多边形最优三角剖分的问题是:给定一个凸多边形P=<v0,v1,…,vn-1>以及定义在由多边形的边和弦组成的三角形上的权函数ω。要求确定该凸多边形的一个三角剖分,使得该三角剖分对应的权即剖分中诸三角形上的权之和为最小。
可以定义三角形上各种各样的权函数ω。例如:定义ω(△vivjvk)=| vivj |+| vivk |+| vkvj |,其中,| vivj |是点vi到vj的欧氏距离。相应于此权函数的最优三角剖分即为最小弦长三角剖分。
(1)最优子结构性质
凸多边形的最优三角剖分问题有最优子结构性质。事实上,若凸(n+1)边形P=<v0,v1 ,…,vn>的一个最优三角剖分T包含三角形v0vkvn,1≤k≤n-1,则T的权为3个部分权的和,即三角形v0vkvn的权,子多边形<v0,v1,…,vk>的权和<vk,vk+1,…,vn>的权之和。可以断言由T所确定的这两个子多边形的三角剖分也是最优的,因为若有<v0,v1,…,vk>或<vk,vk+1,…,vn>的更小权的三角剖分,将会导致T不是最优三角剖分的矛盾。
(2)最优三角剖分对应的权的递归结构
首先,定义t[i,j](1≤i<j≤n)为凸子多边形<vi-1,vi,…,vj>的最优三角剖分所对应的权值,即最优值。为方便起见,设退化的多边形<vi-1,vi>具有权值0。据此定义,要计算的凸(n+1)边多边形P对应的权的最优值为t[1,n]。
t[i,j]的值可以利用最优子结构性质递归地计算。由于退化的2顶点多边形的权值为0,所以t[i,i]=0,i=1,2,…,n 。当j一i≥1时,子多边形<vi-1,vi,…,vj>至少有3个顶点。由最优于结构性质,t[i,j]的值应为t[i,k]的值加上t[k+1,j]的值,再加上△vi-1vkvj的权值,并在i≤k≤j-1的范围内取最小。由此,t[i,j]可递归地定义为:

(3)计算最优值
下面描述的计算凸(n+1)边形P=<v0,v1,…,vn>的三角剖分最优权值的动态规划算法MINIMUM_WEIGHT,输入是凸多边形P=<v0,v1,…,vn>的权函数ω,输出是最优值t[i,j]和使得t[i,k]+t[k+1,j]+ω(△vi-1vkvj)达到最优的位置(k=)s[i,j],1≤i≤j≤n 。
Procedure  MINIMUM_WEIGHT(P,w);
Begin
n=length[p]-1;
for i=1 to n do t[i,i]:=0;
for ll=2 to n do
for i=1 to n-ll+1 do
begin
j=i+ll-1;
t[i,j]=∞;
for k=i to j-1 do
begin
   q=t[i,k]+t[k+1,j]+ω(△vi-1vkvj);
if q<t[i,j] then
begin
t[i,j]=q;
s[i,j]=k;
end;
            end;
end;
   return(t,s);
end;
算法MINIMUM_WEIGHT_占用θ(n2)空间,耗时θ(n3)。
(4)构造最优三角剖分
如我们所看到的,对于任意的1≤i≤j≤n ,算法MINIMUM_WEIGHT在计算每一个子多边形<vi-1,vi,…,vj>的最优三角剖分所对应的权值t[i,j]的同时,还在s[i,j]中记录了此最优三角剖分中与边(或弦)vi-1vj构成的三角形的第三个顶点的位置。因此,利用最优子结构性质并借助于s[i,j],1≤i≤j≤n ,凸(n+l)边形P=<v0,v1,…,vn>的最优三角剖分可容易地在Ο(n)时间内构造出来。
 
习题:
1、汽车加油问题:
设有路程长度为L公里的公路上,分布着m个加油站,它们的位置分别为p (i=1,2,……,m),而汽车油箱加满油后(油箱最多可以加油k升),可以行驶n公里。设计一个方案,使汽车经过此公路的加油次数尽量少(汽车出发时是加满油的)。
2、最短路径:
设有一个 网络 ,要求从某个顶点出发到其他顶点的最短路径
3、跳马问题:
在8*8方格的棋盘上,从任意指定的方格出发,为马寻找一条走遍棋盘每一格并且只经过一次的一条路径。
4、二叉树的遍历
5、背包问题
6、用分治法实现两个大整数相乘
7、设x1,x2,…,xn是直线上的n个点,若要用单位长度的闭区间去覆盖这n个点,至少需要多少个这样的单位闭区间?
8、用关系“<”和“=”将3个数A、B和C依次排列时,有13种不同的序关系:
A=B=C,A=B<C,A<B=C,A<B<C,A<C<B,A=C<B,B<A=C,
B<A<C,B<C<A,B=C<A,C<A=B,C<A<B,C<A<B。
若要将n个数依序进行排列,试设计一个动态规划算法,计算出有多少钟不同的序关系。
9、有一种单人玩的游戏:设有n(2<=n<=200)堆薄片,各堆顺序用0至 n-1编号,极端情况,有的堆可能没有薄片。在游戏过程中,一次移动只能取某堆上的若干张薄片,移到该堆的相邻堆上。如指定
I堆k张 k 移到I-1(I>0)堆,和将k 张薄片移至I+1(I<n-1)堆。所以当有两个堆与 I 堆相邻 时,I堆原先至少有2k 张薄片;只有一个堆与 I 堆相邻 时, I 堆原先至少有k张薄片。
   游戏的目标是对给定的堆数,和各堆上的薄片数,按上述规则移动薄片,最终使 各堆的薄片数相同。为了使移动次数较少些,移动哪一堆薄片,和移多少薄片先作以下估算:

ci:I堆的薄片数(0<=I<n,0<=ci<=200);
v:每堆 的平均薄片数;
ai:I堆的相邻堆可以从I堆得到的薄片数。
估算方法如下:
v=c0+a1-a0          a1=v+a0-c0
v=c1+a0+a2-2a1              a2=v+2a1-a0-c1
……..                                ……….
V=ci+ai-1+ai+1-2aI        ai+1=v+2ai-ai-1-ci
这里并不希望准确地求出A0 至an-1,而是作以下处理:若令  a0 为0,能按上述算式计算出 A1至 an-1。程序找出 a 中的最小值,并让全部a值减去这最小值,使每堆移去的薄片数大于等于0。
实际操作采用以下贪心策略:
(1)每次从第一堆出发顺序搜索每一堆,若发现可从 I堆移走薄片,就完成一次移动。即, I堆的相邻堆从 I堆取走  ai片薄片。可从I  堆移薄片到相邻堆取于 I堆薄片数:若I 堆是处于两端位置( I=0    I=n-1), 要求 ci>=ai ;若 I堆是中间堆,则要求ci>=2ai。
(2)因在ai>0的所有堆中,薄片数最多的堆 在平分过程中被它的相邻堆取走的薄片数也最多。在用策略(1)搜索移动时,当发生没有满足条件(1)的可移走薄片的堆时,采用本策略,让在ai>0的所有堆中,薄片数最多的堆被它的相邻堆取走它的全部薄片。

你可能感兴趣的:(常用算法设计方法(八) ---动态规划法)