Appearance
机试读入:多组数据与不定长输入
场景引入
一道题的算法你十分钟就想明白了,代码写完,本地样例跑对,交上去红了。改了三版逻辑还是红,最后发现题面第一行写着一句「本题包含多组测试数据」——你的程序只处理了第一组就退出了。
这类翻车在机试里很常见,而它和算法能力没什么关系。面试写的是函数,参数由平台喂给你;机试写的是完整程序,从标准输入读到什么、读多少、什么时候停,全部由你自己决定。这一篇把题面的几种写法和对应的读入骨架对上号。
先看懂题面在说哪一种
输入格式的写法就那么几种,认出来就知道该套哪个骨架。
下面逐个说。
一、EOF 结尾:读到没有为止
题面的说法通常是「处理到文件结束」「输入包含多组数据,请一直读到 EOF」,或者干脆什么都不说,只给一个输入样例里有好几组。
C 的骨架靠 scanf 的返回值。scanf 返回的是成功赋值的变量个数,读到文件末尾时返回 EOF:
c
#include <stdio.h>
int main(void) {
int a, b;
while (scanf("%d %d", &a, &b) != EOF) {
printf("%d\n", a + b);
}
return 0;
}C++ 用 cin 更短,因为 istream 在读取失败时会转成 false:
cpp
#include <iostream>
using namespace std;
int main() {
int a, b;
while (cin >> a >> b) {
cout << a + b << '\n';
}
return 0;
}有一个更稳的写法:把 != EOF 换成 == 参数个数。
c
while (scanf("%d %d", &a, &b) == 2) { ... }区别在于遇到格式不匹配的时候。假如输入里混进了一个字母,scanf 会赋值失败并返回已成功赋值的个数(比如 1),这时 != EOF 判定为真、循环继续,而那个字母还堵在缓冲区里没被消费掉,下一轮又读不动——程序就在原地死循环。写成 == 2 则直接退出,行为可预期。
本地怎么模拟输入结束:在终端里手动输入数据时,Windows 按 Ctrl+Z 再回车,Linux 和 macOS 按 Ctrl+D。更省事的做法是把样例存成 in.txt,运行时重定向:./a.out < in.txt。
二、T 组数据:先读组数再循环
题面会明确写「第一行为一个整数 T,表示测试数据的组数」。
c
int T;
scanf("%d", &T);
while (T--) {
int n;
scanf("%d", &n);
// 处理这一组
}while (T--) 是机试里的惯用写法:判断 T 是否非零,然后把它减一。T 为 3 时循环体执行 3 次,结束后 T 是 -1。
这个骨架有一个容易漏的点:每组数据用到的数组、计数器、标记,都要在循环体内部重新初始化。
c
while (T--) {
int cnt = 0; // 在循环内定义,天然是新的
memset(vis, 0, sizeof(vis)); // 全局数组要手动清
// ...
}全局数组不清零是这一类题最典型的错误:第一组答案对,第二组开始就被上一组的残留污染。而样例往往只有一两组,本地跑着是对的。
如果 vis 很大而每组实际只用到前 n 个,用 memset(vis, 0, sizeof(int) * (n + 1)) 只清用到的部分,避免每组都白清几百万个字节。
三、哨兵结尾:读到某个特殊值停下
题面会写「输入以一行 0 结束」「当 n 和 m 都为 0 时输入结束,该行不作处理」。
c
int n, m;
while (scanf("%d %d", &n, &m) == 2 && !(n == 0 && m == 0)) {
// 处理
}注意题面那句「该行不作处理」——终止条件必须在处理之前判断完,不能先算再判,否则会多输出一组答案。
单个哨兵值的写法更短:
c
int n;
while (scanf("%d", &n) == 1 && n != 0) { ... }四、不定行数、每行不定个数
这一类的输入长这样:每一行是一组数,行内数字个数不固定,行数也不固定。按 %d 一个个读会把所有行连成一片,因为空格和换行在 scanf 眼里是一回事。
办法是先整行读进来,再从这一行里拆数。
C 用 fgets 配 sscanf 的偏移量:
c
#include <stdio.h>
int main(void) {
char line[100000];
while (fgets(line, sizeof(line), stdin) != NULL) {
int sum = 0, x, pos = 0, len;
// %n 把「本次匹配消耗了多少字符」写进 len,用来推进 pos
while (sscanf(line + pos, "%d%n", &x, &len) == 1) {
sum += x;
pos += len;
}
printf("%d\n", sum);
}
return 0;
}C++ 用 getline 配 istringstream,可读性好很多:
cpp
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
int main() {
string line;
while (getline(cin, line)) {
istringstream iss(line);
int x, sum = 0;
while (iss >> x) sum += x;
cout << sum << '\n';
}
return 0;
}istringstream 把一行字符串包装成一个输入流,于是行内的拆分复用了 >> 的空白分隔规则,而行与行之间由 getline 隔开。这是处理「行结构有意义」的输入时的通用手法。
拿不准的时候怎么办
题面没说清数据组数,样例里也看不出来,这种情况是有的。此时 EOF 骨架是更安全的选择:只有一组数据时,循环体执行一次然后遇到 EOF 退出,结果和单组写法完全一样;而单组写法遇上多组数据就只能答对第一组。
换句话说,EOF 骨架在两种情况下都成立,单组写法只在一种情况下成立。默认写 EOF 骨架,代价只是多一层 while。
延伸阅读
- 机试读入:字符串与混合格式 ——
%c吃掉换行、带空格的整行怎么读 - 机试输出:格式与精度 —— 输出对了却判错的几种原因
- 机试环境:IO 速度与内存边界 —— 数组开多大、什么时候会超时