![](https://i-blog.csdnimg.cn/blog_migrate/253f0b9fc7bc3a4440a6449b693d151c.png#pic_center)
392. 判断子序列
动态规划
步骤
-
确定dp数组以及下标的含义
dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度。判断s是否为t的子序列。即t的长度是大于等于s的 -
确定递推公式
和题1143一样,不同的是只有t可以删除元素,1143题两个字符串都可以删除元素
- t中找到一个在s中有的字符,
if (s[i - 1] == t[j - 1])
,相当于t要删除元素,继续匹配,dp[i][j] = dp[i - 1][j - 1] + 1; - t中没有找到s中有的字符,
if (s[i - 1] != t[j - 1])
,相当于t要删除元素,如果删除当前元素t[j - 1],那么dp[i][j]的数值就是看s[i - 1]与 t[j - 2]的比较结果了,dp[i][j] = dp[i][j - 1];
-
dp数组如何初始化
dp[i][j]依赖于dp[i - 1][j - 1] 和 dp[i][j - 1],dp[0][0]和dp[i][0]都要初始化的,dp[i][0] 表示以下标i-1为结尾的字符串与空字符串的相同子序列长度,所以dp[i][0]=0,同理dp[0][j]=0。
和题718一样,如果dp[i][j]是以下标i为结尾的字符串s和以下标j为结尾的字符串t,需要对第一行和第一列单独初始化,dp[i][0]=1,dp[0][j]=1 -
确定遍历顺序
dp[i][j]依赖于dp[i - 1][j - 1] 和 dp[i][j - 1],遍历顺序从上到下,从左到右
-
举例推导dp数组
以示例一为例,输入:s = “abc”, t = “ahbgdc”,如果过dp[s.size()][t.size()] 与 字符串s的长度相同说明:s与t的最长相同子序列就是s,s 就是 t 的子序列
-
C++实现
时间复杂度mn,时间复杂度mn
class Solution {
public:
bool isSubsequence(string s, string t) {
//动态规划
vector<vector<int>> dp(s.size()+1, vector<int>(t.size()+1, 0));
for(int i=1; i<=s.size(); i++)
{
for(int j=1; j<=t.size(); j++)
{
if(s[i-1] == t[j-1]) dp[i][j] = dp[i-1][j-1] + 1;
else dp[i][j] = dp[i][j-1];
}
}
if(dp[s.size()][t.size()] == s.size()) return true;
return false;
}
};
双指针
两个指针i和j分别指向s和t的起始位置,如果s[i]==t[j],双指针同时后移;如果不相等,i不移动,j后移。如果i到达s的末尾,说明s是t的子序列。
时间复杂度n,空间复杂度1
class Solution {
public:
bool isSubsequence(string s, string t) {
//双指针
int i=0, j=0;
for(; i<s.size() && j<t.size(); j++)
{
if(s[i] == t[j]) i++;
}
//if(i == s.size()) return true;
return i == s.size();
}
};
115.不同的子序列
步骤
-
确定dp数组以及下标的含义
dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数 -
确定递推公式
1)如果s[i - 1] = t[j - 1],dp[i][j]可以有两部分组成。
- 用s[i - 1]来匹配,个数为dp[i - 1][j - 1],即不需要考虑当前s子串和t子串的最后一位字母,只需要 dp[i-1][j-1]
- 不用s[i - 1]来匹配,个数为dp[i - 1][j]
- 例如: s:bagg 和 t:bag ,s[3] 和 t[2]是相同的。不用s[3]来匹配的话就是用s[0]s[1]s[2]组成的bag,用s[3]来匹配的话就是s[0]s[1]s[3]组成的bag。所以,
if(s[i - 1] == t[j - 1]) dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
2)如果s[i - 1] 与 t[j - 1]不相等时,dp[i][j]只有一部分组成
- 不用s[i - 1]来匹配,相当于模拟在s中删除这个元素,
if(s[i - 1] != t[j - 1]) dp[i][j] = dp[i - 1][j];
- 注意题目求的是 s 中有多少个 t,而不是t中有多少个s,所以只考虑 s中删除元素的情况,故不用s[i - 1]来匹配 的情况。
- dp数组如何初始化
- dp[i][0]:以i-1为结尾的s出现空字符串的个数,s删除所有元素出现空字符串的个数只能是1,dp[i][0]=1
- dp[0][j]:空字符串s出现以j-1为结尾的字符串t的个数,s子序列不可能组成t,dp[0][j]=0,
- dp[0][0]:空字符串s,可以删除0个元素,变成空字符串t,dp[0][0]=1
-
确定遍历顺序
dp[i][j]依赖于dp[i - 1][j - 1] 和 dp[i][j - 1],遍历顺序从上到下,从左到右
-
举例推导dp数组
输入,s:“baegg”,t:“bag”
-
C++实现
class Solution {
public:
int numDistinct(string s, string t) {
//三种情况初始化 dp[0][0] dp[i][0] dp[0][j]
//s匹配t dp[i][j] 以i-1为结尾的s子序列中出现以j-1为结尾的t的个数
vector<vector<uint64_t>> dp(s.size()+1, vector<uint64_t>(t.size()+1));//不能用int
for(int i=0; i<s.size(); i++) dp[i][0] = 1;//初始化 dp[0][0] dp[i][0]
for(int j=1; j<t.size(); j++) dp[0][j] = 0;//初始化dp[0][j]
//dp
for(int i=1; i<=s.size(); i++)
{
for(int j=1; j<=t.size(); j++)
{
//s匹配t s:bagg t:bag s[0]s[1]s[2]或者s[0]s[1]s[3] 组成t
if(s[i-1] == t[j-1]) dp[i][j] = dp[i-1][j-1] + dp[i-1][j];
else dp[i][j] = dp[i-1][j];
}
}
return dp[s.size()][t.size()];
}
};
583. 两个字符串的删除操作
方法一
和题115思路一致,两个字符串都可以删除操作。
步骤
-
确定dp数组以及下标的含义
dp[i][j]:以i-1为结尾的字符串word1,和以j-1位结尾的字符串word2,达到相等需要删除元素的最少次数。 -
确定递推公式
1)if(word1[i - 1] == word2[j - 1])
,dp[i][j] = dp[i - 1][j - 1];
2)if(word1[i - 1] != word2[j - 1])
,dp[i][j]有三种情况
- 情况一:删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
- 最少删除次数,取最小值,
dp[i][j] = min({dp[i - 1][j - 1] + 2, dp[i - 1][j] + 1, dp[i][j - 1] + 1});
。 - dp[i][j-1]是以i-1为结尾的字符串word1和以j-2位结尾的字符串,不考虑word2[j - 1],那么在dp[i][j-1]基础上再删除 word1[i - 1],相当于同时删除word1[i - 1]和word2[j - 1],即dp[i][j-1] + 1 = dp[i - 1][j - 1] + 2,递推公式简化
dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);
。
-
dp数组如何初始化
dp[i][0]:word2为空字符串,以i-1为结尾的字符串word1要删除i个元素才能和word2相等,因此dp[i][0] = i
。同理,dp[0][j] = j
-
确定遍历顺序
dp[i][j]依赖于dp[i - 1][j - 1] 和 dp[i][j - 1],遍历顺序从上到下,从左到右
-
举例推导dp数组
输入,word1:“sea”,word2:“eat”
-
C++实现
class Solution {
public:
int minDistance(string word1, string word2) {
vector<vector<int>> dp(word1.size()+1, vector<int>(word2.size()+1));
//初始化
for(int i=0; i<=word1.size(); i++) dp[i][0] = i;
for(int j=0; j<=word2.size(); j++) dp[0][j] = j;
//dp数组更新
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()];
}
};
方法二
和题1143.最长公共子序列一样,只要求出两个字符串的最长公共子序列长度即可。除了最长公共子序列,其他字符都是必须删除的,用两个字符串的总长度减去两个最长公共子序列的长度就是删除的最少步数。
class Solution {
public:
int minDistance(string word1, string word2) {
//方法2
vector<vector<int>> dp(word1.size()+1, vector<int>(word2.size()+1, 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] + 1;
else dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
}
return word1.size()+word2.size() - dp[word1.size()][word2.size()]*2;
}
};
72. 编辑距离
步骤
-
确定dp数组以及下标的含义
dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离 -
确定递推公式
1)if(word1[i - 1] == word2[j - 1])
,字符相同,不操作,dp[i][j] = dp[i - 1][j - 1];
2)if(word1[i - 1] != word2[j - 1])
,字符不同,插入、删除、替换三种操作
- 操作一:word1删除一个元素,相当于是以下标i - 2为结尾的word1 与 j-1为结尾的word2的最近编辑距离dp[i - 1][j] 加一个操作,最少操作次数为:
dp[i][j] = dp[i - 1][j] + 1;
- 操作二:word1删除一个元素,同理dp[i][j - 1]加一个操作,最少操作次数为:
dp[i][j] = dp[i][j - 1] + 1;
- 操作三:替换元素,word1替换word1[i - 1],使其与word2[j - 1]相同。此时相当于字符相同时不操作,那么在此基础上再加一次替换操作即可,
dp[i][j] = dp[i - 1][j - 1] + 1;
- 添加元素相当于删除元素,例如word1 = “ad” ,word2 = “a”,word1删除元素’d’ 和 word2添加一个元素’d’,变成word1=“a”, word2=“ad”, 最终的操作数是一样的。
因此,当 if (word1[i - 1] != word2[j - 1]) 时,最小的操作数为:dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;
-
dp数组如何初始化
dp[i][0]表示以下标i-1为结尾的字符串word1和空字符串word2的最近编辑距离。相当于把word1里的元素全部删除,dp[i][0] = i;
。同理,dp[0][j] = j;
-
确定遍历顺序
- 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[i][j],所以要从前向后,从上到下来遍历
-
举例推导dp数组
以示例1为例,输入:word1 = “horse”, word2 = “ros”
-
C++实现
class Solution {
public:
int minDistance(string word1, string word2) {
vector<vector<int>> dp(word1.size()+1, vector<int>(word2.size()+1, 0));
//初始化 dp[i][0] dp[0][j]
for(int i=0; i<=word1.size(); i++) dp[i][0] = i;
for(int j=0; j<=word2.size(); j++) dp[0][j] = j;
//dp数组更新
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];//字符相同不操作
//字符不相同 替换dp[i - 1][j - 1] + 1
//或者 删word1 dp[i-1][j]+1
//或者 删word2 dp[i][j-1]+1
else dp[i][j] = min({dp[i-1][j-1], dp[i-1][j], dp[i][j-1]}) + 1;
}
}
return dp[word1.size()][word2.size()];
}
};
647. 回文子串
动态规划
时间、空间复杂度:O(n^2)
字符串s=“cbabc”, 如果s[1]、s[2]、s[3]组成的字符串是回文,那么进一步要判断s[0]与s[4]是否相等,如果相等,s就是回文字符串。
步骤
-
确定dp数组以及下标的含义
布尔类型的dp[i][j]:表示左闭右闭区间范围[i,j]的子串是否为回文子串,如果是,dp[i][j]为true,否则为false。 -
确定递推公式
1)s[i]与s[j]不相等,dp[i][j]一定是false。
2)s[i]与s[j]相等时,有如下三种情况
- 情况一:下标i 与 j相同,同一个字符,例如a,回文子串
- 情况二:下标i 与 j相差为1,例如aa,回文子串
- 情况三:下标i 与 j相差大于1时,例如cabac,此时s[i]与s[j]都是“c”,再看[i, j]区间是不是回文子串,也就是看aba是否为回文。aba的区间[i+1, j-1]是否为回文,就看dp[i + 1][j - 1]是否为true。
-
dp数组如何初始化
dp[i][j]初始化为false -
确定遍历顺序
情况三中dp[i][j]依赖于dp[i + 1][j - 1],dp[i + 1][j - 1] 在 dp[i][j]的左下角,所以从下到上,从左到右遍历
-
举例推导dp数组
输入:“aaa”,j一定是大于等于i的,那么在填充dp[i][j]的时候一定是只填充右上半部分
-
C++实现
class Solution {
public:
int countSubstrings(string s) {
vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));//包括初始化
int result = 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)//情况1 2
{
result++;
dp[i][j] = true;
}
else if(dp[i+1][j-1])//情况3
{
result++;
dp[i][j] = true;
}
}
}
}
return result;
}
};
优化
class Solution {
public:
int countSubstrings(string s) {
vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));//包括初始化
int result = 0;
for(int i=s.size()-1; i>=0; i--)
{
for(int j=i; j<s.size(); j++)
{
if(s[i] == s[j] && (j - i <= 1 || dp[i+1][j-1]))
{
result++;
dp[i][j] = true;
}
}
}
return result;
}
};
双指针
时间复杂度:O(n^2),空间复杂度:O(1)
class Solution {
public:
int countSubstrings(string s) {
//双指针
int result = 0;
for(int center=0; center<2 * s.size() - 1; center++)
{
int left = center / 2;
int right = left + center % 2;
while(left>=0 && right<s.size() && s[left]==s[right])
{
left--;
right++;
result++;
}
}
return result;
}
};
516.最长回文子序列
回文子串是要连续的,回文子序列不是连续的
步骤
-
确定dp数组以及下标的含义
dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度 -
确定递推公式
- 如果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]取最大的长度,dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
-
dp数组如何初始化
从递推公式:dp[i][j] = dp[i + 1][j - 1] + 2; 可以看出,递推公式是计算不到 i == j的情况,需要手动初始化。当i与j相同时,dp[i][j]=1,一个字符的回文子序列长度就是1。其他情况dp[i][j]初始为0。 -
确定遍历顺序
dp[i][j] 依赖于 dp[i + 1][j - 1] ,dp[i + 1][j] 和 dp[i][j - 1],从下到上,从左向右遍历。
-
举例推导dp数组
输入s:“cbbd”
-
C++实现
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];
}
};