动规:子序列系列

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

因为不连续的特性,子序列并不像求子数组一样考虑问题只有两个方面。在思考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;
    }
};

此篇完。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_dindong

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值