【Coel.算法笔记】【小技巧】快读快写
几句闲话
最近试了试 fread
和fwrite
,感觉还是大有门道的,所以写个博客讲讲。
明明就是不知道写什么随便混一篇
洛谷传送门
题目描述
输入若干个数,然后输出它们。
输入格式
仅一行,若干个数,数与数之间有一个或两个空格,数据以 结尾。
输出格式
一行,依次输出输入的数,数与数之间有且仅有一个空格。
数据范围
记输入数字的个数为 。
Subtask 编号 | 数据范围与约定 | 时限 |
---|---|---|
Subtask #1 () | ||
Subtask #2 () |
对于 的数据,输入数字为整数且绝对值 。
笔记主体
我个人觉得这玩意不应该放在“算法笔记”里面……
优化1:关闭流同步
先从最简单的读入优化说起。
众所周知, C++ 的cin
和 cout
是很慢的,其中一个原因是它们和 C 的scanf
和printf
兼容,导致需要交互很多次。
所以我们可以通过关闭兼容来加速:
ios::sync_with_stdio(false);
cin.tie(0);//这一行的 0 在 C++11 及以上标准应替换为 nullptr
注意:关闭了兼容之后,不能把两个语言的输入输出(包括 puts
、getchar
等输入输出操作)混用。
优化2:使用 scanf/printf
没什么好讲的。
优化3:用getchar/putchar代替常规输入输出
scanf
和 printf
在程序运行的时候要解析格式串,所以速度还是有一点慢。
我们可以用 getchar
和 putchar
代替他们。
思路也比较简单:先把空格、负号等不是数字的部分丢掉,再把读入到的字符转换成数字。
判断读到的字符是否为数字,可以用 cctype
头文件里的 isdigit
函数。
int read() {
int x = 0, f = 1;//f 用来标记数字是否为负数
char ch = getchar();
while (!isdigit(ch)) {//丢掉非数字
if (ch == '-')
f = -1;//读到负号时做个标记
ch = getchar();
}
while (isdigit(ch)) {//读取数字
x = x * 10 + ch - '0';//ch - '0' 就得到了转换后的数字
ch = getchar();
}
return x * f;
}
输出和读入原理是一样的,但由于我们要把数字倒着输出(比如输出 ,要先输出 再输出 , 而非先 后 ),所以要开一个 buf
数组存倒过来的数字。
void write(int x) {
if (x < 0) {//输出负号
x = -x;
putchar('-');
}
static int buf[35];//作为静态数组,减少内存反复调用
/*当然也可以开在函数外面,看个人习惯*/
int top = 0;
do {
buf[top++] = x % 10;
x /= 10;
} while (x);//分解数字到 buf 里面
/*注意这里要写成 do-while 的循环,防止 x=0 时没有输出*/
while (top)
putchar(buf[--top] + '0');
putchar(' ');//输出空格,如果题目要求换行就改成 \n
}
用法: a = read();
读入 a, write(a)
输出 a。
优化4:舍弃线程安全
getchar
和 putchar
仍然有一些安全保护措施,会对输入输出时间有一点影响。
由于竞赛里线程安全不会有什么影响,所以我们可以使用去掉线程安全的函数。
Update: 特别注意: getchar_unlocked 和 putchar_unlocked 由于舍弃了进程安全,在极端情况下可能出现读写错误的问题。
在 Linux (包括洛谷等主流评测平台、比赛用的 NOI-Linux)中为 getchar_unlocked
和 putchar_unlocked
, Windows (包括CodeForces等评测平台)中为 _getchar_nolock
和 _putchar_nolock
,需要注意一下。
优化5:采用 fread 和 fwrite
getchar
和 putchar
的操作是逐个读入/输出字符,如果数据量特别大会有一点损耗(当然损耗并不是特别大)。
我们可以一次性读入比较多的数据放到缓存里面,进一步减少读取字符的时间,也就是使用 fread
和fwrite
。
fread(buf_r, 1, SIZE, stdin)
表示读取 SIZE 字节的字符(一字节能存一个字符,所以可以理解成读取 SIZE 个字符)并放到 buf_r 数组里面。
依照这个我们就可以写出加强版的 getchar
。开一个大小为 SIZE (通常为 字节,也就是 1MB)的数组,两个指针 *left
和*right
。当两个指针重合的时候再次进行 fread
,并把 left
移动到开头,right
移动到结尾。
由于 fread
只有在读取完之后才会重新进行,所以在调试的时候,我们要手动给出“输入结束”的标志,方法是按下Ctrl+Z
再按回车。
const int SIZE = 1 << 20;
char getc() {
static char buf_r[SIZE], *left = buf_r, *right = buf_r;
if (left == right) {//读取完了,重新 fread
left = buf_r;
right = buf_r + fread(buf_r, 1, SIZE, stdin);
}
return *left++;//每次返回一个字符
}
fwrite(buf_w, 1, SIZE, stdout)
表示从 buf_w 里输出 SIZE 字节的字符。
输出的思路也是一样的。一个数组 buf_w ,一个指针 *cur
,当数组占满的时候 fwrite
全部输出出来,并把 cur
移到开头。
同样的理由,在程序结束之前,我们要手动把没输出的内容全部输出。
注意:在实际应用的时候我们不能直接输出 SIZE 个字符,因为数组没占满的时候会额外输出一堆的空格。所以我们要控制输出的字符数,把 SIZE 改成 cur - buf_w (这个操作会得到 cur 对应的数组下标,在这里就可以代表输出的字符数)即可。
char buf_w[SIZE], *cur = buf_w;
void flush () {//清空数组并全部输出
fwrite(buf_w, 1, cur - buf_w, stdout);
cur = buf_w;
}
void putc(const char &ch) {
if (cur - buf_w == SIZE) flush();//存满了,全部输出
*cur++ = ch;//存字符
}
/*歪比巴卜...直到最后要退出程序*/
return flush(), 0;
用上面的 getc
和 putc
替换 getchar
和 putchar
可以得到更快的读入。
补充一下:在一般情况下,舍弃进程安全的 getchar_nolock
和 fread
相差无几,如果懒得写这么多东西,也可以不采用 fread
。
封装
为了让你的代码更好看一些,可以把这些东西封装到一个名字空间里面。
namespace __Coel_FastIO {
const int SIZE = 1 << 20;
static char buf_r[SIZE], *left = buf_r, *right = buf_r;
static char buf_w[SIZE], *cur = buf_w;
void flush () {
fwrite(buf_w, 1, cur - buf_w, stdout);
cur = buf_w;
}
char getc() {
if (left == right) {
left = buf_r;
right = buf_r + fread(buf_r, 1, SIZE, stdin);
}
return *left++;
}
void putc(const char &ch) {
if (cur - buf_w == SIZE) flush();
*cur++ = ch;
}
int read() {
int x = 0, f = 1;
char ch = getc();
while (!isdigit(ch)) {
if (ch == '-') f = -1;
ch = getc();
}
while (isdigit(ch)) x = x * 10 + ch - '0', ch = getc();
return x * f;
}
void write(int x) {
static int buf[35];
int top = 0;
if (x < 0) putc('-'), x = -x;
do {
buf[top++] = x % 10;
x /= 10;
} while (x);
while (top) putc(buf[--top] + '0');
putc(' ');
}
}
using namespace __Coel_FastIO;//调用名字空间
进一步封装:像 cin/cout 一样调用
利用重载 << 和 >> 运算符,可以把快读快写封装成更方便写的样子。
下面的结构体可以按照 coel >> n
和 coel << n
的方式输入输出整数或字符。如果想输入输出其它类型数字也可以照样写重载运算符。
这样封装会略微降低效率(在本模板中为 ),不过差距很小不用考虑。
struct __Coel_FastIO {
#ifndef LOCAL
#define _getchar_nolock getchar_unlocked
#define _putchar_nolock putchar_unlocked
#endif
__Coel_FastIO& operator >>(int &x) {
x = 0; //这里要记得初始化,防止局部变量出问题
bool f = false;
char ch = _getchar_nolock();
while (!isdigit(ch)) {
if (ch == '-') f = true;
ch = _getchar_nolock();
}
while (isdigit(ch)) {
x = x * 10 + ch - '0';
ch = _getchar_nolock();
}
if (f) x = -x;
return *this;
}
__Coel_FastIO& operator <<(int x) {
if (x < 0) {
x = -x;
_putchar_nolock('-');
}
static int buf[35];
int top = 0;
do {
buf[top++] = x % 10;
x /= 10;
} while (x);
while (top) _putchar_nolock(buf[--top] + '0');
return *this;
}
__Coel_FastIO& operator <<(char x) { return _putchar_nolock(x), *this; }
} coel;
补充——究极优化!
在这位大佬的首页看到了一个更加厉害的快读,这里一并贴出。
众所周知,C++ 的 cin
和 cout
都很慢。但是,iostream
库内部封装了一套非常强悍的缓冲区——streambuf
。
当然,强大的后果是使用起来有一点麻烦。我们首先要对输入和输出各开设一个 strreambuf
的指针,分别指向 cin
和 cout
的 rdbuf
;然后用 sgetn
和 sputn
代替 fread
和 fwrite
即可。
接下来利用各种玄学的关键字卡常就可以得到质的飞跃了。
这里用一道输入量很大的合并果子加强版演示一下。
#include <iostream>
namespace __Coel_FastIO {
const int SIZE = 1 << 20;
static char buf_r[SIZE], *p1 = buf_r, *p2 = buf_r;
static char buf_w[SIZE], *p3 = buf_w, *p4 = buf_w + SIZE;
static std::streambuf *inbuf = std::cin.rdbuf(), *outbuf = std::cout.rdbuf(); //流初始化
inline static void gc(char &ch) { //这里三目运算符的效率略高一些
ch = p1 == p2 && (p2 = (p1 = buf_r) + inbuf->sgetn(buf_r, SIZE), p1 == p2) ? EOF : *p1++;
/*逻辑:先判断有没有读完一个缓存,如果缓存空了就继续往下读,
否则用 sgetn 读入一个缓存,如果还是空的就返回 EOF*/
}
inline static void pc(char c) {
p3 == p4 ? outbuf->sputn(p3 = buf_w, SIZE), *p3++ = c : *p3++ = c;
/*逻辑差不多,如果写完整个缓存就全部输出,否则写入缓存*/
}
inline static int Flush() { return outbuf->sputn(buf_w, p3 - buf_w), 0; } //清空数组
struct IOdesu {
template<typename T> IOdesu& operator >>(T &x) {
x = 0;
int f = 1;
char ch;
gc(ch);
while (ch < '0' || ch > '9') {
if (ch == '-') f = -1;
gc(ch);
}
while (ch >= '0' && ch <= '9') x = x * 10 + ch - '0', gc(ch);
return x *= f, *this;
}
template<typename T> IOdesu& operator <<(T x) {
if (x < 0) x = -x, pc('-');
static int buf[35];
int top = 0;
do {
buf[top++] = x % 10;
x /= 10;
} while (x);
while (top) pc(buf[--top] + '0');
return *this;
}
IOdesu& operator <<(char ch) { return pc(ch), *this; }
} qwq;
}
using namespace __Coel_FastIO;
const int maxn = 1e5 + 10, maxl = 1e7 + 10;
int n, b[maxn], x;
long long ans, Q1[maxl], Q2[maxl];
int l1, l2, r1, r2;
int main(void) {
qwq >> n;
for (int i = 1; i <= n; i++)
qwq >> x, b[x]++;
for (int i = 1; i <= 1e5; i++)
while (b[i]--) Q1[r1++] = i;
for (int i = 1; i <= n - 1; i++) {
long long x, y;
if ((l2 == r2) || (l1 != r1 && Q1[l1] < Q2[l2])) x = Q1[l1++];
else x = Q2[l2++];
if ((l2 == r2) || (l1 != r1 && Q1[l1] < Q2[l2])) y = Q1[l1++];
else y = Q2[l2++];
ans += x + y, Q2[r2++] = x + y;
}
qwq << ans;
return Flush();
}
结语
快读快写在一般情况下几乎不会有什么用,除非读写量真的大得惊人(至少百万级)。
当然,这也是一种卡常数的常用手段,如果输入输出成了你的优化瓶颈,不妨用它们进一步加速。
本文作者:Coel's Blog
本文链接:https://www.cnblogs.com/Coel-Flannette/p/16210442.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步