dp算法思想及运用实践例题

来源:互联网 发布:黑手党3低配优化 编辑:程序博客网 时间:2024/05/17 23:40

最优化原理 

    1951年美国数学家R.Bellman等人,根据一类多阶段问题的特点,把多阶段决策问题变换为一系列互相联系的单阶段问题,然后逐个加以解决。一些静态模型,只要人为地引进“时间”因素,分成时段,就可以转化成多阶段的动态模型,用动态规划方法去处理。与此同时,他提出了解决这类问题的“最优化原理”(Principle of optimality):

    上述程序实现方法同样适合于背包问题,最优库存问题等,只是针对具体情况,最优决策表的表示和生成会有所不同。

    “一个过程的最优决策具有这样的性质:即无论其初始状态和初始决策如何,其今后诸策略对以第一个决策所形成的状态作为初始状态的过程而言,必须构成最优策略”。简言之,一个最优策略的子策略,对于它的初态和终态而言也必是最优的。

    这个“最优化原理”如果用数学化一点的语言来描述的话,就是:假设为了解决某一优化问题,需要依次作出n个决策D1D2,…,Dn,如若这个决策序列是最优的,对于任何一个整数k1 < k < n,不论前面k个决策是怎样的,以后的最优决策只取决于由前面决策所确定的当前状态,即以后的决策Dk+1Dk+2,…,Dn也是最优的。

最优化原理是动态规划的基础。任何一个问题,如果失去了这个最优化原理的支持,就不可能用动态规划方法计算。能采用动态规划求解的问题都需要满足一定的条件:

1)问题中的状态必须满足最优化原理;

2)问题中的状态必须满足无后效性。

 所谓的无后效性是指:“下一时刻的状态只与当前状态有关,而和当前状态之前的状态无关,当前的状态是对以往决策的总结”。

 问题求解模式

 动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。如图所示。动态规划的设计都有着一定的模式,一般要经历以下几个步骤:

 初始状态→│决策1│→│决策2│→…→│决策n│→结束状态 

(1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。

2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。

3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两段各状态之间的关系来确定决策。

4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。

算法实现 

动态规划的主要难点在于理论上的设计,也就是上面4个步骤的确定,一旦设计完成,实现部分就会非常简单。使用动态规划求解问题,最重要的就是确定动态规划三要素:问题的阶段,每个阶段的状态以及从前一个阶段转化到后一个阶段之间的递推关系。递推关系必须是从次小的问题开始到较大的问题之间的转化,从这个角度来说,动态规划往往可以用递归程序来实现,不过因为递推可以充分利用前面保存的子问题的解来减少重复计算,所以对于大规模问题来说,有递归不可比拟的优势,这也是动态规划算法的核心之处。

动态规划算法将问题的解决方案视为一系列决策的结果,与贪婪算法不同的是,在贪婪算法中,每采用一次贪婪准则,便做出一个不可撤回的决策;而在动态规划算法中,还要考察每个最优决策序列中是否包含一个最优决策子序列,即问题是否具有最优子结构性质。

动态规划算法的有效性依赖于待求解问题本身具有的两个重要性质:最优子结构性质和子问题重叠性质。

   (1)最优子结构性质。如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。

   (2)子问题重叠性质。子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的解题效率。

当我们已经确定待解决的问题需要用动态规划算法求解时,通常可以按照以下步骤设计动态规划算法: 

   (1)分析问题的最优解,找出最优解的性质,并刻画其结构特征;

   (2)递归地定义最优值; 

   (3)采用自底向上的方式计算问题的最优值;

   (4)根据计算最优值时得到的信息,构造最优解。

    13步是动态规划算法解决问题的基本步骤,在只需要计算最优值的问题中,完成这三个基本步骤就可以了。如果问题需要构造最优解,还要执行第4步;此时,在第3步通常需要记录更多的信息,以便在步骤4中,有足够的信息快速地构造出最优解。·    

〖案例1〗拦截导弹 

描述:  

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。

输入;

    输入数据为导弹依次飞来的高度,所有高度值均为不大于30000

的正整数。

输出: 

输出只有一行是这套系统最多能拦截的导弹数。

样例输入:

389 207 155 300 299 170 158 65 

样例输出:

解题思路:

 因为只有一套导弹拦截系统,并且这套系统除了第一发炮弹能到达任意高度外,以后的每一发炮弹都不能高于前一发炮弹的高度;所以,被拦截的导弹应该按飞来的高度组成一个非递增序列。题目要求我们计算这套系统最多能拦截的导弹数,并依次输出被拦截导弹的高度,实际上就是要求我们在导弹依次飞来的高度序列中寻找一个最长非递增子序列。

 设X={x1,x2,,xn}为依次飞来的导弹序列,Y={y1,y2,,yk}为问题的最优解(即X的最长非递增子序列),s为问题的状态(表示导弹拦截系统当前发送炮弹能够到达的最大高度,初值为s=∞——第一发炮弹能够到达任意的高度)。如果y1=x1,即飞来的第一枚导弹被成功拦截。那么,根据题意“每一发炮弹都不能高于前一发的高度”,问题的状态将由s=∞变成sx1x1为第一枚导弹的高度);在当前状态下,序列Y1={y2,,yk}也应该是序列X1={x2,,xn}的最长非递增子序列(大家用反证法很容易证明)。也就是说,在当前状态sx1下,问题的最优解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。根据以上分析,可归纳出问题的动态规划递归方程为:

            --

            |                1                    i=n;

       D(i) =|           max

            |  X(j)<=X(i)   and   i<j<=n {D(j)+1}    i<n;

            --

假设系统最多能拦截的导弹数为dmax(即问题的最优值),则  dmax=

  max={D(i)}i为被系统拦截的第一枚导弹的顺序号)  

 1<=i<=n

 所以,要计算问题的最优值dmax,需要分别计算出D1)、D2)、„„Dn)的值,然后将它们进行比较,找出其中的最大值。根据上面分析出来的递归方程,我们完全可以设计一个递归函数,采用自顶向下的方法计算Di)的值。然后,对i1n分别调用这个递归函数,就可以计算出D1)、D2)、„„Dn)。但这样将会有大量的子问题被重复计算。比如在调用递归函数计算D1)的时候,可能需要先计算D5)的值;之后在分别调用递归函数计算D2)、D3)、D4)的时候,都有可能需要先计算D5)的值。如此一来,在整个问题的求解过程中,D5)可能会被重复计算很多次,从而造成了冗余,降低了程序的效率。

其实,通过以上分析,我们已经知道:Dn=1。如果将n作为阶段对问题进行划分,根据问题的动态规划递归方程,我们可以采用自底向上的方法依次计算出Dn-1)、Dn-2)、„„D1)的值。这样,每个Di)的值只计算一次,并在计算的同时把计算结果保存下来,从而避免了有些子问题被重复计算的情况发生,提高了程序的效率。

int main(){

   int h[2000],d[2000],count,c;//h表示高度值,d表示最优值,c是能拦截得最多导弹数

   count=0; 

   whilescanf"%d",h+count++!=EOF; //输入高度

   d[0]=h[0]; 

   c=1;

   fori=1;i<count;i++{               //用动态规划计算所有最优值 

      forj=c-1;j>=0;j--{

            ifh[i]<=d[j]) 

            break; 

       } d[j+1]=h[i]; 

      ifj==c-1c++; 

    } 

 printf"%d\n",c;

 } 

                       〖案例2〗公共子序列

描述:

    子序列是给定序列的部分(或者没有)元素组成。给定的序列X = <x1, x2, ..., xm >,另一个序列Z= <z1, z2, ..., zk>X的子序列,如果存在严格递增X的序号序列< i1, i2, ..., ik>,对于所有的j = 1,2,...,k,Xik=Zk。例如Z = < a, b, f, c >X = < a, b, c, f, b, c >的子序列,对应在X中的序号为< 1, 2, 4, 6 >。给定两个序列XY,找到XY的最长的公共子序列的长度。 

输入:

每组测试数据包含两个字符串表示给定的序列,中间用空格隔开。 

输出:

对于每个测试数据,输出最长的公共子序列的长度。

样例输入:  

abcfbc          abfcab 

programming     contest  

abcd            mnp 

样例输出:

解题思路:

动态规划求解

X0 = Ø 

    Xi ={x1,x2,,xi},1im

    Xm = X 

    Y0 = Ø 

Yi ={y1,y2,,yi},1i

    Yn = Y 

LCS(Xi,Yj)表示(Xi,Yj)的最长公共子序列长度,则有: 

                                         --

î                                      |   LCS(Xm-1,Yn-1)U{Z}  Xm=Yn=Z;

            LCS(Xm,Yn) = |

                                      |   max{LCS(Xm-1,Yn),LCS(Xm,Yn-1)},否则

                                      --

一般有:                       --

î                                   |   LCS(Xi-1,Yj-1)U{Z}  Xi=Yj=Z;

            LCS(Xi,Yj) =  |

                                   |   max{LCS(Xi-1,Yj),LCS(Xi,Yj-1)},否则

                                      --

含义:  

 ① 若xm=yn=z(xi=yj=z)xmyn必包含在LCS(Xm,Yn)中。则,     LCS(Xm,Yn) = LCS(Xm-1,Yn-1){z}

 ② 若xmyn,则xmyn中至少有一个不会包含在LCS(Xm,Yn)中,则,

 LCS(Xm,Yn)等于LCS(Xm-1,Yn)LCS(Xm,Yn-1)中长的一个 

 ③ 若i=0j=0,则其中一个(XmYn)为空集合,此时LCS(Xm,Yn)= Ø递推过程:为求LCS(Xi,Yj),需要首先得到LCS(Xi-1,Yj-1)LCS(Xi-1,Yj)LCS(Xi,Yj-1)的值。故递推过程如下: 

    从i=0j=0算起,对所有im的每个值,求j=1n的所有可能的LCS(Xi,Yj)例:令X={a,b,c,f,b,c}, Y={a,b,f,c,a,b } 

LCS

 

  Y

   a

  b

   f

   c

   a

  b

 

     j

i

  0

   1

  2

  3

   4

  5

  6

  X

  0

  0

   0

  0

  0

  0

  0

  0

  a

  1

  0

1

1

1

1

1

1

  b

  2

0

1

2

2

2

2

2

  c

  3

0

1

2

2

3

3

3

  f

  4

0

1

2

3

3

3

3

  b

  5

0

1

2

3

3

3

4

  c

  6

0

1

2

3

4

4

4

    |LCSX,Y| = 4(表格右下脚的值)  

    反向推导,求出LCSX,Y)= {a,b,c,b}/{a,b,f,c} 

整个求解过程就可以用填写一个最长公共子序列长度值的二维表来描述,表中第(i,j)处的值就是XiYj的最长公共子序列长度值。填表的过程就是根据递推关系,从11列开始,以行或者列优先的顺序,依次填写表格,最后根据整个表格的数据通过简单的取舍求得最长公共子序列长度。

    二维表需要的存储空间为O(m*n),在需要处理的字符串较长时,需要的空间比较大。注意到在填表过程中,填写第i行(列)时,

前面i-2行(列)的数据不参与运算,为了节省存储空间,我们使用滚动数组:只保留当前填写的行(列)及前一行(列)的值,这样只需要两个一维数组即可,当前行填写完毕,向后滚动.

 int main(){

    char s1[1000],s2[1000];     //输入两个字符串序列

int  r[1000],s[1000];      //DP记录状态转移结果,采用滚动数组节省空间         

int  l1,l2,i,j;              //l1,l2记录两个字符串的长度

whilescanf"%s%s",s1,s2!=EOF){

    l1=strlens1

    l2=strlens2;

    fori=0;i<=l2;++ir[i]=0; 

    fori=0;i<l1;++i){

        forj=0;j<l2;++j{

            ifs1[i]==s2[j]

                 s[j+1]=r[j]+1; //如果序列对应字符相同 

            else s[j+1]=r[j+1]>s[j]?r[j+1]:s[j];//如果序列对应字符不同,取大的那一个

        forj=1;j<=l2;++jr[j]=s[j];

        }    //数组往后滚动 

  printf"%d\n",r[l2];  //最大的字符串长度值是最后的元素值

       } 

}  

                 〖案例3Uxuhul的表决

     Uxuhul印第安人是世界上最古老的文明之一。在中美洲丛林中,大约从公元前3200年的黄金时代,Uxuhul文化繁荣了近千年。每年代表各阶层的高级牧师都会聚集在首都,投票表决重要事情。每次严格限定表决三个议案,每个议案只有是/否的回答。三个议案在议论表决中同时决定,遵循下列形式:

所有牧师聚集在一大房间,房间中央有一张桌子。在桌子上放了三片石块,一面黑,一面白。每个石块代表一个议案,黑表示“否”,白表示“是”。最初所有的石块都是黑色的面朝上,表示所有的议案都是否定的结果。根据年龄,从年轻的到年长者,每位牧师通过翻动一块石块来表决,也就是改变某个议案的结果。不允许不表决。最年长的牧师做出选择后,石块朝上的颜色决定了三个议案的最终结果。

     Uxuhul的政治相当复杂,大量代表不同利益的游说(和行贿),影响三个议案的八种结果。宗教规则强制每位牧师在投票表决前公开他们对三个议案的八种结果的表决倾向。由于彼此间都知道表决倾向,每位牧师都争取对自己最有利的结果,而且Uxuhul人具有高超的逻辑推理能力,对游戏规则又很了解,每个牧师都能找到最理想的方法!

最后,复杂刻板行政系统导致Uxuhul文明的崩溃。只留下他们的城市和庙宇,历史学家和考古学家试图通过研究每年议案的表决结果来弄清他们的历史。不管怎么说,只有牧师们的表决倾向留下来了,没有实际的表决结果。那么你的任务就是揭示出表决结果。

输入:

输入从一个整数n开始,1<=n<=100,代表表决的次数。后面跟着n个问题。每次表决由一个整数m开始,1<=m<=100,表示牧师的数目。后面m行,按照顺序代表每位投票者的表决倾向。对于每种结果(NNN,NNY, NYN, NYY, YNN, YNY, YYN, YYY, N’表示否,‘Y’表示是),给一个[18]之间的数,越小的数字表示选择可能性越大。对8种结果的表决倾向按照前面列举的顺序的给出。    

输出:

对于每个问题,输出三个议案的表决结果。‘N’表示否,‘Y’表示是。

样例输入:

2

4

8 7 6 5 4 3 2 1 

8 6 3 1 2 4 5 7

8 3 6 5 1 2 7 4

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

样例输出:

 NYY NNY 

解题思路:

 这个题似乎可以采用贪心来做。每一个牧师都从可能的结果中选择对自己最有利的结果,最后的结果似乎就是人人都满意的结果?仔细分析就发现这种思路不正确。假如有两个人来选择,选择倾向是:

 2 1 7 4 5 8 3 6 

 3 8 6 4 5 1 2 7 

 如果按照贪心的思想去做,第一个人应该选择NNY,但第二个人肯定会选择成YNY,结果是第一个人最不满意的。如果第一个人选择NYN,表面上是他不满意的选择(排在第七位的结果),但第二个人会选择YYN(这是他在这种情况下的最好选择,排在第二位),而对于第一个人来说,YYN也是一个不错的结果。

所以,贪心的思路不行。

在解决这个题之前,让我们先看看海盗的传说:  

5个海盗,多年的海上生活使他们积攒了100颗宝石。他们决定将宝石分掉之后就分手,洗手不干了。他们商量许久,最后决定:首先抽签,每个人抽得一个号码(1号到5号),然后1号海盗提出一个分配方案,所有海盗举手表决,如果过半数的海盗同意该分配方案,则按此方案分配,否则将1号海盗扔到大海。再由2号海盗提出他的分配方案,重复以上过程,直到找到分配方案止。海盗都是很聪明的,而且彼此知道别人的聪明,每个海盗都希望得到最多的宝石。如果你是1号海盗,你会提出怎样的分配方案,使你获得最多的宝石且保住性命?

因为海盗很聪明,所以你能想到的别的也能想到。

我们从简单情况出发:

首先看4号海盗分配方案,只可能是0 100(少15号可不会投票支持) 

所以3号海盗分配方案,99 1 0(首先5号一颗都不用给,因为只有100颗才能合他的胃口,4号一颗足矣,总比没有强)

2号海盗分配方案,97 0 2 1(首先3号一颗都不用给,因为只有100颗才能合他的胃口,4号两颗,5号一颗,要比3号分得多)

所以1号海盗最佳分配方案,97 0 1 0 2(原因同上,135号海盗投票支持)

和海盗问题一样,要倒过来思考,我们以杨例中的第一组数据来说明牧师是怎么思考的

  4 

  8 7 6 5 4 3 2 1

  8 6 3 1 2 4 5 7 

  8 3 6 5 1 2 7 4

  1 2 3 4 5 6 7 8 

首先最后投票的很简单,从可能出现的结果里找个最喜欢的。

倒数第二怎么投?  假设他面对的是NYY,他可以变为{NNYNYNYYY},这其中他最喜欢的是NNY,但他就会选择NNY吗?不会!

因为选择NNY,最后(通过最后牧师表决)的结果就是NNN,是他最不喜欢的,他的选择是YYY,因为最后的结果将是NYY,是可以得到的最好结果。

    从后往前,记录当前祭司面对每种情况(NNN, NNY, NYN, NYY, YNN, YNY, YYN, YYY)下选择能得到的最好结果。 

得到的状态序列将是

NNY NNN NNN NNY NNN NNY NYN NYY 

NNN NNY NNY NYY NNY NYY NYY NNY

  NNY NYY NYY NNY NYY NNY NNY NYY

NYY NNY NNY NYY NNY NYY NYY NNY

这是一个系列的决策过程,所以可以采用动态规划的方法。

因为一号祭司首先面对NNN,所以最好也是最后结果将是NYY

具体算法就是用一个数组r记录每位牧师面对八种情况,它能取得的最好结果。从后面往前计算,对每一种情况,可以选择的变化是三个,从这三个变化中找到最好的结果。 重复这样的过程,一直到第一个人面对NNN能取得的最好结果就是全体牧师表决的结果。

代码核心及注释:

charoutcome[8][4]={"NNN","NNY","NYN","NYY","YNN","YNY","YYN","YYY"};                  //表决结果

int a[100][8];            //存储表决倾向

int main()

     int r[8],t[8],x[3];   //r,t纪录最佳的表决结果,采用滚动数组, 

     scanf"%d",&c

     whilec--

          scanf"%d",&n;

           fori=0;i<n;++i

                forj=0;j<8;++j

                      scanf"%d",&a[i][j]

           fori=0;i<8;++it[i]=i;            //初始化 

           fori=n-1;i>=0;--i{ //从后往前采用动态规划  

                 forj=0;j<8;++j)   {            //j表示8种结果

                       fork=0;k<3;k++x[k]=t[1<<k^j]; //x表示3种变化                       r[j]=x[0];  

                       ifa[i][x[1]]<a[i][r[j]]r[j]=x[1]; 

                       ifa[i][x[2]]<a[i][r[j]]r[j]=x[2]; //这三步选择最好的结果                                        

                   fork=0;k<8;++kt[k]=r[k];     //滚动数组 

              printf"%s\n",outcome[r[0]]

     } 

}  

                          小  结

  动态规划是研究最优化问题的算法  动态规划算法的有效性依赖于待求解问题本身具有的两个重要性质:最优子结构性质和子问题重叠性质。  动态规划算法往往可以用递归解决,但动态规划具有更高的效率 

0 0
原创粉丝点击