个人主页 : 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 分析
-
状态表示
dp[i]表示:以i位置为结尾的所有子序列中,最长递增子序列的长度。
-
状态转移方程
可以分两类,一类是就以i为子序列;
第二类是跟前面元素一起构成子序列,但此时必须是前面的元素小于i,此时条件下再找最长子序列,要想和i组成最长,j也是最长子序列就是dp[j],再加上1就是最大的子序列。
-
初始化
把dp表里面所有值初始化为1,就不用考虑为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

-
状态表示
dp[i]表示:以i位置为结尾的所有的子序列中,最长的摆动序列的长度。
最后一个位置可以分为是下降位置,也开始是上升位置:
此时就细分为两个:
g[i]表示:以i位置为结尾的所有的子序列中,最后一个位置呈现"下降"趋势的最长摆动序列的长度。
f[i]表示:以i位置为结尾的所有的子序列中,最后一个位置呈现"上升"趋势的最长摆动序列的长度。
-
状态转移方程
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])。
-
初始化
f表和g表全部初始化为1,就不用考虑长度为1的情况。
-
填表顺序
从左往右,两个表一起填
-
返回值
两个表里面的最大值
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 分析
- 状态表示
dp[i]表示:以i位置为结尾的所有子序列中,最长递增子序列的个数。
但是此时连最长的子序列多长都不知道,这个状态表示是不够的。
len[i]表示:以i位置为结尾的所有子序列中,最长递增子序列的"长度"。
count[i]表示:以i位置为结尾的所有子序列中,最长递增子序列的"个数"。
- 状态转移方程
第一步以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
-
填表顺序
从左往右
-
返回值
贪心策略

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位置左边。
-
状态表示
dp[i]表示以i位置为结尾的所有的数对链中,最长的数对链的长度
-
状态转移方程
如果就以i位置为结尾构成一个数对链,长度就是1,;
如果i位置和前面位置一起构成数对链,就得满足前面i-1位置pair中右边元素dp[j][1]小于以i位置为结尾pair中左边元素dp[i][0],这时长度就是dp[j]+1,如果取里面最长的数对链:
-
初始化
把他们初始化为最差的长度,就都初始化为1
-
填表顺序
从左往右
-
返回值
返回里面的最大值
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 分析
-
状态表示
dp[i]表示以i位置的元素为结尾的所以子序列中,最长的等差子序列长度。
-
状态转移方程
如果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]。
直接在哈希表中做动态规划
-
初始化
以0位置的元素为结尾的所以子序列中,长度就是1。
hash[arr[0]]=1 -
填表顺序
从左往右
-
返回值
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 分析
-
状态表示
dp[i]表示:以i位置元素为结尾的所有子序列中,最长斐波那契子序列的长度。此时用这个状态表示,就只能知道斐波那契子序列的长度,但并不知道具体的斐波那契数列,所以这个状态表示是不行的。
如果知道斐波那契数列最后面两个数a,b的值,就能知道斐波那契数列前面的数。
dp[i][j]表示以i位置以及j位置为结尾的所有子序列中,最长斐波那契子序列的长度。此时规定了i<j
-
状态转移方程
假设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
优化:
在做动态规划之前,先把值和它们下标绑定,就可以先存到哈希表中,就能直接找到下标
-
初始化
把表里所有的值都初始为2
-
填表顺序
从上往下
-
返回值
返回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 分析
同上面一题类似
- 状态表示
dp[i]表示:以i位置元素为结尾的所有子序列中,最长等差数列子序列的长度。此时用这个状态表示,就只能知道等差数列子序列的长度,但并不知道具体的等差数列的值,所以这个状态表示是不行的。
如果知道等差数列最后面两个数a,b的值,就能知道等差数列前面的数。

dp[i][j]表示以i位置以及j位置为结尾的所有子序列中,最长等差数列子序列的长度。
此时规定了i<j
- 状态转移方程
分三种情况,(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位置的值放入哈希表中即可
-
初始化
dp表里面初始化为2
-
填表顺序
有两种填表顺序:
(1)先固定最后一个数j,再枚举倒数第二个数,此时i是一直移动的
(2)先固定倒数第二个数,再枚举最后一个数,这时候i和j同时向后移动一位
-
返回值
返回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 分析
同上面一题类似,只是这里要找到是等差数列的个数
- 状态表示
- dp[i]表示:以i位置元素为结尾的所有子序列中,等差数列子序列的个数。此时用这个状态表示,就只能知道等差数列子序列的个数,但并不知道具体的等差数列的值,所以这个状态表示是不行的。
如果知道等差数列最后面两个数a,b的值,就能知道等差数列前面的数。
dp[i][j]表示以i位置以及j位置为结尾的所有子序列中,最长等差数列子序列的长度。
此时规定了i<j
- 状态转移方程

优化
在dp之前,将<元素,下标数组>绑定在一起,放在哈希表中
-
初始化
dp表中值都初始化为0
-
填表顺序
先固定倒数第一个数,再枚举倒数第二个数
-
返回值
返回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 ;
}
};
有问题请指出,大家一起进步!!!