卡常小技巧

常系数尽量用 2 的幂次

这样可以用很多位运算,当然可以优化。不过应当使得常数尽量平衡。(比如基数排序的时候一般用接近 \(\sqrt n\)\(2^k\) 时最快的,bitset 比较小的时候也有点用,不过会多占空间。)

内存分配

容器相关

STL 都大量使用了动态的内存分配,导致它们都很慢,一种改进方式是先 reserve 一下。

正确的 vector 建图
for(int i=1,x,y;i<=m;++i)
	e[x].emplace_back(y),
	e[y].emplace_back(x);

这样用 e 时 cache 命中率低,可以改为

for(int i=1,x,y;i<=m;++i)
	g[x].emplace_back(y),
	g[y].emplace_back(x);
for(int i=1;i<=n;++i) e[i]=vector<int>(g[i].begin(),g[i].end());

这样就几乎和链式前向星一样快了。

重标号

有时候遍历一幅图或者一棵树的时候,我们可能走出如下路径:\(1\rightarrow n\rightarrow 2\rightarrow n-1\rightarrow\cdots\)

此时我们访问的内存是极其不连续的,导致 cache 命中率奇低。

在树上可以使用树链剖分重新标号,再重新建图的方式降低 cache miss,不过在本来遍历次数就很小时这样的预处理可能反而增大常数。

正确的快读长度

fread 一次读入的数据量应该和题目最大读入的字符量差不多大(这个要结合输入形式判断,不要忽略空格和换行),此时快读的优化效果最为明显。

正确的运算

乘法

一般我们认为这个是 \(O(1)\) 的。(虽然实际上不是,但是乘法真的很快。)

__int128 会比自己手写的高精快很多,自信的话可以试试。

除法

不管是整除还是浮点数除法,它们都不是 \(O(1)\) 的,而是和字长 \(w\) 有关的。

如果是整除常数,可以用 Barrett Reduction 优化;如果值域不大(比如分块的时候),可以把所有结果不用除法的预处理出来。

如果是浮点数除法,反复除以一个数,可以先算出其倒数,之后用倒数乘。(虽然这个看着很像编译器会做的优化,但是在函数很复杂的时候并不一定会这样。)

取模

龟速乘慢疯了,比 long double__int128 都慢,如果是膜常数那可以用各种优化。(const 的变量编译器会自动优化。)

还可以积累数据直到下一次操作会爆值域才取模,不过写起来会比较繁琐,即使只积累一次也可以有显著的优化。(1ll*a*b%mod*c%mod\(\rightarrow\)(__int128)a*(__int128)b*(__int128)c%mod

unsigned 类型的变量取模快于 signed

位运算

有时候直接优化了复杂度,比如 bitset 可以搞出 \(O(\dfrac{n^2}{w})\) 之类的复杂度。

有多种写法的时候可以都试一试,说不定。

函数

内联

如果你函数太复杂,即使没有递归,inline 也可能失效,此时可以使用 __attribute__(__always_inline) 代替 inline,不过真的太复杂了也会不行。

递归

大家都直到递归很慢吧,所以能写递推就不要写递归,除非实在想不清楚。

posted @ 2022-11-22 21:58  嘉年华_efX  阅读(91)  评论(0编辑  收藏  举报