位运算常数优化的骚操作

2018-01-21 21:32:47


零、前言:从门电路说起

在计算机内部,一切数据都是以二进制进行储存,其支持的最基本的元操作应该是布尔逻辑运算,例如逻辑与(and)、逻辑或(or)、逻辑非(not)、逻辑亦或(xor)等等。

这些运算的实现,几乎可以说是直接与电路的基本元件逻辑门相对应起来,是计算机天生所最擅长的操作,其执行速度无疑也是最快的。

比如,左上面那个门,叫“与门”,对应“逻辑与”运算。它左边的两只脚是输入,右边头上的一根毛是输出。计算机中用1表示true,0表示false,所以,当“与门”两只脚的输入都是1的时候,它头顶的毛才会输出1,否则,头顶上的毛就输出0。

类似的,右上边尖脑袋的家伙对应的是“逻辑或”运算。当且仅当两个输入都是0的时候,头顶上输出0,否则输出1。

另外左下边单脚的三角形家伙,叫“非门”:它头顶的毛总是输出跟脚底下的输入相反的结果。脚底输入0,头顶就输出1;脚底输入1,头顶就输出0。

这三个家伙是最基本的“门”。其他的什么与非门,或非门,同或门,异或门,都可以通过这哥仨组合得到。

当然了,上面这些都是画图的时候用的符号,就跟电路图里的电阻一样,画出来是这样:

可是在现实生活中,它长这样:

那么,这些计算机中的“门”,在现实生活中长什么样呢?

组成门电路的基本单元,叫“晶体管”,一个逻辑门由两个或多个晶体管组合而成。单个晶体管长这样:

这是一个早期的晶体管在电子显微镜下的照片。可以看到图中的比例尺是180nm。估算一下,这个晶体管的大小在350nm左右。

CCF的评测机CPU AMD Athlon II x2 240的制程为45nm。作为对比,人类头发单根直径是70μm左右,是这个晶体管大小的1500倍。如果用这样的晶体管来搭建“门”,在人类发断面那么小的面积上,可以搭出数百万个“门”。

整数的四则运算等操作,就是用众多逻辑门所搭建起来的。加减法电路的逻辑门数量比较少,同样可以在较短的时间内进行运算,但如果像除法一般逻辑复杂,电路也相应庞大,速度就很难以接受了。再到更复杂的浮点运算,速度也就更慢。简而言之,在众多的CPU 指令中,凡是在bit级别进行操作的,基本上都能够在一个clock cycle中完成,例如位移(bit shift)、位扫描(bit scan)、逻辑运算等等,再加上加减法、取补码等等简单的整数运算,组成了一个非常高速的指令集合。

巧妙地组合这些运算,往往能取代一些原本缓慢的操作,获得速度上的极大提升。

一、状压DP中的位运算

1、读取第k位的值:

int bit_read(int a,int k)
{
    return a>>k&1;
}

2、将第k位清0:

void bit_clear(unsigned int &a)
{
    a&=~(1<<k);
}

3、将第k位置1:

void bit_set(unsigned int &a)
{
    a|=1<<k;
}

4、将第k位取反:

void bit_not(unsinged int &a,int k)
{
    a^=1<<k;
}

5、将第k1~k2位取反:

void bits_not(unsigned int &a,int k1,int k2)
{
    a^=((1<<(k2-k1+1))-1)<<k2;
}

二、打包位统计

1、统计true的个数的奇偶性

int bits_parity(unsigned int a)
{
    x^=x>>1;
    x^=x>>2;
    x^=x>>4;
    x^=x>>8;
    x^=x>>16;
}

运算结果的第i 位表示在原始数据中从第i 位到最高位true 数目的奇偶性,有了这个结果,我们就可以很方便地得到任意一段的奇偶性。

如果想要得到k1~k2 位中true 个数的奇偶性,直接计算

(x>>k1^x>>(k2+1))&1

即可。

2、统计true 的数目

int popcount(unsigned int x)
{
    x=(x&0x55555555)+(x>>1&0x55555555);
    x=(x&0x33333333)+(x>>2&0x33333333);
    x=(x&0x0F0F0F0F)+(x>>4&0x0F0F0F0F);
    x=(x&0x00FF00FF)+(x>>8&0x00FF00FF);
    x=(x&0x0000FFFF)+(x>>16&0x0000FFFF);
    return x;
}

原理:

用分治法实现,将原始问题化为两个小问题——统计16个位元中值为1的位元个数,分别将其解决,然后再将结果合并。统计16位中值为1的位元个数时,又进一步化为求两个8个位元中值为1的位元个数...递归使用这个策略,直到分解 到统计1个位元中值为1的位元个数,把相邻两个位元的值相加,即得2个位元中中值为1的位元个数。

进一步优化:

int popcount(unsigned int i)
{
    i=i-((i>>1)&0x55555555);
       i=(i&0x33333333)+((i>>2)&0x33333333);
    i=(i+(i>>4))&0x0f0f0f0f;
    i=i+(i>>8);
    i=i+(i>>16);
    return i&0x3f;
}

【知乎】面试经典问题——统计 32 位有符号整型二进制表示中 1 的数目

当然也可以以空间换时间:

const unsigned char popcount_tab[]=
{
    0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4,
    1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,
    1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,
    2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,
    1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,
    2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,
    2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,
    3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,
    1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,
    2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,
    2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,
    3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,
    2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,
    3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,
    3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,
    4,5,5,6,5,6,6,7,5,6,6,7,6,7,7,8,
};

int popcount (register int x)
{
    return popcount_tab[(x>>0)&0xff]+popcount_tab[(x>>8)&0xff]+popcount_tab[(x>>16)&0xff]+popcount_tab[(x>>24)&0xff];
}

3、反转位的顺序

unsigned int rev(unsigned int x)
{
    x=(x&0x55555555)<<1|(x>>1&0x55555555);
    x=(x&0x33333333)<<2|(x>>2&0x33333333);
    x=(x&0x0F0F0F0F)<<4|(x>>4&0x0F0F0F0F);
    x=(x&0x00FF00FF)<<8|(x>>8&0x00FF00FF);
    x=(x&0x0000FFFF)<<16|(x>>16&0x0000FFFF);
    return x;
}

三、常用操作的位运算优化

1、计算绝对值

int abs(int x)
{
    int y=x>>31;
    return (x+y)^y;
}

2、求较大值

int max(int x,int y)
{
    int m=(x-y)>>31;
    return y&m|x&~m;
}

3、交换变量

void swap(int &a,int &b)
{
    a^=b;
    b^=a;
    a^=b;
}

4、求两个整数的平均值,不会溢出

int ave(int x,int y)
{
    return (x&y)+((x^y)>>1);
}

四、用位运算装逼

1、a+b Problem

#include<cstdio>

int add(int a, int b) {
      while (b)
    {
        int sw = a;
        a ^= b;
        b = (b & sw) << 1;
    }
    return a;
}

int main()
{
    int a, b, ans;
    scanf("%d%d", &a, &b);
    printf("%d", add(a, b));
    return 0;
}