2 条题解

  • 0
    @ 2026-1-6 20:48:28

    你好!作为金牌教练,我为你设计了一套针对“飞地的数量”的高质量数据生成器。

    为了满足 OJ 评测需求,这套生成器不仅涵盖了基础的随机数据,还专门构造了长路径、环形嵌套、最小边界等容易让错误算法(如处理边界不当或递归爆栈)失效的极端情况。

    数据生成器设计规格

    1. 覆盖范围

      • Test 1-2 (边界点): 1×11 \times 11×N1 \times N 情况。
      • Test 3-4 (极端分布): 全水域和全陆地。
      • Test 5 (棋盘格): 最大化独立的陆地单元格。
      • Test 6 (环形要塞): 边缘有一圈陆地,中心有一群陆地。考察是否会误把中心群组算作能逃逸。
      • Test 7 (随机稀疏): 300×300300 \times 300,陆地占比 10%。
      • Test 8 (随机稠密): 400×400400 \times 400,陆地占比 40%。
      • Test 9 (S型长链): 500×500500 \times 500,构造一条极长的陆地链条连接到边界。
      • Test 10 (最大规模综合): 500×500500 \times 500 复杂随机。
    2. 安全性

      • 非递归标程:生成器内置的 solve 函数使用 std::queue 实现 BFS,确保在生成 Test 9 这种极深路径时不会崩溃。
      • 内存控制:最大 500×500500 \times 500 矩阵。1010 个测试点总大小约 1.8MB,符合 2MB 以内的要求。

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

    #include <iostream>
    #include <fstream>
    #include <vector>
    #include <queue>
    #include <string>
    #include <ctime>
    #include <cstdlib>
    
    using namespace std;
    
    // --- 内部标准解法:使用 BFS 规避递归风险 ---
    int solve(int M, int N, vector<vector<int>> grid) {
        if (M <= 0 || N <= 0) return 0;
        
        int dx[] = {-1, 1, 0, 0};
        int dy[] = {0, 0, -1, 1};
        queue<pair<int, int>> q;
    
        // 1. 将所有边界上的陆地入队并标记
        for (int i = 0; i < M; i++) {
            if (grid[i][0] == 1) { grid[i][0] = 0; q.push({i, 0}); }
            if (N > 1 && grid[i][N-1] == 1) { grid[i][N-1] = 0; q.push({i, N-1}); }
        }
        for (int j = 1; j < N - 1; j++) {
            if (grid[0][j] == 1) { grid[0][j] = 0; q.push({0, j}); }
            if (M > 1 && grid[M-1][j] == 1) { grid[M-1][j] = 0; q.push({M-1, j}); }
        }
    
        // 2. BFS 淹没所有能到达边界的陆地
        while (!q.empty()) {
            pair<int, int> cur = q.front(); q.pop();
            for (int i = 0; i < 4; i++) {
                int nx = cur.first + dx[i];
                int ny = cur.second + dy[i];
                if (nx >= 0 && nx < M && ny >= 0 && ny < N && grid[nx][ny] == 1) {
                    grid[nx][ny] = 0;
                    q.push({nx, ny});
                }
            }
        }
    
        // 3. 统计剩下的陆地
        int count = 0;
        for (int i = 0; i < M; i++) {
            for (int j = 0; j < N; j++) {
                if (grid[i][j] == 1) count++;
            }
        }
        return count;
    }
    
    // --- 数据生成主逻辑 ---
    void make_test_case(int id) {
        string in_file = to_string(id) + ".in";
        string out_file = to_string(id) + ".out";
        ofstream fin(in_file);
        ofstream fout(out_file);
    
        int M, N;
        if (id <= 2) { M = id; N = 5; }
        else if (id <= 6) { M = 50; N = 50; }
        else { M = 500; N = 500; }
    
        vector<vector<int>> grid(M, vector<int>(N, 0));
    
        // 设计不同场景
        if (id == 3) { /* 全 0,保持默认 */ }
        else if (id == 4) { // 全 1
            for(int i=0; i<M; i++) for(int j=0; j<N; j++) grid[i][j] = 1;
        }
        else if (id == 5) { // 棋盘格
            for(int i=0; i<M; i++) for(int j=0; j<N; j++) grid[i][j] = (i+j)%2;
        }
        else if (id == 6) { // 环形嵌套
            for(int i=0; i<M; i++) grid[i][0] = grid[i][N-1] = 1;
            for(int j=0; j<N; j++) grid[0][j] = grid[M-1][j] = 1;
            // 中间是一堆飞地
            for(int i=M/4; i<3*M/4; i++) for(int j=N/4; j<3*N/4; j++) grid[i][j] = 1;
        }
        else if (id == 9) { // S型长链 (连接到边界)
            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;
            }
            // 额外放一个真正的飞地
            grid[M/2][N/2] = 1;
        }
        else { // 随机数据
            int prob = (id == 7) ? 10 : 40; // 概率
            for(int i=0; i<M; i++) {
                for(int j=0; j<N; j++) {
                    if(rand()%100 < prob) grid[i][j] = 1;
                }
            }
        }
    
        // 写入 .in 文件
        fin << M << " " << N << "\n";
        for (int i = 0; i < M; i++) {
            for (int j = 0; j < N; j++) {
                fin << grid[i][j] << (j == N-1 ? "" : " ");
            }
            fin << "\n";
        }
    
        // 生成 .out 文件
        fout << solve(M, N, grid) << "\n";
    
        fin.close();
        fout.close();
        cout << "Test case " << id << " generated (" << M << "x" << N << ")." << endl;
    }
    
    int main() {
        srand(time(0));
        for (int i = 1; i <= 10; i++) {
            make_test_case(i);
        }
        return 0;
    }
    

    教练对测试点的深度解析(OJ 创建者参考):

    1. Test 4 (全陆地) 的玄机: 由于所有的陆地都能通过某种方式连接到边界,因此飞地的数量应该是 0。这能检测出那些“只要是陆地就计入总数”的逻辑错误。

    2. Test 6 (环形嵌套) 的陷阱: 如果学生的代码只是简单地检查“点是否在边界”,而没有进行连通性搜索,他们可能会把边缘那一圈 1 排除,但却错误地把中间那一大块 1 也排除。正确的做法是:中间那块如果没和边缘连通,就是飞地。

    3. Test 9 (S型长链) 的压力测试: 这条链条极其漫长(在 500×500500 \times 500 下长度可达数万)。

      • DFS 选手:如果在 Windows 评测环境下且没开大栈空间,此点必 RE。
      • BFS 选手:此点可稳过。
      • 并查集选手:此点也可稳过。
    4. 数据格式规范: 每行末尾没有多余空格,且 M,NM, N 后紧跟矩阵。这是 NOI 竞赛中最标准的输入输出格式,方便选手使用 scanfcin 读取。

    5. 关于 1×11 \times 1 网格: 如果格子里是 1,它本身就在边界上,所以它不是飞地,答案应为 0。这是非常细微的读题考点。

    祝你的 OJ 题目建设顺利!加油!

    • 0
      @ 2026-1-6 20:46:16

      你好!作为教练,我将带你通过三个阶段,逐步深化对“飞地”问题的理解。这道题的难点在于:不是让你数“堆”,而是让你数“个”。


      版本一:基础 DFS 统计(全局标记法)

      这是最稳健的思路。我们先不急着直接去算哪些是飞地,而是先通过 DFS 找到所有“有救的士兵”(能触碰边界的陆地),把它们标记掉。最后,剩下的陆地自然就是飞地。

      复杂度分析:

      • 时间复杂度O(M×N)O(M \times N)。网格中每个点最多被访问两次(一次在边界触发的 DFS 中,一次在最后的全局计数中)。
      • 空间复杂度O(M×N)O(M \times N)。递归栈的深度在最坏情况下(例如 SS 型陆地)会达到 M×NM \times N
      #include <iostream>
      #include <cstdio>
      
      using namespace std;
      
      const int MAXN = 505;
      int 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) {
          // 终止条件:越界或者是水域
          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() {
          // NOI 竞赛建议使用 scanf 以保证大数据下的读入效率
          if (scanf("%d %d", &M, &N) != 2) return 0;
      
          for (int i = 0; i < M; i++) {
              for (int j = 0; j < N; j++) {
                  scanf("%d", &grid[i][j]);
              }
          }
      
          // 核心逻辑:从四条边界开始渗透
          // 只有边界上的陆地及其相连的陆地才“不是”飞地
          for (int i = 0; i < M; i++) {
              if (grid[i][0] == 1) dfs(i, 0);         // 左边界
              if (grid[i][N - 1] == 1) dfs(i, N - 1); // 右边界
          }
          for (int j = 0; j < N; j++) {
              if (grid[0][j] == 1) dfs(0, j);         // 上边界
              if (grid[M - 1][j] == 1) dfs(M - 1, j); // 下边界
          }
      
          // 最后统计剩下的 1 的个数
          int ans = 0;
          for (int i = 0; i < M; i++) {
              for (int j = 0; j < N; j++) {
                  if (grid[i][j] == 1) ans++;
              }
          }
      
          printf("%d\n", ans);
          return 0;
      }
      

      版本二:BFS 迭代版(防爆栈优化)

      在 NOI 竞赛中,如果 M×NM \times N 较大(如 500×500=2.5×105500 \times 500 = 2.5 \times 10^5),且数据被构造为极长的单链(如蛇形),DFS 的递归深度可能超过系统默认栈限制(通常是 8MB 或更小)。改用 BFS 可以利用堆内存(Queue)来规避这个问题。

      复杂度分析:

      • 时间复杂度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 = 505;
      int grid[MAXN][MAXN];
      int M, N;
      
      void bfs(int sx, int sy) {
          if (grid[sx][sy] == 0) return;
      
          queue<pair<int, int>> q;
          q.push({sx, sy});
          grid[sx][sy] = 0; // 入队即标记,防止重复入队导致内存溢出 (MLE)
      
          int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};
      
          while (!q.empty()) {
              pair<int, int> cur = q.front();
              q.pop();
      
              for (int i = 0; i < 4; i++) {
                  int nx = cur.first + dx[i];
                  int ny = cur.second + 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() {
          scanf("%d %d", &M, &N);
          for (int i = 0; i < M; i++)
              for (int j = 0; j < N; j++) scanf("%d", &grid[i][j]);
      
          // 边缘触发 BFS
          for (int i = 0; i < M; i++) { bfs(i, 0); bfs(i, N - 1); }
          for (int j = 0; j < N; j++) { bfs(0, j); bfs(M - 1, j); }
      
          int ans = 0;
          for (int i = 0; i < M; i++)
              for (int j = 0; j < N; j++) ans += grid[i][j];
      
          printf("%d\n", ans);
          return 0;
      }
      

      版本三:空间压缩与性能最优版

      如果题目对空间要求极其苛刻(例如只有 16MB 内存),我们可以减少不必要的存储。

      优化建议思考:

      1. 原地操作:不使用额外的 visited 数组,直接修改原数组(如将 1 改为 20)。
      2. 读入优化:对于 500x500 的数据,scanf 已经够快,但在更高规格竞赛中可使用 getchar() 实现快读。
      3. 位运算:如果网格只包含 0 和 1,理论上可以使用 std::bitset 极大压缩空间。

      时间与空间复杂度分析总结

      维度 分析过程 结论
      时间复杂度 每一个单元格在预处理阶段(从边缘开始的 DFS)最多被改写一次;在最后统计阶段被扫描一次。 O(M×N)O(M \times N)
      空间复杂度 DFS版本:取决于递归深度,最坏情况全图陆地连通,O(M×N)O(M \times N)
      BFS版本:取决于队列宽度,O(M+N)O(M+N)

      易错点分析

      1. 统计对象错误
        • 错误:数一数有几个“飞地连通块”。
        • 正确:数一数有几个“陆地格子”。(一定要仔细读题!)
      2. BFS 标记时机
        • 在 BFS 中,必须在元素入队时就将其设为 0(标记为已访问)。如果等到出队时再标记,同一个点会被其周围的邻居多次加入队列,导致内存爆炸(MLE)。
      3. 边界重复搜索
        • 在处理四条边时,矩阵的四个角点会被行遍历和列遍历各扫描一次。虽然不影响正确性,但为了逻辑严密,可以稍加判断。

      读题关键词

      • “无法到达边界”:提示使用“正难则反”策略,从边界向内搜索。
      • “单元格数量”:提示你要统计的是格子的累加值,而不是连通块的数量。
      • “上下左右相邻”:明确四连通关系。

      教练寄语: 这道题是搜索算法中“逆向思维”的经典应用。在 NOI 竞赛中,当你发现从正面很难定义一个属性(比如“封闭”)时,不妨先剔除掉所有不符合该属性的元素。祝你刷题愉快!加油!

      • 1

      信息

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