常数优化的一些技巧
前言
- 卡常数是OIer的基本素质之一,但一些人对其很不了解。
- 本文介绍了一些基本的卡常技巧,更适用于初学者。
- 文中若有不恰当的地方请及时指出,博主会尽快更正。
- 喜欢就点个推荐呗~
- 不喜欢请在评论区随便dis千万别点反对啊
一.STL
- (附:为了方便理解,一些数据结构博主用的是c++封装成STL时所用的函数名,如果想借鉴的话请不要开万能库,不然会疯狂CE)
- 原则上手写要比用STL快,不过有些确实难打···
- set直接用就好,都是基于红黑树实现(根本不会打),效率已经足够高(当然不惧码量的巨佬也可以手打)。
#include<cstdio>
#include<cstdlib>
using namespace std;
#define L tree[x].l
#define R tree[x].r
int const N=2e5+5;
int root,n,opt,st,t;
struct Treap{
int l,r,id,weight,size;
}tree[N];
inline int apply(int x){
int k=++t;
tree[k].id=x,tree[k].weight=rand();
tree[k].size=1;
return k;
}
inline void get(int x){
tree[x].size=tree[L].size+tree[R].size+1;
return ;
}
void split(int x,int val,int &a,int &b){
if(!x){
a=b=0;
return ;
}
if(tree[x].id<=val){
a=x;
split(R,val,R,b);
}
else{
b=x;
split(L,val,a,L);
}
get(x);
return ;
}
int merge(int x,int y){
if(!x || !y)return x+y;
if(tree[x].weight<tree[y].weight){
R=merge(R,y);
get(x);
return x;
}
tree[y].l=merge(x,tree[y].l);
get(y);
return y;
}
void insert(int x){
int a,b;
split(root,x,a,b);
root=merge(merge(a,apply(x)),b);
return ;
}
void del(int y){
int a,b,x;
split(root,y,a,b);
split(a,y-1,a,x);
root=merge(merge(a,merge(L,R)),b);
return ;
}
int rk(int x){
int a,b,ans;
split(root,x-1,a,b);
ans=tree[a].size+1;
root=merge(a,b);
return ans;
}
int find(int x,int y){
while(tree[L].size+1!=y)
if(y<=tree[L].size)x=L;
else y-=tree[L].size+1,x=R;
return tree[x].id;
}
int pre(int x){
int a,b,ans;
split(root,x-1,a,b);
ans=find(a,tree[a].size),root=merge(a,b);
return ans;
}
int nxt(int x){
int a,b,ans;
split(root,x,a,b);
ans=find(b,1),root=merge(a,b);
return ans;
}
int main(){
scanf("%d",&n);
while(n--){
scanf("%d%d",&opt,&st);
switch(opt){
case 1:
insert(st);
break;
case 2:
del(st);
break;
case 3:
printf("%d\n",rk(st));
break;
case 4:
printf("%d\n",find(root,st));
break;
case 5:
printf("%d\n",pre(st));
break;
case 6:
printf("%d\n",nxt(st));
break;
}
}
return 0;
}
- map确实很方便,不过有一种神奇的东西叫做hash表,可以以近似$\Theta(1)$的效率进行查询^_^(就是有点不稳定……)。
- unorder_map理论复杂度确实也是常数级别的,但实际上比手打的Hash表慢了不少,如果有足够的时间建议手打。
#include<cstdio>
using namespace std;
int const mod=7e5+1,N=1e5+5;
int head[mod+2],Next[N],to[N],t;
inline int up(int x){
return x<0?x+mod:x;
}
inline int ins(int x){
int z=up(x%mod);
for(register int i=head[z];i;i=Next[i])
if(to[i]==x)return i;
Next[++t]=head[z],head[z]=t;
to[t]=x;
return t;
}
int main(){
int n;
scanf("%d",&n);
for(register int i=1;i<=n;++i){
int x;
scanf("%d",&x);
printf("%d\n",ins(x));//返回离散后对应的值
}
return 0;
}
- 堆(优先队列)可以考虑手写,不过大部分情况直接用就行。
- 但手写堆也有好处,就是快啊可以删除堆中的元素。
- upd.竟然被B哥dis……还是说明一下,博主堆的删除是需要知道元素在堆中的位置的,这个记录一下就好了,应该没人不会吧……
博主稍懒没有打见谅见谅。
#include<cstdio>
#include<cstring>
using namespace std;
int const N=1e5+5;
template<class T>
inline void swap(T &x,T &y){
T z=x;
x=y,y=z;
return ;
}
template<class T>
struct priority_queue{ //大根堆
T heap[N];
int n;
inline void clear(){ //清空
n=0;
return ;
}
inline bool empty(){ //判断是否为空
return !n;
}
inline int size(){ //返回元素个数
return n;
}
inline void up(int x){ //向上调整
while(x^1)
if(heap[x]>heap[x>>1])swap(heap[x],heap[x>>1]),x>>=1;
else return ;
}
inline void down(int x){ //向下调整
int s=x<<1;
while(s<=n){
if(s<n && heap[s]<heap[s|1])s|=1;
if(heap[s]>heap[x]){swap(heap[s],heap[x]);x=s,s<<=1;}
else return ;
}
}
inline void push(T x){ //插入元素x
heap[++n]=x;
up(n);
return ;
}
inline T top(){return heap[1];} //返回堆中的最大值
inline void pop(){heap[1]=heap[n--];down(1);return ;} //删除堆顶
inline void erase(int x){ //删除下标为x的节点
heap[x]=heap[n--];
up(x),down(x);
return ;
}
inline T* begin(){ //返回堆中第一个元素的指针(实在不会搞迭代器……)
return &heap[1];
}
inline T* end(){ //返回堆的尾部边界
return &heap[n+1];
}
inline T &operator [] (int x){
return heap[x];
}
};
int main(){
//freopen("1.in","r",stdin);
//freopen("1.out","w",stdout);
int t;
priority_queue<int>q;
q.clear(); //注意所有手打的数据结构用之前需要clear
scanf("%d",&t);
for(register int i=1;i<=t;++i){
int z;
scanf("%d",&z);
q.push(z);
}
for(register int* i=q.begin();i!=q.end();++i) //遍历1
printf("%d ",*i);
puts("");
for(register int i=1;i<=q.size();++i) //遍历2
printf("%d ",q[i]);
puts("");
while(!q.empty()){ //从大到小输出
printf("%d ",q.top());
q.pop();
}
puts("");
return 0;
}
- upd.博主发现有时候要开好多好多堆,像上面那样开静态数组会炸。所以博主又用vector实现了一下。
只是实现了STL中优先队列的动态内存,删除任意点操作没有打,和上面大同小异。
至于为什么博主这么不走心……因为博主不会指针所以不会动态数组,而vector实现的手写堆并不比系统堆快……
struct Pri_Q{ int tp; vector<ll>hp; inline void _Clear(){tp=0;hp.push_back(0ll);return ;} inline bool Empty(){return !tp;} inline void down(int x){ int y=x<<1; while(y<=tp){ if(y^tp && hp[y]<hp[y|1])y|=1; if(hp[x]<hp[y])_Swap(hp[x],hp[y]),x=y,y<<=1; else return ; } } inline void up(int x){ while(x^1){ if(hp[x>>1]<hp[x])_Swap(hp[x>>1],hp[x]),x>>=1; else return ; } } inline void Pop(){return hp[1]=hp[tp--],hp.pop_back(),down(1);} inline void Push(ll x){return hp.push_back(x),up(++tp);} inline ll Top(){return hp[1];} inline int Size(){return tp;} }q[100002];
- 双端队列手打稍清奇,需要把l,r指针初始化到元素数量的位置。
- 双端队列的STL也挺好用的,其实手打不是很有必要……
- 还是附个代码吧。
#include<cstdio>
using namespace std;
int const N=1e5+5;
template<class T>
struct deque{
int l,r;
T a[N];
inline void clear(){l=r=N>>1;return ;}
inline bool empty(){return l==r;}
inline void push_back(T x){a[r++]=x;return ;}
inline void push_front(T x){a[--l]=x;return ;}
inline void pop_front(){++l;return ;}
inline void pop_back(){--r;return ;}
inline T front(){return a[l];}
inline T back(){return a[r-1];}
inline int size(){return r-l;}
inline T &operator [] (int x){return a[l+x-1];}
inline T* begin(){return &a[l];}
inline T* end(){return &a[r];}
};
int main(){
deque<int>q;
q.clear();
return 0;
}
- 至于stack和queue,必须手写!!!
- 注意stack打法,do-while循环更加方便取出栈顶元素。
#include<iostream>
using namespace std;
int stack[1000],top;//栈
int q[1000],t,u; //队列
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin>>n;
for(register int i=1;i<=n;++i){
cin>>q[++t];
stack[top++]=q[t];
}
do{
cout<<stack[--top]<<" ";
}while(top);
cout<<endl;
while(u^t)cout<<q[++u]<<" ";
return 0;
}
- vector其实不是很快,有个题我没用vecotr但比用vector的多个sort,结果比开vector的快1倍(虽然我二分手打sort随机化还加了fread)。
- 所以内存允许的话直接开2维数组(但vector确实挺方便,也不能总牺牲码量和内存优化时间吧,所以想用就用)。
- pair也挺好,不过自定义结构体更快。
- 总之,c++内置数据结构的改成手打一定变快,除非你打错了或者自带巨大常数……
- B哥也手打了各类STL而且有真正的红黑树!不过红黑树这东西也只能刷题的时候打着爽一爽,考场上不会有人考虑吧……
二.运算
- mod定义成const。
- 能乘不除,能加减别用魔法模法。
- 能位运算就别用加减乘除···
- x2^n改成<<n。
- /2^n改成>>n。
- swap(x,y)改成x^=y^=x^=y。
- 模数若为2^n可以直接&(mod-1)。
- 也可以先开unsigned int最后取模。
- 两个小于模数相加和将值域为(-mod,mod)的数值域改成[0,mod)可以用下面代码中的取模优化。
inline int down(int x){
return x<mod?x:x-mod;
}
inline int up(int x){
return x<0?x+mod:x;
}
- 数据范围不大可以开long long,中间不取模最后再取。
- 判断奇偶&1。
- i!=-1改为~i。
- !=直接改成^。
三.读入
- 别用cin,用cin就在主函数加:
ios::sync_with_stdio(false);
cin.tie(0);
- 打着还麻烦,所以就用scanf或快读。
- 不超过1e5的数据scanf即可。
- 再大了最好用快读。
- 记得位运算优化···
- 还嫌慢上fread(不过这个玩意有时候还会有意想不到的奇效,比如让你的程序慢十倍,慎用)。
- 用fread后无法键入,请初学者不要担心,freopen就行。
- 快读还有一些妙用,比如定义常量不能scanf但可用快读赋值,这在一些读取模数的题目中很有用。
- 下面代码里快读读的是非负整数,读整数特判一下有无‘-’即可,就不给出实现了。
#include<cstdio>
#include<iostream>
using namespace std;
int const L=1<<20|1;
char buf[L],*S,*T;
#define getchar() ((S==T&&(T=(S=buf)+fread(buf,1,L,stdin),S==T))?EOF:*S++)
inline int read(){
int ss=0;char bb=getchar();
while(bb<48||bb>57)bb=getchar();
while(bb>=48&&bb<=57)ss=(ss<<1)+(ss<<3)+(bb^48),bb=getchar();
return ss;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin>>n;
n=read();
puts("233");
return 0;
}
- 良心博主出于善意放了个读整数的:
inline int read(){
int ss(0),pp(1);char bb(getchar());
for(;bb<48||bb>57;bb=getchar())if(bb=='-')pp=-1;
while(bb>=48&&bb<=57)ss=(ss<<1)+(ss<<3)+(bb^48),bb=getchar();
return ss*pp;
}
四.输出
- 同理,用printf别用cout。
- 快输是个危险东西,搞不好还会变慢。
- 慎用非递归版快输,输不了零。
- 不过非递归快一点,实在不行特判~
#include<cstdio>
#include<iostream>
using namespace std;
char ch[20],top;
inline void write(int x){ //非递归
while(x){
ch[top++]=x%10;
x=x/10;
}
do{
putchar(ch[--top]+48);
}while(top);
puts("");
return ;
}
void out(int x){ //递归
if(x>=10)out(x/10);
putchar(x%10+48);
return ;
}
int main(){
int n=233;
write(n);
out(n);
return 0;
}
- upd. skyh大神表示非递归版改成do-while循环就能输出0了,%%%
#include<bits/stdc++.h>
using namespace std;
char ch[100],top;
inline void write(int x){
do{
ch[top++]=x%10;
x/=10;
}while(x);
do{
putchar(ch[--top]+48);
}while(top);
return ;
}
signed main(){
write(5);write(2);write(0);
return 0;
}
- 有fread同理也有fwrite。
- upd.非递归快输打while也是可以输0的,当时啥也不会意淫的……
#include<cstdio>
#include<algorithm>
int const L=1<<20|1;
char buf[L],z[22],zt;
int t=-1;
int a[22];
inline void write(int x){
if(x<0)buf[++t]='-',x=-x;
while(z[++zt]=x%10+48,x/=10);
while(buf[++t]=z[zt--],zt);
buf[++t]=32;
}
int main(){
int n;
scanf("%d",&n);
for(register int i=1;i<=n;++i)scanf("%d",&a[i]);
std::sort(a+1,a+n+1);
for(register int i=1;i<=n;++i)write(a[i]);
fwrite(buf,1,t+1,stdout);
return 0;
}
五.dp
- 其实已经不算卡常了,可以说是剪枝···
- 1.排除冗杂
- 能不dp的就别dp。
- 说白了就是for循环里多设几个限制条件。
- 比如可怜与超市一题,yzh巨佬重设了个tmp数组实现$\Theta(N^2)$转移,还证明了一波复杂度,%%%
- 但其实$\Theta(N^3)$可过···
#include<cstdio>
#include<cstring>
using namespace std;
int const N=5005,lar=0x3f3f3f3f,L=1<<20|1;
char buf[L],*S,*T;
#define getchar() ((S==T&&(T=(S=buf)+fread(buf,1,L,stdin),S==T))?EOF:*S++)
inline int read(){
int ss=0;char bb=getchar();
while(bb<48 || bb>57)bb=getchar();
while(bb>=48&&bb<=57)ss=(ss<<1)+(ss<<3)+(bb^48),bb=getchar();
return ss;
}
inline void swap(int &x,int &y){
int z=x;
x=y,y=z;
return ;
}
inline int max(int x,int y){
return x>y?x:y;
}
inline int min(int x,int y){
return x<y?x:y;
}
int n,m,pp;
int c[N],d[N],f[N][N][2];
int head[N],Next[N],to[N],t;
int siz[N],lim[N];
inline void add(int x,int y){
to[++t]=y;
Next[t]=head[x],head[x]=t;
return ;
}
void dfs(int x){
int y,now=2;
siz[x]=1;
f[x][1][0]=c[x];
f[x][1][1]=c[x]-d[x];
for(int i=head[x];i;i=Next[i]){
dfs(y=to[i]);
siz[x]+=siz[y=to[i]];
for(register int j=siz[x];j>=0;--j){
int lit=min(now,j);
for(register int k=(j>lim[y])?j-lim[y]:1;k<lit;++k){
int o=j-k;
f[x][j][0]=min(f[x][j][0],f[y][o][0]+f[x][k][0]);
f[x][j][1]=min(f[x][j][1],min(f[y][o][0],f[y][o][1])+f[x][k][1]);
}
f[x][j][0]=min(f[x][j][0],f[y][j][0]);
}
for(register int j=siz[x];j>=0;--j)
if(f[x][j][0]<=m || f[x][j][1]<=m){now=j+1;break;}
}
for(register int i=1;i<=siz[x];++i)
if(f[x][i][1]>=m && f[x][i][0]>=m){lim[x]=i;return ;}
lim[x]=siz[x];
return ;
}
int main(){
memset(f,0x3f,sizeof(f));
n=read(),m=read(),c[1]=read(),d[1]=read();
for(register int i=2;i<=n;++i){
c[i]=read(),d[i]=read();
add(read(),i);
}
dfs(1);
for(register int i=lim[1];i>=0;--i)
if(f[1][i][0]<=m || f[1][i][1]<=m){
printf("%d",i);
return 0;
}
}
- 2.等效替代
- 说起来很模糊···
- 以HAOI2015树上染色为例。
- 染黑点和染白点其实一样。
- 所以你完全可以加一句k=min(k,n-k);
六.初始化
- 单个变量可以直接初始化,好像比赋值初始化略快。
- 小范围初始化数组直接memset。
- 对于单一数组memset就是比for循环要快,不要怀疑!!!
- 有时后你觉得for循环快,那不是因为数据水与极限数据相差太远就是因为你连清了五个以上数组。
- 清大量范围相同的数组才采用for。
- 对于一些题目你觉得memset用sizeof(数组名)清数组很浪费也可以改成sizeof(int)*长度,不过一般没有必要。
- 当然一些情况你完全可以边for边初始化。
- 最典型的就是矩阵乘法:
struct ljj{
int a[101][101];
friend ljj operator * (ljj a1,ljj a2){
ljj c;
for(register int i=1;i<=100;++i)
for(register int j=1;j<=100;++j){
c.a[i][j]=0;
for(register int k=1;k<=100;++k)
c.a[i][j]+=a1.a[i][k]*a2.a[k][j];
}
}
};
七.排序
- 动态维护的用堆或者平衡树,堆最好手打,平衡树只要不自带大常数就可以手打(一大哥手打Splay T飞改成用set就A了)。
- 静态可以sort,归并希尔并不推荐(主要是我不会···)。
- 当然一些算法如CDQ可以边分治边归并的就别sort了。
- sort结构体时注意最好重载运算符,定义比较函数比较慢。
- 值域小的桶排序。
- 至于基数排序,看情况吧,一些题目还是要用的。
- 关于sort还有一个神奇操作,叫做随机化快排。
- 大量用sort且待排序的数比较多得话可以考虑一下,因为直接用普通快排容易栈溢出。
- 顺带一提,随机化快排对有序数列的排序比普通快排快上个几百倍。
- 说白了就是随机化快排不容易被特殊数据卡。
- 再说白了就是能比普通快排多骗点分…
#include<bits/stdc++.h>
using namespace std;
int a[1000001];
int random_div(int *q,int l,int r){
int z=l+rand()%(r-l+1),zl=l-1,tp;
swap(q[z],q[r]),tp=q[r];
for(register int i=l;i<r;++i)
if(q[i]<=tp)
++zl,swap(q[zl],q[i]);
swap(q[++zl],q[r]);
return zl;
}
void Random_Sort(int *q,int l,int r){
if(l<r){
int z=random_div(q,l,r);
Random_Sort(q,l,z-1);
Random_Sort(q,z+1,r);
}
return ;
}
int ran(int x){
return (long long)rand()*rand()%x;
}
int main(){
srand(time(NULL));
int n;
scanf("%d",&n);
for(register int i=1;i<=n;++i)scanf("%d",&a[i]);
Random_Sort(a,1,n);
for(register int i=1;i<=n;++i)
printf("%d ",a[i]);
puts("");
return 0;
}
- 然而以上都是针对手打快排,事实上c++内置的sort比手打的快排要飞快很多……
- 当然随机化给我们带来的启示是,只在sort前加个random_shuffle有时候会让sort更加飞快(也有可能让你T飞)。
八.编译优化
- 慎用!!!!!!!!
#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")
九.其他
- 其实这里才是精华。
- inline和register尽量用。
- 注意inline不要在递归函数里用。
- register不要在递归过程中用,回溯时可以用。
- 自加自减运算符前置,就是i++改成++i。
- 内存允许,值域不大且不需memset时bool改int。
- 能int绝对不要用long long(一哥们数颜色(洛谷上兔子那个,不是带修莫队)调了一下午一直TLE,我把他long long改成int就A了……)。
- 如果方便可以循环展开,偶尔有奇效。
- if-else能改就改成三目运算符?:
- 边权为1的图跑最短路用bfs别用dijkstra。
- (一个名叫Journeys的题的暴力bfs比dijstra高10分···)
- 稠密图建图vector实现邻接表要比不用vector快很多,因为vector遍历时内存连续。
- 倍增lca进行倍增时从深度的log值开始。
- 多维数组顺序按for循环来。
- eg. CodeForces 372CWatching Fireworks is fun
- dp[N][M]与dp[M][N]一个TLE60,一个AC。
- 其实就是内存访问的连续性,一直跳跃着访问内存自然慢。
- 数组维数越少越好。
- 离散化可以利用pair记录下标,映射时更加方便,不用再lower_bound了。
- 还有一个玄学操作叫做卡时,就是你打了个dfs或者随机化,里面用clock()判断运行时间,快TLE的时候直接输出当前答案。
- 注意clock()返回的时间不是特别准,别把跳出时间定的太极端。
- 还有注意Linux下1e6是一秒,想象一下跳出时间设成1000的酸爽。。
结束语
- 到这里卡常的内容已经结束了,但博主还想说说自己对卡常的看法。
- 不得不说卡常有它的局限性。
- 就像蒟蒻与神犇的区别,卡常数是名副其实的底层优化,它不能将指数级算法优化成多项式级别,甚至改变不了时间复杂度的任何一个字母。
- 但是,我们却不能忽视它,因为它能让一些看似无法优化的程序绝地逢生。
- 而卡常的作用本是锦上添花,但更多人将其视作骗分的手段,因为卡常确实能让暴力更快,多过一些测试点。
- 正因如此,卡常成了暴力的代名词,为一些神犇所不齿。这是卡常的悲剧性。
- 不过,至少在我看来,越来越多的人开始注意常数,开始去卡常,这让我莫名欣慰,因为博主还是很喜爱卡常的。
- 然而卡常虽好,但它终究是卡,有常数才会卡,我们需要的是,在构造出程序的过程中减少常数。
- 卡常的最高境界,是告别卡常,因为打出的程序已经不再需要卡常。
- 这需要将减少常数当作一种习惯,在编写程序的过程中无时无刻不在关注着常数,写出自己最完美的程序。
- 培养这种习惯,这也是卡常真正的意义所在吧。
- 以上。
rp++