状态转移方程分类
对于动态规划类题目,当我们完成数据建模的之后,需要列出状态转移方程(事实上这两个步骤是相互关联的),通过总结leetcode上动态规划类题目的解法,一般来说状态转移方程分为以下两个大类:
- 找到当前状态与之前某个状态之间的关系,通过之前状态的值求解当前状态的值
- 找到当前状态与一些子状态之间的关联,通过遍历这些子状态求解当前状态的值
以上这两种情况在某些题目中可以相互转换的, 有些时候并不能直接找到历史某个状态与当前状态的关系,但可以遍历所有历史状态求解;有些题目很直观地看到是与所有历史的状态有关联,但通过对状态转移方程的化简,可以得到的推导式只与某个历史的状态相关。
状态转移方程实例
- 遍历子状态,求解当前状态的解的例题
//思路:Strange Printer
//模型:dp[i][j]表示s.substr(i,j+1-i)需要多少次操作
//状态转移方程: dp[i][j] = min(dp[i][k] + dp[k+1][j] - s[j] == s[k]) k = i...j-1
// 当s[j] == s[j]的时候可以少进行一次操作
int strangePrinter(string s) {
if(s.empty() ) return 0;
int n = s.size();
vector<vector<int>> dp(n, vector<int>(n, n));
for(int i = 0; i < n; i++) dp[i][i] = 1;
for(int len = 1; len < n; len++) {
for(int i = 0, j = i + len; j < n; i++,j++) {
dp[i][j] = j + 1 - i;
//遍历子状态去求解当前状态
for(int k = i + 1; k <= j; k++) {
int tmp = dp[i][k-1] + dp[k][j];
if (s[k-1] == s[j]) tmp--;
dp[i][j] = min(dp[i][j], tmp);
}
}
}
return dp[0][n-1];
}
- 由一个子状态能得到当前状态的例题
//思路:Palindromic Substrings
//模型:dp[i][j]表示s.substr(i, j+1-i)是否是回文串
//状态转移方程:dp[i][j] = dp[i+1][j-1] && s[i] == s[j] (注意边界条件)
//据说还有Manchester算法,可以将算法复杂度减少至O(n)
int countSubstrings(string s) {
if(s.empty()) return 0;
int size = s.size();
int res = 0;
vector<vector<bool>> dp(size, vector<bool>(size, false));
for(int i = 0; i < size; i++) dp[i][i] = true;
for(int len = 1; len < size; len++) {
for(int i = 0, j = i+len; j < size; ++i, ++j) {
//当前状态只与dp[i+1][j-1]有关
dp[i][j] = s[i]==s[j] && (i+1 >= j -1 || dp[i+1][j-1]);
res += dp[i][j];
}
}
return res + size;
}
处理边界条件或者遍历的一些技巧
- 对于用于动态规划的记忆数组/矩阵,可以适当增加一些边界状况下的值,这样不用单独挑出来考虑;
- 遍历的方向
dp[i][j]
- 正常的方向:从左到右
j =0, 1,...n-1
,从上到下i = 0, 1 ... m-1
; - 可以考虑从右到左,从下到上遍历;排序矩阵查找目标值
- 也可以从
j - i = 1, 2 ... m
进行循环 Palindromic Substrings
- 正常的方向:从左到右
- 背包类问题
- 针对物品是否能重复利用,
dp[i][j]
关于价值的j的递归方向是不一样的。
- 针对物品是否能重复利用,
写在最后
这里总结了一些动态规划类题目常用的解法,然后希望通过参考这些解法,让我们在面临新的动态规划类的题目,有一些着角点进行尝试建模; 这是治标不治本的方式,不太符合学习规律;但这也是无奈之举,希望能帮助自己在较短的时间可以尝试做一些动态规划类的题目,以面对校招笔试面试中的各种面试题吧。