python算法入门:动态规划

一、什么是动态规划?

动态规划(Dynamic Programming)是一个非常经典的算法,它的核心思想很简单:

把一个大问题拆解成已知解的子问题。

怎么说呢?举一个简单的例子吧:

python算法入门:动态规划

斐波那契数列: 1, 1, 2, 3, 5, 7, 12, 19, 31 ...

即:第一个数和第二个数都是1,接下来的每一个数都是前两个数的和。

也即:

<code>f(1) = f(2) = 1
f(n) = f(n-1) + f(n-2)/<code>

例如:

<code>f(1) = 1

f(2) = 1

f(3) = f(1) + f(2) = 1+ 1 = 2

f(4) = f(3) + f(2) = 2 + 1 = 3

...

f(n) = f(n-1) + f(n-2)/<code>

假设我们想要求f(4)的值,如果我们已经之前已经计算过f(3)和f(2)就可以直接给出f(4) = f(3) + f(2) = 2 + 1 = 3,而不需要从f(1) = f(2) = 1来一步步推算到第四个数了。


有这样一句话非常生动的说明了动态规划的核心:

不能记住历史,就注定会重复历史。

python算法入门:动态规划

二、爬楼梯问题


python算法入门:动态规划

再来看看经典的爬楼梯问题:

<code>有一个楼梯,一共有n个台阶。
每次你可以上一个或者两个台阶,那么爬到楼梯顶有多少中不同的爬法呢?

注意:n是一个正整数


例子一:
输入:2个台阶
输出:2种爬法
解释:两种爬法分别为:
1. 爬一个台阶 + 爬一个台阶
2. 爬两个台阶

例子二:
输入:3个台阶
输出:3种爬法
解释:三种爬法分别为:
1. 爬一个台阶 + 爬一个台阶 + 爬一个台阶
2. 爬一个台阶 + 爬两个台阶
3. 爬两个台阶 + 爬一个台阶/<code>

大家先思考一下怎么解决这个问题?

python算法入门:动态规划

好了,有想法了吗?如果没有思路的话,联想一下一开始说的动态规划:把一个大问题拆解成已知解的子问题。

这里要反直觉地假设一下,如果我现在已经站在楼梯顶(第n个台阶),那我是怎么上来的呢?

由于我只能一次跨一个或者两个台阶,所以我上一步必然在第n-1个台阶或者第n-2个台阶上,那么我爬到楼梯顶的爬法就是爬到第n-1个台阶的爬法+爬到第n-2个台阶的爬法之和,也就是:

<code>f(n) = f(n-1) + f(n-2)/<code>

且一阶台阶的爬法只有1种,二阶台阶的爬法只有2种(1+1或者2),即:

<code>f(1) = 1
f(2) = 2/<code>

用python代码实现就是:

<code>class Solution:
def climbStairs(self, n: int) -> int:
if n == 1:
return 1
elif n == 2:
return 2
return self.climbStairs(n-1) + self.climbStairs(n-2)/<code>

是不是很简单?!

是的,以上就是爬梯子问题使用了递归(Recursion)方法的暴力解法(Brute Force)。在不考虑时间复杂度和空间复杂度的情况下,能够解决问题。


三、给递归加上记忆(Memoization)

上面的那个解法就是最基本的递归解法。

<code>递归:一个函数在内部调用自身/<code>

基本的递归解法确实能够解决问题,但是递归解法的问题也十分明显:在n数值大的时候,时间复杂度和空间复杂度非常之大(分别为O(2^n)和O(n)),对于较大的n,甚至无法在给定时间内解出。让我们看看当n等于5时的递归调用树:

python算法入门:动态规划

<code>f(5)  
= f(4) + f(3)
= f(3) + f(2) + f(2) + f(1)
= f(2) + f(1) + f(2) + f(2) + f(2) + f(1)/<code>

f函数的计算次数实在是太多。

那如何改进呢?

其实很简单,我们都可以看到对于相同的值,并不需要计算多次,比如说上图中对于f(1) , f(2), f(3)的结果,只需要在第一次计算后保存下来,下一次需要的时候直接查询就可以大大减少计算量。

用python代码实现就是:

<code>class Solution:
res = {}

def climbStairs(self, n: int) -> int:
if n == 1:
self.res[1] = 1
return 1
elif n == 2:
self.res[2] = 2
return 2
res_val = self.res.get(n) or self.climbStairs(n-1) + self.climbStairs(n-2)
self.res[n] = res_val
return res_val/<code>

以上解法的时间复杂度和空间复杂度可以降低到:O(n) 和 O(n)


四、动态规划

上面的递归解法的思路是自顶向下,先算f(5), 再算f(4), f(3), 再算f(2), f(1)。

那我们能不能利用动态规划的思路,先算小问题,最后算出大问题,自底向上地算出最终解呢

python算法入门:动态规划

当然可以:

<code>已知:
f(1) = 1, f(2) = 2
可知:
f(3) = f(2) + f(1) = 2 + 1 = 3
f(4) = f(3) + f(2) = 3 + 2 = 5
.../<code>

用python代码实现就是:

<code>class Solution:
def climbStairs(self, n: int) -> int:
if n == 1:
return 1
elif n == 2:
return 2
res_n_1 = 2
res_n_2 = 1
res_n = None
for i in range(n-2):
res_n = res_n_1 + res_n_2
res_n_2 = res_n_1
res_n_1 = res_n
return res_n/<code>

时间复杂度和空间复杂度分别为:O(n) 和 O(n)


五、后续

到这里,有没有觉得动态规划其实很简单呢?

只要掌握了正确了学习方法,加上持之以恒的学习训练,算法并不难~

上面的爬楼梯问题其实还有性能更优化的方案,你能想到吗?



我是零度橙子,5年以上python经验,软件工程师,科技达人,谷歌认证云计算架构师,AWS认证devops专家,欢迎大家关注我,了解有用有趣的科技知识~


分享到:


相關文章: