刷OJ的老哥应该都体会过那种憋屈:算法想破了头,逻辑写出来也自认为天衣无缝,结果一提交不是WA就是TLE,最后定睛一看,问题居然出在输入输出上。要么是数据读不全,要么是多输出一个空格被判格式错误,要么是死循环等不到EOF直接超时。我甚至见过有人因为没处理行尾的\r字符,在本地怎么跑都对,一上OJ就疯狂WA,排查了半小时才发现是换行符在作怪。
这篇东西就是专门聊OJ在线编程里的输入输出,不聊算法,只聊怎么把数据正确、高效、不出幺蛾子地读进来,再按题目要求的格式写出去。无论你是在华为OJ、东华OJ这类平台刷题,还是备战机试,输入输出这一关不过,后面全是白搭。我会把常见的输入格式归纳成几类模板,用Python、Java、C++三种语言各写一套可直接抄的写法,再把那些藏在角落里的坑一个个挖出来。刚接触OJ的新手可以照着抄,刷了几年题的老手也可以看看有没有你自己都没注意到的细节。
1. 为什么说OJ的输入输出是先于算法的一道坎
1.1 本地IDE和OJ评测的真正区别
很多人一开始不理解,我在自己电脑上跑得好好的,凭什么到了OJ上就各种奇怪报错?关键差异在于,本地IDE里你通常是自己手动输入数据,或者写死在代码里,程序只要能跑出预期值就算"通过";而OJ的评测系统是把你编译好的程序跑起来,然后把测试数据通过标准输入(stdin)灌进去,再把你的程序输出到标准输出(stdout)的内容抓出来,和标准答案做逐字节比对。
这就意味着两件事:第一,你的程序必须自己知道"什么时候该停止读入",没人会像你本地测试那样摁一下回车再手工结束输入;第二,你输出的每一个空格、每一个换行、每一处大小写都会被评测机拿来做严格比对,多一个行末空格都可能被判Presentation Error,而在部分平台上这种格式错误和WA按同一个结果处理。
所以,读入方式本质上是在和一个"没有感情的比对机器"打交道。你需要明确这套规则,而不是凭本地测试的体验去猜。
1.2 我见过的几类输入格式模板
刷了这么多年OJ,题目里出现的输入格式翻来覆去就那么几种。把它们归纳成一个表,你以后做题就可以直接对照:
| 类型 | 特征描述 | 典型结束条件 |
|---|---|---|
| 固定数量 | 第一行给定总数据量,或者干脆固定几个数值 | 读完指定数量即可 |
| T组模式 | 第一行是T,表示后面有T组测试数据 | 处理完T组 |
| EOF模式 | 不告诉你有多少数据,一直读到文件末尾 | stdin返回EOF |
| 标记结束 | 读入特殊值(如0 0)表示输入结束 | 遇到哨兵值 |
| 行数未知的字符串/矩阵 | 每行是字符串或矩阵行,但不告诉你共有几行 | 读到EOF,按需拼接 |
我见过太多人在EOF模式那里栽跟头。比如用C语言写while(1){ scanf("%d",&a); ... },永远等不到结束;或者用Python的input()在数据读完时抛EOFError导致Runtime Error。这些问题的本质都是同一个:你没有理解评测系统用文件重定向的方式给程序喂数据,所谓"输入结束"就是读到文件末尾,不是让你敲Ctrl+D或者Ctrl+Z。
另外,题目里说的"输入包含多组测试数据"并不一定把组数写在第一行,你得学会分辨。我在下面第2节会把这些模板逐一展开,每一步都给出完整代码和背后的原理。
2. 核心细节解析与实操要点:先把输入读明白
2.1 读多组数据必须掌握的EOF写法
先说说EOF(End of File)这个概念。OJ评测时,测试数据并不是你在终端里手敲进去的,而是一个预先存好的文件,系统把文件内容作为标准输入流喂给你的程序。你的程序读数据读到文件末尾,就是EOF,这时必须主动结束读取,否则会一直阻塞下去,最后被判超时。
三种语言处理EOF的标准姿势如下:
Python读整行,循环到读不到为止:
import sys for line in sys.stdin: line = line.strip() if not line: continue a, b = map(int, line.split()) print(a + b)Java用BufferedReader循环读行:
import java.io.BufferedReader; import java.io.InputStreamReader; public class Main { public static void main(String[] args) throws Exception { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); String line; while ((line = br.readLine()) != null) { line = line.trim(); if (line.isEmpty()) { continue; } String[] parts = line.split(" "); int a = Integer.parseInt(parts[0]); int b = Integer.parseInt(parts[1]); System.out.println(a + b); } } }C++最简洁,cin >> a >> b这个表达式本身在到达EOF时会返回false,所以可以直接作为循环条件:
#include <iostream> using namespace std; int main() { int a, b; while (cin >> a >> b) { cout << a + b << endl; } return 0; }注意Python的input()在读到EOF时会抛EOFError,所以多数据读取我一般优先用sys.stdin的迭代方式,而不是死循环调input()。另外,line.strip()这一步不能省,因为读取到的行末尾通常带着换行符\n,你如果直接split(),它是能把\n当作空白符切掉的,但万一题目数据里带空行,或者行首行尾有多余空格,不处理的话容易出现解析异常或者解析到空数组。
2.2 读一行字符串时最容易翻车的点
如果题目给的每一行是整数,用split()按空格切一切基本没什么问题。真正容易翻车的是读字符串。比如要读一行带空格的英文句子,或者按字符矩阵读图,这时候很多新手会踩两个坑。
第一个坑是Python的input()和sys.stdin.readline()混用。input()本质上也是读一行,但它会去掉行尾的换行符,而sys.stdin.readline()不会。如果数据量大,推荐用sys.stdin.readline(),但记得自己strip()一下。某些平台数据量大到input()都会超时,这时候要换成sys.stdin.buffer.readline(),读出来是字节串,再用.decode()转成字符串,或者直接用字节流处理。我在第3.2节再展开讲高性能读取。
第二个坑是Java的Scanner和BufferedReader混用时的"吞行"问题。比如你先用nextInt()读了一个整数,再用nextLine()读字符串,会发现读到的是个空字符串。因为nextInt()只读数字,不消费数字后面的换行符,换行符留在了缓冲区里,nextLine()一读就只读到这个残留的换行,直接返回空串。解决方法是读完数字后再加一次nextLine()把换行吞掉,或者干脆所有数据都用BufferedReader.readLine()按行读,再自己解析。后者更稳,尤其是数据量大的时候,Scanner慢得让人想砸键盘。
C++那边也有类似的坑,cin >>之后如果再用getline(cin, str),一样会读到残留换行。我的习惯是如果一行内既有数字又有字符串,要么全用cin >>按空白符切分,要么全用getline读整行再手动解析,两者混用必须小心处理换行符。
2.3 输出格式:那些被判格式错误的魔鬼细节
输入读明白了,输出这关又有一堆说多了都是泪的细节。我整理成一份清单,每条都是真实踩坑记录:
- 行末空格:一组数据内多个元素要用空格分隔,但最后一个元素后面不要再跟空格。有些平台对行末空格睁一只眼闭一只眼,但也有不少平台严格比对,直接判格式错误。保险做法是用
print(" ".join(map(str, arr)))或者C++里判断是不是最后一个元素。 - 多个case之间的空行:有的题目要求"每组输出之间用一个空行隔开",这时候就要特别注意最后一组输出后面不能再多打一个空行,否则末尾多一个换行也可能被判定为格式错误。
- 小数位数:保留几位小数不是四舍五入就行,格式化时"四舍六入五成双"的坑在部分语言里也存在。C++里
printf("%.2f")做的是四舍五入,但Python的round()在某些边界值上表现不同。建议按题目要求用格式化字符串处理,不要自己手写舍入逻辑。 - 大小写问题:有些输出要求YES/NO,你输出Yes/yes,WA得不明不白,这个纯属不仔细。
- 变量范围:题目里如果要求输出大数,直接用
int可能溢出。Python不用管,Java用long甚至BigInteger,C++用long long。这种错误往往表现为答案"看起来差不多但最后几位不对",最迷惑。
3. 实操过程:三语言处理同一套输入输出模板
3.1 模板一:第一行T组数据,每组两个数
有些题目把测试组数写明白,格式是"第一行一个整数T,表示有T组测试数据,接下来T行,每行两个整数"。这种模式最简单,因为循环次数是确定的。三种语言的标准写法和对应的执行逻辑如下:
Python:
import sys def main(): data = sys.stdin.read().strip().split() if not data: return t = int(data[0]) idx = 1 out = [] for _ in range(t): a = int(data[idx]) b = int(data[idx + 1]) idx += 2 out.append(str(a + b)) sys.stdout.write("\n".join(out)) if __name__ == "__main__": main()Java:
import java.io.BufferedReader; import java.io.InputStreamReader; public class Main { public static void main(String[] args) throws Exception { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); int t = Integer.parseInt(br.readLine().trim()); StringBuilder sb = new StringBuilder(); for (int i = 0; i < t; i++) { String[] parts = br.readLine().trim().split(" "); int a = Integer.parseInt(parts[0]); int b = Integer.parseInt(parts[1]); sb.append(a + b).append("\n"); } System.out.print(sb.toString()); } }C++:
#include <iostream> using namespace std; int main() { int t; cin >> t; while (t--) { int a, b; cin >> a >> b; cout << a + b << endl; } return 0; }这里我特别推荐一个习惯:不要每算一个结果就立刻print一次。频繁的输出操作会拖慢程序速度,更好的是把结果收集到列表、StringBuilder或者ostringstream里,全部算完一次性输出。尤其在数据量大、输出行数多的时候,这种"攒一波再输出"的做法能明显减少运行时间,说不定就从TLE变成AC了。
3.2 模板二:读取到文件末尾,行数未知
更常见的模式是"输入包含多组测试数据,每组一行,处理到文件末尾"。这种题型你需要显式处理EOF,我在2.1节给过基础写法,但这里要补充大数据量场景下的性能优化。
Python优先用sys.stdin.buffer:
import sys def main(): out = [] for line in sys.stdin.buffer: if not line.strip(): continue a, b = map(int, line.split()) out.append(str(a + b)) sys.stdout.write("\n".join(out)) if __name__ == "__main__": main()sys.stdin.buffer读出来的是字节串,遍历它的每一行相当于走缓冲区迭代,比input()快很多。之前我在一个数据量上百万的题里实测过,用input()会超时,换成buffer后耗时直接降到一半以下。瓶颈常常不在算法复杂度上,而是IO效率,这个点很多新手完全没意识到。
Java用BufferedReader是标配,但如果追求极致性能,可以用StreamTokenizer或者把输出拼到StringBuilder里,避免大量System.out.println调用:
import java.io.BufferedReader; import java.io.InputStreamReader; public class Main { public static void main(String[] args) throws Exception { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StringBuilder sb = new StringBuilder(); String line; while ((line = br.readLine()) != null) { line = line.trim(); if (line.isEmpty()) continue; String[] parts = line.split(" "); int a = Integer.parseInt(parts[0]); int b = Integer.parseInt(parts[1]); sb.append(a + b).append("\n"); } System.out.print(sb.toString()); } }C++这边,如果你用的是cin,建议在main开头加上这两句:
ios::sync_with_stdio(false); cin.tie(nullptr);第一句关闭C++标准IO和C标准IO的同步,第二句取消cin和cout的绑定,可以明显加速。不加这两句,cin在某些OJ上会被慢到怀疑人生。当然,scanf/printf本身也不慢,但和cin/cout混用可能出乱序问题,所以二选一,别混着来。
3.3 模板三:遇到指定标记(如0 0)才结束
有的题目会用特殊值充当"哨兵",比如"输入包含多组数据,每组占一行,包含两个整数a和b,输入以0 0结束"。这种情况下,你要先把数据读进来,判断是不是哨兵值,是就直接结束,不是就继续处理。
Python:
import sys def main(): out = [] for line in sys.stdin: line = line.strip() if not line: continue a, b = map(int, line.split()) if a == 0 and b == 0: break out.append(str(a + b)) sys.stdout.write("\n".join(out)) if __name__ == "__main__": main()Java:
import java.io.BufferedReader; import java.io.InputStreamReader; public class Main { public static void main(String[] args) throws Exception { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StringBuilder sb = new StringBuilder(); String line; while ((line = br.readLine()) != null) { line = line.trim(); if (line.isEmpty()) continue; String[] parts = line.split(" "); int a = Integer.parseInt(parts[0]); int b = Integer.parseInt(parts[1]); if (a == 0 && b == 0) break; sb.append(a + b).append("\n"); } System.out.print(sb.toString()); } }C++:
#include <iostream> using namespace std; int main() { int a, b; while (cin >> a >> b) { if (a == 0 && b == 0) break; cout << a + b << endl; } return 0; }注意一个细节:哨兵值本身不应该作为有效数据处理。有些题目里0 0可能同时是合法数据,那就得看题目明确说"输入以某值结束"这类字眼。如果题目没说,就别自己脑补结束条件,优先用EOF。
3.4 输入带空格的字符串行和矩阵场景
字符串题是另一个重灾区。比如输入第一行是整数n,接下来n行是包含空格的句子,你需要逐行处理。这种场景要是用cin >>或者input().split(),空格会被当成切分符,整句话就被拆碎了。
正确做法是"读整行再处理"。Python里直接用input()读整行,Java用readLine(),C++用getline(cin, str)。
假设题目要求:第一行是n,接下来n行是字符串,每行字符串可能含空格,你需要输出每行字符串去掉首尾空格后的长度。Python:
import sys def main(): data = sys.stdin.read().splitlines() if not data: return n = int(data[0].strip()) for i in range(1, n + 1): s = data[i].strip() print(len(s)) if __name__ == "__main__": main()这里用read().splitlines()把整个输入按行拆开,比逐行readline()好在可以自由跳转行号。但要注意,如果文件很大,一次性读入所有内容会占内存,一般OJ题目不会变态到那种程度,但如果真遇到了,还是老老实实逐行读。
矩阵输入类似,每行是若干整数,自己按空格拆分后存进二维数组即可。C++里如果输入格式是"n m"开头,后面跟着n行m列矩阵,通常这么写:
int n, m; cin >> n >> m; vector<vector<int>> grid(n, vector<int>(m)); for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { cin >> grid[i][j]; } }4. 常见问题与排查技巧实录
4.1 本地跑得好好的,一提交就WA或Runtime Error
这道题我见得太多了,我自己也栽过一回。有一次写递归题,本地测试几个用例全对,一提交就WA,后来发现题目要求输入到EOF,而我的代码只读了固定数量的case,后面数据根本没进程序。所以排查顺序应该是:先确认读入方式是否符合题目描述,是EOF还是T组还是哨兵值;再看是不是有潜在的空行或首尾空格没处理;最后看有没有数组越界、除零、空指针这类运行时崩溃隐患。
另外还有一个非常隐蔽的点:Java的类名必须是Main,不能是别的,否则评测系统找不到入口直接报Runtime Error。我见过有人类名写Solution,本地能跑,提交就错。C++和Python倒没有这个规定,但Java这套必须记住。
4.2 大数据量读入超时(TLE)的处理
TLE不一定是你算法问题,也可能是IO太慢。我在3.2节已经提到Python换成sys.stdin.buffer、Java用BufferedReader代替Scanner、C++关闭cin同步。这里再补充一个点:Python里如果要用sys.stdin.buffer读字节串,但后续处理需要字符串,可以这样转:
import sys def main(): data = sys.stdin.buffer.read().split() # data里的每个元素都是bytes类型 arr = [int(x) for x in data] # 直接用int(bytes)能转,不需要decodeint()可以直接接收bytes类型,所以不需要手动decode,省一步是一步。输出端也一样,用sys.stdout.write而不是print也会快一点,尤其是循环打印几千行时。
Java那边还有个常见优化点:不要用System.out.println在循环里逐行输出,而是拼接到StringBuilder,最后一次性print。我实测过一个题,逐行输出耗时比一次性输出高出几倍,这个差距在某些严格判题环境下足以让你TLE。
4.3 行尾空格和多余空行:怎么自查
输出格式问题最气人,因为逻辑明明对,就是格式不对。我自己的检查方法很简单:如果题目要求每组输出一行,那就用我前面说的"\n".join(...)方式,把每个结果拼成一个大字符串再输出,这样行与行之间只有一个换行,行尾天然没有多余空格。
如果输出一行里是多个数字,最稳的是先放到列表,再用" ".join(map(str, list))拼接。C++的话可以这样:
for (int i = 0; i < n; i++) { if (i) cout << " "; cout << arr[i]; } cout << endl;也就是除了第一个元素,其余前面都输出一个空格,这样行尾不会有多余字符。千万别写for循环里面每次输出arr[i] << " ",最后整个行尾就多一个空格。
多个case之间要求空行时,我习惯用一个first标志位来判断是不是第一个输出块,不是第一个就先输出一个空行再输出内容。Java和Python的处理思路一样,用列表或者StringBuilder判断是否需要额外换行。
4.4 字符串输入里带着\r和看不见的字符
Windows下编辑的测试数据文件,行尾是\r\n,而Linux下是\n。OJ服务器大多是Linux,但数据如果是从Windows环境生成上传的,你的程序可能会读到\r。Python的strip()能去掉\r,所以还好;但C++的getline读出来的字符串末尾可能带着一个\r,你拿它去比较字母或哈希,就会莫名WA。
排查方法很简单,写个临时调试输出,把每个字符的ASCII码打出来看。C++里这样查:
for (char c : s) { cout << (int)c << " "; } cout << endl;如果末尾出现13,那就是\r没跑了。处理方式是在getline之后手动去掉末尾的\r:
if (!s.empty() && s.back() == '\r') { s.pop_back(); }还有一类"看不见的坑"是编码问题。比如题目要求处理英文字母,结果测试数据里带了全角空格或者中文标点,split()切不干净,length()也和你预期的不一样。这种基本只能靠仔细读题确认字符集,如果题目说"仅包含小写字母",那可以放心按ASCII处理。
整体而言,输入输出看起来是每个OJ题最简单的一步,但恰恰是它决定了你的程序能不能正确和评测系统对接。把EOF、换行符、缓冲区、输出拼接这些底层机制吃透,很多WA和TLE其实是可以在写算法之前就避免的。我自己的习惯是每换一个新OJ平台,先做一道纯输入输出练习题试试水,把该平台的读入行为摸清楚,再上难度。毕竟每个平台的判题规则偶尔会有细微差异,用一道水题探路,比等做到难题才发现平台特性要划算得多。