携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第15天,点击查看活动详情
题目链接:873. 最长的斐波那契子序列的长度
题目描述
如果序列 满足下列条件,就说它是 斐波那契式 的:
n >= 3- 对于所有
i + 2 <= n,都有 给定一个 严格递增 的正整数数组形成序列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] 。
整理题意
题目给定一个 严格递增 的正整数数组 arr,要求返回 arr 中最长的 斐波那契式 的 子序列 的长度。
题目提示 子序列 不需要连续,连续的叫 子串
解题思路分析
由 斐波那契式 的定义可知,组成斐波那契式至少三个元素,且满足 。那么我们可以固定其中两个元素从而得到第三个元素的值,然后依次推出整个斐波那契式。
这里我们可以很容易想到暴力枚举 和 从而依次求得包含 和 的最长斐波那契式。
既然是递推,那换一种思维,我们可以通过已知来推未知,也就是 动态规划 的方法:
- 定义
dp[j][i]:表示以arr[j]和arr[i]结尾最长的斐波那契式的子序列的长度。 - 状态转移:那么我们只需通过
arr[j]和arr[i]求得数组中是否存在上一个arr[k],通过已知的dp[k][j]来推未知的dp[j][i]即可,那我们可以得到转移方程为:dp[j][i] = dp[k][j] + 1,另外,如果存在这样的k,那么斐波那契式的长度至少为3,所以完整的转移方程为:dp[j][i] = max(3, dp[k][j] + 1);
具体实现
因为我们可以由枚举的 arr[j] 和 arr[i] 直接得到 arr[k] 的值,我们只需判断 arr[k] 是否存在于数组 arr 中并得到这个下标 k,这里可以预处理建立 arr 数组的所有 逆映射,那我们在判断 arr[k] 是否存在于数组 arr 中时就无需再通过遍历或者二分的方法来判断,可以直接通过映射关系来判断,并且可以同时得到 arr[k] 的下标 k,提高效率。
所谓的 逆映射 就是值映射下标,原本在数组中为下标映射值,现在为了快速判断数组中是否存在某个值时,建立逆映射来解决,不仅可以快速判断是否存在,还能直接得到该值所在的下标位置。
注意: 这里因为不仅需要判断
arr[k]是否存在,还要得到k的值,所以不能用set集合来完成,只能用映射map来完成。但是在暴力枚举 和 时我们仅需使用set集合来判断 是否存在即可。
在固定 arr[i] 枚举 arr[j] 时可以利用数组 arr 的单调性优化。由于数组 arr 是严格单调递增的,因此在确定下标 i 的情况下可以反向遍历下标 j,计算 dp[j][i] 的值,只有当 时才能找到满足 k < j 且 的下标 k,当 时不需要对当前下标 i 继续遍历更小的下标 j,因为此时已经无法找到满足 k < j 且 的下标 k。
复杂度分析
- 时间复杂度:时间复杂度:,其中
n是数组arr的长度。动态规划的状态数是 ,每个状态的计算时间都是 。 - 空间复杂度:,其中
n是数组arr的长度。需要创建二维数组dp,空间是 。
代码实现
class Solution {
public:
int lenLongestFibSubseq(vector<int>& arr) {
int n = arr.size();
//逆映射,方便通过 arr[i] 和 arr[j] 寻找 arr[k]
unordered_map<int, int> mp;
mp.clear();
for(int i = 0; i < n; i++) mp[arr[i]] = i;
//定义 dp[j][i] 表示以 arr[j] 和 arr[i] 结尾构成的最长斐波那契式的子序列
int dp[n][n];
memset(dp, 0, sizeof(dp));
int ans = 0;
for(int i = 0; i < n; i++){
//由于arr为递增序列,所以当 arr[j] * 2 <= arr[i] 时就找不到 arr[k] + arr[j] = arr[i] 了
for(int j = i - 1; j >= 0 && arr[j] * 2 > arr[i]; j--){
int k = -1;
//是否存在 arr[k] + arr[j] == arr[i]
if(mp.count(arr[i] - arr[j])){
k = mp[arr[i] - arr[j]];
dp[j][i] = max(3, dp[k][j] + 1);
}
ans = max(ans, dp[j][i]);
}
}
return ans;
}
};
总结
- 该题核心在于 定义动态规划的表达式
dp[j][i]:表示以arr[j]和arr[i]结尾最长的斐波那契式的子序列的长度。 - 根据动态规划的定义就可以很好的写出动态规划的转移方程式,所以难点在于 状态如何定义。
- 我们还可以通过暴力枚举 和 的方法,从而依次求得以 和 开头的最长斐波那契式长度,但该种方法在时间复杂度上差于动态规划,不过空间上更优于动态规划,但是从时间和空间的置换比例来看,用空间来置换时间往往是更优的。
- 测试结果:
可以看到利用动态规划的时间复杂度更低,但是同时也需要更多的内存消耗,这也就是空间置换了时间。并且花费的空间置换时间性价比较高,所以更推荐使用动态规划的方法。
结束语
坚持运动的人生,真的大不相同。运动不仅能锻炼体魄,还能帮助我们缓解压力、释放情绪。当压力随着汗水挥发,我们收获的就是一个更健康、更快乐的自己。新的一天,加油!