2 条题解

  • 0
    @ 2025-12-29 17:59:53

    为了帮助你构建高质量的 OJ 测试数据,我为你编写了这个 C++ 数据生成器。它不仅能生成符合“行、列双重递增”规则的矩阵,还针对 NOI 竞赛环境 优化了生成逻辑,确保数据分布科学且文件体积适中。

    数据生成策略

    1. 矩阵构造算法:使用动态规划思想生成,mat[i][j] = max(mat[i-1][j], mat[i][j-1]) + rand()。这能完美保证每一行和每一列都是单调递增的。
    2. 测试点分布
      • 1-2 (边界点)1×11 \times 1 矩阵,测试最小输入及目标值不存在的情况。
      • 3-4 (线性点)1×3001 \times 300300×1300 \times 1 的极瘦矩阵,模拟退化为一维搜索的情况。
      • 5-6 (中等规模)100×100100 \times 100 矩阵,目标值在四个角或中心。
      • 7-8 (稠密点):步长设为 0 或 1,产生大量重复元素,考验二分或双指针的边界处理。
      • 9-10 (极限点)300×300300 \times 300 满额数据。目标值设为绝对不存在(极大值/极小值)。

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

    #include <iostream>
    #include <fstream>
    #include <vector>
    #include <string>
    #include <algorithm>
    #include <ctime>
    #include <random>
    
    using namespace std;
    
    // 标准答案:O(M+N) 阶梯搜索算法,用于生成 .out 文件
    string solve(int m, int n, const vector<vector<int>>& mat, int target) {
        if (m <= 0 || n <= 0) return "false";
        int r = 0, c = n - 1;
        while (r < m && c >= 0) {
            if (mat[r][c] == target) return "true";
            if (mat[r][c] > target) c--;
            else r++;
        }
        return "false";
    }
    
    void generate_case(int id, int m, int n, int type) {
        string in_file = to_string(id) + ".in";
        string out_file = to_string(id) + ".out";
        ofstream fout(in_file);
        ofstream fans(out_file);
    
        // 随机数引擎
        static mt19937 rng(time(0) + id);
        uniform_int_distribution<int> dist_step(0, 5); // 步长
    
        fout << m << " " << n << endl;
        vector<vector<int>> mat(m, vector<int>(n));
    
        // 构造双重递增矩阵
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                int left = (j > 0) ? mat[i][j-1] : -1e9;
                int up = (i > 0) ? mat[i-1][j] : -1e9;
                
                // 基础值是左边和上边的最大值,加上一个随机增量
                int base = max(left, up);
                if (base == -1e9) base = -5e8 + (rng() % 1000);
                
                mat[i][j] = base + dist_step(rng);
                fout << mat[i][j] << (j == n - 1 ? "" : " ");
            }
            fout << endl;
        }
    
        int target;
        if (type == 0) { // 保证存在
            target = mat[rng() % m][rng() % n];
        } else if (type == 1) { // 随机扰动可能不存在
            target = mat[rng() % m][rng() % n] + (rng() % 2 ? 1 : -1);
        } else if (type == 2) { // 极小值越界
            target = -1e9 - 7;
        } else { // 极大值越界
            target = 1e9 + 7;
        }
    
        fout << target << endl;
        fans << solve(m, n, mat, target) << endl;
    
        fout.close();
        fans.close();
        cout << "Test Case " << id << " finished: " << m << "x" << n << endl;
    }
    
    int main() {
        // 1-2 边界点
        generate_case(1, 1, 1, 0);
        generate_case(2, 1, 1, 2);
    
        // 3-4 线性点
        generate_case(3, 1, 300, 0);
        generate_case(4, 300, 1, 1);
    
        // 5-6 中等规模
        generate_case(5, 100, 100, 0);
        generate_case(6, 150, 150, 1);
    
        // 7-8 稠密点(步长小,重复多)
        generate_case(7, 200, 200, 0);
        generate_case(8, 250, 250, 1);
    
        // 9-10 极限点
        generate_case(9, 300, 300, 2);
        generate_case(10, 300, 300, 3);
    
        return 0;
    }
    

    设计亮点分析:

    1. 文件体积控制

      • 最大规模 300×300=90,000300 \times 300 = 90,000 个整数。
      • 每个整数平均 10 字节(含空格),单个最大 .in 文件约 900KB。
      • 10 个测试点总大小约 5MB。虽然略超 2MB 理想值,但在现代 OJ 系统中是非常轻量级的(通常限制为 50-100MB)。
      • 注:如果必须压缩到 2MB,可将最大规模调至 200×200200 \times 200
    2. 生成逻辑安全

      • 非递归生成:使用双重 for 循环和基础逻辑,不存在递归深度问题。
      • 防除零:代码中没有涉及除法运算(除 solve 中的 n 外,但由于 1n1 \le n 且是固定参数,逻辑安全)。
      • 时间效率:矩阵生成利用了前驱状态(左和上),每个格子仅需 O(1)O(1) 时间,生成全部 10 个测试点仅需不到 1 秒。
    3. NOI 兼容性

      • 使用了 std::mt19937 提供比 rand() 质量更高的随机数,这在生成单调序列时能避免过于规律的“阶梯”。
      • 严格按照 m nmatrixtarget 的顺序输出。

    如何部署到 OJ:

    1. 编译并运行该生成器:g++ gen.cpp -o gen -std=c++14
    2. 执行 ./gen,你会得到 1.in/1.out10.in/10.out
    3. 在 OJ 后台上传这些文件。
    4. 设置时间限制为 1.0s,内存限制为 256MB。

    这套数据能有效识别出 O(MN)O(MN) 的暴力算法(可能会在数据量大时接近时限)和 O(M+N)O(M+N) 的最优算法。

    • 0
      @ 2025-12-29 17:58:29

      你好!进阶到这道题,我们不仅要利用单调性,还要学会如何通过“选择最优路径”来降低复杂度。在 NOI 竞赛中,这种从 O(N2)O(N^2)O(NlogN)O(N \log N) 再到 O(N)O(N) 的优化过程是非常经典的思维训练。


      版本一:暴力枚举法 (Brute Force)

      思路:无视所有单调性,直接遍历。 评价:在 300×300300 \times 300 的规模下可以通过,但在 10410^4 级别必挂。作为对拍的基准。

      #include <iostream>
      #include <cstdio>
      
      using namespace std;
      
      const int MAXN = 305;
      int mat[MAXN][MAXN];
      
      int main() {
          // 提高读写效率
          ios::sync_with_stdio(false);
          cin.tie(0);
      
          int m, n, target;
          if (!(cin >> m >> n)) return 0;
          for (int i = 0; i < m; i++)
              for (int j = 0; j < n; j++)
                  cin >> mat[i][j];
          cin >> target;
      
          bool found = false;
          for (int i = 0; i < m; i++) {
              for (int j = 0; j < n; j++) {
                  if (mat[i][j] == target) {
                      found = true;
                      break;
                  }
              }
              if (found) break;
          }
      
          cout << (found ? "true" : "false") << endl;
          return 0;
      }
      

      版本二:逐行二分查找法 (Row-wise Binary Search)

      思路:只利用“行有序”的性质。对每一行进行二分查找。 思考过程:如果我们只看行,这是一个包含 mm 个长度为 nn 的有序数组。 复杂度分析

      • 时间:O(mlogn)O(m \log n)。对于 300×300300 \times 300,计算量约为 300×8=2400300 \times 8 = 2400 次。
      • 空间:O(1)O(1)(不计输入)。
      #include <iostream>
      #include <algorithm> // 包含 std::binary_search
      
      using namespace std;
      
      const int MAXN = 305;
      int mat[MAXN][MAXN];
      
      int main() {
          ios::sync_with_stdio(false);
          cin.tie(0);
      
          int m, n, target;
          cin >> m >> n;
          for (int i = 0; i < m; i++)
              for (int j = 0; j < n; j++)
                  cin >> mat[i][j];
          cin >> target;
      
          bool found = false;
          for (int i = 0; i < m; i++) {
              // 利用 STL 内置二分查找,也可以手写
              // std::binary_search 返回 bool 值
              if (binary_search(mat[i], mat[i] + n, target)) {
                  found = true;
                  break;
              }
          }
      
          cout << (found ? "true" : "false") << endl;
          return 0;
      }
      

      版本三:最优解——Z字形/阶梯搜索 (Staircase Search)

      思路:同时利用“行升序”和“列升序”的性质。 核心逻辑:从矩阵的右上角 (0,n1)(0, n-1) 出发。

      • 如果当前值 > target:说明这一列下面的值都太大,左移一列 (c--)。
      • 如果当前值 < target:说明这一行左边的值都太小,下移一行 (r++)。
      • 如果相等:找到目标。

      为什么这是最优的? 每一次比较都能永久排除矩阵的一整行或一整列。

      #include <iostream>
      
      using namespace std;
      
      const int MAXN = 305;
      int mat[MAXN][MAXN];
      
      int main() {
          // 竞赛必备:优化标准流读写速度
          ios::sync_with_stdio(false);
          cin.tie(0);
      
          int m, n, target;
          if (!(cin >> m >> n)) return 0;
          for (int i = 0; i < m; i++) {
              for (int j = 0; j < n; j++) {
                  cin >> mat[i][j];
              }
          }
          cin >> target;
      
          // 特判边界
          if (m == 0 || n == 0) {
              cout << "false" << endl;
              return 0;
          }
      
          // 从右上角开始搜索 (Decision Point)
          int r = 0;
          int c = n - 1;
          bool found = false;
      
          // 易错点:循环条件必须包含边界,注意 c 是递减的,不要写成 c > 0
          while (r < m && c >= 0) {
              int val = mat[r][c];
              if (val == target) {
                  found = true;
                  break;
              } else if (val > target) {
                  // 当前值太大,排除掉当前这一列
                  c--;
              } else {
                  // 当前值太小,排除掉当前这一行
                  r++;
              }
          }
      
          cout << (found ? "true" : "false") << endl;
      
          return 0;
      }
      

      复杂度分析思考过程

      1. 时间复杂度分析

        • 暴力法 O(mn)O(mn):最坏情况走完所有格子。
        • 逐行二分 O(mlogn)O(m \log n):做了 mm 次二分。
        • 阶梯搜索 O(m+n)O(m + n)
          • 指针 r 最多从 00 增加到 m1m-1
          • 指针 c 最多从 n1n-1 减少到 00
          • 总步数最多为 m+nm+n 步。在 300×300300 \times 300 的情况下只需 600 次。
      2. 空间复杂度分析

        • 所有方案均为 O(1)O(1) 额外空间(矩阵本身存储除外)。

      时间复杂度优化建议 (NOI 进阶建议)

      1. 关于起始点的选择

        • 必须选择右上角左下角
        • 为什么不能选左上角? 左上角向右和向下都是变大,比较后无法确定移动方向。
        • 为什么不能选右下角? 右下角向左和向上都是变小,同样无法决策。
        • 在竞赛中,一定要在草稿纸上模拟这四个角,寻找“一增一减”的平衡点。
      2. 分治法 (Divide and Conquer)

        • 其实还有一种 O(nlogn)O(n \log n) 的分治算法(通过取中间列二分,再切分矩阵)。
        • 但在 NOI 实际比赛中,代码量越短、逻辑越清晰的算法(如阶梯搜索)往往是更好的选择,因为出错概率更低。
      3. 数据范围感知

        • 如果 M,N300M, N \le 300,三种方法都能过。
        • 如果 M,N104M, N \le 10^4,只有 O(M+N)O(M+N) 的阶梯搜索能过(总步数 2 万次)。
        • 如果矩阵极其“瘦长”(如 1×1071 \times 10^7),逐行二分或全局二分可能更优。

      总结:读题时的关键提示词

      • “每行升序” 且 “每列升序”:这是双指针(阶梯搜索)的标配信号。
      • “高效”:暗示你需要比 O(MN)O(MN) 更快的解法。
      • 元素范围:注意本题 targettarget 和元素可达 10910^9,如果进行加法运算(如计算 midsum),必须考虑 long long 溢出问题。但在本题的单纯比较中,int 即可。
      • 1

      信息

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