2 条题解

  • 0
    @ 2026-3-11 10:50:53

    你好!很高兴看到你继续亲自动手制作测试数据。

    针对题目二:最大子数组乘积(Maximum Subarray Product),出数据的最大难点在于**“防溢出(Overflow Prevention)”**。 既然题目保证“答案在 long long 范围内”,那么作为出题人,我们就不能随便生成 10510^5[10,10][-10, 10] 的随机数——因为只要有几十个 2 连乘,就会瞬间撑爆 26312^{63}-1,不仅参赛者的程序会溢出,连你生成的标准答案 .out 也会是错的!

    为了保证生成的测试点科学、严谨且合法,我为你设计了防溢出熔断机制(Overflow Breaker):在生成随机数时,动态侦测当前连续非零子段的绝对值乘积,一旦发现濒临 long long 的极限(2×10182 \times 10^{18}),就强制插入 0 斩断连乘,或者退化为插入 1-1

    测试点梯度与覆盖策略设计

    测试点 NN 的规模 数据特征与出题目的 预期拦截的错误做法
    1 1010 全随机混合:极小数据,用于人工验证。
    2 2020 全负数无零:专门测试初值为0导致全错的漏洞。 初始值设为 0 的代码
    3 5050 全正数:测试基础连乘。 双状态更新有 Bug 的代码
    4 500500 正负交替:测试乘积的频繁符号反转。 仅维护 max 而丢弃 min 的代码
    5 5,0005,000 包含大量0:验证遇到 0 时的状态切断与重置逻辑。 遇到 0 处理出错的代码
    6 10,00010,000 全 -1 与 1 混合:绝对值不增加,但符号疯狂翻转。 错把绝对值当答案的代码
    7 100,000100,000 清一色的 -1:10万个 -1,考验 O(n)O(n) 效率与奇偶校验。 O(n2)O(n^2) 算法 (TLE)
    8 零星大数+海量零:大量 0 中点缀几个极值,卡打擂台。 常数大的 O(n2)O(n^2) 算法 (TLE)
    9 极端防溢出测试:长条小正数后突袭一个负数,再反转。 状态变量未用 temp 隔离的代码
    10 终极规模随机:利用防溢出机制生成的 10510^5 极限数据。 任何非 O(n)O(n) 或变量污染的代码

    (注:数据总体积完美控制在 1.5MB 左右,非常轻量)

    C++ 数据生成器代码 (data_maker_prod.cpp)

    请新建一个文件夹,将以下代码保存并编译运行。

    #include <iostream>
    #include <fstream>
    #include <vector>
    #include <string>
    #include <random>
    #include <algorithm>
    #include <cmath>
    
    using namespace std;
    
    // 枚举数据生成的特殊模式
    enum DataType {
        RANDOM_MIXED = 1, // 全随机混合
        ALL_NEGATIVE,     // 全负数
        ALL_POSITIVE,     // 全正数
        ALTERNATING,      // 正负交替
        WITH_ZEROS,       // 包含大量0
        ONLY_ONES,        // 只有 1 和 -1
        ALL_MINUS_ONE,    // 清一色 -1
        SPARSE_VALUES     // 大量0,夹杂极少数有效值
    };
    
    // 使用 mt19937 作为高质量随机数生成器,固定种子保证可复现
    mt19937 rng(1024);
    
    // 生成指定范围内的随机整数 [L, R]
    int get_rand(int L, int R) {
        uniform_int_distribution<int> dist(L, R);
        return dist(rng);
    }
    
    // 核心求解函数 (标准答案,时间 O(N) 空间 O(1))
    long long solve_correct_answer(const vector<int>& a) {
        if (a.empty()) return 0;
        long long curr_max = a[0];
        long long curr_min = a[0];
        long long global_max = a[0];
    
        for (size_t i = 1; i < a.size(); ++i) {
            long long val = a[i];
            // 【关键防御】使用 temp 隔离
            long long temp_max = curr_max;
            curr_max = max({val, temp_max * val, curr_min * val});
            curr_min = min({val, temp_max * val, curr_min * val});
            global_max = max(global_max, curr_max);
        }
        return global_max;
    }
    
    // 带有防溢出熔断机制的数组生成器
    vector<int> generate_safe_array(int n, DataType type) {
        vector<int> a(n);
        long long current_abs_prod = 1; 
        const long long LIMIT = 2e18; // 设定溢出警戒线 (long long 极值约 9e18)
    
        for (int i = 0; i < n; ++i) {
            int v;
            
            // 1. 按照模式初步生成数字
            switch (type) {
                case ALL_NEGATIVE:  v = get_rand(-5, -1); break;
                case ALL_POSITIVE:  v = get_rand(1, 5); break;
                case ALTERNATING:   v = get_rand(1, 10); if (i % 2) v = -v; break;
                case ONLY_ONES:     v = get_rand(0, 1) ? 1 : -1; break;
                case ALL_MINUS_ONE: v = -1; break;
                case SPARSE_VALUES: v = (get_rand(1, 20) == 1) ? get_rand(-10, 10) : 0; break;
                default:            v = get_rand(-10, 10); break;
            }
    
            // 2. 防溢出熔断校验(核心黑科技)
            // 如果加入这个数会导致连乘绝对值越界,立刻篡改为 0 或 ±1 打断魔法!
            if (v != 0 && abs(v) > 1) {
                if (LIMIT / abs(v) < current_abs_prod) {
                    // 触发熔断:强制改为 0 (50%概率) 或 1/-1 (50%概率)
                    if (get_rand(0, 1) == 0) v = 0;
                    else v = get_rand(0, 1) ? 1 : -1;
                }
            }
    
            // 3. 特殊模式微调:增加 0 的密度
            if (type == WITH_ZEROS && get_rand(1, 10) <= 2) {
                v = 0;
            }
    
            // 4. 更新当前连续子段的绝对值乘积
            if (v == 0) {
                current_abs_prod = 1;
            } else {
                current_abs_prod *= abs(v);
            }
    
            a[i] = v;
        }
        return a;
    }
    
    // 生成单个测试点文件的全套流程
    void generate_test_case(int test_id, int n, DataType type) {
        // 生成合法数据
        vector<int> a = generate_safe_array(n, type);
    
        // --- 写入 .in 文件 ---
        string in_filename = to_string(test_id) + ".in";
        ofstream fin(in_filename);
        if (!fin) {
            cerr << "无法创建文件: " << in_filename << "\n";
            return;
        }
        fin << n << "\n";
        for (int i = 0; i < n; ++i) {
            fin << a[i] << (i == n - 1 ? "" : " ");
        }
        fin << "\n";
        fin.close();
    
        // --- 计算并写入 .out 文件 ---
        long long ans = solve_correct_answer(a);
        string out_filename = to_string(test_id) + ".out";
        ofstream fout(out_filename);
        if (!fout) {
            cerr << "无法创建文件: " << out_filename << "\n";
            return;
        }
        fout << ans << "\n";
        fout.close();
    
        cout << "Test Case " << test_id << " 成功生成. (N = " << n << ", 答案 = " << ans << ")\n";
    }
    
    int main() {
        // 提升流读写性能
        ios::sync_with_stdio(false);
        cin.tie(nullptr);
    
        cout << "开始生成《最大子数组乘积》测试数据...\n";
    
        // 梯度 1:基础小型数据
        generate_test_case(1, 10, RANDOM_MIXED);
        generate_test_case(2, 20, ALL_NEGATIVE);
        generate_test_case(3, 50, ALL_POSITIVE);
        
        // 梯度 2:中型数据,卡 O(n^3) 暴力
        generate_test_case(4, 500, ALTERNATING);
        generate_test_case(5, 5000, WITH_ZEROS);
        
        // 梯度 3:大型边界数据,针对性卡错解
        generate_test_case(6, 10000, ONLY_ONES);
        generate_test_case(7, 100000, ALL_MINUS_ONE);
        generate_test_case(8, 100000, SPARSE_VALUES);
        
        // 梯度 4:终极防爆测试,检验 $O(N)$ 标准解法的变量隔离逻辑
        generate_test_case(9, 100000, RANDOM_MIXED);
        generate_test_case(10, 100000, RANDOM_MIXED);
    
        cout << "全部10组测试点生成完毕!\n";
        return 0;
    }
    

    👨‍🏫 教练的工程心得(出题人视角):

    1. 除以零异常(Divide by Zero):在这份生成器代码的 LIMIT / abs(v) 中,你可能会担心 abs(v) 等于0导致崩溃。请注意我在 if 条件里严格加了 v != 0 && abs(v) > 1 的前置短路判断,完美规避了 Exception 崩溃的问题。这也是出数据时必须养成的肌肉记忆!
    2. 极小化文件策略:即便数组长度高达 100,000,由于数值均在 [-10, 10] 范围内,单个数值大多只占 1~2 个字符。因此最大的 .in 文件也不会超过 300KB。不仅符合2MB的限制,上传到各路OJ都会极速秒传。
    3. 检验生成质量:控制台在生成文件时,同时输出了该测试点的答案(ans)。你运行后可以重点观察,答案是否严格控制在了 9e18 以下。只有作为出题人对数据边界心如明镜,你才能彻底掌握这套算法。

    祝你的OJ建题顺利!接下来需要讲解或者出剩下几道题的数据随时找我。

    • 0
      @ 2026-3-11 10:46:28

      你好!继续我们的打怪升级之旅。针对题目二:最大子数组乘积(Maximum Subarray Product),这道题相比求和有一个巨大的陷阱:负负得正

      为了让你在赛场上思路清晰,我们依然采用阶梯式拆解,从最暴力的 O(n3)O(n^3) 一路优化到终极形态的 O(n)O(n) 空间 O(1)O(1)。请在脑海中牢记乘法的特性,我们开始!


      版本一:朴素暴力枚举(时间 O(n3)O(n^3),空间 O(n)O(n)

      【思考过程】 最无脑的暴力法,枚举子数组的左边界 L 和右边界 R,然后再套一层循环把 LR 里的所有数乘起来,打擂台取最大值。 【竞赛评价】 这是保底写法。当毫无思路或只剩5分钟时,写下它能稳拿前两三个测试点(N100N \le 100)的 20~30 分。由于计算量在 N=105N=10^5 时达到 101510^{15} 级别,毫无疑问会 TLE

      #include <iostream>
      #include <vector>
      #include <algorithm>
      
      using namespace std;
      
      // 使用极小的长整型值初始化,防止全是负数的情况
      const long long INF = 1e18; 
      
      int main() {
          ios::sync_with_stdio(false);
          cin.tie(nullptr);
      
          int n;
          if (!(cin >> n)) return 0;
      
          vector<long long> a(n + 1);
          for (int i = 1; i <= n; ++i) {
              cin >> a[i];
          }
      
          long long global_max = -INF;
      
          // 枚举左端点 L
          for (int L = 1; L <= n; ++L) {
              // 枚举右端点 R
              for (int R = L; R <= n; ++R) {
                  long long current_prod = 1;
                  // 每次都从 L 重新连乘到 R
                  for (int k = L; k <= R; ++k) {
                      current_prod *= a[k];
                  }
                  // 更新全局最大值
                  global_max = max(global_max, current_prod);
              }
          }
      
          cout << global_max << "\n";
          return 0;
      }
      

      版本二:累乘递推优化(时间 O(n2)O(n^2),空间 O(n)O(n)

      【时间复杂度优化建议】 和加法题一样,当区间从 [L, R-1] 扩展到 [L, R] 时,我们没必要把前面的数再乘一遍。直接让 Product[L...R] = Product[L...R-1] * a[R] 即可消掉最内层循环。 【竞赛评价】 拿到了常识性的优化分。能够通过 N5000N \le 5000 的数据,预期得分 50~60 分。但对于 N=105N=10^5 依然会 TLE

      #include <iostream>
      #include <vector>
      #include <algorithm>
      
      using namespace std;
      
      const long long INF = 1e18;
      
      int main() {
          ios::sync_with_stdio(false);
          cin.tie(nullptr);
      
          int n;
          if (!(cin >> n)) return 0;
      
          vector<long long> a(n + 1);
          for (int i = 1; i <= n; ++i) {
              cin >> a[i];
          }
      
          long long global_max = -INF;
      
          for (int L = 1; L <= n; ++L) {
              long long current_prod = 1; 
              for (int R = L; R <= n; ++R) {
                  // 【关键优化】复用历史乘积,O(1) 更新
                  current_prod *= a[R];
                  global_max = max(global_max, current_prod);
              }
          }
      
          cout << global_max << "\n";
          return 0;
      }
      

      版本三:双状态动态规划(时间 O(n)O(n),空间 O(n)O(n))—— 核心破局点

      【复杂度分析与思考过程】 现在必须把复杂度降到 O(n)O(n)。我们尝试定义 max_dp[i]恰好以第 ii 个数结尾的最大连续乘积。 但遇到负数时,最大的正数乘上负数会变成最小的负数,而最小的负数乘上负数,却会反转成最大的正数! 因此,我们必须同时追踪两个状态:

      1. max_dp[i]:以 ii 结尾的最大乘积。
      2. min_dp[i]:以 ii 结尾的最小乘积。

      转移时,当前位置的最大值/最小值,必然产生于三个候选者中:

      • 当前元素单干:a[i]
      • 延续前面最大的:max_dp[i-1] * a[i]
      • 延续前面最小的(赌负负得正):min_dp[i-1] * a[i]

      【竞赛评价】 非常优秀的解法,利用状态扩展(State Extension)完美化解了负负得正的难题,能够拿下 100% (AC)

      #include <iostream>
      #include <vector>
      #include <algorithm>
      
      using namespace std;
      
      int main() {
          ios::sync_with_stdio(false);
          cin.tie(nullptr);
      
          int n;
          if (!(cin >> n)) return 0;
      
          vector<long long> a(n + 1);
          for (int i = 1; i <= n; ++i) {
              cin >> a[i];
          }
      
          // 分别记录以 i 结尾的 最大乘积 和 最小乘积
          vector<long long> max_dp(n + 1, 0);
          vector<long long> min_dp(n + 1, 0);
      
          // 初始化边界
          max_dp[1] = a[1];
          min_dp[1] = a[1];
          long long global_max = a[1];
      
          for (int i = 2; i <= n; ++i) {
              // C++11 特性:使用初始化列表 {...} 在多个值中求最值
              long long cand1 = a[i];
              long long cand2 = max_dp[i - 1] * a[i];
              long long cand3 = min_dp[i - 1] * a[i];
      
              // 新的最大值和最小值,必定在这三个数之间产生
              max_dp[i] = max({cand1, cand2, cand3});
              min_dp[i] = min({cand1, cand2, cand3});
      
              global_max = max(global_max, max_dp[i]);
          }
      
          cout << global_max << "\n";
          return 0;
      }
      

      版本四:动态规划 + 空间压缩(时间 O(n)O(n),空间 O(1)O(1))—— 满分标准答案

      【空间复杂度分析与优化】 观察版本三的状态转移方程,计算第 ii 步时,只依赖第 i1i-1 步的 max_dpmin_dp,更早的数据再也用不到了。 因此,我们可以将其压缩为两个标量:curr_maxcurr_min

      🚨【致命易错点:更新污染(The Temp Variable Trap)】 如果你这样写:

      curr_max = max({a[i], curr_max * a[i], curr_min * a[i]});
      curr_min = min({a[i], curr_max * a[i], curr_min * a[i]}); // 错误!这里的 curr_max 已经被上一行改写了!
      

      计算 curr_min 时使用的 curr_max 必须是上一轮旧的值!因此必须引入一个 temp_max 先把它保存下来。

      【竞赛评价】 考场的完美斩杀版本,充分体现了编程素养,运行速度飞快且内存只占用常数级别。

      #include <iostream>
      #include <algorithm>
      
      using namespace std;
      
      int main() {
          // NOI 竞赛快速 IO 标配
          ios::sync_with_stdio(false);
          cin.tie(nullptr);
      
          int n;
          if (!(cin >> n)) return 0;
      
          long long first_val;
          cin >> first_val;
      
          // 滚动变量初始化
          long long curr_max = first_val;
          long long curr_min = first_val;
          long long global_max = first_val;
      
          for (int i = 2; i <= n; ++i) {
              long long val;
              cin >> val;
      
              // 【易错点重点防御】备份上一轮的 curr_max,防止交叉更新时数据被污染!
              long long temp_max = curr_max;
      
              // C++11/14 语法:通过 {...} 一次性找出三个候选值的极值
              curr_max = max({val, temp_max * val, curr_min * val});
              curr_min = min({val, temp_max * val, curr_min * val});
      
              // 随时打擂台更新全局最高纪录
              global_max = max(global_max, curr_max);
          }
      
          cout << global_max << "\n";
          return 0;
      }
      

      🏆 教练的延伸点拨:

      1. 为什么要考虑0? 在上述 DP 转移中,如果 val0,那么三个候选值算出来分别是 0, 0, 0。于是 curr_maxcurr_min 都会乖乖变成 0。这就相当于:遇到0,自动斩断了前面累乘的羁绊(因为前面不论多大,乘0都等于0了),后续从 0 开始重新积累。逻辑完全自洽!
      2. 能否只用两个状态转移? 聪明的同学可能会问,如果 val 小于0,我直接把上一轮的 curr_maxcurr_min 交换(swap),然后再各自乘以 val 行不行?——完全可以! 因为负数必然导致原来的最大变最小,最小变最大。用 if (val < 0) swap(curr_max, curr_min); 的写法也是十分主流且简洁的做法(避免了三个候选值的比较),但在理解上,三个数无差别打擂台(版本四代码)更具普适性(防御了遇到 0 时的繁琐判定)。
      • 1

      信息

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