3 条题解

  • 0
    @ 2025-12-23 14:23:39

    在 NOI 竞赛中,很多同学喜欢使用 cincout,因为它们的语法更简洁,且不容易像 scanf 那样因为写错格式控制符(如 %d 写成 %f)而导致错误。

    但是,原生的 cin/cout 速度极慢,在处理 10510^5 以上的数据时极易导致超时(TLE)。因此,如果要将代码改为 cin 版本,必须包含 I/O 优化流控制

    以下是适配 NOIP/CSP 风格的 cin 优化版代码:


    一、 优化版 cin 题解代码 (C++14)

    /*
     * Problem: Next Greater Element I (Stream IO Version)
     * Algorithm: Monotonic Stack + Array Mapping
     * Standard: C++14 (NOIP/CSP Compatible)
     */
    #include <iostream>
    #include <vector>
    #include <stack>
    
    using namespace std;
    
    // 根据题目数据范围设定常量
    const int MAXV = 10005; // 值域范围
    const int MAXN = 1005;  // 数组长度范围
    
    int nums1[MAXN];
    int nums2[MAXN];
    int next_greater[MAXV];
    
    int main() {
        /**
         * 【核心优化 1】
         * ios::sync_with_stdio(false);
         * 作用:取消 C++ 标准流与 C 标准库(stdio)的同步。
         * 优化后,cin 的效率可以接近 scanf。
         * 注意:使用此行后,绝对不能在同一个程序中混合使用 cin/cout 和 scanf/printf。
         */
        ios::sync_with_stdio(false);
    
        /**
         * 【核心优化 2】
         * cin.tie(nullptr); 
         * 作用:解除 cin 与 cout 的绑定。
         * 默认情况下,cin 会在读取前刷新 cout 的缓冲区,解除绑定可进一步提升速度。
         */
        cin.tie(nullptr);
    
        int n, m;
        // 使用 cin 进行读取
        cin >> n >> m;
    
        for (int i = 0; i < n; ++i) {
            cin >> nums1[i];
        }
    
        // 初始化映射表
        for (int i = 0; i < MAXV; ++i) {
            next_greater[i] = -1;
        }
    
        stack<int> s;
        for (int i = 0; i < m; ++i) {
            cin >> nums2[i];
            
            // 单调栈逻辑保持不变
            while (!s.empty() && nums2[i] > s.top()) {
                next_greater[s.top()] = nums2[i];
                s.pop();
            }
            s.push(nums2[i]);
        }
    
        // 输出部分
        for (int i = 0; i < n; ++i) {
            cout << next_greater[nums1[i]];
            
            /**
             * 【核心优化 3】
             * 使用 '\n' 替代 endl。
             * endl 会强制刷新缓冲区(flush),在循环中会导致巨大的性能开销。
             * 在竞赛中,除非特殊需要,一律使用 '\n'。
             */
            if (i == n - 1) cout << '\n';
            else cout << ' ';
        }
    
        return 0;
    }
    

    二、 关键点与避坑指南

    1. 关于 sync_with_stdio(false) 的副作用

    • 混用禁止:一旦加入这行代码,你的程序里就不能再出现 printf, scanf, getchar, putchar。如果混用,输出顺序可能会发生错乱,导致 WA。
    • 不可回退:这是一个全局开关,一旦关闭无法中途开启。

    2. endl vs '\n'

    • 在 NOI 评测环境中,endl 的执行效率极低。
    • 实验数据:在输出 10610^6 个整数的情况下,使用 endl 的耗时可能是使用 '\n'10 倍以上。请务必养成使用 '\n' 的习惯。

    3. 为什么不用 std::vector

    • 虽然 std::vector 很方便,但在 NOI 竞赛中,如果能确定数据上限(如本题的 10051005),使用静态数组不仅内存访问更快,而且能有效避免动态内存分配带来的常数抖动。

    三、 总结:读题时的“IO 预警”

    当你通过关键词识别出题目类型后,就要决定使用哪种 IO 方式:

    1. 数据量 < 10^5:随意使用,cin 不加优化也能过。
    2. 数据量 10^5 ~ 5*10^5:必须使用 scanf/printf优化的 cin/cout
    3. 数据量 > 10^6 或 时限 < 0.5s:建议直接上 Fast IO(快读快写) 以保万全。

    这道题的数据量只有 10001000,属于第一种情况,但作为职业选手,你应该养成只要写 cin 就必加优化的好习惯。

    • 0
      @ 2025-12-23 11:15:40

      为了方便你创建 OJ(Online Judge)测试数据,我编写了一个全自动的数据生成器。它会根据 NOI 竞赛的标准,自动生成 10 组测试点(1.in/out10.in/out),并涵盖了从基础边界到极限规模的所有情况。

      该生成器集成了解题逻辑(标准代码),确保生成的 .out 文件与 .in 文件完全对应。

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

      #include <iostream>
      #include <vector>
      #include <algorithm>
      #include <random>
      #include <stack>
      #include <string>
      #include <fstream>
      #include <set>
      #include <chrono>
      
      using namespace std;
      
      // --- 核心算法:用于生成 .out 文件 ---
      vector<int> solve(int n, int m, const vector<int>& nums1, const vector<int>& nums2) {
          int next_greater[10005];
          for (int i = 0; i < 10005; ++i) next_greater[i] = -1;
      
          stack<int> s;
          for (int x : nums2) {
              while (!s.empty() && x > s.top()) {
                  next_greater[s.top()] = x;
                  s.pop();
              }
              s.push(x);
          }
      
          vector<int> res;
          for (int x : nums1) {
              res.push_back(next_greater[x]);
          }
          return res;
      }
      
      // --- 数据构造工具 ---
      mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
      
      int get_rand(int l, int r) {
          return uniform_int_distribution<int>(l, r)(rng);
      }
      
      void write_test_case(int case_id, int n, int m, vector<int> nums1, vector<int> nums2) {
          string in_name = to_string(case_id) + ".in";
          string out_name = to_string(case_id) + ".out";
      
          // 写 .in 文件
          ofstream fin(in_name);
          fin << n << " " << m << "\n";
          for (int i = 0; i < n; ++i) fin << nums1[i] << (i == n - 1 ? "" : " ");
          fin << "\n";
          for (int i = 0; i < m; ++i) fin << nums2[i] << (i == m - 1 ? "" : " ");
          fin << "\n";
          fin.close();
      
          // 写 .out 文件
          vector<int> ans = solve(n, m, nums1, nums2);
          ofstream fout(out_name);
          for (int i = 0; i < n; ++i) fout << ans[i] << (i == n - 1 ? "" : " ");
          fout << "\n";
          fout.close();
          
          cout << "Test Case " << case_id << " generated: n=" << n << ", m=" << m << endl;
      }
      
      int main() {
          // 设置值域
          const int MAX_VAL = 10000;
      
          for (int t = 1; t <= 10; ++t) {
              int n, m;
              vector<int> nums2_pool;
              
              // --- 针对不同测试点构造不同特性的数据 ---
              if (t == 1) { // 边界:极小规模
                  n = 1, m = 1;
              } else if (t == 2) { // 边界:n=m
                  n = 10, m = 10;
              } else if (t == 3) { // 特殊:严格递增 (所有都有答案)
                  n = 50, m = 100;
              } else if (t == 4) { // 特殊:严格递减 (所有都为-1)
                  n = 50, m = 100;
              } else if (t == 5) { // 特殊:先减后增(V型)
                  n = 100, m = 200;
              } else if (t <= 8) { // 一般:随机中等规模
                  n = get_rand(100, 500);
                  m = get_rand(500, 1000);
              } else { // 极限:最大规模
                  n = 1000, m = 1000;
              }
      
              // 生成互不相同的 nums2
              set<int> used;
              while (nums2_pool.size() < (size_t)m) {
                  int val = get_rand(0, MAX_VAL);
                  if (used.find(val) == used.end()) {
                      used.insert(val);
                      nums2_pool.push_back(val);
                  }
              }
      
              // 针对特殊情况排序
              if (t == 3) sort(nums2_pool.begin(), nums2_pool.end());
              if (t == 4) sort(nums2_pool.rbegin(), nums2_pool.rend());
              if (t == 5) {
                  sort(nums2_pool.begin(), nums2_pool.end());
                  vector<int> v_shape;
                  for(int i=0; i<m; i+=2) v_shape.push_back(nums2_pool[m-1-i]); // 放入大的
                  for(int i=1; i<m; i+=2) v_shape.insert(v_shape.begin(), nums2_pool[m-1-i]);
                  nums2_pool = v_shape;
              }
      
              // 从 nums2 中随机抽取 n 个数作为 nums1 (保证是子集)
              vector<int> nums1_indices;
              for (int i = 0; i < m; ++i) nums1_indices.push_back(i);
              shuffle(nums1_indices.begin(), nums1_indices.end(), rng);
              
              vector<int> nums1;
              for (int i = 0; i < n; ++i) {
                  nums1.push_back(nums2_pool[nums1_indices[i]]);
              }
              // nums1 内部顺序可以打乱
              shuffle(nums1.begin(), nums1.end(), rng);
      
              write_test_case(t, n, m, nums1, nums2_pool);
          }
      
          return 0;
      }
      

      二、 测试点分布说明

      测试点 nn mm 特性说明
      1 最小边界:只有一个元素。
      2 10 全集测试nums1nums2 的全排列。
      3 50 100 单调性测试nums2 严格递增,除最后一个外都有答案。
      4 单调性测试nums2 严格递减,全部答案应为 -1
      5 100 200 波动测试nums2 呈 “V” 字型,考察栈的多次弹出逻辑。
      6-7 300+ 600+ 随机分布:考察代码处理乱序数据的鲁棒性。
      8 500 1000 较大规模:考察哈希映射或映射表的效率。
      9-10 1000 最大规模n,mn, m 均达到上限,值域覆盖 01040 \sim 10^4

      三、 优化与注意事项

      1. 非递归安全:生成器和解题逻辑均采用循环+栈(迭代法),即使数据规模扩大到 10610^6,也不会出现爆栈风险。
      2. 唯一性保证:使用了 std::set 和随机打乱,确保了题目要求的“不包含重复元素”和“子集关系”。
      3. 时间复杂度
        • 生成逻辑:O(MlogM)O(M \log M)O(M)O(M)
        • 求解逻辑:O(N+M)O(N + M)
        • 总时间:生成 10 个文件在毫秒级完成,完全不会超时。
      4. 随机性:使用了 mt19937 高性能随机数引擎,比传统的 rand() 随机分布更均匀,避免在 OJ 测试中被“骗分”代码通过。

      四、 如何使用

      1. 复制上述代码到 gen.cpp
      2. 编译:g++ gen.cpp -o gen -std=c++14
      3. 运行:./gen
      4. 生成的 1.in ~ 10.out 即可直接上传至 OJ 平台。
      • 0
        @ 2025-12-23 11:13:29

        作为教练,我需要提醒你:在 NOI 竞赛中,代码的稳定性和执行效率同等重要。这道题虽然在 LeetCode 上是简单题,但它所蕴含的“单调栈”思想是解决许多 hard 题(如:修筑祭坛、最大矩形面积)的核心。

        下面是符合 NOIP/CSP 规范的标准程序,采用了单调栈 + 快速映射的方案。


        一、 复杂度分析思考过程

        1. 暴力法的瓶颈: 若对 nums1 中的每个元素都在 nums2 中线性扫描,时间复杂度为 O(n×m)O(n \times m)。当 n,mn, m 达到 10510^5 级别时就会超时。
        2. 单调栈的引入: 我们需要一种“预处理”手段。当我们遍历 nums2 时,如果当前元素比栈顶大,说明栈顶元素找到了它的“下一个更大者”。
          • 时间复杂度nums2 的每个元素仅进栈一次、出栈一次,复杂度为 O(m)O(m)。最后查询 nums1 复杂度为 O(n)O(n)。总计 O(n+m)O(n + m)
        3. 空间复杂度: 我们需要一个栈 O(m)O(m) 和一个存储映射关系的数组或哈希表。由于题目给定数值范围较小(01040 \dots 10^4),直接开数组作为映射表是最稳妥的,复杂度 O(max_val)O(\max\_val)

        二、 时间复杂度优化建议

        1. 容器选择:在竞赛中,std::unordered_map 的常数项较大且在极端数据下可能发生哈希冲突导致退化。由于本题值域只有 10410^4直接使用数组 ans_map[10005] 代替哈希表,可以将常数级时间优化到极致。
        2. 输入输出:若数据量达到 10610^6 级别,建议使用 scanf/printf 或快读(Fast I/O),cin/cout 即使解绑在某些评测机上依然较慢。

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

        /*
         * Problem: Next Greater Element I
         * Algorithm: Monotonic Stack (单调栈) + Hash Mapping
         * Standard: C++14 (NOIP/CSP Compatible)
         */
        #include <iostream>
        #include <vector>
        #include <stack>
        #include <cstdio>
        
        using namespace std;
        
        // 根据题目数据范围设定常量
        const int MAXV = 10005; // 值域范围
        const int MAXN = 1005;  // 数组长度范围
        
        int nums1[MAXN];
        int nums2[MAXN];
        int next_greater[MAXV]; // 核心:映射表,存储值 v 对应的下一个更大元素
        
        int main() {
            // 1. 竞赛建议:如果需要文件 IO,请取消下面两行注释
            // freopen("next.in", "r", stdin);
            // freopen("next.out", "w", stdout);
        
            int n, m;
            if (scanf("%d %d", &n, &m) == EOF) return 0;
        
            // 读取第一个数组
            for (int i = 0; i < n; ++i) {
                scanf("%d", &nums1[i]);
            }
        
            // 2. 预处理映射表,初始化为 -1(表示默认没有更大元素)
            for (int i = 0; i < MAXV; ++i) {
                next_greater[i] = -1;
            }
        
            // 3. 单调栈核心逻辑
            stack<int> s;
            for (int i = 0; i < m; ++i) {
                scanf("%d", &nums2[i]);
                
                // 如果当前元素比栈顶大,说明栈顶元素找到了“大哥”
                while (!s.empty() && nums2[i] > s.top()) {
                    // 易错点:映射表的下标是栈顶的“值”而不是索引
                    next_greater[s.top()] = nums2[i];
                    s.pop(); // 已找到答案,弹出
                }
                
                // 当前元素入栈,等待它的“大哥”出现
                s.push(nums2[i]);
            }
        
            // 4. 根据 nums1 的顺序查询结果并输出
            for (int i = 0; i < n; ++i) {
                // 直接从映射表中 $O(1)$ 获取结果
                printf("%d%c", next_greater[nums1[i]], (i == n - 1 ? '\n' : ' '));
            }
        
            return 0;
        }
        

        四、 关键点与易错点注释(金牌教练提醒)

        1. 映射逻辑

          • 很多同学容易混淆“下标”和“数值”。题目明确 nums1nums2 里的数值互不相同,所以我们可以直接用 next_greater[数值] 来存答案。
          • 如果数值范围很大(如 10910^9),则必须使用 std::unordered_map<int, int> 或者先进行离散化处理。
        2. 单调性的维护

          • 在这个场景下,栈内元素必须保持单调递减(从栈底到栈顶)。
          • 一旦出现一个“破坏者”(比栈顶大的元素),就要不断执行 pop 操作,直到栈恢复单调性。
        3. 边界处理

          • 遍历结束后,留在栈里的元素意味着它们右侧没有比它们更大的数。在初始化映射表为 -1 时,我们已经优雅地处理了这种情况。
        4. NOI 风格细节

          • 注意输出格式:最后一个数字后通常不建议有多余空格,但在大多数评测系统中不影响。上面的代码用了 (i == n - 1 ? '\n' : ' ') 来处理空格。
          • 使用 scanf 提高读取效率。
        • 1

        信息

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