这一节不讲新东西。5 道题、90 分钟,只考第 1~4 节学过的四件事:读入输出、循环、图形规律、一维数组。目标不是“能做出来”,而是“限时内稳定做出来”——同一道题,赛场上多花 10 分钟,就等于少做一道后面的大题。
先看清这 5 道题分别考什么,再决定先做哪道:
| 标记 | 题目 | 主要考点 | 难度 | 建议用时 |
|---|---|---|---|---|
| A | P1909 买铅笔 | 循环 + 向上取整 + 取最小 | 普及- | 12 分钟 |
| B | P1014 Cantor 表 | 图形规律,循环找第 k 条斜线 | 普及- | 13 分钟 |
| C | P1047 校门外的树 | 数组标记,区间覆盖 | 普及- | 15 分钟 |
| D | P1980 计数问题 | while 拆位,逐个数字统计 | 普及- | 14 分钟 |
| E | P1328 生活大爆炸版石头剪刀布 | 数组 + 取模 + 查胜负表 | 普及/提高- | 20 分钟 |
总原则:先把会做的分全部拿到,再回头啃没思路的。题目难度不是按顺序排的,别顺着往下做;规则题(B)想不出就跳过,它值得的时间只有 5 分钟。
| 阶段 | 时间段(分钟) | 做什么 |
|---|---|---|
| 通读 5 题 | 0 ~ 5 | 标“会做 / 有思路 / 完全不会”,此阶段不许动笔写代码 |
| A 买铅笔 | 5 ~ 17 | 读 2 + 写 6 + 测 4,必须一次写对 |
| B Cantor 表 | 17 ~ 30 | 5 分钟没思路就跳过,最后再回来 |
| C 校门外的树 | 30 ~ 45 | 标记法,两个循环,重点全在边界 |
| D 计数问题 | 45 ~ 59 | 代码极短,卡住必是拆位拆错了循环变量 |
| E 石头剪刀布 | 59 ~ 79 | 抄表 6 + 写 8 + 测 6 |
| 检查 | 79 ~ 90 | 只查数组大小、初值、输出格式,不改算法 |
单题内部再切一刀:想 3 分钟(在纸上写清楚)、写 8 分钟、测 4 分钟。“想”的 3 分钟里只干四件事:抄下数据范围、抄下输出格式、手算一遍样例、写下要用的数组大小。
如果整块时间更充裕,多出来的部分全给最没把握的那道题,不要平均分给 5 题——能做出来的题不需要第二遍,做不出来的题才需要。
“想 3 分钟”不是发呆。这 3 分钟里,纸上必须出现 4 行东西,写完再碰键盘:
再看一张“条件反射表”。题面出现这些词,思路应该立刻跳出来——5 道训练题全在这张表里:
| 题面里的词 | 第一反应 |
|---|---|
| “至少”“装满”“够不够” | 向上取整:(n + p - 1) / p |
| “出现几次”“有多少个” | 计数器 cnt,初值写 0 |
| “每棵树”“每个点” + 范围不大 | 开数组打标记,最后扫一遍统计 |
| “周期”“循环出现” | 下标取模 a[k % na] |
| “连续的一段”“最长” | 边扫边维护 cur,随时 max 更新答案 |
| “含端点”“0 到 l” | 循环边界写 <=,元素个数是 l+1 |
表里有三个动作,最简的题也要用:取极值、向上取整、下标取模。下面三张卡不是新知识,只是把这三个写法再拆一遍——看着眼熟就快速扫过,看着陌生说明前面某节没吃透,回去补。
| 组成 | 含义 / 格式要求 |
|---|---|
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"; // 9ans 用 0 初始化去求最大值,遇到全是负数就错;最稳的初值是 a[0]。
min 与 max 的两个参数类型不同时可能编译不过,统一写 int 最省事。
| 组成 | 含义 / 格式要求 |
|---|---|
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 写式子。
| 组成 | 含义 / 格式要求 |
|---|---|
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 道题里反复出现。
输入长度为 n 的正整数序列 a,求最长的连号长度。连号指:序列中一段连续的子串,其中每一项正比如前一项大 1。例如 2 3 4 5 是连号(长度 4),而 2 3 5 不是(3 到 5 断了)。
输入格式:第一行一个整数 n(1 ≤ n ≤ 10⁴);第二行 n 个整数 aᵢ(1 ≤ aᵢ ≤ 10⁹),空格分隔。
输出格式:一个整数,最长连号的长度。
题目要的是“最长的一段”,不是“最长上升子序列”。差别只有一条:连号要求差恰好为 1,断了就必须重新数,不能跳过中间某个数接着数。
于是只需要维护一个状态:cur = 以当前位置结尾的连号有多长。扫描时只有两种转移:
每走一步都用 ans = max(ans, cur) 更新答案。因为“最长的那一段”一定在某一步结尾,所以扫描过程中记下 cur 的最大值就够了,不需要存第二遍。
复杂度:一次线性扫描 O(n),n ≤ 10⁴,耗时可以忽略。
样例 a = 1 5 6 2 3 4 5 6。i 从 1 开始(要比较 a[i-1]):
| i | a[i] | 与前一数差 1? | cur | ans |
|---|---|---|---|---|
| 0 | 1 | —(起点) | 1 | 1 |
| 1 | 5 | 否,断 | 1 | 1 |
| 2 | 6 | 是 | 2 | 2 |
| 3 | 2 | 否,断 | 1 | 2 |
| 4 | 3 | 是 | 2 | 2 |
| 5 | 4 | 是 | 3 | 3 |
| 6 | 5 | 是 | 4 | 4 |
| 7 | 6 | 是 | 5 | 5 |
答案 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"; | 只输出一个整数加换行。题目没要求输出长度以外的任何东西。 |
数组开小 → 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. 重读题面最后两行:八成是边界、输出格式、多算或少算一个数,而不是算法想错了。
数组大小 ≥ 数据范围上限 + 5。
该用 long long 的地方都用了吗(和、积、平方)。
输出格式:几个数?空格还是换行?行末有没有多余空格?
边界跑过没有:n=1、l=0、全部相同、全是最小值。
计数变量初值写了没有。
题目给的样例,输出和标准答案逐字一样吗。
下面是 5 道训练题。先自己做,卡满 25 分钟再看提示;提示看完还没思路,再点开思路;代码一定要自己敲一遍再提交。
限时 100 分钟。每题先自己写 20~25 分钟;写不出来先看提示,再给自己 10 分钟;还不行才点开思路。每题都独立限时,不要一题耗死。
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); | 打擂台:每结算完一种包装就更新一次答案,循环结束即最优。 |
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, k | N ≤ 10⁷ 时 int 本来就够,但 k*(k+1) 这类中间量会涨到 2×10⁷ 以上,统一用 long long 免掉以后改数据范围时的隐患。 |
cout << a << "/" << b << "\n"; | 输出格式是“分子/分母”:中间一个斜杠,不是空格、不是小数。 |
马路长 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; | 计数变量必须显式初始化,局部变量不赋初值就是垃圾值。 |
统计 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 开始会多算一个数。 |
猜拳升级版有 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。 |