说明
编辑距离序列问题算是动态规划问题中的一个小分支,这里单独写一篇文章介绍。至于动态规划基础问题和详细的处理步骤我在我的另一篇文章中详细介绍过。具体解决步骤请移步观看——动态规划基础篇。如果想了解01背包问题和滚动数组相关内容请移步观看——动态规划——01背包问题。
例题讲解
1.判断⼦序列
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
提示:
- 0 <= s.length <= 100
- 0 <= t.length <= 10^4
- 两个字符串都只由小写字符组成
1.1确定dp(dp table)数组及其下标的含义
dp[i][j]:以s.charAt(i-1)结尾的字符串s和以t.charAt(j-1)结尾的字符串t的相同子序列的长度为dp[i][j]。
大家看到这里可能会疑惑,这里不是要判断s 是否为 t 的子序列吗?为什么要将dp数组含义定位相同子序列长度?大家可以这样理解,如果递推结束后,最终结果dp[s.length()][t.length()]=s.length(),不就意味着s是t的子序列。
1.2确定递推公式
在确定递推公式的时候,首先要考虑如下两种操作,整理如下:
-
if(s.charAt(i-1)==t.charAt(j-1))
-
if(s.charAt(i-1)!=t.charAt(j-1))
如果s.charAt(i-1)==t.charAt(j-1),s.charAt(i-1)和t.charAt(j-1)也可以作为相同子序列中的一个字符,所以dp[i][j]=dp[i-1][j-1]+1;
如果s.charAt(i-1)!=t.charAt(j-1),我们可以删除t.charAt(j-1),那么此时以s.charAt(i-1)结尾的字符串s和以t.charAt(j-1)结尾的字符串t的相同子序列的长度显然等于s.charAt(i-1)结尾的字符串s和以t.charAt(j-2)结尾的字符串t的相同子序列的长度,因为我们已经把t.charAt(j-1)删掉了,所以dp[i][j]=dp[i][j-1]。
综上分析,递推公式如下:
if(s.charAt(i-1)==t.charAt(j-1)){
dp[i][j]=dp[i-1][j-1]+1;
}else{
dp[i][j]=dp[i][j-1];
}
1.3dp数组初始化
从递推公式可以看出,我们需要初始化dp[i][0]和dp[0][j],显然dp[i][0]=0,dp[0][j]=0;
1.4代码示例
class Solution {
public boolean isSubsequence(String s, String t) {
int len1=s.length(),len2=t.length();
int[][] dp=new int[len1+1][len2+1];
for(int i=1;i<=len1;i++){
for(int j=1;j<=len2;j++){
if(s.charAt(i-1)==t.charAt(j-1)){
dp[i][j]=dp[i-1][j-1]+1;
}else{
dp[i][j]=dp[i][j-1];
}
}
}
return dp[len1][len2]==len1;
}
}
2.不同的⼦序列
给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。
字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,“ACE” 是 “ABCDE” 的一个子序列,而 “AEC” 不是)
题目数据保证答案符合 32 位带符号整数范围。
提示:
- 0 <= s.length, t.length <= 1000
- s 和 t 由英文字母组成
2.1确定dp(dp table)数组及其下标的含义
dp[i][j]:以s.charAt(i-1)为结尾的s⼦序列中出现以t.charAt(j-1)为结尾的t的个数为dp[i][j]。
2.2确定递推公式
在确定递推公式的时候,首先要考虑如下两种操作,整理如下:
-
if(s.charAt(i-1)==t.charAt(j-1))
-
if(s.charAt(i-1)!=t.charAt(j-1))
如果s.charAt(i-1)==t.charAt(j-1),我们可以选择s.charAt(i-1)加入匹配,那么dp[i][j]=dp[i-1][j-1];也可以选择不让s.charAt(i-1)加入匹配,那么dp[i][j]=dp[i-1][j],例如s=“bagg”,t=“bag”,s.charAt(3)和s.charAt(2)是相同的我们选择s.charAt(3)可以不选择也可以,所以最终dp[i][j]=dp[i-1][j-1]+dp[i-1][j];
如果s.charAt(i-1)!=t.charAt(j-1),dp[i][j]=dp[i-1][j]。
综上分析,递推公式如下:
if(s.charAt(i-1)==t.charAt(j-1)){
dp[i][j]=dp[i-1][j-1]+dp[i-1][j];
}else{
dp[i][j]=dp[i-1][j];
}
2.3dp数组初始化
从递推公式可以看出,我们需要初始化dp[i][0]和dp[0][j];
dp[i][0]的含义是以s.charAt(i-1)结尾的字符串s子序列中出现空串的个数,很显然我们只需要把s字符串中所有的字符都删除即可得到空串,而且只有这一种可能,即dp[i][0]=1;
dp[0[][j]:的含义是空串的子序列中出现以t.charAt(j-1)结尾的字符串t,显然dp[0][j]=0。
2.4代码示例
class Solution {
public int numDistinct(String s, String t) {
int len1=s.length(),len2=t.length();
int[][] dp=new int[len1+1][len2+1];
for(int i=0;i<=len1;i++) dp[i][0]=1;
for(int i=1;i<=len1;i++){
for(int j=1;j<=len2;j++){
if(s.charAt(i-1)==t.charAt(j-1)){
dp[i][j]=dp[i-1][j-1]+dp[i-1][j];
}else{
dp[i][j]=dp[i-1][j];
}
}
}
return dp[len1][len2];
}
}
2.5转换成01背包问题
class Solution {
public int numDistinct(String s, String t) {
int len1=s.length(),len2=t.length();
int[] dp=new int[len2+1];
dp[0]=1;
for(int i=0;i<len1;i++){
for(int j=len2;j>0;j--){
if(s.charAt(i)==t.charAt(j-1)) dp[j]+=dp[j-1];
}
}
return dp[len2];
}
}
3.两个字符串的删除操作
给定两个单词 word1 和 word2 ,返回使得 word1 和 word2 相同所需的最小步数。
每步 可以删除任意一个字符串中的一个字符。
提示:
- 1 <= word1.length, word2.length <= 500
- word1 和 word2 只包含小写英文字母
3.1确定dp(dp table)数组及其下标的含义
dp[i][j]:以word1.charAt(i-1)结尾的字符串word1,以word2.charAt(j-1)结尾的字符串word2,使得word1和word2相同的最小步数为dp[i][j]。
3.2确定递推公式
在确定递推公式的时候,首先要考虑如下两种操作,整理如下:
-
if(word1.charAt(i-1)==word2charAt(j-1))
-
if(word1.charAt(i-1)!=word2charAt(j-1))
如果word1.charAt(i-1)==word2charAt(j-1),我们显然不需要做任何操作,即dp[i][j]=dp[i-1][j-1];
如果word1.charAt(i-1)!=word2charAt(j-1),我们可以选择删除word1.charAt(i-1),则dp[i][j]=dp[i-1][j]+1,也可以选择删除word2.charAt(j-1),则dp[i][j]=dp[i][j-1]+1,我们只需要选择步数最小的即可,即dp[i][j]=Math.min(dp[i-1][j],dp[i][j-1])+1;
综上分析,递推公式如下:
if(word1.charAt(i-1)==word2.charAt(j-1)){
dp[i][j]=dp[i-1][j-1];
}else{
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
3.3dp数组初始化
从递推公式可以看出,我们需要初始化dp[i][0]和dp[0][j];
dp[i][0]的含义是word1.charAt(i-1)结尾的字符串word1和空串相同所需最小步数,所以我们需要将word1中所有的字符删去,即dp[i][0]=i;
同理,dp[0][j]=j。
3.4代码示例
class Solution {
public int minDistance(String word1, String word2) {
int len1=word1.length(),len2=word2.length();
int[][] dp=new int[len1+1][len2+1];
for(int i=1;i<=len1;i++) dp[i][0]=i;
for(int j=1;j<=len2;j++) dp[0][j]=j;
for(int i=1;i<=len1;i++){
for(int j=1;j<=len2;j++){
if(word1.charAt(i-1)==word2.charAt(j-1)){
dp[i][j]=dp[i-1][j-1];
}else{
dp[i][j]=Math.min(dp[i-1][j],dp[i][j-1])+1;
}
}
}
return dp[len1][len2];
}
}
4.编辑距离
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
提示:
- 0 <= word1.length, word2.length <= 500
- word1 和 word2 由小写英文字母组成
4.1确定dp(dp table)数组及其下标的含义
dp[i][j]:以word1.charAt(i-1)结尾的字符串word1,以word2.charAt(j-1)结尾的字符串word2,将 word1 转换成 word2 所使用的最少操作数。
4.2确定递推公式
在确定递推公式的时候,首先要考虑如下两种操作,整理如下:
-
if(word1.charAt(i-1)==word2charAt(j-1))
-
if(word1.charAt(i-1)!=word2charAt(j-1))
如果word1.charAt(i-1)==word2charAt(j-1),我们显然不需要做任何操作,即dp[i][j]=dp[i-1][j-1];
如果word1.charAt(i-1)!=word2charAt(j-1),我们有三种选择
- 替换一个字符
替换word1.charAt(i-1)和替换word2.charAt(j-1)所需操作数一样,即dp[i][j]=dp[i-1][j-1]+1; - 删除一个字符
删除word1.charAt(i-1),则dp[i][j]=dp[i-1][j]+1; 删除word2.charAt(j-1),则dp[i][j]=dp[i][j-1]+1; - 插入一个字符
其实对于word1插入一个字符等价于word2删除一个字符,如word1=“flag”,word2=“flagw” ,,word1插入一个字符’w’和word22删除一个字符’w’是等价的,即都是需要操作一次;
同理对于word2插入一个字符等价于word1删除一个字符;
所以其实"插入一个字符这种情况可以不用考虑"
综上分析,递推公式如下:
if(word1.charAt(i-1)==word2.charAt(j-1)){
dp[i][j]=dp[i-1][j-1];
}else{
dp[i][j]=Math.min(Math.min(dp[i-1][j],dp[i][j-1),dp[i-1][j-1])+1;
}
4.3dp数组初始化
从递推公式可以看出,我们需要初始化dp[i][0]和dp[0][j];
dp[i][0]的含义是word1.charAt(i-1)结尾的字符串word1转换为空串所需最小操作数,所以我们需要将word1中所有的字符删去,即dp[i][0]=i;
同理,dp[0][j]=j。
4.4代码示例
class Solution {
public int minDistance(String word1, String word2) {
int len1=word1.length(),len2=word2.length();
int[][] dp=new int[len1+1][len2+1];
for(int i=0;i<=len1;i++) dp[i][0]=i;
for(int j=1;j<=len2;j++) dp[0][j]=j;
for(int i=1;i<=len1;i++){
for(int j=1;j<=len2;j++){
if(word1.charAt(i-1)==word2.charAt(j-1)){
dp[i][j]=dp[i-1][j-1];
}else{
dp[i][j]=Math.min(Math.min(dp[i][j-1],dp[i-1][j]),dp[i-1][j-1])+1;
}
}
}
return dp[len1][len2];
}
}