583. 两个字符串的删除操作
给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。
1. dp[i][j]:以i-1为结尾的字符串word1,和以j-1位结尾的字符串word2,想要达到相等,所需要删除元素的最少次数。
2. 确定递推公式:
- 当word1[i - 1] 与 word2[j - 1]相同的时候
- 当word1[i - 1] 与 word2[j - 1]不相同的时候
当word1[i - 1] 与 word2[j - 1]相同的时候,dp[i][j] = dp[i - 1][j - 1];
当word1[i - 1] 与 word2[j - 1]不相同的时候:
情况一:删word1[i - 1],最少操作次数为dp[i - 1][j] + 1
情况二:删word2[j - 1],最少操作次数为dp[i][j - 1] + 1
情况三:同时删word1[i - 1]和word2[j - 1],操作的最少次数为dp[i - 1][j - 1] + 2
3. dp[i][0]:word2为空字符串,以i-1为结尾的字符串word1要删除多少个元素,才能和word2相同呢,很明显dp[i][0] = i。
4. 从递推公式 dp[i][j] = min(dp[i - 1][j - 1] + 2, min(dp[i - 1][j], dp[i][j - 1]) + 1); 和dp[i][j] = dp[i - 1][j - 1]可以看出dp[i][j]都是根据左上方、正上方、正左方推出来的。
5. 打印数组:
class Solution {
public:
int minDistance(string word1, string word2) {
vector<vector<int>> dp(word1.size()+1, vector<int>(word2.size()+1, 0));
//dp[i][j]表示截止到i-1和j-1,想要两个words相等最小需要删掉的字母数是
for(int i=0; i<=word1.size(); i++) dp[i][0] = i;
for(int i=0; i<=word2.size(); i++) dp[0][i] = i;
// dp[0][0] = 0;
for(int i=1; i<=word1.size(); i++){
for(int j=1; j<=word2.size(); j++){
if(word1[i-1]==word2[j-1]){
dp[i][j] = dp[i-1][j-1];
}else{
dp[i][j] = min(dp[i-1][j]+1, dp[i][j-1]+1);
}
}
}
return dp[word1.size()][word2.size()];
}
};
- 时间复杂度: O(n * m)
- 空间复杂度: O(n * m)
72. 编辑距离
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
1. dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]
2. 在确定递推公式的时候,首先要考虑清楚编辑的几种操作,整理如下:
if (word1[i - 1] == word2[j - 1])
不操作
if (word1[i - 1] != word2[j - 1])
增
删
换
- 操作一:word1删除一个元素,那么就是以下标i - 2为结尾的word1 与 j-1为结尾的word2的最近编辑距离 再加上一个操作。
即 dp[i][j] = dp[i - 1][j] + 1;
- 操作二:word2删除一个元素,那么就是以下标i - 1为结尾的word1 与 j-2为结尾的word2的最近编辑距离 再加上一个操作。
即 dp[i][j] = dp[i][j - 1] + 1;
word2添加一个元素,相当于word1删除一个元素,例如 word1 = "ad" ,word2 = "a"
,word1
删除元素'd'
和 word2
添加一个元素'd'
,变成word1="a", word2="ad"
, 最终的操作数是一样!
- 操作三:替换元素,
word1
替换word1[i - 1]
,使其与word2[j - 1]
相同,此时不用增删加元素。只需要一次替换的操作,就可以让 word1[i - 1] 和 word2[j - 1] 相同。
即 dp[i][j] = dp[i - 1][j - 1] + 1;
3. 初始化:
跟上一题一样,因为就是在做全删除的操作。
4. 遍历顺序:
从如下四个递推公式:
dp[i][j] = dp[i - 1][j - 1]
dp[i][j] = dp[i - 1][j - 1] + 1
dp[i][j] = dp[i][j - 1] + 1
dp[i][j] = dp[i - 1][j] + 1
所以在dp矩阵中一定是从左到右从上到下去遍历。
5. 举例打印
class Solution {
public:
int minDistance(string word1, string word2) {
vector<vector<int>> dp(word1.size()+1, vector<int>(word2.size()+1, 0));
//初始化的时候就是做全删除的方式
for(int i=0; i<=word1.size(); i++) dp[i][0] = i;
for(int i=0; i<=word2.size(); i++) dp[0][i] = i;
for(int i=1; i<=word1.size(); i++){
for(int j=1; j<=word2.size(); j++){
if(word1[i-1]==word2[j-1]){
dp[i][j] = dp[i-1][j-1];
}else{
dp[i][j] = min(min(dp[i-1][j]+1, dp[i][j-1]+1), dp[i-1][j-1]+1);
}
}
}
return dp[word1.size()][word2.size()];
}
};
注意也可以写成:dp[i][j] = min({dp[i-1][j]+1, dp[i][j-1]+1, dp[i-1][j-1]+1});
- 时间复杂度: O(n * m)
- 空间复杂度: O(n * m)
647. 回文子串
给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。
在分析dp[i][j]的含义的时候,如果我们定义,dp[i] 为 下标i结尾的字符串有 dp[i]个回文串的话,我们会发现很难找到递归关系。
我们在判断字符串S是否是回文,那么如果我们知道 s[1],s[2],s[3] 这个子串是回文的,那么只需要比较 s[0]和s[4]这两个元素是否相同,如果相同的话,这个字符串s 就是回文串。
于是我们可以找到一个递归关系,也就是字符串范围[i, j]是否回文取决于[i+1, j-1]是否回文。
1. 于是我们可以构建一个布尔类型的二维dp数组,表示在范围[i,j]内的子串是否回文,如果是的话则为true,否则为false。
2. 递推公式:取决于两种情况,当s[i]和s[j]不相等时,dp[i][j]是false,而当s[i]和s[j]相等时,有如下三种情况:
下标i和j相同,这个字符串只有一个字符,所以是true
下标i和j只相差1,也是true
下标i和j相差不只是1,那么要看dp[i+1][j-1]是不是true,如果是的话则也是true。
3. 初始化:必须要全部初始化为false
4. 递归顺序:由于dp[i][j]由dp[i+1][j-1]推出,可以判断递归顺序从下往上,从左往右。
5. 打印数组举例
class Solution {
public:
int countSubstrings(string s) {
vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
int res = 0;
for(int i=s.size()-1; i>=0; i--){
for(int j=i; j<s.size(); j++){
if(s[i]==s[j]){
if(j-i<=1){
dp[i][j] = true;
res++;
}else if(dp[i+1][j-1]){
dp[i][j] = true;
res++;
}
}
}
}
return res;
}
};
- 时间复杂度:O(n^2)
- 空间复杂度:O(n^2)
516.最长回文子序列
给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。
示例 1: 输入: "bbbab" 输出: 4 一个可能的最长回文子序列为 "bbbb"。
1. dp数组定义:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]。需要确定一个开头和一个结尾的index
2. 确定递推公式:在判断回文串的题目里面,关键就是看s[i]和s[j]是否相同,如果相同的话,那么dp[i][j] = dp[i + 1][j - 1] + 2;
如果s[i]与s[j]不相同,说明s[i]和s[j]的同时加入 并不能增加[i,j]区间回文子序列的长度,那么分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列。
加入s[j]的回文子序列长度为dp[i + 1][j]。加入s[i]的回文子序列长度为dp[i][j - 1]。
即:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
3. 所以需要手动初始化一下,当i与j相同,那么dp[i][j]一定是等于1的,即:一个字符的回文子序列长度就是1。
其他情况dp[i][j]初始为0就行,这样递推公式:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); 中dp[i][j]才不会被初始值覆盖。
4. 遍历顺序:dp[i][j] 依赖于 dp[i + 1][j - 1] ,dp[i + 1][j] 和 dp[i][j - 1](可以画图看出来是从左下角往右上角遍历)
所以遍历i的时候一定要从下到上遍历,这样才能保证下一行的数据是经过计算的。
而j可以正常的从左到右遍历,就是从大到小遍历
5. 举例推导dp数组
class Solution {
public:
int longestPalindromeSubseq(string s) {
vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
for (int i = 0; i < s.size(); i++) dp[i][i] = 1;
for (int i = s.size() - 1; i >= 0; i--) {
for (int j = i + 1; j < s.size(); 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][s.size() - 1];
}
};
- 时间复杂度: O(n^2)
- 空间复杂度: O(n^2)