数据结构与算法|算法总结|动态规划篇之子序列、子数组问题

首先我们要明确以下两个问题:
子序列:子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
子数组:子数组是数组中的一个连续部分
先给此类题目定一个基调,首先编辑距离相关的问题也属于序列和子序列问题,并且他们除了递推公式之外,还需要想明白一个很重要的问题,那就是递推公式成立的条件

好了现在开始做总结!

首先我们一起复习动规五部曲:

  1. 确定 dp 数组以及下标含义
  2. 确定递推公式
  3. dp 数组如何初始化
  4. 确定遍历顺序
  5. 距离推导 dp 数组

然后建议搭配 数据结构与算法|算法总结|动态规划之编辑距离总结篇(或者叫两个序列之间的比较问题)食用。

在这里插入图片描述

⭐️300.最长递增子序列

力扣题目链接

文章讲解:300.最长递增子序列

视频链接:动态规划之子序列问题,元素不连续!| LeetCode:300.最长递增子序列
状态:子序列问题的开篇,主要是子序列问题思路的开端,非常重要。特别是关于他的dp数组,反复推敲反复琢磨:dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度

这里需要提到的一个重点是:
对于递推公式:
d p [ i ] = m a x ( d p [ i ] , d p [ j ] + 1 ) dp[i] = max(dp[i], dp[j] + 1) dp[i]=max(dp[i],dp[j]+1)
这里的主要含义不是取其中的最大值,而是去遍历 dp[j],拿到他的最大值。

既然这里有个 j 出现了,我们当然可以判断到:

if (nums[j] < nums[i]) dp[i] = max(dp[i], dp[j] + 1)

所以遍历顺序也很容易得到:

for (int i = 0; i < nums.size(); i++) {
	for (int j = 0; j < nums.size(); j++) {
		...
	}
}

674.最长连续递增序列

力扣题目链接

文章讲解:674.最长连续递增序列

视频讲解:动态规划之子序列问题,重点在于连续!| LeetCode:674.最长连续递增序列

状态:连续递增子序列和递增子序列区别在哪里?体现在代码中的话又在哪里呢?

  • 首先之前的300.最长递增子序列只要求递增子序列,并不要求序列在数组是是一组连续的数,而本题要求连续的递增子序列。其实只要一连续,很明显就可以用贪心算法了(贪心算法的求解会放在最后面);
  • 再一个,代码中我们要求的连续的子序列,所以两次遍历肯定是不用了,只要每次发现连续小的话,直接来个+1即可。

这里我们可以使用贪心算法,某种程度来说,也可以使用滑动窗口来解决该问题,也就是说,只要一连续,尝试滑动窗口也是非常不错的选择。

但是在这里只描述动态规划的方法:

  • 明确dp数组的含义
    跟上一题一样,以下标i为结尾的最长连续递增子序列的长度为dp[i]

  • 确定递推公式
    在300.最长递增子序列中,我们的dp[i]是由i面前的某个元素j来进行推导。
    本题中我们求的是连续的递增子序列,所以我们没有必要去比较前面的所有元素了,在上一题中,我们可是遍历了0~i-1的j。
    所以如果我们nums[i] > nums[i-1],我们就做对应的那个dp[i] + 1的操作,即:
    d p [ i ] = d p [ i − 1 ] + 1 dp[i]=dp[i-1]+1 dp[i]=dp[i1]+1

  • dp数组的初始化
    以下标i为结尾的连续递增的子序列长度最少也应该是1,即就是nums[i]这一个元素。
    所以dp[i]应该初始1;

  • 确定遍历顺序
    从递推公式上可以看出, dp[i + 1]依赖dp[i],所以一定是从前向后遍历。

⭐️718.最长重复子数组

力扣题目链接

文章讲解:718.最长重复子数组

视频讲解:动态规划之子序列问题,想清楚DP数组的定义 | LeetCode:718.最长重复子数组
状态:题目风格开始突变!现在我们要比较的是两个数组!

其实如果我们能够提前知道这题使用动态规划,我们就可以拿 dp 数组去硬套!

题目要求返回 两个数组中 公共的 、长度最长的子数组的长度 。

我们可以设 dp 的含义为:dp[i][j] 表示分别以第 i - 1 和第 j - 1 结尾的数组 A 和数组 B公共的、长度最长的子数组。为什么这么设置呢?因为我们需要统一递推公式。等我们推理出递推公式之后自然可以理解。

那么递推公式应该是什么样的呢?其实我们对着两个数组稍微比较一下就知道了,如果想推理出第 i -1 个位置和第 i - 2 个位置的值,我们需要在前一个位置上 + 1,所以有:
d p [ i ] [ j ] = d p [ i − 1 ] d p [ j − 1 ] + 1 dp[i][j] = dp[i - 1]dp[j - 1] + 1 dp[i][j]=dp[i1]dp[j1]+1

如果我们在设计 dp 数组含义的时候使用 dp[i][j] 表示分别以第 i 和第 j 结尾,那么我们如何计算 dp[0][0]呢?

至此,题目基本解决。

1143.最长公共子序列

力扣题目链接

文章讲解:1143.最长公共子序列

视频讲解:动态规划子序列问题经典题目 | LeetCode:1143.最长公共子序列
状态:本题其实就跟718.最长重复子数组类似,但是不要求连续了。

本题的难点我认为主要还是在:

  1. 递推数组的分情况讨论:
    我们如何得出需要讨论这两个情况呢?因为我们对本题的 dp 数组的设计为:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]
    如果 i - 1 位置 和 j - 1 位置相等了,我们肯定就是把 i - 2 位置和 j - 2 位置的值 + 1;
    如果不想等,那么说明我们不能考虑同时考虑这两个位置的字母,选择比较 i - 2 与 j - 1 位置 or i - 1 位置 与 j - 2 位置
    • i f ( t e s t [ i − 1 ] = = t e x t 2 [ j − 1 ] ) if (test[i - 1] == text2[j - 1]) if(test[i1]==text2[j1])

    • i f ( t e s t [ i − 1 ] ! = t e x t 2 [ j − 1 ] if (test[i - 1] != text2[j - 1] if(test[i1]!=text2[j1]

  2. 理解第二个递推公式

⭐️647.回文子串

说实话,回文子串这道题更适合使用双指针来完成,其实就是确定一个基准点,然后左右指针忘两边扩,如果是回文子串则进行记录,不是就跳出子循环。

力扣题目链接

文章链接:647.回文子串
给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。

在这里我们只讲动态规划的解法。

这里最强的就是使用布尔类型的 dp 数组,dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。

下面我们一起考虑一下递推公式:

回文子串一个最基础的判定肯定就是从中间到两边散开,s[i] == s[j],所以我们的递推的条件就是,如果二者相等,我们开始计算 dp[i][j]:

if s[i] == s[j]:
	1. i = j,下标相同,当然是回文子串
	2. i 、j,相差1,比如说aa,也是回文子串
	3. i、j,相差大于1,那么dp[i][j]为回文子串的条件是,dp[i+1][j-1]是回文子串,所以递推代码如下
if (s[i] == s[j] && (j - i <= 1 || dp[i + 1][j - 1]) {
	ans++;
	dp[i][j] = true;
}

并且我们需要稍微关注一下遍历顺序,我们推理方向为 i + 1 ==> i、j - 1 ==>j
所以很明显是从下到上,从左到右:

int len = s.length();
for (int i = len - 1; i >= 0; i--) {
	for (int j = i; j < len; j++) {
		...
	}
}

并且在这里 i 和 j 是可以相等的,这一点要特别注意,因为下一题二者就不能相等了。

为什么他们两个能相等呢?因为我们在 dp[i + 1][j - 1] 之前就做了 j - i <= 1 的判断,所以是允许的。

516最长回文子序列

好了,经典的序列问题又来了

力扣题目链接

文章链接:516最长回文子序列
延续上一题的思路,只不过此时我们不能使用布尔类型的 dp 数组。因为这个最大值的计算肯定不能直视简单的 ans++ 就能完成的,所有的子序列问题都不能。

那么基本的设置思路和上一题一样:dp[i][j]----字符串s[i, j]范围内最长的回文子序列的长度为dp[i][j]

这里我们一起来推理递推公式:
首先,如果我们遍历的过程中有 s[i] == s[j],那么我们本能得知道,只要我们删除 [i, j] 范围中不是回文的字母,就应该有 dp[i][j] = xxx + 2
然后,如果 s[i] != s[j],我们dp[i][j]的含义是[i, j] 范围内的最长子序列长度,那么就有:dp[i][j] = max(dp[i +1][j], dp[i][j - 1]),这里我们不做过多解释,之前也用过类似思路,既然同时考虑 i, j 不满足条件,那么我们就只考虑其中一项,然后取最值。基于这一步,我们做回推,当 s[i] == s[j] 满足时,就有 dp[i][j] = dp[i + 1]dp[j - 1] + 2; 因为 dp[i + 1][j - 1] 肯定已经是 dp 数组的最大回文子序列了。

综上所述:

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]);
}

本题我认为最重要的还是要关注 dp 数组的初始化
从递推公式可以看出,递推公式计算不到i 和j相同时候的情况。
当i与j相同,那么dp[i][j]一定是等于1的,即:一个字符的回文子序列长度就是1。
其他都应该初始化成0,这样题对公式dp[i][j]=max(dp[i + 1][j], dp[i][j - 1]);才不会被初始值覆盖

vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
for (int i = 0; i < s.size(); i++) dp[i][i] = 1;

最后我们不得不提,此时在做遍历顺序时,j 千万不能等于 i ,因为我们是没有做任何前置判断的。

for (int i = s.size() - 1; i >= 0; i--) {
    for (int j = i + 1; j < s.size(); j++) {
    ...
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值