今天我们通过动态规划的概念、过程、适用条件、实例解析四个部分来充分认识下动态规划。
一、动态规划的概念
- 问题分解
通过把原问题分解为相对简单的子问题,当我们求解出这些子问题的答案,原问答案便解出。
- 答案复用
在求解子问题的过程中,需要把会重复计算的子问题的答案缓存记录下来(如放在数组),下次遇到同样的子问题需要计算,直接查询出结果即可。
二、动态规划的过程
- 建立状态转移方程
核心思想:当已经知道f(1)~ f(n- 1)的值,想办法利用它们求得f(n)。
也就是把求原问题的解,转换为求若干子问题的解。
- 缓存并复用以往结果
这一步不难,但很重要。如果没有合适地处理,很有可能就是指数和线性时间复杂度的区别。
- 按顺序从小往大算
这里的“小和“大”对应的是问题的规模,在这里也就是我们要从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的顺序求解,让问题按着从小规模往大规模求解的顺序走。