动态规划
子序列或连续序列问题
最长递增子序列
- 题目
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
- dp定义:dp[i] 代表 以nums[i]为结尾的最长递增子序列的长度
- 初始化:dp数组都为1
- 遍历:
for (i = 1; i < n; ++i) {
for (j = 0; j < i; ++j) {
if (nums[i] > nums[j]) {
dp[i] = max(dp[i], dp[j] + 1);
}
res = max(res, dp[i]);
}
}
最长连续递增序列
- 题目:
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], …, nums[r - 1], nums[r]] 就是连续递增子序列。
- dp定义:dp[i] 代表 以nums[i]为结尾的最长连续递增序列的长度
- 初始化:dp数组都为1
- 遍历:
for (i = 0; i < n - 1; ++i) {
if (nums[i + 1] > nums[i])
dp[i + 1] = dp[i] + 1;
}
最长重复子数组
- 题目:注意,这里是子数组,不是子序列
给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。
示例:
输入:
- A: [1,2,3,2,1]
- B: [3,2,1,4,7]
- 输出:3
- 解释:长度最长的公共子数组是 [3, 2, 1] 。
- dp定义:dp[ i ] [ j ] 代表 以 A[i-1]和B[j-1]为结尾的最长的子数组的长度
- 初始化:dp[0] [j] = dp[i] [0] = 0
- 遍历
dp [lena+1][lenb+1];
for (i = 1; i <= lena; ++i) {
for (j = 1; j <= lenb; ++j) {
if (A[i - 1] == B[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
// /*这里是子序列的写法*/dp[i][j] = max(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]);
dp[i][j] = 0;
}
}
}
最长公共子序列
- 题目:
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。
示例 1:
- 输入:text1 = “abcde”, text2 = “ace”
- 输出:3
- 解释:最长公共子序列是 “ace”,它的长度为 3。
- dp定义:dp[i] [j] 代表text1[0 ~ i-1] 和 text2[0 ~ j-1]两个子数组最长公共子序列的长度
- 初始化:dp[0] [j] = dp[i] [0] = 0
- 遍历
dp[n][m]; n:text1.size m:text2.size
for (i = 1; i <= n; ++i) {
for (j = 1; j <= m; ++j) {
if (text1[i-1] == text2[j-1]) {
dp[i][j] = dp[i-1][j-1] + 1; // 二维
// dp[j] = dp[j-1] + 1; // 一维
} else {
dp[i][j] = max({dp[i - 1][j], dp[i][j-1]}); // 二维
// dp[j] = max({dp[j], dp[j-1]}); // 一维
}
}
}
不相交的线
- 题目:这道题主要是理解,找规律,注意不能相交这个点。连线要在两个相同的数字,不能相交:要求按照相对顺序来。那不就是找按相对顺序的相同数字的对数,那不就是求最长子序列。
我们在两条独立的水平线上按给定的顺序写下 A 和 B 中的整数。
现在,我们可以绘制一些连接两个数字 A[i] 和 B[j] 的直线,只要 A[i] == B[j],且我们绘制的直线不与任何其他连线(非水平线)相交。
以这种方法绘制线条,并返回我们可以绘制的最大连线数。
- dp定义:dp[i] [j] 为 A[0 ~ i-1] 和 B[0 ~ j-1] 最长相同子序列的长度
- 初始化:dp[0] [j] = dp[i] [0] = 0
- 遍历
for (i = 1; i <= n; ++i) {
for (j = 1; j <= m; ++j) {
if (A[i - 1] == B[i - 1]) {
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]
判断子序列
- 题目:其实还是求公共子序列。求s和t的最长子序列,看是不是为s本身
- 给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
示例 1:
- 输入:s = “abc”, t = “ahbgdc”
- 输出:true
示例 2:
- 输入:s = “axc”, t = “ahbgdc”
- 输出:false
提示:
- 0 <= s.length <= 100
- 0 <= t.length <= 10^4
两个字符串都只由小写字符组成。
不同的子序列
- 题目:
给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。
字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,“ACE” 是 “ABCDE” 的一个子序列,而 “AEC” 不是)
题目数据保证答案符合 32 位带符号整数范围。
- dp定义:dp[i] [j] 代表 s[0 ~ i-1] 的子序列中出现 t[0 ~ j-1]的个数
- 初始化:dp[0] [j] = dp[i] [0] = 0, dp[0] [0] = 1
- 遍历:
n = s.size();
m = t.size();
for (i = 1; i <= n; ++i) {
for (j = 1; j <= m; ++j) {
if (s[i-1] == t[j-1]) {
dp[i][j] = dp[i-1][j] /*s[i-1]不参与匹配*/ + dp[i-1][j-1]/*s[i-1]参与匹配*/;
} else {
dp[i][j] = dp[i-1][j] /*s[i-1]不参与匹配*/
}
}
}
return dp[n][m]
编辑距离
两个字符串的删除操作
- 题目:
- 给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。
示例:
- 输入: “sea”, “eat”
- 输出: 2
- 解释: 第一步将"sea"变为"ea",第二步将"eat"变为"ea"
- dp定义:dp[i] [j] 代表 word1[0 ~ i-1] 和 word2[0 ~ j-1] 相同需要的最小步数
- 初始化:dp[0] [j] = j ; dp[i] [0] = i ; dp[0] [0] = 0
- 遍历:
for (i = 1; i <= n; ++i) {
for (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]+1, dp[i][j-1]+1, dp[i-1][j-1]+2});
}
}
}
return dp[n][m]
编辑距离
- 题目:
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
- 示例 1:
- 输入:word1 = “horse”, word2 = “ros”
- 输出:3
- 解释: horse -> rorse (将 ‘h’ 替换为 ‘r’) rorse -> rose (删除 ‘r’) rose -> ros (删除 ‘e’)
- 示例 2:
- 输入:word1 = “intention”, word2 = “execution”
- 输出:5
- 解释: intention -> inention (删除 ‘t’) inention -> enention (将 ‘i’ 替换为 ‘e’) enention -> exention (将 ‘n’ 替换为 ‘x’) exention -> exection (将 ‘n’ 替换为 ‘c’) exection -> execution (插入 ‘u’)
- dp定义:dp[i] [j] 代表 word1[0 ~ i-1] 和 word2[0 ~ j-1] 相同需要的最小编辑次数步数
- 初始化:dp[0] [j] = j ; dp[i] [0] = i ; dp[0] [0] = 0
- 遍历:
for (i = 1; i <= n; ++i) {
for (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]+1/*删除或修改word1[i-1]*/,
dp[i][j-1]+1/*删除或修改word2[j-1]*/,
/*没必要*/ dp[i-1][j-1]+2/*删除word1[i-1]和word2[i-2]*/,
});
}
}
}
return dp[n][m]
回文
回文子串
- 题目:这道题dp不好作,用双指针遍历更简单
给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例 1:
- 输入:“abc”
- 输出:3
- 解释:三个回文子串: “a”, “b”, “c”
class Solution {
public:
int helper(const string& s, int left, int right) {
int ret = 0;
while (left >= 0 && right < s.size()) {
if (s[left] == s[right]) {
--left;
++right;
++ret;
} else {
break;
}
}
return ret;
}
int countSubstrings(string s) {
int ret = 0;
for (int i = 0; i < s.size(); ++i) {
ret += helper(s, i, i + 1);
ret += helper(s, i, i);
}
return ret;
}
};
最长回文子序列
- 题目: 注意是子序列,不是连续的
给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。
示例 1: 输入: “bbbab” 输出: 4 一个可能的最长回文子序列为 “bbbb”。
示例 2: 输入:“cbbd” 输出: 2 一个可能的最长回文子序列为 “bb”。
- dp定义:dp[i] [j] 代表 以s[i ~ j]的最长回文子序列的最长长度
- 初始化:dp[i] [i] = 1; else = 0
- 遍历:这里注意遍历顺序。(因为上一行(i)是依赖下一行(i+1),所以需要先计算下一行)。并且 i > j 时,dp[i] [j] 无意义,所以j是从i+1开始
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];
}
};