动态规划 (Dynamic Programming)
参考链接:https://www.zhihu.com/question/23995189/answer/613096905
0 从凑钱开始
凑钱问题
现在有1、5、10、20、50、100元面值的钞票,目标是用尽量少的钞票凑出某个金额Target。
尝试解决凑钱问题
既然要用尽量少的钞票,那么我们在凑钱的时候就先从面值大的开始,当面值大的钞票超过要凑的金额时选择面值小的钞票,依次下去。举例来说,如果要凑666元,那么先取100(还差566),再取100(重复5次),最后取出6张100元后(还差66),开始取50(还差16),开始取10(还差6),开始取5(还差1),开始取1(还差0),结束。
上述的策略(从大的面值开始,直到超标后开始选择小的面值)为贪心策略,贪心策略关注当前这一步做出来最优选择,所以贪心是一种只考虑眼前情况的策略,对于有些场景贪心策略的方案可能是低效的,例如,只有面值为1、5、11的钞票,需要凑出15元,贪心策略的步骤是:先拿一张11(还差4元),再拿4张1元,一种拿了五张。但是,正确的策略是:拿三张5元钞票。
所以,更好的策略不能只考虑当前,需要考虑全局的情况。如果选择考虑所有情况的暴力枚举,把所有能凑出15元的方案都找到,然后选择一个最少张的方案,缺点很明显就是时间复杂度很高。那么,如果尝试取11,接下来就是Target=4的情况;如果尝试取5,接下来就是Target=10的情况;如果尝试取1,接下来就是Target=14的情况。可以看出,做出一个选择后,问题重新变成:给定Target,凑出它的最少钞票数是多少张。上述过程是把原问题拆解成了子问题,变化的(状态)是Target值。
由上面的分析可知:假设\(f(n)\)为凑出n所需的最少钞票数,那么\(f(15) = min{f(15-1), f(15-5), f(15-11)} + 1\).一般情况即为:\(f(n) = min{f(n-1), f(n-5), f(n-11)} + 1\)
所以我们只要从小到大,把\(f(1)\)、\(f(2)\)、\(f(3)\)、\(f(4)\)、\(f(5)\)...计算出来,等到了\(f(n)\),结果就直接出来了。python参考代码如下: 1
2
3
4
5
6
7
8
9
10
11
12def money(target):
f = [0 for i in range(target +1)]
for i in range(1, target+1):
cost = 100000 ## 假设这个值为不会取到的最大值
if i - 1 >= 0:
cost = min(cost, f[i-1] + 1)
if i - 5 >= 0:
cost = min(cost, f[i-5] + 1)
if i - 11 >= 0:
cost = min(cost, f[i-11] + 1)
f[i] = cost
return f[target]
之所以能像上面那样解决是因为问题的性质:如果要求f(n),只需知道几个更小的f(c)。求解f(c)成为求解f(n)的子问题。这就是动态规划(Dynamic programming)。
众所周知,Dynamic programming是作者随便起的名字,和其本身的思想无关。
1 动态规划的思路
动态规划用于解决问题时需要明确的四个部分:
- 1.1 确定状态
- 研究最优策略的最后一步。也就是问题的最后一步怎么做。
- 转化为子问题。知道了最后一步怎么做之后,那么最后一步的前一步是什么,总结之后就是子问题。
子问题和最后一步的问题相似的地方便是求解的重点。状态就是dp[i]---也就是动态规划数组中的i对应的元素的含义。通常dp[i]是我们要求的最优的目标,i是变化的量。
- 1.2 转移方程
根据子问题的定义直接得到状态转移的方程。也就是dp[i]关于其他dp[j]的表达式。这里的表达式要么可以直接写出数学方程或者就是一个相关的描述。
- 1.3 初始条件和边界情况
考虑问题的最开始的条件,以及问题的边界。
- 1.4 计算顺序
利用之前的计算结果。利用先前计算好的结果求解当前需要计算的目标。
2 动态规划编程题
从简单到困难,依次来看看动态规划的编程题。
2.1 爬楼梯--Leetcode 70.爬楼梯
先来看题目和示例:
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例:
开始解题,你有什么想法吗?
我想到,这个题目的目的是爬到n阶楼梯上去,为了爬到 n 阶上面,根据题目的要求,那么我只能从 n-1 阶和 n-2。最后爬上去的情况考虑完了,那么再往前是怎么爬上来的呢?分析一下发现跟我们最后一步的情况一样的。所以,这个爬楼梯的问题就被分解掉了。对应第1节里面动态规划的思路,这部分是确定状态中要做的事情。子问题中一直在变化的是楼梯阶数,记为i
。dp[i]
就是我们要求的最优目标:爬到当前阶数i的方法总数。
从上面的分析我们能知道,所求目标dp[i]
可以通过式子dp[i] = dp[i-1] + dp[i-2]
,这个也就是转移方程(dp[i]关于其他dp[j]的表达式)。
该问题的初始条件和边界情况是第1阶和第2阶楼梯,它们的次数都是1。其余的位置初始化为0。
最后考虑计算顺序。因为后面的楼梯需要从前面的楼梯上去,所以需要自底向上进行计算。
Talk is cheap, show me the code:
1 | class Solution: |
2.2 打家劫舍——LeetCode 198
python代码
1 | def rob(nums): |