「 labuladong的算法小抄 」学习笔记
内容
最长公共子串
最长公共子序列
最长回文子串
最长回文子序列
最长递增子序列
原题链接:力扣
先看最长公共子序列再看这道题。
时间复杂度O(MN),空间复杂度O(MN)。
类似最长公共子序列的分析,这里,我们使用c[i,j] 表示 以 Xi 和 Yj 结尾的最长公共子串的长度,因为要求子串连续,所以对于 Xi 与 Yj 来讲,它们要么与之前的公共子串构成新的公共子串;要么就是不构成公共子串。故状态转移方程:
class Solution {
public:
int longestCommonSubstring(string str1, string str2) {
int len1 = str1.length();
int len2 = str2.length();
vector<vector<int> > dp(len1 + 1, vector<int>(len2 + 1, 0));
int result = 0;
for (int i = 1; i <= len1; i++) {
for (int j = 1; j <= len2; j++) {
if (str1[i - 1] == str2[j - 1]) {
dp[i][j] = dp[i - 1][j -1] + 1;
result = max(dp[i][j], result);
} else {
dp[i][j] = 0;
}
}
}
return result;
}
};
摘自:labuladong,原题链接:力扣1143
子序列类型的问题,穷举出所有可能的结果都不容易,而动态规划算法做的就是穷举 + 剪枝,它俩天生一对儿。所以可以说只要涉及子序列问题,十有八九都需要动态规划来解决,往这方面考虑就对了。
这道题是定义一个二维数组dp。
- 第一步,一定要明确dp数组的含义。对于s1[1…i]和s2[1…j],它们的 LCS 长度是dp[i][j]。
- 第二步,定义 base case。专门让索引为 0 的行和列表示空串,dp[0][…]和dp[…][0]都应该初始化为 0,这就是 base case。
- 第三步,找状态转移方程。那么对于 s1 和 s2 中的每个字符,有什么选择?很简单,两种选择,要么在 lcs 中,要么不在。如果 s1[i] == s2[j] ,那么这个字符一定在 lcs 中;否则的话,s1[i] 和 s2[j] 这两个字符至少有一个不在 lcs 中,需要丢弃一个。
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int m = text1.size();
int n = text2.size();
int** dp = new int*[m+1];
for(int i=0;i<=m;i++){
dp[i] = new int[n+1];
dp[i][0] = 0;
}
fill_n(dp[0],n+1,0);
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
if(text1[i-1]==text2[j-1]){
dp[i][j] = dp[i-1][j-1]+1;
}else{
dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
}
}
}
return dp[m][n];
}
};
摘自:labuladong,原题链接:力扣5
回文串:正着读和反着读都一样的字符串
寻找回文串的问题核心思想:从中间开始向两边扩散来判断回文串。——使用双指针。
for 0 <= i < len(s):
找到以 s[i] 为中心的回文串
更新答案
因为回文串长度可能是技术或者偶数,所以做如下更改:
for 0 <= i < len(s):
找到以 s[i] 为中心的回文串
找到以 s[i] 和 s[i+1] 为中心的回文串
更新答案
寻找最长字串的代码:
string palindrome(string& s, int l, int r) {
// 防止索引越界
while (1 >= 0 && r < s.size()&& s[l] == s[r]) {
// 向两边展开
l--; r++;
}
// 返回以 s[I] 和 s[r] 为中心的最长回文串
return s.substr(l + 1,r - l - 1);
}
主函数完整代码如下:
string longestPalindrome(string s) {
string res;
for (int i = 0;i < s.size();i++) {
// 以 s[i] 为中心的最长回文子串
string s1 = palindrome(s,i,i);
// 以 s[i] 和 s[i+1] 为中心的最长回文子串
string s2 = palindrome(s,i,i + 1);
// res = longest(res,s1,s2)
res = res.size() > s1.size() ? res : s1;
res = res.size() > s2.size() ? res : s2;
}
return res;
}
时间复杂度 O(N^2),空间复杂度 O(1)。
摘自:labuladong,原题链接:力扣516
一旦涉及到子序列和最值,那几乎可以肯定,考察的是动态规划技巧,时间复杂度一般都是 O(n^2)。既然要用动态规划,那就要定义 dp 数组,找状态转移关系。
其实有了前面的最长公共子序列做例子,这道题就好做了。
具体来说,如果我们想求 dp[i][j] ,假设知道了子问题 dp[i+1][j-1] 的结果(s[i+1…j-1]中最长回文子序列的长度),是否能想办法算出dp[i][j]的值(s[i…j]中,最长回文子序列的长度)呢?
可以!这取决于s[i]和s[j]的字符:
- 如果它俩相等,那么它俩加上s[i+1…j-1]中的最长回文子序列就是s[i…j]的最长回文子序列;
- 如果它俩不相等,说明它俩不可能同时出现在s[i…j]的最长回文子序列中,那么把它俩分别加入s[i+1…j-1]中,看看哪个子串产生的回文子序列更长即可。
这道题定义二维 dp 是因为前后字母都要判断。
base case 以及状态转移:
为了保证每次计算 dp[i][j] ,左、下、左下三个方向的位置已经被计算出来,只能斜着遍历或者反着遍历:
class Solution {
public:
int longestPalindromeSubseq(string s) {
int n = s.size();
vector< vector<int> > dp(n,vector<int>(n,0));
for(int i=0;i<n;i++){
dp[i][i] = 1;
}
for(int i=n-1;i>=0;i--){
for(int j=i+1;j<n;j++){
if(s[i]==s[j]){
dp[i][j] = dp[i+1][j-1]+2;
}else{
dp[i][j] = max(dp[i+1][j],dp[i][j-1]);
}
}
}
return dp[0][n-1];
}
};
摘自:labuladong,原题链接:力扣300
设计动态规划的通用技巧:数学归纳思想。比如我们想证明一个数学结论,那么我们先假设这个结论在 k<n 时成立,然后想办法证明 k=n 的时候此结论也成立。如果能够证明出来,那么就说明这个结论对于 k 等于任何数都成立。
-
类似的,设计动态规划算法,不是需要一个 dp 数组吗?我们可以假设 dp[0…i−1] 都已经被算出来了,然后问自己:怎么通过这些结果算出dp[i] ?
-
直接拿最长递增子序列这个问题举例,首先要定义清楚 dp 数组的含义,即 dp[i] 的值到底代表着什么?
-
定义是这样的:dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度。
-
根据这个定义,最终结果(子序列的最大长度)应该是 dp 数组中的最大值。
其实到这里我就会了,下面思考的就是状态转移了。这道题就是每到一个节点遍历一遍前面的就好,所以时间复杂度就是O(n^2)了。再有就是细节问题,base case,需要把dp数组初始化填充为1。
总结一下动态规划的设计流程:
首先明确 dp 数组所存数据的含义。这步很重要,如果不得当或者不够清晰,会阻碍之后的步骤。
然后根据 dp 数组的定义,运用数学归纳法的思想,假设 dp[0…i−1] 都已知,想办法求出 dp[i],一旦这一步完成,整个题目基本就解决了。
但如果无法完成这一步,很可能就是 dp 数组的定义不够恰当,需要重新定义 dp 数组的含义;或者可能是 dp 数组存储的信息还不够,不足以推出下一步的答案,需要把 dp 数组扩大成二维数组甚至三维数组。
这道题,东哥还有一种二分查找的解法,叫做耐心排序。(东哥代码放下面了)