动态规划思路整理:
- 首先,动态规划问题的一般形式就是【求最值】。比如求最长递增子序列呀,最小编辑距离等。
- 既然是要求最值,核心问题是什么呢?求解动态规划的核心问题是【穷举】。因为要求最值,肯定要把所有可行的答案穷举出来,然后在其中找最值。
- 动态规划的穷举有点特别,因为这类问题存在【重叠子问题】,如果暴力穷举的话效率会极其低下,所以需要【备忘录】或者【DP table】来优化穷举过程,避免不必要的计算。
- 确定【最优子结构】,列出【状态转移方程】
考虑能否将问题规模减小,思考如何由较小规模问题的解获得最终问题的解
总结: 解决动态规划问题最难的地方有两点:
- 如何定义 f(n)
- 如何通过 f(1), f(2), … f(n - 1)推导出 f(n),即状态转移方程
1. 递归
有了状态转移方程,实际上已经可以直接用递归进行实现了。
int f(vector<int>& nums, int i)
{
int a = 1;
for(int j = 0; j < i; ++j)
{
if(nums[j] < nums[i])
¦ a = max(a, f(nums, j) + 1);
}
return a;
}
2. 自顶向下(记忆化)
递归的解法需要非常多的重复计算,如果有一种办法能避免这些重复计算,可以节省大量计算时间。记忆化就是基于这个思路的算法。在递归地求解子问题 f(1)f(1), f(2)f(2)… 过程中,将结果保存到一个表里,在后续求解子问题中如果遇到求过结果的子问题,直接查表去得到答案而不计算。
int f(vector<int>& nums, int i, vector<int>& dp)
{
if(dp[i] != -1) return dp[i];
int a = 1;
for(int j = 0; j < i; ++j)
{
if(nums[j] < nums[i])
¦ a = max(a, f(nums, j) + 1);
}
dp[i] = a;
return dp[i];
}
对于这种将问题规模不断减少的做法,我们把它称为自顶向下的方法。
3. 自底向上(迭代)
在自顶向下的算法中,由于递归的存在,程序运行时有额外的栈的消耗。
有了状态转移方程,我们就知道如何从最小的问题规模入手,然后不断地增加问题规模,直到所要求的问题规模为止。在这个过程中,我们同样地可以记忆每个问题规模的解来避免重复的计算。这种方法就是自底向上的方法,由于避免了递归,这是一种更好的办法。
但是迭代法需要有一个明确的迭代方向,例如线性,区间,树形,状态压缩等比较主流的动态规划问题中,迭代方向都有相应的模式。参考后面的例题。但是有一些问题迭代法方向是不确定的,这时可以退而求其次用记忆化来做。