总结今日动态规划
1143. 最长公共子序列力扣
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。示例 1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3 。
示例 2:输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3 。
直接说结论的思考过程。其实可以这样想,假如每次我们都取到了最后一个字母了(实际上不一定是),那么就有四种可能,都选,或者二选一,再或者都不选,那么我们来定义一个二维数组,dp[i][j]代表0 -- i 和0 -- j 中最大有多少个公共子序列,然后根据上述结论
1.两个一样,那么都可以选
dp[i][j] = d[i - 1][j - 1] + 1
2.不一样,那么就得讨论
dp[i][j] = max(max(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1])
其实这里我们可以证明的到dp[i - 1][j - 1] 一定是比前面那两个小的,所以我们直接简化
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
这里还要提一嘴初始化的问题
dp[i][0] = 0 dp[0][j] = 0 这两个式子应该很好理解,就是代表只有一个字符串,另外一个字符串是空的,所以公共子序列一定是0
然后再来看代码怎么写
//二维数组
int n = text1.size(), m = text2.size();
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
for(int i = 1; i <= n; ++i)
for(int j = 1; j <= m; ++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[n][m];
// 一维数组
int n = text1.size(), m = text2.size();
vector<int> dp(m + 1, 0);
for(int i = 1; i <= n; ++i)
{
int pre = dp[0]; // 左上角元素,dp[i - 1][j - 1];
for(int j = 1; j <= m; ++j)
{
int temp = dp[j];
if(text1[i - 1] == text2[j - 1]) dp[j] = pre + 1;
else dp[j] = max(dp[j], dp[j - 1]);
pre = temp;
}
}
return dp[m];
这里提一嘴如何变成一维数组,因为我们只涉及到了左边dp[i][j - 1],上边dp[i - 1][j],左上dp[i - 1][j - 1],所以每次我们更新数据的时候,只有左上会被覆盖,那么就需要我们来一个临时变量去储存左上,也就是代码中的pre,然后每次在j的循环里记录左上。如果写的不清楚,看不懂,建议看灵神的视频最长公共子序列 编辑距离【基础算法精讲 19】_哔哩哔哩_bilibili
583. 两个字符串的删除操作力扣
给定两个单词
word1
和word2
,返回使得word1
和word2
相同所需的最小步数。每步 可以删除任意一个字符串中的一个字符。
示例 1:
输入: word1 = "sea", word2 = "eat" 输出: 2 解释: 第一步将 "sea" 变为 "ea" ,第二步将 "eat "变为 "ea"示例 2:
输入:word1 = "leetcode", word2 = "etco" 输出:4
这个可以说跟上面那个一模一样,只不过你得想的到这个点,要让他们两个字符串经过一系列操作相等,那么其实就是求公共子序列的最大长度,然后两个原本的长度相加减去2倍的公共长度就可以得到答案。
class Solution
{
public:
int minDistance(string text1, string text2)
{
int n = text1.size(), m = text2.size();
vector<int> dp(m + 1, 0);
for(int i = 1; i <= n; ++i)
{
int pre = dp[0]; // 左上角元素,dp[i - 1][j - 1];
for(int j = 1; j <= m; ++j)
{
int temp = dp[j];
if(text1[i - 1] == text2[j - 1]) dp[j] = pre + 1;
else dp[j] = max(dp[j], dp[j - 1]);
pre = temp;
}
}
return n + m - 2 * dp[m];
}
};
72. 编辑距离力扣
给你两个单词
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')
其实这个题也是一样的思路,只不过都得能变式到这一步,首先是删除操作,那么不就是dp[i - 1][j],相当于把dp[i][j]变成了它,然后插入就相当于dp[i][j - 1],因为你插入了之后两个字符串的最后一个字母就相等了,那么其实就是j删除一个,然后就是替换,两个都一样了,然后都得没,所以是dp[i - 1][j - 1],然后来看一下代码怎么写。
int n = word1.size(), m = word2.size();
if(n == 0) return m;
if(m == 0) return n;
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
// 这个题很相似1143
// 其实插入就是dp[i][j - 1],删除就是dp[i - 1][j],替换就是dp[i - 1][j - 1]
for(int i = 1; i <= n; ++i)
{
dp[i][0] = i;
for(int j = 1; j <= m; ++j)
{
dp[0][j] = 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], dp[i][j - 1]),
dp[i - 1][j - 1]) + 1;
}
}
return dp[n][m];
// 还可以改成一维数组,跟1143一样
int minDistance(string word1, string word2)
{
int n = word1.size(), m = word2.size();
if(n == 0) return m;
if(m == 0) return n;
vector<int> dp(m + 1, 0);
for(int j = 1; j <= m; ++j) dp[j] = j;
for(int i = 1; i <= n; ++i)
{
int pre = dp[0];
// 初始化第一列
++dp[0];
for(int j = 1; j <= m; ++j)
{
int temp = dp[j];
if(word1[i - 1] == word2[j - 1]) dp[j] = pre;
else dp[j] = min(min(dp[j - 1], dp[j]), pre) + 1;
pre = temp;
}
}
return dp[m];
}
值得一提的是,这里的初始化和前面两个不一样,因为这里看的是操作数,所以
dp[i][0] = i dp[0][j] = j 因为你从一个字符串变成一个空的,那么肯定要操作你的长度次数才能成功。
97. 交错字符串力扣
给定三个字符串 s1、s2、s3,请你帮忙验证 s3 是否是由 s1 和 s2 交错 组成的。
两个字符串 s 和 t 交错 的定义与过程如下,其中每个字符串都会被分割成若干 非空 子字符串:
s = s1 + s2 + ... + sn
t = t1 + t2 + ... + tm
|n - m| <= 1
交错 是 s1 + t1 + s2 + t2 + s3 + t3 + ... 或者 t1 + s1 + t2 + s2 + t3 + s3 + ...
注意:a + b 意味着字符串 a 和 b 连接。示例 1:
输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
输出:true
示例 2:输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"
输出:false
示例 3:输入:s1 = "", s2 = "", s3 = ""
输出:true
首先这个题官解说不能用双指针,其实是可以的,因为我们正常使用双指针的时候会出现一个错误,那就是一但我们两个都可以匹配的时候不知道该选哪一个,那么我们可以在双指针的基础上写一个记忆化搜索,那么就可以完美的避开选择的问题,这里我就不给出代码了,因为主要是写动态规划的代码。 这里我看别人用了路径方法,其原理就是两个字符串构成一个二维数组,然后每一次都只能往右和往下走力扣,然后我们定义一个二维数组,dp[i][j]这里代表前i个字符和前j个字符可以构成目标i + j 的部分吗。那么我们每一次都只要比较最后一个字,直接看代码。
class Solution
{
public:
// 这个题得画个图就清楚了
// 二维数组求路径是否合格
// https://leetcode.cn/problems/interleaving-string/solutions/335561/lei-si-lu-jing-wen-ti-zhao-zhun-zhuang-tai-fang-ch/
bool isInterleave(string s1, string s2, string s3)
{
auto f = vector < vector <int> > (s1.size() + 1, vector <int> (s2.size() + 1, false));
int n = s1.size(), m = s2.size(), t = s3.size();
if (n + m != t)
{
return false;
}
f[0][0] = true;
for (int i = 0; i <= n; ++i)
{
for (int j = 0; j <= m; ++j)
{
int p = i + j - 1;
if (i > 0)
{
f[i][j] |= (f[i - 1][j] && s1[i - 1] == s3[p]);
}
if (j > 0)
{
f[i][j] |= (f[i][j - 1] && s2[j - 1] == s3[p]);
}
}
}
return f[n][m];
}
};
1137. 第 N 个泰波那契数力扣
简单题,就不解释了。
总结,这一次的动态规划其实跟记忆化搜索很像,都是选与不选,然后由上一个状态转移过来,说实话跟背包问题很像,等有时间把背包问题也写一篇博客来介绍。