TUNIVERSE

动态规划子序列问题

字数统计: 5.1k阅读时长: 22 min
2024/01/16

动态规划

不连续子序列问题

[300] 最长递增子序列

LeetCode Medium

题目

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

解法

只有一个字符串 => 一维dp数组

求递增子序列 => 判断条件即两个树的大小比较

子序列 => 不需要连续,因此要固定一个位置然后再开循环遍历,双层

动态规划五部曲

  1. 确定dp数组的含义

dp[i]表示以i为结尾且包括nums[i]的最长严格递增子序列的最长长度

  1. 确定状态转移方程

若当前元素nums[j]大于nums[i],那么以nums[i]为结尾的子序列就可以加上nums[j],所以长度应该加上1;而如果不大于那么就保持原值即可。我们需要遍历每个小于j的i的值,比较并更新。

1
if (nums[j] > nums[i])	dp[j] = max(dp[j], dp[i] + 1);
  1. 如何初始化dp数组

对于每个dp[j]最初都至少包含了自己,即1

  1. 如何遍历

外层循环固定j表示每一个位置,然后内层循环遍历i,i应该是从0到j-1以此来更新以nums[j]为结尾的子序列的长度

  1. 拿个例子推导一下
image-20240304215613285

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int res = 1;
vector<int> dp(nums.size(), 1);
for (int i = 1; i < nums.size(); i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j])
dp[i] = max(dp[i], dp[j] + 1);
res = max(res, dp[i]);
}
}
return res;
}
};

[1143] 最长公共子序列

LeetCode Medium

题目

给定两个字符串 text1text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

  • 例如,"ace""abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。

两个字符串的公共子序列是这两个字符串所共同拥有的子序列。

解法

  1. 确定dp数组的含义

dp[i][j] 表示对于text1的0到i-1的子串和text2的0到j-1的子串的最长公共子序列的长度

  1. 确定状态转移方程

公共 => 判断两个数字是否相同。如果相同,就是上一个dp[i - 1][j - 1]的公共长度加上1

​ => 不相同,继承上一个状态的最大值,上一个状态是0到i-2和0到j-1(左边一格)的公共子序列长度 以及 0到i-1和0到j-2(右边一格)的公共子序列长度 中的较大值

1
2
3
4
5
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]);
}
  1. 如何初始化dp数组

0下标表示空串,text1和空串公共子序列长度为0,text2同理,所以初始化全为0。

  1. 如何遍历

从小到大遍历

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
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数组的含义
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];
}
};

[1035] 不相交的线

题目

LeetCode Medium

在两条独立的水平线上按给定的顺序写下 nums1nums2 中的整数。

现在,可以绘制一些连接两个数字 nums1[i]nums2[j] 的直线,这些直线需要同时满足满足:

  • nums1[i] == nums2[j]
  • 且绘制的直线不与任何其他连线(非水平线)相交。

请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。

以这种方法绘制线条,并返回可以绘制的最大连线数。

解法

上一道题目(1143)套皮,本质是一道题

连续子序列问题

[674] 最长连续递增序列

题目

LeetCode Easy

给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。

连续递增的子序列 可以由两个下标 lrl < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。

解法

  1. dp数组的含义

dp[i]表示以i结尾下标包含nums[i]连续递增子序列的最长长度

  1. 状态转移方程

递增 => 判断后面是否大于前面

连续 => 不用固定再遍历,只需要简单比较前一位即可,因此只需要一层循环

1
if (nums[i] > nums[i - 1])	dp[i] = dp[i - 1] + 1;
  1. dp数组如何初始化

最短都是自己,因此都初始化为1

  1. 如何遍历

依赖前一个,从前往后遍历

  1. 推导例子

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
vector<int> dp(nums.size(), 1);
int res = 1;
for (int i = 1; i < nums.size(); i++) {
if (nums[i] > nums[i - 1]) {
dp[i] = dp[i - 1] + 1;
}
res = max(res, dp[i]);
}
return res;
}
};

[718] 最长重复子数组

题目

LeetCode Medium

给两个整数数组 nums1nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度

解法

  1. dp数组的含义

dp[i][j]表示nums1[i - 1]和nums2[j - 1]的最长公共子数组

  1. 状态转移方程

公共 => 判断两个元素是否相等,不相等则为0,因为要是连续的

1
2
3
4
if (nums1[i - 1] == nums[j - 1])
dp[i][j] = dp[i - 1][j - 1] + 1;
else
dp[i][j] = 0; // 这个可以省去不写,因为默认就是0
  1. dp数组如何初始化

下标0表示空数组,所以dp[i][0] = 0, dp[0][j] = 0

  1. 如何遍历

从前往后双层遍历,都从1开始,需要一个res来保存

  1. 推导例子
image-20240305115021176

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
int n = nums1.size(), m = nums2.size();
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
int res = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (nums1[i - 1] == nums2[j - 1])
dp[i][j] = dp[i - 1][j - 1] + 1;
res = max(res, dp[i][j]);
}
}
return res;
}
};

[53] 最大子序和

题目

LeetCode Medium

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组是数组中的一个连续部分。

解法

  1. dp数组的含义

dp[i]表示以i为结尾包含nums[i]元素的连续子数组的最大和

  1. 状态转移方程
  • dp[i - 1] + nums[i],即:nums[i]加入当前连续子序列和
  • nums[i],即:从头开始计算当前连续子序列和(类比最长连续递增取1,但是因为本身就是1所以判断完不用写else语句)

一定是取最大的,所以dp[i] = max(dp[i - 1] + nums[i], nums[i]);

  1. 初始化 => dp[i]初始化为nums[i](类比那个最长连续递增初始化为1),但是我们可以用递推公式免去除了0以外的初始化
  2. 遍历顺序 => 从小到大遍历
  3. 推导一遍例子

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
int maxSubArray(vector<int>& nums) {
vector<int> dp(nums.size());
dp[0] = nums[0];
int res = nums[0];
for (int i = 1; i < nums.size(); i++) {
dp[i] = max(dp[i - 1] + nums[i], nums[i]);
res = max(res, dp[i]);
}
return res;
}
};

距离问题

[392] 判断子序列

LeetCode Easy

题目

给定字符串 st ,判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace""abcde"的一个子序列,而"aec"不是)。

进阶:

如果有大量输入的 S,称作 S1, S2, … , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?

解法

  1. 确认dp数组含义

s是t的子序列,dp[i][j]代表s[i - 1]是否是t[j - 1]的子序列

  1. 状态转移方程

判断当前s[i - 1]是否和t[j - 1]相等,如果相等且dp[i - 1][j - 1] == true,则dp[i][j]也为true,所以就是如果相等的话dp[i][j]的值取决于dp[i - 1][j - 1]

如果不相等那么当前字符这个t[j - 1]可能就是我们需要删去的字符,是否为子序列就需要看s[i - 1]和t[j - 2]的匹配结果,即dp[i][j - 1]

  1. 如何初始化dp数组

下标0表示为空串,因此dp[0][j]是true,dp[i][0]除了dp[0][0]其它都为false

  1. 如何遍历 => 从小到大
  2. 推导例子

代码

这道题也可以去记录匹配到的子串的长度,最后判断其是否和s字符串的长度相等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
bool isSubsequence(string s, string t) {
int n = s.size(), m = t.size();
vector<vector<int>> dp(n + 1, vector<int>(m + 1, false));
for (int i = 0; i <= m; i++) dp[0][i] = true;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s[i - 1] == t[j - 1])
dp[i][j] = dp[i - 1][j - 1];
else
dp[i][j] = dp[i][j - 1];
}
}
return dp[n][m];
}
};

[115] 不同的子序列

LeetCode Hard

题目

给你两个字符串 st ,统计并返回在 s子序列t 出现的个数,结果需要对 109 + 7 取模。

解法

  1. dp数组的含义

求什么就定义什么,因为求s中有多少个t,因此dp[i][j]就表示s[i - 1]中t[j - 1]的个数

  1. 状态转移方程

(1)如果当前s[i - 1]和t[j - 1]相等,那么我们就要看dp[i - 1][j - 1]的大小,比如beag和bag依赖于bea和ba中的个数,因为后者为1所以递推前者也为1。但是这是最简单的情况,因为我们可以选择不用s[i - 1]来匹配。

例如: s:bagg 和 t:bag ,s[3] 和 t[2]是相同的,但是字符串s也可以不用s[3]来匹配,即用s[0]s[1]s[2]组成的bag。当然也可以用s[3]来匹配,即:s[0]s[1]s[3]组成的bag。

如果不选择用s[i - 1]来匹配即等于删去此字符的操作,个数就取决于s[i - 2]匹配t[j - 1]的情况,即dp[i - 1][j]

所以即

1
2
if (s[i - 1] == t[j - 1])
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];

(2)如果不等的话就单纯只有dp[i - 1][j]

  1. 初始化问题

dp[i][0]表示s里以s[i - 1]为结尾的子串中空串的个数,都为1

dp[0][j]表示空串里出现的t[j - 1]的个数,为0

其中具体问题具体分析dp[0][0]应该是0。

  1. 遍历,从小到大遍历
  2. 例子推导

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int numDistinct(string s, string t) {
int n = s.size(), m = t.size();
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
for (int i = 0; i <= n; i++) dp[i][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
dp[i][j] = dp[i - 1][j];
if (s[i - 1] == t[j - 1])
dp[i][j] += dp[i - 1][j - 1];
dp[i][j] = dp[i][j] % ((int)1e9 + 7);
}
}
return dp[n][m];
}
};

[583] 两个字符串的删除操作

LeetCode Medium

题目

给定两个单词 word1word2 ,返回使得 word1word2 相同所需的最小步数

每步 可以删除任意一个字符串中的一个字符。

解法

当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

如果不相等的情况为什么不考虑同时删除两个不相等元素即dp[i][j] = dp[i - 1][j - 1] + 2的情况,因为dp[i][j - 1]的值是由dp[i - 1][j - 1] + 1dp[i][j - 2] + 1以及dp[i - 1][j - 2] + 2得来的,而且是取得最优情况,所以dp[i][j - 1] <= dp[i - 1][j - 1],所以没必要再在dp[i][j]中画蛇添足加一个已经被包含了的情况进去了。

所以递推公式可简化为:dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
int minDistance(string word1, string word2) {
int n = word1.size(), m = word2.size();
vector<vector<int>> dp(n + 1, vector<int>(m + 1));
for (int i = 1; i <= n; i++) dp[i][0] = i;
for (int j = 1; j <= m; j++) dp[0][j] = j;

for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (word1[i - 1] == word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + 1;
}
}
}
return dp[n][m];
}
};

[72] 编辑距离

LeetCode Medium

题目

给你两个单词 word1word2请返回将 word1 转换成 word2 所使用的最少操作数

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

解法

来自leetcode评论区的神仙解析

这题如果不是编辑距离模板题感觉根本就是hard

(1)当word1[i] == word2[j]时,由于遍历到了i和j,说明word1的0到i-1和word2的0到j-1的匹配结果已经生成,由于当前两个字符相同,因此无需做任何操作,dp[i][j] = dp[i - 1][j - 1]
(2)当word1[i] != word2[j]时,可以进行的操作有3个:
① 替换操作:可能word1的0到i-1位置与word2的0到j-1位置的字符都相同,只是当前位置的字符不匹配,进行替换操作后两者变得相同,所以此时dp[i][j] = dp[i - 1][j - 1] + 1(这个加1代表执行替换操作)
② 删除操作:若此时word1的0到i-1位置与word2的0到j位置已经匹配了,此时多出了word1的i位置字符,应把它删除掉,才能使此时word1的0到i(这个i是执行了删除操作后新的i)和word2的0到j位置匹配,因此此时dp[i][j] = dp[i - 1][j] + 1(这个加1代表执行删除操作)
③ 插入操作:若此时word1的0到i位置只是和word2的0到j-1位置匹配,此时只需要在原来的i位置后面插入一个和word2的j位置相同的字符使得此时的word1的0到i(这个i是执行了插入操作后新的i)和word2的0到j匹配得上,所以此时dp[i][j] = dp[i][j - 1] + 1(这个加1代表执行插入操作)
④ 由于题目所要求的是要最少的操作数:所以当word1[i] != word2[j] 时,需要在这三个操作中选取一个最小的值赋格当前的dp[i][j]

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int minDistance(string word1, string word2) {
int n = word1.size(), m = word2.size();
vector<vector<int>> dp(n + 1, vector<int>(m + 1));
for (int i = 1; i <= n; i++) dp[i][0] = i;
for (int j = 1; j <= m; j++) dp[0][j] = j;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (word1[i - 1] == word2[j - 1])
dp[i][j] = dp[i - 1][j - 1];
else
dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
}
}
return dp[n][m];
}
};

[712] 两个字符串的最小ASCII删除和

LeetCode Medium

题目

给定两个字符串s1s2,返回 使两个字符串相等所需删除字符的 ASCII 值的最小和

解法

难得AC如此之快,编辑距离双向删减版套皮

1
2
3
4
5
6
7
8
9
10
11
1. dp[i][j]数组的含义:字符串s1[0: i]和字符串s2[0: j]相等需要删除的最小和

2. 状态转移方程
s1[i - 1] == s2[j - 1]: dp[i][j] = dp[i - 1][j - 1]
s1[i - 1] != s2[j - 1]: dp[i][j] = min(dp[i][j - 1] + (int)s2[j - 1], dp[i - 1][j] + (int)s1[i - 1])

3. 初始化:
for (int i = 1; i <= n; i++)
dp[i][0] += dp[i - 1][0] + (int)s1[i - 1];
for (int i = 1; i <= m; i++)
dp[0][i] += dp[0][i - 1] + (int)s2[i - 1];

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
int minimumDeleteSum(string s1, string s2) {
int n = s1.size(), m = s2.size();
vector<vector<int>> dp(n + 1, vector<int>(m + 1));

for (int i = 1; i <= n; i++)
dp[i][0] += dp[i - 1][0] + (int)s1[i - 1];
for (int i = 1; i <= m; i++)
dp[0][i] += dp[0][i - 1] + (int)s2[i - 1];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s1[i - 1] == s2[j- 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = min(dp[i][j - 1] + (int)s2[j - 1], dp[i - 1][j] + (int)s1[i - 1]);
}
}
}
return dp[n][m];
}
};

回文

总结一下:

  • 回文子串得定义成vector<vector<bool>>类型,另外求长度
  • 回文子序列得定义成vector<vector<int>>,递推求长度

[647] 回文子串

LeetCode Medium

题目

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。

回文字符串 是正着读和倒过来读一样的字符串。

子字符串 是字符串中的由连续字符组成的一个序列。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

解法

  1. dp数组含义

因为要统计回文的个数,所以我们需要开dp数组判断当前是否是回文,如果是的话,计数器cnt++。

回文字符串的判断老生常谈

  1. 状态转移方程
1
2
3
4
5
6
if (s[i] == s[j]) {
if (j - i <= 1 || dp[i + 1][j - 1]) {
cnt++;
dp[i][j] = true;
}
}
  1. 初始化 => 如果j从i开始遍历就无需初始化,开dp数组的时候全false即可
  2. 遍历顺序

i依赖于i+1,j依赖于j-1,从左下角开始遍历,j要大于i

代码

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public:
int countSubstrings(string s) {
int cnt = 0;
vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
for (int i = s.size() - 1; i >= 0; i--)
for (int j = i; j < s.size(); j++)
if (s[i] == s[j] && (j - i <= 1 || dp[i + 1][j - 1]))
cnt++, dp[i][j] = true;
return cnt;
}
};

[516] 最长回文子序列

LeetCode Medium

题目

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

解法

  1. dp[i][j]代表s[i: j]回文子序列长度
  2. 状态转移方程
1
2
3
4
5
if (s[i] == s[j]) {
dp[i][j] = dp[i + 1][j - 1] + 2; // 首尾加入
} else {
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); // 试探一下单个加入
}
  1. 初始化 => dp[i][i] = 1
  2. 遍历顺序 => 左下角

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int longestPalindromeSubseq(string s) {
vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
for (int i = 0; i < s.size(); i++) dp[i][i] = 1;
for (int i = s.size() - 1; i >= 0; i--) {
for (int j = i + 1; j < s.size(); j++) {
if (s[i] == s[j]) {
dp[i][j] = dp[i + 1][j - 1] + 2;
} else {
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
}
}
}
return dp[0][s.size() - 1];
}
};
CATALOG
  1. 1. 不连续子序列问题
    1. 1.1. [300] 最长递增子序列
      1. 1.1.1. 题目
      2. 1.1.2. 解法
      3. 1.1.3. 代码
    2. 1.2. [1143] 最长公共子序列
      1. 1.2.1. 题目
      2. 1.2.2. 解法
      3. 1.2.3. 代码
    3. 1.3. [1035] 不相交的线
      1. 1.3.1. 题目
      2. 1.3.2. 解法
  2. 2. 连续子序列问题
    1. 2.1. [674] 最长连续递增序列
      1. 2.1.1. 题目
      2. 2.1.2. 解法
      3. 2.1.3. 代码
    2. 2.2. [718] 最长重复子数组
      1. 2.2.1. 题目
      2. 2.2.2. 解法
      3. 2.2.3. 代码
    3. 2.3. [53] 最大子序和
      1. 2.3.1. 题目
      2. 2.3.2. 解法
      3. 2.3.3. 代码
  3. 3. 距离问题
    1. 3.1. [392] 判断子序列
      1. 3.1.1. 题目
      2. 3.1.2. 解法
      3. 3.1.3. 代码
    2. 3.2. [115] 不同的子序列
      1. 3.2.1. 题目
      2. 3.2.2. 解法
      3. 3.2.3. 代码
    3. 3.3. [583] 两个字符串的删除操作
      1. 3.3.1. 题目
      2. 3.3.2. 解法
      3. 3.3.3. 代码
    4. 3.4. [72] 编辑距离
      1. 3.4.1. 题目
      2. 3.4.2. 解法
      3. 3.4.3. 代码
    5. 3.5. [712] 两个字符串的最小ASCII删除和
      1. 3.5.1. 题目
      2. 3.5.2. 解法
      3. 3.5.3. 代码
  4. 4. 回文
    1. 4.1. [647] 回文子串
      1. 4.1.1. 题目
      2. 4.1.2. 解法
      3. 4.1.3. 代码
    2. 4.2. [516] 最长回文子序列
      1. 4.2.1. 题目
      2. 4.2.2. 解法
      3. 4.2.3. 代码