2 条题解
-
0
你好!作为教练,我为你准备了一个完整的测试数据生成与校验系统。该程序使用 C++14 编写,能够一键生成符合 NOI 规范的 10 组测试点(
.in和.out文件)。数据生成策略说明
为了确保测试点能有效区分不同水平的代码,我设计了以下 10 个测试点:
- 最小边界: 网格。
- 全水域:没有任何陆地。
- 全陆地:整个网格就是一个大岛屿。
- 棋盘格:最大岛屿数量情况(每两个陆地互不相邻)。
- 稀疏随机:模拟零星岛屿。
- 稠密随机:模拟复杂且连通的大型岛屿。
- S型迷宫:极限路径长度,测试 DFS 递归深度。
- 长条形状: 和 的极端比例。
- 大规模随机 A:接近 限制。
- 大规模随机 B:包含环形岛屿和空心岛屿的复杂结构。
数据生成器完整代码 (C++)
该代码会生成
1.in到10.in,并同时调用内置的 BFS 算法生成对应的1.out到10.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; }
数据说明与优化建议
-
数据范围控制:
- 本题最大数据为 的矩阵。
- 单个
.in文件大小约为 字节(约 90KB)。 - 10 个文件总大小约 1MB,远低于 2MB 的要求,适合 OJ 快速下载和评测。
-
生成器健壮性:
- 防止爆栈:生成器中的
solve函数使用了基于std::queue的 BFS 算法。这确保了在处理测试点 6(长 S 型路径,深度 90,000)时,生成器本身不会崩溃。 - I/O 速度:使用
std::string和文件流,避免了频繁的字符操作。
- 防止爆栈:生成器中的
-
OJ 评测建议 (Coach's Tips):
- 内存限制:建议设置为 256MB。虽然代码只需几 MB,但 NOI 规范通常给足内存。
- 时间限制:建议设置为 1.0s。 的 算法在 1.0s 内绰绰有余。
- 栈空间:如果学生使用 DFS 解题,请务必在评测机环境中设置
ulimit -s unlimited,否则测试点 6 会导致栈溢出(Runtime Error)。
-
读取技巧:
- 提示学生在读入字符矩阵时,注意过滤每行末尾的换行符。使用
scanf("%s", grid[i])或cin >> s是比较稳妥的做法。
- 提示学生在读入字符矩阵时,注意过滤每行末尾的换行符。使用
这套数据生成器可以帮助你建立一个高质量的练习题。如果有任何关于并查集或更复杂剪枝的测试需求,请随时告诉我!
-
0
你好!作为教练,我将带你从最直观的搜索思路开始,逐步演进到竞赛中各种应对复杂环境的版本。在 NOI 竞赛中,稳健性和速度同样重要。
版本一:基础深度优先搜索 (DFS)
这是最符合直觉的“扫雷式”解法。利用递归模拟“沉岛”过程。
时间复杂度分析:
- 每个点最多被访问一次(一旦变成 '0' 就不会再进递归)。
- 复杂度:。对于 ,运算量约为 ,远低于 的秒级门槛。
空间复杂度分析:
- 递归深度最坏情况下为 (例如全岛呈 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) —— 规避栈溢出风险
如果题目数据范围扩大到 以上,DFS 可能导致系统栈溢出。在 NOI 竞赛中,BFS(利用队列)通常更稳健。
时间复杂度: 。 空间复杂度: ,队列中最多同时存在的节点数。
#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)
并查集在处理“连通块”计数时非常强大。它的优势在于:如果地图是动态变化的(例如不断有水变陆地),并查集可以实现近乎 的实时查询。
时间复杂度: , 为阿克曼反函数,几乎可视为常数。 空间复杂度: ,用于存储父亲节点数组。
#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; }
教练的复杂度优化建议与总结
- I/O 优化: 在 NOI 级别比赛中,如果数据量达到 级,请使用
scanf/printf或cin.tie(NULL)。 - 空间优化:
- 如果题目不允许修改
grid数组,可以使用bitset<MAXN> vis[MAXN]代替bool vis[MAXN][MAXN],空间可压缩至 1/8。 - 本题中,直接原地修改
grid[i][j] = '0'是最高效的技巧,无需额外vis数组。
- 如果题目不允许修改
- 时间优化:
- 剪枝: 虽然本题 DFS/BFS 已经是最优线性复杂度,但在回溯类题目中,剪枝是金牌的关键。
- 方向数组: 使用
dx[],dy[]循环遍历邻居,比写四个if更不容易出错,且代码整洁。
- 易错警示:
- BFS 标记时机: 记住,在 BFS 中,节点入队前就应该标记为已访问。如果在出队时才标记,同一个节点可能会被周围四个邻居分别发现并重复入队,导致队列爆炸。
这道题是典型的建模思维训练:将二维矩阵想象成一张图,将搜索算法想象成填充工具。希望你能在草稿纸上多画几次“沉岛”的过程,加深对“状态迁移”的理解!加油!
- 1
信息
- ID
- 19437
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者