2 条题解

  • 0
    @ 2026-1-6 20:08:37

    你好!作为教练,我为你准备了一个完整的测试数据生成与校验系统。该程序使用 C++14 编写,能够一键生成符合 NOI 规范的 10 组测试点(.in.out 文件)。

    数据生成策略说明

    为了确保测试点能有效区分不同水平的代码,我设计了以下 10 个测试点:

    1. 最小边界1×11 \times 1 网格。
    2. 全水域:没有任何陆地。
    3. 全陆地:整个网格就是一个大岛屿。
    4. 棋盘格:最大岛屿数量情况(每两个陆地互不相邻)。
    5. 稀疏随机:模拟零星岛屿。
    6. 稠密随机:模拟复杂且连通的大型岛屿。
    7. S型迷宫:极限路径长度,测试 DFS 递归深度。
    8. 长条形状1×3001 \times 300300×1300 \times 1 的极端比例。
    9. 大规模随机 A:接近 300×300300 \times 300 限制。
    10. 大规模随机 B:包含环形岛屿和空心岛屿的复杂结构。

    数据生成器完整代码 (C++)

    该代码会生成 1.in10.in,并同时调用内置的 BFS 算法生成对应的 1.out10.out

    #include <iostream>
    #include <fstream>
    #include <vector>
    #include <string>
    #include <queue>
    #include <ctime>
    #include <cstdlib>
    #include <sstream>
    
    using namespace std;
    
    // --- 算法核心:BFS 版标程 (用于生成 .out) ---
    // 使用 BFS 避免在大数据下爆栈
    int solve(int M, int N, vector<string>& grid) {
        if (M == 0 || N == 0) return 0;
        int count = 0;
        int dx[] = {-1, 1, 0, 0};
        int dy[] = {0, 0, -1, 1};
        
        vector<vector<bool>> vis(M, vector<bool>(N, false));
        
        for (int i = 0; i < M; i++) {
            for (int j = 0; j < N; j++) {
                if (grid[i][j] == '1' && !vis[i][j]) {
                    count++;
                    queue<pair<int, int>> q;
                    q.push({i, j});
                    vis[i][j] = true;
                    while (!q.empty()) {
                        pair<int, int> cur = q.front(); q.pop();
                        for (int k = 0; k < 4; k++) {
                            int nx = cur.first + dx[k];
                            int ny = cur.second + dy[k];
                            if (nx >= 0 && nx < M && ny >= 0 && ny < N && 
                                grid[nx][ny] == '1' && !vis[nx][ny]) {
                                vis[nx][ny] = true;
                                q.push({nx, ny});
                            }
                        }
                    }
                }
            }
        }
        return count;
    }
    
    // --- 数据生成逻辑 ---
    void make_data(int id) {
        string in_name = to_string(id) + ".in";
        string out_name = to_string(id) + ".out";
        ofstream fin(in_name);
        ofstream fout(out_name);
    
        int M, N;
        vector<string> grid;
    
        // 根据测试点编号设计不同场景
        switch(id) {
            case 1: M = 1, N = 1; grid = {"1"}; break;
            case 2: M = 5, N = 5; grid = vector<string>(M, string(N, '0')); break;
            case 3: M = 10, N = 10; grid = vector<string>(M, string(N, '1')); break;
            case 4: // 棋盘格 (最大岛屿数)
                M = 20, N = 20;
                for(int i=0; i<M; i++) {
                    string row = "";
                    for(int j=0; j<N; j++) row += ((i+j)%2 == 0 ? '1' : '0');
                    grid.push_back(row);
                }
                break;
            case 5: // 稀疏随机
                M = 50, N = 50;
                for(int i=0; i<M; i++) {
                    string row = "";
                    for(int j=0; j<N; j++) row += (rand()%10 == 0 ? '1' : '0');
                    grid.push_back(row);
                }
                break;
            case 6: // 极限路径 (S型)
                M = 300, N = 300;
                grid = vector<string>(M, string(N, '0'));
                for(int i=0; i<M; i++) {
                    if(i % 4 == 0) for(int j=0; j<N-2; j++) grid[i][j] = '1';
                    if(i % 4 == 1) grid[i][N-3] = '1';
                    if(i % 4 == 2) for(int j=2; j<N; j++) grid[i][j] = '1';
                    if(i % 4 == 3) grid[i][2] = '1';
                }
                break;
            case 7: // 细长条
                M = 1, N = 300;
                grid.push_back("");
                for(int j=0; j<N; j++) grid[0] += (j%2==0 ? '1' : '0');
                break;
            case 8: // 稠密随机
                M = 200, N = 200;
                for(int i=0; i<M; i++) {
                    string row = "";
                    for(int j=0; j<N; j++) row += (rand()%2 == 0 ? '1' : '0');
                    grid.push_back(row);
                }
                break;
            default: // 大规模随机
                M = 300, N = 300;
                for(int i=0; i<M; i++) {
                    string row = "";
                    for(int j=0; j<N; j++) row += (rand()%3 == 0 ? '1' : '0');
                    grid.push_back(row);
                }
        }
    
        // 写入输入文件
        fin << M << " " << N << "\n";
        for(const string& row : grid) fin << row << "\n";
    
        // 计算答案并写入输出文件
        fout << solve(M, N, grid) << "\n";
    
        cout << "Test Case " << id << " Generated. (Size: " << M << "x" << N << ")" << endl;
        fin.close();
        fout.close();
    }
    
    int main() {
        srand(time(0));
        cout << "Starting data generation for Number of Islands..." << endl;
        for (int i = 1; i <= 10; i++) {
            make_data(i);
        }
        cout << "All test cases (1-10) generated successfully." << endl;
        return 0;
    }
    

    数据说明与优化建议

    1. 数据范围控制

      • 本题最大数据为 300×300300 \times 300 的矩阵。
      • 单个 .in 文件大小约为 300×300=90,000300 \times 300 = 90,000 字节(约 90KB)。
      • 10 个文件总大小约 1MB,远低于 2MB 的要求,适合 OJ 快速下载和评测。
    2. 生成器健壮性

      • 防止爆栈:生成器中的 solve 函数使用了基于 std::queueBFS 算法。这确保了在处理测试点 6(长 S 型路径,深度 90,000)时,生成器本身不会崩溃。
      • I/O 速度:使用 std::string 和文件流,避免了频繁的字符操作。
    3. OJ 评测建议 (Coach's Tips)

      • 内存限制:建议设置为 256MB。虽然代码只需几 MB,但 NOI 规范通常给足内存。
      • 时间限制:建议设置为 1.0s。300×300300 \times 300O(N2)O(N^2) 算法在 1.0s 内绰绰有余。
      • 栈空间:如果学生使用 DFS 解题,请务必在评测机环境中设置 ulimit -s unlimited,否则测试点 6 会导致栈溢出(Runtime Error)。
    4. 读取技巧

      • 提示学生在读入字符矩阵时,注意过滤每行末尾的换行符。使用 scanf("%s", grid[i])cin >> s 是比较稳妥的做法。

    这套数据生成器可以帮助你建立一个高质量的练习题。如果有任何关于并查集或更复杂剪枝的测试需求,请随时告诉我!

    • 0
      @ 2026-1-6 20:06:53

      你好!作为教练,我将带你从最直观的搜索思路开始,逐步演进到竞赛中各种应对复杂环境的版本。在 NOI 竞赛中,稳健性速度同样重要。


      版本一:基础深度优先搜索 (DFS)

      这是最符合直觉的“扫雷式”解法。利用递归模拟“沉岛”过程。

      时间复杂度分析:

      • 每个点最多被访问一次(一旦变成 '0' 就不会再进递归)。
      • 复杂度:O(M×N)O(M \times N)。对于 300×300300 \times 300,运算量约为 9×1049 \times 10^4,远低于 10810^8 的秒级门槛。

      空间复杂度分析:

      • 递归深度最坏情况下为 O(M×N)O(M \times N)(例如全岛呈 S 型分布)。
      • 注意:NOIP/NOI 比赛中,默认栈空间通常与内存限制一致,但早期环境中栈空间较小。
      #include <iostream>
      #include <cstdio>
      
      using namespace std;
      
      const int MAXN = 305;
      char grid[MAXN][MAXN];
      int M, N;
      
      // 方向数组:上下左右
      int dx[] = {-1, 1, 0, 0};
      int dy[] = {0, 0, -1, 1};
      
      // DFS 沉岛函数
      void dfs(int x, int y) {
          // 易错点1:越界检查与终止条件
          if (x < 0 || x >= M || y < 0 || y >= N || grid[x][y] == '0') {
              return;
          }
      
          // 关键点:将当前陆地“沉没”,防止死循环和重复计数
          grid[x][y] = '0';
      
          // 启发式:向四个方向扩散
          for (int i = 0; i < 4; i++) {
              dfs(x + dx[i], y + dy[i]);
          }
      }
      
      int main() {
          // 优化 I/O 效率
          if (scanf("%d %d", &M, &N) != 2) return 0;
      
          for (int i = 0; i < M; i++) {
              scanf("%s", grid[i]); // 读入整行字符串,比逐字符读快
          }
      
          int count = 0;
          for (int i = 0; i < M; i++) {
              for (int j = 0; j < N; j++) {
                  if (grid[i][j] == '1') {
                      count++;
                      dfs(i, j); // 发现一个岛屿起点,并将其全部连通部分抹除
                  }
              }
          }
      
          printf("%d\n", count);
          return 0;
      }
      

      版本二:广度优先搜索 (BFS) —— 规避栈溢出风险

      如果题目数据范围扩大到 1000×10001000 \times 1000 以上,DFS 可能导致系统栈溢出。在 NOI 竞赛中,BFS(利用队列)通常更稳健。

      时间复杂度: O(M×N)O(M \times N)空间复杂度: O(min(M,N))O(\min(M, N)),队列中最多同时存在的节点数。

      #include <iostream>
      #include <cstdio>
      #include <queue>
      
      using namespace std;
      
      const int MAXN = 305;
      char grid[MAXN][MAXN];
      int M, N;
      
      struct Node {
          int x, y;
      };
      
      int dx[] = {-1, 1, 0, 0};
      int dy[] = {0, 0, -1, 1};
      
      void bfs(int sx, int sy) {
          queue<Node> q;
          q.push({sx, sy});
          grid[sx][sy] = '0'; // 关键点:入队时立即标记,防止节点重复入队导致内存耗尽 (MLE)
      
          while (!q.empty()) {
              Node cur = q.front();
              q.pop();
      
              for (int i = 0; i < 4; i++) {
                  int nx = cur.x + dx[i];
                  int ny = cur.y + dy[i];
      
                  if (nx >= 0 && nx < M && ny >= 0 && ny < N && grid[nx][ny] == '1') {
                      grid[nx][ny] = '0'; // 必须在这里修改状态
                      q.push({nx, ny});
                  }
              }
          }
      }
      
      int main() {
          if (scanf("%d %d", &M, &N) != 2) return 0;
          for (int i = 0; i < M; i++) scanf("%s", grid[i]);
      
          int count = 0;
          for (int i = 0; i < M; i++) {
              for (int j = 0; j < N; j++) {
                  if (grid[i][j] == '1') {
                      count++;
                      bfs(i, j);
                  }
              }
          }
          printf("%d\n", count);
          return 0;
      }
      

      版本三:最优扩展版本——并查集 (Union-Find)

      并查集在处理“连通块”计数时非常强大。它的优势在于:如果地图是动态变化的(例如不断有水变陆地),并查集可以实现近乎 O(1)O(1) 的实时查询。

      时间复杂度: O(M×Nα(MN))O(M \times N \cdot \alpha(MN))α\alpha 为阿克曼反函数,几乎可视为常数。 空间复杂度: O(M×N)O(M \times N),用于存储父亲节点数组。

      #include <iostream>
      #include <cstdio>
      #include <vector>
      
      using namespace std;
      
      const int MAXN = 305;
      char grid[MAXN][MAXN];
      int fa[MAXN * MAXN]; // 二维坐标压缩成一维存储
      int island_count = 0;
      
      // 并查集:查找父亲并进行路径压缩
      int find(int x) {
          if (fa[x] == x) return x;
          return fa[x] = find(fa[x]);
      }
      
      // 并查集:合并
      void unite(int x, int y) {
          int rootX = find(x);
          int rootY = find(y);
          if (rootX != rootY) {
              fa[rootX] = rootY;
              island_count--; // 每成功合并两个陆地,岛屿总数减 1
          }
      }
      
      int main() {
          int M, N;
          if (scanf("%d %d", &M, &N) != 2) return 0;
      
          // 初始化并查集
          for (int i = 0; i < M; i++) {
              scanf("%s", grid[i]);
              for (int j = 0; j < N; j++) {
                  if (grid[i][j] == '1') {
                      fa[i * N + j] = i * N + j;
                      island_count++;
                  }
              }
          }
      
          // 遍历网格,只向右和向下合并即可覆盖所有边
          for (int i = 0; i < M; i++) {
              for (int j = 0; j < N; j++) {
                  if (grid[i][j] == '1') {
                      // 易错点:合并前需检查右侧/下方是否也是陆地
                      if (j + 1 < N && grid[i][j + 1] == '1') unite(i * N + j, i * N + (j + 1));
                      if (i + 1 < M && grid[i + 1][j] == '1') unite(i * N + j, (i + 1) * N + j);
                  }
              }
          }
      
          printf("%d\n", island_count);
          return 0;
      }
      

      教练的复杂度优化建议与总结

      1. I/O 优化: 在 NOI 级别比赛中,如果数据量达到 10610^6 级,请使用 scanf/printfcin.tie(NULL)
      2. 空间优化:
        • 如果题目不允许修改 grid 数组,可以使用 bitset<MAXN> vis[MAXN] 代替 bool vis[MAXN][MAXN],空间可压缩至 1/8。
        • 本题中,直接原地修改 grid[i][j] = '0' 是最高效的技巧,无需额外 vis 数组。
      3. 时间优化:
        • 剪枝: 虽然本题 DFS/BFS 已经是最优线性复杂度,但在回溯类题目中,剪枝是金牌的关键。
        • 方向数组: 使用 dx[], dy[] 循环遍历邻居,比写四个 if 更不容易出错,且代码整洁。
      4. 易错警示:
        • BFS 标记时机: 记住,在 BFS 中,节点入队前就应该标记为已访问。如果在出队时才标记,同一个节点可能会被周围四个邻居分别发现并重复入队,导致队列爆炸。

      这道题是典型的建模思维训练:将二维矩阵想象成一张图,将搜索算法想象成填充工具。希望你能在草稿纸上多画几次“沉岛”的过程,加深对“状态迁移”的理解!加油!

      • 1

      信息

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