输入输出是笔试编程题目的一个考点,题目中经常会出现比较复杂的输入情况,例如最短路径问题,其输入描述如下:
输入n,m,点的编号是1~n,然后是m行,每行4个数 a,b,d,p,表示a和b之间有一条边,且其长度为d,花费为p。最后一行是两个数 s,t;起点s,终点t。n和m为0时输入结束。
(1
输入示例:
3 2
1 2 5 6
2 3 4 5
1 3
0 0
对于此类包含多行数据的复杂输入,在调试代码时如何提升输入效率是个值得重视的问题。手动输入效率太低,复制粘贴是一种可行的办法,但是每次运行都需要操作一次,而且遇到不定输入的情况,还需要手动按ctrl
+ c
终止,很不方便,这种情况有一种很好的解决方法:文件输入。
由于C++代码中要求#include
,即使用标准输入输出流,输入输出都需要通过windows的控制台窗口进行,因此当输入复杂时会带来不便。
接下来,本文将以C++语言为例,讲解如何有效应用文件输入解决复杂输入问题。
使用文件输入需要包含
头文件,其基本代码如下:
#include
#include
using namespace std;
int main()
{
ifstream cin("./in.txt");
cin.close();
return 0;
}
我们构造一个与标准输入同名的文件输入流ifstream
变量cin
,它默认以读的方式打开当前目录下的in.txt
文件(./
代表当前目录,即当前正在编辑的代码文件所在文件夹),之后题目的n
、m
等所有输入都可以从文件输入流cin
中获取,如:
#include
#include
using namespace std;
int main()
{
ifstream cin("./in.txt");
int n, m;
cin >> n >> m;
...
cin.close();
return 0;
}
将题目输入数据复制到in.txt
中,直接运行代码即可得到输出结果。因为之前将ifstream
变量也命名为cin
,因此在提交代码时,将ifstream cin("./in.txt")
与cin.close()
两行代码注释或删除掉,此时cin
又恢复成默认的标准输入流了,直接提交代码即可。
接下来以Visual Studio 2017(Enterprise)为例,介绍使用文件输入的流程。
定义文件输入代码模板
事先定义一个使用文件输入的代码模板,在包含常用头文件的基础上,包含前述文件输入代码,例如:
#include "pch.h"
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
ifstream cin("./in.txt");
cin.close();
return 0;
}
代码模板可以由代码片段功能实现,若遇到输入较复杂的题目,使用快捷键展开为文件输入模板。
创建 in.txt 文件
右键当前正在编辑的cpp文件,打开文件所在的文件夹,在弹出的资源管理器窗口中新建in.txt
文件,将题目输入全部复制到该文件中,保存退出。
编辑代码并运行
正常编辑代码,运行即可,此时不再需要手动输入数据,可以直接得到运行结果。
提交代码
代码调试完毕,将以下三行注释或删除掉:
#include “pch.h”
ifstream cin("./in.txt");
cin.close();
其余代码即可正常提交,关于pch.h
头文件的说明见代码片段这篇文章。
最短路径问题虽然输入较为复杂,但输入内容是固定的,因此在编写输入代码时难度不大,只是文件输入可以“一劳永逸”,效率更高而已,但是输入不定的情况下,其优势非常明显。
不定输入在笔试中是一个令人头疼的问题,它通常分为两种情况:输入行数不定,每行输入内容不定,例如流水线问题:
题目有m行输入,代表有m条流水线,m的数值未定
其中每行有若干个整数,代表当前流水线上若干个物品的编号,每两个整数之间用空格隔开
请将所有流水线上的物品按照编号升序排列并输出。
上述输入就包含了所述的两种情况,下面我们分别对其进行处理。
对于每行输入不定的情况,基本只有一种解决方法,使用getline
将整行作为字符串读入,然后按照题目输入规则进行拆分。在拆分时,可以自己实现split
函数,这里要介绍的是使用字符串流sstream
,其代码实现如下:
#include // 包含头文件
...
string line;
getline(cin, line); // 使用 getline 读入当前整行
istringstream is(line); // 用 line 构造输入字符串流 is
int tmp;
vector<int> curLine;
while (is >> tmp) // 用 is 作为输入流,输入到 tmp
{
curLine.push_back(tmp);
}
以上代码实现的功能为:将当前行若干个整数依次添加到curLine
中,这里需要注意的有两点:
is
是以字符串line
构造的输入字符串流,那么之后它的作用类似于标准输入流cin
,可以对int
、string
等变量进行输入赋值,其风格与cin
相同,单次输入遇到空格与回车停止,不读入空格与回车,下次读入时跳过空格与回车,直到有效输入开始,并一直读到下一个空格或回车时结束,该过程持续到读到字符串末尾后结束。由于当前字符串不包含回车,因此该代码实现的功能为:按空格拆分字符串。is >> tmp
若输入成功,会有非零返回值(cin
同样),因此可以作为while
循环的判断条件,当到达字符串末尾后输入失败,返回值为0,循环结束,以此来处理输入不定的情况。由此可以总结,对每行输入不定情况的处理方法即为:将当前行整个读入到字符串中,然后按照指定规则拆分字符串,其中我们借助到了输入字符串流istringstream
,它可以实现类似于cin
风格的赋值操作,并借助输入成功有非零返回值这一特性,使用while
循环来控制结束。
另外,有些题目每行内容并非是按照空格分隔,如将题目改为:
每两个整数之间用
','
隔开,如1,2,3
那么又该如何处理呢?一种可行的办法是读入字符串后,将其中所有的 ','
改为空格,但这种方法效率不高,我们介绍一种使用getline
按照指定字符拆分字符串的方法:
string line;
getline(cin, line); //同上
istringstream is(line); // 同上
vector<int> curLine;
string tmp; // 注意,这里必须将 tmp 定义为 string 变量
while (getline(is, tmp, ',')) // 按照 ',' 拆分字符串
{
curLine.push_back(stoi(tmp));
}
以上代码实现的功能为,使用getline
将is
按照','
拆分依次赋值给tmp
,这里要求tmp
必须为string
类型。有些情况下,使用stoi
函数会出现一些问题,这里建议统一使用atoi
函数:
curLine.push_back(atoi(tmp.c_str()));
处理输入行数不定的情况,其核心在于利用流输入的返回值,使用while
循环进行控制。
每行内容固定
虽然行数不定,但每行内容固定的情况处理时较为简单,例如:
有若干行输入,每行有3个整数,分别为 x, y, z
那么这种情况可以直接用如下方式读入:
int x, y, z;
while(cin >> x)
{
cin >> y >> z;
...
}
每行内容不定
如流水线问题,输入行数不定,每行内容也不定,这种情况下需要使用getline
每次读入一行,并用while
循环控制,其代码如下:
string line;
while(getline(cin, line))
{
...
}
读入之后再对每行单独处理即可。
完整代码
之前提到,对于不定行输入的情况下,手动输入需要按ctrl
+ c
手动停止,而使用文件输入的话,当读到文件末尾时while
循环会自动停止,因此输入代码建议结合文件输入使用。
接下来,我们给出流水线问题的完整输入代码以供参考:
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
ifstream cin("./in.txt");
string line;
vector<vector<int> > input;
while (getline(cin, line))
{
istringstream is(line);
vector<int> curLine;
int tmp;
while (is >> tmp)
{
curLine.push_back(tmp);
}
input.push_back(curLine);
}
... // 解题代码
cin.close();
return 0;
}
若题目说明为用','
分隔,则将处理代码对应修改即可。
需要注意的是,若将vector
定义在循环外部,则循环内需要在最后添加代码curLine.clear();
,切记!
假如有这样一道航班信息的题目,输入如下:
第一行为两个整数 n, m, 代表有 n 个城市,m 批次开放的航班信息
接下来有 m 行
每一行包含该批次开放的若干个航班信息,每个航班信息用 "a,b,w"的形式表示,表示从城市 a 飞到城市 b 需要价格为 w,每两个航班信息之间用空格隔开。
输入案例如下
9 2
1,2,5 2,4,6
1,3,6 3,7,9 7,8,9
该题的输入方法非常复杂,我们需要对输入处理得到所有的航班信息,思路为沿用之前的读入框架,对每行的处理方法为首先按空格拆分为若干个航班信息,再对每个航班按照','
拆分得到每个航班的信息,参考代码如下:
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
ifstream cin("./in.txt");
int n, m;
cin >> n >> m;
string line;
vector<vector<int> > flight; // 每一行为一个独立的航班信息
vector<int> curFlight; // 单个独立的航班信息,大小为3,形式对应为 a b w
for(int i = 0; i < m; ++i) // 读入 m 行
{
getline(cin, line); // 读入每一行
istringstream is(line); // 用 line 创建输入字符串流
string tmp; // 按空格拆分后的单元为 “a,b,w"形式,故应用string
while (is >> tmp) // tmp 为单个航班信息字符串
{
istringstream istmp(tmp); // 使用 tmp 创建新的输入字符串流
string tmpStr; // 使用 getline拆分,此处必须为 string
while (getline(istmp, tmpStr, ',')) // 按 ',' 拆分
{
curFlight.push_back(atoi(tmpStr.c_str())); // 依次放入 curFlight 数组中
}
flight.push_back(curFlight); // 添加到 flight 中
curFlight.clear(); // 因curFlight定义在循环外,这里要清空
}
}
//...
cin.close();
return 0;
}
上述代码处理读入的思路非常正确,但当我们编写完代码运行的时候,会发现结果却不对。读者不妨测试一下,新建一个cpp文件,复制上述代码,添加代码输出flight
信息,新建in.txt文件并写入输入案例,运行或进行调试,会发现flight
中只有两条航班信息:
1 2 5
2 4 6
对比输入案例发现,这两条是第一批次开放的航班信息,而第二批次的航班信息没有读入,这是为什么呢?
读入规则
之前介绍过,使用输入流直接对 string,int 等变量赋值时,是通过空格和回车来分隔的,具体如下
而getline
是一次读入一整行,其规则如下:
假设有下面两行输入
abc 123
def
当我们输入时,按键过程如下
abc
' '
123'\n'
def'\n'
其中' '
表示实际的空格键,'\n'
表示实际的回车键。
若我们使用输入流,其读入过程如下:
string str;
int num;
cin >> str; // 从读入字符'a'开始,读到'c'停止, str = abc
cin >> num; // 读入空格,跳过,从字符'1'开始,读到'3'停止,以 int 的形式读入,num = 123
cin >> str; // 读入回车,跳过,从字符'd'开始,读到'f'结束,str = def
即使 abc 和 123 之间有多个空格与回车,读入时都会依次跳过。
若结合使用getline
,其读入过程如下:
string str;
cin >> str; // 从读入字符'a'开始,读到'c'停止, str = abc
getline(cin, str); // 从当前位置' '开始,一直读到'\n'结束,str = ' 'abc 123\n
getline(cin, str); // 从当前位置'd'开始,一直读到'\n'结束,str = def\n
似乎读入很正常,但是如果我们想要分别以string
和int
的类型读入abc
和123
,而将def
以整行读入,我们执行以下代码:
string str1, str2;
int num;
cin >> str1 >> num;
getline(cin, str2);
此时str1
和num
都能正确读入,但str2
的内容却不是预期的def
。这是因为根据读入规则,读入num
时在'3'
处停止,而接下来调用getline
函数时,会从当前位置开始,读到'\n'
结束,由于123
后面的换行符'\n'
还留在输入流中,故此时str2 = "\n"
。
到这里,我们就发现了问题所在,在航班信息的输入中,我们先输入了n
、m
两个整数,却并未读入m
后面的换行符。接下来我们用for
循环调用m
次getline
函数,在第一次的读入会发生line = "\n"
。由于getline
函数会将当前行的换行符一起读入,所以在这之后的每行数据都能正确读入。
综上,所写代码只能正确读入测试案例中m - 1
行信息,最后一行信息会丢失。
解决方法
当使用getline
函数,而之前有其它的变量使用流输入时需要注意该问题,首先将流中的换行符跳过,再开始使用getline
函数。
一般情况下,可以使用cin.get()
函数空读一个字符,但是有些题目为了增加输入难度,会在数字后面添加空格,再添加换行,这种情况使用cin.get()
也会出现问题,因此,我们建议直接使用getline(cin, line)
空读一次,根据其规则,line
会一直将当前行读完,包括最后的换行符,之后就可以正常读取了。
修改代码如下:
int n, m;
cin >> n >> m;
string line;
getline(cin, line); // 将当前行剩余内容读完,包括换行符
vector<vector<int> > flight;
for(int i = 0; i < m; ++i)
{
getline(cin, line); // 正常读入
//...
}
再运行代码即可发现,flight
包含了全部的5条航班信息。