392. 判断子序列
思路:只需要计算删除的情况,不用考虑增加和替换的情况。动态规划五步曲:
-
dp
[
i][
j]
表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[
i][
j]
。注意这里是判断s是否为t的子序列。即t的长度是大于等于s的。
-
递推公式:分为当前字符相同和当前字符不同两种情况。
① if (s[i - 1] == t[j - 1]),那么dp
[
i][
j]
= dp[
i - 1][
j - 1]
+ 1;,因为找到了一个相同的字符,相同子序列长度自然要在dp[
i - 1][
j - 1]
的基础上加1② if (s[i - 1] != t[j - 1]),此时相当于t要删除元素,t如果把当前元素t[j - 1]删除,那么dp
[
i][
j]
的数值就看s[i - 1]与 t[j - 2]的比较结果了,dp[
i][
j]
= dp[
i][
j - 1]
-
初始化:第一行和第一列初始化为0
在定义dp
[
i][
j]
含义的时候为什么要表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[
i][
j]
。因为这样的定义在dp二维矩阵中可以留出初始化的区间,如图:
-
遍历顺序:从递推公式可以看出dp
[
i][
j]
都是依赖于dp[
i - 1][
j - 1]
和 dp[
i][
j - 1]
,那么遍历顺序也应该是从上到下,从左到右。 -
举例推导dp数组
输入:s = “abc”, t = “ahbgdc”,dp状态转移图如下:
dp
[
i][
j]
表示以下标i-1为结尾的字符串s和以下标j-1为结尾的字符串t 相同子序列的长度,所以如果dp[
s.length()][
t.length()]
与 字符串s的长度相同说明:s与t的最长相同子序列就是s,那么s 就是 t 的子序列。
class Solution {
public boolean isSubsequence(String s, String t) {
char[] arr_s = s.toCharArray();
char[] arr_t = t.toCharArray();
int[][] dp = new int[arr_s.length + 1][arr_t.length + 1];
for (int i = 1; i <= arr_s.length; i++) {
for (int j = 1; j <= arr_t.length; j++) {
if (arr_s[i - 1] == arr_t[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = dp[i][j - 1];
}
}
}
if (dp[arr_s.length][arr_t.length] == s.length()) return true;
return false;
}
}
也可以使用双指针法解题,利用双指针 i, j
分别指向 s, t
,一边前进一边匹配子序列。代码如下:
class Solution {
public boolean isSubsequence(String s, String t) {
int i = 0, j = 0;
while (i < s.length() && j < t.length()) {
if (s.charAt(i) == t.charAt(j)) {
i++;
}
j++;
}
return i == s.length();
}
}
115. 不同的子序列
思路:难度较大,动态规划五步曲:
-
dp
[
i][
j]
:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[
i][
j]
。 -
递推公式:同样是有当前字符相等和当前字符不相等两种情况。
① 当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[i - 1] 与 t[j - 1]相等时,dp
[
i][
j]
= dp[
i - 1][
j - 1]
+ dp[
i - 1][
j]
② 当s[i - 1] 与 t[j - 1]不相等时,dp
[
i][
j]
只有一部分组成。不用s[i - 1]来匹配(就是模拟在s中删除这个元素),即:dp
[
i - 1][
j]
-
初始化:由递推公式可知dp
[
i][
j]
是从上方和左上方推导而来,那么dp[
i][
0]
和dp[
0][
j]
是一定要初始化的。dp
[
i][
0]
表示:以i-1为结尾的s可以随便删除元素,出现空字符串的个数。那么dp[
i][
0]
一定都是1,因为也就是把以i-1为结尾的s,删除所有元素,出现空字符串的个数就是1。dp
[
0][
j]
:空字符串s可以随便删除元素,出现以j-1为结尾的字符串t的个数。那么dp[
0][
j]
一定都是0,s如论如何也变成不了t。最后就要看一个特殊位置了,即:dp
[
0][
0]
应该是多少。dp[
0][
0]
应该是1,空字符串s,可以删除0个元素,变成空字符串t。 -
遍历顺序:从上到下,从左到右遍历。
-
举例推导dp数组
以s:“baegg”,t:"bag"为例,推导dp数组状态如下:
class Solution {
public int numDistinct(String s, String t) {
char[] arr_s = s.toCharArray();
char[] arr_t = t.toCharArray();
int[][] dp = new int[arr_s.length + 1][arr_t.length + 1];
// 初始化
for (int i = 0; i <= arr_s.length; i++) {
dp[i][0] = 1;
}
for (int i = 1; i <= arr_s.length; i++) {
for (int j = 1; j <= arr_t.length; j++) {
if (arr_s[i - 1] == arr_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[arr_s.length][arr_t.length];
}
}
583. 两个字符串的删除操作
思路:本题和动态规划:115.不同的子序列相比,其实就是两个字符串都可以删除了,情况虽说复杂一些,但整体思路是不变的。动态规划五步曲:
-
dp
[
i][
j]
:以i-1为结尾的字符串word1,和以j-1位结尾的字符串word2,想要达到相等,所需要删除元素的最少次数。 -
递推公式:
① 当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那最后当然是取最小值,所以当word1[i - 1] 与 word2[j - 1]不相同的时候,递推公式:dp
[
i][
j]
= min(dp[
i - 1][
j - 1]
+ 2, dp[
i - 1][
j]
+ 1, dp[
i][
j - 1]
+ 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)从字面上理解就是当同时删word1[i - 1]和word2[j - 1],dp
[
i][
j - 1]
本来就不考虑 word2[j - 1]了,那么再删 word1[i - 1],是不是就达到两个元素都删除的效果,即 dp[
i][
j - 1]
+ 1。 -
初始化:dp
[
i][
0]
代表当word2为空串式,以i-1为结尾的字符串word1要删除多少个元素,才能和word2相同呢,很明显,dp[
i][
0]
= i,同理dp[
0][
j]
= j。 -
遍历顺序:从左到右、从上到下去遍历。
-
举例推导dp数组
以word1:“sea”,word2:"eat"为例,推导dp数组状态图如下:
class Solution {
public int minDistance(String word1, String word2) {
char[] arr_1 = word1.toCharArray();
char[] arr_2 = word2.toCharArray();
int[][] dp = new int[arr_1.length + 1][arr_2.length + 1];
// 初始化
for (int i = 0; i <= arr_1.length; i++) dp[i][0] = i;
for (int j = 0; j <= arr_2.length; j++) dp[0][j] = j;
for (int i = 1; i <= arr_1.length; i++) {
for (int j = 1; j <= arr_2.length; j++) {
if (arr_1[i - 1] == arr_2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);
}
}
}
return dp[arr_1.length][arr_2.length];
}
}
第二种思路:本题和动态规划:1143.最长公共子序列基本相同,只要求出两个字符串的最长公共子序列长度即可,那么除了最长公共子序列之外的字符都是必须删除的,最后用两个字符串的总长度减去两个最长公共子序列的长度就是删除的最少步数。
class Solution {
public int minDistance(String word1, String word2) {
char[] arr_1 = word1.toCharArray();
char[] arr_2 = word2.toCharArray();
int[][] dp = new int[arr_1.length + 1][arr_2.length + 1];
for (int i = 1; i <= arr_1.length; i++) {
for (int j = 1; j <= arr_2.length; j++) {
if (arr_1[i - 1] == arr_2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return arr_1.length + arr_2.length - 2 * dp[arr_1.length][arr_2.length];
}
}