[dp11_最长子序列(不连续)] 最长数对链 | 最长定差子序列 最长的斐波那契子序列的长度
目录
1.最长数对链
题解
2.最长定差子序列
题解
3.最长的斐波那契子序列的长度
题解
- 将判断过的状态用 dp 存起来
- 这样就可以来避免重复的判断
其实DP的本质就是一种空间换时间
- 一些东西,当我们理解了我到底在做什么,为什么要这么做之后就很容易的,可以发现如何初始化
⭕1.最长数对链
链接: 646. 最长数对链
给你一个由 n
个数对组成的数对数组 pairs
,其中 pairs[i] = [lefti, righti]
且 lefti < righti
。
现在,我们定义一种 跟随 关系,当且仅当 b < c
时,数对 p2 = [c, d]
才可以跟在 p1 = [a, b]
后面。我们用这种形式来构造 数对链 。
找出并返回能够形成的 最长数对链的长度 。
你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。
示例 1:
输入:pairs = [[1,2], [2,3], [3,4]]
输出:2
解释:最长的数对链是 [1,2] -> [3,4] 。
示例 2:
输入:pairs = [[1,2],[7,8],[4,5]]
输出:3
解释:最长的数对链是 [1,2] -> [4,5] -> [7,8] 。
- 给你一个由 n 个数对组成的数对数组 pairs
- 其中 pairs[i] = [lefti, righti] 且 lefti < righti 。
现在,我们定义一种 跟随 关系,当且仅当 b < c 时,数对 p2 = [c, d] 才可以跟在 p1 = [a, b] 后面。
- 我们用这种形式来构造 数对链 。
- 也就是说两个数对之间必须构成严格递增关系,才能构造数对链。
找出并返回能够形成的 最长数对链的长度 。
你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。
这道题意很简单,但是和我们前面做的题有不一样的地方是
- 我们之前做动规的题必须要保证填表是有序的,也就是说填当前位置的时候
- 之前所依赖的状态必须是已经计算过的了。
- 比如说子数组和子序列,当我们以i位置元素为结尾的时候,然后都是往前去选。
但是我们这道题就如第二个例子,选[7,8]为结尾研究数对链的时候,它左边和右边都可以选,这样就不能保证填表顺序,填 i 位置的时候就不能更新dp[i]的值。
- 所以,动规之前,要预处理一下:按照第一个元素排序即可(升序)
- 虽然不能保证 i 位置元素能连在前面元素的后面,但是一定能保证连不到后面,数对链内部严格递增
题解
接下来和最长递增子序列长度是一模一样的
1.状态表示
- dp[i] 表示,以 i 位置元素为结尾的所有数对链,最长的数对链的长度
2.状态转移方程
3.初始化
- dp表中元素都初始化为1,这样就不要考虑长度为1的情况了。
4.填表顺序
- 从左往右
5.返回值
- dp 表里面的最大值
class Solution {
public:
int findLongestChain(vector<vector<int>>& pairs)
{
sort(pairs.begin(),pairs.end(),[](vector<int>& x,vector<int>&y){return x[0] < y[0];});
int n=pairs.size();
vector<int> dp(n,1);
for(int i=1;i<n;i++)
{
for(int j=0;j<=i-1;j++)
{
if(pairs[j][1]<pairs[i][0])
{
dp[i]=max(dp[j]+1,dp[i]);
//只关心 长度 这一状态
}
}
}
int ret=0;
for(int i=0;i<n;i++)
{
ret=max(ret,dp[i]);
}
return ret;
}
};
- 所有的里面找出最长,划分为了以某一个位置为结尾的,最长。
- 我们也可以这么理解,结果所得到的最长,他肯定是以某一个位置为结尾的
- 相当于就是降低了一定程度的时间复杂度。因为这样我们在找某一个位置为结尾的最长的时候,可以把它和上一个位置找最长之间建立某种联系,就不用一直对某一个区域进行反复的判断,而是通过它们之间的关系,可以直接推论得出结果。例如就可以把三层循环降低为两层循环的时间复杂度
lambda :[ ]( 传参 ){ 函数 }
- [ ](vector<int>& x,vector<int>&y) {return x[0] < y[0];}
2.最长定差子序列
链接: 1218. 最长定差子序列
给你一个整数数组 arr
和一个整数 difference
,请你找出并返回 arr
中最长等差子序列的长度,该子序列中相邻元素之间的差等于 difference
。
子序列 是指在不改变其余元素顺序的情况下,通过删除一些元素或不删除任何元素而从 arr
派生出来的序列。
示例 1:
输入:arr = [1,2,3,4], difference = 1
输出:4
解释:最长的等差子序列是 [1,2,3,4]。
示例 2:
输入:arr = [1,3,5,7], difference = 1
输出:1
解释:最长的等差子序列是任意单个元素。
示例 3:
输入:arr = [1,5,7,8,5,3,4,2,1], difference = -2
输出:4
解释:最长的等差子序列是 [7,5,3,1]。
给你一个整数数组 arr 和一个整数 difference
- 请你找出并返回 arr 中最长等差子序列的长度
- 该子序列中相邻元素之间的差等于 difference 。
题解
1.状态表示
- 经验 + 题目要求
- dp[i] 表示:以 i 位置的元素为结尾的所有子序列中,最长的等差子序列的长度
2.状态转移方程
- 设 i 位置里面元素为a,如果以 i 位置元素为结尾研究最长等差子序列的长度,其实 i 位置前面那个元素已经确定了
- 这道题和最长递增子序列是有区别的,那道题 i 位置前面的元素是要从前往后遍历一遍的来找倒数第二个元素在哪里。
- 这道题已经告诉我们相邻两个元素的差是diff,我只要知道最后一个元素就能推出倒数第二个元素。
- 设倒数第二个元素为b。 a - b = diff —> b = diff - a。
根据 b 存不存在可以分两个情况:
- b不存在,a本身就是
- b存在,因为是乱序的,前面可能有很多元素等于b
- 其实我们只需要考虑最后一个b元素的位置就可以了。因为我们想要的是以b元素为结尾的最长等差子序列的长度
- 最后一个b元素所在位置dp[j1]里面的值至少大于等于前面以b元素为结尾最长等差子序列的长度。
- 然后加上 i 位置的元素。
这里想到的话,就可以相对于之前的,两层for循环查找最大值,来减少了一层循环。
这里有个优化:
虽然我们是可以从后往前遍历找到最后一个b元素所在的位置的
但是我们可以这也做,可以之间把 b 元素 和它对应的 dp[i] 放在hash表。就不用去遍历了,可以O(1)的时间复杂度就找到。
- 将 元素 + dp[j] 的值,绑定放进哈希表中
甚至我们还可以将 a 元素(arr[i]) 和 dp[i] 绑定放进哈希表。
- 直接在哈希表中,做动态规划
3.初始化
- 我们只需要将以第0个位置元素为结尾子序列,最长的等差子序列的长度初始化一下就行了,反正没得选,就是自己本身。
- dp[arr[0]] = 1
4.填表顺序
- 从左往右
5.返回值
- 我们要返回的是最长的等差子序列长度,但是这个长度可能在dp表里任意一个位置
- 因此返回 dp 表里面的最大值
class Solution {
public:
int longestSubsequence(vector<int>& arr, int difference)
{
int n=arr.size();
unordered_map<int,int> dp;
dp[arr[0]]=1;
for(int i=1;i<n;i++)
{
int cur=arr[i]-difference;
dp[arr[i]]=max(dp[cur]+1,dp[arr[i]]);
//交给 hash 去实现自动覆盖
//来确保拿到的cur就是最后一次出现的
//因为是从前往后的填表,所以我们可以确保
}
int ret=0;
for(int i=0;i<n;i++)
{
ret=max(ret,dp[arr[i]]);
}
return ret;
}
};
- 相当于是将多层for循环进行了一个剥离。
- 比如说我先找到以每一个位置为结尾的最长长度
- 再遍历这个DP表,就可以得到所有的最长长度
设置状态的参照物,就是以某一个位置为结尾这一参照
- 因为这样的话下一个状态和上一个状态之间就存在某种联系,可以减少很多的重复计算
- 可以理解为和快速幂有一点类似的一种思想吧
3.最长的斐波那契子序列的长度
链接: 873. 最长的斐波那契子序列的长度
如果序列 X_1, X_2, ..., X_n
满足下列条件,就说它是 斐波那契式 的:
n >= 3
- 对于所有
i + 2 <= n
,都有X_i + X_{i+1} = X_{i+2}
给定一个严格递增的正整数数组形成序列 arr ,找到 arr 中最长的斐波那契式的子序列的长度。如果一个不存在,返回 0 。
(回想一下,子序列是从原序列 arr 中派生出来的,它从 arr 中删掉任意数量的元素(也可以不删),而不改变其余元素的顺序。例如, [3, 5, 8]
是 [3, 4, 5, 6, 7, 8]
的一个子序列)
示例 1:
输入: arr = [1,2,3,4,5,6,7,8]
输出: 5
解释: 最长的斐波那契式子序列为 [1,2,3,5,8] 。
示例 2:
输入: arr = [1,3,7,11,12,14,18]
输出: 3
解释: 最长的斐波那契式子序列有 [1,11,12]、[3,11,14] 以及 [7,11,18] 。
满足斐波那契数列条件
- 元素长度大于等于3
- 还有从第三个元素开始,每个元素都等于前两个元素的和
如果序列满足这两个条件我们就称之为斐波那契数列。
- 注意这道题给的是严格递增,不仅是递增的而且元素还是不重复的。填表的时候有优化的作用。
题解
1.状态表示
- 经验 + 题目要求
- dp[i] 表示:以 i 位置元素为结尾所有子序列中,最长斐波那契数列子序列的长度
我们尝试用这个状态表示来看看能不能推导出来状态转移方程。
- 我们是 i 位置来划分问题的,在 0 ~ i -1 任选一个位置 j ,通过 j 位置的 dp[j] ,来更新出 i 位置的dp[i],这是之前做子序列的方法。
但是这道题就不行了。
- 我们的状态表示只是表示出来最长斐波那契数列子序列的长度,但是并不知道具体的斐波那契序列。
- 其实如果我们知道最后两个位置的值nums[j],nums[i],我们是能推导出来在前面一个位置的。
- 但是dp[j]根本不知道这个斐波那契数列是什么样。
- 所有我们不能直接用dp[j]的值去更新dp[i]的值。所以上面的状态表示不对!
通过刚才的分析我们发现
- 如果在一个斐波那契数列知道最后两个元素,其实是可以把前面所有的元素都推出来的。
- … a-(b-a),b-a,b,a。
- 既然单独一个位置为结尾推不出来斐波那契子序列长什么样子,但是如果能用两个元素为结尾就能推出斐波那契子序列长什么样子。
- 一维解决不了问题,在扩一维。
dp[i][j] 表示: 以 i 位置以及 j 位置的元素为结尾的所有子序列中,最长斐波那契子序列长度。 (i 位置 < j 位置)
2.状态转移方程
- 如果我们以最后两个位置为结尾,其实我们是可以直接知道倒数第三个数
- 设倒数第三个数为 a, a = c - b,设 a 的下标为 k
根据a可以分下面三种情况:
- a不存在
- a存在且b<a<c
- a存在且a<b
a不存在,根本构不成斐波那契子序列,dp[i][j]本应该是0的,但是dp[i][j] 表示以 i,j位置为结尾,至少有两个元素。所以给2。
- 如果dp里面都是2说明,没有都没有构成斐波那契子序列,最后直接返回0就可以了。
a存在且b<a<c,a虽然存在,但是在bc中间,不符合斐波那契数列,所以也是给2。
a存在且a<b,我们要的是最长斐波那契子序列长度
- 先把 k 位置元素拿出来,在把 i 位置元素拿出来,先找到以着两个位置元素为结尾的最长斐波那契子序列
- 后面在加上一个j位置元素就可以了。
- 而以ki位置元素为结尾最长斐波那契子序列正好在dp[k][i]中,然后+1
- 优化:找a元素所在位置比较花时间,因此我们将数组中所有元素与它们的下标绑定,存在哈希表中。
- 依据:(数组里面的元素是严格递增,不会存在重复元素)
3.初始化
- 表里面所有的值都初始化为2,就不用考虑前面两种情况了。
但是细心的会发现不能把表里面的值都初始化为2 - dp[0][0] 相当于里面就一个元素,里面的值就是1。
一些东西,当我们理解了为什么要这么做之后就很容易的,可以发现如何初始化
- 其实我们在状态表示就已经规定好了 i < j。对于这个二维dp表,我们只会用到上半部分,
4.填表顺序
- 从上到下,从左到右
5.返回值
- dp表里面的最大值 ret , ret < 3 ? 0 : ret
class Solution {
public:
int lenLongestFibSubseq(vector<int>& arr)
{
int n=arr.size();
if(n<3) return 0;
vector<vector<int>> dp(n,vector<int>(n,2));
unordered_map<int,int> hash;//arr[i] i
for(int i=0;i<n;i++)
{
hash[arr[i]]=i;
}
dp[0][0]=1;
int ret=0;
//以i j 结尾的长度
for(int i=1;i<n-1;i++)
{
for(int j=i+1;j<n;j++)
{
int a=arr[j]-arr[i];
if(hash.count(a) && hash[a]<i)
{
dp[i][j]=dp[hash[a]][i]+1;
}
ret=max(ret,dp[i][j]);
}
}
//小于三的话就说明不满足,直接返回零
return ret<3?0:ret;
}
};
- 实现了剥离去重复操作,只有找到每一步的最大即可~
上一篇当中的 dp,涉及到斐波纳契是利用 dp 求斐波纳契的值
- dp[i] 为第I个斐波那契数的值
- dp[i]=dp[i-1]+dp[i-2]
- 在这个题目当中,斐波纳契只是其中找下标的一环,实际上求的是子序列长度问题