← 目录 第 5 / 60 节 全部 60 节

05 · 回顾一:基础语法综合训练

90 分钟 5 道真题,只考第 1~4 节的输入输出、循环、图形规律与一维数组:用一套限时流程把“会语法”练成“会做题”,并留下 6 条交卷前的检查清单。
⭐ 已学范围回顾训练入门+建议 90 分钟回顾5 题

这一节不讲新东西。5 道题、90 分钟,只考第 1~4 节学过的四件事:读入输出、循环、图形规律、一维数组。目标不是“能做出来”,而是“限时内稳定做出来”——同一道题,赛场上多花 10 分钟,就等于少做一道后面的大题。

决定分数的不是“你学过什么”,而是“读完题 30 秒内知不知道要开几个数组、循环怎么写”。

先看清这 5 道题分别考什么,再决定先做哪道:

标记题目主要考点难度建议用时
AP1909 买铅笔循环 + 向上取整 + 取最小普及-12 分钟
BP1014 Cantor 表图形规律,循环找第 k 条斜线普及-13 分钟
CP1047 校门外的树数组标记,区间覆盖普及-15 分钟
DP1980 计数问题while 拆位,逐个数字统计普及-14 分钟
EP1328 生活大爆炸版石头剪刀布数组 + 取模 + 查胜负表普及/提高-20 分钟
90 分钟怎么切

总原则:先把会做的分全部拿到,再回头啃没思路的。题目难度不是按顺序排的,别顺着往下做;规则题(B)想不出就跳过,它值得的时间只有 5 分钟。

阶段时间段(分钟)做什么
通读 5 题0 ~ 5标“会做 / 有思路 / 完全不会”,此阶段不许动笔写代码
A 买铅笔5 ~ 17读 2 + 写 6 + 测 4,必须一次写对
B Cantor 表17 ~ 305 分钟没思路就跳过,最后再回来
C 校门外的树30 ~ 45标记法,两个循环,重点全在边界
D 计数问题45 ~ 59代码极短,卡住必是拆位拆错了循环变量
E 石头剪刀布59 ~ 79抄表 6 + 写 8 + 测 6
检查79 ~ 90只查数组大小、初值、输出格式,不改算法

单题内部再切一刀:想 3 分钟(在纸上写清楚)、写 8 分钟、测 4 分钟。“想”的 3 分钟里只干四件事:抄下数据范围、抄下输出格式、手算一遍样例、写下要用的数组大小。

如果整块时间更充裕,多出来的部分全给最没把握的那道题,不要平均分给 5 题——能做出来的题不需要第二遍,做不出来的题才需要。

“想 3 分钟”不是发呆。这 3 分钟里,纸上必须出现 4 行东西,写完再碰键盘:

  1. 数据范围:n、l、m 各是多少?据此写死数组大小,决定 int 还是 long long。
  2. 输出格式:几个数、用什么分隔、行末要不要空格、有没有固定的符号(比如 “/”)。
  3. 边界:n=1、l=0、全部相等、全是 0,这几种输入你的程序会不会崩或算错。
  4. 样例手算:题目给的输入自己在纸上走一遍。答案对不上,是题读错了,不是代码错了。

再看一张“条件反射表”。题面出现这些词,思路应该立刻跳出来——5 道训练题全在这张表里:

题面里的词第一反应
“至少”“装满”“够不够”向上取整:(n + p - 1) / p
“出现几次”“有多少个”计数器 cnt,初值写 0
“每棵树”“每个点” + 范围不大开数组打标记,最后扫一遍统计
“周期”“循环出现”下标取模 a[k % na]
“连续的一段”“最长”边扫边维护 cur,随时 max 更新答案
“含端点”“0 到 l”循环边界写 <=,元素个数是 l+1

表里有三个动作,最简的题也要用:取极值、向上取整、下标取模。下面三张卡不是新知识,只是把这三个写法再拆一遍——看着眼熟就快速扫过,看着陌生说明前面某节没吃透,回去补。

新函数拆解 · min / max(取极值)
min(a, b) max(a, b)
组成含义 / 格式要求
min(a, b)返回两值中较小的那个;a、b 的类型必须一致(都 int 或都 long long)。
max(a, b)返回较大的那个。
最常用形态ans = max(ans, x):边扫边更新答案,扫完 ans 就是最大值。
只能比两个三个以上要嵌套 max(max(a,b),c),或先塞进数组再循环。
初值ans 必须先有初值;求最大值常用 a[0](或一个极小值)打底。
最小可运行示例
示例
int a[5] = {3, 9, 2, 9, 5};
int mx = a[0];                       // 拿第一个数当擂主
for (int i = 1; i < 5; ++i)
    mx = max(mx, a[i]);              // 谁大谁当擂主
cout << mx << "\n";                  // 9
注意

ans 用 0 初始化去求最大值,遇到全是负数就错;最稳的初值是 a[0]。

min 与 max 的两个参数类型不同时可能编译不过,统一写 int 最省事。

新函数拆解 · 整数向上取整 (n + p - 1) / p
(n + p - 1) / p // 正整数 n 除以正整数 p 的向上取整
组成含义 / 格式要求
n一共有多少(要凑够多少)。
p每份多少(每包几支、每页几行)。
+ p - 1先补上“差一点点”的那一份,让整数除法多进 1。
/ p整数除法本身向下取整;补过之后再向下取整,结果等于原式向上取整。
前提n、p 都是正整数。n 恰好被 p 整除时不会多算:n=60、p=2 → (60+1)/2 = 30,等于 60/2。
最小可运行示例
示例
int n = 57, p = 2;
cout << n / p << "\n";               // 28  ← 只有 56 支,不够
cout << (n + p - 1) / p << "\n";     // 29  ← 58 支,够
注意

凡题面出现“至少”“够不够”“装满”“凑够”这类词,八成要向上取整。

n 接近 10⁹ 时 n+p-1 会溢出 int,改用 long long 写式子。

新函数拆解 · 取模取循环下标 a[k % na]
a[k % na] // 长度 na 的周期序列,第 k 项(k 从 0 数)
组成含义 / 格式要求
k第几项,从 0 开始编号。
na周期长度,也就是数组元素个数。
k % na余数范围 0 ~ na-1,正好全是合法下标;k 一直加,下标就在周期里绕圈。
注意如果一开始按下标从 1 数,就得写 (k-1) % na,两种编号方式不能混。
最小可运行示例
示例
int a[3] = {4, 1, 7};               // 一个周期的 4 1 7
for (int k = 0; k < 8; ++k)
    cout << a[k % 3] << " ";         // 4 1 7 4 1 7 4 1
注意

两个长度不同的周期序列要各算各的下标:a[k % na] 与 b[k % nb],不能共用一个。

k 为负时 % 会得到负数,这里 k 一定非负,安全。

策略讲完,先看一道示范。这题不难,但“边扫边维护一个状态”的套路会在本组 5 道题里反复出现。

例题 · 最长连号洛谷 P1420普及-去洛谷做这题 ↗

输入长度为 n 的正整数序列 a,求最长的连号长度。连号指:序列中一段连续的子串,其中每一项正比如前一项大 1。例如 2 3 4 5 是连号(长度 4),而 2 3 5 不是(3 到 5 断了)。

输入格式:第一行一个整数 n(1 ≤ n ≤ 10⁴);第二行 n 个整数 aᵢ(1 ≤ aᵢ ≤ 10⁹),空格分隔。

输出格式:一个整数,最长连号的长度。

输入: 8 1 5 6 2 3 4 5 6 输出: 5
思路拆解:只需要维护一个数

题目要的是“最长的一段”,不是“最长上升子序列”。差别只有一条:连号要求差恰好为 1,断了就必须重新数,不能跳过中间某个数接着数。

于是只需要维护一个状态:cur = 以当前位置结尾的连号有多长。扫描时只有两种转移:

  1. 和前一数差 1 → 接上了,cur 加 1;
  2. 否则 → 断了,cur 重置为 1(当前这个数自己算一段长度 1 的连号)。

每走一步都用 ans = max(ans, cur) 更新答案。因为“最长的那一段”一定在某一步结尾,所以扫描过程中记下 cur 的最大值就够了,不需要存第二遍。

复杂度:一次线性扫描 O(n),n ≤ 10⁴,耗时可以忽略。

把样例走一遍:cur 与 ans 怎么变

样例 a = 1 5 6 2 3 4 5 6。i 从 1 开始(要比较 a[i-1]):

ia[i]与前一数差 1?curans
01—(起点)11
15否,断11
26是22
32否,断12
43是22
54是33
65是44
76是55

答案 5(子串 2 3 4 5 6)。注意 i=3 时 cur 必须回到 1,不是回到 0:2 自己也是一段。

完整代码
#include <bits/stdc++.h>
using namespace std;

int a[10005];                    // 全局数组默认全 0;n ≤ 10⁴,10005 留余量

int main() {
    int n;
    cin >> n;
    for (int i = 0; i < n; ++i) cin >> a[i];

    int ans = 1, cur = 1;        // cur:以当前元素结尾的连号长度
    for (int i = 1; i < n; ++i) {
        if (a[i] == a[i - 1] + 1) ++cur;   // 接上了,长度加一
        else cur = 1;                      // 断了,从这个数重新数
        ans = max(ans, cur);               // 每一步都可以成为答案
    }
    cout << ans << "\n";
    return 0;
}
逐行拆解
代码片段含义与理由
int a[10005];开在 main 外面(全局):默认全 0,也不会挤爆栈。数据范围 n ≤ 10⁴,10005 留出余量,开数组前先看数据范围,这是要养成肌肉记忆的第一件事。
for (int i = 0; i < n; ++i) cin >> a[i];必须先把整个数组读进来:判断要回头看 a[i-1],边读边算做不到。
int ans = 1, cur = 1;n ≥ 1,至少一个数,答案至少是 1,所以两个数都从 1 起步。
for (int i = 1; i < n; ++i)从 1 开始,因为循环体里要用 a[i-1];从 0 开始就会访问 a[-1],越界。
a[i] == a[i - 1] + 1连号的定义就是“差恰好 1”。写成 a[i] > a[i-1] 会把上升序列误判成连号,WA。
else cur = 1;断了之后,当前元素自己是一段长度 1 的连号。写成 cur = 0 会让下一段整段少算一个。
ans = max(ans, cur);放在 if/else 之外,两条分支都要更新;写进 if 里面就会漏掉“整段从头到尾都连号”的情况。
cout << ans << "\n";只输出一个整数加换行。题目没要求输出长度以外的任何东西。
这 5 道题里最常见的 6 个丢分点

数组开小 → RE。看到 n ≤ 10⁴ 就开 10005,看到 n ≤ 200 就开 205。数组大小永远从数据范围推,不要凭感觉写 100。

循环边界差一 → WA。“0 到 l 有 l+1 个数”“区间含端点”这类句子必须逐字读,写成 j < v 就少砍一棵树。

行末多一个空格 → WA。一行输出多个数时,最后一个数后面只能换行;严格评测(NOIP 系列)会逐字节比对。

该向上取整写成整除 → WA。题面出现“至少”“够”,先想 (n+p-1)/p,样例里 n 不是 p 的倍数时立刻能暴露这个错。

变量没初值 / 复用了上一题的变量 → 答案随机。局部变量不初始化就是垃圾值,cnt、sum、ans 一律写 = 0 或 = 1。

10⁶ 次输出用 endl → TLE。endl 每次刷新缓冲区,比 "\n" 慢一个数量级;循环里只用 "\n"。

自己动手复现

关掉页面,把 P1047 用“标记法”写一遍再提交。

动手前先回答三个问题:数组要开多大?两个循环的边界各是什么?区间端点算不算?三个都答对再写。

写完把 l 改成 10、m 改成 2、两个区间改成 [1,3] 和 [2,5],纸算答案:0~10 共 11 棵,被砍的是 1,2,3,4,5,剩 6 棵。程序对不上就回头查边界。

改一改:如果 l 变成 10⁸、m 变成 10⁵,标记法为什么直接废了?先想清楚再往下节走。

卡住时按顺序做这三件事

1. 手算一组更小的数据:把 n 缩到 3、把 l 缩到 5,纸上算出答案再和程序比。第一条对不上的输入,就指出了 bug 在哪。

2. 打印中间变量:临时在循环里加一句 cout << i << " " << cur << "\n";,看它是不是按你预期的变;提交前删掉这句。

3. 重读题面最后两行:八成是边界、输出格式、多算或少算一个数,而不是算法想错了。

交卷前 60 秒,只查这 6 项

数组大小 ≥ 数据范围上限 + 5。

该用 long long 的地方都用了吗(和、积、平方)。

输出格式:几个数?空格还是换行?行末有没有多余空格?

边界跑过没有:n=1、l=0、全部相同、全是最小值。

计数变量初值写了没有。

题目给的样例,输出和标准答案逐字一样吗。

下面是 5 道训练题。先自己做,卡满 25 分钟再看提示;提示看完还没思路,再点开思路;代码一定要自己敲一遍再提交。

本次训练 · 共 5 题

做题方式

限时 100 分钟。每题先自己写 20~25 分钟;写不出来先看提示,再给自己 10 分钟;还不行才点开思路。每题都独立限时,不要一题耗死。

大题 挑战题 1 · 买铅笔
洛谷 P1909普及-去洛谷做这题 ↗

P 老师要买 n 支铅笔。商店只有 3 种包装,每种包装里的支数和价格都不同。为了公平,她决定只买同一种包装;包装不能拆开,所以可能买到的支数超过 n。求买够至少 n 支最少要花多少钱。

输入格式:第一行一个正整数 n;接下来 3 行,每行两个正整数,依次表示一种包装的支数与价格。7 个数都不超过 10000。

输出格式:一个整数,表示最少花费。

引导性提示(先只看这里)

三种包装互不影响:先分别算“只买第一种要多少”“只买第二种”“只买第三种”,再挑最小的。

买几包?拿 n = 57、2 支一包算一遍:28 包只有 56 支,不够。

“不够就得再补一包”这件事,会让结果比直接整除多 1。想想怎么用一行整数运算写出来。

想看思路了?点开完整思路

对每种包装单独结算:包数 × 价格,就是“只用这一种包装”的总花费。

包数 = n ÷ 每包支数 向上取整。整数除法的写法是 (n + p - 1) / p:先补到“刚好差一点点也进一”。

三种花费取 min,就是答案。

为什么不能“先挑单价最低的”?单价不是整数,而且凑包会浪费;n=57 时第三种包装单价更低、浪费更多,但总价仍然最低——只有结算总价才能比较。

复杂度 O(1),只看 3 组数据。int 足够:包数 ≤ 10⁴,花费 ≤ 10⁸。

还是不会?点开题解代码与拆解
参考代码
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    cin >> n;

    int best = INT_MAX;               // 擂主:int 的最大值,保证第一次一定被替换
    for (int i = 0; i < 3; ++i) {
        int p, c;
        cin >> p >> c;                // p 支一包,c 元一包
        int packs = (n + p - 1) / p;  // 向上取整:不足一包也要买一包
        best = min(best, packs * c);  // 三种包装各自结算,取最小
    }
    cout << best << "\n";
    return 0;
}
关键片段拆解
片段作用
int best = INT_MAX;INT_MAX 是 int 上限 2147483647,当“还没选过任何方案”的初值;若写 best = 0,min 永远是 0。
for (int i = 0; i < 3; ++i) { cin >> p >> c; }三种包装固定是 3 组,边读边算,不用开数组存下来。
(n + p - 1) / p整数向上取整的标准写法。n=57、p=2:(57+1)/2 = 29;直接写 n/p 得 28,少一包。
packs * c总价 = 包数 × 单价。本题最大约 10⁸,int 不会溢出。
best = min(best, packs * c);打擂台:每结算完一种包装就更新一次答案,循环结束即最优。
这题最容易错的地方
  • 写成 n / p,n 不是 p 的倍数时少买一包,WA。
  • 只按“每支单价最低”选包装,忽略凑包浪费,样例就错。
  • best 初始化为 0(或忘了初始化),min 结果恒为 0,WA。
  • 只读了第一行包装数据就输出,后两组根本没读,答案必然偏大。
大题 挑战题 2 · Cantor 表
洛谷 P1014普及-去洛谷做这题 ↗

Cantor 把有理数按斜线排成一张表,并按 Z 字形依次编号:第一项 1/1;接着 1/2、2/1;接着 3/1、2/2、1/3;接着 1/4、2/3、3/2、4/1;接着 5/1、4/2、3/3、2/4、1/5……也就是第 1 条斜线 1 个数、第 2 条 2 个数、第 3 条 3 个数,且相邻两条斜线的走向相反。

输入格式:一个整数 N(1 ≤ N ≤ 10⁷)。

输出格式:表中的第 N 项,格式为“分子/分母”,例如 1/4。

引导性提示(先只看这里)

别急着求坐标。先数一数:第 k 条斜线上有几个数?前 k 条斜线一共几个数?

1+2+…+k 这个和你已经会算。拿它跟 N 比,就能判断 N 落在第几条斜线上。

同一条斜线上的数,分子与分母的和是固定的。知道 N 是这条斜线的第几个,两个数就都定了。

想看思路了?点开完整思路

第 k 条斜线有 k 个数,前 k 条一共 k(k+1)/2 个数。所以找最小的 k 使 k(k+1)/2 ≥ N,它就在第 k 条斜线上。

第 k 条斜线之前的数是 k(k-1)/2 个,于是 N 在这条斜线上是第 pos = N − k(k-1)/2 个(从 1 数)。

同一条斜线上分子 + 分母 = k+1。k 为偶数时分子从 1 递增,分子 = pos;k 为奇数时分子从 k 递减,分子 = k+1−pos。分母由和减去分子得到。

验证:N=7。k=3 时 3×4/2=6 < 7,k=4 时 4×5/2=10 ≥ 7,所以在第 4 条斜线;pos = 7 − 4×3/2 = 1;k 是偶数,分子 1、分母 4 → 1/4。

复杂度:k 最大约 4472,循环几千次,O(√N),10⁷ 的 N 瞬间出结果。

还是不会?点开题解代码与拆解
参考代码
#include <bits/stdc++.h>
using namespace std;

int main() {
    long long n;
    cin >> n;

    long long k = 1;
    while (k * (k + 1) / 2 < n) ++k;       // 找到 N 所在的斜线编号 k

    long long pos = n - k * (k - 1) / 2;   // 在这条斜线上是第几个(从 1 数)
    long long a, b;                        // a 分子,b 分母
    if (k % 2 == 0) {                      // 偶数斜线:分子 1,2,3,...
        a = pos;
        b = k + 1 - pos;
    } else {                               // 奇数斜线:分子 k,k-1,...
        a = k + 1 - pos;
        b = pos;
    }
    cout << a << "/" << b << "\n";
    return 0;
}
关键片段拆解
片段作用
while (k*(k+1)/2 < n) ++k;k(k+1)/2 是第 k 条斜线结束时的编号。k 从小到大试,第一次 ≥ n 就停,此时 k 就是答案所在的斜线。
k*(k-1)/2前 k−1 条斜线一共有多少个数。N 减去它,就是 N 在本斜线上的序号 pos。
k % 2 == 0偶数斜线自右上向左下,分子从 1 开始递增,所以分子直接等于 pos。
k + 1 - pos同一条斜线上 分子+分母 = k+1 恒成立;知道分子就能反推分母。
long long n, kN ≤ 10⁷ 时 int 本来就够,但 k*(k+1) 这类中间量会涨到 2×10⁷ 以上,统一用 long long 免掉以后改数据范围时的隐患。
cout << a << "/" << b << "\n";输出格式是“分子/分母”:中间一个斜杠,不是空格、不是小数。
这题最容易错的地方
  • 找到 k 之后忘了算 pos,直接把 k 或 N 输出,WA。
  • 奇偶方向写反:偶数线分子递增、奇数线分子递减。写反时样例全错。
  • 位置用 n − k(k+1)/2 算(用了线末编号),得到的是 0 或负数,WA;要用线首之前的 k(k−1)/2。
  • k*(k+1) 全程用 int,数据稍大就溢出成负数,循环直接跑飞(TLE)。
大题 挑战题 3 · 校门外的树
洛谷 P1047普及-去洛谷做这题 ↗

马路长 l 米,数轴上 0 到 l 每个整数点都种了一棵树,共 l+1 棵。有 m 个区间要建地铁,区间 [u,v] 上的树(含端点)都要移走,区间之间可能重叠。求移走后还剩多少棵树。

输入格式:第一行两个整数 l、m(1 ≤ l ≤ 10⁴,1 ≤ m ≤ 100);接下来 m 行,每行两个整数 u、v(0 ≤ u ≤ v ≤ l)。

输出格式:一行一个整数,剩余的树的数量。

引导性提示(先只看这里)

树的规模只有一万棵,区间只有一百个——这是“每棵树都看一眼”也完全能过的范围。

想办法让每棵树自己记住“我还在不在”,这样重叠区间就不需要任何特殊处理。

区间端点处的树也要移走。数一下 0 到 l 到底有几棵树。

想看思路了?点开完整思路

开一个数组 cut[0..l],cut[i] = 1 表示第 i 米处那棵树已被移走,初值全 0。

每读入一个区间 [u,v],就 for (j = u; j <= v; ++j) cut[j] = 1。

全部区间处理完后,统计 cut[j] == 0 的个数,就是剩下的树。

为什么用标记法而不是“总长度减并集长度”:并集要判断重叠,而标记法把“砍两次”自动压成“砍一次”,重叠问题根本不存在。

复杂度 O(l·m) = 10⁴ × 100 = 10⁶,稳过 1 秒。

还是不会?点开题解代码与拆解
参考代码
#include <bits/stdc++.h>
using namespace std;

int cut[10005];                  // cut[i] = 1 表示第 i 米处的树被移走

int main() {
    int l, m;
    cin >> l >> m;

    for (int i = 0; i < m; ++i) {
        int u, v;
        cin >> u >> v;
        for (int j = u; j <= v; ++j) cut[j] = 1;   // 端点也要砍,所以是 <=
    }

    int ans = 0;
    for (int j = 0; j <= l; ++j)                   // 0..l 一共 l+1 棵树
        if (cut[j] == 0) ++ans;

    cout << ans << "\n";
    return 0;
}
关键片段拆解
片段作用
int cut[10005];开在全局:默认全 0,也没有栈溢出风险。l ≤ 10⁴,10005 够用且留了余量。
for (int j = u; j <= v; ++j)u 到 v 的每个整数点都有树,端点在区间内,所以是 <= 而不是 <。
cut[j] = 1;标记式覆盖:同一棵树被两个区间覆盖也只记一次,重叠区间天然被处理掉。
for (int j = 0; j <= l; ++j)树的编号是 0,1,…,l,一共 l+1 棵。写成 j < l 会漏掉末尾那棵。
if (cut[j] == 0) ++ans;统计没被标记的点,就是没被砍掉的树。
int ans = 0;计数变量必须显式初始化,局部变量不赋初值就是垃圾值。
这题最容易错的地方
  • 内层写成 j < v,端点 v 那棵树没被砍,WA。
  • 统计时写 j < l,漏掉 l(或忘了 0),答案偏小。
  • 数组开成 1005(l 可以到 10⁴),越界后可能 RE,也可能悄悄改坏别的变量。
  • 想用“总棵数减去各区间长度”硬算,重叠区间被重复减,WA。
大题 挑战题 4 · 计数问题
洛谷 P1980普及-去洛谷做这题 ↗

统计 1 到 n 的所有整数中,数字 x 一共出现了多少次。例如 1 到 11 中,数字 1 出现了 4 次:1 里有 1 个,10 里有 1 个,11 里有 2 个。

输入格式:两个整数 n、x(1 ≤ n ≤ 10⁶,0 ≤ x ≤ 9),空格分隔。

输出格式:一个整数,表示 x 出现的次数。

引导性提示(先只看这里)

统计对象是“位”,不是“数”:1 到 11 一共 22 个数字位。

怎么把一个整数的每一位拿出来?想想取末位用什么运算、去掉末位用什么运算。

x 可以是 0。检查一下你的程序会不会漏掉 10、20 这类数里的 0。

想看思路了?点开完整思路

外层枚举 i = 1 … n,内层把 i 的每一位拆出来,等于 x 就加一。

拆位三件套:t % 10 取末位,t /= 10 去掉末位,循环到 t == 0 为止。

关键:拆位一定要用副本 t = i,否则会把外层循环变量 i 改成 0,直接死循环。

复杂度 O(n log n):10⁶ 个数平均 6~7 位,约 7×10⁶ 次操作,过。

答案最大约 6×10⁶,int 够用;若 n 改成 10⁹ 就必须 long long。

还是不会?点开题解代码与拆解
参考代码
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n, x;
    cin >> n >> x;

    int cnt = 0;
    for (int i = 1; i <= n; ++i) {
        int t = i;                     // 用副本拆位,绝不能动 i
        while (t > 0) {
            if (t % 10 == x) ++cnt;    // 末位等于 x 就计数
            t /= 10;                   // 砍掉末位:123 -> 12 -> 1 -> 0
        }
    }
    cout << cnt << "\n";
    return 0;
}
关键片段拆解
片段作用
int t = i;拆位的循环会把数字除到 0,必须用副本。直接写 while (i > 0) 会把 for 的循环变量改掉,要么死循环(TLE),要么直接跳过一大段数(WA)。
t % 10 == x取末位与 x 比较。x = 0 时同样成立,所以 10、20 里的 0 能被统计到。
t /= 10;整数除法去掉末位,是取模的逆操作。
while (t > 0)i ≥ 1,t 至少有一位,循环至少进一次。用 t > 0 而不是 do-while,避免 i = 0 时凭空多算一个 0。
++cnt每个匹配的位加一。本题答案量级 10⁶,int 足够。
for (int i = 1; ...)从 1 开始。题目统计的是 1 到 n,从 0 开始会多算一个数。
这题最容易错的地方
  • 直接对 i 做 t /= 10 一类的操作,改坏外层循环变量,TLE 或答案错误。
  • 用 do-while 拆位,i = 0 时会白算一次(0 % 10 == 0),x = 0 时答案偏大。
  • 外层从 0 开始枚举,凭空多统计一个数。
  • cnt 用 int 在本题没问题,但换成 n = 10⁹ 的版本就溢出;养成先估量级的习惯。
大题 挑战题 5 · 生活大爆炸版石头剪刀布
洛谷 P1328普及/提高-去洛谷做这题 ↗

猜拳升级版有 5 种手势:0 剪刀、1 石头、2 布、3 蜥蜴人、4 斯波克。胜负关系:剪刀胜布和蜥蜴人;石头胜剪刀和蜥蜴人;布胜石头和斯波克;蜥蜴人胜布和斯波克;斯波克胜剪刀和石头;手势相同为平局。赢者得 1 分,输者或平局得 0 分。

小 A 的出拳以长度 nA 的序列为周期循环,小 B 以长度 nB 的序列为周期循环,两人一共出 N 拳。输入格式:第一行三个整数 N、nA、nB;第二行 nA 个整数(小 A 的周期);第三行 nB 个整数(小 B 的周期)。

输出格式:一行两个整数,分别是小 A、小 B 的得分,用一个空格分隔。数据范围:0 < N, nA, nB ≤ 200。

引导性提示(先只看这里)

10 组胜负关系硬写 if 很容易漏。先把它们整理成一张表,让程序只做“查表”这一件事。

第 k 拳两个人各出什么?两个周期长度不同,得各算各的下标。

查表只能回答“A 出 i、B 出 j 时谁赢”。另一种情况怎么办?换个顺序再查一次就够了。

想看思路了?点开完整思路

把“A 出 i、B 出 j 时 A 是否赢”做成一张 5×5 的表,值为 1 表示 A 赢。

5×5 的表按行拼成一个长度 25 的一维数组:第 i 行第 j 列存在 W[i*5 + j]。这样只用一维数组,不用等后面的二维数组。

第 k 拳(k 从 0 数):x = a[k % nA],y = b[k % nB]。若 W[x*5+y] 则 A 得分;否则若 W[y*5+x] 则 B 得分;两个都不成立就是平局,两人都不加分。

为什么用表而不是 if:胜负关系有 10 组,if 分支至少 10 个,漏一个就 WA 且很难查;表只有 25 个数字,能逐行核对(每行恰好 2 个 1,全表 10 个 1)。

复杂度 O(N),N ≤ 200,几乎不耗时。

还是不会?点开题解代码与拆解
参考代码
#include <bits/stdc++.h>
using namespace std;

// W[i*5+j] == 1 表示:小 A 出 i、小 B 出 j 时,A 赢
// 0 剪刀   1 石头   2 布   3 蜥蜴人   4 斯波克
const int W[25] = {
    0, 0, 1, 1, 0,        // 剪刀 胜 布、蜥蜴人
    1, 0, 0, 1, 0,        // 石头 胜 剪刀、蜥蜴人
    0, 1, 0, 0, 1,        // 布   胜 石头、斯波克
    0, 0, 1, 0, 1,        // 蜥蜴人 胜 布、斯波克
    1, 1, 0, 0, 0         // 斯波克 胜 剪刀、石头
};

int a[205], b[205];

int main() {
    int n, na, nb;
    cin >> n >> na >> nb;

    for (int i = 0; i < na; ++i) cin >> a[i];
    for (int i = 0; i < nb; ++i) cin >> b[i];

    int sa = 0, sb = 0;
    for (int k = 0; k < n; ++k) {
        int x = a[k % na];             // 周期序列:第 k 拳 = 周期的第 k%na 项
        int y = b[k % nb];
        if (W[x * 5 + y])      ++sa;   // 查表:A 赢
        else if (W[y * 5 + x]) ++sb;   // 交换顺序再查:B 赢
        // 都不成立就是平局,两人都不加分,不必写分支
    }
    cout << sa << " " << sb << "\n";
    return 0;
}
关键片段拆解
片段作用
const int W[25] = {...}5×5 的胜负表按行首尾相接压成一维:第 i 行第 j 列存在下标 i*5+j。表写成常量并加注释,抄错时一眼能看出来。
a[k % na]k 是第几拳(从 0 数),k%na 把它映射回周期内的下标,周期序列就变成了任意长序列。
W[x * 5 + y]还原成二维:行是 A 的手势,列是 B 的手势,值为 1 表示这一拳 A 赢。
else if (W[y * 5 + x])把两人手势交换再查一次,含义变成“B 出 y、A 出 x 时 B 是否赢”。不需要写第二张表。
没有 else 分支平局两人都得 0 分,正好什么都不做。少一个分支,少一个出错的地方。
int a[205], b[205];nA、nB ≤ 200,开 205 留余量;下标 0 开始,所以读入循环写 i < na。
这题最容易错的地方
  • 胜负表抄错一格,WA 且极难自查。抄完先验:每行恰好 2 个 1,全表 10 个 1。
  • 下标偏移写错(用 (k-1)%na 或 k%n+1),整场比赛全盘错位,WA。
  • 只判 A 赢、其余全算 B 赢(写成 else ++sb),平局被算成 B 得分,WA。
  • 数组开 105,nA 到 200 时越界,RE 或答案错乱。
做完之后自问
延伸阅读