代码随想录Day43:动态规划(最长递增子序列、最长连续递增序列、最长重复子数组)
一、实战
300最长递增子序列
300. 最长递增子序列 - 力扣(LeetCode)
子序列是什么?是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序,也就是说子序列不用连续,只要不改变原来数组中的顺序即可。
- dp[i]的定义
dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度。为什么以nums[i]结尾?因为递增比较的时候,如果比较 nums[j] 和 nums[i] 的大小,那么两个递增子序列一定分别以nums[j]为结尾 和 nums[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]的初始化
起始大小至少都是1
- 确定遍历顺序
dp[i] 是有0到i-1各个位置的最长递增子序列 推导而来,那么遍历i一定是从前向后遍历。
j其实就是遍历0到i-1,那么是从前到后,还是从后到前遍历都无所谓,只要吧 0 到 i-1 的元素都遍历了就行了。 所以默认习惯 从前向后遍历。
- 举例推导dp数组
674最长连续递增序列
674. 最长连续递增序列 - 力扣(LeetCode)
- dp[i]的定义
dp[i]:以下标i为结尾的连续递增的子序列长度为dp[i]。也是和上一题的区别所在,上一题的基础上需要加一个连续的条件
- 递推公式
如果 nums[i] > nums[i - 1],那么以 i 为结尾的连续递增的子序列长度 一定等于 以i - 1为结尾的连续递增的子序列长度 + 1 。即:dp[i] = dp[i - 1] + 1;
因为本题要求连续递增子序列,所以就只要比较nums[i]与nums[i - 1],而不用去比较nums[j]与nums[i] (j是在0到i之间遍历),所以只需要一层for循环
- dp[i]的初始化:连续递增的子序列长度最少也应该是1
- 确定遍历顺序:从前向后遍历
- 举例推导dp数组
代码就是在上一题的基础上改的
package org.example.DP;import java.util.Arrays;public class findLengthOfLCIS674 {public int findLengthOfLCIS(int[] nums) {// dp[i] 表示以 nums[i] 结尾的最长连续递增子序列的长度int[] dp = new int[nums.length];// 初始化:每个位置至少可以构成长度为1的子序列(自己)Arrays.fill(dp, 1);// 记录全局最大长度,初始为1int result = 1;// 从第二个元素开始遍历每个位置 ifor (int i = 1; i < nums.length; i++) {if (nums[i] > nums[i-1]) {// 更新dp[i] =dp[i-1] + 1;}// 更新全局最长连续递增子序列长度result = Math.max(result, dp[i]);}return result;}
}
718最长重复子数组
718. 最长重复子数组 - 力扣(LeetCode)
- dp[i]的定义
题目中说的子数组,其实就是连续子序列。
dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。 (特别注意: “以下标i - 1为结尾的A” 标明一定是 以A[i-1]为结尾的字符串 )——主要是为了简化初始化的过程
- 递推公式
根据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[i]的初始化
根据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。
举例:A[0]如果和B[0]相同的话,dp[1][1] = dp[0][0] + 1,只有dp[0][0]初始为0,正好符合递推公式逐步累加起来。
- 确定遍历顺序
外层for循环遍历A,内层for循环遍历B。交换可以。
- 举例推导dp数组
dp数组为什么定义:以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。定义dp[i][j]为 以下标i为结尾的A,和以下标j 为结尾的B,最长重复子数组长度不行么?
当然可以,就是实现起来麻烦一些。如果定义 dp[i][j]为 以下标i为结尾的A,和以下标j 为结尾的B,那么 第一行和第一列毕竟要进行初始化,如果nums1[i] 与 nums2[0] 相同的话,对应的 dp[i][0]就要初始为1, 因为此时最长重复子数组为1。 nums2[j] 与 nums1[0]相同的话,同理。
package org.example.DP;public class findLength718 {/*** 求两个数组的最长公共连续子数组的长度(即最长重复子数组)*/public int findLength(int[] nums1, int[] nums2) {// dp[i][j] 表示以 nums1[i-1] 和 nums2[j-1] 结尾的公共子数组的长度// 多开一行一列,方便处理边界int[][] dp = new int[nums1.length + 1][nums2.length + 1];// 记录最长公共子数组的长度int result = 0;// 遍历 nums1 的每个元素(从1开始,对应 dp 的有效区域)for (int i = 1; i < nums1.length + 1; i++) {// 遍历 nums2 的每个元素for (int j = 1; j < nums2.length + 1; j++) {// 如果两个元素相等if (nums1[i - 1] == nums2[j - 1]) {// 公共子数组长度加1(基于左上角的值)dp[i][j] = dp[i - 1][j - 1] + 1;// 更新最大长度result = Math.max(result, dp[i][j]);}}}return result;}
}