今天不学新算法 —— 今天练的是「在 3 小时里把会做的题全部拿到分」
| 0 - 10 min | 四道题全部读一遍,判断难度,决定做题顺序(先别写代码) |
| 10 - 70 min | 拿下最容易的 1 ~ 2 题(每题限时 30 分钟) |
| 70 - 130 min | 啃第 3 题;卡住超过 50 分钟就先去写暴力拿分 |
| 130 - 160 min | 第 4 题 + 回头检查数组大小 / long long / 输出格式 |
| 160 - 180 min | 最后 20 分钟:不写新代码,只做检查 |
| 180 - 240 min | 赛后复盘:把没 AC 的题不看题解重写一遍(这才是重点) |
Day 28 你刚学完栈和队列。今天的 4 道题里,P1981 表达式求值 和 P1996 约瑟夫问题 正好就是栈和队列的直接应用 —— 出题人就是这么设计的。另外两道(P1067、P1328)是纯模拟, 考的是 Day 1 ~ Day 6 攒下来的基本功:数组、循环、条件判断、边界处理。
很多人第一次打模拟赛会有一种很憋屈的感觉:这四道题的算法我全都学过,为什么还是拿不到分?
原因通常不是「不会」,而是下面这几种:
| 丢分原因 | 典型表现 | 今天怎么防 |
|---|---|---|
| 读题太快 | 漏掉「系数为 1 时省略」「输出最后 4 位」这类细节 | 把题面里每一句「要求」抄成清单,写完逐条打钩 |
| 顺序选错 | 一上来死磕最难的题,一小时过去 0 分 | 先花 10 分钟全部读一遍,从最简单的开始 |
| 卡死不放 | 一道题耗掉 100 分钟,其余三题没时间 | 单题超过 50 分钟就换题,先把能拿的分拿到手 |
| 不检查 | 数组开小、忘开 long long、多测没清空 | 留最后 20 分钟专门检查,不写新代码 |
| 没有暴力兜底 | 正解写不出就交白卷 | 很多题有子任务分,写个暴力能拿 30 ~ 60 分 |
比赛比的不是「谁会得更多」,而是「谁丢得更少」。
会做的题全对 + 不会的题骗到部分分,这个分数往往比死磕一道难题高得多。
| 题号 | 题目 | 难度 | 类型 |
|---|---|---|---|
| P1067 | 多项式输出 | 普及- | 细节模拟(特判地狱) |
| P1328 | 生活大爆炸版石头剪刀布 | 普及/提高- | 打表 + 模拟 |
| P1981 | 表达式求值 | 普及- | 栈 / 优先级处理 |
| P1996 | 约瑟夫问题 | 普及- | 队列模拟 |
建议顺序:P1996 → P1067 → P1981 → P1328,或者 P1067 → P1996 → P1981 → P1328。 P1996 最短最快,先拿一题稳住心态;P1328 要自己打一张表,读题最花时间,放后面。
别急着想算法。先用一句话说清楚:程序读进来什么,要吐出去什么。
拿 P1067 举例,翻译完是这样:
| 环节 | 内容 |
|---|---|
| 输入 | 第一行次数 n,第二行 n+1 个整数(从最高次到常数项的系数) |
| 处理 | 按次数从高到低,把非零项拼成一个字符串 |
| 输出 | 一行,形如 100x^5-x^4+x^3-3x^2+10 |
翻译完你立刻就能发现:这题没有算法,只有一个循环加一堆 if。剩下的全是细节,那就去抠细节。
题面里凡是出现「如果……则……」「不需要……」「只输出……」的句子,每一句都对应代码里一个 if。
P1067 的清单长这样(这题一共 5 条):
| # | 规则 | 代码里对应什么 |
|---|---|---|
| 1 | 按次数递减输出 | 循环写 for(i = n; i >= 0; i--) |
| 2 | 只输出系数不为 0 的项 | if(a == 0) continue; |
| 3 | 最高次项为正不输出 +,为负输出 − | 用 first 标记第一项 |
| 4 | 非最高次项用 + / - 连接 | 后面每项都先输出符号 |
| 5 | 次数 > 0 且系数绝对值为 1 → 不输出 1 次数为 1 → 写 x;次数为 0 → 只写系数 | 一层 if(i == 0) / else if(i == 1) / else |
写完代码后,把这张表拿出来一条一条打钩。P1067 这种题,只要你 5 条都实现了,就一定能过; 少一条就是 0 分。比赛里靠感觉写细节题,翻车率极高。
样例 2 是 n=3,系数 -50 0 0 1,期望输出 -50x^3+1。手动走一遍:
| i(次数) | 系数 | 该输出什么 | 结果拼接 |
|---|---|---|---|
| 3 | -50 | 第一项且为负 → 输出 −,|系数|=50 ≠ 1 → 输出 50,次数>1 → x^3 | -50x^3 |
| 2 | 0 | 跳过 | -50x^3 |
| 1 | 0 | 跳过 | -50x^3 |
| 0 | 1 | 非第一项且为正 → +,次数为 0 → 只输出系数 1 | -50x^3+1 |
和样例输出一模一样。这一步花你 2 分钟,能省掉 20 分钟调试。
first 标记替代「特判最高次项」新手最容易写出这样的代码:
// ❌ 又长又容易错:对最高次项单独特判一遍 if(最高次项){ if(a > 0) cout << a << "x^" << n; else cout << a << "x^" << n; } for(int i = n - 1; i >= 0; i--){ // 这里还要再写一遍系数、次数、符号的处理…… }
不要问「这是不是最高次项」,而问「我前面输出过东西没有」。
bool first = true; // 每一项的处理只有一份,符号部分按 first 分叉: if(first){ if(a < 0) cout << '-'; } // 开头:只有负号 else { cout << (a < 0 ? '-' : '+'); } // 后面:正负都要 first = false;
这样「系数 / 字母 / 指数」的逻辑只写一遍,不会出现两份代码改了一处忘了另一处的情况。
而且它还顺手解决了「最高次项系数为 0 怎么办」这个题面没明说的边界 —— 因为判断的是「有没有输出过」,
不是「是不是最高次项」。
P1328 石头剪刀布升级版有 5 种手势,两两组合有 25 种情况。用 if 写就是 25 个分支,写完自己都不敢改。
改成二维数组,逻辑就从「代码」变成了「数据」:
int r[5][5] = {
{ 0, -1, 1, 1, -1}, // 剪刀
{ 1, 0, -1, 1, -1}, // 石头
{-1, 1, 0, -1, 1}, // 布
{-1, -1, 1, 0, 1}, // 蜥蜴人
{ 1, 1, -1, -1, 0} // 斯波克
};
// r[a][b] = 1 甲赢 / 0 平 / -1 甲输
查结果就是一句话:r[a][b]。这张表有两条性质,写完可以自查:
对角线全是 0(自己和自己平),关于对角线反对称(r[a][b] == -r[b][a],甲赢就是乙输)。
P1328 里两人要出拳 N 次,但每个人的出拳规律是长度 NA、NB 的循环。 根本不用造出一个长度 N 的序列,直接:
int res = r[ a[i % na] ][ b[i % nb] ]; // i % na 自动回到周期开头
i % na 的结果正好落在 0 ~ na-1,刚好是数组下标的合法范围,不需要 +1 / −1 折腾。
如果题目给的是「第 i 个人」这种从 1 开始的东西,就要写成 (i - 1) % n —— 用错会越界。
P1981 只要答案的最后 4 位。这时候不需要真的算出完整的大数,每一步都对 10000 取模即可:
a * b % 10000 == ((a % 10000) * (b % 10000)) % 10000 // 恒成立 (a + b) % 10000 == ((a % 10000) + (b % 10000)) % 10000 // 同理
取模不改变「低位」,只丢「高位」。题目只要低位,高位丢了也不心疼 —— 这就是用信息换范围。
它同时解决了两件事:结果不会溢出 long long,而且数字本身也能读进来就取模(题面里数字可以到 2³¹−1)。
表达式 1+2*3,如果从左往右一个个算,会得到 (1+2)*3 = 9,答案是错的。乘法的优先级更高,必须先把 2*3 粘在一起。
因为乘法优先级最高,所以整个式子可以看成:若干「项」用 + 连接起来,
而每个项内部只有乘法。
1+2*3+4*5 = (1) + (2*3) + (4*5) = 1 + 6 + 20
所以只要维护两个变量:已经结算完的总和 和 当前正在累乘的这一项。
1+1*3+4| 读到的字符 | num | op | term(当前项) | ans(总和) | 说明 |
|---|---|---|---|---|---|
| 1 | 1 | — | 1 | 0 | 数字,继续攒 |
| + | 0 | + | 1 | 0 | 第一个数遇到运算符,它就是当前项 |
| 1 | 1 | + | 1 | 0 | 数字 |
| * | 0 | * | 1 | 1 | 上一项结算进 ans,开始新项 |
| 3 | 3 | * | 1 | 1 | 数字 |
| + | 0 | + | 3 | 1 | 乘法优先:term = 1 * 3 = 3,不结算 |
| 4 | 4 | + | 3 | 1 | 数字 |
| 结束 | — | — | 4 | 4 | 结算上一项 → ans = 1+3 = 4 |
| 收尾 | — | — | — | 8 | 别忘了最后一项!ans = 4+4 = 8 |
循环里只处理「遇到运算符时结算上一项」,所以最后一项永远不会被结算。
必须在循环结束后补一句 ans = (ans + term) % 10000;。
样例 1+1*3+4 里最后一项是 4,忘了它就是输出 4 而不是 8 —— 一眼就能看出错,
但如果最后一项是 0,你就永远发现不了这个 bug。这种"有时对有时错"的 bug 最要命。
题面说参与运算的数字可以是 0 到 2³¹−1。如果先完整读进 long long 再取模,其实也行(2³¹−1 没超)。
但更省事的做法是边读边取模:
num = (num * 10 + (s[i] - '0')) % 10000; // 读的时候就只留后四位
因为 num 始终小于 10000,num * 10 + d < 100000,连 int 都不会溢出。
于是整份代码里所有中间量都在 10000 以内,你把 long long 全换成 int 都不会错。
不是「先算完再取模」,而是让数字从一开始就长不大。范围小 = 不溢出 = 少一类 bug。
下面每道题的代码我都用 g++ -std=c++14 -O2 -Wall 实际编译并跑过官方样例,
「实测结果」里写的就是真实输出。
题意:给一个一元 n 次多项式从高次到低次的 n+1 个系数,按竞赛规定的格式把它输出成一行字符串。
0 ≤ n ≤ 100,−100 ≤ 系数 ≤ 100。系数可能是 0,也可能是 1 / −1 / 100 / −100 这些边界值。
另外题目还给了一条容易漏的:「多项式只包含系数不为 0 的项」——
也就是说系数为 0 的项要整个消失,不是输出一个 0x^3。
这题没有算法,只有一个循环加一堆特判。核心是把题面 5 条规则翻译成一张清单(见第 2 节),
然后逐条实现。最关键的技巧是用一个 first 标记区分「第一项」和「后面的项」——
| 位置 | 正数怎么输出 | 负数怎么输出 |
|---|---|---|
| 第一个被输出的项 | 直接输出,不带 + | 输出 - |
| 后面所有项 | 输出 +,再输出系数 | 输出 -,再输出系数 |
然后按次数分三种情况输出后半截:
| 次数 i | |系数| = 1 | |系数| ≠ 1 |
|---|---|---|
| i = 0 | 输出 1 | 输出系数 |
| i = 1 | 输出 x | 输出 3x 这样 |
| i ≥ 2 | 输出 x^i | 输出 3x^i 这样 |
5 100 -1 1 -3 0 10
100x^5-x^4+x^3-3x^2+10
3 -50 0 0 1
-50x^3+1
样例 1 一次性考了 4 个特判:系数 1 省掉(x^4、x^3)、系数 0 整项跳过(x 那一项没了)、
常数项只写系数(+10)、以及正负号切换。这就是它被叫做「特判地狱」的原因。
#include <iostream>
using namespace std;
int main(){
int n;
cin >> n;
bool first = true; // 还没有输出过任何一项
for(int i = n; i >= 0; i--){ // i 就是当前这一项的次数
int a;
cin >> a;
if(a == 0) continue; // ★ 系数为 0 的项直接不输出
// ---------- 1. 符号 ----------
if(first){
if(a < 0) cout << '-'; // 开头只有负数才输出负号
}else{
cout << (a < 0 ? '-' : '+'); // ★ 后面每项都要带符号
}
// ---------- 2. 系数 + 字母 + 指数 ----------
int ab = (a < 0 ? -a : a); // 系数的绝对值
if(i == 0){
cout << ab; // 常数项:只输出系数
}else if(i == 1){
if(ab != 1) cout << ab; // ★ 一次项:|系数|=1 时省略 1
cout << 'x'; // 指数 1 不写
}else{
if(ab != 1) cout << ab; // ★ 高次项:|系数|=1 时省略 1
cout << "x^" << i;
}
first = false;
}
cout << endl;
return 0;
}
官方样例 1 5 / 100 -1 1 -3 0 10 → 100x^5-x^4+x^3-3x^2+10 完全一致
官方样例 2 3 / -50 0 0 1 → -50x^3+1 完全一致
边界 n=0, 系数 10 → 10 正确(n=0 时只有常数项)
边界 n=1, 1 1 → x+1;n=1, -1 -1 → -x-1 正确(系数 ±1 被省略、指数 1 不写)
边界 n=2, 1 6 5 → x^2+6x+5 正确(首项系数 1 省略且不带 + 号)
边界 n=100, 只有 x^100 和常数项 7 → x^100+7 正确(中间 99 个 0 项全部消失)
边界 n=2, 系数 0 0 5 → 5 正确(最高次项系数为 0 时也不多打 + 号)
另外我还写了一个自检程序:随机生成 200000 组 n ∈ [0,100]、系数 ∈ [−100,100] 的多项式, 把输出串再解析回系数数组和原多项式比对(往返一致性),200000 组全部通过、0 组失败。
- 之后,后面只能输出 |a|。
如果直接 cout << a,就会出现 --3x^2 这种两个负号叠在一起的东西。1 的,
别顺手也省掉(那就什么都不剩了)。x(系数 1、次数 1)会同时触发两条;
-x 同理。别只写一条。first 标记的写法会自然处理成「谁最先被输出,谁就是第一项」,
这是社区通行做法,我也按这个实现了。题意:石头剪刀布的升级版,共 5 种手势(0 剪刀、1 石头、2 布、3 蜥蜴人、4 斯波克)。 小 A 和小 B 各有自己的周期出拳规律,一共比 N 局,赢的加 1 分,问两人各得几分。
0 < N ≤ 200,0 < NA ≤ 200,0 < NB ≤ 200。
数据极小,N 最多 200 —— 这题的难点 100% 在读题和打表,不在效率。 不要想复杂了,就是老老实实模拟 N 局。
最容易错的是手势编号的含义:0 是剪刀不是石头!石头是 1。记反了全盘皆错。
5 种手势两两组合共 25 种,用 if 写就是 25 个分支。正确的做法是把胜负关系存成一张二维表,
用的时候只需要 r[a][b] 查一下。
升级版的 10 条胜负规则(每句记一半,反过来就是输)是:
| 赢的一方 | 赢法 | 输的一方 |
|---|---|---|
| 剪刀 (0) | 剪断 | 布 (2) |
| 剪刀 (0) | 斩首 | 蜥蜴人 (3) |
| 石头 (1) | 砸碎 | 剪刀 (0) |
| 石头 (1) | 压扁 | 蜥蜴人 (3) |
| 布 (2) | 包住 | 石头 (1) |
| 布 (2) | 揭穿 | 斯波克 (4) |
| 蜥蜴人 (3) | 毒死 | 斯波克 (4) |
| 蜥蜴人 (3) | 吃掉 | 布 (2) |
| 斯波克 (4) | 蒸发 | 石头 (1) |
| 斯波克 (4) | 砸碎 | 剪刀 (0) |
整理成 5×5 的表(行是甲、列是乙):
| 甲 \ 乙 | 0 剪刀 | 1 石头 | 2 布 | 3 蜥蜴人 | 4 斯波克 |
|---|---|---|---|---|---|
| 0 剪刀 | 平 | 甲输 | 甲赢 | 甲赢 | 甲输 |
| 1 石头 | 甲赢 | 平 | 甲输 | 甲赢 | 甲输 |
| 2 布 | 甲输 | 甲赢 | 平 | 甲输 | 甲赢 |
| 3 蜥蜴人 | 甲输 | 甲输 | 甲赢 | 平 | 甲赢 |
| 4 斯波克 | 甲赢 | 甲赢 | 甲输 | 甲输 | 平 |
1. 对角线全是 0(自己打自己一定平)。
2. 关于对角线反对称:r[a][b] == -r[b][a](甲赢就等于乙输)。
我这版表两条都满足。你也可以顺便记一个规律:每种手势都恰好赢 2 种、输 2 种。
然后模拟 N 局,第 i 局两人出的是 a[i % na] 和 b[i % nb] —— 取模就实现了循环。
10 5 6 0 1 2 3 4 0 3 4 2 1 0
6 2
9 5 5 0 1 2 3 4 1 0 3 2 4
4 4
#include <iostream> using namespace std; // 手势编号:0 剪刀 1 石头 2 布 3 蜥蜴人 4 斯波克 // r[a][b] = 甲出 a、乙出 b 时甲的结果: 1 甲赢 0 平 -1 甲输 int r[5][5] = { { 0, -1, 1, 1, -1}, // 剪刀 vs 剪/石/布/蜥/斯 { 1, 0, -1, 1, -1}, // 石头 {-1, 1, 0, -1, 1}, // 布 {-1, -1, 1, 0, 1}, // 蜥蜴人 { 1, 1, -1, -1, 0} // 斯波克 }; int main(){ int n, na, nb; cin >> n >> na >> nb; int a[205], b[205]; 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 i = 0; i < n; i++){ int res = r[a[i % na]][b[i % nb]]; // ★ 取模实现无限循环 if(res == 1) sa++; else if(res == -1) sb++; } cout << sa << ' ' << sb << endl; return 0; }
官方样例 1 10 5 6 → 6 2 完全一致
官方样例 2 9 5 5 → 4 4 完全一致
边界 N=1、甲出斯波克(4)、乙出蜥蜴人(3) → 0 1 正确(蜥蜴人毒死斯波克,乙赢)
边界 N=1、甲出剪刀(0)、乙出石头(1) → 0 1 正确
最大规模 N=200, NA=2, NB=3(0 1 / 2 3 4) → 101 99 合理(两人总分 101+99 = 200 = N,说明每局都分出了胜负,没有平局)
0 是剪刀,1 才是石头。写表之前先把这句话抄在草稿纸上。r[0][3] 填了 1(剪刀斩首蜥蜴人),
那 r[3][0] 必须是 −1。手写表很容易两边都填成 1,一定要回头查一遍。i % na 不是 (i + 1) % na。这里的周期序列本来就从下标 0 开始存,
直接取模就对。[205]。开成 [200] 就是越界。题意:给一个只含 + 和 * 的表达式(没有括号、没有空格),算出它的值,
但只输出最后 4 位。
参与运算的数字都是 0 到 2³¹−1 之间的整数;保证输入这一行只含 0123456789+* 这 12 种字符。
运算符总数:30% 的数据 ≤ 100,80% 的数据 ≤ 1000,100% 的数据 ≤ 100000。
所以表达式长度大约 10⁶ 级别。O(长度) 的线性扫一遍就够了,别写 O(n²) 的。
用第 4 节讲的「加法切项」思路:维护 ans(已结算总和)和 term(当前正在累乘的项)。
num 里攒,并且边攒边取模 10000。op:
op 还没记过 → 这是第一个数,直接 term = numop == '*' → term *= num(乘法优先,并进当前项,不结算)op == '+' → 把 term 结算进 ans,然后 term = num 开新项整个过程中所有变量都小于 10000,不会有任何溢出风险。
1+1*3+4
8
1+1234567890*1
7891
1+1000000003*1
4
样例 2 说明:1234567890 的后四位是 7890,1 + 7890 = 7891。
样例 3 说明:1000000003 的后四位是 0003 = 3,1 + 3 = 4。
注意这里不能先把 1000000003 当完整数字算完再取模——虽然这题数字没超 int,但读进来就取模更省心。
#include <iostream>
#include <string>
#include <cctype>
using namespace std;
int main(){
string s;
cin >> s; // 这一行只含 0123456789+*
long long ans = 0; // 已经算完的所有项的和
long long term = 1; // 当前正在累乘的这一项
long long num = 0; // 正在读的这个数
char op = 0; // 上一个读到的运算符,0 表示还没读到
for(int i = 0; i <= (int)s.size(); i++){
if(i < (int)s.size() && isdigit(s[i])){
num = (num * 10 + (s[i] - '0')) % 10000; // ★ 只留后四位就够
}else{
// 走到这里说明一个数字读完了,用 op 决定怎么合并它
if(op == 0) term = num; // 第一个数
else if(op == '*') term = term * num % 10000; // ★ 乘法优先,先并到本项
else { ans = (ans + term) % 10000; term = num; } // ★ 遇到加号:本项结算,开新项
if(i < (int)s.size()) op = s[i]; // 记下这个运算符
num = 0;
}
}
ans = (ans + term) % 10000; // ★ 别忘了最后一项
cout << ans << endl;
return 0;
}
官方样例 1 1+1*3+4 → 8 完全一致
官方样例 2 1+1234567890*1 → 7891 完全一致
官方样例 3 1+1000000003*1 → 4 完全一致
边界 0(只有数字、没有运算符)→ 0 正确
边界 2147483647(单个最大数)→ 3647 正确(2147483647 mod 10000 = 3647)
9999*9999 → 1 正确(99980001 的后四位 0001)
1+2+3+4+5 → 15;2*3*4*5 → 120 正确(纯加法 / 纯乘法都对)
随机对拍:用 bigint 写了一个「按 + 拆项、项内连乘、最后整体取模」的参考实现,
随机生成 30 组表达式(每项数字到 9×10⁶)对拍,30 组全部一致,0 组失败。
满规模压测:造了一条含 100000 个运算符(长度 1044219 字符)的表达式,
输出 6460,耗时 84 ms(时限 1000 ms,非常宽裕)。
num = num*10 + (c-'0') 攒起来。1+2*3 会算成 9。乘法的优先级必须处理。cin >> n 一个个读:这题没有空格分隔,cin >> int 读 1+1*3+4
会在 1 后面直接失败。要么整行读成字符串自己拆,要么用 cin >> char 逐个读字符。long long(100000 个 2³¹ 量级的数连乘,天文数字)。
必须每一步都取模。题意:n 个人围成一圈,从第 1 个人开始报数,数到 m 的人出圈,然后从下一个人重新从 1 报数, 直到所有人都出圈。按顺序输出每个出圈人的编号。
1 ≤ m, n ≤ 100。非常小,随便怎么模拟都行 —— 队列、数组、链表都能过。
题目特意提醒了一句:本题要求「全部出圈」,不是像某些教材那样只淘汰 n−1 个人。 所以输出必须是 n 个数,不是 n−1 个。
题目描述本身就已经是算法了:「数到 m 的出圈 → 下一个人重新从 1 开始报数」。 这正是队列的操作方式:
| 步骤 | 队列操作 | 含义 |
|---|---|---|
| 1 | q.pop() | 队头的人报数 |
| 2 | 计数器 +1 | 他报的是几 |
| 3 | 如果计数器 == m → 输出他 | 出圈,不再回队 |
| 4 | 否则 q.push(x) | 没数到 m,回到队尾继续排队 |
把队头拿到队尾,循环 n 轮,每个人都恰好被取出并处理到出圈为止。
报数是一个「转圈」的过程。数组要自己维护下标并处理「走到底了绕回开头」,
而队列的 pop + push 天然就是「转了一圈回到队尾」——
数据结构的形状正好对上题目的形状,代码就短。
10 3
3 6 9 2 7 1 8 5 10 4
10 个人数到 3 出圈:先出 3、6、9,然后从 10 后面的 1 继续数,出 2、7、1、8、5、10,最后剩 4。
#include <iostream>
#include <queue>
using namespace std;
int main(){
int n, m;
cin >> n >> m;
queue<int> q;
for(int i = 1; i <= n; i++) q.push(i); // 所有人排队,队头是 1 号
int cnt = 0; // 当前报数报到几了
bool first = true;
while(!q.empty()){
int x = q.front();
q.pop();
cnt++;
if(cnt == m){ // ★ 数到 m,出圈
if(!first) cout << ' ';
cout << x;
first = false;
cnt = 0; // ★ 下一个人重新从 1 开始报
}else{
q.push(x); // ★ 没数到 m,回到队尾接着排
}
}
cout << endl;
return 0;
}
官方样例 10 3 → 3 6 9 2 7 1 8 5 10 4 完全一致
边界 1 1 → 1 正确(1 个人,数到 1 就出圈)
边界 5 5 → 5 1 3 4 2 正确(5 个人数到 5 出圈)
最大规模 100 100 → 输出 100 个数,第一个是 100,最后一个 22,n=100 个数一个不少。
穷举对拍:我又写了一个朴素做法(bool 数组 + 下标绕圈)作参考实现, 对 n = 1…100 × m = 1…100 全部 10000 组逐一比对, 10000 组答案完全一致、0 组失败;同时检查了每组输出都是 1…n 的一个排列(不多不漏不重复),也全部通过。
if(!first) cout << ' '; 这种「先判断再输出」的写法最稳。cnt = 0,等于下一次循环 cnt++ 后正好是 1。
如果忘了清零,报数就错位了。while(!q.empty()),不要用 for(i = 1; i < n; i++)——
那样只会淘汰 n−1 个人,少输出一个数(正是题面提醒的那个坑)。q.push(x),否则死循环。| # | 错误写法 / 习惯 | 后果 | 正确做法 |
|---|---|---|---|
| 1 | 细节题不列清单,凭感觉写 | 漏掉一条特判就是 0 分,而且样例可能还过 | 把题面里每句「如果……则……」抄成表格,写完逐条打钩 |
| 2 | P1067 用 cout << a 直接输出负系数 |
出现 --3x^2 这种双负号 |
符号单独输出,系数只输出 |a| |
| 3 | P1067 常数项也省略 1 | 常数项 1 什么都输出不出来 | 省略 1 只对次数 > 0 的项生效 |
| 4 | P1328 把 0 当成石头 |
整张胜负表全错,样例都过不了 | 0 剪刀、1 石头、2 布、3 蜥蜴人、4 斯波克 |
| 5 | P1328 手写 25 个 if 分支 | 改一处漏一处,反对称性对不上 | 用 5×5 二维数组打表,查 r[a][b] |
| 6 | P1981 忘记结算最后一项 | 答案少了最后一项;最后一项为 0 时又不报错,极难发现 | 循环后补 ans = (ans + term) % 10000; |
| 7 | P1981 数字按单个字符读 | 1234567890 会变成 1、2、3…… 一串单数字 | num = num * 10 + (c - '0') 攒多位数 |
| 8 | P1981 只在最后取模 | 中间结果溢出 long long,WA |
每读一位、每做一次运算都 % 10000 |
| 9 | P1996 用 for(i=1;i<n;i++) |
只淘汰 n−1 个人,少输出一个编号 | 用 while(!q.empty()) 直到全部出圈 |
| 10 | 输出多余空格 / 少空格 | PE 或 WA。P1996 是空格分隔、P1067 完全没空格 | 统一用「第一个不加分隔符,后面都加」的写法 |
| 11 | 比赛时不检查就交 | 数组开小 → RE;int 溢出 → WA |
留 20 分钟专门核对数组大小、数据范围、边界样例 |
n=2,系数 1 0 1 应该输出什么?答:x^2+1。
过程:i=2 系数 1 → 是第一项且为正,不打 +;|1| = 1 且次数 > 0 → 省略 1;次数 > 1 → 输出 x^2。
接着 i=1 系数 0 → 跳过。最后 i=0 系数 1 → 不是第一项且为正 → 打 +;次数为 0 → 只输出系数 1。
拼起来就是 x^2+1。注意结尾不是 x^2+1x^0,也不会把常数项的 1 省掉。
答:因为同一场比赛,站在甲的视角和站在乙的视角看是同一件事。
r[a][b] = 1(甲赢)等价于 r[b][a] = -1(乙输),也就是 r[a][b] == -r[b][a]。
这是个很好的自查工具:填完表后逐对检查一遍,能揪出大半的手写错误。
2+3*4+5*6,按本文的算法每一步是什么?答:
| 读到 | op | term | ans |
|---|---|---|---|
| 2 + | + | 2 | 0 |
| 3 * | * | 2 | 2 |
| 4 + | + | 12 | 2 |
| 5 * | * | 5 | 14 |
| 6 结束 | * | 30 | 14 |
| 收尾 | — | — | 44 |
验算:2 + 12 + 30 = 44 ✓(如果从左往右硬算,会得到 ((((2+3)*4)+5)*6) = 150,完全错了)
n=4, m=2 的输出是什么?手动推一遍。答:2 4 3 1。
队列初始 1 2 3 4:
· 1 报 1 → 回队尾,队列 2 3 4 1
· 2 报 2 → 出圈 2,队列 3 4 1
· 3 报 1 → 回队尾,队列 4 1 3
· 4 报 2 → 出圈 4,队列 1 3
· 1 报 1 → 回队尾,队列 3 1
· 3 报 2 → 出圈 3,队列 1
· 1 报 1 → 回队尾,队列 1
· 1 报 2 → 出圈 1,队列空
结果 2 4 3 1 ✓
答:先判断这题有没有子任务分(题面里常常写「对于 30% 的数据,……」)。 有的话立刻写一个最朴素的暴力版本交上去,把部分分拿到手 —— 这比继续干想正解划算得多。
如果连暴力都想不出,就把前面已过题的边界情况再验一遍,保证会做的题一分不丢。 最后 20 分钟不要写新代码。
【细节模拟的三步】
1. 翻译:输入什么 → 处理什么 → 输出什么
2. 列清单:题面每句「如果……则……」= 一个 if
3. 手算样例:拿样例走一遍表格,再写代码
【"第一项"的通用写法】
bool first = true;
if(first){ 只有负数才输出负号; }
else { 正负都要输出; }
first = false;
// 好处:主逻辑只写一份,顺便解决最高次项系数为 0
【两两关系 → 打表】
int r[5][5] = {...}; // r[a][b]: 1 甲赢 / 0 平 / -1 甲输
res = r[a[i % na]][b[i % nb]];
// 自查:对角线全 0 + 关于对角线反对称
【只保留后 n 位的取模技巧】
num = (num * 10 + (c - '0')) % 10000; // 边读边模
term = term * num % 10000;
ans = (ans + term) % 10000;
// 让所有中间量都长不大 → 永不溢出
【加法切项的表达式求值(无括号,只有 + *)】
ans = 0; term = 1; num = 0; op = 0;
扫到数字结尾时:
op == 0 → term = num; // 第一个数
op == '*' → term = term * num % MOD; // 乘法优先,不结算
op == '+' → ans += term; term = num; // 结算上一项,开新项
循环结束后:ans += term; // ★★ 最容易忘的一句
【约瑟夫问题 · 队列模拟】
q = 1..n;
while(!q.empty()){
x = q.front(); q.pop(); cnt++;
if(cnt == m){ 输出 x; cnt = 0; } // 出圈,不回队
else q.push(x); // 回队尾继续排
}
【比赛前 10 分钟 / 最后 20 分钟】
开局:4 题全读一遍 → 定顺序 → 从最简单的开始
收尾:数组大小?long long?输出格式?多测清空?边界样例?
// 会做的题一分不丢,比多做一道难题更重要
高精度运算 + 一个月总结
long long 大概只能装 19 位十进制数,可有些题要算 500 位、2000 位、甚至上万位。
明天学的就是:用字符串/数组把大数拆成一位一位存起来,自己实现加减乘。
四道题是 P1601 高精度加法、P1303 A*B Problem、P2142 高精度减法、P1009 阶乘之和。 我们会写一套统一的模板(倒序存 → 逐位算 → 处理进位 → 去前导零),你以后可以直接抄进自己的代码本。
而且明天是 30 天计划的最后一天,会有一节完整的知识脉络回顾和下一步建议。