动态规划学习笔记
初次接触动态规划,从一个简单例子入手
概念
官方概念晦涩难懂,这里不做描述,有兴趣的可以去看《运筹学》动态规划相关章节。
简单来说就是:给定一个问题,将这个问题拆分成若干个有关联的可以直接解决的子问题
,同时把子问题的答案记录下来
,减少重复计算
,最后,根据子问题的答案,推导出原问题的答案的一种方法。
子问题间的关联一般可以通过函数关系式递推出来,这个地推关系式称为动态规划的基本方程
。
基本思想(重点)
- 拆分成有关联的子问题
- 找出递推关系式并确定边界条件
- 保存历史结果,减少重复计算
举例说明
leetcode原题:《爬楼梯》
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
分析:
要爬到楼顶,会出现两种情况
- 最后爬1个台阶到楼顶
- 最后爬2个台阶到楼顶
假设要爬10阶到达楼顶
- 要么从第9阶爬到楼顶;要么从第8阶爬到楼顶;
即:f(10) = f(9) + f(8)
- 要到达第9阶,要么从第8阶爬到第9阶;要么从第7阶爬到第9阶;
即:f(9) = f(8) + f(7)
- …
- 依此类推;
f(3) = f(2) + f(1)
得出递推关系式:f(n) = f(n - 1) + f(n - 2)
当只有2个台阶或者1个台阶的时候,单独列出:f(2) = 2; f(1) = 1
;这就是边界条件
。
递归求解
1 | function climbStairs(n) { |
是不是和《斐波那契数列》一样?
当你在leetcode提交代码时,你会发现:
这是为什么呢?
从前面的分析:
- 要计算f(10), 先要计算出f(9)和f(8);
- 要计算f(9), 先要计算出f(8)和f(7);
- 要计算f(8), 先要计算出f(7)和f(6);
- …
- 要计算f(3),先要计算f(2)和f(1);
得到如下树状图:
递归的时间复杂度 = 递归的次数 * 每次递归的时间复杂度
前面的每个子问题的复杂度 = f(n - 1) + f(n - 2),加法操作,为常数型,即为 O(1)。
问题的个数 = 递归的节点总数。由上面树状图可以知道 sum = 1 + 2 + 4 + 8 + ...
是一个等比数列,由等比数列求和公式,得出节点总数为 2^n - 1,所以时间复杂度为 O(2^n)。
所以,前面递归解法的时间复杂度 = O(1) * O(2^n) = O(2^n),是指数型的,当n变大时,很快就超时了。
由树状图可以看出:f(8)被计算了2次,f(7)被计算了3次,f(6)被计算了4次…,存在大量的重复计算,所以导致算法低效。
这时,我们需要把计算过的结果保存下来直接使用,减少重复计算
1 | let temp = {}; // 来存储计算过的结果 |
再去leetcode提交代码时,就不会超时了。
递归通常是从f(n)到f(1)的方向求解,通常被称为自顶向下
的解法
动态规划求解
动态规划一般是从f(1)到f(n)的方向求解,通常被称为
自底向上
的解法
由递推公式 f(n) = f(n - 1) + f(n - 2)
可以知道,f(n)的结果只和他前面两个数相关
,所以用两个变量 a 和 b 来存储,时间复杂度为 O(1)。
f(1) | f(2) | f(3) | f(4) | … | f(10) |
---|---|---|---|---|---|
a = 1 | b = 2 | a = 1 + 2 = 3 | b = 3 + 2 = 5 | … | b = a + b |
代码如下:
1 | var climbStairs = function(n) { |
看完上面的例子,对动态规划有不有一定的认识了呢?
来多刷几道题巩固下:leetcode动态规划相关题目
感谢你的阅读。