2 条题解

  • 0
    @ 2025-12-16 18:21:52

    这是一份包含完整数据生成标程解题功能的 C++ 工具代码。

    使用说明

    1. 功能:编译并运行此代码后,它会在当前目录下自动生成 1.in / 1.out10.in / 10.out 共20个文件。
    2. 覆盖度:数据点设计完全贴合 NOI/NOIP 考查标准,涵盖了:
      • 边界情况N<4N < 4
      • 特殊数值:全0、全重复、正负混合。
      • 溢出检测:极大值(和超过 int)与极小值。
      • 压力测试N=200N=200 且解非常多(稠密解)的情况。
    3. 安全性:使用了 mt19937_64 生成高质量 64 位随机数,避免了 rand() 在大范围数据下的分布不均问题。

    数据生成器代码 (C++14)

    /**
     * Title: NOI 4Sum Data Generator & Solver
     * Author: OI Coach
     * Standard: C++14
     * Description: Generates 10 sets of .in and .out files for the 4Sum problem.
     */
    
    #include <iostream>
    #include <vector>
    #include <algorithm>
    #include <fstream>
    #include <string>
    #include <random>
    #include <chrono>
    #include <set>
    
    using namespace std;
    
    typedef long long ll;
    
    // 全局随机数引擎
    mt19937_64 rng(chrono::steady_clock::now().time_since_epoch().count());
    
    // 生成指定范围内的随机整数 [min, max]
    ll random_ll(ll min, ll max) {
        if (min > max) return min;
        uniform_int_distribution<ll> dist(min, max);
        return dist(rng);
    }
    
    // ==========================================
    // 标准题解逻辑 (用于生成 .out 文件)
    // ==========================================
    void solve(string in_filename, string out_filename) {
        ifstream cin(in_filename);
        ofstream cout(out_filename);
        
        if (!cin.is_open() || !cout.is_open()) {
            cerr << "Error opening files: " << in_filename << " / " << out_filename << endl;
            return;
        }
    
        int n;
        ll target;
        if (!(cin >> n >> target)) return;
    
        vector<ll> nums(n);
        for (int i = 0; i < n; ++i) {
            cin >> nums[i];
        }
    
        // 核心逻辑开始
        sort(nums.begin(), nums.end());
        
        if (n < 4) return; // 无输出
    
        for (int i = 0; i < n - 3; ++i) {
            if (i > 0 && nums[i] == nums[i - 1]) continue;
            if (nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target) break;
            if (nums[i] + nums[n - 3] + nums[n - 2] + nums[n - 1] < target) continue;
    
            for (int j = i + 1; j < n - 2; ++j) {
                if (j > i + 1 && nums[j] == nums[j - 1]) continue;
                if (nums[i] + nums[j] + nums[j + 1] + nums[j + 2] > target) break;
                if (nums[i] + nums[j] + nums[n - 2] + nums[n - 1] < target) continue;
    
                int left = j + 1;
                int right = n - 1;
    
                while (left < right) {
                    ll sum = nums[i] + nums[j] + nums[left] + nums[right];
                    if (sum == target) {
                        cout << nums[i] << " " << nums[j] << " " << nums[left] << " " << nums[right] << "\n";
                        while (left < right && nums[left] == nums[left + 1]) left++;
                        while (left < right && nums[right] == nums[right - 1]) right--;
                        left++;
                        right--;
                    } else if (sum < target) {
                        left++;
                    } else {
                        right--;
                    }
                }
            }
        }
        // 核心逻辑结束
        
        cin.close();
        cout.close();
        cout << "Generated: " << out_filename << endl;
    }
    
    // ==========================================
    // 数据生成逻辑
    // ==========================================
    void generate_data(int case_id) {
        string in_file = to_string(case_id) + ".in";
        string out_file = to_string(case_id) + ".out";
        
        ofstream fout(in_file);
        
        int n;
        ll target;
        vector<ll> nums;
    
        // 根据测试点 ID 定制数据策略
        switch (case_id) {
            case 1: 
                // 边界测试:N < 4
                n = 3; 
                target = 10;
                for(int i=0; i<n; ++i) nums.push_back(random_ll(-10, 10));
                break;
    
            case 2: 
                // 基础测试:N = 4,恰好有解
                n = 4;
                nums = {1, 5, -2, 8};
                target = 1 + 5 - 2 + 8; // = 12
                break;
    
            case 3: 
                // 基础测试:N = 4,无解
                n = 4;
                nums = {1, 2, 3, 4};
                target = 100;
                break;
    
            case 4: 
                // 特殊值:全 0
                n = 20;
                target = 0;
                for(int i=0; i<n; ++i) nums.push_back(0);
                break;
    
            case 5: 
                // 重复元素处理:大量重复数字
                n = 50;
                target = 8;
                for(int i=0; i<n; ++i) nums.push_back(2);
                // nums 中全是 2,结果应该只有一行 2 2 2 2
                break;
    
            case 6: 
                // 常规随机小数据:N=50,范围小
                n = 50;
                for(int i=0; i<n; ++i) nums.push_back(random_ll(-100, 100));
                // 随机选4个数的和作为target,保证大概率有解
                target = nums[0] + nums[10] + nums[20] + nums[30]; 
                break;
    
            case 7: 
                // 溢出测试 (MAX):正数大值
                // 数值接近 10^9,4个数相加会爆 int
                n = 100;
                for(int i=0; i<n; ++i) nums.push_back(random_ll(900000000, 1000000000));
                // 构造解
                target = nums[0] + nums[1] + nums[2] + nums[3];
                break;
    
            case 8: 
                // 溢出测试 (MIN):负数大值
                // 数值接近 -10^9
                n = 100;
                for(int i=0; i<n; ++i) nums.push_back(random_ll(-1000000000, -900000000));
                target = nums[0] + nums[1] + nums[2] + nums[3];
                break;
    
            case 9: 
                // 极限数据:N=200,大范围随机,可能有解也可能无解
                n = 200;
                target = random_ll(-1000000, 1000000);
                for(int i=0; i<n; ++i) nums.push_back(random_ll(-1000000000, 1000000000));
                break;
    
            case 10: 
                // 压力测试:N=200,稠密解 (解非常多)
                // 范围很小 [-10, 10],导致大量组合和为 target
                n = 200;
                target = 0;
                for(int i=0; i<n; ++i) nums.push_back(random_ll(-10, 10));
                break;
        }
    
        // 写入输入文件
        fout << n << " " << target << endl;
        for (int i = 0; i < n; ++i) {
            fout << nums[i] << (i == n - 1 ? "" : " ");
        }
        fout << endl;
        fout.close();
    
        cout << "Generated: " << in_file << endl;
    
        // 调用解题逻辑生成输出文件
        solve(in_file, out_file);
    }
    
    int main() {
        // 生成 1 到 10 个测试点
        for (int i = 1; i <= 10; ++i) {
            generate_data(i);
        }
        cout << "All 10 test cases generated successfully." << endl;
        return 0;
    }
    

    必读:测试点详细设计思路

    1. TestCase 1 (边界): N=3N=3。测试程序是否在 N<4N<4 时能正确退出,不进行非法下标访问。
    2. TestCase 2 (基础): N=4N=4 且恰好有解。测试最基本的逻辑正确性。
    3. TestCase 3 (基础): N=4N=4 但无解。测试无解时的输出(应为空)。
    4. TestCase 4 (特殊值): 全为0,目标为0。测试去重逻辑,标准输出应仅为一行 0 0 0 0
    5. TestCase 5 (重复): 全为2,目标为8。测试大量重复元素下的去重和指针移动效率。
    6. TestCase 6 (常规): N=50N=50,混合正负数。通过选取数组中实际存在的4个数之和作为 Target,保证了至少有一组解,用于常规逻辑验证。
    7. TestCase 7 (溢出): 元素在 10910^9 级别。4×1094 \times 10^9 超过了 32位有符号整数范围。测试代码是否使用了 long long 存储中间结果。
    8. TestCase 8 (溢出): 元素在 109-10^9 级别。测试负数溢出情况。
    9. TestCase 9 (综合): N=200N=200,全范围随机。模拟随机情况,测试剪枝效率。
    10. TestCase 10 (高压): N=200N=200,但数值范围极小 [10,10][-10, 10]。这会产生极其大量的合法四元组(稠密图效果),这是最考验程序去重逻辑效率和 I/O 速度的测试点。
    • 0
      @ 2025-12-16 18:19:51

      你好!作为金牌教练,我为你准备了这份完全符合 NOIP/NOI 竞赛标准的 C++14 题解代码。

      这份代码不仅通过了算法逻辑,还特别强调了在赛场上必须注意的数据类型溢出边界检查常数级优化(剪枝),这些往往是决定你能否拿满分的细节。

      NOIP 标准题解代码 (C++14)

      /**
       * 题目: [NOI 模拟] 四数之和 (4Sum)
       * 语言: C++14
       * 算法: 排序 + 双指针 + 剪枝优化
       * 时间复杂度: O(N^3)
       * 空间复杂度: O(log N) (排序栈空间)
       */
      
      #include <iostream>
      #include <vector>
      #include <algorithm>
      
      using namespace std;
      
      // 建议使用 long long 替代 int,防止加法运算溢出
      // 题目中虽说单个元素是 10^9,但在计算和时 4 * 10^9 会超过 int (约 2.1 * 10^9) 的范围
      typedef long long ll;
      
      int main() {
          // 【NOI 技巧】关闭同步流,加速 cin/cout,防止大数据量 IO 超时
          ios::sync_with_stdio(false);
          cin.tie(nullptr);
      
          int n;
          ll target;
          
          // 输入检查,增强鲁棒性
          if (!(cin >> n >> target)) return 0;
      
          vector<ll> nums(n);
          for (int i = 0; i < n; ++i) {
              cin >> nums[i];
          }
      
          // 【关键点 1】排序
          // 排序是使用双指针和去重逻辑的前提
          sort(nums.begin(), nums.end());
      
          // 特判:如果数据量不足 4 个,直接结束,避免下标越界
          if (n < 4) return 0;
      
          // 第一层循环:固定第一个数 nums[i]
          for (int i = 0; i < n - 3; ++i) {
              // 【关键点 2】第一层去重
              // 如果当前数和上一个数相同,跳过,因为上一个数已经覆盖了所有组合
              if (i > 0 && nums[i] == nums[i - 1]) continue;
      
              // 【优化技巧 1】最小性剪枝 (Min Pruning)
              // 如果当前数 + 紧跟的三个最小数 > target,说明后续无论怎么选都大了,直接结束本层循环
              // 这里的加法必须用 long long
              if (nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target) break;
      
              // 【优化技巧 2】最大性剪枝 (Max Pruning)
              // 如果当前数 + 最大的三个数 < target,说明当前数太小了,配不上 target,跳过看下一个更大的 nums[i]
              if (nums[i] + nums[n - 3] + nums[n - 2] + nums[n - 1] < target) continue;
      
              // 第二层循环:固定第二个数 nums[j]
              for (int j = i + 1; j < n - 2; ++j) {
                  // 【关键点 3】第二层去重
                  // 注意范围限定:j > i + 1,即 j 不是刚开始的那一次
                  if (j > i + 1 && nums[j] == nums[j - 1]) continue;
      
                  // 【优化技巧 3】第二层剪枝
                  // 逻辑同上,针对 nums[j] 进行判断
                  if (nums[i] + nums[j] + nums[j + 1] + nums[j + 2] > target) break;
                  if (nums[i] + nums[j] + nums[n - 2] + nums[n - 1] < target) continue;
      
                  // 【关键点 4】双指针法 (Two Pointers)
                  // 将 O(N^2) 的暴力搜索转化为 O(N) 的线性扫描
                  int left = j + 1;
                  int right = n - 1;
      
                  while (left < right) {
                      ll sum = nums[i] + nums[j] + nums[left] + nums[right];
      
                      if (sum == target) {
                          // 找到答案,直接输出
                          cout << nums[i] << " " << nums[j] << " " << nums[left] << " " << nums[right] << "\n";
      
                          // 【关键点 5】双指针去重
                          // 记录当前值,指针跳过所有重复值,防止输出重复的四元组
                          while (left < right && nums[left] == nums[left + 1]) left++;
                          while (left < right && nums[right] == nums[right - 1]) right--;
      
                          // 移动到下一个不同的位置
                          left++;
                          right--;
                      } 
                      else if (sum < target) {
                          // 和太小,左指针右移变大
                          left++;
                      } 
                      else {
                          // 和太大,右指针左移变小
                          right--;
                      }
                  }
              }
          }
      
          return 0;
      }
      

      复杂度分析与思考过程

      作为教练,我希望你能不仅会写代码,还能在纸上推导出它的性能表现。

      1. 时间复杂度分析

      • 推导过程

        • 排序std::sort 的时间复杂度是 O(NlogN)O(N \log N)
        • 循环结构
          • 第一层循环 i0N
          • 第二层循环 ji+1N
          • 内层是一个 while 双指针循环,leftright 从两端向中间逼近,每个元素最多被访问一次,复杂度为 O(N)O(N)
        • 总计:总时间复杂度约为 i=0Nj=iNO(N)O(N3)\sum_{i=0}^{N} \sum_{j=i}^{N} O(N) \approx O(N^3)
      • 数据规模验证

        • 题目给定的 N200N \le 200
        • 最坏计算次数:2003=8,000,000200^3 = 8,000,000 (8百万次)。
        • 竞赛标准:通常 C++ 程序 1秒钟可以处理 10810^8 (1亿次) 运算。
        • 结论8×1061088 \times 10^6 \ll 10^8,即使在不进行剪枝的最坏情况下,该算法也能轻松在 100ms 内通过,绝对安全

      2. 空间复杂度分析

      • 分析
        • 我们只使用了几个变量 (i, j, left, right, sum),这是 O(1)O(1)
        • 但是,std::sort 并不是完全原地的,C++ 的 introsort 实现通常需要 O(logN)O(\log N) 的栈空间来进行递归。
        • 如果题目要求存储答案再输出(而不是像上面代码那样直接打印),还需要额外的空间存储答案。
      • 结论:辅助空间复杂度 O(logN)O(\log N)(用于排序)。

      时间复杂度优化建议(剪枝详解)

      NN 较小(如 200)时,O(N3)O(N^3) 足够。但如果 NN 增大到 1000 左右,或者针对特殊构造的数据,我们需要利用剪枝 (Pruning) 来大幅降低实际运行时间。

      代码中已经集成了四个核心剪枝点:

      1. Min Check (下界剪枝)

        • if (nums[i] + nums[i+1] + nums[i+2] + nums[i+3] > target) break;
        • 思考:当前的 nums[i] 已经是能选到的最小的数了,如果连紧跟它后面最小的三个数加起来都超过了 target,那后面无论怎么选只会更大,完全没有必要继续尝试 i 及其后面的循环了。直接 break 掉第一层循环。
      2. Max Check (上界剪枝)

        • if (nums[i] + nums[n-3] + nums[n-2] + nums[n-1] < target) continue;
        • 思考:当前的 nums[i] 加上数组里最大的三个数都还不到 target,说明这个 nums[i] 太小了,没有任何组合能满足条件。应该直接 continue 尝试更大的 nums[i+1]

      同理,这两个剪枝策略也应用在了第二层 j 的循环中。

      实战效果:加上这几行剪枝代码,对于随机数据,算法的期望运行时间会大幅缩短,甚至接近 O(N2)O(N^2) 的表现,这是体现竞赛选手“常数优化”能力的关键点。

      • 1

      信息

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