动态规划法(一)从斐波那契数列谈起

动态规划法与分治方法

  动态规划(Dynamic Programming)与分治方法相似,都是通过组合子问题的解来求解原问题。不同的是,分治方法通常将问题划分为互不相交子问题递归地求解子问题,再讲它们的解组合起来,求出原问题的解。而动态规划应用于子问题重叠的情况,即不用的子问题具有公共的子子问题。在这种情况下,如果采用分治算法,则分治算法会做许多不必要的工作,它会反复地求解那些公共子子问题。对于动态规划法,它对每个子子问题只求解一次,将其保存在一个表格中,从而无需每次求解一个子子问题时都重新计算,避免了这种不必要的计算工作。
  也就是说,动态规划法与分治方法相比,是用空间来换时间,而时间上获得的效益是很客观的,这是一种典型的时空平衡(time-memory trade-off)的策略。通常,动态规划法用来求解最优化问题(optimization problem),如斐波那契数列求值问题,钢条切割问题,0-1背包问题,矩阵链乘法问题,最长公共子序列(LCS)问题,最优二叉搜索树问题等。
  一般情况下,动态规划算法的步骤如下:

  1. 刻画一个最优解的结构特征。
  2. 递归地定义最优解的值。
  3. 计算最优解的值,通常采用自底向上的方法。
  4. 利用计算出的信息构造一个最优解。

  接下来,我们将从斐波那契数列求值这个简单的例子入手,来分析动态规划法的具体步骤和优点。

斐波那契数列

  斐波那契数列记为{f(n)},其表达式如下:
        f(0)=0,f(1)=1,f(n)=f(n-1)+f(n-2),n>1.
  具体写出前几项,就是:0,1,1,2,3,5,8,13,21,34,55,89,144,233......
  接下来,我们将会采用递归法和动态规划法来求解该数列的第n项,即f(n)的值。

递归法求解

  首先,我们采用递归法来求解斐波那契数列的第n项f(n),其算法描述如下:

function fib(n)
    if n = 0 return 0
    if n = 1 return 1
    return fib(n − 1) + fib(n − 2)

分析上述伪代码,先是定义一个函数fib(n),用来计算斐波那契数列的第n项,当n>1时,它的返回值会调用函数fib(n-1)和fib(n-2).当n=5时,计算fib(5)的函数调用情况如下图所示:

image

在计算fib(5)时,fib(5)调用1次,fib(4)调用1次,fib(3)调用2次,fib(2)调用3次,fib(1)调用5次,fib(0)调用3次,一共调用函数fib()15次。由此,我们可以看到,在计算fib(5)时,存在多次重复的fib()函数的调用,当n增大时,重复调用的次数会急剧增加,如计算fib(50)时,fib(1)和fib(0)大约会被调用2.4*10^10次。由此可见,该算法的效率并不是很高,因为该算法的运行时间是指数时间。
  我们用Python实现上述算法,并计算f(38)的值及运算时间。Python代码如下:

import time

# recursive method
def rec_fib(n):
    if n <= 1:
        return n
    else:
         return rec_fib(n-1) + rec_fib(n-2)
    
# time cost of cursive method
t1 = time.time()
t = rec_fib(38)
t2 = time.time()

print('结果:%s, 运行时间:%s'%(t, t2-t1))

输出结果如下:

结果:39088169, 运行时间:22.93831205368042

动态规划法求解

  在使用递归法来求解斐波那契数列的第n项时,我们看到了递归法的不足之处,因为递归法在使用过程中存在大量重复的函数调用,因此,效率很差,运行时间为指数时间。为了解决递归法存在的问题,我们可以尝试动态规划法,因为动态规划法会在运行过程中,保存上一个子问题的解,从而避免了重复求解子问题。对于求解斐波那契数列的第n项,我们在使用动态规划法时,需要保存f(n-1)和f(n-2)的值,牺牲一点内存,但是可以显著地提升运行效率。
  动态规划法来求解斐波那契数列第n项的伪代码如下:

function fib(n)

    var previousFib := 0, currentFib := 1
    
    if n = 0
    return 0
    else if n = 1
    return 1
    
    repeat n−1 times
        var newFib := previousFib + currentFib
        previousFib := currentFib
        currentFib := newFib
        
    return currentFib

在上述伪代码中,并没有存在重复求解问题,只是在每次运行过程中,保存上两项的值,再利用公式f(n)=f(n-1)+f(n-2)来求解第n项的值。用Python实现上述过程,代码如下:

import time

# bottom up approach of Dynamic Programming
def dp_fib(n):
    previousFib = 0
    currentFib = 1
    if n <= 1:
        return n

    # repeat n-1 times
    for _ in range(n-1):
        newFib = previousFib + currentFib
        previousFib = currentFib
        currentFib = newFib

    return currentFib

# time cost of DP method
t1 = time.time()
t = dp_fib(38)
t2 = time.time()

print('结果:%s, 运行时间:%s'%(t, t2-t1))

输出结果如下:

结果:39088169, 运行时间:0.0

  显然,使用动态规划法来求解斐波那契数列第n项的运行效率是很高的,因为,该算法的时间复杂度为多项式时间。

参考文献

  1. 算法导论(第四版)
  2. https://www.cs.upc.edu/~jordicf/Teaching/programming/pdf/IP07_Recursion.pdf
  3. https://www.saylor.org/site/wp-content/uploads/2011/06/Dynamic-Programming.pdf

附录

用递归法和动态规划法来求解该数列的第n项,完整的Python代码如下:

# calculate nth item of Fibonacci Sequence
import time

# recursive method
def rec_fib(n):
    if n <= 1:
        return n
    else:
         return rec_fib(n-1) + rec_fib(n-2)

# bottom up approach of Dynamic Programming
def dp_fib(n):
    previousFib = 0
    currentFib = 1
    if n <= 1:
        return n

    # repeat n-1 times
    for _ in range(n-1):
        newFib = previousFib + currentFib
        previousFib = currentFib
        currentFib = newFib

    return currentFib

 # time cost of cursive method
t1 = time.time()
t = rec_fib(38)
t2 = time.time()
print('结果:%s, 运行时间:%s'%(t, t2-t1))
# time cose of DP method
s = dp_fib(38)
t3 = time.time()
print('结果:%s, 运行时间:%s'%(t, t3-t2))

输出结果如下:

结果:39088169, 运行时间:22.42628264427185
结果:39088169, 运行时间:0.0

完整的Java代码如下:

package DP_example;

import java.util.Date;
import java.math.BigInteger;

public class fib {
    // 主函数
    public static void main(String[] args) {
        Date start_time =  new Date(); //开始时间
        int n = 38;
        BigInteger t1 = DP_fib(n);  // 动态规划法求解
        Date end_time1 =  new Date(); // 结束时间
        Long cost_time1 = end_time1.getTime()-start_time.getTime();  // 计算时间,返回毫秒数
        System.out.println(String.format("The fib(%d) is %s.\nCost time is %.3fs.", n, t1, cost_time1*1.0/1000));


        BigInteger t2 = rec_fib(n);  // 递归法求解
        Date end_time2 =  new Date(); // 结束时间
        Long cost_time2 = end_time2.getTime()-end_time1.getTime();  // 计算时间,返回毫秒数
        System.out.println(String.format("The fib(%d) is %s.\nCost time is %.3fs.", n, t2, cost_time2*1.0/1000));

    }

    // 利用递归方法计算斐波那契数列的第n项
    public static BigInteger rec_fib(int n){
        if(n == 0)
            return BigInteger.ZERO;
        if(n ==1)
            return BigInteger.ONE;
        else
            return rec_fib(n-1).add(rec_fib(n-2));
    }

    // 利用动态规划法(DP)计算斐波那契数列的第n项
    public static BigInteger DP_fib(int n){
        if(n == 0)
            return BigInteger.ZERO;
        if(n == 1)
            return BigInteger.ONE;
        else {
            BigInteger previousFib = BigInteger.ZERO;
            BigInteger currentFib = BigInteger.ONE;
            BigInteger newFib;

            for(int i=1; i<n; i++){ // 重复循环n-1次
                newFib =  previousFib.add(currentFib);
                previousFib = currentFib;
                currentFib = newFib;
            }

            return currentFib;
        }
    }
}

输出结果如下:

The fib(38) is 39088169.
Cost time is 0.001s.
The fib(38) is 39088169.
Cost time is 2.172s.

注意:本人现已开通两个微信公众号: 用Python做数学(微信号为:python_math)以及轻松学会Python爬虫(微信号为:easy_web_scrape), 欢迎大家关注哦~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,039评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,223评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,916评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,009评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,030评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,011评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,934评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,754评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,202评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,433评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,590评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,321评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,917评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,568评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,738评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,583评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,482评论 2 352

推荐阅读更多精彩内容