动态规划
2020年12月29日
14:06
目录 |
- - 经典三概念与解决思想 - 经典的解决思想 - 动态规划的方法论 - 案例:找零 - 求解框架 - REF
|
经典三概念与解决思想
动态规划是什么?动态规划有三个特征。
- 重复子问题,这个特点很好理解,递归树是一个非常好方法,它的一个功能就是能直观地感受到重复子问题
- 最优子结构,当前问题可以由子问题的solution解决,这个特性,递归地使得当前问题和子问题具备相同的结构
- 状态转移方程,这也是最难的一步,既然当前问题可以由子问题解决,那么转移公式是?
经典的解决思想
- 自顶向下,逐步求解
- 自底向上,反向求精
这是计算机世界一种经典的思想,在动态规划、递归、回溯中,自顶向下的方法通常是非常直接的暴力解法——这非常有用,是一切优化的开始。
动态规划的方法论
解决这些问题需要找到四个要素:
- base,求解停止的基础条件是?到哪种状态可以直接返回。
- state,状态,这是非常关键的,它指的是可以代表当前问题的一些状态量的集合,对应到递归树中,这个状态就是递归树中的一个节点——这非常重要。
- select,选择,改变状态的行为
- dp function,dp动态规划函数,函数一般返回某个状态的solution,这个也是最难定义的暴力解决方案
以找零钱问题为例,请参考REF中原题
- base,当amount=0的时候,表示不用凑硬币了,直接return 0
- state,状态,这个问题的关键状态量只有一个,那就是当前问题的amount——这个目标金额,最少需要多少枚硬币可以凑齐
- select,改变目标金额的唯一行为是选择某个硬币
- dp函数,根据以上dp函数可以定义为,c(最少需要的硬币数) = dp(目标金额)
案例:找零
根据上述分析找零问题的暴力求解伪代码框架是:
1 def dp(n): 2 for coin in coins: 3 res = min(res, dp(n-coin)+1) 4 return res |
详细解法,可以参考原文,见ref。
上述框架,加上基值条件,完整的代码如下。
1 def dp(n): 2 if n == 0: return 0 3 if n < 0: return -1 4 res = float('INF') 5 for coin in coins: 6 t = dp(n-coin) 7 if t == -1: continue 8 res = min(res, t+1) 9 return res if res != float('INF') else -1 10 # 备忘录 11 memo = {} 12 def dp(n): 13 # memo 14 if n in memo: return memo[n] 15 # base 16 if n == 0: return 0 17 if n < 0: return -1 18 # init res 19 res = float('INF') 20 # chose coin 21 for coin in coins: 22 t = dp(n-coin) 23 # invalid 24 if t == -1: continue 25 res = min(res, t+1) 26 memo[n] = res if res != float('INF') else -1 27 return memo[n] 28 # 迭代 29 memo = [0] 30 31 for n in range(1, amount+1): 32 memo.append(amount+1) 33 for coin in coins: 34 # 检查越界 35 if n - coin >= 0: 36 memo[n] = min(memo[n-coin]+1, memo[n]) 37 return memo[n] if memo[n] != (amount+1) else -1 |
- 去除重复子问题的最直接的方式就是使用备忘录记录状态
- 完成了备忘录方法,那么最后的问题就是,如何自底向上计算出备忘录(所有状态对应的解法的数组)
代码如上。
求解框架
该框架有点令人费解,但是仔细对照找零问题,其中核心问题就是理解“状态”——状态备忘录,记录的是当前状态的solution。
1 # 初始化 base case 2 dp[0][0][...] = base 3 # 进行状态转移 4 for 状态1 in 状态1的所有取值: 5 for 状态2 in 状态2的所有取值: 6 for ... 7 dp[状态1][状态2][...] = 求最值(选择1,选择2...) |
REF
经典的入门问题是斐波那契数列数列,以及找零钱问题。解法太过常见,这次来谈一些正确的思路。