动态规划 (Dynamic Programming)
动态规划问题的通性
动态规划问题一般都是用来求最值的,例如最长递增子序列,最小编辑距离等等。动态规划的核心思路是穷举, 因为要求最值,肯定要知道所有的值是什么才能知道最值是多少。
怎么穷举以及如果优化穷举便是我们研究的问题。
动态规划的三要素
重叠子问题、最优子结构、状态转移方程是动态规划的三要素。
定义一个状态转移方程,有以下思维框架:
明确base case -> 明确[状态] -> 明确[选择] -> 定义dp
数组/函数的含义
动态规划算法框架
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| # 自顶向下递归的动态规划 def dp(状态1, 状态2, ...): for 选择 in 所有可能的选择: # 此时的状态已经因为做了选择而改变 result = 求最值(result, dp(状态1, 状态2, ...)) return result
# 自底向上迭代的动态规划 # 初始化 base case dp[0][0][...] = base case # 进行状态转移 for 状态1 in 状态1的所有取值: for 状态2 in 状态2的所有取值: for ... dp[状态1][状态2][...] = 求最值(选择1,选择2...)
|
例子:凑零钱问题
力扣第322题 [ 零钱兑换 ]
给你k
种面值的硬币,面值分别为c1, c2, c3,....ck
,每种硬币的数量无限,请你求出凑够amount
金额所使用的最小硬币数。如果不可能凑出,请返回-1. 例如,amount = 11
, c1 = 1, c2 = 2, c3 = 5
。 则最少需要三枚硬币凑出 11 = 5 + 5 + 1
-
问题符合最优子结构
要符合最优子结构,子问题之间必须相互独立。例如,考试中的每门科目都是项目独立的,要考到最高分,就需要每科考到最高分。每科考到最高分,那么该科中的每种题型都要拿到最多的分。 每门科目之间、每个题型拿到最高分之间是相互独立的。反之,如果你的语文成绩和数学成绩相互制约,一个拿最高分,另一个就不能那最高分,那他们之间就不是相互独立的,就破坏了最优子结构的相互独立性。
该题是符合最优子结构的,因为硬币有无限枚,我们要求凑出11的最少硬币,就需要求得凑出 10,9,6三种金额的最少硬币数,然后加1. 求出凑齐10,9,6的最少硬币数量,就要求他们的子问题最小值然后加1.可以看出子问题之间是相互独立的。
-
状态转移方程
- base case, 该问题中就是amount数值为0的时候,不需要任何硬币就可以凑出amount
- 确定[状态],也就是原问题和子问题中会变化的量, 由于硬币数量无限,在子问题中amout值会不断趋近于0(base case),所以状态就是amout的值
- 确定[选择], 也就是导致[状态]变化的行为,在该题中,即为不同的硬币面值。
- 明确
dp
函数/数组的定义。 一般来说函数的参数就是状态转移中会变化的量,也就是状态;函数的返回值就是题目要求我们计算的量。这样我们可以定义一个dp
函数: dp(n)
表示,输入一个目标金额n
, 返回凑出目标金额n所需的最少硬币数量。
根据以上我们可以写出伪代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| int coinChange(int[] coins, int amount) { return dp(coins, amount) }
int dp(int[] coins, int n) { for (int coin : coins) { res = min(res, 1 + dp(coins, n - coin)) } return res }
|
加入base case 完整的代码就可以写出来了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| int coinChange(int[] coins, int amount) { return dp(coins, amount) }
int dp(int[] coins, int amount) { if (amount == 0) return 0; if (amount < 0) return -1;
int res = Integer.MAX_VALUE; for (int coin : coins) { int subProblem = dp(coins, amount - coin); if (subProblem == -1) continue; res = Math.min(res, subProblem + 1); }
return res == Integer.MAX_VALUE ? -1 : res; }
|
接下来就是去优化整个递归穷举过程,因为这里面存在重叠子问题,一些金额所需要的硬币数量我们已经算过,如下图,金额为9、5的在不同分支中出现了多次,可以把它们存在备忘录中,如果在另一个树分支中用到了,直接调用即可,不用重复计算,即[剪枝]。
带备忘录的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| class Solution { int[] memo;
int coinChange(int[] coins, int amount) { memo = new int[amount + 1]; Arrays.fill(memo, -666);
return dp(coins, amount); }
int dp(int[] coins, int amount) { if (amount == 0) return 0; if (amount < 0) return -1; if (memo[amount] != -666) return memo[amount];
int res = Integer.MAX_VALUE; for (int coin : coins) { int subProblem = dp(coins, amount - coin); if (subProblem == -1) continue; res = Math.min(res, subProblem + 1); } memo[amount] = (res == Integer.MAX_VALUE) ? -1 : res; return memo[amount]; } }
|