动态规划——子序列问题——连续?不连续?
子序列问题是动态规划的一个常考题型,本篇文章主要介绍 子序列(连续)和子序列(不连续)两个问题。连续和不连续指的是子序列是否是连续的,还是说中间是可以有间隔的子序列。
一:子序列(不连续)
300. 最长递增子序列
分析:从题意我们可以读出该题想求的是不连续的递增子序列。
解法:动态规划
第一步:dp[i]的定义
dp[i]表示i之前包括i的最长上升子序列的长度。
第二步:状态转移方程
位置 i 的最长升序子序列等于 j 从 0 到 i-1 各个位置的最长升序子序列 + 1 的最大值。
所以:if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
注意这里不是要dp[i] 与 dp[j] + 1进行比较,而是我们要取dp[j] + 1的最大值。
第三步:dp[i]的初始化
每一个i,对应的dp[i](即最长上升子序列)起始大小至少都是1.
第四步:确定遍历顺序
dp[i] 是有 0 到 i-1 各个位置的最长升序子序列推导而来,那么遍历 i 一定是从前向后遍历。
j 其实就是 0 到 i-1,遍历i的循环在外层,遍历j则在内层。
代码如下:
class Solution {
public int lengthOfLIS(int[] nums) {
int[] dp = new int[nums.length];
Arrays.fill(dp,1);
int result = 1;
for(int i = 1 ; i < nums.length ; i++){
for(int j = 0 ; j < i ; j++){
if(nums[i]>nums[j]){
dp[i] = Math.max(dp[i],dp[j]+1);
}
}
if(dp[i] > result){
result = dp[i];
}
}
return result;
}
}
1143. 最长公共子序列
分析:
第一步:确定dp数组(dp table)以及下标的含义
dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]
第二步:确定递推公式
主要就是两大情况: ①text1[i - 1] 与 text2[j - 1]相同 ②text1[i - 1] 与 text2[j - 1]不相同
如果① text1[i - 1] 与 text2[j - 1]相同,那么找到了一个公共元素,所以dp[i][j] = dp[i - 1][j - 1] + 1;
如果② text1[i - 1] 与 text2[j - 1]不相同,那就看看text1[0, i - 2]与text2[0, j - 1]的最长公共子序列 和 text1[0, i - 1]与text2[0, j - 2]的最长公共子序列,取最大的。
即:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
第三步:dp数组如何初始化
先看看dp[i][0]应该是多少呢?
text1[0, i-1]和空串的最长公共子序列自然是0,所以dp[i][0] = 0;
同理dp[0][j]也是0。
其他下标都是随着递推公式逐步覆盖,初始为多少都可以,那么就统一初始为0。
第四步:确定遍历顺序
从递推公式,可以看出,有三个方向可以推出dp[i][j],如图:
那么为了在递推的过程中,这三个方向都是经过计算的数值,所以要从前向后,从上到下来遍历这个矩阵。
代码如下:
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int[][] dp= new int[text1.length()+1][text2.length()+1];
int res = 0;
for(int i = 1 ; i <= text1.length() ; i++){
for(int j = 1 ; j <= text2.length() ; 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]);
}
if(dp[i][j] > res){
res = dp[i][j];
}
}
}
return res;
}
}
1035. 不相交的线
分析:仔细读题会发现其实本题就是求最长公共子序列(不连续)和上题一模一样,换一下参数就可以直接AC。
代码如下:
class Solution {
public int maxUncrossedLines(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;
}else{
dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);
}
if(dp[i][j] > res){
res = dp[i][j];
}
}
}
return res;
}
}
二:子序列(连续)
674. 最长连续递增序列
第一步:确定dp数组(dp table)以及下标的含义
dp[i]:以下标i为结尾的数组的连续递增的子序列长度为dp[i]。
注意这里的定义,一定是以下标i为结尾,并不是说一定以下标0为起始位置。
第二步:确定递推公式
如果 nums[i] > nums[i-],那么以 i 为结尾的数组的连续递增的子序列长度 一定等于 以i-1为结尾的数组的连续递增的子序列长度 + 1 。
即:dp[i] = dp[i-1] + 1;
因为本题要求连续递增子序列,所以就必要比较nums[i + 1]与nums[i],而不用去比较nums[j]与nums[i] (j是在0到i之间遍历)。
既然不用j了,那么也不用两层for循环,本题一层for循环就行,比较nums[i] 和 nums[i-1]。
第三步:dp数组如何初始化
以下标i为结尾的数组的连续递增的子序列长度最少也应该是1,即就是nums[i]这一个元素。
所以dp[i]应该初始1;
第四步:确定遍历顺序
从递推公式上可以看出, dp[i]依赖dp[i-1],所以一定是从前向后遍历。
代码如下:
class Solution {
public int findLengthOfLCIS(int[] nums) {
int dp[] = new int[nums.length];
Arrays.fill(dp,1);
int res = 1;
for(int i = 1 ; i < nums.length ; i++){
if(nums[i] > nums[i-1]){
dp[i] = dp[i-1]+1;
}
if(dp[i] > res){
res = dp[i];
}
}
return res;
}
}
718. 最长重复子数组
分析:
第一步:确定dp数组(dp table)以及下标的含义
dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。
第二步:确定递推公式
根据dp[i][j]的定义,dp[i][j]的状态只能由dp[i - 1][j - 1]推导出来。
即当A[i - 1] 和B[j - 1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1;
根据递推公式可以看出,遍历i 和 j 要从1开始!
第三步:dp数组如何初始化
根据dp[i][j]的定义,dp[i][0] 和dp[0][j]其实都是没有意义的!
但dp[i][0] 和dp[0][j]要初始值,因为 为了方便递归公式dp[i][j] = dp[i - 1][j - 1] + 1;
所以dp[i][0] 和dp[0][j]初始化为0。
第四步:确定遍历顺序
外层for循环遍历A,内层for循环遍历B。或者相反都可以。
同时题目要求长度最长的子数组的长度。所以在遍历的时候顺便把dp[i][j]的最大值记录下来。
代码如下:
class Solution {
public int findLength(int[] nums1, int[] nums2) {
int res = 0 ;
int[][] dp = new int[nums1.length+1][nums2.length+1];
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;
if(dp[i][j] > res){
res = dp[i][j];
}
}
}
}
return res;
}
}
总结:这道题我觉得和子序列(不连续)里面的第二题非常像,只不过这道题要求必须是联系的,所以只有在 nums1[i-1] == nums2[j-1] 时,dp[i][j] == dp[i-1][j-1] + 1,如果不相等就无需记录了,默认值1即可。但是上面的第二题,还需要比较dp[i][j-1] 和 dp[i-1][j] 的最大值进行记录,因为之后如果有相等的还需要前面的计算结果。而本题一旦不相等,这个子数组就断了,无需记录之前的数据。
53. 最大子数组和
分析:这个题之前用贪心也写过,但是这里还是使用动态规划来进行解决,下面我会贴上贪心的代码。
第一步:确定dp数组(dp table)以及下标的含义
dp[i]:包括下标i之前的最大连续子序列和为dp[i]。
第二步:确定递推公式
dp[i]只有两个方向可以推出来:
一定是取最大的,所以dp[i] = max(dp[i - 1] + nums[i], nums[i]);
第三步:dp数组如何初始化
从递推公式可以看出来dp[i]是依赖于dp[i - 1]的状态,dp[0]就是递推公式的基础。
dp[0]应该是多少呢?
根据dp[i]的定义,很明显dp[0]应为nums[0]即dp[0] = nums[0]。
第四步:确定遍历顺序
递推公式中dp[i]依赖于dp[i - 1]的状态,需要从前向后遍历。
动态规划代码如下:
class Solution {
public int maxSubArray(int[] nums) {
int dp[] = new int[nums.length];
int res = nums[0];
dp[0] = nums[0];
for(int i = 1 ; i < nums.length ; i++){
dp[i] = Math.max(dp[i-1]+nums[i],nums[i]);
if(dp[i]>res){
res = dp[i];
}
}
return res;
}
}
贪心代码如下:
class Solution {
public int maxSubArray(int[] nums) {
int max = Integer.MIN_VALUE;
int sum = 0;
for(int i = 0 ; i < nums.length ; i++){
sum += nums[i];
if(sum > max){
max = sum;
}
if(sum < 0){
sum = 0;
}
}
return max;
}
}
总结:动态规划在解决子序列问题这里有两把刷子,以后遇到子序列的问题都可以考虑一下使用动态规划来进行解决。
如果有写的不对的地方,欢迎指出。感谢代码随想录。