基础算法学习——动态规划篇
提示:本文随时更新,以记录对于该类型算法的学习过程,作者水平有限,所有内容仅为我个人一孔之见,如果大家觉得有用欢迎点赞收藏。
一.动态规划是什么
- 动态规划问题的一般形式就是求最值。动态规划其实是运筹学的一种最优化方法,只不过在计算机问题上应用比较多,比如说让你求最长递增子序列呀,最小编辑距离呀等等。
- 既然是要求最值,核心问题是什么呢?求解动态规划的核心问题是穷举。因为要求最值,肯定要把所有可行的答案穷举出来,然后在其中找最值呗。
- 动态规划的穷举有点特别,因为这类问题存在「重叠子问题」,如果暴力穷举的话效率会极其低下,所以需要「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。(空间换时间)
- 动态规划问题一定会具备「最优子结构」,才能通过子问题的最值得到原问题的最值。
- 虽然动态规划的核心思想就是穷举求最值,但是问题可以千变万化,穷举所有可行解其实并不是一件容易的事,只有列出正确的「状态转移方程才能正确地穷举。
- 以上提到的重叠子问题、最优子结构、状态转移方程就是动态规划三要素。具体什么意思等会会举例详解,但是在实际的算法问题中,写出状态转移方程是最困难的,这也就是为什么很多朋友觉得动态规划问题困难的原因,我来提供我研究出来的一个思维框架,辅助你思考状态转移方程:
明确 base case -> 明确「状态」-> 明确「选择」 -> 定义 dp 数组/函数的含义。
# 初始化 base case
dp[0][0][...] = base
# 进行状态转移
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 求最值(选择1,选择2...)
二.什么是重叠子问题以及如何解决它
- 什么是重叠子问题:例如计算斐波拉契数列会有很多重复子问题存在(例如在各个递归中计算很多次f(3))
- 如何解决重叠子问题:一般使用一个数组充当「备忘录」,当然你也可以使用哈希表(字典),思想都是一样的(除第一次计算外,之后需要使用,查备忘录即可)
- 上图对相同的结点进行剪枝操作(用备忘录存储)
- 计算f(20)的斐波拉契问题只需要计算20个子问题即可(用长20的备忘录存储)(自顶向下)
int fib(int N) {
if (N < 1) return 0;
// 备忘录全初始化为 0
vector<int> memo(N + 1, 0);
// 进行带备忘录的递归
return helper(memo, N);
}
int helper(vector<int>& memo, int n) {
// base case
if (n == 1 || n == 2) return 1;
// 已经计算过
if (memo[n] != 0) return memo[n];
memo[n] = helper(memo, n - 1) + helper(memo, n - 2);
return memo[n];
}
- dp 数组的迭代解法(自底向上)
int fib(int N) {
vector<int> dp(N + 1, 0);
// base case
dp[1] = dp[2] = 1;
for (int i = 3; i <= N; i++)
dp[i] = dp[i - 1] + dp[i - 2];
return dp[N];
}
三.什么是状态转移方程
- 实际上就是描述问题结构的数学形式:
四.什么是状态压缩
- 例如:根据斐波那契数列的状态转移方程,当前状态只和之前的两个状态有关,其实并不需要那么长的一个 DP table 来存储所有的状态,只要想办法存储之前的两个状态就行了。
//状态压缩下的斐波拉契算法实现code
class Solution {
public:
int fib(int n) {
if(n == 0){
return 0;
}
if(n == 2 || n == 1){
return 1;
}
int prev = 1,curr = 1;
for(int i = 3;i <= n;i++){
int sum = prev + curr;
prev = curr;
curr = sum;
}
return curr;
}
};
五.什么是最优子结构
- 待补充