首先说明一下子序列和子数组的概念。在数组中,子数组是由连续的元素组成的,而子序列则不一定是连续的。 在字符串中,子串是由连续的字符组成的,而子序列则不一定是连续的。
1. 最长递增子序列
1.题目链接:子序列问题
2.题目描述:
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
提示:
1 <= nums.length <= 2500
-10^4 <= nums[i] <= 10^4
3.问题分析:这道题是要求递增子序列的最长长度,那么如果是求递增子数组的最长长度,又该如何求解?两者之间又有什么区别?
首先看求递增子数组的最长长度这道问题,然后以 i 位置为结尾进行分析,如果nums的 i 位置的值比nums的 i - 1位置的值大,那么dp表中该位置(i位置)的数值等于前一个位置的数值加1;由上述可知,求递增子数组的最长长度关键是寻找 i 位置前一个位置的数。
那么子序列呢?因为子序列是不一定连续的,以 i 位置进行分析,我们需要找到的是 i 位置前面所有元素中比nums[i]小的数值,然后在这些位置对应的dp表中寻找最大的数值,即最长长度。
- 状态表示:dp[i] 表⽰:以 i 位置元素为结尾的所有⼦序列中,最⻓递增⼦序列的⻓度。
- 状态转移方程:有上述问题分析知,求dp[i]位置的值,需要遍历 i 位置前面的所有比nums[i]小的值,找到对应的dp表中寻找最大的数值,所以状态转移方程为:
dp[i] = max(dp[i], dp[i前面的元素] + 1)
。 - 初始化:求得值子序列的长度,递增子序列的最小长度为1,所以全部初始化为1即可。
- 填表顺序:
从左往右。
- 返回值:返回dp表中最大的数值。
4.代码如下:
class Solution
{
public:
int lengthOfLIS(vector<int>& nums)
{
int n = nums.size();
//创建dp表,并全初始化为1
vector<int> dp(n, 1);
int ret = 1;
for (int i = 1; i < n; ++i)
{
//寻找i位置前面的所有值
for (int j = 0; j < i; ++j)
{
//将递增的最大值保留下来
if (nums[i] > nums[j])
dp[i] = max(dp[i], dp[j] + 1);
}
//找出dp表中的最大值
ret = max(ret, dp[i]);
}
return ret;
}
};
2. 摆动序列
1.题目链接:摆动序列
2.题目描述:
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。
示例 1:
输入:nums = [1,7,4,9,2,5]
输出:6
解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3)。
示例 2:
输入:nums = [1,17,5,10,13,15,10,5,16,8]
输出:7
解释:这个序列包含几个长度为 7 摆动序列。
其中一个是 [1, 17, 10, 13, 10, 16, 8] ,各元素之间的差值为 (16, -7, 3, -3, 6, -8) 。
示例 3:
输入:nums = [1,2,3,4,5,6,7,8,9]
输出:2
提示:
1 <= nums.length <= 1000
0 <= nums[i] <= 1000
3.题目分析:这道题可以说是摆动数组(即最长湍流子数组)的升级,如果是摆动数组,那么这道题该如何解决?因为两元素之间的差有两种结果(0不考虑),所以需要两个dp表来将所有结果管理起来;通常一个表f[i]用来表示相减的差为正数的最长子数组的个数,另一个表g[i]用来表示相减的差为负数的最长子数组的个数。如果 i 位置的元素⽐ i - 1 位置的元素⼤,说明接下来应该去找 i -1 位置结尾,并且 i - 1 位置元素⽐前⼀个元素⼩的序列,那就是 g[i - 1] 。更新 f[i] 位置的值: f[i] = g[i - 1] + 1 ; 2.arr[i] < arr[i - 1] :如果 i 位置的元素⽐ i - 1 位置的元素⼩,说明接下来应该去找 i - 1 位置结尾,并且 i - 1 位置元素⽐前⼀个元素⼤的序列,那就是f[i - 1] 。更新 g[i] 位置的值: g[i] = f[i - 1] + 1 ; arr[i] == arr[i - 1] :不构成湍流数组。
那么这道题该如何解决?因为子序列不一定相连,所以需要将 i 位置前面的元素都找一遍,每次保留最大的长度即可。
- 状态表示:f[i] 表⽰:以 i 位置元素为结尾的所有的⼦序列中,最后⼀个位置相减为正数的最⻓摆动序列的⻓度;g[i] 表⽰:以 i 位置元素为结尾的所有的⼦序列中,最后⼀个位置相减为负数的最⻓摆动序列的⻓度。
- 状态转移方程:1.如果子序列长度为1,那么f[i]、g[i]就只能为1;2.如果子序列长度大于1,以i位置进行分析,如果nums[i] > nums[i - 1],即最后⼀个位置相减为正数,此时应该入f表,并且应该寻找i - 1位置为值相减为负数的表,也就是g表,所以
f[i] = g[i - 1] + 1,那么当nums[i] < nums[i - 1]时,g[i] = f[i - 1] + 1
;因为要求子序列的最长长度,所以将i位置前面的元素都遍历一遍即可。 - 初始化:所有的元素单独都能构成⼀个摆动序列,因此可以将f、g表内所有元素初始化为1 。
- 填表顺序:从左往右。
- 返回值:返回f表、g表中的最大值。
4.代码如下:
class Solution
{
public:
int wiggleMaxLength(vector<int>& nums)
{
int n = nums.size();
vector<int> f(n, 1), g(n, 1); // 1. 创建 dp 表 + 初始化
int ret = 1; // 更新最终结果
for (int i = 1; i < n; i++) // 2. 填写 f[i] 以及 g[i]
{
for (int j = i - 1; j >= 0; j--)
{
if (nums[j] < nums[i]) //j相当于i - 1,i - 2等
f[i] = max(g[j] + 1, f[i]);
else if (nums[j] > nums[i])
g[i] = max(f[j] + 1, g[i]);
}
ret = max(ret, max(f[i], g[i]));
}
// 3. 返回结果
return ret;
}
};
3. 最长递增子序列的个数
1.题目链接:最长递增子序列的个数
2.题目描述:
给定一个未排序的整数数组 nums , 返回最长递增子序列的个数 。
注意 这个数列必须是 严格 递增的。
示例 1:
输入: [1,3,5,4,7]
输出: 2
解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。
示例 2:
输入: [2,2,2,2,2]
输出: 5
解释: 最长递增子序列的长度是1,并且存在5个子序列的长度为1,因此输出5。
提示:
1 <= nums.length <= 2000
-10^6 <= nums[i] <= 10^6
3.问题分析:这道题就有些意思,由题知有两点是变化的,一个是递增数列的长度,另一个是递增数列的个数,将上述这两个信息能管理起来,那么就可以求出最长递增数列的个数。用一个f表表示以 i 为结尾的最⻓递增⼦序列的⻓度;g表表示:以 i 为结尾的最⻓递增⼦序列的个数;用len表示返回递增子序列的长度,用count表示返回递增子序列的个数。
以 i 位置进行分析:如果nums[i] > nums[j]( j 表示的是i位置前面的元素),1.此时如果f[j] + 1 == f[i],说明有不同序列相同的长度,所以g[i] += g[ j ],因为 j 位置不一定为1个元素,可能为多个,所以+=;2.如果f[j] + 1 > f[i],说明需要更新长度,即更新f表,但f表一更新,g表也要更新,此时f[i] = f[ j ],g[i] = g[ j ],长度更新后,i 位置的递增子序列的个数等于 j 位置的个数。遍历完一个循环后统计要返回的个数,先看要返回的长度是否增加,如果增加那么就更新,更新为f表、g表的最新值,否则就加上遍历后的递增子序列的新个数。
4.代码如下:
class Solution {
public:
int findNumberOfLIS(vector<int>& nums) {
int n = nums.size();
vector<int> f(n, 1), g(n, 1);
int len = 1, count = 1;
for (int i = 1; i < n; ++i)
{
for (int j = 0; j < i; ++j)
{
if (nums[i] > nums[j])
{
if (f[j] + 1 == f[i])
g[i] += g[j];
else if (f[j] + 1 > f[i])
{
f[i] = f[j] + 1;
g[i] = g[j];
}
}
}
if (len == f[i])
count += g[i];
else if (len < f[i])
{
len = f[i];
count = g[i];
}
}
return count;
}
};
4. 最长等差数列
1.题目链接:最长等差数列
2.题目描述:
给你一个整数数组 nums,返回 nums 中最长等差子序列的长度。
回想一下,nums 的子序列是一个列表 nums[i1], nums[i2], …, nums[ik] ,且 0 <= i1 < i2 < … < ik <= nums.length - 1。并且如果 seq[i+1] - seq[i]( 0 <= i < seq.length - 1) 的值都相同,那么序列 seq 是等差的。
示例 1:
输入:nums = [3,6,9,12]
输出:4
解释: 整个数组是公差为 3 的等差数列。
示例 2:
输入:nums = [9,4,7,2,10]
输出:3
解释: 最长的等差子序列是 [4,7,10]。
示例 3:
输入:nums = [20,1,15,3,10,5,8]
输出:4
解释: 最长的等差子序列是 [20,15,10,5]。
提示:
2 <= nums.length <= 1000
0 <= nums[i] <= 500
3.问题分析:前几道所求是递增或递减序列,只需要两个数就可以知道它两是递增还是递减;而这道题所求为等差数列,要知道一个序列是否为等差数列,最少需要知道3个数;所以答题思路就和前几道差不多,一个for循环确定一个数,三个for循环就可以解决这道题;这里有一种思路,就是可以将数组中的元素和其下标存到一个哈希表中,找第三个数时,可以降低时间复杂度。其中填哈希表时应该注意:因为nums数组中可能会有相同的数值,当填到最后一个相同的元素时,下标会被覆盖,所以可以用数组存下标,另一种方式就是一遍寻找,一遍保存,保存距离最近的元素。
- 状态表示:dp[i][j] 表⽰:以 i 位置以及 j 位置的元素为结尾的所有的⼦序列中,最⻓的等差序列的⻓度。
- 状态转移方程:以i,j位置进行分析,i,j位置所表示等差序列中第二,第三位数,然后在i位置之前寻找符合条件的数,如果符合,那么dp[i][j]位置的值该如何确定?i, j表示以 i 及 j 位置为结尾 的,那么新找到数的线标(符合条件数的下标)是不是就为新的 i ,而之前的 i 是不是就为新的 j ;如:符合条件的下标 i 、 j ;dp[i][j] = dp[符合条件的下标][i] + 1。
- 初始化:因为等差数列的长度不是0,就是比3大,所以将所有值初始化为2
- 填表顺序:固定倒数第二个数,再由倒数第一个数寻找符合条件的数。
- 返回值:返回dp表中最大的元素。
4.代码如下:
class Solution
{
public:
int longestArithSeqLength(vector<int>& nums)
{
int n = nums.size();
unordered_map<int, int> hash;
hash[nums[0]] = 0;
int ret = 2;
vector<vector<int>> dp(n, vector<int>(n, 2));
for (int i = 1; i < n; ++i) //固定倒数第二个数
{
for (int j = i + 1; j < n; ++j)
{
int a = 2 * nums[i] - nums[j];
if (hash.count(a))
dp[i][j] = dp[hash[a]][i] + 1;
ret = max(ret, dp[i][j]);
}
hash[nums[i]] = i;
}
return ret;
}
};
5. 等差数列划分 II - 子序列
1.题目链接:等差数列划分 II - 子序列
2.题目描述:
给你一个整数数组 nums ,返回 nums 中所有 等差子序列 的数目。
如果一个序列中 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该序列为等差序列。
例如,[1, 3, 5, 7, 9]、[7, 7, 7, 7] 和 [3, -1, -5, -9] 都是等差序列。
再例如,[1, 1, 2, 5, 7] 不是等差序列。
数组中的子序列是从数组中删除一些元素(也可能不删除)得到的一个序列。
例如,[2,5,10] 是 [1,2,1,2,4,1,5,10] 的一个子序列。
题目数据保证答案是一个 32-bit 整数 。
示例 1:
输入:nums = [2,4,6,8,10]
输出:7
解释:所有的等差子序列为: [2,4,6] [4,6,8] [6,8,10] [2,4,6,8] [4,6,8,10] [2,4,6,8,10] [2,6,10]
示例 2:
输入:nums = [7,7,7,7,7]
输出:16
解释:数组中的任意子序列都是等差子序列。
提示:
1 <= nums.length <= 1000
-2^31 <= nums[i] <= 2^31 - 1
3.题目解析:这道题是在一个数组中寻找等差子序列的总个数,并且等差数列可以相同。对于寻找等差数列还是用和前几道一样的方法,一个for循环固定等差数列倒数第二的元素,另一个固定等差数列倒数第一的元素,然后寻找构成等差数列的数,如果再加一个for循环,那么时间复杂度就太大了,所以用一个hash表将nums中的元素和元素下标管理起来,这样寻找的时间复杂度接近O(1)。需要注意的是nums数组中的元素可能会有重复的,所以可以用1对多的关系来管理下标。 因为用两层循环来确定等差数列,所以定义一个二维dp[i][j]表示:以 i 位置及 j 位置为结尾所有等差数列的个数,因为每个i,j位置是不一样的,所以最后返回的是dp表中所有元素的总和。
- 状态表示:
dp[i][j] 表示:以 i 位置以及 j 位置的元素为结尾的所有的⼦序列中,等差⼦序列的个数。其中 i < j ,即 i 为等差数列倒数第二个位置,j 为倒数第一个。
- 状态转移方程:如果寻找的倒数第三个等差数列,这个位置记为 k,那么k,i,j 构成等差数列,k前面有可能还有元素;这里举个例子,将k,i,j 当做子序列的话不容易想到,那么就k,i,j 当做是连续的等差数列,k前面再记几个元素,如下m,n,k,i,j 这5位数构成等差数列,以k,i 位置来分析,dp表表示的是以 i 位置以及 j 位置的元素为结尾等差⼦序列的个数,那么以i,j 为结尾是比k,i 为结尾的个数多一个,详解如等差数列划分1。所以状态转移方程为:dp[i][j] += dp[k][i] = 1 。
- 填表顺序:从上往下,从左往右。
- 初始化:dp表全初始化为0。
- 返回值:返回的是dp表中所有元素的总和。
4.代码如下:
class Solution
{
public:
int numberOfArithmeticSlices(vector<int>& nums)
{
int n = nums.size();
unordered_map<long long, vector<int>> hash; //一对多的关系
for (int i = 0; i < n; ++i) //将所有nums数组中的元素与其对应的下标管理起来
hash[nums[i]].push_back(i);
int ret = 0;
vector<vector<int>> dp(n, vector<int>(n));
for (int i = 1; i < n; ++i) //确定倒数第二个数
{
for (int j = i + 1; j < n; ++j) //确定倒数第一个数
{
long long a = ((long long)nums[i]) * 2 - nums[j]; //相乘可能会越界,所以先强转,再相乘
if (hash.count(a)) //寻找符合条件的数
{
for (auto& k : hash[a])
{
if (k < i) //降符合的个数全加起来
dp[i][j] += dp[k][i] + 1;
}
}
ret += dp[i][j];
}
}
return ret;
}
};