题单 · 习题解析

洛谷 P1135 奇怪的电梯

★★★ 同一轮 P1379 的反面:这张图的边是有向的(只有 24.8% 的边反过来也走得通)⇒「从终点反着跑一次」当场就错

原题:洛谷 P1135出自 第 15 章 BFS 变形:多源 BFS 与状态图搜索 的题单题面本地存档:2026-08-28
⚠ 先自己写一遍,再往下看

这一页不是标准答案,是一条阶梯:把大多数人真实会写出的第一版摆出来, 看它错在哪、慢在哪,再一步一步改。所以它对「已经自己动手撞过一次墙」的人最有用。
还没写过的话,先去写 —— 写出来的哪怕是错的,也比直接看这一页收获大。

0题目原文(本地存了一份)

为什么要在这儿抄一份题面

原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。

转录自洛谷 P1135,日期见页头。两边不一致时信原站。

题目描述

呵呵,有一天我做了一个梦,梦见了一种很奇怪的电梯。大楼的每一层楼都可以停电梯, 而且第 i 层楼(1 ≤ i ≤ N)上有一个数字 Kᵢ0 ≤ Kᵢ ≤ N)。 电梯只有四个按钮:开,关,上,下。上下的层数等于当前楼层上的那个数字。 当然,如果不能满足要求,相应的按钮就会失灵。 例如:3, 3, 1, 2, 5 代表了 KᵢK₁ = 3K₂ = 3,……),从 1 楼开始。 在 1 楼,按「上」可以到 4 楼,按「下」是不起作用的,因为没有 -2 楼。 那么,从 A 楼到 B 楼至少要按几次按钮呢?

输入格式

共二行。

第一行为三个用空格隔开的正整数,表示 N, A, B1 ≤ N ≤ 2001 ≤ A, B ≤ N)。

第二行为 N 个用空格隔开的非负整数,表示 Kᵢ

输出格式

一行,即最少按键次数,若无法到达,则输出 -1

数据规模与约定

对于 100% 的数据,1 ≤ N ≤ 2001 ≤ A, B ≤ N0 ≤ Kᵢ ≤ N

输入输出样例

输入

5 1 5
3 3 1 2 5

输出

3

1★ 又是一次「把格子换成状态」—— 这一版就能 AC

同一轮的 P1379 是同一个动作:

网格 BFS 这道题
一个「点」 一个格子 你现在在第几层1 ~ N
一条「边」 上下左右 按一次按钮i → i + Kᵢi → i − Kᵢ
距离 走几步 按几次
p1135.cpp★ 这一版就能 AC
// P1135 奇怪的电梯 —— 状态图 BFS(★ 这一版就能 AC)
//
// 题目:N 层楼,第 i 层写着 K[i]。在第 i 层只能按「上」或「下」,
// 一次正好走 K[i] 层(越界就按不动)。问从 A 到 B 最少按几次,到不了输出 -1。
//
// ★ 又是一次「把格子换成状态」:
// 一个「点」= 你现在在第几层(1 ~ N)
// 一条「边」= 按一次按钮:i → i + K[i],或 i → i − K[i]
// BFS 一个字不用改,第一次到达 B 就是最少按键次数。
//
// ⚠⚠ 而这张图和前面几道**有一个根本区别:它的边是有向的。**
// 在第 i 层能上到 i + K[i],可站在 i + K[i] 上时,能走多远由 **K[i + K[i]]** 说了算 ——
// 回不回得来完全是另一回事。
// ⇒ [P1379](/sol/p1379/) 那页「从目标态反着 BFS 一次就够」的技巧,
// 在这道题上**直接就错**(p1135RevBad.cpp);要反着做必须**先建反图**(p1135Rev.cpp)。
//
// ⚠ 两个边界:K[i] 可能是 0(原地不动 —— 一个自环,没有 vis 就死循环了);A 可能等于 B(答案 0)。
//
// 复杂度 O(N):每层最多进队一次,每层最多两条出边。N ≤ 200,小得离谱。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, a, b;
if (scanf("%d %d %d", &n, &a, &b) != 3) return 0;
vector<int> k(n + 1);
for (int i = 1; i <= n; i++)
if (scanf("%d", &k[i]) != 1) return 0;
vector<int> dist_(n + 1, -1);
queue<int> q;
dist_[a] = 0;
q.push(a);
while (!q.empty()) {
int cur = q.front();
q.pop();
if (cur == b) break;
for (int nxt : {cur + k[cur], cur - k[cur]}) {
if (nxt < 1 || nxt > n) continue;
if (dist_[nxt] != -1) continue; // ★ 入队时就标记 —— K[i] = 0 的自环也靠它挡住
dist_[nxt] = dist_[cur] + 1;
q.push(nxt);
}
}
printf("%d\n", dist_[b]); // 到不了就是 -1,正是题面要的
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

N ≤ 200、每层最多两条出边 ⇒ 整张图不到 400 条边,BFS 是 O(N),小得离谱。 这道题的难点一点都不在复杂度上。

2⚠⚠ 难点在这儿:这张图的边是有向的

★★★ 「从终点反着跑一次」这个技巧,缺一个主语

同一轮的 P1379 刚用过这个技巧:八数码每一步都可逆 ⇒ 状态图的边是无向的 ⇒ 从目标态反着 BFS 一次,整张表就出来了。 P1747(12 个方向成对相反)、P1332(网格四连通)也一样。

这道题不一样:

    在第 i 层,能走到 i ± K[i]                     <- 步长由**出发那层**的数字决定
    站在第 j 层想「反着走回 i」,走的却是 j ± K[j]  <- 步长换成了**落脚那层**的数字

i → j 走得通,完全不代表 j → i 走得通

「有多不一样」是可以量的(300 组随机数据,楼层 ≤ 60):

一共几条边 其中「反过来也走得通」的
10152 2516(24.8%,而且里面还有 2344 条是自环)

四分之三的边是单向的。 照搬「从终点反着走」会怎样:

p1135RevBad.cpp⚠⚠ 从终点出发,却还照着正图走
// ⚠⚠ 错法:照搬「从终点反着 BFS」这个技巧 —— 可这张图的边是**有向**的
//
// [P1379](/sol/p1379/) 那页刚说过:八数码的每一步都可逆 ⇒ 状态图的边是无向的
// ⇒ 从目标态反着 BFS 一次,整张表就出来了。
// [P1747](/sol/p1747/)(12 个方向成对相反)和 [P1332](/sol/p1332/)(网格)也一样。
//
// 于是很自然会想:这道题也从 B 反着走呗。而**这道题的边是有向的**:
//
// 在第 i 层,能走到 i ± K[i] <- 步长由**出发那层**的 K 决定
// 站在第 j 层想「反着走回 i」,走的却是 j ± K[j] <- 步长换成了**落脚那层**的 K
//
// ⇒ 这一版从 B 出发、照着 `j ± K[j]` 走,**求的根本不是这道题的答案**
// (它求的是「从 B 出发能不能到 A」,而那是另一个问题)。
//
// ★ 草稿在这儿写过「它在官方样例上照样给 3」—— **跑一遍就被打回来了**:
// 样例里 B = 5 而 K[5] = 5,从第 5 层按上是 10 层、按下是 0 层,**两个方向都出界** ——
// 它一步都走不了,直接输出 -1。⇒ **这一版官方样例就挡得住。**
// ⚠ 但别把「样例挡得住」当成它无害:只要终点那层的 K 走得通,它就会给出一个
// 像模像样的错数字。抓获率见 p1135Count.cpp 第 ③ 段。
//
// ⚠ 要反着做**不是不行**,但必须**先建反图**(把每条 i → i ± K[i] 反过来存),
// 那一版是 p1135Rev.cpp,它和正解逐字节相同。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, a, b;
if (scanf("%d %d %d", &n, &a, &b) != 3) return 0;
vector<int> k(n + 1);
for (int i = 1; i <= n; i++)
if (scanf("%d", &k[i]) != 1) return 0;
vector<int> dist_(n + 1, -1);
queue<int> q;
dist_[b] = 0; // ⚠⚠ 从终点出发……
q.push(b);
while (!q.empty()) {
int cur = q.front();
q.pop();
if (cur == a) break;
for (int nxt : {cur + k[cur], cur - k[cur]}) { // ⚠⚠ ……却还是照着「当前层」的 K 走
if (nxt < 1 || nxt > n) continue;
if (dist_[nxt] != -1) continue;
dist_[nxt] = dist_[cur] + 1;
q.push(nxt);
}
}
printf("%d\n", dist_[a]);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

官方样例当场就挡住了(给 -1,正确是 3):样例里 B = 5K₅ = 5, 从第 5 层按「上」是 10 层、按「下」是 0 层,两个方向都出界 —— 它一步都走不了。

⚠ 这句话是被实测打回来的

草稿里我给这一版写的是「它在官方样例上照样给 3,因为样例那组 K 恰好两边都走得通」。 跑一遍就发现不是。 注释已经订正。

⇒ 这已经是这一轮第三次「草稿里的判断被实测打回来」了 (另两次在 P1379P1332)。 硬规矩第 1 条那句「正文里的每个数字都必须是实测的」,管的其实不止是数字。

3★ 真想反着做,得先建反图

「反着跑一次」这个技巧本身没问题,出问题的是边有方向时还照着正图走。 把每条 i → i ± Kᵢ 掉个头存进邻接表,在反图上从 B 做 BFS 就对了:

p1135Rev.cpp★ 建反图之后,和正解逐字节相同
// ★ 真想反着做,得先建反图 —— 这一版和正解逐字节相同
//
// 正图的边是 `i → i + K[i]` 和 `i → i − K[i]`(步长由**出发那层**决定)。
// 反图就是把每一条边掉个头:对每个 i,把 i 挂到 `i + K[i]` 和 `i − K[i]` 的入边表里。
//
// ⇒ 在反图上从 B 做 BFS,得到的就是「每一层到 B 要按几次」,查 A 即可。
//
// ★ 它和正解一样对,但要多存一张邻接表。
// 放它在这儿只为了说清一件事:**「反着跑一次」这个技巧本身没问题,
// 出问题的是「边有方向时还照着正图走」**(p1135RevBad.cpp)。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, a, b;
if (scanf("%d %d %d", &n, &a, &b) != 3) return 0;
vector<int> k(n + 1);
for (int i = 1; i <= n; i++)
if (scanf("%d", &k[i]) != 1) return 0;
vector<vector<int>> rev(n + 1); // rev[j] = 「一步能走到 j」的那些层
for (int i = 1; i <= n; i++)
for (int j : {i + k[i], i - k[i]})
if (j >= 1 && j <= n) rev[j].push_back(i);
vector<int> dist_(n + 1, -1);
queue<int> q;
dist_[b] = 0;
q.push(b);
while (!q.empty()) {
int cur = q.front();
q.pop();
if (cur == a) break;
for (int nxt : rev[cur]) {
if (dist_[nxt] != -1) continue;
dist_[nxt] = dist_[cur] + 1;
q.push(nxt);
}
}
printf("%d\n", dist_[a]);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

⇒ 这道题只问一次,正着做更省事;放这一版在这儿,是为了把上一步那个主语说完整。

4⚠⚠ 题面里那个 `0 ≤ Kᵢ` 是有牙齿的:忘了入队标记就出不来了

第 14 章正文写死过一句:标记要打在入队的时候。 在网格上忘了它,后果是「退化成暴力」(P1443 量过:入队次数爆炸,但答案还对)。

⚠ 这道题上后果更硬 —— 题面写着 0 ≤ Kᵢ ≤ N,也就是 Kᵢ 可以是 0: 那一层上「上」和「下」都走到它自己,那是一个自环

p1135NoVis.cpp⚠⚠ 忘了入队时标记
// ⚠⚠ 错法:BFS 忘了「入队时就标记」
//
// 第 14 章正文写死过一句:**标记要打在入队的时候**。在网格上忘了它,
// 后果是「退化成暴力」([P1443](/sol/p1443/) 那页量过:入队次数爆炸,但答案还对)。
//
// ⚠ 而在这道题上,后果直接是**死循环**,理由是题面给的一个边界:
//
// K[i] 的范围是 0 ≤ K[i] ≤ N —— **K[i] 可以是 0**
//
// `K[i] = 0` 那一层上,「上」和「下」都走到它自己 —— 那是一个**自环**。
// 没有 vis 的话,队列里立刻开始 i → i → i → …,永远出不来。
// ★ 就算没有 K[i] = 0,只要图里有环(这题多得是),照样出不来。
//
// ⚠ 这一版自带一道闸:入队次数超上限就报出来并退出,
// **绝不悄悄少入几次队把自己救活**([P1443](/sol/p1443/) 那条教训)。
#include <bits/stdc++.h>
using namespace std;
const long long PUSH_LIMIT = 20000000LL;
int main() {
int n, a, b;
if (scanf("%d %d %d", &n, &a, &b) != 3) return 0;
vector<int> k(n + 1);
for (int i = 1; i <= n; i++)
if (scanf("%d", &k[i]) != 1) return 0;
vector<int> dist_(n + 1, -1);
queue<pair<int, int>> q; // (层, 已经按了几次)
q.push({a, 0});
long long pushes = 1;
while (!q.empty()) {
auto [cur, d0] = q.front();
q.pop();
if (cur == b) { printf("%d\n", d0); return 0; }
for (int nxt : {cur + k[cur], cur - k[cur]}) {
if (nxt < 1 || nxt > n) continue;
// ⚠⚠ 这里本来该有一句 if (dist_[nxt] != -1) continue;
q.push({nxt, d0 + 1});
if (++pushes > PUSH_LIMIT) {
fprintf(stderr, "入队次数超过 %lld —— 没有 vis,它出不来了\n", PUSH_LIMIT);
return 1;
}
}
}
printf("%d\n", -1);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

一个最小的例子就够:3 层、K = {0, 1, 1}、从 1 走到 3 —— 第 1 层的 K 是 0,队列里立刻开始 1 → 1 → 1 → …入队一千万次还没完(度量程序里掐掉了)。

★ 而它在官方样例上是对的

样例里没有 Kᵢ = 0,图也小,它照样给 3。 ⇒ 又一次「样例挡不住」——这一版是靠题面那句 0 ≤ Kᵢ 才逼出来的, 而生成器里那个「K 取 0 的百分比」旋钮要是拧到 0,就等于亲手撤掉这一档数据 (第 13 章 P1451 那条:一个旋钮只护着一部分 bug)。

5★★★ 对拍:抓获率掉头的那一段,又是「两版一起输出 -1」

p1135Gen.cpp生成器:楼层数 + K 取 0 的比例
// P1135 对拍生成器:`./p1135Gen <seed> [maxN] [K 取 0 的百分比]`
//
// 输出:`n a b` + 一行 n 个 K[i]。
//
// ★ 两个旋钮:
// · maxN —— 楼层数(1 ~ maxN 随机),默认 8;
// · zeroPct —— 每一层的 K 有多大概率取 0,默认 10。
// ⚠ `K[i] = 0` 是题面明写允许的(`0 ≤ K[i] ≤ N`),而它正是
// p1135NoVis.cpp(忘了入队标记)的那个自环 —— 把它调到 0 就等于亲手撤掉一档数据。
#include <bits/stdc++.h>
using namespace std;
int main(int argc, char** argv) {
unsigned seed = argc > 1 ? (unsigned)atoi(argv[1]) : 1u;
int maxN = argc > 2 ? atoi(argv[2]) : 8;
int zeroPct = argc > 3 ? atoi(argv[3]) : 10;
if (maxN < 1) maxN = 1;
if (zeroPct < 0) zeroPct = 0;
if (zeroPct > 100) zeroPct = 100;
mt19937 rng(seed);
int n = (int)(rng() % (unsigned)maxN) + 1;
int a = (int)(rng() % (unsigned)n) + 1;
int b = (int)(rng() % (unsigned)n) + 1;
printf("%d %d %d\n", n, a, b);
for (int i = 1; i <= n; i++) {
int k = ((int)(rng() % 100u) < zeroPct) ? 0 : (int)(rng() % (unsigned)(n + 1));
printf("%d%c", k, i == n ? '\n' : ' ');
}
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

300 轮,p1135RevBad 被抓几次:

楼层 ≤ 4 8 20 60
⚠⚠ 被抓 54 62 61 49
这 300 轮里正解就是 -1 111 146 196 246
★★★ 又是那件事:「一致」有两种 —— 都算对了,和都没算

第一行看着是「楼层多了反而更难抓」。不是。 看第二行:楼层 ≤ 60 那一档,300 轮里有 246 轮正解本来就是 -1, 两版一起输出 -1、逐字节相同、对拍记「通过」—— 可它什么都没验

把那些轮次剔掉再看:

楼层 ≤ 4 8 20 60
有解的轮数 189 154 104 54
其中被抓 27(14.3%) 41(26.6%) 37(35.6%) 25(46.3%)

曲线不再掉头,一路上升。

⇒ 这是 P1746 那条的第二个现场(那道题是「起终点不连通」, 这道题是「电梯到不了」): 看到抓获率往下走,先问一句「这一段里两边比的还是同一件事吗」。

★ 顺带:这道题的 -1 真的很常见

上面第二行本身就是个提醒:随手造的数据里,一大半是到不了的 (楼层 ≤ 60 时 246 / 300)。 ⇒ 「无法到达输出 -1」不是题面凑字数 —— 它是这道题的主要出口之一

6这一页所有数字都出自这一份

p1135Count.cpp度量:边有多单向 + 自环 + 抓获率的两条曲线
// P1135 的度量程序 —— 这一页所有数字都出自这一份。
//
// `./p1135Count` 人看的版本
// `./p1135Count csv` 一行一项,给 scripts/check-viz.mjs 写断言用
//
// 四段:
// ① ★★★ 这张图**有多有向**:一条边 i → j 里,反过来 j → i 也存在的占多少;
// ② ⚠ 忘了「入队时标记」的后果:K[i] = 0 的自环 ⇒ 入队次数直接爆掉;
// ③ 「照搬反着 BFS」那个错法的抓获率(沿楼层数 n 走);
// ④ 「到不了」有多常见 —— `-1` 不是摆设。
#include <bits/stdc++.h>
using namespace std;
static bool CSV = false;
static void row(const char* key, const vector<long long>& v) {
if (!CSV) return;
printf("%s", key);
for (long long x : v) printf(",%lld", x);
printf("\n");
}
/** 照生成器造一组数据(和 p1135Gen.cpp 逐字一致)。 */
static void gen(unsigned seed, int maxN, int zeroPct, int& n, int& a, int& b, vector<int>& k) {
mt19937 rng(seed);
n = (int)(rng() % (unsigned)maxN) + 1;
a = (int)(rng() % (unsigned)n) + 1;
b = (int)(rng() % (unsigned)n) + 1;
k.assign(n + 1, 0);
for (int i = 1; i <= n; i++)
k[i] = ((int)(rng() % 100u) < zeroPct) ? 0 : (int)(rng() % (unsigned)(n + 1));
}
/** 正解:从 a 出发正着 BFS。 */
static int solve(int n, int a, int b, const vector<int>& k) {
vector<int> d(n + 1, -1);
queue<int> q;
d[a] = 0;
q.push(a);
while (!q.empty()) {
int cur = q.front();
q.pop();
if (cur == b) break;
for (int nxt : {cur + k[cur], cur - k[cur]}) {
if (nxt < 1 || nxt > n || d[nxt] != -1) continue;
d[nxt] = d[cur] + 1;
q.push(nxt);
}
}
return d[b];
}
/** 错法:从 b 出发,却还是照着「当前层」的 K 走。 */
static int solveRevBad(int n, int a, int b, const vector<int>& k) {
vector<int> d(n + 1, -1);
queue<int> q;
d[b] = 0;
q.push(b);
while (!q.empty()) {
int cur = q.front();
q.pop();
if (cur == a) break;
for (int nxt : {cur + k[cur], cur - k[cur]}) {
if (nxt < 1 || nxt > n || d[nxt] != -1) continue;
d[nxt] = d[cur] + 1;
q.push(nxt);
}
}
return d[a];
}
int main(int argc, char** argv) {
CSV = (argc > 1 && string(argv[1]) == "csv");
/* ① 这张图有多「有向」 */
{
long long edges = 0, both = 0, selfLoop = 0;
for (unsigned seed = 1; seed <= 300; seed++) {
int n, a, b; vector<int> k;
gen(seed, 60, 10, n, a, b, k);
for (int i = 1; i <= n; i++)
for (int j : {i + k[i], i - k[i]}) {
if (j < 1 || j > n) continue;
edges++;
if (j == i) { selfLoop++; both++; continue; }
// 反过来那条边在不在:站在 j 上,一步能不能回到 i
if (j + k[j] == i || j - k[j] == i) both++;
}
}
if (!CSV)
printf("① 这张图有多「有向」(300 组随机数据,楼层 ≤ 60):\n"
" 一共 %lld 条边,其中「反过来也走得通」的只有 %lld 条(%.1f%%);自环 %lld 条\n"
" ⇒ 绝大多数边是**单向**的 —— 所以「从终点反着照正图走」求的根本不是这道题\n\n",
edges, both, 100.0 * (double)both / (double)edges, selfLoop);
row("edges", {edges, both, selfLoop});
}
/* ② 忘了入队标记:入队次数 */
{
// 一个最小的例子:3 层,K = {0, 1, 1},从 1 走到 3
// 第 1 层 K = 0 ⇒ 自环,没有 vis 的话队列里永远是它自己
vector<int> k = {0, 0, 1, 1};
long long pushes = 1;
queue<int> q;
q.push(1);
bool blew = false;
while (!q.empty()) {
int cur = q.front();
q.pop();
if (cur == 3) break;
for (int nxt : {cur + k[cur], cur - k[cur]}) {
if (nxt < 1 || nxt > 3) continue;
q.push(nxt);
if (++pushes > 10000000LL) { blew = true; break; }
}
if (blew) break;
}
if (!CSV)
printf("② 忘了「入队时标记」:3 层、K = {0, 1, 1}、从 1 走到 3 ——\n"
" %s(第 1 层 K = 0 是个自环,队列里出不来)\n\n",
blew ? "入队 1000 万次还没完,掐掉" : "居然跑完了?");
row("noVisBlew", {blew ? 1 : 0});
}
/* ③ 「照搬反着 BFS」的抓获率 */
{
vector<long long> caught;
vector<int> ns = {4, 8, 20, 60};
if (!CSV) printf("③ 「从终点反着照正图走」那个错法的抓获率(300 轮):\n");
for (int maxN : ns) {
int c = 0;
for (unsigned seed = 1; seed <= 300; seed++) {
int n, a, b; vector<int> k;
gen(seed, maxN, 10, n, a, b, k);
if (solve(n, a, b, k) != solveRevBad(n, a, b, k)) c++;
}
caught.push_back(c);
if (!CSV) printf(" 楼层 ≤ %2d:%3d / 300\n", maxN, c);
}
if (!CSV) printf("\n");
row("revBadCatch", caught);
}
/* ④ 「到不了」有多常见 */
{
vector<long long> unreach;
vector<int> ns = {4, 8, 20, 60};
if (!CSV) printf("④ 正解输出 -1(到不了)的比例(300 轮):\n");
for (int maxN : ns) {
int c = 0;
for (unsigned seed = 1; seed <= 300; seed++) {
int n, a, b; vector<int> k;
gen(seed, maxN, 10, n, a, b, k);
if (solve(n, a, b, k) == -1) c++;
}
unreach.push_back(c);
if (!CSV) printf(" 楼层 ≤ %2d:%3d / 300\n", maxN, c);
}
if (!CSV) printf(" ⇒ -1 不是摆设,随手造的数据里就有一大半到不了\n\n");
row("unreach", unreach);
/* ⑤ ★★★ 第 ③ 段那条抓获率曲线为什么会掉头:把「两版都输出 -1」的轮次剔掉再看
* ([P1746](/sol/p1746/) 那条:「一致」有两种 —— 都算对了,和都没算。) */
vector<long long> solvable, caughtIn;
if (!CSV) printf("⑤ 把「正解就是 -1」的轮次剔掉,再看那个错法的抓获率:\n");
for (size_t t = 0; t < ns.size(); t++) {
int maxN = ns[t];
int tot = 0, c = 0;
for (unsigned seed = 1; seed <= 300; seed++) {
int n, a, b; vector<int> k;
gen(seed, maxN, 10, n, a, b, k);
int ref = solve(n, a, b, k);
if (ref == -1) continue;
tot++;
if (ref != solveRevBad(n, a, b, k)) c++;
}
solvable.push_back(tot);
caughtIn.push_back(c);
if (!CSV)
printf(" 楼层 ≤ %2d:有解的 %3d 轮里抓到 %3d 轮(%.1f%%)\n",
maxN, tot, c, 100.0 * (double)c / (double)tot);
}
if (!CSV) printf(" ⇒ 曲线不再掉头 —— 掉下去的那一段是「两版一起输出 -1」\n\n");
row("solvable", solvable);
row("caughtInSolvable", caughtIn);
}
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

7一张总表

版本 错在哪 官方样例 300 轮对拍(楼层 ≤ 60) 结果
⚠⚠ p1135RevBad 边有方向,却照正图反着走 ✗ 给 -1 被抓 49(有解的 54 轮里 25) WA
⚠⚠ p1135NoVis 忘了入队时标记 Kᵢ = 0 那档直接出不来 TLE
p1135Rev 0 ★ AC(要多存一张反图)
p1135 0 AC
这一页记住三句话
  1. ★★★ 「从终点反着跑一次」缺一个主语:这张图的边是不是无向的。 P1379(每步可逆)、P1747P1332 都成立; 这道题的边四分之三是单向的(10152 条里只有 2516 条反过来也走得通), 同一个动作当场就错。★ 真要反着做,先建反图
  2. ★★ 题面里那个不起眼的 0 ≤ Kᵢ 是有牙齿的。 Kᵢ = 0 就是一个自环 —— 忘了「入队时标记」的话, 3 层的数据就能让它入队一千万次还出不来(而官方样例里没有 0,挡不住)。
  3. ★★★ 看到抓获率往下走,先问「这一段里两边比的还是同一件事吗」。 楼层 ≤ 60 那档抓获数从 62 掉到 49,而那 300 轮里有 246 轮正解本来就是 -1; 把它们剔掉之后,抓获率从 14.3% 一路升到 46.3%,不再掉头。