DP是算法学习中非常重要的一种思想,关于动态规划的解释,可以参考这篇文章。
0 概念
使用DP三个条件:
- 最优子结构:待解决的问题可以被分解为若干子问题,并且递归地找到子问题的最优解;
- 重叠子问题:在解决子问题的过程中,很多子问题都会被求解多次,第一次计算后存储该子问题的解,以后就可以直接使用,即降低了时间复杂度。如果子问题没有重叠,那么这就是分治的问题;
- 无后效性:子问题的最优解是确定的,完全可以用来解决更大的子问题。
DP一般有两种形式:
- Top-down: 记忆化递归可能stackoverflow
- Bottom-up
模板:
// 记忆化递归
unordered_map<int, int> hash; // memory dict
int f(i, j, ...) {
if base_case(i, j)
return ...;
if (i, j) not in hash
hash[(i, j)] = ...;
return hash[(i, j)];
}
return f(n, m);
// DP
int dp[][]; // need padding sometimes
dp[0][0] = ...; // base case
for(int i = 0;i < n;++i)
for(int j = 0;j < m;++j)
dp[i][j] = ... // 状态转移
return dp[n][m];
1 分类
- Fibonacci(House Thief, Jump Game)
- 0/1 Knapsack
- Unbounded Knapsack
- Shortest Path(Unique Paths I/II)
- Longest Common Substring/Subsequeunce
- ......
2 步骤
一般来讲,都是通过暴力->记忆化递归->Bottom-up三部曲,当然熟悉后可以快速判断这是一个DP问题,然后直接写出Bottom-up的解法。
我个人认为最难的一步在于判断出你的暴力解法满足DP的性质(你要能认出来这是一个DP问题),可以用DP去优化暴力解法。
- 确定问题分类
- 确定状态:需要几个变量来跟踪目前的状态,一般来讲至少需要index,因为这决定了我们已经考虑过了哪些值,没考虑哪些值,正在考虑哪些值。选定的变量组合要能唯一确定一个状态
- 状态转移:为了达到base case,当前状态怎么才能由之前的状态得到。也就是Top-down逐渐分解问题,每一次递归调用都会分解一下
- base case:一般比较简单,不废话了
- code:思路清楚了,也不难
- 优化:一般优化空间复杂度