论OI中各种玄学卡常

当你在写程序的时候一般出现过这种无比悲剧的情况:

你讨厌卡常?下面有二则小故事:

作为一个经常出题的人,其实很多时候出题时的画风是这样的:“我有一个绝妙的\(O(nlog^2n)\)的算法,我来出道题吧”“咦怎么只能跑 \(5w\) 啊,好咸鱼啊,我要让它能跑 \(10w\),嗯现在 \(10w\) 只要 \(0.3s\) 了,要不努把力跑个 \(20w\) 吧”然后就没有然后了..

开O2之后FFT会比不开快几倍?

  • 不开\(O2\):\(NTT\)比\(FFT\)快
  • 开\(O2\):\(FFT\)比\(NTT\)快。。。。。

我们作为\(OIer\),在\(OI\)中卡常数可以说是必备技巧。在此总结一下我所知卡常数的神奇手法:


什么是卡常数?

程序被卡常数,一般指程序虽然渐进复杂度可以接受,但是由于实现/算法本身的时间常数因子较大,使得无法在\(OI/ACM\)等算法竞赛规定的时限内运行结束。

卡常数被称为计算机算法竞赛之中最神奇的一类数字,主要特点集中于令人捉摸不透,有时候会让水平很高的选手迷之超时或者超空间。
普遍认为卡常数是埃及人\(Qa'a\)及后人发现的常数。也可认为是卡普雷卡尔(\(Kaprekar\))常数的别称。主要用于求解括号序列问题。

\(Time\) \(is\) \(the\) \(most\) \(precious\) \(asset\) \(of\) \(all\) \(wealth.\) —— \(Deoflasdo\)

时间是一切财富中最宝贵的财富。 —— 德奥弗拉斯多

下面的内容不完全是卡常数,但可以优化程序。


读入优化

读入优化是卡常数最重要的一条!一般用于读入整数

inline int read()
{
    int x=0,f=1;char c=getchar();
    while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
    while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();}
    return x*f;
}

输出优化不常用,但是当你认为你的程序慢的话,还是用上为好

inline void write(int x)
{
     if(x<0) putchar('-'),x=-x;
     if(x>9) write(x/10);
     putchar(x%10+'0');
}

函数优化

  • 在声明函数之前写上\(inline\),可以加快一下函数调用,但只能用于一些操作简单、调用频繁的函数。涉及递归,大号的循环等很复杂的函数,编译器会自动忽略\(inline\)。
  • 尽量减少值传递,多用引用来传递参数。至于其中的原因,相信大家也很清楚,如果参数是int等语言自定义的类型可能能性能的影响还不是很大,但是如果参数是一个类的对象,那么其效率问题就不言而喻了。
  • 如果你在比赛中如果实在不能优化了还没把握通过的话,可以自己手写一下库函数(尤其是\(STL\)里的),因为没空氧气优化库函数常数会凭空增加很多。

定义优化

  • register

在定义变量前写上\(register\),用于把变量放到\(CPU\)寄存器中,适用于一些使用频繁的变量(比如循环变量),但寄存器空间有限,如果放得变量太多,多余变量就会被放到一般内存中。如果太太多,速度可能变慢。

那么快到什么境界?

register int a=0;
for(register int i=1;i<=999999999;i++)
a++;

int b=0;
for(int i=1;i<=999999999;i++)
b++;

优化:\(0.2826\) \(sec\)

不优化:\(1.944\) \(sec\)

。。。。。

  • 局部与静态变量

很多人认为局部变量在使用到时才会在内存中分配储存单元,而静态变量在程序的一开始便存在于内存中,所以使用静态变量的效率应该比局部变量高,其实这是一个误区,使用局部变量的效率比使用静态变量要高。

这是因为局部变量是存在于堆栈中的,对其空间的分配仅仅是修改一次\(esp\)寄存器的内容即可(即使定义一组局部变量也是修改一次)。而局部变量存在于堆栈中最大的好处是,函数能重复使用内存,当一个函数调用完毕时,退出程序堆栈,内存空间被回收,当新的函数被调用时,局部变量又可以重新使用相同的地址。当一块数据被反复读写,其数据会留在\(CPU\)的一级缓存(\(Cache\))中,访问速度非常快。而静态变量却不存在于堆栈中。

可以说静态变量是低效的。

  • 初始化

推荐直接初始化

与直接初始化对应的是复制初始化,什么是直接初始化?什么又是复制初始化?举个简单的例子,

ClassTest ct1;
ClassTest ct2(ct1);  //直接初始化
ClassTest ct3 = ct1;  //复制初始化

那么直接初始化与复制初始化又有什么不同呢?直接初始化是直接以一个对象来构造另一个对象,如用\(ct1\)来构造\(ct2\),复制初始化是先构造一个对象,再把另一个对象值复制给这个对象,如先构造一个对象\(ct3\),再把\(ct1\)中的成员变量的值复制给\(ct3\),从这里,可以看出直接初始化的效率更高一点,而且使用直接初始化还是一个好处,就是对于不能进行复制操作的对象,如流对象,是不能使用赋值初始化的,只能进行直接初始化。

另外:在初始化\(Floyd\)或者其他类似的东西

for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)
        gra[i][j]=inf
for(int i=1;i<=b;i++)
    gra[i][i]=0
 

是比

for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)
    {
        if(i==j) gra[i][j]=0;
        else gra[i][j]=inf
    }

快的(测试大约\(1\)是\(2\)的\(80\)%的时间)

原因后者每次都要判断

  • 其他

不要开\(bool\),所有\(bool\)改成\(char\),\(int\)是最快的(原因不明)。

尽量不用\(double\),能用\(char\)就别用\(string\)。

对于一个值的重复运算,存入临时变量中。

如果你知道要处理的值是非负数的,使用无符号整数



循环优化

  • 循环展开

循环展开也许只是表面,在缓存和寄存器允许的情况下一条语句内大量的展开运算会刺激 \(CPU\) 并发(前提是你的 \(CPU\) 不是某 \(CPU\))...

减少了不直接有助于程序结果的操作的数量,例如循环索引计算和分支条件。
提供了一些方法,可以进一步变化代码,减少整个计算中关键路径上的操作数量。
用法(下面是一个将一个\(int\) 类型数组初始化为\(0\)的代码段):

void Init_Array(int *dest, int n)
{
    int i;
    for(i = 0; i < n; i++)
        dest[i] = 0;
}

而如果用循环展开的话,代码如下:

void Init_Array(int *dest, int n)
{
    int i;
    int limit = n - 3;
    for(i = 0; i < limit; i+= 4)//每次迭代处理4个元素
    {
        dest[i] = 0;
        dest[i + 1] = 0;
        dest[i + 2] = 0;
        dest[i + 3] = 0;
    }
    for(; i < n; i++)//将剩余未处理的元素再依次初始化
        dest[i] = 0;
}
  • 循环引发的讨论

请看下面的两段代码,
代码1:

for(int i = 0; i < n; ++i)
{
  fun1();
  fun2();
}

代码2:

for(int i = 0; i < n; ++i)
{
  fun1();
}
for(int i = 0; i < n; ++i)
{
  fun2();
}

注:这里的\(fun1()\)和\(fun2()\)是没有关联的,即两段代码所产生的结果是一样的。

以代码的层面上来看,似乎是代码\(1\)的效率更高,因为毕竟代码\(1\)少了\(n\)次的自加运算和判断,毕竟自加运算和判断也是需要时间的。但是现实真的是这样吗?

这就要看\(fun1\)和\(fun2\)这两个函数的规模(或复杂性)了,如果这多个函数的代码语句很少,则代码\(1\)的运行效率高一些,但是若\(fun1\)和\(fun2\)的语句有很多,规模较大,则代码\(2\)的运行效率会比代码\(1\)显著高得多。可能你不明白这是为什么,要说是为什么这要由计算机的硬件说起。

由于\(CPU\)只能从内存在读取数据,而\(CPU\)的运算速度远远大于内存,所以为了提高程序的运行速度有效地利用\(CPU\)的能力,在内存与\(CPU\)之间有一个叫\(Cache\)的存储器,它的速度接近\(CPU\)。而\(Cache\)中的数据是从内存中加载而来的,这个过程需要访问内存,速度较慢。

这里先说说\(Cache\)的设计原理,就是时间局部性和空间局部性。时间局部性是指如果一个存储单元被访问,则可能该单元会很快被再次访问,这是因为程序存在着循环。空间局部性是指如果一个储存单元被访问,则该单元邻近的单元也可能很快被访问,这是因为程序中大部分指令是顺序存储、顺序执行的,数据也一般也是以向量、数组、树、表等形式簇聚在一起的。

看到这里你可能已经明白其中的原因了。没错,就是这样!如果\(fun1\)和\(fun2\)的代码量很大,例如都大于\(Cache\)的容量,则在代码\(1\)中,就不能充分利用\(Cache\)了(由时间局部性和空间局部性可知),因为每循环一次,都要把\(Cache\)中的内容踢出,重新从内存中加载另一个函数的代码指令和数据,而代码2则更很好地利用了\(Cache\),利用两个循环语句,每个循环所用到的数据几乎都已加载到\(Cache\)中,每次循环都可从\(Cache\)中读写数据,访问内存较少,速度较快,理论上来说只需要完全踢出\(fun1\)的数据\(1\)次即可。


取模优化

//设模数为 mod
inline int inc(int x,int v,int mod){x+=v;return x>=mod?x-mod:x;}//代替取模+
inline int dec(int x,int v,int mod){x-=v;return x<0?x+mod:x;}//代替取模-

前置自增

后置 \(++\) 与\(--\)需要保存临时变量以返回之前的值,在 \(STL\) 中非常慢。事实上,\(int\) 的后置 \(++\) 与\(--\)在实测中也比前置 \(++\) 与\(--\)慢 \(0.5\) 倍左右.


选择结构优化

\(if()\) \(else\)语句比\(()?():()\)语句要慢,逗号运算符比分号运算符要快。

另外,在一个逻辑条件语句中常数项永远在左侧。


除法运算优化

无论是整数还是浮点数运算,除法都是一件运算速度很慢的指令,在计算机中实现除法是比较复杂的。所以要减少除法运算的次数,下面介绍一些简单方法来提高效率:

  • 通过数学的方法,把除法变为乘法运算,如if\((a > b/c)\),如果\(a\)、\(b\)、\(c\)都是正数,则可写成\(if(a*c > b)\)
  • 让编译器有优化的余地,如里你要做的运算是\(in\)t型的\(n/8\)的话,写成\((unsigned)n/8\)有利于编译器的优化。而要让编译器有优化的余地,则除数必须为常数,而这也可以用\(const\)修饰一个变量来达到目的。

位运算优化

内容比较多,但是对程序的优化很大,建议大家学一下。

  • 乘以\(2\)运算
int mulTwo(int n){//计算n*2
    return n << 1;
}
  • 除以\(2\)运算
int divTwo(int n){//负奇数的运算不可用
    return n >> 1;//除以2
}
  • 乘以\(2\)的\(m\)次方
int mulTwoPower(int n,int m){//计算n*(2^m)
    return n << m;
}
  • 除以\(2\)的\(m\)次方
int divTwoPower(int n,int m){//计算n/(2^m)
    return n >> m;
}
  • 判断一个数的奇偶性
boolean isOddNumber(int n){
    return (n & 1) == 1;
}
  • 取绝对值(某些机器上,效率比\(n>0\) \(?\) \(n:-n\) 高)
int abs(int n){
return (n ^ (n >> 31)) - (n >> 31);
/* n>>31 取得n的符号,若n为正数,n>>31等于0,若n为负数,n>>31等于-1
若n为正数 n^0=0,数不变,若n为负数有n^-1 需要计算n和-1的补码,然后进行异或运算,
结果n变号并且为n的绝对值减1,再减去-1就是绝对值 */
}
  • 取两个数的最大值(某些机器上,效率比\(a>b\) \(?\) \(a:b\)高)
int max(int a,int b){
    return b & ((a-b) >> 31) | a & (~(a-b) >> 31);
    /*如果a>=b,(a-b)>>31为0,否则为-1*/
}
  • 取两个数的最小值(某些机器上,效率比\(a>b\) \(?\) \(b:a\)高)
int min(int a,int b){
    return a & ((a-b) >> 31) | b & (~(a-b) >> 31);
    /*如果a>=b,(a-b)>>31为0,否则为-1*/
}
  • 判断符号是否相同
boolean isSameSign(int x, int y){ //有0的情况例外
    return (x ^ y) >= 0; // true 表示 x和y有相同的符号, false表示x,y有相反的符号。
}
  • 计算\(2\)的\(n\)次方
int getFactorialofTwo(int n){//n > 0
    return 2 << (n-1);//2的n次方
}
  • 判断一个数是不是\(2\)的幂
boolean isFactorialofTwo(int n){
    return n > 0 ? (n & (n - 1)) == 0 : false;
    /*如果是2的幂,n一定是100... n-1就是1111....
       所以做与运算结果为0*/
}
  • 对\(2\)的\(n\)次方取余
int quyu(int m,int n){//n为2的次方
    return m & (n - 1);
    /*如果是2的幂,n一定是100... n-1就是1111....
     所以做与运算结果保留m在n范围的非0的位*/
}
  • 求两个整数的平均值
int getAverage(int x, int y){
        return (x + y) >> 1;
}

定义常量优化

  • 在进行较大数字时,可以#define一下例如某道题中需要多次\(mod\):

\(mod=10^9+7\)

如果您将\(mod\)作为一个\(long\) \(long\) 或者是\(int\)变量来打,那么恭喜您\(TLE\)(\(3518ms\),时限\(3s\))

而如果把\(1000000007\)换成了#const int \(mod\) $1000000007 $

那么您的程序就是\(AC\)了(\(2398ms\),时限\(3s\))

那么#define在定义数组大小的时候是首选

\(const\)在运算(比如\(mod\))是首选


数组优化

    • \(C++\)里面\(STL\)自带的\(vector\)存取效率不高,在可能的情况下用指针数组代替会大幅提高性能。
  • 关于\(memset\)和\(memcpy\)以及\(memmove\)

这几个函数效率都非常高,比循环的速度快很多

其实用法也很简单,比如原来是这样的

`for(int i=l;i<=r;++i) a[i]=0; for(int i=l;i<=r;++i) a[i]=b[i]; for(int i=l;l<=r;++i) a[i]=b[i],b[i]=0;
我们可以优化成

memset(a+l,0,r-l+1<<2);
memcpy(a+l,b+l,r-l+1<<2);
memmove(a+l,b+l,r-l+1<<2);

注意,左移的位数和\(a\),\(b\)的类型有关,这里默认为\(int\),\(sizeof(int)=4\),所以就是左移\(2\)(乘\(4\))
如果不知道\(a\),\(b\)类型所占字节数,可以改成如下

memset(a+l,0,(r-l+1)*sizeof(a[0]));
memcpy(a+l,b+l,(r-l+1)*sizeof(a[0]));
memmove(a+l,b+l,(r-l+1)*sizeof(a[0]));

注意:

  • \(register\)变量不能开太多,它本身也优化不了多少,再出点什么事情可就真\(GG\)了,暂时用不到的变量不要过早的初始化,它会存在你的缓存里,如果之后继续调用该变量,速度会较快但若在之后调用许多其他变量,则会将该变量清出你的缓存,之后再有对该变量的操作时,则会花费比从缓存中调用较长的时间调用该变量
  • 在遍历高维数组时,并在定义数组时将元素多的维度放在靠前的位置,将循环次数多的维度放在外层,可减少一定的运行时间。高维数组在内存中都是线性安放的,在C语言中,按照的是行优先顺序,就像上面提到的。当我们使用行优先顺序遍历数组时,恰好就是顺次访问内存数据,会非常有利于\(CPU\)高速缓存的工作
  • 循环变量开为形如\(register\) \(int\) \(i\)的形式,看上去是一个优化,但事实上,编译器并不傻,它会在汇编中给你搞成\(register\)。同样的,循环变量在自增/自减时,写为形如\(++i/--i\)的形式,它看上去也是对\(i++\)的一种优化,但在汇编中,它也相应的优化成的类似\(++i\)的操作.

例题:

  • CF86D

莫队,不卡常数非常难过。

  • BZOJ3815

正如题,一些程序进行常数优化后可以过

  • BZOJ3583

正解需要卡常数。



终于讲完了,如果你还想更更深入的学,推荐一本书:

《论程序底层优化的一些方法与技巧》

如果你了解并使用这些优化,那么你可以做到:

送给大家一句话:

过早的优化是效率低下的根源,程序的优化是永无止境的。


本文参考文献:

https://baike.baidu.com/item/%E5%8D%A1%E5%B8%B8%E6%95%B0/16211104?fr=aladdin

https://blog.csdn.net/a1351937368/article/details/78162078

https://www.cnblogs.com/ibilllee/p/7674633.html

https://www.cnblogs.com/xcysblog/p/8493750.html

https://www.zhihu.com/question/53107298

https://blog.csdn.net/hzj1054689699/article/details/70338662

https://blog.csdn.net/leader_one/article/details/78430083

https://blog.csdn.net/zmazon/article/details/8262185

https://www.jb51.net/article/54792.htm

原文地址:https://www.cnblogs.com/lyfoi/p/OI-kachang.html

时间: 2024-10-11 07:51:47

论OI中各种玄学卡常的相关文章

[luogu1972][bzoj1878][SDOI2009]HH的项链【莫队+玄学卡常】

题目大意 静态区间查询不同数的个数. 分析 好了,成功被这道题目拉低了AC率... 打了莫队T飞掉了,真的是飞掉了QwQ. 蒟蒻想不出主席树的做法,就换成了莫队... 很多人都不知道莫队是什么... 一句话概括莫队:离线询问分块排序,玄学降低复杂度 那么这道题目就是简单的莫队模板套一下就好了,每一次看看更新的点是不是会对答案造成贡献就可以过掉了. 但是复杂度很明显是\(Q(\sqrt{n}m)\),成功T掉,加上玄学卡常,破罐子破摔了100+终于过掉了. #include <bits/stdc+

时间卡常技巧

以下内容出自: 时间卡常技巧 先放一句话镇场: 我觉得,卡常数的出题人都是xx,这违背了算法竞赛考察思路的初衷 ——LYD 推荐:论OI中各种玄学卡常 我们一般说的复杂度都是O(n)O(n^2)O(nlogn)是一个级别. 但是我们其实每一个步可能计算很多次,然后会乘上一个2*n,3*n,甚至10*n 我们都叫O(n) 这个乘上的数就是常数.有的时候,你(chu)自(ti)己(ren)的(sang)程(xin)序(bing)可(kuang)能(ka)常(chang)数(shu)太(qwq)大(Q

GCC&amp;&amp;GDB在OI中的介绍

序言 这本来是用Word写的,但是后来我换了系统所以只能用markdown迁移然后写了...... $\qquad$本文主要投食给那些在Windows下活了很久然后考试时发现需要用命令行来操作时困惑万分以及觉得GDB很好吃的人 $\qquad$以及---- $\qquad$经常眼瞎看不见i++和j++的区别 $\qquad$经常访问a[-1]然而使编译器无可奈何(除非在使用O2的情况下的明显访问越界)的人 ... $\qquad$正式地说,本文介绍GCC&&GDB命令在OI中的应用. 提要

浅谈OI中的底层优化!

众所周知,OI中其实就是算法竞赛,所以时间复杂度非常重要,一个是否优秀的算法或许就决定了人生,而在大多数情况下,我们想出的算法或许并不那么尽如人意,所以这时候就需要一中神奇的的东西,就是底层优化: 其实底层优化比较简单,比如我们经常使用的 register还有快读,这些都可以进行优化.还有fread,但是fread在一些情况(尤其是在重要的的比赛时)但是还是给出下面的优化 const int L=1<<20|1; char buffer[L],*S,*T; #define getchar()

Codeforces 988D Points and Powers of Two 【性质】【卡常】

这道题关键在于想到两个性质,想到就好做了.这还是我做过的第一道卡常题 1.满足题目中条件的子集,其中元素个数不能大于3 2.如果最大子集为3的话,那一定是x-2^i,  k, x+2^i的形式,我们枚举x就好了,然后i的次数是log10^9:如果最大子集是2,那就是x,x+2^i的形式,同样枚举x:如果最大子集是1,输出a[1]就行 整体复杂度是O(n*logn*log10^9) 1 #include<iostream> 2 #include<set> 3 using namesp

uboot移植之uboot中的SD卡驱动解析

1:地址对硬件操作的影响 (1)操作系统(指的是linux)下MMU肯定是开启的,也就是说linux驱动中肯定都使用的是虚拟地址.而纯裸机程序中根本不会开MMU,全部使用的是物理地址.这是裸机下和驱动中操控硬件的一个重要区别. (2)uboot早期也是纯物理地址工作的,但是现在的uboot开启了MMU做了虚拟地址映射,这个东西驱动也必须考虑.查uboot中的虚拟地址映射表,发现210开发板里面,除了0x30000000-0x3FFFFFFF映射到了0xC0000000-0xCFFFFFFF之外,

OI中整数的读入优化

将整数一个字符一个字符地读入,再转成整数,比直接作为整数读入快. 在读入大规模的整数数据时比较有效. 代码如下: inline void read_int(int &num) { char c; while (c = getchar(), c < '0' || c > '9'); num = c - '0'; while (c = getchar(), c >= '0' && c <= '9') num = num * 10 + c - '0'; } OI中

十九、android中判断sim卡状态和读取联系人资料的方法

在写程序中,有时候可能需要获取sim卡中的一些联系人资料.在获取sim卡联系人前,我们一般会先判断sim卡状态,找到sim卡后再获取它的资料,如下代码我们可以读取sim卡中的联系人的一些信息. PhoneTest.java package com.android.test; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.datab

Android中向SD卡读写数据,读SD卡和手机内存

package com.example.sdoperation; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import android.support.v7.app.Actio