【动态规划】子序列问题
个人主页 : zxctscl
专栏 【C++】、 【C语言】、 【Linux】、 【数据结构】、 【算法】
如有转载请先通知
文章目录
- 前言
- 1 ==300. 最长递增子序列(经典)==
- 3.1 分析
- 3.2 代码
- 2 376. 摆动序列
- 2.1 分析
- 2.2 代码
- 3 673. 最长递增子序列的个数
- 3.1 分析
- 3.2 代码
- 4 646. 最长数对链
- 4.1 分析
- 4.2 代码
- 5 1218. 最长定差子序列
- 5.1 分析
- 5.2 代码
- 6 873. 最长的斐波那契子序列的长度
- 6.1 分析
- 6.2 代码
- 7 1027. 最长等差数列
- 7.1 分析
- 7.2 代码
- 8 446. 等差数列划分 II - 子序列
- 8.1 分析
- 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 代码
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 代码
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 代码
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 代码
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 代码
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 代码
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 代码
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 代码
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 ;}
};
有问题请指出,大家一起进步!!!