子序列问题
文章目录
- 300. 最长递增子序列
- 376. 摆动序列
- 673. 最长递增子序列的个数
- 646. 最长数对链
- 1218. 最长定差子序列
- 873. 最长的斐波那契子序列的长度
- 1027. 最长等差数列
- 446. 等差数列划分 II - 子序列
300. 最长递增子序列
题目链接
子序列的性质:不连续
子数组/子串:连续
状态表示:dp[i]:以i为结尾的数组的所有子序列中,最长递增子序列的长度。
状态转移方程:
核心逻辑是:对于每个 i(当前元素),通过遍历它之前的所有元素 j(j < i),判断能否通过 j 对应的子序列延长出更长的子序列。
转移条件:
当 nums[i] > nums[j] 时(即当前元素大于前序元素,满足 “递增” 条件),说明可以将 nums[i] 接在 “以 nums[j] 结尾的子序列” 后面,形成一个更长的子序列。此时,该新子序列的长度为 dp[j] + 1(dp[j] 是前序子序列的长度,加 1 是因为新增了 nums[i])。
class Solution {
public:int lengthOfLIS(vector<int>& nums) {int n=nums.size();vector<int>dp(n,1);int ret=1;for(int i=1;i<n;i++){
for(int j=0;j<i;j++)
if(nums[i]>nums[j])dp[i]=max(dp[i],1+dp[j]);ret=max(ret,dp[i]);}
return ret;}
};
376. 摆动序列
题目链接
根据题意,要找正负交替的子序列的最大长度。
先看看如何才能正负交替,下一个数比上一个数大–》为正。下比上小–》为负。所以我们要找的就是 一组 递增-》递减-》递增-》递减 这样的摆动的数。
和子数组专题里的”湍流数组“一样。
我们可以总结一下,像这种 具有 “二元对立属性” 题,一般都是要两个dp去表示 这两个对立的属性(这里是 数据的方向,向上/向下 (递增/递减)。像子数组专题里还有求 ”乘积最大“ ”乘积为正数“的子数组,二元对立就是 正 和 负)。然后再相互依赖推导转移方程。
核心逻辑是:“当前属性 X 的状态,只能由前序对立属性 Y 的状态转移而来”。
回到这题。我们了解到具体的方法后,直接状态表示就行。
- f[i]:以 nums[i] 为结尾的最长摆动子序列中,最后一步是 “递增”(即 nums[i] 大于前一个元素)的序列长度。
- g[i]:以 nums[i] 为结尾的最长摆动子序列中,最后一步是 “递减”(即 nums[i] 小于前一个元素)的序列长度。
然后状态转移方程和子数组有个区别,其实也就是子序列和子数组的区别。
子序列不要求连续,所以对i位置讨论的时候,i的前序子数组 可能是前面的任意位置。只需要再加一层循环去遍历i前的位置,然后判断即可
class Solution {
public:int wiggleMaxLength(vector<int>& nums) {int n=nums.size();vector<int>f(n,1);auto g=f;int ret=1;for(int i=1;i<n;i++){for(int j=0;j<i;j++){if(nums[i]>nums[j])f[i]=max(f[i],g[j]+1);else if(nums[i]<nums[j])g[i]=max(g[i],f[j]+1);}ret=max(ret,max(f[i],g[i]));}return ret;}
};
673. 最长递增子序列的个数
题目链接
找最长递增子序列我们前面做过了,思路是一样的。
这题只需要再找出个数即可。
分析如何找个数:
count[i]:以i为结尾,递增子序列的个数。
同样根据子序列的特性(不连续),我们知道,前序的递增子序列的结尾是不固定的。但前序递增子序列的长度肯定是 当前所求的长度-1,所以只需要把长度符合的情况累加即可
class Solution {
public:int findNumberOfLIS(vector<int>& nums) {int n=nums.size();vector<int>len(n,1),count(n,1);int maxlen=1,ret=0;for(int i=1;i<n;i++){for(int j=0;j<i;j++){if(nums[i]>nums[j]){if(len[j]+1>len[i]){len[i]=len[j]+1;count[i]=count[j];}else if(len[j]+1==len[i]){count[i]+=count[j];}}}maxlen=max(maxlen,len[i]);}for(int i=0;i<n;i++){if(len[i]==maxlen)ret+=count[i];}return ret;}
};
646. 最长数对链
题目链接
和前面思路一样,子序列不连续,所以引入j来遍历前i组中符合条件的前序dp。
预处理:提前将对数链按照第一个元素排序,保证所有可能作为当前数对前驱的数对都被包含在遍历范围内(j < i)
class Solution {
public:int findLongestChain(vector<vector<int>>& pairs) {int n=pairs.size();vector<int>dp(n,1);int ret=1;sort(pairs.begin(),pairs.end(),[](const vector<int>&a,const vector<int>&b){return a[0]<b[0];});for(int i=1;i<n;i++){for(int j=0;j<n;j++){if(pairs[i][0]>pairs[j][1])dp[i]=max(dp[i],dp[j]+1);}ret=max(dp[i],ret);}return ret;}
};
1218. 最长定差子序列
题目链接
需要找到数组中最长的子序列,使得子序列中相邻元素的差等于给定的 difference
用 unordered_map<int, int> hash 存储状态,其中:hash[x] 表示:以值 x 为结尾的、满足 “相邻元素差为 difference” 的最长子序列的长度。
-
为什么用哈希表?因为子序列的 “前一个元素” 必须是 x - difference(这样 x - (x - difference) = difference,满足等差条件),而哈希表可以快速查询 “x - difference 是否存在” 以及其对应的最长子序列长度,避免了暴力遍历的低效。
-
初始化逻辑:第一个元素 arr[0] 自身可构成长度为 1 的子序列,因此 hash[arr[0]] = 1。
状态转移方程(hash[arr[i]] 的计算方式)
对于数组中的每个元素 arr[i](记为 x),要计算以 x 为结尾的最长子序列长度,需依赖 “前一个符合条件的元素”:
- 前一个元素必须是 x - difference(因为 x - (x - difference) = difference,满足等差条件)。
- 若 x - difference 存在于哈希表中(即之前出现过该值),则以 x 结尾的子序列长度 = 以 x - difference 结尾的子序列长度 + 1(在其基础上新增 x)。
- 若 x - difference 不存在(即之前没出现过),则 x 自身构成一个子序列,长度为 1(此时 hash[x - difference] 默认为 0,加 1 后为 1)。
class Solution {
public:int longestSubsequence(vector<int>& arr, int difference) {unordered_map<int,int>hash;hash[arr[0]]=1;int ret=1;for(int i=1;i<arr.size();i++){hash[arr[i]]=hash[arr[i]-difference]+1;
ret=max(ret,hash[arr[i]]);}return ret;}
};
873. 最长的斐波那契子序列的长度
题目链接
一、问题分析
- 定义:给定严格递增的数组 arr,找出最长的斐波那契子序列的长度。斐波那契子序列需满足:存在下标 i < j < k,使得 arr[i] + arr[j] = arr[k]。
- 目标:返回最长斐波那契子序列的长度;若不存在(长度不足 3),返回 0。
二、动态规划解法
- 状态表示
dp[i][j]:以 arr[i] 和 arr[j] 为最后两个元素的斐波那契子序列的最长长度(需满足 i < j)。 - 状态转移方程
对于每一对 (i, j)(i < j),假设子序列的前一个元素为 a = arr[j] - arr[i]:
- 若 a 存在于数组中(通过哈希表快速判断),且 a < arr[i](因数组严格递增,保证 a 的下标 k < i),则:dp[i][j] = dp[k][i] + 1(在以 arr[k] 和 arr[i] 结尾的子序列后,添加 arr[j],长度 + 1)。
- 若 a 不存在,dp[i][j] 保持初始值 2(仅包含 arr[i] 和 arr[j] 两个元素,不满足 “三个及以上元素” 的斐波那契子序列要求)。
- 预处理:哈希表加速查找
利用数组严格递增的特性,用 unordered_map<int, int> hash 存储「元素值 → 下标」的映射,实现 O(1) 时间判断 “前一个元素 a 是否存在于数组中”。 - 初始化
dp 数组所有元素初始化为 2,因为每一对 (i, j) 至少能构成长度为 2的序列(仅包含 arr[i] 和 arr[j] 两个元素)。
class Solution {
public:int lenLongestFibSubseq(vector<int>& arr) {unordered_map<int,int>hash;int n=arr.size(),res=2;for(int i=0;i<n;i++)hash[arr[i]]=i;vector<vector<int>>dp(n,vector<int>(n,2));
for(int j=2;j<n;j++)
{for(int i=1;i<j;i++){int a=arr[j]-arr[i];if(a<arr[i]&&hash.count(a))dp[i][j]=dp[hash[a]][i]+1;res=max(res,dp[i][j]);}
}
return res<3?0:res;}
};
1027. 最长等差数列
题目链接
和前面的题一样,不过注意一下等差的表示
nums[j] - nums[i]=nums[i]-nums[k]
故而:nums[k] = 2*nums[i] - nums[j]
class Solution {
public:int longestArithSeqLength(vector<int>& nums) {int n=nums.size();unordered_map<int,int>hash;hash[nums[0]]=0;vector<vector<int>>dp(n,vector<int>(n,2));int ret=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;}
};
446. 等差数列划分 II - 子序列
题目链接
dp[i][j] : “以 i, j 为结尾的长度 ≥3 的等差子序列数量”。
统计后累加。
class Solution {
public:
#define ll long longint numberOfArithmeticSlices(vector<int>& nums) {int n=nums.size();int res=0;unordered_map<ll,vector<int>>hash;for(int i=0;i<n;i++)hash[nums[i]].push_back(i);vector<vector<int>>dp(n,vector<int>(n));for(int j=2;j<n;j++){for(int i=1;i<j;i++){auto a=2*(ll)nums[i]-nums[j];if(hash.count(a))for(auto&e:hash[a])if(e<i)dp[i][j]+=dp[e][i]+1;else break;res+=dp[i][j];}}return res;}
};