动态规划详解(第一讲)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Neo_kh/article/details/79630547

第一讲内容来自《算法问题实战策略》【韩】具宗万著

重复子问题与制表

动态规划(DP,dynamic programming)和分治法具有类似的处理方式,都是先把问题分割成若干子问题,然后求出这些子问题的解,再利用这些解得到整个问题的最后答案。
不过,动态规划和分治法在分割子问题的方式上存在不同。DP中,有些子问题的计算结果会用于多个问题的求解过程(如果不理解参考下面的示例)。因此,在对子问题进行一次计算的情况下,重复利用其结果就能提高运算的速度。为了实现这种方法,需将子问题的解预先保存到内存中,保存的方法称为“制表”,能够重复利用两次以上的子问题称为重复子问题(overlapping subproblems)

典型示例

应用动态规划最有名的示例是二项式系数(binomial coefficient)的计算,二项式系数 ( n r ) 表示在n个互不相等的元素中无顺序的挑选出r个元素的方法的总数。二项式系数具有如下递归式:

( n r ) = ( n 1 r 1 ) + ( n 1 r )

利用此递归式就可以编写出对给定的n和r返回 ( n r ) 结果值的函数bino(n,r),如代码1-1所示:

//代码1-1
int bino(int n,int r){
//初始部分:n=r(选择完所有元素的情况)
//或r=0(没有可选元素的情况)
if(r==0||r==n)return 1;
return bino(n-1,r-1)+bino(n-1,r);

下图表示计算bino(4,2)过程中函数调用的流程。
去重前
值得注意的是,bino(2,1)将会被调用两次,因为计算bino(3,1)和bino(3,2)都需要先调用bino(2,1)。不仅如此,bino(1,0)和bino(1,1)也会调用两次。由此可见,有些子问题会被重复计算多次,并且,重复调用的次数会随着n和r的增大而呈几何级数增长。下表给出了计算bino(n,n/2)而调用的函数次数.

n 2 3 4 5 6 18 19 24 25
调用bino()的次数 3 5 11 19 39 97239 184755 5408311 10400599

那么有没有办法避开那些重复的计算呢?输入值n和r一定时,bino(n,r)的返回值也是一定的,利用该原理就可以去掉重复计算。
首先,定义一个缓存数组,查询有没有相应的结果值。如果有,就返回数组中的值,否则直接计算;计算结果先存储到数组中,然后再返回。这种“先定义保存函数结果值的空间,然后重复使用结果值”的优化方法称为制表(memorization)

    int cache[30][30];//初始化为-1
    int bino2(int n,int r){//我们用把经过优化的函数称为bino2
    //初始部分
    if(r==0||n==r)return 1;
    //如果不是-1,则说明这个值是之前已计算过的结果值,直接返回。
    if(cache[n][r]!=-1)
        return cache[n][r];
    //直接计算并保存到数组中。
    return cache[n][r]=bino2(n-1,r-1)+bino2(n-1,r);

去重后bino2()的调用的情况如下图:

这里写图片描述

适用制表方法的情况

有些函数的返回值只依赖于输入值,这种特性称为引用透明性(referential transparency);而有的函数不具备这种特性,因为函数的执行不仅依赖于输入值,而且会受到全局变量、输入文件、类的成员变量等诸多因素的影响。当然,制表的方法只适用于具有引用透明性的函数。

实现制表的范式

范式,即模板。对于一个算法,掌握一种范式是非常好的习惯。不必要求每个人都必须按照下面的范式编写,只是有一种适合自己的固定格式。

//把所有的数组元素初始化为-1
int cache[2500][2500];
//a和b是[0,2500)区间的整数
//返回值总是int类型的非负整数
int someObscureFunction(int a,int b){
//先处理初始化部分
if(...)return...;
//已求解过(a,b),直接返回解过的值
int& ret=cache[a][b];
if(ret!=-1)return ret;
//在此求解
...
return ret;
}
int main(){
//利用memset()初始化cache数组
    memset(cache,255,sizeof(cache));
    //小技巧:用memeset函数想赋值-1的话,写255


分析制表的时间复杂度

分析这种”分治思想”的算法的复杂度有一个简单的公式:

(子问题的个数)x(解一个子问题时循环语句的执行次数)

利用上述公式计算bino2()的时间复杂度:r的最大值是n,所以计算bino2(n,r)时能够分割的最大子问题个数是O( n 2 )(注意并不严格等于 n 2 )。求解子问题时,因为没有循环语句,所以时间复杂度为O(1),所以bino2()耗费的时间复杂度为O( n 2 )xO(1)=O( n 2 ).

猜你喜欢

转载自blog.csdn.net/Neo_kh/article/details/79630547