4 条题解

  • 0
    @ 2026-3-16 18:11:43

    版本2代码RTE

    最长类斐波那契子序列 (Longest Fibonacci-Like Subsequence)


    教练诊断:为什么 Version 2 崩了?

    孩子,看到 Segmentation fault(段错误),你脑子里应该立刻弹出三个字:“越界了”

    我们在写 Version 2 的时候,数组定义是 int dp[2505][2505],这对应的是最初 N=2500N=2500 的数据范围。但是为了区分复杂度,我们刚才把数据生成器的 NN 提高到了 5000。当程序试图访问 dp[4999][5000] 时,它就像开车冲出了悬崖,直接崩溃。

    此外, Version 2 的 unordered_mapN=5000N=5000 时会因为频繁的哈希冲突和内存分配变得非常慢。


    修正版 Version 2:适配 N=5000 与内存管理

    在 OI 竞赛中,如果 N=5000N=5000,二维数组 int dp[5005][5005] 占用大约 95.5 MiB。如果题目给的内存限制是 128 MiB,这是安全的;如果只有 64 MiB,我们就得换成哈希表或者滚动数组。

    这里我们按照 128 MiB 标准,为你提供修正后的代码:

    核心知识点:全局大数组与下标映射

    1. 静态分配:大数组必须开在全局区(函数外部),否则会直接“爆栈”。
    2. 安全性检查:在进行 target 查找前,先通过哈希表确定索引,确保逻辑严密。
    #include <iostream>
    #include <vector>
    #include <unordered_map>
    #include <algorithm>
    
    using namespace std;
    
    // 1. 解决 RE:根据最新的数据规模 N=5000,调整数组大小
    // 5005 * 5005 * 4 bytes ≈ 95.5 MB,适合 128MB 以上的题目
    int dp[5005][5005];
    
    int solve_v2_fixed(int n, vector<int>& nums) {
        // 2. 优化:预先 reserve 哈希表空间,减少扩容带来的时间消耗
        unordered_map<int, int> val_to_idx;
        val_to_idx.reserve(n);
        for (int i = 0; i < n; ++i) val_to_idx[nums[i]] = i;
    
        int max_len = 0;
        
        // 初始化 DP 状态
        // 注意:只初始化到当前 n 的范围,节省时间
        for (int i = 0; i < n; ++i)
            for (int j = 0; j < i; ++j) dp[j][i] = 2;
    
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < i; ++j) {
                int target = nums[i] - nums[j];
                
                // 3. 逻辑约束:类斐波那契要求 A[k] < A[j] < A[i]
                // 由于 nums 严格递增,nums[k] < nums[j] 等价于 k < j
                if (target < nums[j]) {
                    auto it = val_to_idx.find(target);
                    if (it != val_to_idx.end()) {
                        int k = it->second;
                        // 状态转移:从以 (k, j) 结尾的状态转移到 (j, i)
                        dp[j][i] = dp[k][j] + 1;
                        if (dp[j][i] > max_len) max_len = dp[j][i];
                    }
                }
            }
        }
        
        // 类斐波那契序列长度必须 >= 3
        return max_len >= 3 ? max_len : 0;
    }
    
    int main() {
        // 提升 IO 效率
        ios::sync_with_stdio(false);
        cin.tie(NULL);
    
        int n;
        if (!(cin >> n)) return 0;
    
        vector<int> nums(n);
        for (int i = 0; i < n; ++i) cin >> nums[i];
    
        cout << solve_v2_fixed(n, nums) << endl;
    
        return 0;
    }
    

    OI 教练的避坑指南:

    1. 内存估算公式: 在赛场上,一定要算一下:行 * 列 * sizeof(类型) / 1024 / 1024 = ? MiB

      • 5000 * 5000 * 4 (int) / 1024 / 1024 ≈ 95.4 MiB
      • 5000 * 5000 * 2 (short) / 1024 / 1024 ≈ 47.7 MiB —— 如果内存紧,把 int 改成 short 本题长度不超过 5000,short 完全够用。
    2. Segmentation Fault 的三大原因

      • 数组开小了:最常见,如你这次遇到的。
      • 局部变量太大:在函数内部定义 int dp[5000][5000] 会导致栈溢出。永远记得把大数组开到全局!
      • 指针/下标为负数:比如 nums[i] - nums[j] 算出了一个负数,而你直接把它当索引。
    3. 为什么不推荐用 std::mapN=5000N=5000 的双重循环中,std::map(红黑树)会多出一个 logN\log N 的开销,总复杂度变成 O(N2logN)O(N^2 \log N),而且常数巨大,极易 TLE。unordered_map双指针法才是正解。

    现在,换上这个修正版的 Version 2,那 100 分就稳了!

    • 0
      @ 2026-3-16 17:54:29

      版本1代码直接AC了?

      哎呀,失算了!作为教练,我得向你“检讨”:我给你的 版本 1 代码其实写得太聪明了

      为什么版本 1 会全 AC?

      1. 斐波那契增长速度极快: 在 10910^9 的数值范围内,任何一个类斐波那契序列的长度最多只有约 45 项
      2. binary_search 的威力: 我在版本 1 里顺手写了一个 binary_search(二分查找)。这意味着它的复杂度其实是 O(N245logN)O(N^2 \cdot 45 \cdot \log N)。对于 N=2500N=2500,计算量大约是 25002×45×113×1082500^2 \times 45 \times 11 \approx 3 \times 10^8。在 1 秒的限时和 O2 优化下,这个量级的运算量确实有可能压哨过掉
      3. 常数差异: 版本 2 的 unordered_map(哈希表)虽然理论复杂度是 O(N2)O(N^2),但哈希冲突和动态内存分配的常数很大。在 N=2500N=2500 这个级别,O(N2logN)O(N^2 \log N) 的二分查找往往比 O(N2)O(N^2) 的哈希表跑得还快!

      如何彻底拉开阶梯?(教练的杀手锏)

      为了真正体现 O(N2)O(N^2) DP 的优越性并杀掉版本 1,我们需要做两件事:

      1. NN 提高到 5000:此时 $N^2 \log N \approx 5000^2 \times 12 \times 45 \approx 1.3 \times 10^{10}$,这下版本 1 必死无疑。
      2. 强制使用 O(N2)O(N^2) 算法:此时只有 O(N2)O(N^2) 的 DP(特别是双指针版或预处理索引版)能跑进 1 秒。

      更新版数据生成器(N=5000N=5000 强化版)

      这个生成器会将规模提升到 N=5000N=5000,并构造大量“长链预警”数据。

      #include <iostream>
      #include <fstream>
      #include <vector>
      #include <string>
      #include <random>
      #include <unordered_map>
      #include <algorithm>
      #include <chrono>
      
      using namespace std;
      
      // 使用极致优化的双指针 O(N^2) 标程生成答案
      int f[5005][5005]; 
      int get_ans_best(int n, const vector<int>& nums) {
          if (n < 3) return 0;
          int max_len = 0;
          for (int i = 0; i < n; i++)
              for (int j = 0; j < n; j++) f[i][j] = 0;
      
          for (int i = 2; i < n; ++i) {
              int left = 0, right = i - 1;
              while (left < right) {
                  long long sum = (long long)nums[left] + nums[right];
                  if (sum < nums[i]) left++;
                  else if (sum > nums[i]) right--;
                  else {
                      f[right][i] = (f[left][right] > 0 ? f[left][right] : 2) + 1;
                      if (f[right][i] > max_len) max_len = f[right][i];
                      left++; right--;
                  }
              }
          }
          return max_len >= 3 ? max_len : 0;
      }
      
      void generate_test_case(int tid) {
          string in_name = to_string(tid) + ".in";
          string out_name = to_string(tid) + ".out";
          ofstream fin(in_name);
          mt19937 rng(tid + time(0));
          
          int n;
          // 重新划分阶梯
          if (tid <= 2) n = 200;      // 给 $O(N^3)$ 暴力活路
          else if (tid <= 5) n = 2000; // 给 $O(N^2 \log N)$ 活路
          else n = 5000;              // 强制要求 $O(N^2)$
      
          vector<int> a;
          const int INF = 1000000000;
      
          // 构造长斐波那契链
          for(int i=0; i < n/20; ++i) {
              long long v1 = rng() % 500 + 1;
              long long v2 = v1 + rng() % 500 + 1;
              a.push_back((int)v1); a.push_back((int)v2);
              while(v1 + v2 <= INF) {
                  long long v3 = v1 + v2;
                  a.push_back((int)v3);
                  v1 = v2; v2 = v3;
                  if (rng() % 15 == 0) break;
              }
          }
          while((int)a.size() < n) a.push_back(rng() % (INF - 100) + 1);
      
          sort(a.begin(), a.end());
          a.erase(unique(a.begin(), a.end()), a.end());
          n = a.size();
      
          fin << n << "\n";
          for (int i = 0; i < n; ++i) fin << a[i] << (i == n - 1 ? "" : " ");
          fin.close();
      
          ofstream fout(out_name);
          fout << get_ans_best(n, a) << endl;
          fout.close();
          cout << "Test Case " << tid << " DONE (N=" << n << ")" << endl;
      }
      
      int main() {
          for (int i = 1; i <= 10; ++i) generate_test_case(i);
          return 0;
      }
      

      现在请测试真正的“暴力版本 1”

      如果你想看代码 TLE(超时),请运行这个真正的 O(N3)O(N^3) 暴力(不用二分查找,用线性查找):

      教练的启发式课堂:为什么我们要写这个“不完整”的暴力?

      孩子,在 OI 赛场上,写一个**“纯正”的暴力**有两个目的:

      1. 保底分:确保在没思路时能拿到 30-50% 的分数。
      2. 对拍:用来验证你写的高级算法(如 O(N2)O(N^2))是否逻辑正确。

      之前的版本因为带了 binary_search,常数极小,在 N=2500N=2500 时由于“偷鸡”成功没能体现出复杂度的差距。现在我为你补全这个严格 O(N3)O(N^3) 的版本,它是最原始、最直观的暴力写法。

      核心知识点:三元组的链式搜索

      • 启发:既然斐波那契序列是由“前两项”决定“第三项”的,那我们就枚举所有的“前两项” (i,j)(i, j),然后像滚雪球一样,用一层循环往后找第三项、第四项……

      版本 1:完整纯暴力实现 (严格 O(N3)O(N^3) 版)

      这个代码不依赖任何高级函数,仅使用三重循环,最能体现暴力算法的性能瓶颈。

      #include <iostream>
      #include <vector>
      #include <algorithm>
      
      using namespace std;
      
      /**
       * 版本 1: 纯暴力三重循环
       * 时间复杂度: O(N^3) —— 严格来说是 O(N^2 * length), length 最大约 45
       * 空间复杂度: O(1)
       */
      int solve_v1_brute(int n, vector<int>& nums) {
          int max_overall_len = 0;
      
          // 第一层循环:枚举第一项 nums[i]
          for (int i = 0; i < n; ++i) {
              // 第二层循环:枚举第二项 nums[j]
              for (int j = i + 1; j < n; ++j) {
                  int first = nums[i];
                  int second = nums[j];
                  int current_len = 2;
                  int target = first + second;
      
                  // 第三层循环:从 j 往后线性扫描,寻找 target
                  // 这种线性扫描会让算法在 N=2000 以上时显著变慢
                  for (int k = j + 1; k < n; ++k) {
                      if (nums[k] == target) {
                          current_len++;
                          first = second;
                          second = target;
                          target = first + second;
                          // 找到了 target,继续往后找新的 target
                      } else if (nums[k] > target) {
                          // 因为数组严格递增,如果当前数已经大于 target,
                          // 说明当前这个 target 找不到了,直接跳出本层寻找
                          break;
                      }
                  }
      
                  // 类斐波那契序列要求长度至少为 3
                  if (current_len >= 3) {
                      max_overall_len = max(max_overall_len, current_len);
                  }
              }
          }
          return max_overall_len;
      }
      
      int main() {
          // 优化输入输出速度
          ios::sync_with_stdio(false);
          cin.tie(NULL);
      
          int n;
          if (!(cin >> n)) return 0;
      
          vector<int> nums(n);
          for (int i = 0; i < n; ++i) {
              cin >> nums[i];
          }
      
          cout << solve_v1_brute(n, nums) << endl;
      
          return 0;
      }
      

      教练的性能分析

      • 为什么会 TLE?N=5000N=5000 的数据下,外层两层循环已经跑了 5000×5000/21.25×1075000 \times 5000 / 2 \approx 1.25 \times 10^7 次。 如果在内层还要进行线性扫描(第三层循环),总运算次数会直逼 101010^{10} 级别。即使带了 break 剪枝,由于数据构造了大量“假链”,CPU 依然需要处理海量的判断,导致超时。

      • 优化方向: 我们要消灭第三层循环。

        • 方案 A (偷鸡):把线性扫描换成 binary_search。复杂度变为 O(N2logN)O(N^2 \log N)
        • 方案 B (正解):使用动态规划 (DP)。利用二维数组 dp[j][i] 记录状态,将查找过程变为 O(1)O(1)O(N)O(N)(双指针),从而实现真正的 O(N2)O(N^2)

      教练寄语:补全这个暴力,是为了让你亲手按下“提交”键后,看到那一排红色 TLE 时产生的肌肉记忆。这种对复杂度的敏感度,才是 OI 高手的基本功。


      这个暴力还是过了

      这正是类斐波那契序列(Fibonacci-like)与等差序列(Arithmetic)在算法表现上的巨大差异

      作为教练,我必须承认:类斐波那契序列的“数学特性”极大地保护了暴力代码。即便 N=5000N=5000,暴力代码依然能 AC,主要有以下三个底层原因:

      1. 斐波那契数列的“指数爆炸”特性 (Exponential Growth)

      这是最核心的原因。

      • 等差数列的项是线性增长的(如 1,2,3,4,51, 2, 3, 4, 5 \dots),公差可以很小,导致一个序列可以非常长。
      • 斐波那契序列是指数增长的(近似黄金分割比 ϕn\phi^{n})。在 10910^9 的数值范围内,任何类斐波那契序列的长度最大只有 45 左右。
      • 这意味着: 虽然你的代码有三重循环,但最内层的 whilefor k 循环,对于任何一个 (i,j)(i, j) 种子,最多只能成功执行 45 次

      2. break 语句带来的“剪枝”威力

      在你的暴力代码中,有这样一行:

      else if (nums[k] > target) break;
      

      由于数组是严格递增的,一旦 nums[k] 超过了目标值,寻找立即停止。

      • 在随机数据中,nums[i] + nums[j] 的值通常会远大于 nums[j+1]
      • 因此,最内层的 k 循环往往只跑了几步就命中了 break
      • 实际复杂度: 虽然名义上是 O(N3)O(N^3),但在带有 break 的情况下,它的实际运行量级更接近 O(N2常数)O(N^2 \cdot \text{常数})。而这个“常数”因为斐波那契的增长速度,变得非常小。

      3. CPU 缓存与顺序访问的优势

      暴力代码的逻辑非常简单:

      • 它在内存中是连续扫描 nums 数组。
      • 现代 CPU 的 缓存预取 (Cache Prefetching) 机制对这种顺序扫描优化到了极致。
      • 相比之下,版本 2 的 unordered_map 涉及到复杂的哈希计算、内存地址的随机跳跃,这些操作在 CPU 看来是非常“重”的。
      • 结果: 即使暴力代码多做了一些判断,但由于它跑得“顺溜”,实际耗时反而和带哈希表的 O(N2)O(N^2) 差不多。

      那么,什么样的暴力会死?

      如果你把最内层的查找改成不带剪枝的线性扫描,或者把 target 的判断条件变得极其复杂,它就会 TLE。

      但作为一名优秀的竞赛选手,你应该学到:

      1. 数据特性决定算法表现: 对于增长极快的序列,暴力往往有奇效。
      2. 简单即是快: 尽量利用数组的连续性,少用复杂的动态容器。
      3. 不能迷信大 OO 阶: O(N2logN)O(N^2 \log N) 配合极小的常数,在 N=5000N=5000 左右经常能跑赢常数巨大的 O(N2)O(N^2)

      真正的“杀手”数据是什么?

      如果要卡掉这个暴力,教练需要构造一种**“高密度、近等差”**的数据,例如: nums = [1, 2, 3, 4, 5, ..., 5000] 在这种数据下,虽然斐波那契链依然不长,但每一对 (i,j)(i, j) 产生的 target 都会迫使 k 循环往后走很多步才能触发 break。但即便是这样,由于 N=5000N=5000N2N^2 只有 2.5×1072.5 \times 10^7,在 O2O2 优化下依然很难卡死。

      总结: 你的暴力能过,是因为你利用了**“递增数组+早停剪枝+斐波那契指数级增长”**这三重物理 Buff。这在赛场上叫“高效暴力”,也是高分选手必备的素质!

      • 0
        @ 2026-3-16 17:30:13

        针对“最长类斐波那契子序列”,我为你准备了这个集成化的数据生成器。它能够自动生成 10 组测试点,并根据竞赛逻辑实现了阶梯式难度,以确保暴力算法(O(N3)O(N^3))能拿部分分,而只有 O(N2)O(N^2) 算法能拿满分。

        数据生成器与标程 (C++14 标准)

        该代码运行后会直接在当前目录下生成 1.in10.out 共 20 个文件。

        #include <iostream>
        #include <fstream>
        #include <vector>
        #include <string>
        #include <random>
        #include <unordered_map>
        #include <algorithm>
        #include <chrono>
        
        using namespace std;
        
        // ======================== 标程 (Solver) O(N^2) ========================
        // 采用哈希表优化的 DP 算法,用于生成 .out 文件
        int get_ans(int n, const vector<int>& a) {
            if (n < 3) return 0;
            unordered_map<int, int> val_to_idx;
            val_to_idx.reserve(n);
            for (int i = 0; i < n; ++i) val_to_idx[a[i]] = i;
        
            // dp[j][i] 表示以 a[j], a[i] 结尾的序列长度
            // 使用一维数组模拟二维以提升内存连续性性能
            vector<int> dp(n * n, 0);
            int max_len = 0;
        
            for (int i = 0; i < n; ++i) {
                for (int j = 0; j < i; ++j) {
                    int target = a[i] - a[j];
                    // 因为数组严格递增,target 必须小于 a[j] 才能保证其索引 k < j
                    if (target < a[j] && val_to_idx.count(target)) {
                        int k = val_to_idx[target];
                        dp[j * n + i] = dp[k * n + j] + 1;
                        if (dp[j * n + i] == 1) dp[j * n + i] = 3; // 初始长度应为 3 (k, j, i)
                        max_len = max(max_len, dp[j * n + i]);
                    }
                }
            }
            return max_len >= 3 ? max_len : 0;
        }
        
        // ======================== 构造器 (Generator) ========================
        void generate_test_case(int tid) {
            string in_name = to_string(tid) + ".in";
            string out_name = to_string(tid) + ".out";
            ofstream fin(in_name);
            
            mt19937 rng(tid + chrono::steady_clock::now().time_since_epoch().count());
            
            int n;
            vector<int> a;
            const int INF = 1000000000;
        
            // --- 难度阶梯设计 ---
            if (tid <= 3) n = 150;        // 暴力 O(N^3) 可过
            else if (tid <= 6) n = 800;   // O(N^2 log N) 勉强过,O(N^3) 必挂
            else n = 2500;                // 只有最优 $O(N^2)$ 可过
        
            // --- 数据分布构造 ---
            if (tid == 2) { 
                // 边界点:构造完全没有解的情况 (等比数列)
                for(int i=0; i<n; ++i) a.push_back(100 + i * 1000000);
            } 
            else if (tid == 3) {
                // 构造:包含一个极长的斐波那契序列
                vector<long long> fib = {1, 2};
                while(fib.back() < INF) {
                    fib.push_back(fib[fib.size()-1] + fib[fib.size()-2]);
                }
                for(auto v : fib) if(v <= INF) a.push_back((int)v);
                while((int)a.size() < n) a.push_back(rng() % INF);
            }
            else {
                // 普通构造:随机增加的序列
                // 为了增加成链几率,随机选取两个数作为起点构造若干短链
                for(int i=0; i < n/10; ++i) {
                    long long v1 = rng() % 1000 + 1;
                    long long v2 = v1 + rng() % 1000 + 1;
                    a.push_back((int)v1); a.push_back((int)v2);
                    while(v1 + v2 <= INF) {
                        long long v3 = v1 + v2;
                        a.push_back((int)v3);
                        v1 = v2; v2 = v3;
                        if(rng() % 10 == 0) break; // 随机断裂
                    }
                }
                while((int)a.size() < n) a.push_back(rng() % INF);
            }
        
            // 必须保证严格递增且不重复
            sort(a.begin(), a.end());
            a.erase(unique(a.begin(), a.end()), a.end());
            n = a.size();
        
            // 写入文件
            fin << n << "\n";
            for (int i = 0; i < n; ++i) fin << a[i] << (i == n - 1 ? "" : " ");
            fin.close();
        
            // 计算标准答案
            ofstream fout(out_name);
            fout << get_ans(n, a) << endl;
            fout.close();
        
            cout << "Test Case " << tid << " generated: N=" << n << endl;
        }
        
        int main() {
            cout << "Generating 10 test cases for Longest Fibonacci-Like Subsequence..." << endl;
            for (int i = 1; i <= 10; ++i) {
                generate_test_case(i);
            }
            cout << "Done." << endl;
            return 0;
        }
        

        数据分布与复杂度分析

        测试点 NN 范围 构造特征 区分算法
        1 N150N \approx 150 随机分布 基础 O(N3)O(N^3) 暴力能拿分
        2 无合法子序列 测试对 max_len < 3 返回 0 的处理
        3 包含标准 FnF_n 验证最长路径检索能力
        4-6 N800N \approx 800 混合随机链 O(N3)O(N^3) 超时,O(N2logN)O(N^2 \log N) 刚好 AC
        7-10 N=2500N = 2500 大规模稀疏/稠密链 必须使用 O(N2)O(N^2) (哈希或双指针)

        关键设计点说明

        1. 区分算法复杂度
          • N=2500N=2500 是关键。O(N2)O(N^2) 的操作次数约为 6.25×1066.25 \times 10^6,在 C++ 中配合哈希表查找约耗时 0.1s0.3s0.1s \sim 0.3s
          • O(N3)O(N^3) 的操作次数约为 1.5×10101.5 \times 10^{10},在 1 秒时限内绝对无法通过。
        2. 文件大小控制
          • 即使是 N=2500N=2500 的测试点,每个整数约 10 字节,文件大小约 25KB25KB。10 组总大小约 250KB250KB,远低于 2MB2MB 的限制。
        3. 安全性与异常
          • 严格递增:使用了 sort + unique,满足题目“严格递增”的要求。
          • 溢出处理:在生成 Fibonacci 链时使用了 long long 判断,防止计算 A[k]+A[j]A[k]+A[j] 时超过 int 上限导致死循环。
          • 非递归实现:标程和生成器均采用循环结构,避免深度递归导致的爆栈。
        4. 随机性
          • 使用 mt19937 配合时间戳种子,保证每次生成的数据分布均匀且不重复。

        使用方法:

        1. 将代码保存为 gen.cpp
        2. 编译运行:g++ gen.cpp -o gen -O3 -std=c++14
        3. 执行 ./gen,即可在文件夹内看到 20 个标准的测试文件。
        • 0
          @ 2026-3-16 17:22:31

          孩子,这道题是 “状态压缩”与“降维打击” 的经典博弈。在类斐波那契序列中,三元关系 A[k]+A[j]=A[i]A[k] + A[j] = A[i] 是核心。

          下面我按算法进阶的顺序,为你提供三个版本的完整代码,并分析其演变过程。


          版本 1:暴力搜索(适合初学者,预计得分 30-50%)

          思路: 既然序列由前两项决定,我们就枚举数组中任意两个数作为序列的“种子”,然后按照斐波那契规则不断向后寻找。 时间复杂度: O(N3)O(N^3)空间复杂度: O(1)O(1)

          #include <iostream>
          #include <vector>
          #include <algorithm>
          
          using namespace std;
          
          // 版本 1: 暴力枚举起点种子 O(N^3)
          int solve_v1(int n, vector<int>& nums) {
              int max_len = 0;
              // 枚举序列的第一项和第二项
              for (int i = 0; i < n; ++i) {
                  for (int j = i + 1; j < n; ++j) {
                      int a = nums[i];
                      int b = nums[j];
                      int curr_len = 2;
                      // 按照规则在剩余部分寻找 a + b
                      // 注意:因为数组递增,可以使用 binary_search 提速至 O(N^2 log N)
                      while (true) {
                          int next_val = a + b;
                          // 在 j 之后寻找 next_val
                          if (binary_search(nums.begin() + j + 1, nums.end(), next_val)) {
                              curr_len++;
                              a = b;
                              b = next_val;
                          } else {
                              break;
                          }
                      }
                      if (curr_len >= 3) max_len = max(max_len, curr_len);
                  }
              }
              return max_len;
          }
          
          int main() {
              int n; cin >> n;
              vector<int> nums(n);
              for (int i = 0; i < n; ++i) cin >> nums[i];
              cout << solve_v1(n, nums) << endl;
              return 0;
          }
          

          版本 2:哈希表优化 DP(资料推荐做法,预计得分 100%)

          思路: 引入二维 DP。dp[j][i] 表示以索引 jjii 结尾的类斐波那契序列长度。 转移方程: 找到 kk 使得 nums[k]=nums[i]nums[j]nums[k] = nums[i] - nums[j]k<jk < j,则 dp[j][i] = dp[k][j] + 1时间复杂度: 平均 O(N2)O(N^2)空间复杂度: O(N2)O(N^2)

          #include <iostream>
          #include <vector>
          #include <unordered_map>
          #include <algorithm>
          
          using namespace std;
          
          // 全局静态数组,2500*2500 占用约 25MB 内存,NOIP 建议放在全局
          int dp[2505][2505];
          
          // 版本 2: 哈希表辅助 DP O(N^2)
          int solve_v2(int n, vector<int>& nums) {
              // 预处理数值到索引的映射,实现 O(1) 查找 k
              unordered_map<int, int> val_to_idx;
              for (int i = 0; i < n; ++i) val_to_idx[nums[i]] = i;
          
              int max_len = 0;
              
              // 初始化:任何两项都可以视为长度为 2 的序列起点
              for (int i = 0; i < n; ++i)
                  for (int j = 0; j < i; ++j) dp[j][i] = 2;
          
              for (int i = 0; i < n; ++i) {
                  for (int j = 0; j < i; ++j) {
                      int target = nums[i] - nums[j];
                      // 易错点:必须保证 target < nums[j] (因为数组递增,即保证索引 k < j)
                      // 且 target 必须在数组中存在
                      if (target < nums[j] && val_to_idx.count(target)) {
                          int k = val_to_idx[target];
                          dp[j][i] = dp[k][j] + 1;
                          max_len = max(max_len, dp[j][i]);
                      }
                  }
              }
              return max_len >= 3 ? max_len : 0;
          }
          
          int main() {
              ios::sync_with_stdio(false); cin.tie(0);
              int n; cin >> n;
              vector<int> nums(n);
              for (int i = 0; i < n; ++i) cin >> nums[i];
              cout << solve_v2(n, nums) << endl;
              return 0;
          }
          

          版本 3:双指针优化 DP(最优常数版,适合 NN 较大时)

          思路: 既然 nums严格递增的,对于固定的 ii,当 jj 减小时,target=nums[i]nums[j]target = nums[i] - nums[j] 会增大。我们可以用左右双指针在 O(N)O(N) 内处理完所有的 jj优点: 彻底抛弃哈希表,常数极小,且空间利用率高。 时间复杂度: 严格 O(N2)O(N^2)空间复杂度: O(N2)O(N^2)

          #include <iostream>
          #include <vector>
          
          using namespace std;
          
          int f[2505][2505]; // f[j][i]
          
          int solve_v3(int n, vector<int>& nums) {
              int max_len = 0;
              // 技巧:i 作为序列最后一项
              for (int i = 2; i < n; ++i) {
                  int left = 0, right = i - 1;
                  while (left < right) {
                      int sum = nums[left] + nums[right];
                      if (sum < nums[i]) {
                          left++; // 和太小,左指针右移
                      } else if (sum > nums[i]) {
                          right--; // 和太大,右指针左移
                      } else {
                          // 刚好满足:nums[left] + nums[right] = nums[i]
                          // 此时 k=left, j=right
                          f[right][i] = (f[left][right] > 0 ? f[left][right] : 2) + 1;
                          max_len = max(max_len, f[right][i]);
                          left++;
                          right--;
                      }
                  }
              }
              return max_len;
          }
          
          int main() {
              int n; cin >> n;
              vector<int> a(n);
              for(int i=0; i<n; ++i) cin >> a[i];
              cout << solve_v3(n, a) << endl;
              return 0;
          }
          

          复杂度分析思考过程

          1. 为什么需要二维 DP? 一维 DP 只能代表“以 ii 结尾的最优解”。但在类斐波那契中,同样是以数字 8 结尾,[1, 7, 8][3, 5, 8] 是完全不同的序列。增加一维 j(前一项)是为了锁死序列的公和关系

          2. 空间换时间的权衡:

            • N=2500N=2500 时,N26.25×106N^2 \approx 6.25 \times 10^6
            • 使用 int 数组占用约 25MB,远低于常见的 128MB 限制。
            • 如果 NN 增加到 1000010000,我们需要考虑使用哈希表(只存存在的对)或滚动数组(如果可能)。

          时间复杂度优化建议

          1. 哈希表 vs 二分查找:
            • 如果 A[i]A[i] 范围很大但 NN 很小,哈希表(unordered_map)表现良好。
            • 但在 NOIP 竞赛中,如果数组有序且数据密集,双指针 (Version 3) 的常数是最小的,通常比哈希表快 3-5 倍。
          2. 提前终止:
            • 在双指针扫描时,如果 nums[left] * 2 > nums[i],说明不可能再找到满足条件的 leftright(因为 nums[left]<nums[right]nums[left] < nums[right]),可以提前跳出。

          教练寄语: 孩子,记住:子序列问题的维度通常取决于它“回看”了多少项。 既然类斐波那契要看两项,那就给它两个维度。掌握了这个“升维”思想,你就掌握了复杂 DP 的命门。

          • 1

          最长类斐波那契子序列 (Longest Fibonacci-Like Subsequence)

          信息

          ID
          19492
          时间
          1000ms
          内存
          128MiB
          难度
          10
          标签
          递交数
          5
          已通过
          6
          上传者