前言:本来是给我朋友小达人写的,比写作文还认真,这里分享给大家,有不足的地方请多多指教。
动态规划(DP,Dynamic Programming)
我认为动态规划是所有算法中最最重要的,也是最最难的。在我看来它也是真正意义上省时间一种算法,除此之外的其它算法我姑且称之为暴力算法。它主要是用空间来换取时间,这是很值的,因为它将指数级别的时间复杂度直接降为多项式级别。这个寒假搞懂太值了。
定义
动态规划的核心思想就是把原问题分解成若干个子问题进行求解,其利用的就是分治法的思想。但与分治法不同的是,分治法是把原问题分解成若干个子问题,分别解决各个子问题来组合成原问题的解,子问题之间没有相互的关系,是相互独立的。而动态规划中的子问题之间是有联系的,有重叠子问题,根据子问题的解来一步一步地递推出原问题的解,注意是递推而不是分治法的简单的组合。
动态规划问题的特点
先来看个栗子吧。
最长公共子序列问题(LCS)。
子序列是指一串序列中选出几个元素按顺序组成的序列,如because,其中eau、bs、case就是它的一部分子序列,共有2^7-1个,因为为空不算。那么两串序列的最长公共子序列就很好理解了,比如序列A为abcdefghijk和序列B为aahcdekfg,则A和B的最长公共子序列就是acdefg。先假设一般A串为a1,a2,a2,a4…ai,共i个元素(字母),B串为b1,b2,b3…bj,共j个元素。求A和B的最长公共子序列的长度。
分析
先设一个结构C[i,j],它是一个二维数组,你可以把它看出等一张二位表格。前面说过动态规划是用空间来换时间的,而且动态规划的子问题具有重叠性,这张表就是动态规划中所使用的记录重叠子问题的数据结构。接下来的所有操作都是维护这张表,并从这张表中推断出最优解。C[i,j]中i表是A中下标为1i的字串,j表示B中下表为1j的字串,C[i,j]表示a1,a2,a3…ai和b1,b2,b3…bj的最长公共子序列的长度。
1.当i=0或j=0时,有一个串为空,所以C[i,j]=0。
3.当ai=bj时,那么ai就是最长公共子序列中的元素,而且是最后一个,所以最长公共子序列的长度是C[i-1,j-1]+1。
3.当ai!=bj时,那么最长公共子序列就是C[i-1,j]和C[i,j-1]中的最大一个,所以最长公共子序列就是Max(C[i-1,j],C[i,j-1])。
Nice,递推关系式(别人叫状态转移方程,太高大上了,还听不懂,其实就是高中学的递推和分类讨论)找到了。
0 i=0或j=0
C[i,j] = C[i-1,j-1]+1 ai=bj
Max(C[i-1,j],C[i,j-1]) ai!=bj
实现
动态规划的实现方式有两种:自底向上和自顶向下。 这里我们先使用最简单容易理解的自顶向下,也叫做备忘录算法,一般使用递归的形式,所以时间上不如自底向上,这里的备忘录就是刚才的那个二维表C,至于为什么叫备忘录待会儿就显而易见了。
递归肯定先定义一个函数了,我们定义函数为fun(i,j),意义为a1,a2…ai和b1,b2…bj的最长公共子序列的长度,根据我们之前推推导的递推关系式就很容易写出函数体。
//我们先把C那张二维表初始化数据都为-1
public int fun(i,j){
if(i==0||j==0){
//递归的三要素的出口,i=0或j=0。
C[i][j]=0;
return C[i][j];
}
else if(C[i][j]!=-1){
//如果之前C表中有计算过的数据,就直接使用,免去重复计算,也就是重叠子问题,这就叫备忘录。
return C[i][j];
}
else if(a[i]==b[j]){
//如果ai=bj。
C[i][j]=fun(i-1,j-1)+1;
return C[i][j];
}
else{