【动态规划】子序列问题

个人主页zxctscl
专栏 【C++】【C语言】【Linux】【数据结构】【算法】
如有转载请先通知

文章目录

  • 前言
  • [1 ==300. 最长递增子序列(经典)==](#1 ==300. 最长递增子序列(经典)==)
    • [3.1 分析](#3.1 分析)
    • [3.2 代码](#3.2 代码)
  • [2 376. 摆动序列](#2 376. 摆动序列)
    • [2.1 分析](#2.1 分析)
    • [2.2 代码](#2.2 代码)
  • [3 673. 最长递增子序列的个数](#3 673. 最长递增子序列的个数)
    • [3.1 分析](#3.1 分析)
    • [3.2 代码](#3.2 代码)
  • [4 646. 最长数对链](#4 646. 最长数对链)
    • [4.1 分析](#4.1 分析)
    • [4.2 代码](#4.2 代码)
  • [5 1218. 最长定差子序列](#5 1218. 最长定差子序列)
    • [5.1 分析](#5.1 分析)
    • [5.2 代码](#5.2 代码)
  • [6 873. 最长的斐波那契子序列的长度](#6 873. 最长的斐波那契子序列的长度)
    • [6.1 分析](#6.1 分析)
    • [6.2 代码](#6.2 代码)
  • [7 1027. 最长等差数列](#7 1027. 最长等差数列)
    • [7.1 分析](#7.1 分析)
    • [7.2 代码](#7.2 代码)
  • [8 446. 等差数列划分 II - 子序列](#8 446. 等差数列划分 II - 子序列)
    • [8.1 分析](#8.1 分析)
    • [8.2 代码](#8.2 代码)

前言

在上一篇有关动态规划的博客中,谈到做这类题目的步骤,有需要的可以点这个链接: 【动态规划】斐波那契额数列模型。继续分享这个模型类型的题目。

1 300. 最长递增子序列(经典)

子序列中挑选的元素是可以不连续 的,但是得保证子序列中元素的出现的相对顺序和原数组中是一致的

像[a,b,d]就是[a,b,c,d,e]的一个子序列,而[d,a,b]就不是。

3.1 分析

  1. 状态表示

    dp[i]表示:以i位置为结尾的所有子序列中,最长递增子序列的长度。

  2. 状态转移方程

    可以分两类,一类是就以i为子序列;

    第二类是跟前面元素一起构成子序列,但此时必须是前面的元素小于i,此时条件下再找最长子序列,要想和i组成最长,j也是最长子序列就是dp[j],再加上1就是最大的子序列。

  3. 初始化

    把dp表里面所有值初始化为1,就不用考虑为1的情况了。

  4. 填表顺序

从左往右

  1. 返回值
    dp表里最大值

3.2 代码

cpp 复制代码
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[j]<nums[i])
                {
                    dp[i]=max(dp[j]+1,dp[i]);
                }
            }
            ret=max(ret,dp[i]);
        }

        return ret;
    }
};

2 376. 摆动序列

2.1 分析

题目已知,仅有一个元素或者含两个不等元素的序列也视作摆动序列。

示例二中最长摆动序列长度是7

  1. 状态表示

    dp[i]表示:以i位置为结尾的所有的子序列中,最长的摆动序列的长度。

    最后一个位置可以分为是下降位置,也开始是上升位置:

    此时就细分为两个:
    g[i]表示:以i位置为结尾的所有的子序列中,最后一个位置呈现"下降"趋势的最长摆动序列的长度。
    f[i]表示:以i位置为结尾的所有的子序列中,最后一个位置呈现"上升"趋势的最长摆动序列的长度。

  2. 状态转移方程

    f[i]可以分为,以i自己为一类,还可以分为和前面的i-1组合,此时将j设置为(0,i-1)

    f是呈现上升趋势的,此时i位置的值是大于i-1位置的,也就是说:nums[j]<nums[i],此时如果找到以j最后一个位置呈现"下降"趋势的最长的g[j]再加1,但是要的是一个最大值就是max(g[j]+1),f[i])。

g[i]可以分为,以i自己为一类,还可以分为和前面的i-1组合,此时将j设置为(0,i-1)

g是呈现下降趋势的,此时i位置的值是大于i-1位置的,也就是说:nums[j]>nums[i],此时如果找到以j最后一个位置呈现"上升"趋势的最长的g[j]再加1,但是要的是一个最大值就是max(f[j]+1),g[i])。

  1. 初始化

    f表和g表全部初始化为1,就不用考虑长度为1的情况。

  2. 填表顺序

    从左往右,两个表一起填

  3. 返回值

    两个表里面的最大值

2.2 代码

cpp 复制代码
class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        int n=nums.size();
        
        vector<int> g(n,1),f(n,1);
        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(g[i],f[i]);
       }
        return ret;
    }
};

3 673. 最长递增子序列的个数

如何一次遍历在数组中找到最大值出现的次数?

此时用到贪心,首先可以用两个变量,一个maxval用来记录当前扫描到数组中的最大值,另一个用来记录这个最大值出现的次数。

这时候就会出现三种情况:

3.1 分析

  1. 状态表示
    dp[i]表示:以i位置为结尾的所有子序列中,最长递增子序列的个数。

但是此时连最长的子序列多长都不知道,这个状态表示是不够的。

len[i]表示:以i位置为结尾的所有子序列中,最长递增子序列的"长度"。
count[i]表示:以i位置为结尾的所有子序列中,最长递增子序列的"个数"。

  1. 状态转移方程
    第一步以i位置单独为一个子序列就是1;
    第二步遍历[0,i-1]要形成递增子序列就必须是nums[j]<nums[i],要找到len[j]+1与len[i]中的最大值。

既要找到最长的长度也得找到最长长度出现的次数。

单独一个序列长度就是1;

形成递增序列就必须是nums[j]<nums[i],此时会出现三种情况:(1)加上i位置能形成最长递增子序列,j元素为结尾能形成count[j]个递增子序列,此时总共就有count[i]+count[j]个递增子序列。(2)加上i位置能形成最长递增子序列,但跟在后面形成的子序列个数比len[i]少,此时就无视。(3)加上i位置能形成最长递增子序列长度会变成,此时就得更新最长的递增子序列,而且得重新计数,因为是用j位置来更新的,所以const[i]就等于count[j]。

  1. 初始化

    两个表都初始化为1

  2. 填表顺序

    从左往右

  3. 返回值

    贪心策略

3.2 代码

cpp 复制代码
class Solution {
public:
    int findNumberOfLIS(vector<int>& nums) {
        int n=nums.size();
        vector<int> len(n,1),count(n,1);

        int retlen=1,retcount=1;
        for(int i=1;i<n;i++)
        {
            for(int j=0;j<i;j++)
            {
                if(nums[j]<nums[i])
                {
                    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];
                }
            }
        
         if(retlen==len[i])retcount+=count[i];
         else if(retlen<len[i])retlen=len[i],retcount=count[i];
        }
        
        return retcount;
    }
};

4 646. 最长数对链

4.1 分析

要想得到这种形式的数对链,如果考虑以i位置为结尾,那么比i位置小的都在它前面,比它小的都在它后面,就得提前做一下数据的处理,把原数组按照第一个元素排序。

如果[a,b][c,d]是已经按照第一个元素排好序,那么c>=a,而在pair中左边元素始终小于右边元素,也就是说c<d,此时就能得出d>a,所以[a,b]是绝对不能连在[c,d]后面的。
排序就能保证以i位置为结尾的倒数第二个元素就一定在i位置左边。

  1. 状态表示

    dp[i]表示以i位置为结尾的所有的数对链中,最长的数对链的长度

  2. 状态转移方程

    如果就以i位置为结尾构成一个数对链,长度就是1,;

    如果i位置和前面位置一起构成数对链,就得满足前面i-1位置pair中右边元素dp[j][1]小于以i位置为结尾pair中左边元素dp[i][0],这时长度就是dp[j]+1,如果取里面最长的数对链:

  3. 初始化

    把他们初始化为最差的长度,就都初始化为1

  4. 填表顺序

    从左往右

  5. 返回值

    返回里面的最大值

4.2 代码

cpp 复制代码
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[j][1]<pairs[i][0])
                {
                    dp[i]=max(dp[j]+1,dp[i]);
                }
            }
            ret=max(dp[i],ret);
        }
        return ret;
        
        
    }
};

5 1218. 最长定差子序列

5.1 分析

  1. 状态表示

    dp[i]表示以i位置的元素为结尾的所以子序列中,最长的等差子序列长度。

  2. 状态转移方程

    如果i位置里面的元素是a,倒数第二个元素是b,就是说a-b=diff,此时b=a-diff。

    将b分为两种情况:一种如果b不存在,那么只能是a单独构成子序列,长度就为1;另一种如果b存在,只考虑最后一个,因为b存在那么b前面的元素的值至少是大于或者等于b的值,此时长度就是dp[i]=dp[j]+1

将b的值和dp[j]绑定放在哈希表里 ,就不用再从前往后遍历,就能直接在哈希表里找到b和dp[j],能直接更新dp[i]。

直接在哈希表中做动态规划

  1. 初始化

    以0位置的元素为结尾的所以子序列中,长度就是1。
    hash[arr[0]]=1

  2. 填表顺序

    从左往右

  3. 返回值

    dp表里面的最大值

5.2 代码

cpp 复制代码
class Solution {
public:
    int longestSubsequence(vector<int>& arr, int difference) {
        //创建哈希表
        unordered_map<int,int>hash;//arr[i]-dp[i]
        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;
        
    }
};

6 873. 最长的斐波那契子序列的长度

二维

6.1 分析

  1. 状态表示

    dp[i]表示:以i位置元素为结尾的所有子序列中,最长斐波那契子序列的长度。此时用这个状态表示,就只能知道斐波那契子序列的长度,但并不知道具体的斐波那契数列,所以这个状态表示是不行的。

    如果知道斐波那契数列最后面两个数a,b的值,就能知道斐波那契数列前面的数。

    dp[i][j]表示以i位置以及j位置为结尾的所有子序列中,最长斐波那契子序列的长度。

    此时规定了i<j

  2. 状态转移方程

    假设j位置存放的是c,i位置值是b。

    此时设倒数第三个数下标是k,那么k位置的存放的值就是是c-b

    分三种情况,(1)a存在,而且a<b,那么就能将k和i位置的值拿出来,再往前找前面能构成斐波那契数列的值,也就是dp[k][i],此时长度就是dp[k][i]+1

    (2)a存在,但是a在b c之间,不能构成斐波那契数列,但是此时里面有a b两个元素,所以里面的长度就是2

    (3)a不存在,就不能构成斐波那契数列,但是此时里面有两个元素,所以里面的长度就是2

优化:

在做动态规划之前,先把值和它们下标绑定,就可以先存到哈希表中,就能直接找到下标

  1. 初始化

    把表里所有的值都初始为2

  2. 填表顺序

    从上往下

  3. 返回值

    返回dp表里面的最大值ret,如果里面没有最长斐波那契子序列,就返回0,否则就返回ret

6.2 代码

cpp 复制代码
class Solution {
public:
    int lenLongestFibSubseq(vector<int>& arr) {
        int n = arr.size();
        unordered_map<int, int>hash;
        for (int i = 0; i < n; i++)
        {
            hash[arr[i]] = i;
        }

        int ret = 2;
        vector<vector<int>> dp(n, vector(n, 2));
        for (int j = 2; j < n; j++)//固定最后一个位置
        {
            for (int i = 1; i < j; i++)//固定倒数第二个位置
            {
                int a = arr[j] - arr[i];
                if (hash.count(a) && a < arr[i])dp[i][j] = dp[hash[a]][i] + 1;
                ret = max(ret, dp[i][j]);
            }

        }
        return ret < 3 ? 0 : ret;
    }
};

7 1027. 最长等差数列

7.1 分析

同上面一题类似

  1. 状态表示
    dp[i]表示:以i位置元素为结尾的所有子序列中,最长等差数列子序列的长度。此时用这个状态表示,就只能知道等差数列子序列的长度,但并不知道具体的等差数列的值,所以这个状态表示是不行的。
    如果知道等差数列最后面两个数a,b的值,就能知道等差数列前面的数。

dp[i][j]表示以i位置以及j位置为结尾的所有子序列中,最长等差数列子序列的长度。

此时规定了i<j

  1. 状态转移方程

分三种情况,(1)a存在,而且a<b,那么就能将k和i位置的值拿出来,再往前找前面能构成等差数列的值,也就是dp[k][i],此时长度就是dp[k][i]+1

(2)a存在,但是a在b c之间,不能构成等差数列,但是此时里面有a b两个元素,所以里面的长度就是2

(3)a不存在,就不能构成等差数列,但是此时里面有两个元素,所以里面的长度就是2

优化:

有两种方式:

(1)在做dp之前,先把值和它们下标绑定,就可以先存到哈希表中,<元素,下标组>

(2)一边dp,一边保存离他最近的下标元素的下标<元素,下标>

选择第二种方式填表,当i位置填完之后,将i位置的值放入哈希表中即可

  1. 初始化

    dp表里面初始化为2

  2. 填表顺序

    有两种填表顺序:

    (1)先固定最后一个数j,再枚举倒数第二个数,此时i是一直移动的

    (2)先固定倒数第二个数,再枚举最后一个数,这时候i和j同时向后移动一位

  3. 返回值

    返回dp表中的最大值

7.2 代码

cpp 复制代码
class Solution {
public:
    int longestArithSeqLength(vector<int>& nums) {
        unordered_map<int,int> hash;
        hash[nums[0]]=0;

        int n = nums.size();
        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 ;
    }
    
};

8 446. 等差数列划分 II - 子序列

8.1 分析

同上面一题类似,只是这里要找到是等差数列的个数

  1. 状态表示
  2. dp[i]表示:以i位置元素为结尾的所有子序列中,等差数列子序列的个数。此时用这个状态表示,就只能知道等差数列子序列的个数,但并不知道具体的等差数列的值,所以这个状态表示是不行的。
    如果知道等差数列最后面两个数a,b的值,就能知道等差数列前面的数。


dp[i][j]表示以i位置以及j位置为结尾的所有子序列中,最长等差数列子序列的长度。

此时规定了i<j

  1. 状态转移方程

优化

在dp之前,将<元素,下标数组>绑定在一起,放在哈希表中

  1. 初始化

    dp表中值都初始化为0

  2. 填表顺序

    先固定倒数第一个数,再枚举倒数第二个数

  3. 返回值

    返回dp表里所有元素的和

8.2 代码

cpp 复制代码
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++)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 k:hash[a])
                    {
                        if(k<i)dp[i][j]+=dp[k][i]+1;
                    }
                }
                sum+=dp[i][j];
                
            }

        }
        
        return sum ;
    
    }
};

有问题请指出,大家一起进步!!!

相关推荐
DonciSacer8 分钟前
第一章-Rust入门
开发语言·后端·rust
我是一只鱼02239 分钟前
LeetCode算法题 (移除链表元素)Day15!!!C/C++
c++·算法·leetcode·链表
Lenyiin22 分钟前
《 C++ 点滴漫谈: 三十六 》lambda表达式
c++
西京刀客24 分钟前
golang常用库之-标准库text/template
开发语言·后端·golang
yuanManGan35 分钟前
领略算法真谛: 多源bfs
算法·宽度优先
落榜程序员36 分钟前
浅拷贝和深拷贝的区别
java·开发语言
ATaylorSu39 分钟前
C++ -- 实现日期类
c++·学习
renhl2521 小时前
C++11新特性_委托构造函数
java·前端·c++
purrrew1 小时前
【Java ee初阶】多线程(7)
java·开发语言
?!7141 小时前
数据结构之哈夫曼树
c语言·数据结构·c++·算法