动态规划 (一)

《动态规划 (一)》

爬楼梯问题

问题描述
现在总共有10层台阶,一只青蛙一次只能跳一阶或两阶。问总共有多少种跳法?

  分析一波: 青蛙即将一步到达第10阶前只有两种情况,即:1.从第9阶一阶跳上去;2.从第8阶两阶跳上去。如果用 F(n)表示青蛙跳到第n阶所需要的步数,则有:

F(1) = 1 , 当 n = 1 时
F(2) = 2 , 当 n = 2 时
F(n) = F(n - 1) + F(n - 2),  当 n > 2 时

所以, 二话不说先来一发递归:

 public static int numsOfStairs(int n) {
        if (n == 1) {
            return 1;
        } else if (n == 2) {
            return 2;
        } else if (n > 2) {
            return numsOfStairs(n - 1) + numsOfStairs(n - 2);
        } else {
            return 0;
        }
    }

我将初始层数设置为40,总共耗时328毫秒。如果设置为100层…反正我没等我电脑把结果跑完…执行结果如下:

《动态规划 (一)》

  都说递归慢…递归慢,递归为何慢?为啥还要使用递归呢?首先,为什么要是用递归,这很容易去解释,因为它语句简洁、逻辑表达清晰,写出来更能让人去理解,一目了然。那为什么执行地慢?是因为如果递归层数比较深,需要增加额外的堆栈去处理,这样很容易造成 堆栈溢出!!对于递归的每一层,都需要堆栈去存储调用参数和中间变量,所以会增加很多额外的系统开销。

  那有没有办法既能使代码简洁,又能让效率提升呢? 嗯确实是有的,可以采取一种叫做尾递归的方法。怎么去理解尾递归呢?我这里对其概念不做过多介绍,一句话总结什么是尾递归: 将当前计算结果作为参数传入下一层

改用尾递归的代码如下:

/**
     * 
     * @param n
     * @param init1 初始值1
     * @param init2 初始值2
     * @return
     */
    public static int numsOfStairs(int n, int init1, int init2) {
        if (n == 1) {
            return init1;
        } else if (n > 1) {
            return numsOfStairs(n - 1, init2, init1 + init2);
        } else {
            return 0;
        }
    }

再贴上初始值设为40时的测试结果:

《动态规划 (一)》

  执行速度是不是快了不少?正常的递归需要一层层向下进行递归,然后再向上返回结果。而尾递归只需要向下进行递归,每一层递归时都进行计算,最后再返回一次结果。 除此之外,尾递归以牺牲两个额外空间的方式,来减少了中间变量的存储和返回。这样一来, 不仅效率大大提升,同时也避免了内存溢出。

  那么,接下来就进入正题,引入动态规划算法。使用动态规划算法,可以让每个状态都只计算一次,将结果保存下来进行下一次的计算,同时无需堆栈来保存递归现场。

动态规划算法

  动态规划算法是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推或者分治的方式去解决。
  动态规划算法的基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。
  由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。(百度百科)

  因为每一次都有F(n) = F(n-1)+ F(n-2),因此我们需要用两个临时变量来存储中间结果。同样是爬楼梯问题,使用动态规划后的方案如下:

public static int numsOfStairs(int n) {
        int fn1 = 1, fn2 = 2; // 分别存储 f(n - 1) 和 f(n - 2)
        if (n < 3) {
            return n;
        } else if (n >= 3) {
            int sum = 0;
            for (int i = 3; i <= n; i++) {
                sum = fn1 + fn2;
                fn1 = fn2;
                fn2 = sum;
            }
            return sum;
        } else {
            return 0;
        }
    }
动态规划算法的适用条件:
  1. 最优化原理(最优子结构性质): 一个最优策略的子策略是最优的。一个问题满足最优化原理又称其具有最优子结构性质。
  2. 无后效性: 各阶段的状态都是对过去历史的总结,而只有现阶段状态能影响未来的决策,其他之前的状态都无法影响未来的决策。
  3. 子问题的重叠性: 动态规划将原来具有指数级时间复杂度的搜索算法改进成了具有多项式时间复杂度的算法。其中的关键在于解决冗余,这是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其它的算法。
    原文作者:JYGod丶
    原文地址: https://www.jianshu.com/p/3152ad6c2021
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞