2 条题解

  • 0
    @ 2026-3-11 11:14:42

    你好!作为教练,我非常高兴看到你持续为题目亲手制作高质量的测试数据。

    针对题目三:环形最大子数组和(Max Circular Subarray Sum),这道题的测试数据相比前两题,有着非常明确的**“特殊构造需求”。因为在随机生成的数据下,最大子数组很大概率只是普通的线性子数组(不跨界)。为了确保你的测试数据能逼迫参赛者的程序去计算“跨界情况(Wrapped Subarray)”,我们必须人工干预,构造出“首尾极大正数,中间极大负数”**的特殊测试点。

    同时,我们绝不能漏掉那个人类极易写错的**“全负数(All Negative)”**必杀边界!

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

    测试点 NN 的规模 数据特征与出题目的 预期拦截的错误做法
    1 1010 全随机混合:极小数据,供人工验算。
    2 5050 全负数必杀阵:最大子数组长度为1,跨界值为0。 没有处理 max_linear < 0 的代码(输出0)
    3 全正数:最大和就是全数组之和(跨不跨界一样大)。 逻辑混乱的跨界求和
    4 100100 强制跨界(小型):首尾是正数,中间全是负数。 没有处理环形逻辑的线性代码
    5 500500 随机混合:中型规模,用于卡掉 O(n3)O(n^3) 暴力枚举。 O(n3)O(n^3) 算法 (TLE)
    6 5,0005,000 强制跨界(中型):验证前缀和 / 滚动累加优化。 O(n3)O(n^3) 及大常数 O(n2)O(n^2)
    7 100,000100,000 强制线性(大型):首尾极小负数,中间极大正数。 O(n2)O(n^2) 算法 (TLE)
    8 强制跨界(大型):首尾极大正数,中间极大负数。
    9 高频正负交替:连续跳变,考验 O(N)O(N) 算法的常数时间。 单调队列常数过大写法的代码
    10 极限规模纯随机:最大规模,检验综合鲁棒性。 一切非最优时空复杂度的代码

    (注:数据总体积完美控制在 2MB 内。生成算法为 O(n)O(n) 纯迭代,无任何嵌套递归,毫秒级出数据,绝不爆栈。)

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

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

    #include <iostream>
    #include <fstream>
    #include <vector>
    #include <string>
    #include <random>
    #include <algorithm>
    
    using namespace std;
    
    // 枚举数据生成的特殊模式
    enum DataType {
        RANDOM_MIXED = 1, // 全随机混合
        ALL_NEGATIVE,     // 全负数 (致命边界)
        ALL_POSITIVE,     // 全正数
        FORCE_WRAPPED,    // 强制答案跨界 (首尾正,中间负)
        FORCE_LINEAR,     // 强制答案不跨界 (首尾负,中间正)
        ALTERNATING       // 高频正负交替
    };
    
    // 采用 mt19937 高质量随机生成器,固定种子以保证数据一致性
    mt19937 rng(2026); 
    
    // 生成指定范围内的随机整数 [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], max_linear = a[0];
        long long curr_min = a[0], min_linear = a[0];
        long long total_sum = a[0];
    
        for (size_t i = 1; i < a.size(); ++i) {
            long long val = a[i];
            
            curr_max = max(val, curr_max + val);
            max_linear = max(max_linear, curr_max);
            
            curr_min = min(val, curr_min + val);
            min_linear = min(min_linear, curr_min);
            
            total_sum += val;
        }
    
        // 处理全负数边界情况!
        if (max_linear < 0) {
            return max_linear;
        }
        
        // 跨界最大值 = 整体和 - 最小连续子数组和
        return max(max_linear, total_sum - min_linear);
    }
    
    // 按照不同模式生成数组
    vector<int> generate_array(int n, DataType type) {
        vector<int> a(n);
        switch (type) {
            case RANDOM_MIXED:
                for (int i = 0; i < n; ++i) a[i] = get_rand(-10000, 10000);
                break;
            case ALL_NEGATIVE:
                for (int i = 0; i < n; ++i) a[i] = get_rand(-10000, -1);
                break;
            case ALL_POSITIVE:
                for (int i = 0; i < n; ++i) a[i] = get_rand(1, 10000);
                break;
            case FORCE_WRAPPED:
                // 前 20% 和 后 20% 是正数,中间 60% 是极大的负数
                for (int i = 0; i < n; ++i) {
                    if (i < n * 0.2 || i >= n * 0.8) {
                        a[i] = get_rand(1000, 10000);
                    } else {
                        a[i] = get_rand(-10000, -5000);
                    }
                }
                break;
            case FORCE_LINEAR:
                // 前 20% 和 后 20% 是负数,中间 60% 是极大的正数
                for (int i = 0; i < n; ++i) {
                    if (i < n * 0.2 || i >= n * 0.8) {
                        a[i] = get_rand(-10000, -5000);
                    } else {
                        a[i] = get_rand(1000, 10000);
                    }
                }
                break;
            case ALTERNATING:
                for (int i = 0; i < n; ++i) {
                    if (i % 2 == 0) a[i] = get_rand(1, 10000);
                    else a[i] = get_rand(-10000, -1);
                }
                break;
        }
        return a;
    }
    
    // 生成单个测试点文件的全套流程
    void generate_test_case(int test_id, int n, DataType type) {
        vector<int> a = generate_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 << ", 模式 = " << type << ", 答案 = " << 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, 50, ALL_NEGATIVE); // 核心边界:卡死不判 max_linear < 0 的人
        generate_test_case(3, 50, ALL_POSITIVE);
        generate_test_case(4, 100, FORCE_WRAPPED); // 检查是否实现了跨界逻辑
        
        // 梯度 2:中型规模,用于卡 O(n^3)
        generate_test_case(5, 500, RANDOM_MIXED);
        generate_test_case(6, 5000, FORCE_WRAPPED);
        
        // 梯度 3:大型边界构造,强制检验跨界与不跨界的 O(n) 判断
        generate_test_case(7, 100000, FORCE_LINEAR);
        generate_test_case(8, 100000, FORCE_WRAPPED);
        generate_test_case(9, 100000, ALTERNATING);
        
        // 梯度 4:综合极限盲测
        generate_test_case(10, 100000, RANDOM_MIXED);
    
        cout << "全部10组测试点生成完毕!\n";
        return 0;
    }
    

    👨‍🏫 教练的工程心得:

    1. 为什么要人为控制数据的“区域密度”? 在编写代码中的 FORCE_WRAPPED 模式时,我特意将前 20% 和后 20% 设为巨大的正数,中间 60% 设为负数。 如果不这样做,单纯依赖 RANDOM_MIXED 纯随机生成数据,往往最大的子数组都会恰好落在中间(因为正负抵消概率均等,跨界的优势会被稀释)。如果你不“强迫”数据呈现两头大中间小的态势,很多参赛者的程序即使完全没有写处理环形跨界的逻辑(也就是纯粹当线性数组做),也可能会意外得到 AC。做数据时一定要有“针对性卡人”的直觉。
    2. 生成性能的优化: 与使用系统调用或者树/图结构生成相比,这种纯粹的标量迭代赋值没有任何嵌套循环(O(N)O(N)),加上极少的条件分支(if-else),在现代 CPU 上耗时几乎为 0。这也遵循了你的“非递归、防超时、防爆栈”的要求。
    3. 输出体积控制: 即便达到极限 N=105N=10^5,每个数字大概占用 464 \sim 6 个字符,用单空格分隔,单文件文本大小在 500KB500 \text{KB} 左右,最终整个文件夹压缩成 ZIP 文件大概只有几百 KB,可以顺畅且无压力地导入你的 OJ 系统中。
    • 0
      @ 2026-3-11 11:07:51

      你好!继续我们的训练。题目三:环形最大子数组和(Max Circular Subarray Sum) 是一道极具启发性的经典题。它在基础的线性 DP 上引入了“物理形态的变化”(首尾相连)。

      在考场上,处理环形问题最朴素的思想是**“断环为链”(复制一倍数组拼在后面),但这往往会增加时空常数。今天我将带你从最暴力的模拟开始,一步步推导到利用“正难则反(Inversion Trick)”**的最优 O(n)O(n) 解法。


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

      【思考过程】 既然是环形数组,我们不如直接用“取模(% n)”来模拟环的特性。 我们枚举子数组的起点 i,枚举子数组的长度 len(注意长度不能超过 nn),然后再写一个循环把这 len 个数加起来。 【竞赛评价】 这是极其原始的暴力,没有任何重用历史信息的意识。时间复杂度高达 O(n3)O(n^3)。对于 N=105N=10^5 的数据,不仅会 TLE,而且在写取模下标时如果基本功不扎实,极易越界或写错。预计得分 20 分。

      #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;
      
          // 为了方便取模运算,这里强烈建议使用 0-based 索引 (0 到 n-1)
          vector<long long> a(n);
          for (int i = 0; i < n; ++i) {
              cin >> a[i];
          }
      
          long long global_max = -INF;
      
          // 枚举起点 i
          for (int i = 0; i < n; ++i) {
              // 枚举子数组的长度 len (至少包含1个元素,最多包含n个元素)
              for (int len = 1; len <= n; ++len) {
                  long long current_sum = 0;
                  // 从起点开始,连续加 len 个数
                  for (int k = 0; k < len; ++k) {
                      // 【核心逻辑】利用 (i + k) % n 实现环形跨界访问
                      current_sum += a[(i + k) % n];
                  }
                  global_max = max(global_max, current_sum);
              }
          }
      
          cout << global_max << "\n";
          return 0;
      }
      

      版本二:滚动累加优化(时间 O(n2)O(n^2),空间 O(n)O(n)

      【时间复杂度优化建议】 和做基础题一样,我们在向右扩展子数组长度时,根本不需要每次都从头加一遍。 当我们知道长度为 len-1 的和时,长度为 len 的和只需要加上那个新纳入的元素即可。我们可以把最内层的循环优化掉。 【竞赛评价】 拿到常规暴力分。时间复杂度降维至 O(n2)O(n^2),能够稳定通过 N5000N \le 5000 的数据,拿到约 50~60 分。

      #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);
          for (int i = 0; i < n; ++i) {
              cin >> a[i];
          }
      
          long long global_max = -INF;
      
          // 枚举起点 i
          for (int i = 0; i < n; ++i) {
              long long current_sum = 0;
              // 依次纳入第 1 个, 第 2 个... 直到第 n 个元素
              for (int len = 1; len <= n; ++len) {
                  // 【关键优化】直接在上一轮的基础上累加新元素
                  current_sum += a[(i + len - 1) % n];
                  global_max = max(global_max, current_sum);
              }
          }
      
          cout << global_max << "\n";
          return 0;
      }
      

      版本三:正难则反 DP + 空间压缩(时间 O(n)O(n),空间 O(1)O(1))—— 满分标准答案

      【复杂度分析与思考过程】 O(n2)O(n^2) 的算法必须抛弃,我们重新审视环形子数组的物理意义。 一个环形数组的最大子数组,只可能以两种形态存在:

      1. 常规线性形态(未跨界): 就老老实实呆在数组中间,这不就是用普通的 Kadane 算法求 max_linear_sum 吗?
      2. 两端缠绕形态(跨界了): 它包含了数组的头部一段和尾部一段。此时,中间剩下的那段没被选中的部分,必然是一个常规的线性子数组! 既然我们想要“两头选中的和最大”,就等价于让“中间没被选中的和最小”! 即:跨界最大和 = 整个数组的总和 (total_sum) - 线性最小子数组和 (min_linear_sum)

      🚨 致命易错点:全负数边界(The Edge Case) 假设输入是 [-3, -2, -1]

      • max_linear 会是 -1
      • min_linear 会是整体 -6
      • total_sum 也是 -6。 此时 跨界最大和 = (-6) - (-6) = 0。 程序会误以为 0-1 大,输出 0。但这相当于一个数字都没选(空集)!题目要求子数组必须非空。 解决方案: 如果 max_linear < 0,说明全数组都是负数,直接返回 max_linear 即可,根本不需要考虑跨界的情况。

      【竞赛评价】 极度优雅的 O(n)O(n) 解法,没有任何多余的数组复制(不断环为链),空间复杂度被极致压缩到 O(1)O(1)。体现了极强的数学转化能力。

      #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;
      
          // 初始化所有的 DP 滚动变量和打擂台变量
          long long curr_max = first_val;
          long long max_linear = first_val;
          
          long long curr_min = first_val;
          long long min_linear = first_val;
          
          long long total_sum = first_val;
      
          for (int i = 2; i <= n; ++i) {
              long long val;
              cin >> val;
      
              // 1. 维护常规的最大连续子数组和 (Kadane's Algorithm)
              curr_max = max(val, curr_max + val);
              max_linear = max(max_linear, curr_max);
      
              // 2. 维护常规的最小连续子数组和 (为逆向思维做准备)
              curr_min = min(val, curr_min + val);
              min_linear = min(min_linear, curr_min);
      
              // 3. 统计全数组总和
              total_sum += val;
          }
      
          // 【易错点重点防御】处理全负数陷阱!
          // 如果最大线性和小于 0,说明所有数字都是负数。
          // 此时跨界相减算出来的 0 属于非法空集,必须直接返回最大线性和。
          if (max_linear < 0) {
              cout << max_linear << "\n";
          } else {
              // 最终答案:在 “线性最大和” 与 “跨界最大和” 之间取优
              long long wrapped_max = total_sum - min_linear;
              cout << max(max_linear, wrapped_max) << "\n";
          }
      
          return 0;
      }
      

      🏆 教练的延伸点拨:

      1. 为什么不需要 long long 的特意防溢出(像题2那样)? 因为这道题全是加法。即使是 10510^5 个上限值为 10410^4 的数字相加,最大也就 10910^9,刚好卡在 32 位 signed int 的安全区(2×1092 \times 10^9)。不过,出于 NOI 的肌肉记忆,我仍然为你统一定义了 long long,这能让你在面对绝大多数加法区间题时“免于死刑”。
      2. “断环为链”的对比:处理环形问题的另一大流派是把数组复制一倍拼在后面,变成长度为 2N2N 的线性数组,然后用“滑动窗口(单调队列)”限制窗口大小不超过 NN 来求最大值。虽然也是 O(n)O(n) 的时间复杂度,但那种做法不仅要多开辟一倍的空间,而且常数极大(需要维护双端队列)。而我们的**“正难则反”**解法,无需任何额外数据结构,仅用 5 个变量就降维打击了原问题。学会这种逆向思维,对日后推导其它容斥原理的题目大有裨益!
      • 1

      信息

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