0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库, 连原题那张图一起(这道题的「朝内朝外」全靠它才看得懂)。
转录自洛谷 P1563,日期见页头。两边不一致时信原站。
题目背景:NOIP2016 提高组 D1T1。
题目描述
小南有一套可爱的玩具小人,它们各有不同的职业。
有一天,这些玩具小人把小南的眼镜藏了起来。小南发现玩具小人们围成了一个圈, 它们有的面朝圈内,有的面朝圈外。如下图:

这时 singer 告诉小南一个谜题:「眼镜藏在我左数第 3 个玩具小人的右数第 1 个玩具小人的
左数第 2 个玩具小人那里。」
小南发现,这个谜题中玩具小人的朝向非常关键,因为朝内和朝外的玩具小人的左右方向是相反的: 面朝圈内的玩具小人,它的左边是顺时针方向,右边是逆时针方向; 而面向圈外的玩具小人,它的左边是逆时针方向,右边是顺时针方向。
小南一边艰难地辨认着玩具小人,一边数着:
singer 朝内,左数第 3 个是 archer
archer 朝外,右数第 1 个是 thinker
thinker 朝外,左数第 2 个是 writer
所以眼镜藏在 writer 这里!
虽然成功找回了眼镜,但小南并没有放心。如果下次有更多的玩具小人藏他的眼镜,或是谜题的长度更长, 他可能就无法找到眼镜了。所以小南希望你写程序帮他解决类似的谜题。这样的谜题具体可以描述为:
有 n 个玩具小人围成一圈,已知它们的职业和朝向。现在第 1 个玩具小人告诉小南一个包含 m
条指令的谜题,其中第 z 条指令形如「向左数/右数第 s 个玩具小人」。
你需要输出依次数完这些指令后,到达的玩具小人的职业。
输入格式
输入的第一行包含两个正整数 n、m,表示玩具小人的个数和指令的条数。
接下来 n 行,每行包含一个整数和一个字符串,以逆时针为顺序给出每个玩具小人的朝向和职业。
其中 0 表示朝向圈内,1 表示朝向圈外。保证不会出现其他的数。
字符串长度不超过 10 且仅由英文字母构成,字符串不为空,并且字符串两两不同。整数和字符串之间用一个空格隔开。
接下来 m 行,其中第 i 行包含两个整数 aᵢ、sᵢ,表示第 i 条指令。
若 aᵢ = 0,表示向左数 sᵢ 个人;若 aᵢ = 1,表示向右数 sᵢ 个人。
保证 aᵢ 不会出现其他的数,1 ≤ sᵢ < n。
输出格式:输出一个字符串,表示从第一个读入的小人开始,依次数完 m 条指令后到达的小人的职业。
数据范围(原站是一张 20 行的测试点表,这里把「同上」的 ^ 都展开了):
| 测试点 | n |
m |
全朝内 | 全左数 | sᵢ = 1 |
职业长度为 1 |
|---|---|---|---|---|---|---|
| 1 | 20 | 1000 | ✓ | ✓ | ✓ | ✓ |
| 2 | 20 | 1000 | ✗ | ✓ | ✓ | ✓ |
| 3 | 20 | 1000 | ✓ | ✗ | ✓ | ✓ |
| 4 | 20 | 1000 | ✗ | ✗ | ✓ | ✓ |
| 5 | 20 | 1000 | ✓ | ✓ | ✗ | ✓ |
| 6 | 20 | 1000 | ✗ | ✓ | ✗ | ✓ |
| 7 | 20 | 1000 | ✓ | ✗ | ✗ | ✓ |
| 8 | 20 | 1000 | ✗ | ✗ | ✗ | ✓ |
| 9 | 20 | 1000 | ✓ | ✓ | ✓ | ✗ |
| 10 | 20 | 1000 | ✗ | ✓ | ✓ | ✗ |
| 11 | 20 | 1000 | ✓ | ✗ | ✓ | ✗ |
| 12 | 20 | 1000 | ✗ | ✗ | ✓ | ✗ |
| 13 | 20 | 1000 | ✓ | ✓ | ✗ | ✗ |
| 14 | 20 | 1000 | ✗ | ✓ | ✗ | ✗ |
| 15 | 20 | 1000 | ✓ | ✗ | ✗ | ✗ |
| 16 | 20 | 1000 | ✗ | ✗ | ✗ | ✗ |
| 17 | 10⁵ | 10⁵ | ✓ | ✓ | ✗ | ✗ |
| 18 | 10⁵ | 10⁵ | ✗ | ✓ | ✗ | ✗ |
| 19 | 10⁵ | 10⁵ | ✓ | ✗ | ✗ | ✗ |
| 20 | 10⁵ | 10⁵ | ✗ | ✗ | ✗ | ✗ |
其中几列的意思是:全朝内 = 该测试点保证所有玩具小人都朝向圈内;
全左数 = 所有指令都向左数(对任意 z,a_z = 0);
sᵢ = 1 = 所有指令都只数 1 个人;职业长度为 1 = 所有职业都是长度为 1 的字符串。
输入输出样例
输入
7 3 0 singer 0 reader 0 mengbier 1 thinker 1 archer 0 writer 1 mogician 0 3 1 1 0 2
输出
writer
样例 1 就是【题目描述】里那个例子。上面那段输出是仓库里的 p1563.cpp 真跑出来的。
输入
10 10 1 C 0 r 0 P 1 d 1 e 1 m 1 t 1 y 1 u 0 V 1 7 1 1 1 4 0 5 0 3 0 1 1 6 1 2 0 8 0 4
输出
y
样例 2:10 个人、10 条指令,答案是 y。
1先在纸上把方向定下来(这一步不做完,代码写不对)
输入是按逆时针顺序给的,所以下标 +1 就是逆时针、-1 就是顺时针。
把题面那两句话翻成四种情况:
朝内(0) 左数(0) -> 左边是顺时针 -> -s
朝内(0) 右数(1) -> 右边是逆时针 -> +s
朝外(1) 左数(0) -> 左边是逆时针 -> +s
朝外(1) 右数(1) -> 右边是顺时针 -> -s
朝向和方向一样就往回走(-s),不一样就往前走(+s):
cur = (dir[cur] == a) ? cur - s : cur + s;四个 if 抄错一个就全错,而这一句压根没有可抄错的地方。
⇒ 第 47 章那条老规矩:
能改成「一整类错误不存在」的写法,就别留着「小心别写错」。
⚠ 还有一处一句话带过、但错了就全错的:朝向只看出发的那个人。 一条指令认的是「我」的朝向,走过路过的人朝哪边跟这一条指令没关系。
2第 ① 版:老老实实一步一步走(对,但跑不完)
「向左数第 s 个」就真的走 s 步 —— 题意的逐字翻译,而且它是对的:
走一步和一次走 s 步落点完全一样。
// P1563 的第 ① 版:老老实实一步一步走//// 「向左数第 s 个」就真的走 s 步,每一步问一次「现在这个人朝哪边」—— 题意的逐字翻译,// 而且**它是对的**:走一步和一次走 s 步,落点完全一样(朝向只看出发的那个人)。//// ⚠ 它只是慢:n, m <= 1e5,而 s 可以到 n - 1 ⇒ 最坏要走 m * (n-1) ≈ 10^10 步。// 正文第 ④ 步有秒表:本机 n = m = 1e5 的最坏数据要跑十几秒,题目限时 1 秒。//// ★ 这一版仍然值得写出来:它是标准答案,也是对拍里那一栏「一定对」的东西。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); // 1e5 行输入,关掉同步(第 45 章量过) cin.tie(nullptr); int n, m; cin >> n >> m; vector<int> dir(n); vector<string> job(n); for (int i = 0; i < n; i++) cin >> dir[i] >> job[i]; int cur = 0; for (int i = 0; i < m; i++) { int a, s; cin >> a >> s; int step = (dir[cur] == a) ? -1 : 1; // 方向只由**出发的那个人**决定 for (int k = 0; k < s; k++) cur = (cur + step + n) % n; // 一步一步挪 } cout << job[cur] << '\n'; return 0;}点「运行 ▶」看结果
本机实测(B 机:原生 Ubuntu 7.0.0-30 / i5-10210U 8 线程 / 18 GB,2026-08-26,独占;
数据是 p1563GenBig.cpp 造的最坏形状:每条指令都取 s = n - 1):
n = m |
要走的步数 | ① 一步一步 | ② 一次跳过去 |
|---|---|---|---|
| 10 000 | 10⁸ | 1.00 秒 | 0.00 秒 |
| 30 000 | 9 × 10⁸ | 9.01 秒 | 0.01 秒 |
| 100 000 | ★ 10¹⁰ | ★ 108.47 秒 | ★ 0.02 秒 |
生成器里那句 s = n - 1 是故意的:顺手写 s = 随机 1..n-1,平均只有 n / 2,
量出来的是「一般有多慢」,而我们要问的是「它到底能不能跑完」。
⇒ 要量上限,就把旋钮拧到头(第 51 章那次是「要又大又深」,
这一次是「要顶格的 s」)。
3⚠ 第 ② 版的第一个坑:C++ 的 % 对负数给的是负数
一次跳 s 格,很自然地写成:
cur = (dir[cur] == a) ? cur - s : cur + s;
cur = cur % n; // ⚠ 少了一步
// P1563 的错法演示:忘了「C++ 的 % 对负数给负余数」//// cur = (cur - s) % n; // ⚠ cur 变成负数,下标就飞出去了//// ★ 为什么这份用 job.at(cur) 而不是 job[cur](第 45 章那一课):// `job[cur]` 在 cur 为负时是**未定义行为** —— 它可能崩、可能打出一串乱码、// 也可能在 -O2 下被优化成别的样子,**每台机器给的结果都不一样,教材写不了**。// 换成 .at() 就变成一个确定的 std::out_of_range,谁跑都一样。//// ⇒ 演示错误写法时,要把那个错**钉成可复现的**,不能留给 UB 去发挥。//// ⚠ 另一条(做这一页时量出来的):**一次「崩溃退出」在本机要 1.17 秒**// (abort 走的是 core dump 那条路),而正常跑完只要几毫秒。// 所以这一版只在样例上跑一次,**不进 300 轮对拍表** —— 那会让 check:viz 多花六分钟。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); // 1e5 行输入,关掉同步(第 45 章量过) cin.tie(nullptr); int n, m; cin >> n >> m; vector<int> dir(n); vector<string> job(n); for (int i = 0; i < n; i++) cin >> dir[i] >> job[i]; int cur = 0; for (int i = 0; i < m; i++) { int a, s; cin >> a >> s; cur = (dir.at((size_t)cur) == a) ? cur - s : cur + s; cur = cur % n; // ⚠ 少了 + n:-3 % 7 还是 -3 } cout << job.at((size_t)cur) << '\n'; return 0;}点「运行 ▶」看结果
数学上的取模总落在 [0, n),而 C++ 的 % 保的是商向零取整,
于是负数进去、负数出来。下标一负,vector 一访问就飞。
cur = (cur % n + n) % n; // ★ 这里 s < n,减完最小是 -(n-1),一句 + n 就够★ 这道题样例第一条指令就踩到它(0 - 3 = -3)—— 属于「样例挡得住」的那种坑。
⚠ 但要挡得住有个前提:你真的跑了样例。
// 把「负数取模」摊开看:同一组指令,两种写法的下标逐步对照//// 用法:./p1563Trace 跑样例 1(7 个人 3 条指令)// ./p1563Trace csv 只打 `键,值`,给 check:viz 用//// ★ 看第一步就够了:`0 - 3 = -3`,而 C++ 的 `-3 % 7` 给的是 **-3**,不是 4。// 下标一旦为负,`vector` 一访问就飞 —— 而**样例的第一条指令就会踩到它**。
#include <bits/stdc++.h>using namespace std;
int main(int argc, char** argv) { bool csv = (argc > 1 && string(argv[1]) == "csv"); const int n = 7; int dir[7] = {0, 0, 0, 1, 1, 0, 1}; const char* job[7] = {"singer", "reader", "mengbier", "thinker", "archer", "writer", "mogician"}; int cmd[3][2] = {{0, 3}, {1, 1}, {0, 2}};
int cur = 0; int badAt = 0, badIdx = 0; // 只写 % n 的那版:第几步出界、出界成了几 if (!csv) printf("步 指令 算出来 正确 (cur %% n + n) %% n 只写 cur %% n\n"); for (int i = 0; i < 3; i++) { int a = cmd[i][0], s = cmd[i][1]; int raw = (dir[cur] == a) ? cur - s : cur + s; int good = (raw % n + n) % n; int bad = raw % n; if (!badAt && bad < 0) { badAt = i + 1; badIdx = bad; } if (!csv) printf("%d %s %d %3d %d (%s) %d%s\n", i + 1, a == 0 ? "左数" : "右数", s, raw, good, job[good], bad, bad < 0 ? " <- 负下标!" : ""); cur = good; // ★ 往下走的是正确的那一支 } if (csv) { printf("answer,%s\nbadAt,%d\nbadIdx,%d\n", job[cur], badAt, badIdx); } else { printf("\n正确写法落在 %d 号(%s)—— 正是样例的答案\n", cur, job[cur]); printf("只写 %% n 的那版:第 %d 步就成了 %d —— vector 一访问就飞\n", badAt, badIdx); } return 0;}点「运行 ▶」看结果
这个错版是崩溃退出的,而本机一次崩溃要 1.17 秒(abort 走的是 core dump 那条路),
正常跑完只要几毫秒。⇒ 它只在样例上跑一次,不进 300 轮的对拍表 ——
否则光是崩 300 次就要六分钟,check:viz 会平白慢一大截。
4★ 正解:一句话定方向 + 一次跳过去
// P1563 玩具谜题(NOIP 2016 提高组 D1T1)—— 能 AC 的那一版//// 输入是**按逆时针顺序**给的,所以下标 +1 就是逆时针、-1 就是顺时针。// 于是四种情况可以摆成一张表(0 = 朝内 / 朝外,a = 0 左数 / 1 右数)://// 朝内(0) 左数(0) -> 左边是顺时针 -> -s// 朝内(0) 右数(1) -> 右边是逆时针 -> +s// 朝外(1) 左数(0) -> 左边是逆时针 -> +s// 朝外(1) 右数(1) -> 右边是顺时针 -> -s//// ★ 盯着这张表看十秒:**朝向和方向一样就往回走,不一样就往前走** —— 一句 dir == a 就够了。// 四个 if 抄错一个就全错,而这一句压根没有可抄错的地方。//// ⚠ C++ 的 % 对负数给的是负余数(-3 % 7 == -3),所以要 (x % n + n) % n。// 这里 s < n,减完最小是 -(n-1),一句 + n 就够了。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); // 1e5 行输入,关掉同步(第 45 章量过) cin.tie(nullptr); int n, m; cin >> n >> m; vector<int> dir(n); vector<string> job(n); for (int i = 0; i < n; i++) cin >> dir[i] >> job[i]; int cur = 0; for (int i = 0; i < m; i++) { int a, s; cin >> a >> s; cur = (dir[cur] == a) ? cur - s : cur + s; // ★ 一句话代替四个 if cur = (cur % n + n) % n; // ⚠ 负数取模 } cout << job[cur] << '\n'; return 0;}点「运行 ▶」看结果
两行是全部:
cur = (dir[cur] == a) ? cur - s : cur + s; // 方向:一样就往回,不一样就往前
cur = (cur % n + n) % n; // 取模:先 % 再 + n 再 % 一次
★ 对拍:check:viz 每次跑 600 轮(300 轮随机 + 300 轮「全朝内 + 全左数 + s 尽量大」,
后者专门逼下标往负的方向跑),第 ① 版和正解逐字节相同。
5三个版本并排
| 版本 | 做法 | 样例 | n = m = 10⁵ |
能过吗 |
|---|---|---|---|---|
① p1563Step |
一步一步挪 | ✓ | 108.47 秒 | ✗ 后四个点 TLE |
② p1563Neg |
一次跳,但只写 % n |
✗ 第一条指令就崩 | — | ✗ RE |
③ p1563 |
一次跳 + (x % n + n) % n |
✓ | ★ 0.02 秒 | ★ 能 |
- 职业名带空格:原题样例里
0 mengbier后面是有个空格的。 用cin >> s读就自动没事(它按空白切),用getline就得自己处理。 n和m都到 10⁵,而且每行还有一个字符串 ——cin记得ios::sync_with_stdio(false)(第 45 章量过:读 10⁵ 行的差距不止一倍)。
- ★ 四种情况能缩成一句
dir[cur] == a。 不是为了短,是为了 让「抄错一个 if」这类错误不存在。 - ⚠ C++ 的
%对负数给负数,下标要写成(x % n + n) % n。 这道题样例第一条指令就踩到它 —— 前提是你真的跑了样例。 - ★ 「大数据」不等于「最坏数据」。 顺手随机的
s平均只有n / 2, 要问「能不能跑完」,就得把s拧到n - 1。