【Coel.算法笔记】【小技巧】快读快写

几句闲话

最近试了试 freadfwrite,感觉还是大有门道的,所以写个博客讲讲。
明明就是不知道写什么随便混一篇
洛谷传送门

题目描述

输入若干个数,然后输出它们。

输入格式

仅一行,若干个数,数与数之间有一个或两个空格,数据以 0 结尾。

输出格式

一行,依次输出输入的数,数与数之间有且仅有一个空格。

数据范围

记输入数字的个数为 M

Subtask 编号 数据范围与约定 时限
Subtask #1 (1 pts) M=102 20 ms
Subtask #2 (99 pts) M=107 700 ms

对于 100% 的数据,输入数字为整数且绝对值 2311

笔记主体

我个人觉得这玩意不应该放在“算法笔记”里面……

优化1:关闭流同步

先从最简单的读入优化说起。
众所周知, C++ 的cincout 是很慢的,其中一个原因是它们和 C 的scanfprintf 兼容,导致需要交互很多次。
所以我们可以通过关闭兼容来加速:

ios::sync_with_stdio(false);
cin.tie(0);//这一行的 0 在 C++11 及以上标准应替换为 nullptr

注意:关闭了兼容之后,不能把两个语言的输入输出(包括 putsgetchar 等输入输出操作)混用。

优化2:使用 scanf/printf

没什么好讲的。

优化3:用getchar/putchar代替常规输入输出

scanfprintf 在程序运行的时候要解析格式串,所以速度还是有一点慢。
我们可以用 getcharputchar 代替他们。
思路也比较简单:先把空格、负号等不是数字的部分丢掉,再把读入到的字符转换成数字。
判断读到的字符是否为数字,可以用 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;
}

输出和读入原理是一样的,但由于我们要把数字倒着输出(比如输出 10,要先输出 1 再输出 0, 而非先 01),所以要开一个 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:舍弃线程安全

getcharputchar 仍然有一些安全保护措施,会对输入输出时间有一点影响。
由于竞赛里线程安全不会有什么影响,所以我们可以使用去掉线程安全的函数。
Update: 特别注意: getchar_unlocked 和 putchar_unlocked 由于舍弃了进程安全,在极端情况下可能出现读写错误的问题。
在 Linux (包括洛谷等主流评测平台、比赛用的 NOI-Linux)中为 getchar_unlockedputchar_unlocked , Windows (包括CodeForces等评测平台)中为 _getchar_nolock_putchar_nolock ,需要注意一下。

优化5:采用 fread 和 fwrite

getcharputchar 的操作是逐个读入/输出字符,如果数据量特别大会有一点损耗(当然损耗并不是特别大)。
我们可以一次性读入比较多的数据放到缓存里面,进一步减少读取字符的时间,也就是使用 freadfwrite
fread(buf_r, 1, SIZE, stdin) 表示读取 SIZE 字节的字符(一字节能存一个字符,所以可以理解成读取 SIZE 个字符)并放到 buf_r 数组里面。
依照这个我们就可以写出加强版的 getchar 。开一个大小为 SIZE (通常为 220 字节,也就是 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;

用上面的 getcputc 替换 getcharputchar 可以得到更快的读入。
补充一下:在一般情况下,舍弃进程安全的 getchar_nolockfread 相差无几,如果懒得写这么多东西,也可以不采用 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 >> ncoel << n 的方式输入输出整数或字符。如果想输入输出其它类型数字也可以照样写重载运算符。
这样封装会略微降低效率(在本模板中为 4ms),不过差距很小不用考虑。

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++ 的 cincout 都很慢。但是,iostream 库内部封装了一套非常强悍的缓冲区——streambuf

当然,强大的后果是使用起来有一点麻烦。我们首先要对输入和输出各开设一个 strreambuf 的指针,分别指向 cincoutrdbuf;然后用 sgetnsputn 代替 freadfwrite 即可。

接下来利用各种玄学的关键字卡常就可以得到质的飞跃了。

这里用一道输入量很大的合并果子加强版演示一下。

#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 中国大陆许可协议进行许可。

posted @   秋泉こあい  阅读(170)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起
🔑
  1. 1 アイノマテリアル (feat. 花里みのり&桐谷遥&桃井愛莉&日野森雫&MEIKO) MORE MORE JUMP!
アイノマテリアル (feat. 花里みのり&桐谷遥&桃井愛莉&日野森雫&MEIKO) - MORE MORE JUMP!
00:00 / 00:00
An audio error has occurred.