子序列并不强制要求元素是连续的,子数组才要求元素必须连续,子序列包含子数组,但都有顺序性的概念,因为这种顺序性,就方便我们填表

因为不连续的特性,子序列并不像求子数组一样考虑问题只有两个方面。在思考i位置的dp值是多少时,dp值与i位置之前一整段区间的dp值大小都有关,所以需要遍历全面的区间,再加上i位置综合考虑更新dp值。区间也不一定是i位置之前的(比如dp[i][j]表示依次以下标i和j结尾的等差数列的最大长度),具体需要根据题意,所以往往第一层for循环找当前位置dp值,第二层for循环在dp表里找如何填当前dp值。
上菜!<( ̄︶ ̄)↗[GO!]
首先来一到子序列的最最最经典的题目,点击题目可以跳转网页
No.1 最长递增子序列

题目解析:
返回最长的严格递增的子序列的长度
思路:
依旧以i位置为结尾,dp[i]表示 以i位置元素为结尾 的所有子序列中,最长递增子序列的长度

因为每次填表的时候,我们仅仅考虑nums[i],看这个nums[i]“刚好”可以插入到哪个子序列的尾部,因为子序列是可以不要求连续的,如此dp值就表示以这个元素为结尾的所有子序列的最长长度。
那么,刚哈可以插入到哪个子序列的尾部是怎么实现的?
我们填i位置的时候,要么这个位置自己单飞;要么就插入到前面子序列的尾部,可以用j遍历0~i-1位置的数组的值,遇到比i位置小的就符合相当于一种暴力遍历,那么我们求一下这个区间的最大的dp[j]+1,那么就得到了dp[i]。

因为单飞你dp值就是1,也就是说最起码的长度就是1,所以干脆初始化的时候全部赋值为1,这样就少写一步要单飞的代码
直接从第二个位置,也就是下标为1的位置开始遍历即可
另外我们要返回最长的长度,可以创建一个变量,每次dp值更新的时候,我们也“贪心”地更新这个变量
代码实现:
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n=nums.size();
vector<int> dp(n,1);
int ret=1; //最差情况是1,不能初始化为INT_MIN
for(int i=1;i<n;++i)
{
for(int j=0;j<i;++j)
{
if(nums[i]>nums[j])
{
dp[i]=max(dp[j]+1,dp[i]);
}
ret=max(ret,dp[i]);
}
}
return ret;
}
};
No.2 摆动序列


题目解析:
摆动序列就是元素从左到右扫过去,数值上一上一下,呈波浪状
返回最长摆动序列的长度
思路:
先思考能不能像之前一样,dp[i]表示以i位置为结尾的最长摆动序列的长度。
如果是这样,我们的表其实不好填,因为我们的dp值有两个状态,你不知道你填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,f[i],g[i]});
}
return ret;
}
};
No.3 最长递增子序列个数

题目解析:
其实就是No.1的变型,这次返回个数
思路:
小demo
思考:如何在数组中找到 最大的值出现次数?要求遍历一次数组就完成。
很简单,创建两个变量,一个maxval存最大值,count计数,遍历数组元素假设遍历到的元素是x:

那么我们本题其实可以复刻这样的思路:
先假设dp[i]表示,以i位置元素为结尾的所有子序列中,最长递增子序列的个数。这个dp表的一个位置包含了两个信息,一个是子序列的当前最大长度,一个是次数。所以不如干脆分两个表填,清晰明了:

状态分析如下:
遍历到i位置的时候,再遍历0~i位置的len表,如果遇到小的值,判断j位置的len值+1与i位置len值大小,相等+=,大于的时候更新len表,同时更新coun[i]将其置为count[j],小于不做处理

既然可以单飞,干脆全部初始化为1
另外我们可以同样的思路创建两个变量,这两个变量在i位置len表和count表填好之后,进行判断更新,本质上都是len辅助count更新
代码实现:
class Solution {
public:
int findNumberOfLIS(vector<int>& nums) {
int n=nums.size();
vector<int> len(n,1);
auto count=len;
int retlen=1,retcount=1;
for(int i=1;i<n;++i)
{
for(int j=0;j<i;++j)
{
if(nums[j]<nums[i])
{
//更新count数组
if(len[j]+1==len[i])count[i]+=count[j];
else if(len[j]+1>len[i])len[i]=len[j]+1,count[i]=count[j];
}
}
//同样思路,贪心更新retcount
if(len[i]==retlen)retcount+=count[i];
else if(len[i]>retlen)retlen=len[i],retcount=count[i];
}
return retcount;
}
};
No.4 最长数对链

题目解析:
数对里的元素,第二个大于第一个,返回最长数对链长度
思路:
先按照第一个元素将数组升序排序,后续的处理就是依照第二个元素进行最长递增子序列
因为我们按照第一个元素将数组升序排序,假设现在有两个数对 [a,b] , [c,d] 那么c>a,因为d>c,所以d>a。那么我们其实就只需要看第二个元素就行了,这不就是和第一题一样了。
代码实现:
class Solution {
public:
int findLongestChain(vector<vector<int>>& pairs) {
sort(pairs.begin(),pairs.end());
int n=pairs.size();
vector<int> dp(n,1);
int ret=1;
for(int i=1;i<n;++i)
{
for(int j=0;j<i;++j)
{
if(pairs[i][0]>pairs[j][1])
{
dp[i]=max(dp[i],dp[j]+1);
}
}
ret=max(ret,dp[i]);
}
return ret;
}
};
可以加入哈希表优化0~i-1次的遍历
No.5 最长定差子序列

思路:
dp[i]表示以i位置为结尾的所有子序列中,最长的等差子序列的长度。
遍历i位置的时候假设这个位置数组值为a,那么我们要到之前0~i-1的位置,找一个值看下是否等于a-difference,找到就拿这个位置下标(假设是j),所以dp[i]=dp[j]+1
其实我可以将数组的值存进哈希表,这个值映射最长等差子序列长度,用哈希表代替dp表,这样查找效率就是O(1),而且我们填表的顺序是从左往右填的,这也保证了我找a-difference是之前填表存在的,下标在更前面,这符合子序列的顺序性。
初始化时将arr[0]映射1即可。
代码实现:
class Solution {
public:
int longestSubsequence(vector<int>& arr, int difference) {
//哈希表代替dp数组,这样要访问元素时间复杂度就是O(1)
unordered_map<int, int> hash;
hash[arr[0]] = 1;
int ret=1,n=arr.size();
for (int i = 1; i < n; ++i) {
hash[arr[i]] = hash[arr[i] - difference]+1;
ret=max(ret,hash[arr[i]]);
}
return ret;
}
};
No.6 最长的斐波那契子序列的长度


题目解析:
要求子序列的元素中,每三个相邻元素,前两个相加等于第三个数。
返回满足这种情况的最长子序列长度
原数组是严格递增的,这意味着没有重复元素
思路:
先思考dp[i]表示 以i位置为结尾的所有的子序列中,最长的斐波那契子序列的长度,是否可行?
当然不行,填dp[i]的时候能拿到arr[i]的数值,但是为了符合斐波那契性质,我们还需要拿到,到这个位置的时候,最长的斐波那契子序列的最后一个位置的元素的数值,以及判断arr[i]减去这个数值的数,在子序列中是否存在,这样才能更新dp值。
所以我们还需要一个变量,存储遍历到j位置的时候,我们需要当前最长斐波那契子序列的末尾元素的下标i,(i<j),设c为arr[i],b为arr[j],那么假设a=c-b,判断i位置之前有没有a的存在,存在就更新dp值

我们可以用哈希表,建立数值与下标的映射,提高找a的效率,这也时间复杂度会n的三次方降低到n的平方
而且题目也说了数组严格递增,是没有重复元素的,这恰好能帮助我们数值和下标一一映射
下面就是细节思考:
因为数组的最低大小是2,且当0~i-1位置找不到a的时候,dp[i][j]两个下标的数值要单飞了,长度就是2,所以我们dp值全部初始化为2
返回的时候需要判断ret是否小于3,这在题目中也提示了
代码实现:
class Solution {
public:
int lenLongestFibSubseq(vector<int>& arr) {
int n=arr.size();
unordered_map<int,int> hash;
//使用哈希表存,由值映射下标,方便后续O(1)查找对应值下标
for(int i=0;i<n;++i)hash[arr[i]]=i;
//数组第一个坐标表示左边值下标,第二个坐标表示右边值下标
vector<vector<int>> dp(n,vector<int>(n,2));
int ret=2;//最少都有两个
//j直接从三个位置开始判断,i从第二个位置开始判断
for(int j=2;j<n;++j)
{
for(int i=1;i<j;++i)
{
int a=arr[j]-arr[i];
//先判断a是不是小于arr[i],因为原数组严格递增,这意味着a的坐标在i左边
if(a<arr[i]&&hash.count(a))dp[i][j]=dp[hash[a]][i]+1;
ret=max(ret,dp[i][j]);
}
}
return ret<3?0:ret;
}
};
No.7 最长等差数列


题目解析:
返回最长等差数列长度
思路:
dp[i][j]表示 以i以及j为结尾的所有子序列中最长等差数列长度(i<j)
往i之前找2*nums[i]-nums[j]即可,但可能会有重复,要用距离i下标最近的那个下标的。可以通过边填表边更新hash实现,这样越后出现的重复元素可以覆盖上次的下标,
不过如此我们需要固定倒数第二个下标i,枚举最后一个数,每次填完dp[i][j],更新hash[nums[i]]。
代码实现:
class Solution {
public:
int longestArithSeqLength(vector<int>& nums) {
unordered_map<int,int> hash;
hash[nums[0]]=0;
int n=nums.size(),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;
}
};
No.8 等差数列划分


题目解析:
返回所有等差子序列的个数
思路:
dp[i][j]表示 以i以及j为结尾的所有子序列中等差数列数量(i<j)
因为有重复元素,而且不同下标重复元素形成的相同子序列都有效,所以可以填表之前将数组值映射对应下标数组,
只需要固定末尾的数,倒数第二个数往0~j-1遍历,然后找hash里有没有2*nums[i]-nums[j],有的话遍历下标数组,满足下标<i的,dp[i][j]+=dp[index][i]+1;
因为不考虑 有重复元素要最近的那个重复元素 的情况,所以我们采用固定倒数第一个,枚举倒数第二个数的策略,这样就可以在dp前将hash表填好备用;而不像上一题需要边dp边更新,因为上一题需要考虑最近的那个重复元素。
注意这里nums[i]的范围,如果乘以2可能会溢出,所以用long long 来存,哈希表第一个类型就也要修改成long long。
代码实现:
class Solution {
public:
int numberOfArithmeticSlices(vector<int>& nums) {
unordered_map<long long,vector<int>> hash;
int n=nums.size();
for(int i=0;i<n;++i)hash[nums[i]].push_back(i);
vector<vector<int>> dp(n,vector<int>(n));
int sum=0;
for(int j=2;j<n;++j)
{
for(int i=1;i<j;++i)
{
long long a=(long long)2*nums[i]-nums[j];
if(hash.count(a))
{
for(auto&index:hash[a])
{
if(index<i)dp[i][j]+=dp[index][i]+1;
}
}
sum+=dp[i][j];
}
}
return sum;
}
};
此篇完。

245

被折叠的 条评论
为什么被折叠?



