目录
一、判断子序列 ——>删除元素
1.1、dp定义
1.2、递推公式
1.3、初始化
1.4、遍历顺序
1.5、解题代码
二、不同的子序列 ——>删除元素
2.1、dp定义
2.2、递推公式
2.3、初始化
2.4、遍历顺序
2.5、解题代码
三、两个字符串的删除操作 ——>删除元素
3.1、dp定义
3.2、递推公式
3.3、初始化
3.4、遍历顺序
3.5、解题代码
四、编辑距离 ——>删除、增加、替换元素
4.1、dp定义
4.2、递推公式
4.3、初始化
4.4、遍历顺序
4.5、解题方程
小结
一、判断子序列 ——>删除元素
题目描述:
题目来源:392. 判断子序列
1.1、dp定义
分析:
判断s是否为t的子序列,实际上就是在求s和t的最长公共子序列的长度是否和s的长度相等,这个问题也是“编辑距离”中的一个入门级别的题目。
如果对最长公共子序列不太理解的,可以看看这篇:
http://t.csdn.cn/DAaaK
dp定义:
dp[i][j]表示字符串s以i - 1为结尾,字符串t以j - 1为结尾的最长公共子序列的长度。
建议:对于两个字符串要比较相同字符的情况,都最好定义成以i - 1,j - 1结尾~
1.2、递推公式
分析:
dp[i][j]就表示最长公共子序列的长度,我们该怎么推出他呢?
1.当s[i] - 1 == t[i - 1]时, 说明当前这对字符相等,如下:
那么dp[i][j]就等于s和t相同的这个字符前面这一段最长公共子序列 + 1得到,+1就表示s和t的当前对比的字符相等,所以在原来的基础上dp[i - 1][j - 1]进行+1操作,如下:
2.当s[i] != t[i]时,说明当前这对字符不相等,那么就不进行+1操作,那么当前状态dp[i][j]就可以由dp[i][j - 1]来进行表示,相当于删除了t的一个字符,让s的第i - 1和t的第j - 2个元素比较,而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.3、初始化
分析:
之前我们将dp数组定义成i - 1和j - 1,为什么不是i,j呢?原因就在初始化这里!
若定义成i,j,假设我们定义成以i为结尾,初始化的时候nums[i][0]和nums[0][j]该怎么初始化?dp[i][0]就是“nums1以nums1[i]为结尾,nums2以nums2[0]为结尾的最长子数组”,那么我们就需要揪住nums2[0]这个元素去遍历nums1这个数组,去统计相同的元素,同理,nums[0][j]也是如此!
但如果我们定义成i - 1, j - 1,nums[i][0]这里的0 - 1就是-1,相当于空串,空串的最长公共子序列自然是0,所以直接初始化成0即可;dp数组中其他元素初始化成什么都可以,因为在递推的过程中都会被覆盖。
1.4、遍历顺序
由递推公式可以知道,推出dp[i][j]都是需要前一个元素,所以只需要从上到下,从左往右遍历即可~
1.5、解题代码
class Solution {
public boolean isSubsequence(String s, String t) {
int len1 = s.length();
int 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] == s.length();
}
}
二、不同的子序列 ——>删除元素
题目描述:
题目来源:115. 不同的子序列
2.1、dp定义
分析:
按照之前的套路, 对于两个字符串要比较相同字符的情况,定义成以i - 1,j - 1结尾即可。
定义:
dp[i][j]表示以字符串s的第i - 1个字符为结尾,以字符串t的第j - 1个字符为结尾,s的子序列中t的个数。
2.2、递推公式
分析:
1.当s[i - 1] == t[j - 1]时,就不用考虑当前i-1,j-1为结尾表示的字符了,也就是考虑i-2,j-2为结尾的个数dp[i - 1][j - 1],因为这个状态之前已经推出,直接拿来用即可,如下:
dp[i][j] = dp[i - 1][j - 1]就完了吗?实际上我们这里写的s[i - 1]表示是否要考虑这个元素,以上是考虑这个元素,那么不考虑又是怎么回事呢?如下:
为什么不考虑dp[i][j - 1]呢?因为本题求的是从s的子序列中有多少个t!
2.当s[i - 1] != t[j - 1]时,就删掉s一个元素,考虑s的以s[i - 2]为结尾的序列即可。
递推公式:
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.3、初始化
分析:
从递推公式中可以看出,一直往前递推,找到根源就是需要知道dp[i][0]和dp[0][j]。
dp[i][0]表示s以i - 1为结尾,t以0 - 1为结尾,s的子序列中t的个数。
那么t就相当于是空串,s的子序列中有几个空串呢?当把s中所有的字符都删除掉,就是一个空串了,因此dp[i][0] = 1;
dp[0][j]表示s以0 - 1为结尾,t以j - 1为结尾,s的子序列中t的个数。
这里无论s本身就是一个空串了,就更别谈子序列了,也就是说s无论怎么删,t都不可能等于s,除非t也是空串,因此dp[j][0] = 0,dp[0][0] = 1;
初始化:
dp[i][0] = 1;
dp[j][0] = 0;
dp[0][0] = 1;
2.4、遍历顺序
由递推公式可以知道,只有从以下几个方面可以得到dp[i][j]:
因此遍历顺序因该是从上往下,从左往右。
2.5、解题代码
class Solution {
public int numDistinct(String s, String t) {
int len1 = s.length();
int 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];
}
}
三、两个字符串的删除操作 ——>删除元素
题目描述:
题目来源:583. 两个字符串的删除操作
3.1、dp定义
分析:
对于两个字符串要比较相同字符的情况,都最好定义成以i - 1,j - 1结尾(如果不明白,建议看看上面几道题的讲解,已经说的很清楚了),dp[i][j]就定义成最终问题的解即可。
dp定义:
dp[i][j]:字符串word1以i - 1为结尾,字符串word2以j - 1为结尾的,使两字符串相同的最小步数。
3.2、递推公式
分析:
1.当w1[i - 1] == w2[j - 1]时,也就是当前对比的两个字符相等时,说明不需要进行删除操作,就可以先忽略这对字符,用这一对字符前面的字符串(dp[i - 1][j - 1])表示使字符串相同的最小步数即可。
2.当w1[i - 1] != w2[j - 1]时,也就是当前对比的两个字符不相等时,说明需要进行删除操作,这时候就需要考虑到底是删w1[i - 1]合适(删除后,比较之前进行删除操作的步骤更少)还是删w2[j - 1]合适,那么这时候就需要看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] = Math.min(dp[i - 1][j], dp[i][j - 1]) + 1;
}
3.3、初始化
分析:
从递推公式中可以看出,要初始化 dp[i - 1][j - 1]、dp[i - 1][j]、dp[i][j - 1] 一直往前递推,找到根源就是需要知道dp[i][0]和dp[0][j]。
1.dp[i][0]表示w1以i - 1为结尾,w2以0 - 1为结尾,使w1和w2相同的最小步数。
也就是说w2是一个空串,通过删除w1的字符,使两个字符串相等,具体的,如下图:
2.dp[0][j]同理。
初始化:
for(int i = 0; i <= len1; i++) {
dp[i][0] = i;
}
for(int j = 0; j <= len2; j++) {
dp[0][j] = j;
}
3.4、遍历顺序
由递推公式可以知道,只有从以下几个方面可以得到dp[i][j]:
因此遍历顺序因该是从上往下,从左往右。
3.5、解题代码
class Solution {
public int minDistance(String word1, String word2) {
int len1 = word1.length();
int 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 = 0; 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];
}
}
四、编辑距离 ——>删除、增加、替换元素
Ps:建议理解了上面讲解的三道题,再来看这道题,就不难掌握了~
题目描述:
题目来源:72. 编辑距离
4.1、dp定义
分析:
对于两个字符串要比较相同字符的情况,都最好定义成以i - 1,j - 1结尾(如果不明白,建议看看上面几道题的讲解,已经说的很清楚了),dp[i][j]就定义成最终问题的解即可。
dp定义:
dp[i][j]:字符串w1以i - 1为结尾,字符串w1以j - 1为结尾,w1转化成w2的最少操作次数。
4.2、递推公式
分析:
1.当前对比的这对字符相同时,就不用进行任何的编辑操作(增加、删除、替换),也就是说可以忽略这对字符,直接用这对 字符 前面的 字符串的状态(dp[i - 1][j - 1])来表示当前状态dp[i][j];
2.当前对比的这对字符不相同时,想要让w1转化成w2,可以对字符进行增加、删除、替换的操作,最后从这些操作中选出一个操作次数最少的来表示dp[i][j],具体的:
1) 增加、删除元素
先来谈谈删除元素,当前这对字符不相同,我们因该考虑的是删除w1[i]还是w2[j]这个字符合适,怎么才算合适?就要看忽略(这里的忽略就是删除操作)了w1[i](忽略后的状态为dp[i - 1][j],w1以i - 2为结尾,w2以j - 1为结尾的最小编辑距离)或w2[j](忽略后的状态为dp[i][j - 1])以后,谁的编辑距离要更小!最后再+1,这个+1就表示删除元素这一步的操作,最后就得到了dp[i][j]这个状态。
那么增加元素呢?实际上,增加和删除元素的转移方程是一样的,因为站在w1的角度来删除自身的元素,站在w2的角度看就是在增加自己的元素,如下图:
2)替换元素
替换元素就是在dp[i -1][j - 1]这个状态的基础上,通过替换,使下一对字符相等的一个操作!也就是说,替换就是为了当前这对元素相等,在dp[i - 1][j - 1]这个状态上通过一个+1(+1就是替换)的操作,使下一对字符变成相等。
状态转移方程:
if(word1.charAt(i - 1) == word2.charAt(j - 1)) { dp[i][j] = dp[i - 1][j - 1]; } else { //增加删除 int addDel = Math.min(dp[i - 1][j], dp[i][j - 1]) + 1; //替换 int replace = dp[i - 1][j - 1] + 1; dp[i][j] = Math.min(addDel, replace); }
4.3、初始化
分析:
从递推公式中可以看出,要初始化 dp[i - 1][j - 1]、dp[i - 1][j]、dp[i][j - 1] 一直往前递推,找到根源就是需要知道dp[i][0]和dp[0][j]。
1.dp[i][0]表示w1以i - 1为结尾,w2以0 - 1为结尾,使w1和w2相同的最小编辑距离
也就是说w2是一个空串,通过删除w1的字符或增加w2的字符,使两个字符串相等,具体的,如下图:
2.dp[0][j]同理。
初始化:
for(int i = 0; i <= len1; i++) {
dp[i][0] = i;
}
for(int j = 0; j <= len2; j++) {
dp[0][j] = j;
}
4.4、遍历顺序
由递推公式可以知道,只有从以下几个方面可以得到dp[i][j]:
因此遍历顺序因该是从上往下,从左往右。
4.5、解题方程
class Solution {
public int minDistance(String word1, String word2) {
int len1 = word1.length();
int 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 = 0; 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 {
//增加删除
int addDel = Math.min(dp[i - 1][j], dp[i][j - 1]) + 1;
//替换
int replace = dp[i - 1][j - 1] + 1;
dp[i][j] = Math.min(addDel, replace);
}
}
}
return dp[len1][len2];
}
}
小结
面试遇到,还不得装一装(doge)