浅论 OI 中的卡常技巧(更新中)

文章探讨了在OI/ACM-ICPC等算法竞赛中的程序优化,特别是卡常数优化。除了底层常数优化,还包括输入/输出(I/O)的优化,如fread与cin的比较,以及输出时fputs的高效性。此外,文章还涉及变量类型的选取,switch-case与if-else的性能差异,位运算的作用,以及inline和register关键字的使用。文章指出,许多优化策略需谨慎使用,以免造成负优化。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

好像是八百年前的版本了,[新版](https://zhuanlan.zhihu.com/p/608989466)

绪言

何为“卡常”?

卡常数,又称底层常数优化,特指在OI/ ACM-ICPC等算法竞赛中针对程序基本操作进行的底层优化,一般在对程序性能要求较为严苛的题目或是在算法已经达到理论最优时间复杂度时使用,有时也用于非正解的强行优化。 ——百度百科

然鹅,本文讨论的卡常并不都是底层常数优化,还有一些比较“表面的”常数优化。

同时,本文参见“实用主义”,许多东西不能从底层说清楚(主要是我太菜了),只是说说怎么用。

本文尽量按照从易到难,从广泛到罕见的顺序。

一、I/O 优化

1.输入

没有什么可说的,非常简单,就是fread>read>scanf=cin(关同步)>cin(普通)

值得注意的是,关同步有一些不太好的特征。比如:


ios::sync_with_stdio(0);
cin.tie(0);
cout<<45<<' ';
cin>>a;
cout<<a<<' ';
printf("%lld ",a+6);
cout<<a+7<<' ';
printf("%lld ",a+8);

输入2,结果为 8 10 45 2 9。

这是因为 sync_with_stdio(0) 解绑了本身 c++ 为了防止 cout 和 printf 而制造的绑定,虽然加快了速度,但会使得 cout 晚于 printf 执行。

同时,cin.tie(0) 也解绑了 cin 和 cout ,使得必须 cin 执行结束后才能有 cout ,这在评测中无妨,但调试时候有个这东西就很难受。

因此,笔者的建议是,在有 printf 和 cout 时,或者在调试时,完全不要用这种读入优化。没必要的话,也不要用 cin.tie(0)。

对了,如果快读还嫌慢的话,又不会 fread ,可以在没有负数时删去快读的负数判定(其实没有什么意义但卡常本来不也没有什么意义吗(雾。

2.输出

输出比较友好的一点是,cout 就很快,多数时候甚至比快写快,基本上只比 puts 和 putchar 慢。

问题是怎么在不输出空格的前提下利用 puts 的高速的特点呢? C++Reference告诉了我们答案:用 fputs 。

fputs("asdfghjkl",stdout);

效率比 puts 还吓人。即使把那个 \n 补回来,还是比 puts 快。

经过一系列测试,发现 fputs 在一次输出 2 个字符时跟 putchar 差不多快,输出的字符越多优势越碾压。于是又有了一条建议:如果要一次输出多个字符串的,请合并输出。因为调用 fputs 这个操作本身很费时。fputs("abcdefghijklmnopqrstuvwxyz",stdout);的耗时在我的电脑上只有fputs("",stdout);的两倍。这一点也适用于 printf。

另外,cout<<endl; 比 cout<<'\n'; 慢很多,主要是第一个还要清缓存。

二、变量类型

1.

众所周知,int 比 long long 快,int 比 short 快,int 和 char 差不多。

其实主要是 long long 慢,所以可能的话不要 define int long long ,这确实可以防止不开 long long ,但要是 MLE 或被卡常的话就寄了。而且都思考卡常了应该不会忘了开 long long 吧(大概。

float ,double 等也类似。

2.关于 const

const 确实会快,这主要是底层方面的原因了。这玩意写普通数开 O2 也不会改成 const ,所以必须手写。

而且有一个好处:你改了的话会报错。

至于 const 和 define 那个快这个需要进一步验证()。

另外 const 别乱开,开太多不仅没意义,有时还更慢。所以模数,哈希数,模拟退火啥的开就行了。

三、if-else 与 switch-case

一般来说,当分支较多,case 值连续紧密时,switch 会非常快。

多快?

我做的测试是这样的,switch 的判断条件是 5 个时,大约是 if-else 时间的 1/3 。而当分支的数量越多时,他们的性能相差就越大。

原理上似乎是这样的:

如果分支很少(好像是小于 3),编译器会考虑编译成 if-else 。

而分支很多时,分两种情况,case 值紧密,会用类似基数排序的方式记录 case ,是 O(1)

case 值疏松,会采用二分的方法。

所以无论如何分支多时,switch-case 会比 if-else 快。

四、位运算

其实用处不大,主要是 O2 ,但还是说一说吧因为有意思(雾

(下次更新写)

五、inline 与 register

实际上没有什么用,同样因为现在都开 O2。

在变量名前 register 意为暂存进寄存器,如果频繁调用的话会更快。但是在开 O2 的情况下,需要频繁调用的地方计算机会默认给你放进计算器,自己加了太多 register 可能反而适得其反,造成了负优化。

inline,可以使调用函数更快捷。但是在 O2 的情况下,计算机编译时自动回帮你内联的,如果你无脑给包括递归函数在内所有函数添加 inline,也会造成负优化。

六、内存接近

(下次更新写)

七、并行算法与循环展开

(下次更新写)

八、指针与动态内存

(下次更新写)

九、其他

1.优化取模

(下次更新写)

2.++i

(下次更新写)

3.玄学

(下次更新写)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值