子序列问题
子序列可能拥有的性质:
- 连续:即子数组(数组场景)、子串(字符串场景)
- 递增
解题总结:
- 子序列可以是不连续的;
- 子数组(子字符串)需要是连续的;
- 当单个数组或者字符串要用动态规划时,可以把动态规划dp[i]定义为nums[0:i] 中想要求的结果;
- 当两个数组或者字符串要用动态规划时,可以把动态规划定义成两维的 dp[i][j] ,其含义是在A[0:i−1] 与B[0:j−1] 之间匹配得到的想要的结果。
- DP数组定义为以nums[i]为结尾,需要遍历dp数组来得到结果;定义为范围[0,i-1]内,则dp的最后一个元素即为结果
- DP数组定义为 数量,最后一位相等时需要考虑不使用最后一位的情况,定义为长度则直接+1
1、单个序列,最长递增类
300. 最长递增子序列(递增子序列)
- 子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序
- 解题步骤:
- dp数组含义:以下标i为结尾的最长递增子序列的长度
要求序列有序,所以必须确定序列最后一个元素的值,才能比较新加入序列的元素是不是递增的
- 递推公式:
for(j<i, nums[i] > nums[j]):dp[i] = max(dp[j]+1, dp[i])
。位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 + 1 的最大值,取dp[j] + 1
的最大值 - 初始化:
dp[i] = 1
- 遍历顺序:dp[i] 是由0到i-1各个位置的最长递增子序列 推导而来。用i从小到大遍历子序列,代表以nums[i]为结尾的子序列。再用j遍历0到i-1,计算dp[i]。
- 结果:在以下标i为结尾的最长递增子序列的长度中取最大值
class Solution {
public int lengthOfLIS(int[] nums) {
int[] dp = new int[nums.length];
int res = 0;
for(int i = 0; i < nums.length; i++){
dp[i] = 1;
for(int j = 0; j < i; j++){
if(nums[i] > nums[j]) dp[i] = Math.max(dp[i],dp[j]+1);
}
res = Math.max(dp[i],res);
}
return res;
}
}
674. 最长连续递增序列(递增子数组)
- 解题步骤:
- dp数组含义:以下标i为结尾的连续递增的子序列长度为dp[i]。一定是以下标i为结尾,并不是说一定以下标0为起始位置
- 递推公式:
if( nums[i] > nums[i-1]):dp[i] = dp[i-1] + 1
。本题要求连续递增子序列,所以就只要比较nums[i]与nums[i - 1],而不用去比较nums[j]与nums[i] (j是在0到i之间遍历) - 初始化:
dp[i] = 1
- 结果:在所有以下标i为结尾的最长连续递增子序列的长度中取最大值
动态规划
class Solution {
public int findLengthOfLCIS(int[] nums) {
if(nums.length == 1) return 1;
int res = 0;
int[] dp = new int[nums.length];
for(int i = 0; i < nums.length; i++){
dp[i] = 1;
}
for(int i = 1; i < nums.length; i++){
if(nums[i] > nums[i-1]) dp[i] = dp[i-1] + 1;
res = Math.max(res,dp[i]);
}
return res;
}
}
贪心
class Solution {
public int findLengthOfLCIS(int[] nums) {
if(nums.length == 1) return 1;
int res = 0;
int max = 1;
for(int i = 1; i < nums.length; i++){
if(nums[i] > nums[i-1]) max++;
else{
max = 1;
}
res = Math.max(max,res);
}
return res;
}
}
2、两个序列
718. 最长重复子数组(子数组)
- 子数组其实是连续子序列
- 解题步骤:
- dp数组含义:以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。“以下标i - 1为结尾的A” 标明一定是 以A[i-1]为结尾的子数组
求连续相等子序列,则必须确定序列最后一个元素的值
- 递推公式:
- 当nums1[i - 1] 和nums2[j - 1]相等时, dp[i][j]的状态由dp[i - 1][j - 1]推导出来,故
dp[i][j] = dp[i-1][j-1] + 1
; - 当nums1[i - 1] 和nums2[j - 1]不相等时,由于子数组的连续性,前缀数组不能为它们俩提供公共长度,故
dp[i][j] = 0
- 当nums1[i - 1] 和nums2[j - 1]相等时, dp[i][j]的状态由dp[i - 1][j - 1]推导出来,故
- 初始化:长度为0时,
dp[0][j] = 0 ,dp[i][0] = 0
- 遍历顺序:从小到大,先遍历nums1或者nums2都可以,遍历时记录最大值
class Solution {
public int findLength(int[] nums1, int[] nums2) {
int[][] dp = new int[nums1.length+1][nums2.length+1];
int res = 0;
for(int i = 1; i <= nums1.length; i++){
for(int j = 1; j <= nums2.length; j++){
if(nums1[i-1] == nums2[j-1]) dp[i][j] = dp[i-1][j-1] + 1;
//表明了一定是以下标i为结尾
else dp[i][j] = 0;
res = Math.max(dp[i][j], res);
}
}
return res;
}
}
- 结果:题目要求长度最长的子数组的长度。所以在遍历的时候顺便把dp[i][j]的最大值记录下来。
1143. 最长公共子序列(子序列)
- 元素可以不连续;
- 解题步骤:
- dp数组含义:[0, i - 1]范围内的字符串text1与[0, j - 1]范围内的字符串text2的最长公共子序列长度为dp[i][j]
求不必连续的相等子序列,就不需要知道序列最后一个元素的值,只要知道范围内相等的序列长度就行,新来的相等元素可以直接加在序列后面
- 递推公式:
if( text1[i-1] == text2[j-1]), dp[i][j] = dp[i - 1][j - 1] + 1
。必然使用text1[i-1] 与 text2[j-1] 时if(text1[i-1] != text2[j-1]), dp[i][j] = max(dp[i-1][j],dp[i][j-1])
。必然不使用text1[i-1] 与 必然不使用 text2[j-1] 时
- 遍历顺序
- 举例推导: text1[i-1] 不等于 text2[j-1]的情况下,比如对于 ace 和 bc 而言,他们的最长公共子序列的长度等于 ① ace 和 b 的最长公共子序列长度0 与 ② ac 和 bc 的最长公共子序列长度1 的最大值,即 1。
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int len1 = text1.length(), len2 = text2.length();
int[][] dp = new int[len1 + 1][len2 + 2];
int res = 0;
for(int i = 1; i <= len1; i++){
for(int j = 1; j <= len2; j++){
if(text1.charAt(i-1) == text2.charAt(j-1)) dp[i][j] = dp[i-1][j-1] + 1;
//这里就说明了不一定以text1[i-1]和text2[j-1]为结尾
else dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);
}
}
return dp[len1][len2];
}
}
- 结果:按照dp数组的定义,
dp[text1.size()][text2.size()]
就为最终结果
1035. 不相交的线(子序列)
实际上求的就是最长公共子序列
class Solution {
public int maxUncrossedLines(int[] nums1, int[] nums2) {
int len1 = nums1.length;
int len2 = nums2.length;
int[][] dp = new int[len1+1][len2+1];
for(int i = 1; i <= len1; i++){
for(int j = 1; j <= len2; j++){
if(nums1[i-1] == nums2[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 dp[len1][len2];
}
}
53. 最大子数组和(子数组)
动态规划
- 解题步骤:
- dp含义:以nums[i]为结尾的最大连续子序列和为dp[i]
- 递推公式:
dp[i] = Math.max(nums[i],dp[i-1]+nums[i])
- 初始化:dp[0] = nums[0], res = dp[0]
- 结果:要找最大的连续子序列,就应该找每一个i为终点的连续最大子序列。遍历时维护一个最大的dp[i]。
class Solution {
public int maxSubArray(int[] nums) {
int[] dp = new int[nums.length];
dp[0] = nums[0];
int res = dp[0];
for(int i = 1; i < nums.length; i++){
dp[i] = Math.max(nums[i],dp[i-1]+nums[i]);
res = Math.max(dp[i],res);
}
return res;
}
}
贪心解法
思路:计算累加和,计算后每次都要比较更新一遍最大值,如果累计和小于0则重置累加和为0,代表从下个元素开始重新计算。
思路简单,但是代码比较难写,错误较多
class Solution {
public int maxSubArray(int[] nums) {
int sum = 0;
int res = -10010;
for(int i = 0; i < nums.length; i++){
sum+=nums[i];
res = Math.max(res,sum);
if(sum < 0) sum = 0;
}
return res;
}
}
3、编辑距离类
392. 判断子序列(子序列)
动态规划
- 编辑距离的入门题目,因为从题意中我们也可以发现,只需要计算删除的情况,不用考虑增加和替换的情况
- 问题等价转换在 t 中找到和 s 相同子序列的长度 == s 的大小
- 解题步骤:
- dp数组含义:[0, i - 1]范围内的字符串s与[0, j - 1]范围内的字符串t的相同子序列长度为dp[i][j]
- 递推公式:
if s[i−1]==t[j−1], dp[i][j]=dp[i−1][j−1]+1
。必然使用s[i-1] 与 t[j-1] 时if s[i−1]!=t[j−1], dp[i][j]=dp[i][j−1]
。必然不使用 t[j-1] 时
- 初始化
- 遍历顺序
class Solution {
public boolean isSubsequence(String s, String t) {
int slen = s.length();
int tlen = t.length();
int[][] dp = new int[slen+1][tlen+1];
for(int i = 1; i <= slen; i++){
for(int j = 1; j <= tlen; j++){
if(s.charAt(i-1) == t.charAt(j-1)) dp[i][j] = dp[i-1][j-1] + 1;
//表明了不一定要以t[j-1]为结尾,但是一定要以s[i-1]为结尾
else dp[i][j] = dp[i][j-1];
}
}
return dp[slen][tlen] == slen;
}
}
暴力
class Solution {
public boolean isSubsequence(String s, String t) {
if(t.length() == 0 && s.length() != 0) return false;
if(s.length() == 0) return true;
int slen = s.length();
int tlen = t.length();
int c = 0;
for(int i = 0; i < tlen; i++){
if(s.charAt(c) == t.charAt(i)){
c++;
if(c == slen) return true;
}
}
return false;
}
}
115. 不同的子序列(子序列)
- 解题步骤:
- dp数组含义:[0, i - 1]范围内的字符串s,其子序列中出现j-1为结尾的字符串t的个数为dp[i][j]
- 递推公式:
if s[i-1] == t[j-1]
,dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
。使用s[i-1]的情况+考虑不使用s[i-1]的情况;if s[i-1] != t[j-1]
,dp[i][j] = dp[i-1][j]
。只考虑不使用s[i-1]的情况。
为什么还要考虑 不用s[i - 1]来匹配。例如: s:bagg 和 t:bag ,s[3] 和 t[2]是相同的,但是字符串s也可以不用s[3]来匹配,即用s[0]s[1]s[2]组成的bag。
- 初始化:d[i][0] = 1,dp[0][j] = 0,dp[0][0] = 1;
- 遍历顺序
- 举例推导
class Solution {
public int numDistinct(String s, String t) {
int slen = s.length();
int tlen = t.length();
int[][] dp = new int[slen+1][tlen+1];
for(int i = 0; i <= slen; i++) dp[i][0] = 1;
for(int j = 1; j <= tlen; j++) dp[0][j] = 0;
for(int i = 1; i <= slen; i++){
for(int j = 1; j <= tlen; j++){
if(s.charAt(i-1) == t.charAt(j-1)) dp[i][j] = dp[i-1][j-1] + dp[i-1][j];
//表明了不一定以s[i-1]为结尾,但是一定以t[j-1]为结尾
else dp[i][j] = dp[i-1][j];
}
}
return dp[slen][tlen];
}
}
583. 两个字符串的删除操作(子序列)
- 两个字符串可以相互删
- 解题步骤:
- dp数组:[0, i - 1]范围内的字符串word1,和[0, j - 1]范围内的字符串word2,形成相同字符串的最小删除次数
- 递推公式:
if word1[i - 1] == word2[j - 1], dp[i][j] = dp[i-1][j-1]
;if 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)
。删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。
- 初始化:dp[i][0] = i,dp[0][j] = j
- 遍历顺序
- 举例推导
class Solution {
public int minDistance(String word1, String word2) {
int w1len = word1.length();
int w2len = word2.length();
int[][] dp = new int[w1len+1][w2len+1];
for(int i = 0; i <= w1len; i++) dp[i][0] = i;
for(int j = 0; j <= w2len; j++) dp[0][j] = j;
for(int i = 1; i <= w1len; i++){
for(int j = 1; j <= w2len; 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-1]+2,Math.min(dp[i-1][j],dp[i][j-1])+1);
}
}
}
return dp[w1len][w2len];
}
}
72. 编辑距离(子序列)
- 可以删除、添加、替换
- 解题步骤:
- dp数组:[0, i - 1]范围内的字符串word1,和[0, j - 1]范围内的字符串word2,最近编辑距离为dp[i][j]
- 递推公式:
if (word1[i - 1] == word2[j - 1]), dp[i][j] = dp[i - 1][j - 1]
。if (word1[i - 1] != word2[j - 1]), dp[i][j] = min(dp[i][j] = dp[i - 1][j] , dp[i][j] = dp[i][j - 1] , dp[i][j] = dp[i - 1][j - 1] ) + 1
。操作一:word1删除一个元素;操作二:word2删除一个元素;操作三:替换元素。word2添加一个元素,相当于word1删除一个元素
- 初始化:dp[i][0] = i,dp[0][j] = j
- 遍历顺序
- 举例推导
class Solution {
public int minDistance(String word1, String word2) {
int w1len = word1.length();
int w2len = word2.length();
int[][] dp = new int[w1len+1][w2len+1];
//dp[i][j]:[0,i-1],[0,j-1]
for(int i = 0; i <= w1len; i++) dp[i][0] = i;
for(int j = 0; j <= w2len; j++) dp[0][j] = j;
for(int i = 1; i <= w1len; i++){
for(int j = 1; j <= w2len; 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 - 1][j], dp[i][j - 1]),dp[i - 1][j - 1]) + 1;
}
}
}
return dp[w1len][w2len];
}
}
4、回文类
647. 回文子串(子串)
- 解题思路:判断一个子字符串[i,j]是否回文,依赖于,子字符串[i + 1, j - 1]是否是回文。若子字符串[i,j]回文,则res++。用二维的DP数组来表示一段范围内的字符串是否为回文字符串
- 解题步骤:
- dp数组含义:dp[i][j]表示[i, j]范围内的字串是否为回文子串
- 递推公式:
if( dp[i] != dp[j]), d[i][j] = false
if (dp[i] == dp[j])
。(1)if( i == j || j - i == 1),dp[i][j] = true
,比如a,aa。(2)if( j - i > 1 ), dp[i][j] = dp[i+1][j-1]
,看子字符串[i + 1, j - 1]是否是回文
- 初始化:dp[i][j] = false
- 遍历顺序: dp[i][j]依赖于dp[i+1][j-1],遍历i时从大往小,遍历j时从小往大
class Solution {
public int countSubstrings(String s) {
int len = s.length();
int res = 0;
char[] str = s.toCharArray();
boolean[][] dp = new boolean[len][len];
for(int i = len - 1; i >= 0; i--){
for(int j = i; j < len; j++){
if(str[i] == str[j]){
if(j - i <= 1) dp[i][j] = true;
else dp[i][j] = dp[i+1][j-1];
}
if(dp[i][j]) res++;
}
}
return res;
}
}
516. 最长回文子序列(子序列)
- 解题步骤:
- dp数组含义:字符串s在[i, j]范围内最长回文子序列的长度为dp[i][j]
- 递推公式:
if( dp[i] != dp[j]), dp[i][j] = max(dp[i+1][j],dp[i][j-1])
。分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列;if (dp[i] == dp[j])
。(1)if( i == j ),dp[i][j] = 1
,比如a。(2)if( j > i ), dp[i][j] = dp[i+1][j-1]+2
- 初始化:dp[i][i] = 1,这里的初始化过程搬到了递推公式时做
- 遍历顺序:遍历i时从大往小,遍历j时从小往大
class Solution {
public int longestPalindromeSubseq(String s) {
char[] str = s.toCharArray();
int len = s.length();
int[][] dp = new int[len][len];
for(int i = len - 1; i >= 0; i--){
for(int j = i; j < len; j++){
if(str[i] == str[j]){
if(j == i) dp[i][j] = 1;
else dp[i][j] = dp[i+1][j-1]+2;
}else{
dp[i][j] = Math.max(dp[i+1][j],dp[i][j-1]);
}
}
}
return dp[0][len-1];
}
}
混淆点
1. 为什么两个子序列最后一位相等时,有时需要考虑不使用最后一位的情况?
看dp数组的含义,题目《1143. 最长公共子序列》中DP数组定义的是长度,最后一位相等时,确实是长度+1,而题目《115. 不同的子序列》中DP数组定义的是数量,最后一位相等时,长度确实时+1,但是数量=使用最后一位的数量+不使用最后一位的数量。
因此题目《392. 判断子序列》可以将DP定义为长度或者是数量,解法都没有问题