2 条题解

  • 0
    @ 2025-12-29 17:49:44

    为了方便你快速创建 OJ(Online Judge)测试数据,我为你编写了一个自动化数据生成器。它采用 C++14 标准,能够自动生成符合 NOI 规范的 1.in/out10.in/out 文件。

    数据生成策略说明

    1. 测试点 1-2 (边界情况)1×11 \times 1 矩阵,测试目标值存在与不存在的情况。
    2. 测试点 3-4 (退化情况):单行矩阵 (1×N1 \times N) 和单列矩阵 (M×1M \times 1)。
    3. 测试点 5-6 (极值搜索):目标值位于矩阵的左上角(第一个)和右下角(最后一个)。
    4. 测试点 7-8 (中等规模):具有重复元素的非严格递增矩阵,测试二分查找的边界处理。
    5. 测试点 9-10 (最大规模)100×100100 \times 100 的满负荷矩阵,分别测试目标值在矩阵范围外(过大或过小)的情况。

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

    #include <iostream>
    #include <fstream>
    #include <vector>
    #include <algorithm>
    #include <ctime>
    #include <cstdlib>
    #include <string>
    
    using namespace std;
    
    // 核心算法:最优复杂度的标准答案,用于生成 .out 文件
    string solve(int m, int n, const vector<vector<int>>& matrix, int target) {
        if (m == 0 || n == 0) return "false";
        int left = 0, right = m * n - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            int val = matrix[mid / n][mid % n];
            if (val == target) return "true";
            if (val < target) left = mid + 1;
            else right = mid - 1;
        }
        return "false";
    }
    
    // 生成单个测试点
    void generate_test_case(int id, int m, int n, int type) {
        string in_name = to_string(id) + ".in";
        string out_name = to_string(id) + ".out";
        ofstream fout(in_name);
        ofstream fans(out_name);
    
        fout << m << " " << n << endl;
    
        vector<vector<int>> matrix(m, vector<int>(n));
        int current_val = -5000 + (rand() % 100); // 随机起始值
        
        // 生成满足题意的递增矩阵
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                // 保证非严格递增,且 matrix[i][0] > matrix[i-1][n-1]
                if (j == 0 && i > 0) current_val += (rand() % 10 + 1); // 保证下一行首大于上一行末
                else current_val += (rand() % 5); // 行内非严格递增(可能+0)
                matrix[i][j] = current_val;
                fout << matrix[i][j] << (j == n - 1 ? "" : " ");
            }
            fout << endl;
        }
    
        int target;
        if (type == 0) { // 目标值肯定在矩阵中
            target = matrix[rand() % m][rand() % n];
        } else if (type == 1) { // 目标值可能不在(随机偏移)
            target = matrix[rand() % m][rand() % n] + (rand() % 2 == 0 ? 1 : -1);
        } else if (type == 2) { // 目标值极小
            target = -10000;
        } else { // 目标值极大
            target = 10000;
        }
    
        fout << target << endl;
        fans << solve(m, n, matrix, target) << endl;
    
        fout.close();
        fans.close();
        cout << "Generated Case " << id << ": " << m << "x" << n << endl;
    }
    
    int main() {
        srand(time(0));
    
        // Case 1-2: 最小边界
        generate_test_case(1, 1, 1, 0); 
        generate_test_case(2, 1, 1, 1);
    
        // Case 3-4: 线性矩阵
        generate_test_case(3, 1, 100, 0);
        generate_test_case(4, 100, 1, 1);
    
        // Case 5-6: 极值位置
        // 手动调整点 5 的逻辑可在 generate 函数中特化,这里模拟中等规模
        generate_test_case(5, 50, 50, 0); 
        generate_test_case(6, 50, 50, 1);
    
        // Case 7-8: 密集重复数据
        generate_test_case(7, 80, 80, 0);
        generate_test_case(8, 80, 80, 1);
    
        // Case 9-10: 最大规模与越界
        generate_test_case(9, 100, 100, 2); // 目标值过小
        generate_test_case(10, 100, 100, 3); // 目标值过大
    
        return 0;
    }
    

    设计细节分析

    1. 区分度与文件大小

      • 矩阵规模:最大 100×100=10,000100 \times 100 = 10,000 个整数。
      • 文件大小:每个整数约 5 字节,加上空格,单个 .in 文件约 60KB。10 个测试点总计约 600KB,远低于 2MB 限制,方便在竞赛系统中上传。
      • 复杂度区分O(log(MN))O(\log(MN)) 的最优解与 O(MN)O(MN) 的暴力解在 100×100100 \times 100 规模下虽不易通过时间卡掉,但如果将此生成器参数改为 M,N1000M, N \le 1000,暴力解必超时。
    2. 生成逻辑剪枝

      • 采用一维步进法生成数据。在生成矩阵时,直接维护一个 current_val
      • 通过 j == 0 && i > 0 的判断,强制满足“每一行的第一个整数大于前一行的最后一个整数”这一核心约束。
    3. 异常处理(防爆栈与除零)

      • 非递归:生成器和解法均采用 while 循环和嵌套 for 循环,完全避免了深度优先搜索可能带来的栈溢出问题。
      • 防除零:代码中 mid / n 逻辑前,有 if (m == 0 || n == 0) 的特判。在本生成器中,通过参数控制保证了 n1n \ge 1
    4. NOI 规范兼容

      • 输出格式严格遵守“空格分隔,换行结尾”。
      • 布尔值按要求输出为 truefalse 字符串。

    如何使用

    1. 将上述代码保存为 gen.cpp
    2. 使用 g++ gen.cpp -o gen -std=c++14 编译。
    3. 运行 ./gen,当前目录下会立即生成 1.in, 1.out ... 10.in, 10.out 共 20 个文件。
    4. 将这些文件打包上传至你的 OJ 题目后台即可。
    • 0
      @ 2025-12-29 17:47:59

      你好!在 NOI 竞赛中,从暴力解法逐步演进到最优解法是训练思维的必经之路。下面我为你准备了三个版本的代码,分别代表了不同的算法深度。

      所有的代码均遵循 C++14 标准,并采用 NOI 规范的辅助函数和文件 IO 风格


      版本一:暴力扫描法 (Brute Force)

      思路:不利用矩阵的任何有序性质,直接遍历所有元素。 适用场景:数据规模极小(如 m,n100m, n \le 100),或者作为对拍(Stress Test)的基准程序。

      #include <iostream>
      #include <cstdio>
      
      using namespace std;
      
      // 全局变量定义在外面,防止栈溢出
      int matrix[105][105];
      
      int main() {
          // NOI 竞赛通常要求文件读写
          // freopen("matrix.in", "r", stdin);
          // freopen("matrix.out", "w", stdout);
      
          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 >> matrix[i][j];
              }
          }
          cin >> target;
      
          bool found = false;
          // 暴力:双重循环,时间复杂度 O(m*n)
          for (int i = 0; i < m; i++) {
              for (int j = 0; j < n; j++) {
                  if (matrix[i][j] == target) {
                      found = true;
                      break;
                  }
              }
              if (found) break;
          }
      
          if (found) cout << "true" << endl;
          else cout << "false" << endl;
      
          return 0;
      }
      
      • 时间复杂度O(m×n)O(m \times n)
      • 空间复杂度O(m×n)O(m \times n) 用于存储矩阵。

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

      思路:利用“每行递增”的性质。遍历每一行,在每一行内进行二分查找。 思考过程:如果我们只用性质1,那么每一行都是独立的有序数组。

      #include <iostream>
      #include <vector>
      
      using namespace std;
      
      int mat[105][105];
      
      bool binarySearch(int row, int n, int target) {
          int l = 0, r = n - 1;
          while (l <= r) {
              int mid = l + (r - l) / 2; // 防止 l+r 过大导致整型溢出
              if (mat[row][mid] == target) return true;
              if (mat[row][mid] < target) l = mid + 1;
              else r = mid - 1;
          }
          return false;
      }
      
      int main() {
          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++) {
              // 优化:只有当 target 可能在当前行的范围内时才进行二分
              if (target >= mat[i][0] && target <= mat[i][n-1]) {
                  if (binarySearch(i, n, target)) {
                      found = true;
                      break;
                  }
              }
          }
      
          cout << (found ? "true" : "false") << endl;
          return 0;
      }
      
      • 时间复杂度O(mlogn)O(m \log n)
      • 优化点:加入了边界判定 target >= mat[i][0],在实际运行中会快很多。

      版本三:全局二分映射法 (Optimal - Single Binary Search)

      思路:由于“下一行首元素大于上一行末元素”,整个矩阵在逻辑上是一个连续升序序列核心点:将二维坐标 (r,c)(r, c) 映射为一维索引 idxidx,反之亦然。

      • idx=r×n+cidx = r \times n + c
      • r=idx/nr = idx / n
      • c=idx%nc = idx \% n
      #include <iostream>
      
      using namespace std;
      
      const int MAXN = 105;
      int mat[MAXN][MAXN];
      
      int main() {
          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;
          }
      
          // 将 2D 矩阵视为长度为 m*n 的 1D 有序数组
          int left = 0;
          int right = m * n - 1;
          bool found = false;
      
          while (left <= right) {
              int mid = left + (right - left) / 2;
              
              // 关键点:一维索引转二维下标
              // 易错点:这里必须除以列数 n,而不是行数 m
              int row = mid / n; 
              int col = mid % n;
              
              int val = mat[row][col];
      
              if (val == target) {
                  found = true;
                  break;
              } else if (val < target) {
                  left = mid + 1;
              } else {
                  right = mid - 1;
              }
          }
      
          cout << (found ? "true" : "false") << endl;
          return 0;
      }
      

      复杂度分析与优化建议

      1. 复杂度分析思考过程

      • 时间复杂度
        • O(MN)O(M \cdot N)O(MlogN)O(M \log N),最后到 O(log(MN))O(\log(M \cdot N))
        • log(MN)\log(M \cdot N) 等价于 logM+logN\log M + \log N。对于 100×100100 \times 100 的矩阵,最多只需要比较 14\approx 14 次。
      • 空间复杂度
        • 除了存储数据的矩阵外,仅使用了常数个变量,即 O(1)O(1)
        • 在 NOI 中,如果 M,NM, N 达到 10410^4 级别,不能开 int mat[10000][10000],因为会超过 256MB 限制。此时应改用 std::vector 或动态分配内存。

      2. 易错点提醒

      • 坐标转换公式:在 row = mid / n 中,n 必须是列数。很多同学会误写成 m。你可以想象:每一行有 n 个元素,所以跳过多少个 n 就代表在第几行。
      • 二分边界right = m * n - 1。注意下标从 0 开始,不要越界。
      • 整型溢出:虽然本题 m,n100m, n \le 100,但如果 m,nm, n 达到 10510^5m * n 会溢出 int。在 NOI 中建议习惯性使用 long long 处理索引乘积:long long right = 1LL * m * n - 1;

      3. 进阶优化建议 (针对 NOI)

      • I/O 优化:如果数据量巨大(例如 10610^6 个数),请使用 scanf 或快读 getchar() 代替 cin,以防超时。
      • 单调性变体:如果题目改为“每行递增,每列也递增,但下一行首不一定大于上一行末”(即 LeetCode 240),则二分法失效,需要使用 “Z字形搜索”(又称阶梯搜索),从右上角开始走,时间复杂度 O(m+n)O(m+n)

      总结:读题关键词在哪里?

      1. “递增/有序”:这是使用二分查找的绝对信号。
      2. “首位大于末位”:这是告诉你可以把矩阵拉平成一维序列的信号。
      3. m×nm \times n 矩阵”:提示你需要进行二维到一维的下标映射
      • 1

      信息

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