第一次接触动态规划已经是在我的研究生课堂上了,老师担心本科是外校的同学听不懂,又把基础快速的带一下、、
真实的去思考动态规划,可能就是现在了,为了校招,疯狂刷力扣,现在我研二上学期、、
之前写的帖子讲动态规划理解,总觉得差了些火候,并不是完全的理解,我也找不到更为精准的词语去形容了,直到今天看labuladong 的算法笔记这个笔记,豁然开朗
首先,动态规划问题的一般形式就是求最值。动态规划其实是运筹学的一种最优化方法,
只不过在计算机问题上应用比较多,比如说让你求最长递增子序列呀,最小编辑距离呀等等。
既然是要求最值,核心问题是什么呢?求解动态规划的核心问题是穷举。
因为要求最值,肯定要把所有可行的答案穷举出来,然后在其中找最值呗。
动态规划这么简单,就是穷举就完事了?我看到的动态规划问题都很难啊!
首先,虽然动态规划的核心思想就是穷举求最值,但是问题可以千变万化,
穷举所有可行解其实并不是一件容易的事,需要你熟练掌握递归思维,
只有列出正确的「状态转移方程」,才能正确地穷举。而且,你需要判断算法问题是否具备「最优子结构」,
是否能够通过子问题的最值得到原问题的最值。
另外,动态规划问题存在「重叠子问题」,如果暴力穷举的话效率会很低,
所以需要你使用「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。
以上提到的重叠子问题、最优子结构、状态转移方程就是动态规划三要素。
具体什么意思等会会举例详解,但是在实际的算法问题中,
写出状态转移方程是最困难的,这也就是为什么很多朋友觉得动态规划问题困难的原因,
我来提供我总结的一个思维框架,辅助你思考状态转移方程:
明确 base case -> 明确「状态」-> 明确「选择」 -> 定义 dp 数组/函数的含义。
按上面的套路走,最后的解法代码就会是如下的框架:
# 自顶向下递归的动态规划
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、零钱兑换
最开始研究树的递归也只是把代码背下来,并没有理解
把一个大的问题分析后,可以分解为多个子问题,然后先去写子问题的代码,最后考虑递归的终止条件、、
如果是整体去思考,就很容易乱,那么就要分开去思考,从哪里思考到哪里,把子问题划分清晰,然后思考的关键点是什么要想清楚. 其次是写代码的时候有的逻辑错误未必能马上发现,所以需要找组数据去验证,终止条件也是根据数据例子去得到的,这么思考会更容易
class Solution {
int[] count;
public int coinChange(int[] coins, int amount) {
count = new int[amount + 1];
Arrays.fill(count, Integer.MAX_VALUE);
int i = dp1(count, coins, amount);
return i;
}
//如果是整体去思考,就很容易乱,那么就要分开去思考,从哪里思考到哪里,然后思考的关键点是什么要想清楚
public int dp1(int[] count, int[] coins, int amount) {
if (amount == 0) return 0;
if (amount < 0) return -1;
if (count[amount] != Integer.MAX_VALUE) return count[amount];
int min = Integer.MAX_VALUE;
for (int coin : coins) {
//需要的最少硬币数
int c = dp1(count, coins, amount - coin);
if (c < 0) continue;
min = Math.min(min,c);
}
count[amount] = min != Integer.MAX_VALUE ? min+1 : -1;
return count[amount];
}
}
91、解码方法
这种解法才是一个标准的动态规划
重点还是在分出子问题来
class Solution {
public int numDecodings(String s) {
int n = s.length();
if (n < 1) {
return 0;
}
// 定义:dp[i] 表示 s[0..i-1] 的解码方式数量
int[] dp = new int[n + 1];
// base case: s 为空或者 s 只有一个字符的情况
dp[0] = 1;
dp[1] = s.charAt(0) == '0' ? 0 : 1;
// 注意 dp 数组和 s 之间的索引偏移一位
for (int i = 2; i <= n; i++) {
char c = s.charAt(i-1), d = s.charAt(i-2);
if ('1' <= c && c <= '9') {
// 1. s[i] 本身可以作为一个字母
dp[i] += dp[i - 1];
}
if (d == '1' || d == '2' && c <= '6') {
// 2. s[i] 和 s[i - 1] 结合起来表示一个字母
dp[i] += dp[i - 2];
}
}
return dp[n];
}
}