上一篇,是关于最长回文子串的Manacher算法的详解,这篇,我们进入动态规划的世界。
https://blog.csdn.net/u013309870/article/details/75193592
动态规划 Dynamic Programming:
下面一句话 和 一段对话就能说明动态规划的本质:记住已经解决过的子问题的解
那些记不住过去的人注定要重蹈覆辙
A * "1+1+1+1+1+1+1+1 =?" *
A : "上面等式的值是多少"
B : *计算* "8!"
A *在上面等式的左边写上 "1+" *
A : "此时等式的值为多少"
B : *quickly* "9!"
A : "你怎么这么快就知道答案了"
A : "只要在8的基础上加1就行了"
A : "所以你不用重新计算因为你记住了第一个等式的值为8!动态规划算法也可以说是 '记住求过的解来节省时间'"
只要在8的基础上再+1就知道答案了。
求解的方式有两种:①自顶向下的备忘录法 ②自底向上。
下面我将通过例子,尽可能以最清晰的方式将DP问题解释清楚,读者若跟着我的思路走,必将把DP安排的明明白白。
从Fibonacci讲起:
在牛客网刷剑指offer时,一定会遇到几个问题,Fibonacci,跳台阶,变态跳台阶等等,用的是递归解法,我们来看看递归方式到底如何转变为DP解法。
fib: 1 1 2 3 5 8 13 21 34....
index: 0 1 2 3 4 5 6 7 8....
def fibonacci(n):
if n < 0:
return False
if n == 0:
return 1
if n == 1:
return 1
return fibonacci(n-1) + fibonacci(n-2)
咱们分析一下上面递归解法的问题。下面图希望读者手动画一下,加深记忆:
由图可以看出,出现了大量的重复计算,这才只到6就这样了,想想n如果很大,会有多少重复计算!
这时候大多数人一定会想到:记住中间结果啊,那样就可以避免重复计算已经计算过的值啦,这种思路,就对应我们第一种动态规划方法:
1. 自顶向下的备忘录法:
def fibonacci_memery(n):
if n <= 0:
return n
memery_arr = [-1] * len(n+1)
return fibonacci(n, memery_arr)
def fibonacci(n, memery_arr):
if memery_arr[n] != -1:
return memery_arr[n]
if n <= 2:
mermery_arr[n] = 1
else:
memery_arr = fibonacci(n-1, memery_arr) + fibonacci(n-2, memery_arr)
return memery_arr[n]
备忘录法也是比较好理解的,创建了一个n+1大小的数组来保存求出的斐波拉契数列中的每一个值,在递归的时候如果发现前面fib(n)的值计算出来了就不再计算,如果未计算出来,则计算出来后保存在Memo数组中,下次在调用fib(n)的时候就不会重新递归了。比如上面的递归树中在计算fib(6)的时候先计算fib(5),调用fib(5)算出了fib(4)后,fib(6)再调用fib(4)就不会在递归fib(4)的子树了,因为fib(4)的值已经保存在Memo[4]中。
2. 自底向上的动态规划
不知大家是否看出来,上面的备忘录法虽然是由一个数组保存了中间结果,但是我们计算fibonacci(6)的时候也还是要计算出前面的1,2,3..,我们为什么不能先计算出来1,2,3..呢?
这才是动态规划的核心!!!先计算出来子问题,由子问题来计算出最终的父问题。
def fibonacci(n):
if n <= 0:
return n
memery_arr = []
memery_arr[0] = 1
memery_arr[1] = 1
for i in range(2, n+1):
memery_arr[i] = memery_arr[i-1] + memery_arr[i-2]
return memery_arr[n]
自底向上算法也是利用了数组保存先计算的值,为了后面的调用服务,上面代码中参与循环的只有i,i-1,i-2三项,我们可以降低辅助数组的空间复杂度。
def fibonacci(n):
if n <= 0:
return n
# 好好体会一下下面代码
memo_i_2 = 0
memo_i_1 = 1
memo_i = 1
for i in range(2, n+1):
memeo_i = memo_i_2 + memo_i_1
memo_i_2 = memo_i_1
memo_i_1 = memo_i
return memo_i
一般来说备忘录方式的DP使用了递归,递归就要产生额外的开销,使用自底向上的DP方法要比备忘录法好!
你以为你懂了DP?too young too simple!
菜鸟级:钢条切割问题:
也就是收益最大问题!
下图进行了分析,如何找到收益最佳的解!(是不是划分了所有的子问题,子问题的最优解构成了原问题的最优解--DP)
最优子结构:
问题的最优解由相关子问题的最优解组合而成!而这些子问题也是可以独立求解的!
其实除了上述求解子问题最优解的方法之外,还有更容易理解的:递归
我们将钢条从最左边割下长度为i的一段,只对右边n-i的一段继续进行切割(调用自身,递归求解),对左边的一段不再进行切割。问题的分解方式为:将长度为n的钢条分解为左边开始一段以及剩余部分继续分解的结果。