浅谈OI中常用的卡常技巧(时间效率相关)

近期比赛频频被卡常,特此纪念我挂掉的分数。

读写优化

对于 scanf 语句的优化

使用汇编语言函数 __builtin_scanf 以及 __builtin_printf

__builtin_ 开头的内建函数是直接用汇编语言编写的,自然在理论上会很快。

但是真的有比 scanf 快多少,那还有待考究。

对于 cin cout 语句的优化

可以关闭流同步。

具体如下:

ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);

会比常规的 scanf 语句要快一些。

这里有一道题目,当时用了 scanf 导致 TLE 挂了大分,但使用关闭流同步的 cin 就能过。

普通的快读快写

namespace IO{
    template<typename T>
    inline void read(T &x){
        x = 0; char c = getchar(); int f = false;
        for(; !isdigit(c); c = getchar()) f |= c == '-';
        for(; isdigit(c); c = getchar()) x = (x << 3) + (x << 1) + c - '0';
        if(f) x = -x;
    }
    template<typename T>
    inline void write(T x){
        if(x < 0) putchar('-'), x = -x;
        if(x >= 10) write(x / 10);
        putchar(x % 10 + '0');
    }
}
using namespace IO;

基于 fread/fwrite 优化的快读

一般来说 fwrite 用处没有 fread 大。

容易发现,它相较与普通的快读快写,速度有了较大的提升。

namespace IO{//只有fread版本
    inline char getchar(){
        static char buf[100000], *p1 = buf, *p2 = buf;
        return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2)? EOF : *p1++;
    }
    template<typename T>
    inline void read(T &x){
        x = 0; char c = getchar(); int f = false;
        for(; !isdigit(c); c = getchar()) f |= c == '-';
        for(; isdigit(c); c = getchar()) x = (x << 3) + (x << 1) + c - '0';
        if(f) x = -x;
    }
    template<typename T>
    inline void write(T x){
        if(x < 0) putchar('-'), x = -x;
        if(x >= 10) write(x / 10);
        putchar(x % 10 + '0');
    }
}
using namespace IO;
namespace IO{
	const int Size = 1 << 20;
	namespace IN{
	    inline char gc(){
	        static char buf[Size], *p1 = buf, *p2 = buf;
	        return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, Size, stdin), p1 == p2)? EOF : *p1++;
	    }
	    template<typename T>
	    inline void read(T &x){
	        x = 0; char c = gc(); int f = false;
	        for(; !isdigit(c); c = gc()) f |= c == '-';
	        for(; isdigit(c); c = gc()) x = x * 10 + c - '0';
	        if(f) x = -x;
	    }
	}
	namespace OUT{
	    char buf[Size << 1], r[20]; int p, p2 = -1;
	    inline void flush(){
		    fwrite(buf, 1, p2 + 1, stdout), p2 = -1;
		}
		inline void pc(char c){buf[++p2] = c;}
		inline void write(int x){
			if(p2 > Size) flush();
		    if(x < 0) pc('-'), x = -x;
		    do{
		        r[++p] = x % 10 + 48;
		    } while(x /= 10);
		    do{
		        pc(r[p]);
		    } while(--p);
		}
	}
}
using IO::IN::gc;
using IO::OUT::pc;
using IO::IN::read;
using IO::OUT::write;
using IO::OUT::flush;

对于上述写法,别忘记在所有输出结束后再 flush 一下。

还用一种基于 steambuf 优化的读写,在大部分情况下实用性未必比 fread 高,这里不作提供。

小 trick

有时候不经意间,我们会这样写:

for(int i = 1; i <= n; ++i){
	cout << a[i] << " ";
}

但实际上,我们把字符串 " " 换成单个字符能较大地提升运行效率:

for(int i = 1; i <= n; ++i){
	cout << a[i] << ' ';
}

指令集优化

只在部分OJ上可用。

常见的指令集:

#pragma GCC optimize(2)
#pragma GCC optimize(3)
#pragma GCC optimize("Ofast")
#pragma GCC optimize("inline")
#pragma GCC optimize("-fgcse")
#pragma GCC optimize("-fgcse-lm")
#pragma GCC optimize("-fipa-sra")
#pragma GCC optimize("-ftree-pre")
#pragma GCC optimize("-ftree-vrp")
#pragma GCC optimize("-fpeephole2")
#pragma GCC optimize("-ffast-math")
#pragma GCC optimize("-fsched-spec")
#pragma GCC optimize("unroll-loops")
#pragma GCC optimize("-falign-jumps")
#pragma GCC optimize("-falign-loops")
#pragma GCC optimize("-falign-labels")
#pragma GCC optimize("-fdevirtualize")
#pragma GCC optimize("-fcaller-saves")
#pragma GCC optimize("-fcrossjumping")
#pragma GCC optimize("-fthread-jumps")
#pragma GCC optimize("-funroll-loops")
#pragma GCC optimize("-fwhole-program")
#pragma GCC optimize("-freorder-blocks")
#pragma GCC optimize("-fschedule-insns")
#pragma GCC optimize("inline-functions")
#pragma GCC optimize("-ftree-tail-merge")
#pragma GCC optimize("-fschedule-insns2")
#pragma GCC optimize("-fstrict-aliasing")
#pragma GCC optimize("-fstrict-overflow")
#pragma GCC optimize("-falign-functions")
#pragma GCC optimize("-fcse-skip-blocks")
#pragma GCC optimize("-fcse-follow-jumps")
#pragma GCC optimize("-fsched-interblock")
#pragma GCC optimize("-fpartial-inlining")
#pragma GCC optimize("no-stack-protector")
#pragma GCC optimize("-freorder-functions")
#pragma GCC optimize("-findirect-inlining")
#pragma GCC optimize("-fhoist-adjacent-loads")
#pragma GCC optimize("-frerun-cse-after-loop")
#pragma GCC optimize("inline-small-functions")
#pragma GCC optimize("-finline-small-functions")
#pragma GCC optimize("-ftree-switch-conversion")
#pragma GCC optimize("-foptimize-sibling-calls")
#pragma GCC optimize("-fexpensive-optimizations")
#pragma GCC optimize("-funsafe-loop-optimizations")
#pragma GCC optimize("inline-functions-called-once")
#pragma GCC optimize("-fdelete-null-pointer-checks")

精简版指令集:

#pragma GCC optimize("Ofast,no-stack-protector")
#pragma GCC target("sse,sse2,sse3,ssse3,sse4,popcnt,abm,mmx,avx,tune=native")

其它实用优化

把函数变成内联函数。

也即在函数类型前面加 inline

就像这样:

inline void dfs(int u){
}

寄存器优化

也即在循环变量的类型前面加 register

举个例子:

for(register int i = 1; i <= n; ++i){
	for(register int j = 1; j <= n; ++j){
		for(register int k = 1; k <= n; ++k){
		}
	}
}

但事实上,往往只需要在最内层的循环变量前加 register 就能达到一样或者更优的效果。

值得一提的是,在 c++11 之后的语法,编译的时候会忽略所有 register,甚至有时加了会产生微小的负优化。

循环展开

所谓循环展开就是通过在每次迭代中执行更多的数据操作来减小循环开销的影响。

很多时候能实质性、大幅度减小常数的一个利器。

但是在 O(2) 已至 O(fast) 下优化力度逐渐减少。
常见的形如:

//展开前
for(int i = 1; i <= n; ++i){
    c += a[i];
}
//展开后
for(int i = 1; i <= n; i += 2){
    c += a[i];
    c += a[i + 1];
}
//注:这里假设 a[n + 1] 的值为 0

总结一下,若想要循环展开有实质性优化的必要条件:循环里的语句直接前后没有直接的关联。

比如这样的循环语句,展开了效用就不大:

for(int i = 1; i <= n; ++i){
    a *= c;
    c += d;
}
  • 还有一种原理和他类似的优化:
//优化前
for(int i = 1; i <= n; ++i){
    c += a[i];
}
for(int i = 1; i <= n; ++i){
    d -= b[i];
}
//优化后
for(int i = 1; i <= n; ++i){
    c += a[i];
    d -= b[i];
}

define, constexprconst

在这里,我们探讨用三者来表示一个常量的快慢。

首先,用三者的定义及形式如下:

define:宏定义,等价于直接写下常量的内容;

#define N 100000

constexpr:常量。

constexpr int N = 100000;

const:只读量。

const int N = 100000;

他们和常规变量的运行效率比较:

define > constexpr > const = 变量

故而建议使用 define 或者 constexpr

提高 Cache 命中率(内存连续、减少数组嵌套)

  • 内存连续

    比如以下代码:

    //n为偶数
    for(int i = 1; i <= n / 2; ++i){
        c[i] += v, c[n - i + 1] += v;
    }
    

    就不如这样来的快:

    for(int i = 1; i <= n; ++i){
        c[i] += v;
    }
    

    这说明,我们持续访问连续的内存,会比访问间隔较远的内存来得快。

  • 减少数组嵌套调用

    例如以下代码段,

    for(int i = 1; i <= n; ++i){
    	for(int j = 1; j <= n; j++){
            a[c[b[i]]] = a[c[b[i]]] * 10 + j;
        }
    }
    

    不如这样:

    for(int i = 1; i <= n; ++i){
    	int bi = b[i], ci = c[bi];
    	for(int j = 1; j <= n; j++){
            a[ci] = a[ci] * 10 + j;
        }
    }
    

    时间效率的提升可观。

运算优化(取模优化)

凡是手写过高精,都容易观察到:加减法要比乘除法快很多。其中,乘法又会比除法快很多。

所以我们有时候可以把一些乘除法给拆成左右移与加减法,例如:

a = b * 10 -> a = (b << 3) + (b << 1);
  • 此外,这里还介绍一个经典的取模优化。

    在一个程序中,假设 amod 的值很小(其中 mod 代表模数), 那么我们就可以使用以下代码大大地减小常数:

    while(a >= mod) a -= mod;
    

if-elseswitch-case

条件语句越多,switch-case 相比于 if-else 就越快。

条件少的时候,switch-case 就会自动转化为 if-else

所以在写代码时可以多用后者。

类型优化

众所周知,int 是一个 c++ 中相对很常用的类型。尽管它理论上要比 char, bool, short 等类型运行的慢,但它实际的运行速度是快于其余类型(甚至远远快于)。

对函数递归的优化

假若在方便的情况下,DFS 尽量写迭代,不要写递归。因为递归本身就意味着常数会相对较大,zkw 线段树(一种非递归线段树)就是靠着非递归常数小而出名的。

二进制压位

仅适用于部分题目。可以手写,也可以使用 STL 模板库里的 bitset

其它小优化

  • 把数组的大小开成二的幂次少一点点。

    部分题目优化力度可观。

  • ++i 相比于 i++,不会生成临时变量,故而会更快。

  • 优化条件的先后顺序

    逻辑与运算符,在它的前项为 0 时不会去看后项。

    类似的,对于逻辑或运算符,在它的前项为 1 时不会去看后项。

    所以我们把返回值为 false 概率大一点的条件放在逻辑与的前面,返回值为 true 的概率大一点条件放在逻辑或前面。

    具体效用还是要看具体的题目。

  • ……

posted @   徐子洋  阅读(482)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示