文章目录
- 子序列问题
- 27.最长递增子序列
- 28、最长连续递增序列
- 29、最长重复子数组
- 30、最长公共子序列
- 31、不相交的线
- 32、最大子序和
- 33、判断子序列
- 34、不同的子序列
- 35、两个字符串的删除操作
- 36、编辑距离
- 37、回文子串
- 38、最长回文子序列
- 动态规划总结篇
- 背包问题系列
- 股票系列
- 子序列系列
子序列问题
27.最长递增子序列
力扣题目链接
状态表示:dp[i]
:包含i
的前i
之前的以nums[i]
结尾的最长上升子序列长度
状态计算:由于是不连续的,我们考虑最后一个,要计算dp[i]
就要全部考虑nums[0 ~ i-1]即 j来枚举nums[0~i-1]
,
if(nums[i] > nums[j])
dp[i] = max(dp[i], dp[j] + 1)
动规五部曲
- 确定dp数组以及下标的含义
dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度
- 确定递推公式
if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
注意这里不是要dp[i] 与 dp[j] + 1
进行比较,而是我们要取dp[j] + 1的最大值。
for(int i = 0;i < n;i++){
for(int j = 0;j < i;j++){
if(nums[i] > nums[j]){
dp[i] = Math.max(dp[i],dp[j]+1);
}
}
- dp数组初始化
每一个i,对应的dp[i]
(即最长递增子序列)起始大小至少都是1。
Arrays.fill(dp,1);
- 确定遍历顺序
那么遍历i,j一定是从前向后遍历
- 打印并举例推导dp数组
Code
class Solution {
public int lengthOfLIS(int[] nums) {
int[] dp = new int[nums.length];
int n = nums.length;
Arrays.fill(dp,1);
int res = 1;
for(int i = 0;i < n;i++){
for(int j = 0;j < i;j++){
if(nums[i] > nums[j]){
dp[i] = Math.max(dp[i],dp[j]+1);
}
}
res = Math.max(res,dp[i]);
}
return res;
}
}
28、最长连续递增序列
力扣题目链接
状态表示:dp[i]
:表示包含i
的以nums[i]
结尾的最长连续递增序列的长度为dp[i]
由于是连续的我们就只要比较相邻前后两个元素与i-1
比较即可,不用像不连续的最长递增子序列
那样与j : 0~i-1
上的数进行比较
动规五部曲
- 确定dp数组以及下标的含义
dp[i]:以下标i为结尾的连续递增的子序列长度为dp[i]。
- 确定递推公式
如果 nums[i] > nums[i - 1]
,那么以 i 为结尾的连续递增的子序列长度 一定等于以i - 1为结尾的连续递增的子序列长度 + 1 。
即:dp[i] = dp[i - 1] + 1;
for(int i = 1;i < n;i++){
if(nums[i] > nums[i-1]){
dp[i] = Math.max(dp[i],dp[i-1]+1);
}
res = Math.max(res,dp[i]);
}
- dp数组初始化
从前向后遍历,因为只需要与前一个进行比较,所以dp数组都初始化1
Arrays.fill(dp,1);
- 确定遍历顺序
所以一定是从前向后遍历。
- 举例并推导dp数组
Code
class Solution {
public int findLengthOfLCIS(int[] nums) {
int[] dp = new int[nums.length];
int n = nums.length;
Arrays.fill(dp,1);
int res = 1;
for(int i = 1;i < n;i++){
if(nums[i] > nums[i-1]){
dp[i] = Math.max(dp[i],dp[i-1]+1);
}
res = Math.max(res,dp[i]);
}
return res;
}
}
29、最长重复子数组
力扣题目链接
状态表示:f[i][j]
:nums1[0~i-1]以nums1[i - 1]
结尾的和nums2[0~j-1]以nums2[j - 1]
结尾的最长重复子数组长度为f[i][j]
属性:Max_count
初始化:
根据dp[i][j]
的定义,dp[i][0] 和dp[0][j]
其实都是没有意义的!(-1怎么结尾呢是吧)
但dp[i][0]
和dp[0][j]
要初始值,因为 为了方便递归公式dp[i][j] = dp[i - 1][j - 1] + 1;
所以dp[i][0] 和dp[0][j]
初始化为0
。
动态规划五部曲:
- 确定dp数组以及下标的含义
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;
for(int i = 1;i < n;i++){
for(int j = 1;j < m;j++){
if(nums1[i-1] == nums2[j-1]){
dp[i][j] = Math.max(dp[i][j],dp[i-1][j-1]+1);
}
res = Math.max(dp[i][j],res);
}
}
- dp数组如何初始化
dp[i][0] 和dp[0
][j]要初始值,因为 为了方便递归公式dp[i][j] = dp[i - 1][j - 1] + 1
;
所以dp[i][0] 和dp[0][j]
初始化为0。
- 确定遍历顺序
所以一定是从前向后遍历。
- 举例推导dp数组
Code
class Solution {
public int findLength(int[] nums1, int[] nums2) {
int[][] dp = new int[nums1.length+1][nums2.length+1];
int n = nums1.length + 1;
int m = nums2.length + 1;
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] = Math.max(dp[i][j],dp[i-1][j-1]+1);
}
res = Math.max(dp[i][j],res);
}
}
return res;
}
}
30、最长公共子序列
力扣题目链接
四种基本状态
dp[i-1][j-1]
dp[i][j-1]
dp[i-1][j]
dp[i][j]
1.确定dp数组(dp table)以及下标的含义
dp[i][j]
:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]
2.确定递推公式
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])
;
if(char1 == char2){
dp[i][j] = dp[i-1][j-1] + 1;
}
else{
dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);
}
3.dp数组如何初始化
dp[i][0] = 0;
dp[0][j]也是0。
4.确定遍历顺序
5.举例推导dp数组
Code
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int[][] dp = new int[text1.length()+1][text2.length()+1];
int n = text1.length()+1;
int m = text2.length()+1;
int res = 0;
for(int i = 1;i < n;i++){
for(int j = 1;j < m;j++){
char char1 = text1.charAt(i-1);
char char2 = text2.charAt(j-1);
if(char1 == char2){
dp[i][j] = dp[i-1][j-1] + 1;
}
else{
dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);
}
res = Math.max(res,dp[i][j]);
}
}
return res;
}
}
31、不相交的线
力扣题目链接
要求相等且不相交且要最长的———— 其实就是最长公共子序列!
Code
class Solution {
public int maxUncrossedLines(int[] nums1, int[] nums2) {
int n = nums1.length;
int m = nums2.length;
int res = 0;
int[][] dp = new int[n+1][m+1];
for(int i = 1;i < n + 1;i++){
for(int j = 1;j < m + 1;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][j-1],dp[i-1][j]);
}
res = Math.max(res,dp[i][j]);
}
}
return dp[n][m];
}
}
32、最大子序和
力扣题目链接
状态表示dp[i]
:表示以nums[i]
结尾的最大连续子段和,dp[i]
只与dp[i - 1]
有关
属性:Max
假设数组 nums
的值全都严格大于 0,那么一定有 dp[i] = dp[i - 1] + nums[i]
。
状态转移:但由于存在负数的情况,因此要分类讨论:
dp[i - 1] <= 0
,那么dp[i]
要是加上它反而变小了,为此dp[i] = nums[i]
dp[i - 1] > 0
,那么dp[i] = dp[i - 1] + nums[i]
- 初始化:
f[0] = nums[0]
,以nums[0]
结尾的和即为它本身。
动规五部曲
- 确定dp数组以及其下标的含义
dp[i]表示以nums[i]
结尾的最大连续子段和
- 确定递推公式
dp[i]只有两个方向可以推出来:
-
dp[i - 1] + nums[i],即:nums[i]加入当前连续子序列和
-
nums[i],即:从头开始计算当前连续子序列和
-
dp数组初始化
dp[0]应为nums[0]即dp[0] = nums[0]。
- 确定遍历顺序
递推公式中dp[i]依赖于dp[i - 1]的状态,需要从前向后遍历。
- 举例并推导dp数组
那么我们要找最大的连续子序列,就应该找每一个i为终点的连续最大子序列。
所以在递推公式的时候,可以直接选出最大的dp[i]。
Code
class Solution {
public int maxSubArray(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
dp[0] = nums[0];
int res = nums[0];
for(int i = 1;i < n;i++){
dp[i] = Math.max(dp[i-1]+nums[i],nums[i]);
res = Math.max(res,dp[i]);
}
return res;
}
}
33、判断子序列
力扣题目链接
状态计算:
if (s[i] == t[j ])
,那么dp[i][j] = dp[i - 1][j - 1] + 1;
,因为找到了一个相同的字符,相同子序列长度自然要在dp[i-1][j-1]的
基础上加1
if (s[i ] != t[j])
,此时相当于t要删除元素,t如果把当前元素t[j ]
删除,使得s[1~i]与t[1 ~ j-1]
相匹配!即dp[i][j] = dp[i][j - 1];
if(char1 == char2){
dp[i][j] = dp[i-1][j-1] + 1;
}else{
dp[i][j] = dp[i][j-1];
}
动规五部曲
- 确定dp数组以及下标的含义
dp[i][j]
表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]
。
- 确定递推公式
if (s[i - 1] == t[j - 1]),dp[i][j] = dp[i - 1][j - 1] + 1;
if (s[i - 1] != t[j - 1]),dp[i][j] = dp[i][j - 1];
- dp数组初始化
``dp[i][j]都是依赖于
dp[i - 1][j - 1] 和 dp[i][j - 1],所以
dp[0][0]和dp[i][0]`都要初始化
- 确定遍历顺序
- 举例并推导dp数组
dp[i][j]
表示以下标i-1为结尾的字符串s和以下标j-1为结尾的字符串t 相同子序列的长度,所以如果dp[s.size()][t.size()]
与 字符串s的长度相同说明:s与t的最长相同子序列就是s,那么s 就是 t 的子序列。
if(res == n){
return true;
}else{
return false;
}
Code
class Solution {
public boolean isSubsequence(String s, String t) {
int n = s.length();
int m = t.length();
int[][] dp = new int[n+1][m+1];
int res = 0;
for(int i = 1;i < n + 1;i++){
for(int j = 1;j < m + 1;j++){
char char1 = s.charAt(i-1);
char char2 = t.charAt(j-1);
if(char1 == char2){
dp[i][j] = dp[i-1][j-1] + 1;
}else{
dp[i][j] = dp[i][j-1];
}
res = Math.max(res,dp[i][j]);
}
}
if(res == n){
return true;
}else{
return false;
}
}
}
34、不同的子序列
力扣题目链接
动规五部曲
- 确定dp数组以及其下标的含义
dp[i][j]
:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]
。
-
确定递推公式
-
s[i - 1] 与 t[j - 1]相等
- 用s[i - 1]来匹配,个数为
dp[i - 1][j - 1]
。 - 不用s[i - 1]来匹配,个数为
dp[i - 1][j]
。
- 用s[i - 1]来匹配,个数为
-
s[i - 1] 与 t[j - 1] 不相等
- 个数为
dp[i - 1][j]
。
- 个数为
if(s.charAt(i - 1) == t.charAt(j - 1)){
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
}else{
dp[i][j] = dp[i - 1][j];
}
- dp数组初始化
dp[i][0]
应初始化为1,其余初始化为0
- 确定遍历顺序
从递推公式dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; 和 dp[i][j] = dp[i - 1][j];
中可以看出dp[i][j]
都是根据左上方和正上方推出来的。
所以遍历的时候一定是从上到下,从左到右
- 举例并推导dp数组
Code
class Solution {
public int numDistinct(String s, String t) {
int n = s.length();
int m = t.length();
int[][] dp = new int[n+1][m+1];
for(int i = 0;i < n + 1;i++){
dp[i][0] = 1;
}
for(int i = 1;i < n + 1;i++){
for(int j = 1;j < m + 1;j++){
if(s.charAt(i - 1) == t.charAt(j - 1)){
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
}else{
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[n][m];
}
}
35、两个字符串的删除操作
力扣题目链接
思路一:
求出两个字符串的最长公共子序列长度即可,那么除了最长公共子序列之外的字符都是必须删除的,最后用两个字符串的总长度减去两个最长公共子序列的长度就是删除的最少步数。
思路二:
动规五部曲
- 确定dp数组以及下标的含义
dp[i][j]
:以i-1为结尾的字符串word1,和以j-1位结尾的字符串word2,想要达到相等,所需要删除元素的最少次数。
-
确定递推公式
-
当word1[i - 1] 与 word2[j - 1]相同的时候
-
当word1[i - 1] 与 word2[j - 1]不相同的时候
-
当word1[i - 1] 与 word2[j - 1]相同的时候,dp[i][j] = dp[i - 1][j - 1];
当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
那最后当然是取最小值,所以当word1[i - 1] 与 word2[j - 1]不相同的时候,递推公式:dp[i][j] = min({dp[i - 1][j - 1] + 2, dp[i - 1][j] + 1, dp[i][j - 1] + 1});
- dp数组初始化
dp[i][0] 和 dp[0][j]
是一定要初始化的
for(int i = 0;i < n + 1;i++) dp[i][0] = i;
for(int j = 0;j < m + 1;j++) dp[0][j] = j;
- 确定遍历顺序
从递推公式 dp[i][j] = min(dp[i - 1][j - 1] + 2, min(dp[i - 1][j], dp[i][j - 1]) + 1); 和dp[i][j] = dp[i - 1][j - 1]
可以看出dp[i][j]
都是根据左上方、正上方、正左方推出来的。
- 举例并推导dp数组
Code
解法一
// dp数组中存储word1和word2最长相同子序列的长度
class Solution {
public int minDistance(String word1, String word2) {
int n = word1.length();
int m = word2.length();
int[][] dp = new int[n+1][m+1];
for(int i = 1;i < n + 1;i++){
for(int j = 1;j < m + 1;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]);
}
}
}
return n + m - 2*dp[n][m];
}
}
解法二
class Solution {
public int minDistance(String word1, String word2) {
int n = word1.length();
int m = word2.length();
int[][] dp = new int[n+1][m+1];
for(int i = 0;i < n + 1;i++) dp[i][0] = i;
for(int j = 0;j < m + 1;j++) dp[0][j] = j;
for(int i = 1; i < n + 1;i++){
for(int j = 1;j < m + 1;j++){
if(word1.charAt(i - 1) == word2.charAt(j - 1)){
dp[i][j] = dp[i-1][j-1];
}else{
dp[i][j] = Math.min(Math.min(dp[i-1][j] + 1,dp[i][j-1]+1),dp[i-1][j-1]+2);
}
}
}
return dp[n][m];
}
}
36、编辑距离
力扣题目链接
动规五部曲
- 确定dp数组以及下标的含义
dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。
- 确定递推公式
if (word1[i - 1] == word2[j - 1])
不操作
if (word1[i - 1] != word2[j - 1])
增
删
换
-
word1[i - 1] == word2[j - 1]
- 不操作
dp[i][j] = dp[i-1][j-1]
- 不操作
-
word1[i - 1] != word2[j - 1]
- 增和删等同,操作数一样
dp[i][j] = dp[i][j-1] + 1
dp[i][j] = dp[i-1][j] + 1
- 换
dp[i][j] = dp[i-1][j-1] + 1
- 在增删换最小值
dp[i][j] = Math.min(Math.min(dp[i - 1][j],dp[i][j - 1]),dp[i - 1][j - 1])+1;
- 增和删等同,操作数一样
-
dp数组初始化
p[i][0] = i;
dp[0][j] = j
;
for(int i = 0;i < n + 1;i++) dp[i][0] = i;
for(int j = 0;j < m + 1;j++) dp[0][j] = j;
- 确定遍历顺序
从如下四个递推公式:
dp[i][j] = dp[i - 1][j - 1]
dp[i][j] = dp[i - 1][j - 1] + 1
dp[i][j] = dp[i][j - 1] + 1
dp[i][j] = dp[i - 1][j] + 1
dp矩阵中一定是从左到右从上到下去遍历
- 打印并推导dp数组
以示例1为例,输入:word1 = "horse", word2 = "ros"
为例,dp矩阵状态图如下:
Code
class Solution {
public int minDistance(String word1, String word2) {
int n = word1.length();
int m = word2.length();
int[][] dp = new int[n+1][m+1];
for(int i = 0;i < n + 1;i++) dp[i][0] = i;
for(int j = 0;j < m + 1;j++) dp[0][j] = j;
for(int i = 1; i < n + 1;i++){
for(int j = 1;j < m + 1;j++){
if(word1.charAt(i - 1) == word2.charAt(j - 1)){
dp[i][j] = dp[i - 1][j - 1];
}else{
dp[i][j] = Math.min(Math.min(dp[i - 1][j],dp[i][j - 1]),dp[i - 1][j - 1])+1;
}
}
}
return dp[n][m];
}
}
37、回文子串
力扣题目链接
动规五部曲
- 确定dp数组以及其下标的含义
布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。
- 确定递推公式
整体上是两种,就是s[i]与s[j]相等,s[i]与s[j]不相等这两种。
当s[i]与s[j]不相等,那没啥好说的了,dp[i][j]
一定是false。
当s[i]与s[j]相等时,这就复杂一些了,有如下三种情况
- 情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串
- 情况二:下标i 与 j相差为1,例如aa,也是回文子串
- 情况三:下标:i 与 j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,这个区间是不是回文就看
dp[i + 1][j - 1]
是否为true。
if(s.charAt(i) == s.charAt(j)){
if(j - i <= 1){
dp[i][j] = true;
res++;
}else if(dp[i+1][j-1]){
dp[i][j] = true;
res++;
- dp数组初始化
dp[i][j]
初始化为false。
- 确定遍历顺序
dp[i + 1][j - 1] 在 dp[i][j]
的左下角,如图:
一定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]
都是经过计算的
- 举例并推导dp数组
举例,输入:“aaa”,dp[i][j]
状态如下:
Code
class Solution {
public int countSubstrings(String s) {
int res = 0;
if(s == null || s.length() == 0) return 0;
int n = s.length();
boolean[][] dp = new boolean[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] = true;
res++;
}else if(dp[i+1][j-1]){
dp[i][j] = true;
res++;
}
}
}
}
return res;
}
}
38、最长回文子序列
力扣题目链接
动规五部曲
状态计算:判断s[i] 与 s[j]是否相等
-
s[i] == s[j]
:dp[i][j] = dp[i + 1][j - 1] + 2
-
s[i] != s[j]:dp[i][j] = max(dp[i][j - 1], dp[i + 1][j])
-
删除
s[i]
:dp[i + 1][j]
-
删除
s[j]
:dp[i][j - 1]
-
-
确定dp数组以及其下标的含义
dp[i][j]
:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]
。
- 确定递推公式
如果s[i]与s[j]相同,那么dp[i][j] = dp[i + 1][j - 1] + 2;
如图:
s[i] != s[j]:dp[i][j] = max(dp[i][j - 1], dp[i + 1][j])
如图:
- dp数组初始化
当i与j相同,那么dp[i][j]
一定是等于1,其它情况初始化为0
- 确定遍历顺序
遍历i的时候一定要从下到上遍历,这样才能保证,下一行的数据是经过计算的。
如图:
- 举例并推导dp数组
输入s:“caad” 为例,dp数组状态如图:
Code
class Solution {
public int longestPalindromeSubseq(String s) {
int n = s.length();
int[][] dp = new int[n][n];
for(int i = n - 1; i >= 0; i--){
dp[i][i] = 1;
for(int j = i + 1; j < n; j++){
if(s.charAt(i) == s.charAt(j)){
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];
}
}
动态规划总结篇
动规五部曲
-
确定dp数组(dp table)以及下标的含义
-
确定递推公式
-
dp数组如何初始化
-
确定遍历顺序
-
举例推导dp数组