首先讲一点:动态规划(Dynamic Programming)的名字有些言过其实,“Programming{Programming}Programming”的意思不是“编程”,这里指的是“递推”的意思,没有高瞻远瞩的意思,也没有计划未来什么事情的意思。
这里主要讲四点:
- 递归+记忆化 -> 递推
- 状态的定义:opt[n], dp[n], fib[n]{opt[n],\ dp[n], \ fib[n]}opt[n], dp[n], fib[n]
- 状态转移方程:opt[n]=best_of(opt[n−1], opt[n−2], ...){opt[n]=best\_of (opt[n-1], \ opt[n-2],\ ...)}opt[n]=best_of(opt[n−1], opt[n−2], ...)
- 最优子结构
例子:斐波那契数列
0, 1, 1, 2, 3, 5, 8, 13, 21, ...{0,\ 1,\ 1,\ 2,\ 3,\ 5,\ 8,\ 13,\ 21,\ ...}0, 1, 1, 2, 3, 5, 8, 13, 21, ...
递推公式:F[n]=F[n−1]+F[n−2]{F[n]=F[n-1]+F[n-2]}F[n]=F[n−1]+F[n−2]
先看上面这张图的左上角是斐波那契数列的递推公式;右上角是代码,不过这个代码写的并不简洁,可以改写为:
def fib(n):
return n if n <= 1 else fib(n-1)+fib(n-2)
上面是斐波那契数列最普通的递归算法。递归加上记忆化 就是递推,看下图:
这幅图跟第一个图的区别在哪儿?区别在右上角的代码部分,其中增加了“记忆代码(即,memo数组)”,求解顺序也就变了,变成从下往上推(从fib(0)往上推fib(6))。
所以说:递归+记忆化 ==》递推;递推公式:F[n]=F[n−1]+F[n−2]{F[n]=F[n-1]+F[n-2]}F[n]=F[n−1]+F[n−2]
例子:COUNT THE PATHS
从开始位置到结束位置,小人只能往右或往下走,遇到石头不能走,问一共有多少种走法。
递归的思路是从开始位置走,一直到结束位置。
递推的思路是从结束位置开始,走到开始位置。
看下面递推的走法:
它的状态转移方程:
opt[i, j] = opt[i-1, j] + opt[i, j-1]
======================================
if a[i, j] == '空地':
opt[i, j] = opt[i-1, j] + opt[i, j-1]
else: // 石头
opt[i, j] = 0
从结束位置开始往上递推,就有了下面这幅图,方格子里面的数字就是“记忆体”,表示从当前位置到结束位置的走法总数,特别注意数字指的是一共有多少种走法。
不要从开始位置走,很容易回溯(递归),越走越迷糊。(坚信递推公式是正确的)
看一下递推的时间复杂度:O(m∗n){O(m*n)}O(m∗n)
总结:
- 一定要递推!!(尽量不要用递归+记忆化的方式)
- 状态的定义:opt[n],dp[n],fib[n]{opt[n],dp[n],fib[n]}opt[n],dp[n],fib[n]。斐波那契数列的状态定义为一维数组fib[n]){fib[n])}fib[n]),count the paths的状态定义为二维数组opt[i,j]{opt[i, j]}opt[i,j]。
- 状态转移方程:opt[n]=best_of(opt[n−1], opt[n−2], ...){opt[n]=best\_of (opt[n-1], \ opt[n-2],\ ...)}opt[n]=best_of(opt[n−1], opt[n−2], ...)
- 最优子结构:当前状态取决于前一个状态,和之后的更多过去状态都无关。
DP vs 回溯 vs 贪心
- 回溯(递归)-重复计算
- 贪心-永远局部最优
- DP-记录局部最优子结构/多种记录值,集前两者之大成。