2 条题解

  • 0
    @ 2025-12-19 23:30:13

    在 NOI 竞赛中,构造高质量的测试数据需要包含极端边界测试、满额规模测试、逻辑陷阱测试(如 k=0k=0)。

    以下是为你编写的数据生成器。它包含了标准 O(n)O(n) 解法作为标程,并自动循环生成 10 组测试点。

    数据生成器 (C++14)

    #include <iostream>
    #include <fstream>
    #include <vector>
    #include <string>
    #include <random>
    #include <chrono>
    #include <algorithm>
    
    using namespace std;
    
    // --- 标程逻辑:用于生成 .out 答案文件 ---
    long long getStandardOutput(int n, long long k, const vector<int>& nums) {
        if (k <= 1) return 0;
        long long ans = 0, prod = 1;
        int l = 0;
        for (int r = 0; r < n; ++r) {
            prod *= nums[r];
            while (prod >= k && l <= r) {
                prod /= nums[l];
                l++;
            }
            ans += (r - l + 1);
        }
        return ans;
    }
    
    // --- 数据生成主逻辑 ---
    void make_data() {
        // 高质量随机数引擎
        mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
    
        for (int t = 1; t <= 10; ++t) {
            int n;
            long long k;
            vector<int> nums;
    
            // --- 针对性构造不同类型的测试点 ---
            if (t == 1) { // 边界:k 为 0,结果必为 0
                n = 10; k = 0;
                for(int i=0; i<n; ++i) nums.push_back(rng() % 100 + 1);
            }
            else if (t == 2) { // 边界:n 为 1,k 恰好等于元素值
                n = 1; k = 100;
                nums.push_back(100); // 应该输出 0,因为要求严格小于
            }
            else if (t == 3) { // 边界:所有元素都是 1,k 为 2
                n = 1000; k = 2;
                nums.assign(n, 1); // 结果应为 n*(n+1)/2
            }
            else if (t == 4) { // 特殊:所有元素都大于 k
                n = 100; k = 10;
                for(int i=0; i<n; ++i) nums.push_back(rng() % 50 + 11);
            }
            else if (t == 5) { // 特殊:交替出现大数和小数
                n = 500; k = 1000;
                for(int i=0; i<n; ++i) {
                    if (i % 2 == 0) nums.push_back(1);
                    else nums.push_back(2000);
                }
            }
            else if (t == 6) { // 中等规模随机
                n = 5000; k = 50000;
                for(int i=0; i<n; ++i) nums.push_back(rng() % 1000 + 1);
            }
            else if (t >= 7 && t <= 8) { // 满额规模随机
                n = 30000; k = 1000000;
                for(int i=0; i<n; ++i) nums.push_back(rng() % 1000 + 1);
            }
            else if (t == 9) { // 满额规模:全为 1,且 k 为最大
                n = 30000; k = 1000000;
                nums.assign(n, 1);
            }
            else { // 满额规模:构造大量符合条件的长区间
                n = 30000; k = 1000000;
                for(int i=0; i<n; ++i) nums.push_back(rng() % 5 + 1);
            }
    
            // --- 写入 .in 文件 ---
            string in_name = to_string(t) + ".in";
            ofstream fin(in_name);
            fin << n << " " << k << "\n";
            for (int i = 0; i < n; ++i) {
                fin << nums[i] << (i == n - 1 ? "" : " ");
            }
            fin << "\n";
            fin.close();
    
            // --- 调用标程生成 .out 答案文件 ---
            string out_name = to_string(t) + ".out";
            ofstream fout(out_name);
            fout << getStandardOutput(n, k, nums) << endl;
            fout.close();
    
            cout << "Generated Case " << t << ": n=" << n << ", k=" << k << endl;
        }
    }
    
    int main() {
        make_data();
        cout << "\nAll 10 test cases (1.in~10.out) generated successfully!" << endl;
        return 0;
    }
    

    测试点规划说明 (OJ 建设参考)

    测试点序号 NN 规模 KK 的特征 特征描述 考查意图
    1 10 0 k=0k=0 边界情况:严格小于 0 是不可能的。
    2 1 100 k=nums[0]k=nums[0] 边界情况:验证“严格小于”不含等于。
    3 1000 2 全为 1 极端情况:子数组数量达到最大值。
    4 100 10 nums[i]>knums[i] > k 考查算法在所有单元素都不成立时的表现。
    5 500 1000 巨数间隔 考查滑动窗口频繁开启和关闭。
    6 5000 50000 随机分布 中等规模逻辑验证。
    7-8 30000 1,000,000 满额随机 考察 O(n)O(n) 时间复杂度的稳定性。
    9 最大全 1 考察答案变量是否使用了 long long 防止溢出。
    10 密集分布 考察窗口长时间不收缩的情况。

    生成器设计要点总结

    1. 答案溢出预防: 当 N=30000N=30000 且子数组全符合条件时,答案约为 N(N+1)/24.5×108N(N+1)/2 \approx 4.5 \times 10^8。虽然 int 能存下,但如果 NN 扩大到 10510^5,则必须使用 long long。本生成器标程已统一使用 long long
    2. 乘法溢出预防: 在 prod *= nums[r] 这一步,由于 prod 之前一定小于 kk (10610^6),且 nums[r] 最大 10001000,乘积最大为 10910^9int 依然能存下,但标程使用了 long long 以应对未来数据范围扩大的风险。
    3. 非递归实现: 生成器与标程均采用简单的 forwhile 循环,时间复杂度 O(n)O(n),空间复杂度 O(n)O(n),生成 10 组数据总耗时小于 0.1 秒,绝无爆栈风险。
    4. 符合 NOI 规范: 输出格式严格遵循“行末无空格,文末有换行”的竞赛要求,方便直接在各类 OJ 系统上配置。
    • 0
      @ 2025-12-19 23:27:45

      这是“乘积小于 K 的子数组”问题的标准题解。在 NOI 竞赛中,这道题是考察双指针(滑动窗口)计数原理的入门级好题。

      一、 题解标准代码 (C++14)

      #include <iostream>
      #include <vector>
      
      using namespace std;
      
      /**
       * 核心思路:
       * 由于数组元素均为正整数,区间乘积随区间长度增加而具有单调性(不减)。
       * 我们维护一个滑动窗口 [l, r],确保窗口内所有元素的乘积 prod < k。
       */
      
      int main() {
          // 竞赛优化:加速输入输出
          ios::sync_with_stdio(false);
          cin.tie(0);
      
          int n;
          long long k; // k 的范围虽然在 10^6,但为了防止乘积计算溢出,建议使用 long long
          if (!(cin >> n >> k)) return 0;
      
          vector<int> nums(n);
          for (int i = 0; i < n; ++i) {
              cin >> nums[i];
          }
      
          // 特判:由于 nums[i] >= 1,如果 k <= 1,没有任何子数组乘积严格小于 1
          // 如果不加这个特判,下面的 while 循环逻辑可能会导致 l 超过 r 甚至越界
          if (k <= 1) {
              cout << 0 << endl;
              return 0;
          }
      
          long long ans = 0;    // 最终子数组计数
          long long prod = 1;   // 当前窗口内的乘积
          int l = 0;            // 左指针
      
          // 右指针从左向右移动
          for (int r = 0; r < n; ++r) {
              prod *= nums[r]; // 将右边界元素计入乘积
      
              // 易错点:当窗口积 >= k 时,必须缩小左边界
              // 这里的循环条件 prod >= k 保证了窗口缩小的正确性
              while (prod >= k && l <= r) {
                  prod /= nums[l];
                  l++;
              }
      
              /**
               * 关键计数逻辑:
               * 当窗口 [l, r] 满足 prod < k 时,所有以 r 为结尾的子数组:
               * [nums[r]], [nums[r-1], nums[r]], ..., [nums[l], ..., nums[r]]
               * 都必然满足乘积 < k。
               * 这些子数组的数量恰好是 r - l + 1。
               */
              ans += (r - l + 1);
          }
      
          cout << ans << endl;
      
          return 0;
      }
      

      二、 复杂度分析思考过程

      1. 时间复杂度分析

      • 指针移动:右指针 r00 遍历到 n1n-1,共移动 nn 次。
      • 窗口收缩:左指针 l 在整个 for 循环的过程中,只可能从左向右移动,绝不回退。因此 l 也最多移动 nn 次。
      • 计算开销:每次移动只涉及一次乘法或一次除法,都是 O(1)O(1) 的。
      • 结论:总时间复杂度为 O(n)O(n)
        • n=3×104n = 3 \times 10^4 的规模下,程序运行时间远小于 1 毫秒,表现极其优异。

      2. 空间复杂度分析

      • 存储开销:使用 vector<int> 存储 nn 个整数,占用空间为 O(n)O(n)
      • 额外空间:只使用了 l, r, ans, prod 等若干个变量,占用空间为 O(1)O(1)
      • 结论:总空间复杂度为 O(n)O(n),在 NOI 竞赛 128MB 的内存限制下绰绰有余。

      三、 算法优化与进阶建议

      1. 关于溢出的深度思考

        • 在本题中,k106k \le 10^6,且每次 prod 在乘以 nums[r] 之前必然是 <k< k 的。
        • 最大的可能中间结果是 (k-1) * nums[r],即 106×1000=10910^6 \times 1000 = 10^9。这个数值没有超过 int 的上限(约 2×1092 \times 10^9),但为了代码的鲁棒性(Robustness),在 NOI 赛场上处理乘法建议首选 long long
      2. 对数转换法(进阶技巧)

        • 如果题目给定的 kk 非常大,或者数组元素很大,导致连 long long 也会乘积溢出怎么办?
        • 优化方案:利用对数性质 log(a×b)=loga+logb\log(a \times b) = \log a + \log b
        • nums[i] < k 转换为 log(nums[i])<logk\sum \log(nums[i]) < \log k
        • 由于 log\log 后的数值是增加的,问题变成了“和小于某个定值”,可以使用前缀和 + 二分查找O(nlogn)O(n \log n) 时间内解决,且完全避免了溢出风险。
      3. 读题关键词回顾

        • “严格小于”:意味着 prod == k 时必须收缩左边界。
        • “正整数”:这是滑动窗口成立的基石。如果存在 00,乘积会突变为 00;如果存在负数,区间乘积不再具有单调性。

      教练点评:

      此题最精妙之处在于 ans += (r - l + 1) 这一行。很多同学会尝试枚举所有区间,那将导致 O(n2)O(n^2) 的复杂度。学会 “固定右端点,看左端点能向左延伸多远” 的计数技巧,是掌握线性模拟算法的关键。加油!

      • 1

      信息

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