【C/C++】873. 最长的斐波那契子序列的长度

121 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第15天,点击查看活动详情


题目链接:873. 最长的斐波那契子序列的长度

题目描述

如果序列 X1,X2,...,XnX_1, X_2, ..., X_n 满足下列条件,就说它是 斐波那契式 的:

  • n >= 3
  • 对于所有 i + 2 <= n,都有 Xi+Xi+1=Xi+2X_i + X_{i+1} = X_{i+2} 给定一个 严格递增 的正整数数组形成序列 arr,找到 arr 中最长的斐波那契式的子序列的长度。如果一个不存在,返回 0

(回想一下,子序列是从原序列 arr 中派生出来的,它从 arr 中删掉任意数量的元素(也可以不删),而不改变其余元素的顺序。例如, [3, 5, 8] 是 [3, 4, 5, 6, 7, 8] 的一个子序列)

提示:

  • 3arr.length10003 \leqslant arr.length \leqslant 1000
  • 1arr[i]<arr[i+1]1091 \leqslant arr[i] < arr[i + 1] \leqslant 10^9

示例 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 中最长的 斐波那契式子序列 的长度。

题目提示 子序列 不需要连续,连续的叫 子串

解题思路分析

斐波那契式 的定义可知,组成斐波那契式至少三个元素,且满足 Xi+Xi+1=Xi+2X_i + X_{i+1} = X_{i+2}。那么我们可以固定其中两个元素从而得到第三个元素的值,然后依次推出整个斐波那契式。

这里我们可以很容易想到暴力枚举 XiX_iXi+1X_{i+1} 从而依次求得包含 XiX_iXi+1X_{i+1} 的最长斐波那契式。

既然是递推,那换一种思维,我们可以通过已知来推未知,也就是 动态规划 的方法:

  • 定义 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 来完成。但是在暴力枚举 XiX_iXi+1X_{i+1} 时我们仅需使用 set 集合来判断 Xi+2X_{i+2} 是否存在即可。

在固定 arr[i] 枚举 arr[j] 时可以利用数组 arr 的单调性优化。由于数组 arr 是严格单调递增的,因此在确定下标 i 的情况下可以反向遍历下标 j,计算 dp[j][i] 的值,只有当 arr[j]×2>arr[i]\textit{arr}[j] \times 2 > \textit{arr}[i] 时才能找到满足 k < jarr[k]+arr[j]=arr[i]\textit{arr}[k] + \textit{arr}[j] = \textit{arr}[i] 的下标 k,当 arr[j]×2arr[i]\textit{arr}[j] \times 2 \le \textit{arr}[i] 时不需要对当前下标 i 继续遍历更小的下标 j,因为此时已经无法找到满足 k < jarr[k]+arr[j]=arr[i]\textit{arr}[k] + \textit{arr}[j] = \textit{arr}[i] 的下标 k

复杂度分析

  • 时间复杂度:时间复杂度:O(n2)O(n^2),其中 n 是数组 arr 的长度。动态规划的状态数是 O(n2)O(n^2),每个状态的计算时间都是 O(1)O(1)
  • 空间复杂度:O(n2)O(n^2),其中 n 是数组 arr 的长度。需要创建二维数组 dp,空间是 O(n2)O(n^2)

代码实现

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] 结尾最长的斐波那契式的子序列的长度。
  • 根据动态规划的定义就可以很好的写出动态规划的转移方程式,所以难点在于 状态如何定义
  • 我们还可以通过暴力枚举 XiX_iXi+1X_{i+1} 的方法,从而依次求得以 XiX_iXi+1X_{i+1} 开头的最长斐波那契式长度,但该种方法在时间复杂度上差于动态规划,不过空间上更优于动态规划,但是从时间和空间的置换比例来看,用空间来置换时间往往是更优的。
  • 测试结果:

873-2.png

873.png 可以看到利用动态规划的时间复杂度更低,但是同时也需要更多的内存消耗,这也就是空间置换了时间。并且花费的空间置换时间性价比较高,所以更推荐使用动态规划的方法。

结束语

坚持运动的人生,真的大不相同。运动不仅能锻炼体魄,还能帮助我们缓解压力、释放情绪。当压力随着汗水挥发,我们收获的就是一个更健康、更快乐的自己。新的一天,加油!