卡常小技巧
常系数尽量用 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
,不过真的太复杂了也会不行。
递归
大家都直到递归很慢吧,所以能写递推就不要写递归,除非实在想不清楚。