算法-动态规划-子序列
子序列问题
是 动态规划
解决的经典问题。
1. 最长递增子序列 (LeetCode 300)
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
思路:
- dp[i]表示以nums[i]为结尾的最长子序列的长度
- 用j遍历0至i-1的元素,如果
nums[i] > nums[j]
,则dp[i] = Math.max(dp[i], dp[j]+1)
(可以用nums[i]追加在之前的子序列末尾)
class Solution {
// 时间复杂度:O(n^2)
public int lengthOfLIS(int[] nums) {
int result = 1;
int n = nums.length;
// dp[i]表示 以nums[i]结尾的最长子序列长度
int[] dp = new int[n];
for(int i = 0; i<n; ++i) {
dp[i] = 1;
}
for(int i = 1; i<n; ++i) {
for(int j = 0; j<i; ++j) {
if(nums[j] < nums[i])
dp[i] = Math.max(dp[i], dp[j]+1);
}
result = Math.max(result, dp[i]);
}
return result;
}
}
2. 最长连续递增序列 (LeetCode 674)
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,
如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,
那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。
思路:
- 本题不需要使用动态规划,只需要记录当前最长连续序列的长度即可
class Solution {
public int findLengthOfLCIS(int[] nums) {
int result = 1;
int temp = 1;
for(int i = 1; i<nums.length; ++i) {
if(nums[i] > nums[i-1])
temp++;
else {
result = Math.max(result, temp);
temp = 1;
}
}
return Math.max(result, temp);
}
}
3. 最长重复子数组(LeetCode 718)
给两个整数数组 nums1 和 nums2 ,返回 两个数组中公共的、长度最长的子数组的长度 。
输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1] 。
思路:
dp[i][j]
表示以A[i-1]
和B[j-1]
结尾的最长子序列长度
class Solution {
public int findLength(int[] nums1, int[] nums2) {
int n1 = nums1.length;
int n2 = nums2.length;
int result = 0;
// dp[i][j]表示以下标i-1为结尾的A,和以下标j-1为结尾的B,的最长重复子数组长度
int[][] dp = new int[n1+1][n2+1];
// 第一行和第一列默认初始化为0
// 双层for循环的i和j都从1开始遍历
for(int i = 1; i<=n1; ++i) {
for(int j = 1; j<=n2; ++j) {
if(nums1[i-1] == nums2[j-1]) {
dp[i][j] = dp[i-1][j-1] + 1;
result = Math.max(result, dp[i][j]);
}
}
}
return result;
}
}
4. 最长公共子序列 (LeetCode 1143)
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度
。如果不存在 公共子序列 ,返回 0 。
一个字符串的子序列
是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3 。
思路:
- 设计到两个字符串/数组的比较,很符合二维dp数组的定义,确定好递归关系进行循环遍历
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int n1 = text1.length();
int n2 = text2.length();
// dp[i][j]表示 A[i-1] 和 B[j-1] 的最长公共序列长度
int[][] dp = new int[n1+1][n2+1];
for(int i = 1; i<=n1; ++i) {
for(int j = 1; j<=n2; ++j) {
if(text1.charAt(i-1) == text2.charAt(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[n1][n2];
}
}
5. 不相交的线(LeetCode 1035)
在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。
现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足:
- nums1[i] == nums2[j]
- 且绘制的直线不与任何其他连线(非水平线)相交。
请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。
以这种方法绘制线条,并返回可以绘制的最大连线数。
思路:
- 本题本质上求的是两个数列的最长公共子序列,上一题的代码是一样的。
class Solution {
public int maxUncrossedLines(int[] nums1, int[] nums2) {
int n1 = nums1.length;
int n2 = nums2.length;
// dp[i][j]表示 A[0~i-1] 和 B[0~j-1] 的最多不相交的线
int[][] dp = new int[n1+1][n2+1];
for(int i = 1; i<=n1; ++i) {
for(int j = 1; j<=n2; ++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[n1][n2];
}
}
6. 最大子数组和 (LeetCode 53)
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组是数组中的一个连续部分。
class Solution {
// 动态规划解法
public int maxSubArray(int[] nums) {
int n = nums.length;
// nums[0~i]的最大子数组和
int[] dp = new int[n];
dp[0] = nums[0];
int result = nums[0];
for(int i = 1; i<n; ++i) {
// 累加 or 从头开始
dp[i] = Math.max(dp[i-1] + nums[i], nums[i]);
result = Math.max(result, dp[i]);
}
return result;
}
}
7. 判断子序列 (LeetCode 392)
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。
(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
// 双指针解法
class Solution {
public boolean isSubsequence(String s, String t) {
int i = 0;
int j = 0;
while(j < t.length() && i < s.length()) {
if(s.charAt(i) == t.charAt(j)) {
i++;
j++;
}
else
j++;
}
return (i == s.length());
}
}
8. 不同的子序列 (LeetCode 115)
给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数,结果需要对 109 + 7 取模。
输入:s = "rabbbit", t = "rabbit"
输出:3
解释:
如下所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。
class Solution {
public int numDistinct(String s, String t) {
int n1 = s.length();
int n2 = t.length();
// dp[i][j] 表示s[0~i-1]中t[0~j-1]出现的数量
int[][] dp = new int[n1+1][n2+1];
// 空字符串s中不含目标串t
for(int j = 0; j<=n2; ++j) // 第一行
dp[0][j] = 0;
// s需要删去所有字符得到空字符串t
for(int i = 0; i<=n1; ++i) // 第一列
dp[i][0] = 1;
for(int i = 1; i<=n1; ++i) {
for(int j = 1; j<=n2; ++j) {
if(s.charAt(i-1) == t.charAt(j-1))
// 用s[i-1]去匹配 + 不用s[i-1]去匹配
dp[i][j] = dp[i-1][j-1] + dp[i-1][j];
else
dp[i][j] = dp[i-1][j];
}
}
return dp[n1][n2];
}
}
9. 两个字符串的删除操作(LeetCode 583)
给定两个单词 word1 和 word2 ,返回使得 word1 和 word2 相同所需的最小步数。
每步 可以删除任意一个字符串中的一个字符。
输入: word1 = "sea", word2 = "eat"
输出: 2
解释: 第一步将 "sea" 变为 "ea" ,第二步将 "eat "变为 "ea"
思路:
- 可以将本题转化为最长公共子序列问题
class Solution {
// 转化为最长公共子序列问题
public int minDistance(String word1, String word2) {
// 先求最长公共子序列的长度
int n1 = word1.length();
int n2 = word2.length();
// dp[i][j] 表示 A[0~i-1]和B[0~j-1]的最长公共子序列的长度
int[][] dp = new int[n1+1][n2+1];
for(int i = 1; i<=n1; ++i) {
for(int j = 1; j<=n2; ++j) {
if(word1.charAt(i-1) == word2.charAt(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]);
}
}
int maxCommonSubSeq = dp[n1][n2];
// 编辑距离=n1 + n2 - 最长公共子序列长度*2
return n1 + n2 - maxCommonSubSeq*2;
}
}
10. 编辑距离 (LeetCode 72)
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
思路:
- 如果字符相等,则不需要操作;否则需要进行增、删、替换。
- 增和删除是等价的操作(因此可以只使用删除)
- 不相等的情况下,取
左上
、上方
、左方
三者中的最小值,然后加一 。
class Solution {
public int minDistance(String word1, String word2) {
int n1 = word1.length();
int n2 = word2.length();
// word1[0~i-1] 变为 word2[0~j-1] 需要的操作次数
int[][] dp = new int[n1+1][n2+1];
// 初始化
for(int j = 0; j<=n2; ++j)
dp[0][j] = j;
for(int i = 0; i<=n1; ++i)
dp[i][0] = i;
for(int i = 1; i<=n1; ++i) {
for(int j = 1; j<=n2; ++j) {
// 相等时,不需要进行操作
if(word1.charAt(i-1) == word2.charAt(j-1))
dp[i][j] = dp[i-1][j-1];
else
// 删除word1中的元素,删除word2中的元素,替换
dp[i][j] = Math.min(Math.min(dp[i-1][j], dp[i][j-1]), dp[i-1][j-1]) + 1;
}
}
return dp[n1][n2];
}
}
11. 回文字串(LeetCode 647)(有难度)
给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"
思路:
boolean dp[i][j]
表示 s.charAt(i)到s.charAt(j) 是否为回文字串j-i <= 1
时j-i之间不存在字串,如果s.charAt(i) == s.charAt(j)
则回文字串数量加一j-i >= 2
时dp[i][j] = dp[i+1][j-1]
- 注意遍历的顺序:从下往上,从左往右
class Solution {
public int countSubstrings(String s) {
int n = s.length();
int result = 0;
// dp[i][j] 表示i~j是否为回文子串(左闭右闭)
// 只需要填充右上半部分
boolean[][] dp = new boolean[n][n];
// 因为有一部分需要根据 dp[i+1][j-1] 得到 dp[i][j]
// 所以遍历顺序为 从下往上,从左往右
for(int i = n-1; i>=0; --i) {
for(int j = i; j<n; ++j) {
if(s.charAt(i) == s.charAt(j)) {
// 单个字母 or 两个字母
if(j - i <= 1) {
dp[i][j] = true;
result++;
}
// i与j之间存在字串
else {
dp[i][j] = dp[i+1][j-1];
if(dp[i][j] == true)
result++;
}
}
}
}
return result;
}
}
12. 最长回味子序列(LeetCode 516)
给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
class Solution {
public int longestPalindromeSubseq(String s) {
int n = s.length();
// i~j(左闭右闭)之间的最长回文子序列的长度
int[][] dp = new int[n][n];
for(int i = n-1; i>=0; --i) {
for(int j = i; j<n; ++j) {
if(s.charAt(i) == s.charAt(j)) {
if(j-i<=1)
dp[i][j] = j-i+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][n-1];
}
}