2 条题解
-
0
你好!作为金牌教练,我为你设计了一套针对“飞地的数量”的高质量数据生成器。
为了满足 OJ 评测需求,这套生成器不仅涵盖了基础的随机数据,还专门构造了长路径、环形嵌套、最小边界等容易让错误算法(如处理边界不当或递归爆栈)失效的极端情况。
数据生成器设计规格
-
覆盖范围:
- Test 1-2 (边界点): 和 情况。
- Test 3-4 (极端分布): 全水域和全陆地。
- Test 5 (棋盘格): 最大化独立的陆地单元格。
- Test 6 (环形要塞): 边缘有一圈陆地,中心有一群陆地。考察是否会误把中心群组算作能逃逸。
- Test 7 (随机稀疏): ,陆地占比 10%。
- Test 8 (随机稠密): ,陆地占比 40%。
- Test 9 (S型长链): ,构造一条极长的陆地链条连接到边界。
- Test 10 (最大规模综合): 复杂随机。
-
安全性:
- 非递归标程:生成器内置的
solve函数使用std::queue实现 BFS,确保在生成 Test 9 这种极深路径时不会崩溃。 - 内存控制:最大 矩阵。 个测试点总大小约 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 创建者参考):
-
Test 4 (全陆地) 的玄机: 由于所有的陆地都能通过某种方式连接到边界,因此飞地的数量应该是 0。这能检测出那些“只要是陆地就计入总数”的逻辑错误。
-
Test 6 (环形嵌套) 的陷阱: 如果学生的代码只是简单地检查“点是否在边界”,而没有进行连通性搜索,他们可能会把边缘那一圈
1排除,但却错误地把中间那一大块1也排除。正确的做法是:中间那块如果没和边缘连通,就是飞地。 -
Test 9 (S型长链) 的压力测试: 这条链条极其漫长(在 下长度可达数万)。
- DFS 选手:如果在 Windows 评测环境下且没开大栈空间,此点必 RE。
- BFS 选手:此点可稳过。
- 并查集选手:此点也可稳过。
-
数据格式规范: 每行末尾没有多余空格,且 后紧跟矩阵。这是 NOI 竞赛中最标准的输入输出格式,方便选手使用
scanf或cin读取。 -
关于 网格: 如果格子里是
1,它本身就在边界上,所以它不是飞地,答案应为0。这是非常细微的读题考点。
祝你的 OJ 题目建设顺利!加油!
-
-
0
你好!作为教练,我将带你通过三个阶段,逐步深化对“飞地”问题的理解。这道题的难点在于:不是让你数“堆”,而是让你数“个”。
版本一:基础 DFS 统计(全局标记法)
这是最稳健的思路。我们先不急着直接去算哪些是飞地,而是先通过 DFS 找到所有“有救的士兵”(能触碰边界的陆地),把它们标记掉。最后,剩下的陆地自然就是飞地。
复杂度分析:
- 时间复杂度:。网格中每个点最多被访问两次(一次在边界触发的 DFS 中,一次在最后的全局计数中)。
- 空间复杂度:。递归栈的深度在最坏情况下(例如 型陆地)会达到 。
#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 竞赛中,如果 较大(如 ),且数据被构造为极长的单链(如蛇形),DFS 的递归深度可能超过系统默认栈限制(通常是 8MB 或更小)。改用 BFS 可以利用堆内存(Queue)来规避这个问题。
复杂度分析:
- 时间复杂度:。
- 空间复杂度:,这是队列在层级遍历时的最大宽度。
#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 内存),我们可以减少不必要的存储。
优化建议思考:
- 原地操作:不使用额外的
visited数组,直接修改原数组(如将1改为2或0)。 - 读入优化:对于 500x500 的数据,
scanf已经够快,但在更高规格竞赛中可使用getchar()实现快读。 - 位运算:如果网格只包含 0 和 1,理论上可以使用
std::bitset极大压缩空间。
时间与空间复杂度分析总结
维度 分析过程 结论 时间复杂度 每一个单元格在预处理阶段(从边缘开始的 DFS)最多被改写一次;在最后统计阶段被扫描一次。 空间复杂度 DFS版本:取决于递归深度,最坏情况全图陆地连通,。
BFS版本:取决于队列宽度,。易错点分析
- 统计对象错误:
- 错误:数一数有几个“飞地连通块”。
- 正确:数一数有几个“陆地格子”。(一定要仔细读题!)
- BFS 标记时机:
- 在 BFS 中,必须在元素入队时就将其设为
0(标记为已访问)。如果等到出队时再标记,同一个点会被其周围的邻居多次加入队列,导致内存爆炸(MLE)。
- 在 BFS 中,必须在元素入队时就将其设为
- 边界重复搜索:
- 在处理四条边时,矩阵的四个角点会被行遍历和列遍历各扫描一次。虽然不影响正确性,但为了逻辑严密,可以稍加判断。
读题关键词
- “无法到达边界”:提示使用“正难则反”策略,从边界向内搜索。
- “单元格数量”:提示你要统计的是格子的累加值,而不是连通块的数量。
- “上下左右相邻”:明确四连通关系。
教练寄语: 这道题是搜索算法中“逆向思维”的经典应用。在 NOI 竞赛中,当你发现从正面很难定义一个属性(比如“封闭”)时,不妨先剔除掉所有不符合该属性的元素。祝你刷题愉快!加油!
- 1
信息
- ID
- 19439
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者