DP动态规划专题(一)动态规划基本模型

动态规划程序设计是对解最优化问题的一种途径、一种方法,而不是一种特殊算法。不像前面所述的那些搜索或数值计算那样,具有一个标准的数学表达式和明确清晰的解题方法。动态规划程序设计往往是针对一种最优化问题,由于各种问题的性质不同,确定最优解的条件也互不相同,因而动态规划的设计方法对不同的问题,有各具特色的解题方法,而不存在一种万能的动态规划算法,可以解决各类最优化问题。因此读者在学习时,除了要对基本概念和方法正确理解外,必须具体问题具体分析处理,以丰富的想象力去建立模型,用创造性的技巧去求解。我们也可以通过对若干有代表性的问题的动态规划算法进行分析、讨论,逐渐学会并掌握这一设计方法。

文章目录

    • 一、多阶段决策过程的最优化问题
    • 二、动态规划的基本概念和基本模型构成
    • 三、最优化原理与无后效性
    • 四、基本动态规划模型初应用
      • 【例1】数字金字塔
        • 方法一:搜索
        • 方法二:记忆化搜索
        • 方法三:动态规划(顺推法)
        • 方法四:动态规划(逆推法)

一、多阶段决策过程的最优化问题

在现实生活中,有一类活动的过程,由于它的特殊性,可将过程分成若干个互相联系的阶段,在它的每一阶段都需要作出决策,从而使整个过程达到最好的活动效果。当然,各个阶段决策的选取不是任意确定的,它依赖于当前面临的状态,又影响以后的发展,当各个阶段决策确定后,就组成一个决策序列,因而也就确定了整个过程的一条活动路线,这种把一个问题看作是一个前后关联具有链状结构的多阶段过程就称为多阶段决策过程,这种问题就称为多阶段决策问题。如下图所示:
在这里插入图片描述多阶段决策过程,是指这样的一类特殊的活动过程,问题可以按时间顺序分解成若干相互联系的阶段,在每一个阶段都要做出决策,全部过程的决策是一个决策序列。

【例1】最短路径问题。下图给出了一个地图,地图中的每个顶点代表一个城市,两个城市间的一条连线代表道路,连线上的数值代表道路的长度。现在想从城市A到达城市E,怎样走路程最短?最短路程的长度是多少?
DP动态规划专题(一)动态规划基本模型_第1张图片
【算法分析】
把A到E的全过程分成四个阶段,用K表示阶段变量,第1阶段有一个初始状态A,有两条可供选择的支路A-B1、A-B2;第2阶段有两个初始状态B1、B2,B1有三条可供选择的支路,B2有两条可供选择的支路……。用DK(XI,X+1J)表示在第K阶段由初始状态XI到下阶段的初始状态X+1J的路径距离,FK(XI)表示从第K阶段的XI到终点E的最短距离,利用倒推的方法,求解A到E的最短距离。

具体计算过程如下:
S1: K = 4 有
         F4(D1)= 3,
         F4(D2)= 4,
         F4(D3)= 3;
S2: K = 3 有
         F3(C1)= MIN{ D3(C1,D1)+ F4(D1),D3(C1,D2)+ F4(D2)}
                         = MIN{ 5+36+4 } = 8
         F3(C2)= D3(C2,D1)+ F4(D1)= 5+3 = 8
         F3(C3)= D3(C3,D3)+ F4(D3)= 8+3 = 11
         F3(C4)= D3(C4,D3)+ F4(D3)= 3+3 = 6
S3: K = 2 有   
         F2(B1)= MIN{ D2(B1,C1)+ F3(C1),D2(B1,C2)+ F3(C2),
         D2(B1,C3)+ F3(C3)} = MIN{ 1+8,6+8,3+11} = 9
         F2(B2)= MIN{ D2(B2,C2)+ F3(C2),D2(B2,C4)+ F3(C4)}
                         = MIN{ 8+84+6 } = 10
S4: K = 1 有 
         F1(A)= MIN{ D1(A,B1)+ F2(B1),D1(A,B2)+ F2(B2)}
                       = MIN{ 5+93+10} = 13
                       
因此由A点到E点的全过程最短路径为A→B2→C4→D3→E;最短路程长度为13

从以上过程可以看出,每个阶段中,都求出本阶段的各个初始状态到终点E的最短距离,当逆序倒推到过程起点A时,便得到了全过程的最短路径和最短距离。
在上例的多阶段决策问题中,各个阶段采取的决策,一般来说是与阶段有关的,决策依赖于当前状态,又随即引起状态的转移,一个决策序列就是在变化的状态中产生出来的,故有“动态”的含义,我们称这种解决多阶段决策最优化的过程为动态规划程序设计方法。

二、动态规划的基本概念和基本模型构成

现在我们来介绍动态规划的基本概念:

  1. 阶段和阶段变量:
    用动态规划求解一个问题时,需要将问题的全过程恰当地分成若干个相互联系的阶段,以便按一定的次序去求解。描述阶段的变量称为阶段变量,通常用K表示,阶段的划分一般是根据时间和空间的自然特征来划分,同时阶段的划分要便于把问题转化成多阶段决策过程,如例题1中,可将其划分成4个阶段,即K = 1,2,3,4。
  2. 状态和状态变量:
    某一阶段的出发位置称为状态,通常一个阶段包含若干状态。一般地,状态可由变量来描述,用来描述状态的变量称为状态变量。如例题1中,C3是一个状态变量。
  3. 决策、决策变量和决策允许集合:
    在对问题的处理中作出的每种选择性的行动就是决策。即从该阶段的每一个状态出发,通过一次选择性的行动转移至下一阶段的相应状态。一个实际问题可能要有多次决策和多个决策点,在每一个阶段的每一个状态中都需要有一次决策,决策也可以用变量来描述,称这种变量为决策变量。在实际问题中,决策变量的取值往往限制在某一个范围之内,此范围称为允许决策集合。如例题1中,F3(C3)就是一个决策变量。
  4. 策略和最优策略:
    所有阶段依次排列构成问题的全过程。全过程中各阶段决策变量所组成的有序总体称为策略。在实际问题中,从决策允许集合中找出最优效果的策略成为最优策略。
  5. 状态转移方程
    前一阶段的终点就是后一阶段的起点,对前一阶段的状态作出某种决策,产生后一阶段的状态,这种关系描述了由k阶段到k+1阶段状态的演变规律,称为状态转移方程。

三、最优化原理与无后效性

上面已经介绍了动态规划模型的基本组成,现在需要解决的问题是:什么样的“多阶段决策问题”才可以采用动态规划的方法求解。
一般来说,能够采用动态规划方法求解的问题,必须满足最优化原理和无后效性原则:

  1. 动态规划的最优化原理。作为整个过程的最优策略具有:无论过去的状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略的性质。也可以通俗地理解为子问题的局部最优将导致整个问题的全局最优,即问题具有最优子结构的性质,也就是说一个问题的最优解只取决于其子问题的最优解,而非最优解对问题的求解没有影响。在例题1最短路径问题中,A到E的最优路径上的任一点到终点E的路径,也必然是该点到终点E的一条最优路径,即整体优化可以分解为若干个局部优化。
  2. 动态规划的无后效性原则。所谓无后效性原则,指的是这样一种性质:某阶段的状态一旦确定,则此后过程的演变不再受此前各状态及决策的影响。也就是说,“未来与过去无关”,当前的状态是此前历史的一个完整的总结,此前的历史只能通过当前的状态去影响过程未来的演变。在例题1最短路径问题中,问题被划分成各个阶段之后,阶段K中的状态只能由阶段K+1中的状态通过状态转移方程得来,与其它状态没有关系,特别与未发生的状态没有关系,例如从Ci到E的最短路径,只与Ci的位置有关,它是由Di中的状态通过状态转移方程得来,与E状态,特别是A到Ci的路径选择无关,这就是无后效性。

由此可见,对于不能划分阶段的问题,不能运用动态规划来解;对于能划分阶段,但不符合最优化
原理的,也不能用动态规划来解;既能划分阶段,又符合最优化原理的,但不具备无后效性原则,还是不能用动态规划来解;误用动态规划程序设计方法求解会导致错误的结果。

四、基本动态规划模型初应用

【例1】数字金字塔

观察下面的数字金字塔。写一个程序查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以从当前点走到左下方的点也可以到达右下方的点。
DP动态规划专题(一)动态规划基本模型_第2张图片
在上面的样例中,从13到8到26到15到24的路径产生了最大的和86。
输入:
第一个行包含R(1<= R<=1000),表示行的数目。
后面每行为这个数字金字塔特定行包含的整数。
所有的被供应的整数是非负的且不大于100。
输出:
单独的一行,包含那个可能得到的最大的和。
样例输入:
5 //数塔层数
13
11 8
12 7 26
6 14 15 8
12 7 13 24 11
样例输出:
86

方法一:搜索

问题要求的是从最高点按照规则走到最低点的路径的最大的权值和,路径起点终点固定,走法规则明确,可以考虑用搜索来解决。

定义递归函数void Dfs(int x,int y,int Curr),其中x,y表示当前已从(1,1)走到(x,y),目前已走路径上的权值和为Curr。
当x=N时,到达递归出口,如果Curr比Ans大,则把Ans更新为Curr;否则向下一行两个位置行走,即递归执行Dfs(x+1,y,Curr+A[x+1][y])和Dfs(x+1,y+1,Curr+A[x+1][y+1])。

#include 
using namespace std;
const int MAXN = 1005;
int A[MAXN][MAXN],F[MAXN][MAXN],N,Ans;
void Dfs(int x,int y,int Curr)
{
	  if (x==N)
	  {
	      if (Curr>Ans)Ans=Curr;
	      return;
	  }
	  Dfs(x+1,y,Curr+A[x+1][y]);
	  Dfs(x+1,y+1,Curr+A[x+1][y+1]);
}
int main()
{
     cin >> N;
     for(int i = 1;i <= N;i ++)
       for(int j = 1;j <= i;j ++)
	 cin >> A[i][j];
     Ans =0;
     Dfs(1,1,A[1][1]);
     cout<<Ans<<endl;
     return 0;
}

该方法实际上是把所有路径都走了一遍,由于每一条路径都是由N-1步组成,每一步有“左”、“右”两种选择,因此路径总数为2N-1,所以该方法的时间复杂度为O(2N-1),超时。

方法二:记忆化搜索

方法一之所以会超时,是因为进行了重复搜索,如样例中从(1,1)到(3,2)有“左右”和“右左”两种不同的路径,也就是说搜索过程中两次到达(3,2)这个位置,那么从(3,2)走到终点的每一条路径就被搜索了两次,我们完全可以在第一次搜索(3,2)到终点的路径时就记录下(3,2)到终点的最大权值和,下次再次来到(3,2)时就可以直接调用这个权值避免重复搜索。

我们把这种方法称为记忆化搜索。

记忆化搜索需要对方法一中的搜索进行改装。由于需要记录从一个点开始到终点的路径的最大权值和,因此我们重新定义递归函数Dfs。

定义Dfs(x,y)表示从(x,y)出发到终点的路径的最大权值和,答案就是Dfs(1,1)。计算Dfs(x,y)时考虑第一步是向左还是向右,我们把所有路径分成两大类:
①第一步向左:那么从(x,y)出发到终点的这类路径就被分成两个部分,先从(x,y)到(x+1,y)再从(x+1,y)到终点,第一部分固定权值就是A[x][y],要使得这种情况的路径权值和最大,那么第二部分从(x+1,y)到终点的路径的权值和也要最大,这一部分与前面的Dfs(x,y)的定义十分相似,仅仅是参数不同,因此这一部分可以表示成Dfs(x+1,y)。
综上,第一步向左的路径最大权值和为A[x][y]+Dfs(x+1,y);
②第一步向右:这类路径要求先从(x,y)到(x+1,y+1)再从(x+1,y+1)到终点,分析方法与上面一样,这类路径最大权值和为A[x][y]+Dfs(x+1,y+1);

为了避免重复搜索,我们开设全局数组F[x][y]记录从(x,y)出发到终点路径的最大权值和,一开始全部初始化为-1表示未被计算过。在计算Dfs(x,y)时,首先查询F[x][y],如果F[x][y]不等于-1,说明Dfs(x,y)之前已经被计算过,直接返回 F[x][y]即可,否则计算出Dfs(x,y)的值并存储在F[x][y]中。

  #include 
  #include 
  using namespace std;
  const int MAXN = 505;
  int A[MAXN][MAXN],F[MAXN][MAXN],N;
  int Dfs(int x,int y)
  {
  	if (F[x][y]==-1)
  	{
             if (x==N)
             	F[x][y]=A[x][y];
             else 
             	F[x][y]=A[x][y]+max(Dfs(x+1,y),Dfs(x+1,y+1));
    }
    return F[x][y];
  }
  int main()
  {
  	cin >> N;
  	for(int i = 1;i <= N;i ++)
  		for(int j = 1;j <= i;j ++)
  			cin >> A[i][j];
  	for(int i = 1;i <= N;i ++)
  		for(int j = 1;j <= i;j ++)
  			F[i][j] = -1;
  	Dfs(1,1);
  	cout << F[1][1] << endl;
  	return 0;
  }

由于F[x][y]对于每个合法的(x,y)都只计算过一次,而且计算是在O(1)内完成的,因此时间复杂度为O(N2)。可以通过本题。

方法三:动态规划(顺推法)

方法二通过分析搜索的状态重复调用自然过渡到记忆化搜索,而记忆化搜索本质上已经是动态规划了。下面我们完全从动态规划的算法出发换一个角度给大家展示一下动态规划的解题过程,并提供动态规划的迭代实现法。

①确定状态:
题目要求从(1,1)出发到最底层路径最大权值和,路径中是各个点串联而成,路径起点固定,终点和中间点相对不固定。因此定义F[x][y]表示从(1,1)出发到达(x,y)的路径最大权值和。最终答案Ans=max{F[N][1],F[N][2],…,F[N][N]}。
②确定状态转移方程和边界条件:
不去考虑(1,1)到(x,y)的每一步是如何走的,只考虑最后一步是如何走,根据最后一步是向左还是向右分成以下两种情况:

  • 向左:最后一步是从(x-1,y)走到(x,y),此类路径被分割成两部分,第一部分是从(1,1)走到(x-1,y),第二部分是从(x-1,y)走到(x,y),要计算此类路径的最大权值和,必须用到第一部分的最大权值和,此部分问题的性质与F[x][y]的定义一样,就是F[x-1,y],第二部分就是A[x][y],两部分相加即得到此类路径的最大权值和为F[x-1,y]+A[x,y];
  • 向右:最后一步是从(x-1,y-1)走到(x,y),此类路径被分割成两部分,第一部分是从(1,1)走到(x-1,y-1),第二部分是从(x-1,y-1)走到(x,y),分析方法如上。此类路径的最大权值和为F[x-1,y-1]+A[x,y];

F[x][y]的计算需要求出上面两种情况的最大值。
综上,得到状态转移方程如下:
F[x][y]=max{F[x-1,y-1],F[x-1][y]} + A[x,y]

与递归关系式还需要递归终止条件一样,这里我们需要对边界进行处理以防无限递归下去。观察发现计算F[x][y]时需要用到F[x-1][y-1]和F[x-1,y],是上一行的元素,随着递归的深入,最终都要用到第一行的元素F[1][1],F[1][1]的计算不能再使用状态转移方程来求,而是应该直接赋予一个特值A[1][1]。这就是边界条件。

综上得:
状态转移方程:F[x][y]=max{F[x-1][y-1],F[x-1][y]}+A[x,y]
边界条件:F[1][1]=A[1][1]

现在让我们来分析一下该动态规划的正确性,分析该解法是否满足使用动态规划的两个前提:

  • 最优化原理:这个在分析状态转移方程时已经分析得比较透彻,明显是符合最优化原理的;
  • 无后效性:状态转移方程中,我们只关心F[x-1][y-1]与F[x-1][y]的值,计算F[x-1][y-1]时可能有多种不同的决策对应着最优值,选哪种决策对计算F[x][y]的决策没有影响,F[x-1][y-1]也是一样。这就是无后效性。

③程序实现:
由于状态转移方程就是递归关系式,边界条件就是递归终止条件,所以可以用递归来完成,递归存在重复调用,利用记忆化可以解决重复调用的问题,方法二已经讲过。记忆化实现比较简单,而且不会计算无用状态,但递归也会受到“栈的大小”和“递推+回归执行方式”的约束,另外记忆化实现调用状态的顺序是按照实际需求而展开,没有大局规划,不利于进一步优化。

这里介绍一种迭代法。与分析边界条件方法相似,计算F[x][y]用到状态F[x-1][y-1]与F[x-1][y],这些元素在F[x][y]的上一行,也就是说要计算第x行的状态的值,必须要先把第x-1行元素的值计算出来,因此我们可以先把第一行元素F[1][1]赋为 A[1][1],再从第二行开始按照行递增的顺序计算出每一行的有效状态即可。时间复杂度为O(N2)。

#include 
  #include 
  using namespace std;
  const int MAXN = 1005;
  int A[MAXN][MAXN],F[MAXN][MAXN],N;
  
  int main()
  {
  	cin >> N;
  	for(int i = 1;i <= N;i ++)
  	    for(int j = 1;j <= i;j ++)
  	        cin >> A[i][j];
  	F[1][1] = A[1][1];
  	for(int i = 2;i <= N;i ++)
	    for(int j = 1;j <= i;j ++)
  	       F[i][j]=max(F[i-1][j-1],F[i-1][j])+A[i][j];
  	int ans =0;
  	for(int i = 1;i <= N;i ++)
  	    ans = max(ans,F[N][i]);
  	cout << ans << endl;
  	return 0;
  }

方法四:动态规划(逆推法)

【算法分析】
①贪心法往往得不到最优解:本题若采用贪心法则:13-11-12-14-13,其和为63,但存在另一条路:13-8-26-15-24,其和为86。

贪心法问题所在:眼光短浅。

②动态规划求解:动态规划求解问题的过程归纳为:自顶向下的分析,自底向上计算。

其基本方法是:
划分阶段:按三角形的行,划分阶段,若有n行,则有n-1个阶段。

A.从根结点13出发,选取它的两个方向中的一条支路,当到倒数第二层时,每个结点其后继仅有两个结点,可以直接比较,选择最大值为前进方向,从而求得从根结点开始到底端的最大路径。

B.自底向上计算:(给出递推式和终止条件)
①从底层开始,本身数即为最大数;
②倒数第二层的计算,取决于底层的数据:12+6=18,13+14=27,24+15=39,24+8=32;
③倒数第三层的计算,取决于底二层计算的数据:27+12=39,39+7=46,39+26=65
④倒数第四层的计算,取决于底三层计算的数据:46+11=57,65+8=73
⑤最后的路径:13——8——26——15——24

C.数据结构及算法设计
①图形转化:直角三角形,便于搜索:向下、向右
②用三维数组表示数塔:a[x][y][1]表示行、列及结点本身数据,a[x][y][2]能够取得最大值,a[x][y][3]表示前进的方向——0向下,1向右;
③算法:
数组初始化,输入每个结点值及初始的最大值、前进方向为0;
从倒数第二层开始向上一层求最大路径,共循环N-1次;
从顶向下,输出路径:究竟向下还是向右取决于列的值,若列的值比原先多1则向右,否则向下。

D.参考程序

#include
#include
using namespace std;
int main()
{
	int n,x,y;
	int a[51][51][4];
	cout<<"please input the number of rows:";
	cin>>n;
	memset(a,0,sizeof(a));
	for (x=1;x<=n;x++)                      //输入数塔的初始值
     	for (y=1;y<=x;y++)
	    {
			cin>>a[x][y][1];
			a[x][y][2]=a[x][y][1];			//初始化最大值即为本身
			a[x][y][3]=0;                   //路径走向,默认向下
		} 
      for (x=n-1;x>=1;x--)
            for (y=1;y<=x;y++)
                  if (a[x+1][y][2]>a[x+1][y+1][2])     //选择路径,保留最大路径值
                  { 
                  	a[x][y][2]=a[x][y][2]+a[x+1][y][2]; 
                  	a[x][y][3]=0; //down
                  }
                  else 
                  { 
                  	a[x][y][2]=a[x][y][2]+a[x+1][y+1][2]; 
                  	a[x][y][3]=1; //right
                  }
      cout<<"max="<<a[1][1][2]<<endl;           //输出数塔最大值
      y=1;
      for (x=1;x<=n-1;x++)                      //输出数塔最大值的路径
      {
             cout<<a[x][y][1]<<"->";
             y=y+a[x][y][3];                         //下一行的列数
      }
     cout<<a[n][y][1]<<endl;
}
输入:
5               //数塔层数
13
11   8
12   7    26
6   14    15    8
12   7    13   24    11
输出结果:
 max=86
 13->8->26->15->24

(未完待续…)

你可能感兴趣的:(算法,C/C++,算法入门,动态规划,信息学竞赛,算法详解)