大部分题目是这样的:你的程序先一次性把所有输入读完,然后开始计算,最后输出答案。
交互题不一样。它把"输入"和"输出"交错在了一起:你输出一行,评测机看到你说了什么,再回你一行;你再决定下一步说什么……就像和一个看不见的人玩对话游戏。
输入: 5 3
13 7 9 11 6
输出: 9
所有信息一次性给你。读完→算→输出。
你: GO 机器: SUCC 你: LEFT 机器: W 你: GO 机器: FAIL ...
边问边算。每问一句,信息才多一点点。
cout << "..." << endl; 或加 cout.flush()。否则评测机收不到指令,程序就卡死了(超时)。假如题目把整个迷宫直接给你,这就是一道再普通不过的"找连通块"题:
5 7 ####### #..#..# #.S#..# #...#.# #######
从 S 出发,DFS 四个方向,不是墙就递归——五分钟就写完了:
void dfs(int x, int y) {
vis[x][y] = 1;
for (int d = 0; d < 4; d++) {
int nx = x + dx[d], ny = y + dy[d];
if (g[nx][ny] == '#') continue; // ← 关键:看一眼数组
if (vis[nx][ny]) continue;
dfs(nx, ny);
}
}
if (g[nx][ny] == '#') continue; 这一句,在交互题里写不出来——因为你根本没有 g[nx][ny] 这个数组!GOFAIL = 是墙,SUCC = 能走,而且机器人真的已经走过去了下面这个迷宫,机器人从 S 出发,我们模拟一遍 DFS 的过程。注意看红色箭头——那是机器人的真实位置和朝向,它每发一条指令就真的会动。
普通 DFS 里,递归函数返回的瞬间,就好像"人自动回来了"——其实只是变量 x, y 不再使用了。但机器人不会自动回来。
void dfs(int x, int y) {
...
dfs(nx, ny);
// 函数返回, x,y 自然还是父节点
// 什么都不用做 ✓
}
void dfs(int x, int y) {
...
dfs(nx, ny);
// 机器人还停在儿子节点!
turn_to((d + 2) % 4); // 转 180°
GO; // 走回来
turn_to(d); // 转回原方向
}
下面是一份信奥学生最熟悉的写法——纯二维数组,不用 STL 容器。关键设计在注释里:
a[i][j] 三态表达全部含义 — 0=未探索、1=墙、2=可达。一个数组同时表达"是什么"和"是否访问过",不需要单独的 vis 数组。1,下次循环判断 a[nx][ny] != 0 自动跳过,不会重复撞同一堵墙浪费 GO 次数。
#include <bits/stdc++.h>
using namespace std;
/*
方向编号 (0:N 1:E 2:S 3:W):
0
3 1
2
*/
const int N = 210; // 105*2, 留出"起点在正中"的余量
int a[N][N]; // 0:未探索 1:墙 2:可达 —— 一个数组搞定
int dir_now; // 当前朝向
const int dx[4] = {-1, 0, 1, 0};
const int dy[4] = {0, 1, 0, -1};
// ========= 三种基础交互动作 =========
int dir_id(const string &s) {
if (s == "N") return 0;
if (s == "E") return 1;
if (s == "S") return 2;
return 3;
}
void turn_left() { cout << "LEFT" << endl; string fb; cin >> fb; dir_now = dir_id(fb); }
void turn_right() { cout << "RIGHT" << endl; string fb; cin >> fb; dir_now = dir_id(fb); }
string go_forward(){ cout << "GO" << endl; string fb; cin >> fb; return fb; }
// 转到目标方向(简单起见全用 right 转, 反正 LEFT/RIGHT 不限次数)
void turn_to(int target) {
while (dir_now != target) turn_right();
}
// ========= DFS 主体 =========
void dfs(int x, int y) {
a[x][y] = 2; // 当前格标为可达
for (int d = 0; d < 4; d++) {
int nx = x + dx[d], ny = y + dy[d];
if (a[nx][ny] != 0) continue; // ★ 非 0 = 已探明 (墙或空地都跳过)
turn_to(d);
string fb = go_forward(); // 用操作代替"看数组"
if (fb == "FAIL") {
a[nx][ny] = 1; // 标墙, 下次循环自动跳过
} else {
// a[nx][ny] 会在 dfs 进入时被标成 2
dfs(nx, ny);
// ★ 物理回溯三件套: 转身 → 走回 → 转回
turn_to((d + 2) % 4);
go_forward();
turn_to(d);
}
}
}
// ========= 主函数 =========
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
// 1. 初始朝向未知, 随便转一下评测机就告诉你了
turn_left();
// 2. 起点放数组正中, 四周预留 100+ 格
dfs(105, 105);
// 3. 输出地图: 找出包含所有探明格的最小矩形
cout << "END" << endl;
int x1 = N, y1 = N, x2 = 0, y2 = 0;
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
if (a[i][j] > 0) { // 探明过的格子
x1 = min(x1, i); y1 = min(y1, j);
x2 = max(x2, i); y2 = max(y2, j);
}
}
}
cout << x2 - x1 + 1 << " " << y2 - y1 + 1 << endl;
for (int i = x1; i <= x2; i++) {
for (int j = y1; j <= y2; j++) {
// 题目要求: 不可达的格子统一当墙输出
cout << (a[i][j] == 2 ? '.' : '#');
}
cout << '\n';
}
return 0;
}
| 对比项 | map 版本 | 这份数组版本 |
|---|---|---|
| 数据结构 | map<pair<int,int>, char> | int a[210][210] |
| 单次查找 | O(log n) | O(1) |
| 是否需要 vis 数组 | 用 map.count 判断 | 不需要, 用 a[i][j]=0 表示 |
| 起点处理 | 负坐标也行 | 放数组正中 (105,105) |
| 学生友好度 | 需要懂 pair/map | 纯数组,小学生都能读 |
普通题可以直接复制样例进去跑。交互题不行——评测机的反馈是根据你的指令实时算出来的,你没法提前把所有反馈写好。手动敲 SUCC/FAIL 来调试小迷宫还行,稍微大一点根本顶不住。
cin/cout 都封装到一个 send() 函数里。把散落的"输出指令 + 读反馈"统一收进一个函数:
// ⛔ 不要再这样写散装的:
cout << "GO" << endl;
string fb; cin >> fb;
// ✅ 改成统一封装:
string fb = send("GO");
send() 内部根据宏切换是真发还是假发:
string send(const string &cmd) {
#ifdef LOCAL
return fake_judge(cmd); // 本地: 走假裁判
#else
cout << cmd << endl; // 提交: 真的发给评测机
string fb;
cin >> fb;
return fb;
#endif
}
LOCAL 宏,自动走 else 分支;本地编译加 -DLOCAL 就走假裁判。永远不会出现"忘记换回去导致 WA"的事故。
它要做评测机做的事:维护机器人的真实位置和朝向、维护真实迷宫,根据指令算出反馈。
// ===== 仅本地用,提交时这段被 #ifdef 屏蔽 =====
#ifdef LOCAL
// 1. 把你想测的迷宫写死在这里
vector<string> REAL_MAZE = {
"#####",
"#...#",
"#.#.#",
"#...#",
"#####"
};
// 2. 机器人的真实状态(你设定, 程序"不知道")
int real_r = 1, real_c = 1; // 真实起点
int real_d = 2; // 真实初始朝向 0:N 1:E 2:S 3:W
const char* DNAME[4] = {"N","E","S","W"};
const int DR[4] = {-1,0,1,0};
const int DC[4] = {0,1,0,-1};
string fake_judge(const string &cmd) {
if (cmd == "LEFT") { real_d = (real_d + 3) % 4; return DNAME[real_d]; }
if (cmd == "RIGHT") { real_d = (real_d + 1) % 4; return DNAME[real_d]; }
if (cmd == "GO") {
int nr = real_r + DR[real_d];
int nc = real_c + DC[real_d];
if (REAL_MAZE[nr][nc] == '#') return "FAIL";
real_r = nr; real_c = nc;
return "SUCC";
}
return "?";
}
#endif
下面这个演示展示了同一条指令 GO 在两种模式下的流向。切换模式,点"发送指令",看数据怎么跑:
send("GO")
$ g++ -DLOCAL -O2 sol.cpp -o sol # 加 -DLOCAL 开启假裁判 $ ./sol # 一键跑,看输出对不对 $ g++ -O2 sol.cpp -o sol # 不加 -DLOCAL = 提交模式 $ echo "N" | ./sol # 手动敲反馈调试也行
在 fake_judge 里加一行 cerr(注意是 cerr 不是 cout,不会污染交互流),就能看到机器人每一步的轨迹:
string fake_judge(const string &cmd) {
string fb = /* ...如上... */;
cerr << "[裁判] 指令 " << cmd << " → 反馈 " << fb
<< " 机器人在 (" << real_r << "," << real_c << ") 朝 "
<< DNAME[real_d] << "\n";
return fb;
}
cout 是给评测机看的(你的指令)。如果调试信息也走 cout,评测机会把它当成指令读,直接 WA。cerr 走标准错误流,屏幕上能看见,但不会进入交互流。这是所有交互题、所有调试的铁律。
cin/cout 进一个 send() 函数,主代码再也看不到散装 IO#ifdef LOCAL 决定走真交互还是假裁判,提交无需改代码cerr 输出轨迹把前面所有东西整合起来:一份代码既能本地一键跑测试,又能原样提交到洛谷。这就是你应该真正写在自己电脑上的版本。
#include <bits/stdc++.h>
using namespace std;
/* ==========================================================
B4352 迷宫探险 — 自带本地测试框架
本地调试: g++ -DLOCAL -O2 sol.cpp -o sol && ./sol
提交洛谷: 直接复制全部代码, 不用改任何东西
========================================================== */
const int N = 210;
int a[N][N]; // 0:未探索 1:墙 2:可达
int dir_now; // 0:N 1:E 2:S 3:W
const int dx[4] = {-1, 0, 1, 0};
const int dy[4] = {0, 1, 0, -1};
const char* DNAME[4] = {"N","E","S","W"};
int dir_id(const string &s) {
if (s == "N") return 0;
if (s == "E") return 1;
if (s == "S") return 2;
return 3;
}
/* ==========================================================
↓↓↓ 仅本地用的"假裁判", 提交时整段被宏屏蔽 ↓↓↓
========================================================== */
#ifdef LOCAL
// 你想测的迷宫写在这里 (随便改, # 是墙, . 是空地)
vector<string> REAL_MAZE = {
"#######",
"#..#..#",
"#.#...#",
"#...#.#",
"#.#.#.#",
"#...#.#",
"#######"
};
// 机器人的"真实"初始状态 (你自己设, 程序不知道)
int real_r = 1, real_c = 1; // 真实起点行/列
int real_d = 2; // 真实初始朝向 (S)
int judge_go_cnt = 0; // 统计 GO 次数, 看是否超限
string fake_judge(const string &cmd) {
string fb;
if (cmd == "LEFT") { real_d = (real_d + 3) % 4; fb = DNAME[real_d]; }
else if (cmd == "RIGHT") { real_d = (real_d + 1) % 4; fb = DNAME[real_d]; }
else if (cmd == "GO") {
judge_go_cnt++;
int nr = real_r + dx[real_d];
int nc = real_c + dy[real_d];
if (REAL_MAZE[nr][nc] == '#') fb = "FAIL";
else { real_r = nr; real_c = nc; fb = "SUCC"; }
}
// 用 cerr 输出调试轨迹, 不污染交互流
cerr << "[裁判] " << cmd << " → " << fb
<< " 机器人(" << real_r << "," << real_c
<< ") 朝" << DNAME[real_d] << "\n";
return fb;
}
#endif
/* ↑↑↑ 假裁判结束 ↑↑↑ */
// ========= 统一的交互入口 =========
string send(const string &cmd) {
#ifdef LOCAL
return fake_judge(cmd); // 本地: 走假裁判
#else
cout << cmd << endl; // 提交: 真发给评测机
string fb;
cin >> fb;
return fb;
#endif
}
// ========= 三种基础动作(全都走 send) =========
void turn_left() { dir_now = dir_id(send("LEFT")); }
void turn_right() { dir_now = dir_id(send("RIGHT")); }
string go_forward() { return send("GO"); }
void turn_to(int target) {
while (dir_now != target) turn_right();
}
// ========= DFS 主体 =========
void dfs(int x, int y) {
a[x][y] = 2;
for (int d = 0; d < 4; d++) {
int nx = x + dx[d], ny = y + dy[d];
if (a[nx][ny] != 0) continue;
turn_to(d);
string fb = go_forward();
if (fb == "FAIL") {
a[nx][ny] = 1;
} else {
dfs(nx, ny);
turn_to((d + 2) % 4);
go_forward();
turn_to(d);
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
turn_left(); // 转一下,得到初始朝向
dfs(105, 105); // 起点放数组正中
// 输出地图
cout << "END" << endl;
int x1 = N, y1 = N, x2 = 0, y2 = 0;
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
if (a[i][j] > 0) {
x1 = min(x1, i); y1 = min(y1, j);
x2 = max(x2, i); y2 = max(y2, j);
}
cout << x2 - x1 + 1 << " " << y2 - y1 + 1 << endl;
for (int i = x1; i <= x2; i++) {
for (int j = y1; j <= y2; j++)
cout << (a[i][j] == 2 ? '.' : '#');
cout << '\n';
}
#ifdef LOCAL
cerr << "\n[裁判] 共调用 GO " << judge_go_cnt << " 次 (上限 50000)\n";
#endif
return 0;
}
把上面代码存成 sol.cpp,本地编译跑一下:
$ g++ -DLOCAL -O2 sol.cpp -o sol $ ./sol [裁判] LEFT → E 机器人(1,1) 朝E [裁判] RIGHT → S 机器人(1,1) 朝S [裁判] GO → SUCC 机器人(2,1) 朝S [裁判] RIGHT → W 机器人(2,1) 朝W [裁判] GO → FAIL 机器人(2,1) 朝W [裁判] RIGHT → N 机器人(2,1) 朝N [裁判] GO → SUCC 机器人(1,1) 朝N ... END 7 7 ####### #..#..# #.#...# #...#.# #.#.#.# #...#.# ####### [裁判] 共调用 GO 36 次 (上限 50000)
REAL_MAZE 一模一样。如果一致,说明你的程序成功"摸"出了整张地图;如果不一致,看 cerr 的轨迹找出哪一步错了。judge_go_cnt 如果接近或超过 50000,说明算法效率有问题(比如忘了把墙标 vis,重复撞墙)。
LOCAL 宏,所有 #ifdef LOCAL 包起来的代码都会被屏蔽,主程序自动走 cin/cout 真交互流。设可达空地有 R 个(题目里 R ≤ 100×100 = 10000)。
很多同学学完一道交互题后,会觉得"交互题就是普通题加了几句控制指令的代码"。这话对了一半——表面上确实是。但还有更深的一层,搞清楚以后对所有交互题都受用。
从代码形态看,确实只是在 DFS 外面包了一层操控:
if (g[nx][ny] == '#') continue;
dfs(nx, ny);
turn_to(d); // ← 多了
string fb = go_forward(); // ← 多了
if (fb == "FAIL") ...
else {
dfs(nx, ny);
物理回溯三件套; // ← 多了
}
g[nx][ny]=='#' 一句话搞定。GO → 机器人真的走了LEFT → 机器人真的转了B4352 不仅是道 DFS 题,它在教你一个会受用终身的思维模式:
| 对比维度 | 普通 DFS 迷宫题 | 本题(交互 DFS) |
|---|---|---|
| 地图是否已知 | 已知,数组里 | 未知,要靠 GO 试出来 |
| 判断墙 | g[nx][ny]=='#' | 发 GO 看是否 FAIL |
| 朝向重要吗 | 不重要 | 很重要(GO 只能往当前朝向) |
| 回溯方式 | 函数 return | 必须真的让机器人走回来 |
| 调试方式 | 样例输入直接跑 | 写假裁判函数 / 手动输入 |
endl 刷新缓冲区,不然程序会卡死。endl 刷新的重要性。