算法学习——LeetCode力扣动态规划篇10
583. 两个字符串的删除操作
583. 两个字符串的删除操作 - 力扣(LeetCode)
描述
给定两个单词 word1 和 word2 ,返回使得 word1 和 word2 相同所需的最小步数。
每步 可以删除任意一个字符串中的一个字符。
示例
示例 1:
输入: word1 = “sea”, word2 = “eat”
输出: 2
解释: 第一步将 “sea” 变为 “ea” ,第二步将 "eat "变为 “ea”
示例 2:
输入:word1 = “leetcode”, word2 = “etco”
输出:4
提示
1 <= word1.length, word2.length <= 500
word1 和 word2 只包含小写英文字母
代码解析
动态规划
和1143相同,只要求出两个字符串的最长公共子序列长度即可,那么除了最长公共子序列之外的字符都是必须删除的,最后用两个字符串的总长度减去两个最长公共子序列的长度就是删除的最少步数。
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++)
{
for(int j=0 ;j<word2.size() ;j++)
{
if(word1[i] == word2[j]) dp[i+1][j+1] = dp[i][j] + 1;
else dp[i+1][j+1] = max(dp[i][j+1],dp[i+1][j]);
}
}
return word1.size() + word2.size() - 2*dp[word1.size()][word2.size()];
}
};
72. 编辑距离
72. 编辑距离 - 力扣(LeetCode)
描述
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
示例
示例 1:
输入:word1 = “horse”, word2 = “ros”
输出:3
解释:
horse -> rorse (将 ‘h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)
示例 2:
输入:word1 = “intention”, word2 = “execution”
输出:5
解释:
intention -> inention (删除 ‘t’)
inention -> enention (将 ‘i’ 替换为 ‘e’)
enention -> exention (将 ‘n’ 替换为 ‘x’)
exention -> exection (将 ‘n’ 替换为 ‘c’)
exection -> execution (插入 ‘u’)
提示
0 <= word1.length, word2.length <= 500
word1 和 word2 由小写英文字母组成
代码解析
动态规划
- 确定dp数组以及下标的含义
dp[i][j] 表示前 i 个字符的word1,和前 j 个字符的word2,最近编辑距离。 - 递推公式
- if (word1[ i ] == word2[ j ]) 那么说明不用任何编辑,
即dp[i+1][j+1] = dp[i ][j ]; - if (word1[ i ] != word2[ j ]) 有三种情况
- 删除world1
word1第 i 个字符删除一个元素,就是world1第 i -1 与world2第 j 再加上一个操作。
即 dp[i+1][j+1] = dp[ i ][ j+1] + 1; - 删除world2(相当于添加world1)
word2第 j 个字符删除一个元素,就是world1第 i 与world2第 j -1 再加上一个操作。
即 dp[i+1][j+1] = dp[ i +1][ j] + 1; - 替换元素
word1第 i 个字符和word1第 i -1个字符替换,world2同样,就是world1第 i -1 与world2第 j -1 再加上一个操作。
即 dp[i+1][j+1] = dp[ i ][ j] + 1; - 最终三种情况取最小
dp[i+1][j+1] = min( dp[i][j] , min(dp[i][j+1],dp[i+1][j])) + 1;
- 删除world1
- if (word1[ i ] == word2[ j ]) 那么说明不用任何编辑,
- dp数组初始化
-
dp[i][0] :以下标i-1为结尾的字符串word1,和空字符串word2,最近编辑距离为dp[i][0]。
那么dp[i][0]就应该是i,对word1里的元素全部做删除操作,即:dp[i][0] = i; -
同理dp[0][j] = j;
-
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+1][0] = i+1;
for(int j=0 ; j<word2.size();j++)
dp[0][j+1] = j+1;
for(int i=0;i<word1.size();i++)
{
for(int j=0; j<word2.size();j++)
{
if(word1[i] == word2[j]) dp[i+1][j+1] = dp[i][j];
else dp[i+1][j+1] = min( dp[i][j] , min(dp[i][j+1],dp[i+1][j])) + 1;
}
}
return dp[word1.size()][word2.size()];
}
};
647. 回文子串
647. 回文子串 - 力扣(LeetCode)
描述
给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例
示例 1:
输入:s = “abc”
输出:3
解释:三个回文子串: “a”, “b”, “c”
示例 2:
输入:s = “aaa”
输出:6
解释:6个回文子串: “a”, “a”, “a”, “aa”, “aa”, “aaa”
提示
1 <= s.length <= 1000
s 由小写英文字母组成
代码解析
暴力法
class Solution {
public:
bool cheak(const string s , int left,int right)
{
for(int i=left ,j=right; i<=(right-left)/2 +left; i++,j--)
{
if(s[i]!=s[j]) return false;
}
return true;
}
int countSubstrings(string s) {
if(s.size()<=1) return 1;
int num=0;
int left=0,right=1;
for(int left=0 ; left<s.size() ;left++)
{
for(right=left ; right<s.size();right++)
{
if(cheak(s,left,right)==1)
{
num++;
// cout<<left<<' '<<right<<endl;
}
}
}
return num;
}
};
动态规划
-
确定dp数组(dp table)以及下标的含义
布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。 -
确定递推公式
在确定递推公式时,就要分析如下几种情况。-
当s[i]与s[j]不相等,那没啥好说的了,dp[i][j]一定是false。
-
当s[i]与s[j]相等时,这就复杂一些了,有如下三种情况
情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串
情况二:下标i 与 j相差为1,例如aa,也是回文子串
情况三:下标:i 与 j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,这个区间是不是回文就看dp[i + 1][j - 1]是否为true。
-
-
dp数组如何初始化
dp[i][j]可以初始化为true么? 当然不行,怎能刚开始就全都匹配上了。
所以dp[i][j]初始化为false。 -
确定遍历顺序
遍历顺序可有有点讲究了。
首先从递推公式中可以看出,情况三是根据dp[i + 1][j - 1]是否为true,在对dp[i][j]进行赋值true的。
dp[i + 1][j - 1] 在 dp[i][j]的左下角,
一定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的。
因为dp[i][j]的定义,所以j一定是大于等于i的,那么在填充dp[i][j]的时候一定是只填充右上半部分。
class Solution {
public:
int countSubstrings(string s) {
if(s.size()<=1) return 1;
vector<vector<bool>> dp(s.size(),vector<bool>(s.size() , false));
int num = 0;
for(int i=s.size()-1 ; i>=0;i--)
for(int j=i ;j<s.size();j++)
//s[i]==s[j]为首尾相同
//并且j-i <= 1为"a"或者"aa"的情况,为回文串
//如果j-i不是<=1,但是dp[i+1][j-1]==true,也是回文串,因为首位相同中间是回文串
//如dabcd,首位d相同,中间dp[i+1][j-1]为abc也是回文串,即dabcd为回文串
if( s[i]==s[j] &&
(j-i <= 1||dp[i+1][j-1]==true) )
{
num++;
dp[i][j] = true;
}
return num;
}
};
516. 最长回文子序列
516. 最长回文子序列 - 力扣(LeetCode)
描述
给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
示例
示例 1:
输入:s = “bbbab”
输出:4
解释:一个可能的最长回文子序列为 “bbbb” 。
示例 2:
输入:s = “cbbd”
输出:2
解释:一个可能的最长回文子序列为 “bb” 。
提示
1 <= s.length <= 1000
s 仅由小写英文字母组成
代码解析
动态规划
-
确定dp数组(dp table)以及下标的含义
dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]。 -
确定递推公式
在判断回文子串的题目中,关键逻辑就是看s[i]与s[j]是否相同。- 如果s[i]与s[j]相同,
- j - i ==0 , dp[i][j] = 1;
- j - i == 1, dp[i][j] = 2;
- j - i > 2, dp[i][j] = dp[i + 1][j - 1] + 2;
- 如果s[i]与s[j]不同
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
- 如果s[i]与s[j]相同,
- 确定遍历顺序
从递推公式dp[i][j] = dp[i + 1][j - 1] + 2 和 dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]) 可以看出,dp[i][j]是依赖于dp[i + 1][j - 1] 和 dp[i + 1][j],
也就是从矩阵的角度来说,dp[i][j] 下一行的数据。 所以遍历i的时候一定要从下到上遍历,这样才能保证,下一行的数据是经过计算的。
class Solution {
public:
int longestPalindromeSubseq(string s) {
if(s.size()<=1) return s.size();
vector<vector<int>> dp(s.size() , vector<int>(s.size() ,0));
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) dp[i][j] = 1;
else if(j-i==1) dp[i][j] = 2;
else dp[i][j] = dp[i+1][j-1] + 2;
if(dp[i][j] > result) result = dp[i][j];
}else
{
dp[i][j] = max(dp[i][j-1],dp[i+1][j]);
}
}
// for(int i=0;i<s.size();i++)
// {
// for(int j=0;j<s.size();j++)
// cout<<dp[i][j]<<' ';
// cout<<endl;
// }
return result;
}
};