生活中我们经常能够碰到这样一类问题,它的过程可以按照时间顺序分成若干个相互联系的子阶段。每一个子阶段都可以得出一个结论,学术上我们称之为决策,则这一整个过程的决策就是这些子阶段的决策的总和,即决策序列。这种过程称为多阶段决策过程。

针对这种多阶段决策过程,本文提到一种算法对于解决该类问题非常适用,即动态规划算法。动态规划(Dynamic Programming)算法是解决多阶段决策过程最优化问题的一种常用方法,难度比较大,技巧性也很强。利用动态规划算法解决此类问题,可以减少很多无谓的计算量。

动态规划算法的基本思想是:将待求解的问题分解成若干个相互联系的子问题,先求解子问题,然后从这些子问题的解得到原问题的解;对于重复出现的子问题,只在第一次遇到的时候对它进行求解,并把答案保存起来,以后再次遇到时直接引用答案,不必重新求解。动态规划算法将问题的解决方案视为一系列决策的结果。另外,在动态规划算法中,还要考察每个最优决策序列中是否包含一个最优决策子序列,即问题是否具有最优子结构性质。

必要条件

一般来说,在判定一个问题是否需要采用动态规划方法求解时,我们需要观察分析该问题是否满足以下几个条件:

(1)动态规划的子问题重叠性质:

         这里所说的子问题是指整个过程中的子阶段:假设某n个成员的不规则数据序列中,需要找出该序列中一个递增子序列的最大成员个数。例如给定5个数据分别为2、6、3、9、8,由于该问题比较容易解答,我们在此通过表格的形式来展示各个阶段的最优解。下图中第一行代表的是数据的个数,第二行为给定的数据,第三行在个数为n时的最优解,当有5个数据的时候,此时的递增成员变量的最大个数为3个(S(5))。而图中用椭圆形圈住的即该系统的子系统,也就是我们标题中所说的子问题。

 

动态规划总结

从图中可以看出,我们如果要向计算棕色圈住的即5个数据的系统中的最优解,我们其实可以先将深蓝色的即4个数据的最优解求出,然后使用该最优解中的最大数据与第5个数据进行比较,从而决定第5个数据是否也能放在最大递增子序列中;而如果要求解深蓝色系统的最优解,我们可以先将绿色系统的最优解求出,而绿色系统又是深蓝色系统的子系统。由此可见,该系统中的最优解可以由其子系统中的最优解间接求出。

         子问题的重叠性描述的是在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下上一次计算的结果,因此相较于简单的利用递归处理的要有更高的效率。本文认为子问题重叠是动态规划使用的基础,如果子问题之间没有相关联系,动态规划处理也就不适合使用。

(2)动态规划的最优化原理:

动态规划解决问题的思路一般就是求解问题的最优解,当问题呈现出子问题重叠性之后,问题的最优解即可以理解为求解子问题的局部最优解。所有子问题的最优解会导致整个问题的全局最优,因此称该问题具有最优子结构的性质,也就是说一个问题的最优解只取决于其子问题的最优解,而非最优解对问题的求解没有影响。

(3)动态规划的无后效性原则:

 所谓无后效性原则,指的是这样一种性质:某阶段的状态一旦确定,则此后过程的演变不再受此前各状态及决策的影响。也就是说,“未来与过去无关”,当前的状态是此前历史的一个完整的总结。

设计步骤

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

1、分析问题的最优解;

2、递归地定义最优值;

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

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

1~3步是动态规划算法解决问题的基本步骤,在只需要计算最优值的问题中,完成这三个基本步骤就可以了。如果问题需要构造最优解,还要执行第4步;此处的最优解是对最优值的一个通用表达,可以表达该系统内任意子问题的最优值。因此在第3步通常需要记录更多的信息,以便在步骤4中,有足够的信息快速地构造出最优解。

举例说明

下面我们以一个例子对这个算法的使用过程进行讲解,例子取自Leetcode网站(https://leetcode.com/problems/minimum-swaps-to-make-sequences-increasing/description/),题目(801. Minimum Swaps To Make Sequences Increasing)原文如下:

We have two integer sequences A and B of the same non-zero length.

We are allowed to swap elements A[i] and B[i].  Note that both elements are in the same index position in their respective sequences.

At the end of some number of swaps, A and B are both strictly increasing.  (A sequence is strictly increasing if and only if A[0] < A[1] < A[2] < ... < A[A.length - 1].)

Given A and B, return the minimum number of swaps to make both sequences strictly increasing.  It is guaranteed that the given input always makes it possible.

Example:

Input: A = [1,3,5,4], B = [1,2,3,7]

Output: 1

Explanation:

Swap A[3] and B[3].  Then the sequences are: A = [1, 3, 5, 7] and B = [1, 2, 3, 4]which are both strictly increasing.

题目翻译过来我们可以这么理解:给定两个成员数量均相等的整数序列A和B,要求在最小的调换次数内使两个序列严格递增。调换数据的标准为A序列的第i个数据与B序列对应位置的第i个数据调换,即A[i]与B[i]调换。

首先对于问题进行分析,我们来确定该问题是否可以使用动态规划的方法来解决,因此我们需要判断该问题是否满足动态规划的三个条件。

1、子问题重叠性:

对该问题进行分析,为了保证n个成员变量的数组都可以递增,那么首先需要保证前n-1的数据是严格递增的,那么再进行第n个数据的判定就会容易很多。因此为了求解n个数据需要调换的次数,我们可以先确定前n-1个数据调换的次数,然后再判断第n个数据是否调换从而确定n个数据满足题目要求的调换次数。这样我们就将整个问题拆分成了若干个子问题来解决,而且子问题的状态是可以依次影响下一级问题的状态,由此可见,该问题具有子问题的重叠性。

2、最优化原理:

由于本题要求的是满足数据调换后两个序列严格递增的最少的调换次数,因此这也是一个求解最优值的问题。

3、无后效性原则:

该问题中,每一个子问题(例如n-1子系统)的状态可以影响下一级子问题(n子系统)的状态,但下一级(n)的状态却不能影响上一级(n-1)的结果。因此该问题满足无后效性原则

既然问题可以按照动态规划的思路来解决,我们不妨按照上问提到的设计步骤来逐步进行分析:

  1. 首先确定问题的最优解,本题要求以最小的调换次数来让两个序列都达到严格递增的效果,因此最优解其实就是使序列能够满足要求的最小次数。
  2. 通过对题目的分析,该问题具有子问题重叠性,因此我们就先求出每个子问题的状态,然后通过子问题的状态进而确定该问题的最有状态。由于我们求解n个数据的最优值时,需要调用n-1个数据的最优值;并且求解n-1个数据时要用到n-2个数据的最优值,因此需要递归定义子系统最优值。
  3. 第2步分析问题时,我们采用的是从顶向下的顺序来分析并且递归定义最优值,但对于该问题的求解,如果要确定n个数据的最优解时,我们必然需要先确定n-1个数据的最优解以备后面计算使用。因此,在计算最优值时,我们采用自底向上的顺序来依次确定子系统的最优值。
  4. 针对不同的个数的系统求解最优值,我们需要构造一个通用的表达式来表示所有子问题的最优值。因此这一步需要通过前面构造每一级最优值时的规律来构造最优解。

题目要求中,两个序列在调换对应数据后最终都可以实现各自严格递增,所以给定的数据序列所能满足可以实现数据调换的条件有两种:第一种是A[i-1]<A[i]并且B[i-1]<B[i],此时数据由于本来就是递增,所以i和i-1两组数据可以选择都调换也可以选择都不调换;第二种就是A[i]>B[i-1]并且B[i]>A[i-1],此时需要调换一个数据来实现两组数据递增。

第一种情况很好理解,当满足A[i-1]<A[i]并且B[i-1]<B[i]的条件时,此时两个数据i以及i-1均可以调换。第二种情况比较复杂,如图所示,假设A和B两个数据序列我们用图示的两个线段来表示分别含有两个成员变量的A和B数组,其中线段的长度代表数组成员的大小。由于题目要求最终两个数组都是严格递增的,所以按照这个要求,图中的两组线段在交换之后应该达到的状态是左边线段高度低于右边的线段。所以针对线段A[i-1],当右边的线段A[i]不高于线段A[i-1]时,必然存在线段B[i]高于线段A[i-1],才可以满足题目要求。

动态规划总结

否则,当线段A[i]低于左边的线段A[i-1]时,如果线段B[i]也低于线段A[i-1],此时情形如下图所示,即使交换之后数据序列B中A[i]高于B[i-1],但数据序列A中B[i]是比A[i-1]要低的。所以,最终不管如何调换数据,都是不能满足两个数据序列均保持严格递增。

动态规划总结

因此,在该题目要求前提下,数据序列中如果A[i]不大于A[i-1]时,必然有B[i]大于A[i-1]。同理可证,在B[i]不大于B[i-1]时,必然存在A[i]大于B[i-1]。并且当A[i]大于B[i-1]时,B[i]也需要大于A[i-1],因此我们将该题拆分成这两种情况来分析。下面我们详细分析在这两种情况下需要交换数据的方式。

  1. 当A[i-1]<A[i]并且B[i-1]<B[i]时,两组数据均调换或者是两组数据均不调换;
  2. 当A[i]>B[i-1]并且B[i]>A[i-1]时,A[i]与B[i]调换,A[i-1]与B[i-1]不变或者是A[i]与B[i]不变,A[i-1]与B[i-1]交换。

针对以上两种情况,我们可以给出表达方程,基于动态规划中对于所有阶段的结果学术上称之为状态,同样,这个表达式也有一个更高端的名字叫做状态转移方程。在状态转移方程中,我们将数据调换的结果表达为两种状态:一种是第i个数据不换时两个序列为递增序列所需要的调换次数,我们称为nature[i]。虽然第i个数据不调换,但前i-1个数据可能会有交换的情况存在,所以这个nature[i]不一定为0;另一种是第i个数据调换后两个序列严格递增所需要的次数,我们称为swap[i]。

根据动态规划的基本思想,我们需要对该问题进行子问题重叠的思考,找出第i个状态与前序状态的关系。根据上述分析,得出的状态转移方程如下。

情景1:A[i-1]<A[i]并且B[i-1]<B[i]

如果第i个数据不交换的话,那么第i-1个数据也不用交换,此时它与第i-1次不交换的状态是一致的,即交换次数没有增加,因此

                            NatureCnt[i] = NatureCnt[i-1];                                                       公式1

如果第i个数据交换的话,那么第i-1个数据必然也需要交换,因此它比第i-1个数据交换的状态多一次交换,状态方程表示为:

                            SwapCnt[i] = SwapCnt[i-1] + 1;                                                     公式2              

情景2:A[i]>B[i-1]并且B[i]>A[i-1]

该种情景下状态比较复杂,因为第i个数据交换与否和前序的状态都可能有关系。假如第i个数据保持不变的话,第i-1个数据可以交换也可以保持原位,而当第i-1个数据保持原位时,该种情形和情景1完全一致,所以状态方程如公式1所示;当第i-1个数据进行交换时,其状态转移方程写为:

                            NatureCnt[i] = SwapCnt[i-1];                                                         公式3

同样的,第i个数据交换时也存在两种状态方程:即第i-1交换时,状态转移方程和公式2完全相同,当第i-1个数据不进行交换时,第i个数据交换的状态方程为:

                            SwapCnt[i] =NatureCnt[i-1]+1;                                                      公式4

如果某些数据序列既满足情景1,又满足情景2,例如数组A[2]={1,5},B[2]= {2,4}我们将会分别通过公式1和公式3得到两个NatureCnt[i]的值;同样,通过公式2和4,我们可以得到两个SwapCnt[i]的值。由于题目要求最优解是最小的交换次数,所以我们调用库函数min来实现最小次数的求解,NatureCnt[i]和SwapCnt[i]可以分别表示为:

                            NatureCnt[i] = min(NatureCnt[i],SwapCnt[i-1]);                             公式5

                            SwapCnt[i] = min(SwapCnt[i],NatureCnt[i-1]+1);                           公式6

当然,大多数情况下,数组的关系并不都会两个情景都满足,所以当我们碰到两个数组只满足情景2的时候,此时的NatureCnt[i]是没有初值进行初始化的,因此我们需要给其一个极大值的初始值,来保证函数min可以正常运行。

根据以上分析,我们可以对程序进行编码。按照第3个设计步骤要求的自底向上计算最优值,因此我们可以从0开始依次递增求解n个数据的最优状态。由于当数组成员变量数量为0时不需要变换数据,而且上述推导公式利用到了第i-1个数据的状态,因此我们赋值初始值SwapCnt[0]和NatureCnt[0]均为0,将状态计算从1开始,代码编程如下。

         for(i = 1; i < n; i++)

         {

                   NatureCnt[i] = 9999999999;   //give the maxmum if AB don't reach condition 1,

                   SwapCnt[i] = 99999999999;    //used in condition 2

                   if(A[i] > A[i-1] && B[i]>B[i-1])//condition 1

                   {

                            NatureCnt[i] = NatureCnt[i-1];

                            SwapCnt[i] = SwapCnt[i-1] + 1;

                   }

                   if(A[i] > B[i-1] && B[i]>A[i-1])//condition 2

                   {

                            NatureCnt[i] = min(NatureCnt[i],SwapCnt[i-1]);

                            SwapCnt[i] = min(SwapCnt[i],NatureCnt[i-1]+1);

                   }

         }

通过上面的例子我们可以看出,动态规划解决问题的思路大致都是将问题拆分成若干个相互关联的小问题。例如上述例程中,n个数据经过调换后递增所需要的次数是与n-1个数据调换后递增所需要的次数相关联的。当然还会存在一些过程是没有这么直接明显的相关关系的,所以在确定子问题的相互联系时,需要一定的技巧。但处理效果还是挺明显的,虽然占用了一些空间来记录每一个子问题的状态,但求解整个过程时分别调用子问题的状态大大降低了处理的时间复杂度。

本文的初衷在于对近期学习使用动态规划算法的心得体会进行总结并且与各位分享。本人由最初对动态规划毫无概念的小白到现在能够自如使用动态规划解决问题并且稍微有一些自己的心得体会。期间上网查阅了大量资料并且对于博文中提到的经典例题先尝试自己解决,再看答案分析,加深动态规划使用方法的印象。当然最初针对例题,自己的解决方案完全和动态规划沾不上边,后来某天突然理解了“状态”的含义,才开始对动态规划初窥门径。动态规划里的状态其实指的就是阶段的最优解,如上述例程中最小的调换次数。

学习动态规划期间翻阅的博文中,其中CSDN网站中博主“英雄哪里出来”的“夜深人静看算法”系列文章印象最为深刻,在此向各位推荐。当然其他还有很多博文也都挺不错,给了我很多的提示启发,在此不一一列举,文章附录会将博文链接列举出来。最后向提供动态规划心得体会的前辈们表示衷心的感谢。

附录

夜深人静看算法:https://blog.csdn.net/whereisherofrom/article/details/78922215

01背包问题:https://www.cnblogs.com/Christal-R/p/Dynamic_programming.html

动态规划算法:https://www.cnblogs.com/chuninggao/p/7295793.html

理解动态规划:https://www.zhihu.com/question/39948290

动态规划:http://blog.jobbole.com/83949/

漫话动态规划:https://www.sohu.com/a/153858619_466939

动态规划:http://www.hawstein.com/posts/dp-novice-to-advanced.html

动态规划分析:https://blog.csdn.net/yuxin6866/article/details/52507623

动态规划:https://www.cnblogs.com/raichen/p/5772056.html

.csdn.net/whereisherofrom/article/details/78922215

01背包问题:https://www.cnblogs.com/Christal-R/p/Dynamic_programming.html

动态规划算法:https://www.cnblogs.com/chuninggao/p/7295793.html

理解动态规划:https://www.zhihu.com/question/39948290

动态规划:http://blog.jobbole.com/83949/

漫话动态规划:https://www.sohu.com/a/153858619_466939

动态规划:http://www.hawstein.com/posts/dp-novice-to-advanced.html

动态规划分析:https://blog.csdn.net/yuxin6866/article/details/52507623

动态规划:https://www.cnblogs.com/raichen/p/5772056.html

相关文章: