4 条题解

  • 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。这在赛场上叫“高效暴力”,也是高分选手必备的素质!

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

    信息

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