记住两个函数轻松处理 Go 语言 OJ 输入

处理操作系统的标准输入输出在 Web 项目开发中并不常用,像 LeetCode 一些现代刷题平台也取消了输入输出的处理,让用户专注于算法的实现。但对于传统的 OJ 平台和算法竞赛,往往需要用户从标准输入中读取指定格式的数据,然后使用标准输出返回答案。如果平时没有经验的话,在比赛或笔试时可能会浪费不必要的时间。本文主要介绍 Go 语言的 fmt.ScanScanner.Scan 函数来应对常见的 OJ 输入。

两个函数

先考虑常规的 OJ 数据格式:使用空格分隔数据元素,使用换行符分隔不同组数据。

fmt.Scan

1
func Scan(a ...any) (n int, err error)

Scan scans text read from standard input, storing successive space-separated values into successive arguments. Newlines count as space. It returns the number of items successfully scanned. If that is less than the number of arguments, err will report why.

fmt.Scan 方法会从标准输入中连续读入由空格(或换行)分隔的变量到传入的参数。当读入的变量个数小于传入的参数个数时,err 会报告错误。也就是说,Scan 方法会一直读取变量直至读取完毕所有输入。

根据 fmt.Scan 方法的这个特性,可以方便地处理空格对数据的划分,但由于换行也视作空格,导致无法区分数据组。因此使用该方法读入数据时,我们需要提前知道每组数据的长度,根据这个长度来对数据组进行划分即可。具体见下面两个例子。

例 1. 两数之和

输入描述
1
输入包括两个正整数a,b(1 <= a, b <= 1000),输入数据包括多组。
输出描述
1
输出a+b的结果。

示例

输入
1
2
1 5
10 20
输出
1
2
6
30

解答

由于每个用例固定有两个数据,只需要两个两个输入读入,然后计算两数之和即可:

两数之和
1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func main() {
var a, b int
for {
_, err := fmt.Scan(&a, &b) // 两个两个变量读入
if err != nil { break } // 读到末尾结束循环
fmt.Println(a + b)
}
}

例 2. 数列之和

输入描述
1
2
3
输入数据有多组, 每行表示一组输入数据。
每行的第一个整数为整数的个数n(1 <= n <= 100)。
接下来n个正整数, 即需要求和的每个正整数。
输出描述
1
每组数据输出求和的结果

示例

输入
1
2
4 1 2 3 4
5 1 2 3 4 5
输出
1
2
10
15

解答

这一题输入虽然每组的变量数不固定,但通过读入的第一个变量可以预先知道每组的变量数,因此通过循环控制即可对数据正确分组。

数列之和
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
)

func main() {
var n int
for {
_, err := fmt.Scan(&n) // 获取该组数据长度
if err != nil { break }

sum := 0
// 通过循环控制每一组的数据读入
for i := 0; i < n; i++ {
var v int
fmt.Scan(&v)
sum += v
}

fmt.Println(sum)
}
}

fmt.Scan 小结

fmt.Scan 方法简单易用,无需提前知道用例的组数,可以一直从标准输入读取由空格以及换行分隔的数据直至结束。但需要能够(从题目规定或输入变量)提前获知每组数组的长度。因此当每组数据长度无法提前获知时,该方法使用起来就不方便了。

bufio.Scanner

首先查看 Scanner 的定义:

1
2
3
type Scanner struct {
// contains filtered or unexported fields
}

Scanner provides a convenient interface for reading data such as a file of newline-delimited lines of text. Successive calls to the Scan method will step through the 'tokens' of a file, skipping the bytes between the tokens. The specification of a token is defined by a split function of type SplitFunc; the default split function breaks the input into lines with line termination stripped. Split functions are defined in this package for scanning a file into lines, bytes, UTF-8-encoded runes, and space-delimited words. The client may instead provide a custom split function.

简言之,Scanner 提供了每次读入一行输入的接口。

然后查看 ScannerScan 方法

1
func (s *Scanner) Scan() bool

Scan advances the Scanner to the next token, which will then be available through the Bytes or Text method. It returns false when the scan stops, either by reaching the end of the input or an error. After Scan returns false, the Err method will return any error that occurred during scanning, except that if it was io.EOF, Err will return nil. Scan panics if the split function returns too many empty tokens without advancing the input. This is a common error mode for scanners.

ScannerScan 方法(默认情况下)将 Scanner 推进到下一行,读取的该行数据可以被 Text 方法使用。当扫描结束时,该方法返回 false

其中 Text 方法将读取的该行输入转换为字符串返回:

1
func (s *Scanner) Text() string

Text returns the most recent token generated by a call to Scan as a newly allocated string holding its bytes.

因此通过使用 bufio.Scanner 我们可以逐行读取输入为一个字符串变量,然后通过 strings.Split 方法将每行字符串转变为数组便成功读入了一组数据。

bufio.Scanner 在具体使用中需要注意一些细节,包括传入标准输入,字符串分隔和类型转换等,具体见下例:

例 4. 数列求和

输入描述
1
2
3
输入数据有多组, 每行表示一组输入数据。

每行不定有n个整数,空格隔开。(1 <= n <= 100)。
输出描述
1
每组数据输出求和的结果

示例

输入
1
2
3
1 2 3
4 5
0 0 0 0 0
输出
1
2
3
6
9
0

解答

该题和例 3 是同一个问题,但缺少对每组数据个数的规定,因此无法使用 fmt.Scan 方法读取,需要使用 bufio.Scanner 逐行读取数据:

数列之和
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
"os"
"bufio"
"strings"
"strconv"
)

func main() {
scanner := bufio.NewScanner(os.Stdin)

for scanner.Scan() {
nums := strings.Split(scanner.Text(), " ")

sum := 0
for _, x := range nums {
v, _ := strconv.Atoi(x)
sum += v
}
fmt.Println(sum)
}
}

总结

从上文以及例题中可以发现,bufio.Scanner 的应用场景是可以完全覆盖 fmt.Scan 的,但前者使用起来需要配合很多其他的库,比较繁琐。因此当每组用例的变量数是可知的情况下优先使用 fmt.Scan 方法即可。当 fmt.Scan 方法不适用时再考虑使用 bufio.Scanner

另外还有一点需要注意,fmt.Scan 只处理空格分隔的变量,因此当输入格式由其他字符分隔变量时,则考虑使用 bufio.Scanner 读入后再调用 strings.Split 进行相应的分隔即可。

读取 OJ 常见输入的方法有很多,我查阅相关文章后发现上述两种方法最简单方便。如果有其他更好的方法请留言讨论。