【24K算法系列001】动态规划算法

334 阅读3分钟

    今天我们通过动态规划的概念、过程、适用条件、实例解析四个部分来充分认识下动态规划。

一、动态规划的概念

  • 问题分解

    通过把原问题分解为相对简单的子问题,当我们求解出这些子问题的答案,原问答案便解出。  

  • 答案复用

     在求解子问题的过程中,需要把会重复计算的子问题的答案缓存记录下来(如放在数组),下次遇到同样的子问题需要计算,直接查询出结果即可。

二、动态规划的过程

  1. 建立状态转移方程

        核心思想:当已经知道f(1)~ f(n- 1)的值,想办法利用它们求得f(n)。

        也就是把求原问题的解,转换为求若干子问题的解。

  1. 缓存并复用以往结果

        这一步不难,但很重要。如果没有合适地处理,很有可能就是指数和线性时间复杂度的区别。

  1. 按顺序从小往大算

        这里的“小和“大”对应的是问题的规模,在这里也就是我们要从f(0), f(1).到f(n)依次顺序计算。

三、动态规划适用条件

1.最优化原理(最优子结构性质

        各子问题具有最优解,就能求出原问题的最优解,这样我们就说问题满足最优化原理又称其具有最优子结构性质。

2.无后向性

        以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。

        举例,求解f(5)需要知道f(1)、f(2)、f(2)、f(3)、f(4)。假如状态转移方程是f(n) = f(n-1) + 2, 那么f(5) = f(4) + 2。也就是说f(5)只与f(4)有关,或者说f(3)只能影响f(4)。只能通过当前的f(4)来影响f(5)的结果。

3.子问题的重叠性

        重复的子问题就可以直接复用缓存的结果。用缓存空间换取计算的时间。

        这个性质并不是动态规划适用的必要条件,但是如果该性质无法满足,动态规划算法同其他算法相比就不具备优势。

四、动态规划举例

        我们拿大家都熟悉的斐波那契数列为例用动态规划实现。

        斐波那契数列遵循动态规划适用条件。

   

private int fib(int n) {
    // 定义数组存储计算结果
    int[] dp = new int[n+1];
    // 基础数据
    dp[0] = 0;
    dp[1] = 1;
    // 循环求解
    for (int i = 2; i <= n; i++){
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
}

   

        我们来分析以上代码是如何用三步走来完成动态规划。

        第一,我们建立状态转移方程f(n)= f(n-1)+ f(n- 2)。

        第二,缓存并复用以往结果。在线性规划解法中,我们把结果缓存在dp数组,同时在 dp[i] = dp[i - 1] + dp[i - 2] 中进行了复用。

        第三,按顺序从小往大算。for循环实现了从0到n的顺序求解,让问题按着从小规模往大规模求解的顺序走。

我的微信公众号