阅读总结自动态规划之武林秘籍
1. 基本思想
1.1 将待求解问题分解为若干个子问题
1.2 通过求解子问题的解得到原问题的解
1.3 经分解后的子问题往往不是互相独立的
1.4 需要对已经解决的子问题答案进行保存,防止出现重复计算的情况
2. 基本要素
如何判断一个问题是否可以用动态规划来解决,可以参照动态规划的两个基本要素
- 最优子结构性质
- 重叠子问题性质
2.1 最优子结构性质
当问题的最优解包含了其子结构的最优解时,称该问题具有最优子结构性质
2.2 重叠子问题性质
- 动态规划经分解得到的子问题往往不是互相独立的。
- 如果经分解之后的子问题互相独立,比如二分查找经分解之后的子问题之间相互独立,不存在重叠子问题,所以不适用动态规划,更适合分治算法
2.2.1. 保存重叠子问题的解的常见方法
2.2.1.1 DP table (自底向上)
- 自底向上建立一个表格,用于保存每个子问题的解,并返回表中的最后一个解
实现:
def fib(n):records = []records.append(0)records.append(1)for i in range(2,n+1):records.append(records[i-1] + records[i-2])return records
2.2.1.2 备忘录方法(自顶而下)
备忘录方法时动态规划算法的变形,
- 与动态规划方法一样,备忘录方法用表格保存已经解决的子问题的解,在下次需要解决此子问题的时候,只要简单查看盖子问题的解,不需要重新计算
- 以动态规划方法的不同:备忘录方法时自顶线下的
- 操作过程:
- 为每一个子问题建立一个记录项
- 初始时,该记录项存入一个特殊值,代表该子问题尚未被解决
- 求解过程中,先查看子问题对应的记录项
- 若记录项时初始化的特殊值,代表该子问题未被求解,此时计算子问题的特殊值,并保存在记录项中
- 若记录项存储的是不初始化的特殊值,代表该子问题已经被求解,只需从记录想中提取该子问题的答案即可

- 实现:
def fib(n): initilizer = [-1] * (n+1) if initilizer[n] == -1: if n <= 1: initilizer[n] = n else: initilizer[n] = fib(n-1) + fib(n-2) return initilizer[n]3. 如何解决动态规划问题
动态规划( Dynamic Programming, DP)是在多项式时间解决特定类型问题的一套方法论,且远远快于只是级别的蛮力方法,且动态规划的正确性是可以严格证明的。
四步法
- 辨别是不是一个动态规划问题
- 确定状态
- 建立状态之间的关系
- 为状态添加备忘录或者DP Table
3.1 辨别是不是一个动态规划问题
- 一边情况下,需要求解最优解的问题(最短路径问题,最长公共子序列,最大字段和···),在一定条件下对排列进行计数的计数问题(丑数问题)或某些概率问题都可以考虑使用动态规划来解决
- 所有动态规划问题都满足重叠子问题性质
- 大多数冬天规划问题还满足最优子结构性质
3.2 确定状态
-
什么是状态?
状态 可以视为一组可以唯一标识给定问题中某个子问题解的参数。
- 这组参数要尽可能的小(为了减少状态空间的大小)
- 以斐波那契数例子:
- 0,1,···,n就可以视为参数
- DP[0],DP[1],···,DP[n]就是状态
- DP[n] = DP[n-1] + DP[n-2]就是状态之间的转移方程
- 背包问题:
- index和weight是参数
- DP[index][weight]是状态
3.3 构造状态转移方程
这是DP问题中最难的部分
例子:
问题描述:给定3个数{1,3,5},请问使用这三个数,有多少种方式可以构造出一个给定的数’n’
设n=6,使用{1, 3, 5}则共有8种方式可以构造出n 1+1+1+1+1+1 1+1+1+3 1+1+3+1 1+3+1+1 3+1+1+1 3+3 1+5 5+1问题思路:
- 确定问题状态
- 由于参数n可以唯一标识任意一个子问题,所以使用参数n来确定状态
- 状态为:DP[n]
- 计算DP[n]:
- 求解出n等于1,2,3,4,5,6的状态值
- DP[n=1]=1, DP[n=2]=1, DP[n=3]=2, DP[n=4]=3, DP[n=5]=5, DP[n=6]=8
- 求解DP[7]
- 给状态DP[n=6]的所有序列+1
- 给状态DP[n=4]的所有序列+3
- 给状态DP[n=2]的所有序列+5
- DP[7] = DP[6]+DP[4]+DP[2]
- DP[n] = DP[n-1]+DP[n-3]+DP[n-5]
- 确定问题状态
实现:
def solve(n): if n < 0: # 无效状态,返回0 return 0 if n == 0 or n == 1: # n=0的时候,代表原始可用数字中存在该数,所以要记一次 return 1 return solve(n-1) + solve(n-3) + solve(n-5)3.4 为状态添加备忘录或者是DP Table
动态规划中最简单的部分,仅需要存储子状态,以便下次使用子状态时直接通过查表实现
实现:
def solve(n): initilizer = [-1] * (n+1) initilizer[0] = 1 initilizer[1] = 1 initilizer[2] = 1 initilizer[3] = 2 initilizer[4] = 3 initilizer[5] = 5 for i in range(6, n+1): initilizer[i] = initilizer[i-1] + initilizer[i-3] + initilizer[i-5] return initilizer[n]4. 备忘录和DP Table的选择
4.1 DP Table方法(自底而上的动态规划)
状态转移过程
设DP问题的基态为
dp[0],目标状态为dp[n]- 如果从
dp[0]开始转移,在遵循状态转移方程的情况下达到目标状态dp[n],则是“自底向上”的方法
4.2 备忘录方法(自顶而下的方法)
状态转移过程
- 设DP问题的基态为
dp[n],目标状态为dp[n] - 如果从
dp[n]开始转移,找到所有和dp[n]相关的子状态,并返回dp[n]
4.3 比较
- 状态: DP Table 状态转移关系较难确定, 备忘录 状态转移关系较易确定
- 自顶向下推到较为容易
- 代码:当约束条件较多的情况下,
- DP Table 较为复杂;
- 备忘录 代码相对容易实现,只需要对递归代码进行改造
- 效率:
- DP Table 较快,可以直接从表中获取子状态
- 备忘录 由于大量的递归调用和返回状态操作,较慢
- 子问题的解:
- 当所有子问题的解都需要被计算一遍时, DP Table 通常比 备忘录 方法快常数量级
- 当求解问题的子问题空间中部分子问题不需要计算时, 备忘录 方法要由于 DP Table
- 表空间
- DP Table 一次填充所有子状态的解
- 备忘录 只需要填充需要的子状态的解

