做OJ题解这个系列的时候,我习惯先把一段连续的题号当成一个整体来看,而不是一题一题孤立地刷。nbuoj的T1141到T1160这20道题,老实说不是什么高深莫测的压轴题,但恰恰是这种中前段的题目区间,最能暴露出基础功底的薄弱点。很多同学刷题喜欢挑难题啃,遇到这种“看起来很简单”的题反而眼高手低,结果在细节上反复WA。这篇博文我就把这20道题的命题思路、实现方案、常见卡点全部拆开讲一遍,把那些不写在题目里的经验一次性说清楚。
先说这套题适合谁。如果你正在学C/C++/Java/Python,刚掌握循环、数组、字符串、简单排序,准备打基础或者应付课程上机考试,那T1141-T1160这个区间就是很典型的“从语法到算法”的过渡带。题目本身不会考复杂的图论或动态规划,但它会逼你处理输入输出的边界、养成先想后写的习惯。换句话说,这几道题的价值不在一题一题做对,而在于逼你建立一套“提交前自检”的流程,这才是刷这段题真正的意义。
1. 先搞清楚T1141-T1160到底在考什么——整体设计与思路拆解
1.1 如何解读题目编号与区间特征
接触过几套不同的在线评测系统之后,你会发现一个规律:只要题号是连续递增的,通常意味着命题人按知识点模块在排布题目。nbuoj的命名从T11xx开始进入一个相对基础的训练段,T1141-T1160很可能是在集中训练某个大类的编程基本功。我自己的判断依据有两个:一是这个题号位置通常对应教学进度的中段,也就是语法刚讲完、算法还没系统开始的阶段;二是连续20题的数量,正好能覆盖输入输出、分支循环、一维数组、二维数组、字符串、简单排序、公式类模拟这几类高频考点。
与其猜每道题具体“长什么样”,不如先建立一个心理预期:这套题不会考你“会不会某个冷门函数”,更像是在考“你能否用最朴素的语法把一件事做对”。比如要求你读入若干行数据然后按条件筛选输出,或者给你一个公式让你算结果,再或者让你对一组数做去重和排序。这类题目描述往往不长,但陷阱一个不少:多组输入的终止条件是什么、数据会不会超出int范围、输出格式要求空格还是换行、浮点精度要不要处理。这些细节,恰恰是后面所有复杂题目的地基。
1.2 这段区间常见的题型分布与考察重点
基于对同类OJ题段的观察,T1141-T1160大概率可以按题型分成几类。第一类是纯输入输出模拟,这类题八成会出现在最前面,目的就是让你熟悉评测系统的交互方式,比如EOF判读到结束、每组数据之间的空行处理。第二类是一维数组的统计与变换,常见考法包括求最大值最小值及其位置、数组逆序输出、统计满足条件的元素个数。第三类是字符串相关,考法可能涉及字符统计、单词计数、简单加密移位,这类题看着简单,但对C语言同学来说字符串的\0结尾和gets/fgets的选择会直接影响AC率。
还有一类需要单独拎出来说,就是公式类或递推类题目。这类题描述起来常常带一点数学背景,比如数列求和、利息计算、几何面积求解,但本质上就是“把数学公式翻译成循环”。很多基础薄弱的同学不是不会写公式,而是不会把“n项求和”翻译成循环的起止条件。我会在第3节用一道典型公式题给你完整演示整个过程,包括怎么手算样例、怎么用循环表达累加、怎么避免累加器类型错误。
2. 核心解题套路与细节拆解——从读题到提交的完整链路
2.1 读题三步法:输入输出格式决定实现方式
做OJ题最忌讳的事,就是打开题目编辑器直接开始敲代码。我看过太多同学拿着T1142的代码改个变量名就提交到T1143,理由是“感觉差不多”,结果连样例都过不了。读题必须有步骤,第一步看输入格式,你要明确知道数据从哪里来、什么时候结束;第二步看输出格式,这决定你的结果是每行一个还是用空格分隔;第三步才看具体逻辑要做什么。
输入格式这个概念尤其重要,因为OJ评测机比对的是你的程序输出和标准输出,哪怕多打一个空格都是WA。我举个例子,如果题目说“输入包含多组测试数据,每组占一行,以0 0结束”,那你写的循环条件就是“只要不是0 0就继续读”,而不是“读n组就停”。如果你没注意到“多组”两个字,直接只读一次就输出,那这题几乎不可能AC。还有一种常考格式是“第一行是一个整数T,表示测试数据的组数”,这两种输入模式的代码写法完全不同,一个是while(cin>>a>>b)风格,一个是for(int i=0;i<T;i++)风格,混用就出事。
输出格式上,最常见的坑是“每组输出后跟一个空行”和“每组输出间有一个空行”的区别。前者是cout<<ans<<endl<<endl;,后者是当前组不是第一组就先输出一个换行再输出答案。这两个在题目描述里可能只是措辞不同,实际代码逻辑天差地别。我的习惯是读题时把输入输出两段描述复制到注释里,边读边标注“多组”“末尾无空行”“空格分隔”这些关键词,写代码时对着注释来,基本不会再栽跟头。
2.2 典型题型的实现细节:从暴力到优化意识的萌芽
T1141-T1160这个阶段还谈不上复杂算法,但已经需要你有“复杂度”的概念了。虽然没有明说,但很多题目数据范围是“n不超过1000”还是“n不超过100000”,直接决定了你能不能写二重循环。如果题目给了n不超过100000,你还在用冒泡排序或双重循环暴力统计,结果很可能就是TLE(超时)。这种意识早建立早受益,后面学到二分和哈希的时候,你才会明白为什么暴力不是万能的。
以排序题为例,如果n很小(几百以内),冒泡和选择怎么写都能过,代码也直观;但如果n达到10的5次方量级,你必须用sort()这类O(n log n)的排序。很多OJ是允许用标准库函数的,但有的教学OJ会刻意限制你只能手写排序。这个信息哪里来?看题目底部的提示或限制,如果一句话都没说,那就别炫技,怎么稳怎么来。我在刷这段题目的时候,提交前一定会确认一遍数据规模,再决定是“暴力美学”还是“标准库轻松搞定”。
数组题还有一个极其高频的细节:数组开到多大。很多新手喜欢打死开int a[100],一旦数据范围超过就数组越界,结果在OJ上体现为RE(运行时错误)而不是WA。正确做法是先看题目输入范围,如果是n≤1000,数组至少开到1005或1010,留出裕量;如果是字符串,注意多留一个位置给结束符。这个习惯在T1141-T1160里可能看不出多大差别,但如果你准备继续往后刷,规模一大,数组开小就是家常便饭,改起来还很隐蔽。
2.3 提交前的自检清单:减少一次无效提交
刷题效率高的人,不只是代码写得快,更重要的是他们很少“白交”。每次提交前,我都会在本地过一遍简短的自检清单,来回不到两分钟,却能省下大量无意义的WA后改错时间。
先看变量类型:累加和、乘积、阶乘这类结果会不会超过int的范围。int最大能到约21亿,2的31次方减1,一旦题目里出现“n不超过100000”或“数字不超过10的9次方”,结果很可能溢出,这时候直接用long long(C++里至少8个字节)最稳妥。然后是边界情况:n=1算不算?数组有没有可能是空的?字符串有没有可能包含空格?题目说“正整数”但还是有人读成int结果没有判断,一旦输入0或负数就出错。最后是样例验证:不光要跑通题目给的样例,还要自己构造边界样例,比如最小输入、最大输入、重复元素输入、单字符输入。这四类样例在本地能过,提交后才不会被评测机的隐藏数据教做人。
这套自检流程不需要写成文档贴在屏幕上,习惯之后就是条件反射。我大概用了两周左右,从“WA一次改一次”变成“提交三题AC三题”,核心变化不是代码能力变强了,而是漏掉的边界情况变少了。
3. 实操过程与核心环节实现——完整代码模板与调试方法
3.1 从零到AC的完整流程演示:拿一道公式题说事
思路讲再多不如实跑一遍。假设T1141里有一道很典型的题:输入正整数n,计算1^2 + 2^2 + ... + n^2的值,n不超过10000。题目看起来人畜无害,但它的价值在于逼你把“数学公式”转化成“循环逻辑”。绝大多数人第一反应是:直接套平方和公式n(n+1)(2n+1)/6不就行了?还真不一定,因为有些OJ命题人会故意在判定时避开公式,或者让你体会循环累加的过程。稳妥的解法是这样:
#include <iostream> using namespace std; int main() { int n; while (cin >> n) { long long sum = 0; for (int i = 1; i <= n; i++) { sum += 1LL * i * i; } cout << sum << endl; } return 0; }注意几个细节。第一,while(cin >> n)是因为题目可能包含多组数据,即使题目说了一个n,写成循环也不影响单组数据的正确性,但能兼容多组数据。第二,sum用long long,因为n到10000时,平方和大约是3333亿左右,int根本存不下。第三,1LL * i * i是强迫i在乘的过程中转成long long,避免i*i先按int计算再赋值给sum时已经溢出。
本地测试时,我会跑三个用例:n=1,预期输出1;n=3,手算1+4+9=14;n=10000,输出结果去网上搜平方和公式验算一下。如果这三个都对了,提交基本是稳的。别小看这种流程感,它就是养成“结构化测试”习惯的起点。
3.2 不同语言下的实现要点:C/C++/Java/Python的差异
nbuoj这类教学OJ通常支持多语言,但选择哪一门语言会直接影响你的写法。C语言的话,输入用scanf,输出用printf,注意long long对应的格式符是%lld,这个写错了输出就可能是一串乱码。C++就舒服很多,直接用cin和cout,但要注意关闭同步以加快速度,在main开头写一行ios::sync_with_stdio(false); cin.tie(0);,能避免大数据量输入时cin太慢导致的TLE。
Java写成Scanner配System.out.println最直观,但数据量大的时候Scanner性能偏慢,可以换BufferedReader和BufferedWriter,只是代码会稍微啰嗦。Python用户则要小心输入输出函数的性能差异,input()和print()在这个题量级基本够用,但如果未来刷到大数据量题,建议改成sys.stdin.read()和sys.stdout.write()。这段T1141-T1160的题,重点不是性能竞赛,而是让你把语法掌握到“不看文档能写出来”的程度,所以选你最熟的语言就好。
3.3 本地测试与OJ提交的差异:为什么样例过了还是WA
这是刷题过程中最让人崩溃的瞬间:样例一模一样,提交却是WA。原因通常分三类。第一,样例只是“示意”,覆盖不到所有数据情况,评测机的数据里藏着大量你不知道的边界条件。第二,本地运行环境与OJ环境有差异,比如编译器版本不同导致某些函数行为不一致,C语言用gets()在部分OJ上可能直接编译报错,换成fgets()或cin.getline()才稳。第三,你本地测试时根本就没输入多组数据,而题目却要求处理多组,一旦评测机给的是两行数据,你的程序输出就只有第一行的结果,自然WA。
针对上面这个问题,我的实战经验是:无论题目描述写没写“多组输入”,都用while(读入成功)的模式去写,代价几乎为零,兼容性却极大提升。再有一个细节是空白字符:cin和scanf会自动跳过空格和换行,但如果你用getchar()或者gets()去读混合数据,就要非常小心末尾的换行符会不会残留在缓冲区里。常见表现是“明明输入正确,但第一个数据读不到”,排查半天发现是上一行输入结尾的\n被当成下一次输入读走了。处理办法是读完整行或显式吃掉换行符,这个坑在字符串相关题目里出现频率极高。
4. 常见问题与排查技巧实录——那些让你卡题的真实原因
4.1 AC失败类型速查表与应对策略
T1141-T1160这个阶段,大家卡题的最主要原因其实不是什么高深的算法不会,而是下面这几个低级问题反复出现。我把它们整理成一个速查表,刷题的时候对照着看,排查效率能高不少:
| 反馈类型 | 含义 | 本阶段最常见原因 | 排查方向 |
|---|---|---|---|
| WA | 答案错误 | 输出格式不对、变量类型溢出、边界条件漏判 | 检查空格/换行、试边界数据、换long long |
| TLE | 超时 | 双重循环不必要、cin未关同步、死循环 | 看数据规模、优化循环层数、关同步 |
| RE | 运行时错误 | 数组越界、除以0、栈溢出 | 检查数组大小、确认除数不为0 |
| PE | 格式错误 | 输出多空格、少换行 | 逐字符比对标准输出 |
| CE | 编译错误 | 变量未声明、函数名拼错、语言特性不支持 | 看编译器报错信息第一行 |
这张表里,WA和PE最接近,区别在于PE通常只是格式问题,数据本身是对的。但很多OJ只要格式不对就算WA,不会单独提示PE,所以你要养成最严格的自查习惯:输出每一行的结尾有没有多余的空白字符,组与组之间到底是一个空行还是多个空行。宁可多写一行代码把格式处理精确,也不要抱侥幸心理。
我在刷这段题的时候,遇到最多的是WA中的“变量类型溢出”,特别是累乘类题目。比如题目要求计算n的阶乘,n到20时结果已经超过2.4e18,接近long long的极限了;如果n到30,任何内置整数类型都存不下,题目可能暗示要你用大数或模运算。这些信息不会写在“提示”里,只能靠你算一下数量级来预判。本阶段遇到大数题的概率不高,但阶乘题出现的概率不低,直接把累加器定义成long long能避开80%的坑。
4.2 从WA到AC的排查路径:一个实战案例复盘
模拟一种最容易让人灰心的情况:T1148你写完了,本地样例过,提交WA。这时候不要立刻去改代码,先把刚才的速查表过一遍。本地重新构造几组数据:第一组是最小边界,比如输入1或0;第二组是最大边界,按题目限制来;第三组是重复元素较多的数据;第四组是随机中大规模数据。如果这四组都能得到你预期的输出,再检查是不是输出格式有偏差,比如句末少了一个句号、大小写不对。OJ输出比对是逐字符的,Yes和YES就是两个完全不同的答案,别指望评测机“通情达理”。
如果这轮排查什么都没找到,问题可能出在算法逻辑上。一个很有效的办法是拿笔画出程序流程图,沿着代码逐行走一遍,看每个变量在每个分支里的取值是否和你预期一致。我在调试这种题时,常犯的错是把循环边界写错:本意是i < n却写成了i <= n,结果多算了一个元素;或者字符串长度函数返回的长度包含了结束符,导致字符处理时多处理一位。这些错误用肉眼盯半天可能都发现不了,但输出一对照,答案差的就是最后一个元素。
有时候,WA的根源是题意理解错了。比如题目说“输出最大值及其位置”,如果有多个最大值,要求输出“第一个最大值的位置”还是“最后一个最大值的位置”?很多同学默认从头遍历找第一个,但题目考察的却是最后一个。这种理解偏差,仅靠样例根本发现不了。所以遇到WA,我建议重新逐字读一遍题目描述,特别是副词和限定词。如果题目是英文翻译来的,更要注意“first”和“last”、“maximum”和“minimum”这类看似无关紧要的差别。宁愿多花五分钟重新读题,也别急着交第二次。
4.3 心态与节奏管理:连续做20道题的正确姿势
一口气刷20道题,心态很容易从“兴奋”变成“麻木”。我见过有的同学前面几题AC得很快,到T1145连续WA三次后就开始烦躁,直接去看别人题解,抄完代码AC了,但下次遇到类似题目照样不会。这种做法没有任何意义。正确姿势是给自己设定一个“独立思考时间”:比如每题至少想15分钟,15分钟内绝对不能看题解;如果15分钟还没思路,允许看题解的思路部分,但看完要自己关掉代码独立实现;如果还是卡住,再看完整题解,但看完必须按自己的理解从头写一遍,不能直接复制。
提交策略上也有一点心得:不要一次性把代码全部写完再提交,而是先写一个能跑的主框架,确保没有语法错误,再逐步填充核心逻辑。这样做的好处是,每次提交如果WA,你能更快定位是哪段新增代码出了问题。尤其是字符串题和数组题,代码越长越容易在小地方出错,分阶段提交等于把排查范围一步步缩小。等这套题刷完,你会发现自己写代码的“容错率”明显提升,调试时间变短,这就是20道题带来的实打实的进步。
5. 这套题往后延伸:如何把T1141-T1160的价值最大化
5.1 刷完之后的总结模板:从AC到内化
很多人刷完20道题就觉得自己“完成任务了”,实际上最大的价值是在刷完之后。我会建议你把每一道题按照固定格式做一个总结:题号、考点、思路关键词、代码实现中的独特写法或坑点。比如T1141可能总结成“多组输入+累加器long long”,T1145可能总结成“二维数组行列转换的边界条件”,T1150可能总结成“字符串字符统计注意空格处理”。这个总结不需要很长,每道题两三行就够,但它的作用是让你过一段时间回顾时,能快速回忆起当时的思考路径。
做完总结后,你还可以做一次“变形提问”:把题目条件改一改,比如把整数变成实数、把最大值换成次大值、把固定n次输入换成EOF输入,然后逼自己口述新的解法。这种训练相当于给你大脑里的“解题模板”做迁移测试,比闷头再刷20道新题更高效。我认识的一些竞赛选手,就是靠这种“一题三做”的方法,把每道题的考点吃得透透的,遇到新题时自然见招拆招。
5.2 从这20题到更高级题目的难度跳跃
T1141-T1160无论如何都只是基础区间,刷完之后你大概率会面对一道坎:从“模拟题”跨到“算法题”。模拟题的特点是逻辑直白、数据结构常规,你只要认真读题就能写出大框架;算法题则不一样,它往往不会直白地告诉你“这道题考排序”,而是把排序藏在“求中位数”或“统计众数”的描述里,或者把递推藏在“上楼梯有多少种走法”的伪装下。这种抽象能力不是天生的,需要从基础题里积累“识别考点”的嗅觉。
从T1141-T1160往后的区间,我猜测会逐渐引入结构体排序、前缀和、差分甚至基础的动态规划。如果你现在能在这段基础题里把数组和字符串用得滚瓜烂熟,那后续的难度跳跃会平滑很多。相反,如果你现在连“反转数组”都要想半天,那后面学指针、链表、递归时会加倍吃力。基础题最大的价值不是让你AC,而是让常见操作变成肌肉记忆,这样大脑才能腾出空间去处理更抽象的算法思想。
5.3 推荐的学习节奏与进阶路线
如果每周能拿出固定时间练习,我的建议是每周完成15到20道题左右为宜,具体分配是:周一到周五每天集中刷3到4道,周末用半天时间复盘本周所有WA和TLE的记录。复盘不是看自己错了多少,而是要把同类错误归类,比如“这周有3次是因为输出格式不对”“有2次是因为忘记多组输入”。这样归完类,下周刷题时你就有明确的针对性,效率远高于每天无脑刷。
进阶路线方面,这段结束后可以按顺序挑战:结构体与排序、栈与队列、递归与搜索。不要一上来就碰高深的数据结构,很多教程推荐直接学线段树或并查集,但我认为没有把基础数组和递归吃透之前,学这些东西只会让你产生挫败感。刷OJ像盖楼,T1141-T1160这一层虽然不起眼,但它决定了后面楼层稳不稳。你可以在之后某一天回头看看自己这段时期的代码,那种“原来当年我写这么笨拙的代码”的感觉,恰恰是进步最直观的证据。
最后分享一个我一直在用的习惯:每道题AC之后,我会刻意再把代码重构成更精简的版本。不是所有OJ都要求代码最短,但这个重构过程会让你对语言的表达能力有更深的理解。同样的逻辑,有人写二十行,有人写十行,这个差距不是语法熟练度,而是思维清晰度。T1141-T1160这20道题,足够你在重构中体会这种差异了。