2 条题解

  • 0
    @ 2026-1-8 17:25:40

    为了方便您构建 OJ(Online Judge)测试数据,我编写了一个符合 NOI 竞赛标准的测试数据生成器。该生成器采用 C++14 编写,包含了完善的 BFS 求解逻辑以生成 .out 文件,并设计了 10 个具有代表性的测试点。

    数据生成器设计思路

    1. 覆盖性:涵盖了最小迷宫、无出口迷宫、入口在边界、长路径迷宫以及 100×100100 \times 100 的最大规模随机迷宫。
    2. 安全性:求解器使用非递归的 std::queue 实现 BFS,有效防止在大规模蛇形路径下出现爆栈。
    3. 合规性:输入输出格式严格遵守题目描述。文件大小极小(10个点总计约 150KB),远低于 2MB 限制。

    C++ 数据生成器源代码

    #include <iostream>
    #include <fstream>
    #include <vector>
    #include <string>
    #include <queue>
    #include <random>
    #include <ctime>
    #include <algorithm>
    
    using namespace std;
    
    // 方向数组
    const int dr[] = {-1, 1, 0, 0};
    const int dc[] = {0, 0, -1, 1};
    
    // --- 标准 BFS 求解器 ---
    int solve(int m, int n, const vector<string>& maze, int r0, int c0) {
        queue<pair<int, int>> q;
        vector<vector<int>> dist(m, vector<int>(n, -1));
    
        q.push({r0, c0});
        dist[r0][c0] = 0;
    
        while (!q.empty()) {
            pair<int, int> curr = q.front();
            q.pop();
    
            for (int i = 0; i < 4; i++) {
                int nr = curr.first + dr[i];
                int nc = curr.second + dc[i];
    
                if (nr >= 0 && nr < m && nc >= 0 && nc < n && maze[nr][nc] == '.' && dist[nr][nc] == -1) {
                    dist[nr][nc] = dist[curr.first][curr.second] + 1;
                    
                    // 判断是否为出口
                    if (nr == 0 || nr == m - 1 || nc == 0 || nc == n - 1) {
                        if (!(nr == r0 && nc == c0)) {
                            return dist[nr][nc];
                        }
                    }
                    q.push({nr, nc});
                }
            }
        }
        return -1;
    }
    
    // --- 随机工具 ---
    mt19937 rng(time(0));
    int randRange(int l, int r) {
        return uniform_int_distribution<int>(l, r)(rng);
    }
    
    // --- 测试点生成逻辑 ---
    void make_test(int t) {
        string in_name = to_string(t) + ".in";
        string out_name = to_string(t) + ".out";
        ofstream fin(in_name);
        
        int m, n, r0, c0;
        vector<string> maze;
    
        if (t == 1) { // 极小边界: 入口在边界且唯一
            m = 1, n = 2;
            maze = {".+"};
            r0 = 0, c0 = 0;
        } else if (t == 2) { // 入口在边界且被墙包围
            m = 3, n = 3;
            maze = {"+++", "+.+", "+++"};
            r0 = 1, n = 3, maze[1][0] = '.'; // 入口在边缘
            r0 = 1, c0 = 0;
        } else if (t == 3) { // 普通 10x10 随机
            m = 10, n = 10;
            for(int i=0; i<m; i++) {
                string row = "";
                for(int j=0; j<n; j++) row += (randRange(0, 10) < 3 ? '+' : '.');
                maze.push_back(row);
            }
            r0 = randRange(1, m-2), c0 = randRange(1, n-2);
            maze[r0][c0] = '.';
        } else if (t == 4) { // 无出口迷宫 (边界全是墙)
            m = 50, n = 50;
            for(int i=0; i<m; i++) {
                string row(n, '.');
                if (i == 0 || i == m-1) fill(row.begin(), row.end(), '+');
                row[0] = row[n-1] = '+';
                maze.push_back(row);
            }
            r0 = 25, c0 = 25;
        } else if (t == 5) { // 蛇形长路径
            m = 100, n = 100;
            maze.assign(m, string(n, '+'));
            for(int i=0; i<m; i+=2) {
                if((i/2)%2 == 0) for(int j=0; j<n-1; j++) maze[i][j] = '.';
                else for(int j=1; j<n; j++) maze[i][j] = '.';
                if(i+1 < m) {
                    if((i/2)%2 == 0) maze[i+1][n-2] = '.';
                    else maze[i+1][1] = '.';
                }
            }
            r0 = 0, c0 = 0; 
        } else { // 最大规模随机
            m = 100, n = 100;
            for(int i=0; i<m; i++) {
                string row = "";
                for(int j=0; j<n; j++) row += (randRange(0, 100) < 25 ? '+' : '.');
                maze.push_back(row);
            }
            r0 = randRange(0, m-1), c0 = randRange(0, n-1);
            maze[r0][c0] = '.';
        }
    
        // 输出输入文件
        fin << m << " " << n << "\n";
        for(const string& s : maze) fin << s << "\n";
        fin << r0 << " " << c0 << "\n";
        fin.close();
    
        // 计算答案并输出
        int res = solve(m, n, maze, r0, c0);
        ofstream fout(out_name);
        fout << res << endl;
        fout.close();
        
        cout << "Testcase " << t << " generated. Size: " << m << "x" << n << " Result: " << res << endl;
    }
    
    int main() {
        for (int i = 1; i <= 10; i++) {
            make_test(i);
        }
        return 0;
    }
    

    测试点详细说明

    测试点 M×NM \times N 规模 场景描述 考查点
    1 1×21 \times 2 极小迷宫,入口即边界 出口判定逻辑(入口不可作为出口)
    2 3×33 \times 3 入口在边界且不连通 基础搜索及 -1 的判定
    3 10×1010 \times 10 普通中小型迷宫 基础 BFS 逻辑
    4 50×5050 \times 50 边界全部封死 复杂不连通图处理
    5 100×100100 \times 100 蛇形走廊结构 考查最长路径下的 BFS 效率
    6 棋盘格分布墙壁 考查搜索路径的选择性
    7-9 高密度/低密度随机 综合压力测试与性能考查
    10 大型稀疏迷宫 考查多出口情况下的最短路选择

    使用方法

    1. 将代码复制到本地 gen.cpp
    2. 使用 g++ gen.cpp -o gen -std=c++14 -O2 编译。
    3. 运行 ./gen。当前目录下将生成 1.in/1.out10.in/10.out

    教练寄语

    • 空间优化:在处理 100×100100 \times 100 矩阵时,静态数组 int dist[105][105] 仅占用约 40KB,不需要使用 std::vector
    • 防止死循环:在生成随机迷宫时,务必保证入口坐标处被强制修改为 .
    • 特判逻辑:在 BFS 中,判断“是出口”的代码应放在“新位置入队”时,这样可以提前结束搜索,提高效率。但在本题规模下,O(MN)O(MN) 已经足够快。
    • 0
      @ 2026-1-8 17:23:32

      在 NOI 竞赛中,求迷宫/网格图的最短路径广度优先搜索 (BFS) 是标准且唯一的 O(NM)O(NM) 正解。DFS 在此类问题中效率极低(指数级复杂度),不适合作为“逐渐过渡”的解法。

      因此,这里的过渡将侧重于:从“工程化/易读的 STL 写法”过渡到“竞赛化/高性能的静态数组写法”


      版本一:标准 STL 写法 (适合初学者,逻辑清晰)

      特点:使用 vector, string, queue 等 STL 容器,代码可读性高,不易发生内存越界,适合 N1000N \le 1000 的数据。

      /**
       * 版本一:基于 STL 的标准 BFS
       * 适合:NOIP 普及组/提高组,数据规模中等
       * 优点:代码逻辑清晰,不易写挂
       */
      #include <iostream>
      #include <vector>
      #include <string>
      #include <queue>
      
      using namespace std;
      
      // 方向数组:上、下、左、右
      const int dr[] = {-1, 1, 0, 0};
      const int dc[] = {0, 0, -1, 1};
      
      int main() {
          // 1. IO 提速
          ios::sync_with_stdio(false);
          cin.tie(0);
      
          int m, n;
          if (!(cin >> m >> n)) return 0;
      
          // 2. 读入迷宫
          vector<string> maze(m);
          for (int i = 0; i < m; i++) {
              cin >> maze[i];
          }
      
          int r0, c0;
          cin >> r0 >> c0;
      
          // 3. 初始化距离数组 (dist)
          // -1 代表未访问,>=0 代表步数
          // 空间复杂度 O(MN)
          vector<vector<int>> dist(m, vector<int>(n, -1));
      
          // 4. BFS 队列
          queue<pair<int, int>> q;
      
          // 初始状态入队
          q.push({r0, c0});
          dist[r0][c0] = 0; // 起点步数为0
      
          while (!q.empty()) {
              pair<int, int> curr = q.front();
              q.pop();
      
              int r = curr.first;
              int c = curr.second;
      
              // 尝试 4 个方向
              for (int i = 0; i < 4; i++) {
                  int nr = r + dr[i];
                  int nc = c + dc[i];
      
                  // --- 核心逻辑:严密的“安检”顺序 ---
      
                  // [安检1] 越界检查
                  if (nr < 0 || nr >= m || nc < 0 || nc >= n) continue;
      
                  // [安检2] 墙壁检查
                  if (maze[nr][nc] == '+') continue;
      
                  // [安检3] 判重检查 (是否已访问)
                  if (dist[nr][nc] != -1) continue;
      
                  // --- 通过安检,更新状态 ---
                  
                  // 更新距离
                  dist[nr][nc] = dist[r][c] + 1;
      
                  // [安检4] 目标判定 (是否到达有效出口)
                  // 条件:位于边界
                  if (nr == 0 || nr == m - 1 || nc == 0 || nc == n - 1) {
                      // 条件:不能是入口本身 (虽然入口dist为0,新点dist>0,逻辑上不会重合,但判断坐标更直观)
                      if (!(nr == r0 && nc == c0)) {
                          cout << dist[nr][nc] << endl;
                          return 0; // 找到最近出口,直接结束
                      }
                  }
      
                  // 加入队列,继续扩散
                  q.push({nr, nc});
              }
          }
      
          // 队列空了还没找到
          cout << -1 << endl;
          return 0;
      }
      

      版本二:竞赛极致优化版 (静态数组 + 手写队列)

      特点

      1. 静态数组:避免 vector 动态开辟内存的开销。
      2. 手写队列:避免 std::queue 的封装开销,内存连续性更好。
      3. IO 优化:使用 scanf/printf,在数据量极大时优势明显。

      适用场景:省选/NOI 级别,或时间/空间限制极其严格的题目(如 N2000N \ge 2000)。

      /**
       * 版本二:竞赛级静态数组优化 BFS
       * 适合:卡常数、内存限制严格、大规模数据
       */
      #include <cstdio>
      
      using namespace std;
      
      // 定义最大范围,稍微开大一点防止越界
      const int MAXN = 105;
      
      // 全局静态数组,存储在堆区(Data Segment),不占用栈空间
      char maze[MAXN][MAXN];
      int dist[MAXN][MAXN];
      
      // 手写结构体代替 pair
      struct Node {
          int r, c;
      };
      
      // 手写队列:数组 + 头尾指针
      Node q[MAXN * MAXN]; 
      int head = 0, tail = 0;
      
      const int dr[] = {-1, 1, 0, 0};
      const int dc[] = {0, 0, -1, 1};
      
      int main() {
          int m, n;
          // scanf 返回值检查是好习惯
          if (scanf("%d %d", &m, &n) == EOF) return 0;
      
          for (int i = 0; i < m; i++) {
              scanf("%s", maze[i]);
          }
      
          int r0, c0;
          scanf("%d %d", &r0, &c0);
      
          // 初始化 dist 数组为 -1
          // 可以用 memset(dist, -1, sizeof(dist)); 但需注意头文件 <cstring>
          // 手写循环更通用
          for(int i = 0; i < m; i++)
              for(int j = 0; j < n; j++) 
                  dist[i][j] = -1;
      
          // 初始入队
          q[tail++] = {r0, c0}; // 入队操作:赋值后 tail++
          dist[r0][c0] = 0;
      
          while (head < tail) { // 队列不为空条件
              Node curr = q[head++]; // 出队操作:取值后 head++
              
              int r = curr.r;
              int c = curr.c;
      
              for (int i = 0; i < 4; i++) {
                  int nr = r + dr[i];
                  int nc = c + dc[i];
      
                  // 1. 越界
                  if (nr < 0 || nr >= m || nc < 0 || nc >= n) continue;
                  // 2. 墙
                  if (maze[nr][nc] == '+') continue;
                  // 3. 已访问
                  if (dist[nr][nc] != -1) continue;
      
                  // 更新步数
                  dist[nr][nc] = dist[r][c] + 1;
      
                  // 4. 出口判定 (及早退出优化)
                  if (nr == 0 || nr == m - 1 || nc == 0 || nc == n - 1) {
                      if (!(nr == r0 && nc == c0)) {
                          printf("%d\n", dist[nr][nc]);
                          return 0;
                      }
                  }
      
                  // 入队
                  q[tail++] = {nr, nc};
              }
          }
      
          printf("-1\n");
          return 0;
      }
      

      复杂度分析与思考过程

      1. 时间复杂度

      • 思考:BFS 的本质是遍历图。在这个网格图中,节点数 V=m×nV = m \times n,边数 E4VE \approx 4V(每个格子连4个邻居)。
      • 过程:每个有效格子最多进队一次,也最多出队一次。每次处理涉及 4 次常数级别的检查。
      • 结论O(m×n)O(m \times n)
      • 优化建议
        • 及早判定 (Early Exit):在生成新节点 (nr, nc) 时立即检查它是否是出口,如果是立刻返回。不要等到它出队时再检查。这可以节省最后一层的所有入队出队开销。
        • 分支预测:将最可能发生的情况放在前面判断(通常越界判断必须在最前)。

      2. 空间复杂度

      • 思考:我们需要存什么?
        1. 迷宫本身 maze
        2. 记录步数和是否访问过的 dist
        3. BFS 队列 q
      • 计算
        • maze: 100×100100 \times 100 字节。
        • dist: 100×100×4100 \times 100 \times 4 字节 (int)。
        • q: 最坏情况(如蛇形全图)需要存 m×nm \times n 个点。
      • 结论O(m×n)O(m \times n)
      • 优化建议
        • 复用内存:如果内存限制极其变态(如 1MB),可以直接在 maze 数组上标记已访问(例如把 . 改成 +),并在队列结构体中多存一个 step 变量,从而省去 dist 数组。但在 NOI 正常题目中,空间换时间是主流,保留 dist 数组更直观。

      关键点与易错点总结 (教练笔记)

      1. 逻辑穿透 (Critical)

        • 错误写法:先判断 if(是边界) return success;,后判断 if(是墙) continue;
        • 后果:会“穿墙”,如果边界是墙,会被错误地当成出口。
        • 口诀先判合法性(越界、墙、已访问),再判目标(是否为出口)。
      2. 入口非出口 (The "Entrance Trap")

        • 题目明确规定:出口不能是入口。
        • 如果不加 !(nr == r0 && nc == c0) 判断,当入口位于边界时(如样例 3),程序会瞬间输出 0 或 1(取决于判断时机),导致 WA。
      3. 队列操作 (Queue Logic)

        • 入队即标记:必须在 q.push() 之前或同时更新 dist。如果在 pop() 时才更新 dist,会导致同一个点被多个邻居重复加入队列,导致内存爆炸 (MLE) 或超时 (TLE)。
      4. 方向数组 (Direction Array)

        • 初学者容易写很多 if-else 来处理上下左右,容易写错坐标。使用 dr/dc 数组循环处理是 NOI 的基本素养。
      • 1

      信息

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