【动态规划】原理

动态规划原理
    虽然我们已经用动态规划方法解决了两个问题,但你可能还是弄不清应该在何时使用动态规划。接下来,我们关注适合应用动态规划方法求解的最优化问题应该具备的两个要素:最优子结构和子问题重叠。以及再次讨论备忘方法,更深入地讨论在自顶向下方法中如何借助备忘机制来充分利用子问题重叠特性。
最优子结构
    用动态规划方法求解最优化问题的第一步就是刻画最优解的结构。如果一个问题的最优解包含其子问题的最优解,我们称此问题具有最优子结构性质。因此,某个问题是否适合应用动态规划算法,它是否具有最优子结构性质是一个好线索(当然具有最优子结构性质也可能意味着适合应用贪心策略)。使用动态规划方法时,我们用子问题的最优解来构造原问题的最优解。因此,我们必须小心确保了考察了最优解中用到的所有子问题
    在发掘最优子结构性质的过程中,实际上遵循了如下的通用模式
   1.证明问题最优解的第一个组成部分是做出一个选择,例如,选择钢条第一次切割位置,选择矩阵链的划分位置等。做出这次选择会产生一个或多个待解的子问题。
   2.对于一个给定问题,在其可能的第一步选择中,你假定已经知道哪种选择才会得到最优解。你现在并不关心这种选择具体是如何得到的,只是假定已经知道了这种选择。
   3.给定可获得最优解的选择后,你确定这次选择会产生那些子问题,以及如何最好地刻画子问题空间。
   4.利用“剪切—粘贴”(cut-and-paste)技术证明:作为构成原问题最优解的组成部分,每个子问题的解就是它本身的最优解。证明这一点是利用反证法:假定子问题的解不是其自身的最优解,那么我们可以从原问题的解中“剪切”掉这些非最优解,将最优解粘贴进去,从而得到原问题一个更优的解,与这最初的解时原问题最优解的前提假设矛盾。如果原问题的最优解包含多个子问题,通常他们都很相似,我们可以将针对一个子问题的“剪切—粘贴”论证方法稍加修改,用于其他子问题。

     一个刻画子问题空间的好经验是:保持子问题空间尽可能简单,只有必要时才能扩展它。例如,我们求解钢条切割问题时,子问题空间中包含的问题为:对每个i值,长度为i的钢条的最优切割问题。这个子问题空间很有效,我们不必尝试更一般性的子问题空间。
     与之相对的,假定我们试图限制矩阵链A(1)A(2)…A(j)乘法问题的子问题空间。如前所述,最优括号化方案必然在某个位置k(1<=k<j)处,即A(k)和A(k+1)之间对矩阵链进行划分。除非我们能保证k永远等于j-1,否则我们会发现得到两个形如A(1)A(2)…A(k)和A(k+1)A(k+2)…A(j)的子问题,而后者的形式与A(i)A(i+1)…A(j)是不同的。因此,对矩阵链乘法问题,我们必须允许子问题在两端都可以变化,即允许子问题A(i)A(i+1)…A(j)中i和j都可变。
    对于不同问题领域,最优子结构的不同体现在两个方面:
  1.原问题的最优解中涉及多少个子问题,以及
  2.在确定最优解使用哪些子问题时,我们需要考虑多少种选择。

    在钢条切割问题中,长度为n的钢条的最优切割方案仅仅使用一个子问题(长度为n-i的钢条最优切割),但我们必须考察i的n种不同取值,来确定哪一个会产生最优解。
    A(i)A(i+1)…A(j)的矩阵链乘法问题中,最优解使用两个子问题,我们需要考察j-i种情况。对于给定的矩阵链划分位置—矩阵Ak,我们需要求解两个子问题—A(i)A(i+1)…A(k)和A(k+1)A(k+2)…A(j)的括号化方案—而且两个子问题都必须求解最优方案。一旦我们确定了子问题的最优解,就可以在j-i个候选的k中选取最优者。
     在动态规划方法中,我们通常自底向上地使用最优子结构。也就是说,首先求得子问题的最优解,然后求原问题的最优解。在求解原问题过程中,我们需要在涉及的子问题中做出选择,选出能得到原问题最优解的子问题。原问题最优解的代价通常就是子问题最优解的代价再加上由此次选择直接产生的代价。例如,对于钢条切割问题,我们首先求解子问题,确定长度为i=0,1,…,n-1的钢条的最优切割方案,然后确定哪个子问题解构成长度为n的钢条的最优切割方案。此次选择本身所产生的代价就是pi。在矩阵链乘法问题中,我们先确定子矩阵链A(i)A(i+1)…A(j)的最优括号化方案,然后选择划分位置Ak,选择本身所产生的代价就是p(i-1)p(k)p(j)。
    与贪心算法的异同,能够应用贪心算法的问题也必须具有最优子结构性质。贪心算法和动态规划最大的不同在于,它并不是首先寻找子问题的最优解,然后在其中进行选择,而是首先做出一次“贪心”选择—在当时(局部)看来最优的选择—然后求解选出的子问题,从而不必费心求解所有可能相关的子问题。在某些情况下这一策略也能得到最优解!
一些微妙之处
     在尝试使用动态规划方法时要小心,要注意问题是否具有最优子结构性质。考虑下面两个问题,其中都是给定一个有向图G=(V,E)和两个顶点u,v∈V。
     无权(unweighted)最短路径:找到一条从u到v的边数最少的路径。这条路径必然是简单路径,因为如果路径中包含环,将环去掉显然会减少边的数量。
     无权最长路径:找到一条从u到v的边数最多的简单路径。这里必须加上简单路径的要求,因为我们可以不停地沿着环走,从而得到任意长的路径。
     下面我们证明无权最短路径问题具有最优子结构性质。假设u≠v,则问题时非平凡的。这样,从u到v的任意路径p都必须包含一个中间顶点,比如w(注意,w可能是u和v)。因此,我们可以将路径p:u->v分解为两条子路径,p1:u->w,p2:w->v。显然,p的边数等于p1加上p2的边数。于是,我们断言:如果p从u到v的最优路径,那么p1必须是从u到w的最短路径。可以通过剪切—粘贴方法来证明:如果存在另一条从u到w的路径p1’,其边数比p1少,那么可以剪切掉p1,将p1’粘贴上,构造出一条比p边数更少的路径,与p最优的假设矛盾。对称地,p2必须必须是从w到v的最短路径。因此我们可以通过考察所有中间顶点w来求u到v的最短路径,对每个中间顶点w,求u到w和w和v的最短路径,然后选择两条路径之和最短的顶点w。
     你可能已经倾向于假设无权最长简单路径问题也具有最优子结构性质。毕竟,如果我们将最长简单路径p:u->v分解为子路径p1:u->w,p2:w->v。难道p1不应该是从u到w的最长简单路径,p2不应该使从w到v的最长简单路径吗?但答案是否定的

下图给出一个例子。考虑路径p->r->t,它是从q到t的最长简单路径。q->r是从q到r是最长简单路径吗?不是的,q->s->t->r是一条更长的简单路径。r->t是从r到t的最长简单路径吗?同样不是,r->q->s->t比它更长。

《【动态规划】原理》

      这个例子说明,最长简单路径问题不仅缺乏最优子结构性质,由于子问题的解组合出的甚至都不是原问题的“合法解。如果我们组合最长简单路径q->s->t->r和r->q->s->t,得到的是路径q->s->t->r->q->s->t,并不是简单路径。的确,无权最长简单路径问题看起来不像有任何形式的最优子结构。对此问题尚未找到有效的动态规划算法。实际上,此问题是NP完全的,这意味着我们不太可能找到多项式时间的求解方法。
     为什么最长简单路径问题的子结构与最短路径有这么大的差别?原因在于,虽然最长路径问题和最短路径问题的解都用到了两个子问题,但两个最长简单路径子问题是相关的,而两个最短路径子问题是无关的(nodependent)。这里,子问题无关的含义是,同一个原问题的一个子问题的解不影响另一个子问题的解。对于图中的例子,求q到t的最长简单路径可以分解为两个子问题:求q到r的最长简单路径和r到t的最长简单路径。对于前者,我们可以选择路径
q->s->t->r,其中用到顶点s和t。由于两个子问题的解的组合必须产生一条简单路径,因此我们在求解第二个子问题时就不能用到这两个顶点了。但如果在求解第二个子问题时不允许使用顶点t,就根本无法进行下去,因为t是原问题解的路径终点,是必须用到的,还不像子问题的解的”接合“顶点r那样可以不用。这样,由于一个子问题的解使用了顶点s和t,在另一个子问题的解中就不能再使用它们,但其中至少一个顶点在求解第二个子问题时又必须用到,而获得最优解则两个都要用到。因此,我们说两个子问题是相关的。换个角度来看,我们所面临的困境就是:求解一个子问题时用到了某些资源(在本例中是顶点),导致这些资源在求解其他子问题时不可用
      那么,求解最短路径的子问题间又为什么是无关的呢?根本原因在于,最短路径子问题间是不共享资源的。我们可以断言:如果一个顶点w出现在u到v的最短路径上,那么可以通过拼接任意的最短路径p1:u->w和任意的最短路径p2:w->v来构造u到v的最短路径。我们可以保证,出了w其他任何顶点都不会同时出现在p1和p2上。原因何在?假定某个顶点x≠w同时出现在路径p1和p2上,我们就可以将p1分解为(p(ux):u->x)->w,将p2分解为w->(p(xv):x->v)。根据最优子结构性质,路径p的边数等于p1和p2边数之和,假定为e。接下来我们构造一条u到v的路径p’=(p(xv):(p(ux):u->x)->v)。由于已经删掉了x到w和w到x的路径,每条路径至少包含一条边(x->w或w->x),因此p’最多包含e-2条便,与p为最短路径的假设矛盾。因此,我们可以保证最短路径问题的子问题间是无关的。
     在钢条切割问题中,为了确定长度为n的钢条的最优切割方案,我们考察所有长度为i(i=0,1,…,n-1)的钢条的最优切割方案。由于长度为n的问题的最优解方案只包含一个子问题的解(我们切掉了第一段),子问题无关系显然是可以保证的。
     在矩阵乘法问题中,子问题为子链A(i)A(i+1)…A(k)和A(k+1)A(k+2)A(j)的乘法问题。子链是互不相交的,因此任何矩阵都不会同时包含在两条子链中。
重叠子问题
     

适合用动态规划方法求解的最优化问题应该具备的第二个性质是子问题空间必须足够“小”,即问题的递归算法会反复地求解相同的子问题,而不是一直生成新的子问题。一般来讲,不同子问题的总数是输入规模的多项式函数为好。如果递归算法反复求解相同的子问题,我们就称最优化问题具有重叠子问题(overlapping subproblems)性质(一个问题是否适合动态规划求解同时依赖于子问题的无关性和重叠性,这看起来很奇怪。虽然这两个要求听起来似乎是矛盾,但它们描述的是不同概念,而不是同一个坐标轴上的两个点。两个子问题如果不共享资源,它们就是独立的。而重叠是指两个子问题实际上是同一个子问题,只是作为不同问题的子问题出现而已)与之相对的,适合分治法求解的问题通常在递归的每一步都生成全新的子问题。动态规划算法通常这样利用重叠子问题性质:对每个子问题求解一次,将解存入一个表中,当再次需要这个子问题时直接查表,每次查表的代价为常量时间。
  接下来,是矩阵链乘法直接递归实现,计算过程是低效的


int RECURSIVE_MATRIX_CHAIN(int *p,int m[][M],int i,int j)
{
	if(i==j) return 0;
	m[i][j]=INT_MAX;
	int q;
	for(int k=i;k<=j-1;k++)
	{
		q=RECURSIVE_MATRIX_CHAIN(p,i,k)+RECURSIVE_MATRIX_CHAIN(p,k+1,j)+p[i-1]*p[k]*p[j];
		if(q<m[i][j])
		{
			m[i][j]=q;
		}
	}
	return m[i][j];
}  

     
下图显示了调用RECURSIVE_MATRIX_CHAIN(p,1,4)所产生的递归调用树。每个结点都标记出了参数i和j。可以看到,某些i、j值对出现了许多次。

《【动态规划】原理》

   
上图中,每个结点都包含参数i和j。每颗子树的计算,在MEMOIZED_MATRIX_CHAIN中被一次查表操作代替。 
    通过证明,我们可以发现RECURSIVE_MATRIX_CHAIN(p,1,n)所做的总工作量至少是n的指数函数。将此自顶向下的递归算法(无备忘)与自底向上的动态规划算法进行比较,后者要高效得多,因为它利用了重叠子问题性质。矩阵
乘法问题只有Θ(n^2)个不同子问题,动态规划算法对每个子问题只求解一次。而递归算法则相反,对每个子问题,每当递归树中(递归调用时)遇到它,都要重新计算一次。凡是一个问题的自然递归算法的递归调用树反复出现相同的子问题,而不同子问题的总数很少时,动态规划方法都能提高效率。
重构最优解
    从实际考虑,我们通常将每个子问题所做的选择存在一个表中,这样就不必根据代价价值来重构这些信息。
    对矩阵乘法问题,利用表s[i,j],我们重构最优解时可以节省很多时间。假定我们没有维护s[i,j]表,只是在表m[i,j]中记录了子问题的最优代价。当我们确定A(i)A(i+1)…A(j)的最优括号化方案用到了哪些子问题时,就需要检查所有j-i种可能,而j-i并不是一个常数。因此,对于一个给定问题的最优解,重构它用到了哪些子问题就需花费Θ(j-i)=w(1)的时间。而通过在s[i,j]中保存A(i)A(i+1)…A(j)的划分位置,我们重构每次选择只需O(1)时间。
备忘
    我们可以保持自顶向下策略,同时达到与自底向上动态规划方法相似的效率。思路就是对自然但低效的递归算法加入备忘机制。与自底向上方法一样,我们维护一个表记录子问题的解,但仍保持递归算法的控制流程
    带备忘的递归算法为每个子问题维护一个表项来保存它的解。每个表项的初值设为一个特殊值,表示尚未填入子问题的解。当递归调用过程中第一次遇到子问题时,计算其解,并存入对应表项。随后每次遇到同一个子问题,只是简单地查表,返回其解(这种方法假定我们预先已经知道所有可能的子问题参数(子问题空间),并已在表项和子问题间建立起对应关系。另一个更通用的备忘方法是使用散列技术,以子问题参数为关键字。)
  下面给出的是带备忘的RECURSIVE_MATRIX_CHAIN版本。注意它与带备忘的自顶向下钢条切割算法的相似之处。

int MEMOIZED_MATRIX_CHAIN(int *p,int Length)
{
    int n=Length-1;
    int **m=new int *[n+1];
    for(int i=0;i<=n;i++)
    {
       m[i]=new int[n+1]; 
     }
     for(int i=1;i<=n;i++)
         for(int j=1;j<=n;j++)
             m[i][j]=INT_MAX;
    return LOOKUP_CHAIN(m,p,1,n);
}

LOOKUP_CHAIN:

int LOOKUP_CHAIN(m,p,1,n)
{
    if(m[i][j]<INT_MAX) return m[i][j];
    int q;
    if(i == j) m[i][j]=0;
    else
    {
       for(int k=i;k<=j-1;k++)
       {
          q=LOOKUP_CHAIN(m,p,i,k)+LOOKUP_CHAIN(m,p,k+1,j)+p[i-1]*p[k]*p[j];
          if(q<m[i][j]) m[i][j]=q;
       }
     }
     return m[i][j];
}

 
   MEMOIZED_MATRIX_CHAIN与MATRIX_CHAIN_ORDER一样维护一个表m,来保存计算出的矩阵A(i..j)的最小计算代价m[i,j]。每个表被初始化为无穷大,表示还未存入过值。调用LOOKUP_CHAIN(m,p,i,j)时,如果第1行发现m[i][j]<正无穷,就直接返回之前计算出的代价。虽然LOOKUP_CHAIN(m,p,i,j)总是返回m[i,j]的值,但只在第一次调用时才真正计算
     通常情况下,如果每个子问题都必须至少求解一次,自底向上动规算法会比自顶向下备忘算法快(都是O(n^3)时间,相差一个常量系数),因为自底向上算法没有递归调用的开销,表的维护开销也更小。而且,对于某些问题,我们可以利用表的访问模式来进一步降低时空代价。
     相反,如果子问题空间中的某些子问题不必求解,备忘方法就会体现出优势了,因为它只会求解那些绝对必要的子问题。

    原文作者:动态规划
    原文地址: https://blog.csdn.net/c18219227162/article/details/50417860
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞