硬件加速的二进制算法

求最高有效位

  • 定义末位为第零位

  • __builtin_clz():原理bsr(bit scan reserve,逆向位扫描),可视作O(1),能求出前导零个数

    参数为零时返回结果未定义

    最高有效位为31-__builtin_clz()

  • __lg():直接给出,实际上编译后代码与上述方法一致

    且该方法是log2()的优替,快速返回一个整数的以22为底对数的下取整

与运算

基本性质

  • 与运算只保留两个操作数共同拥有的11

掩码

  • 判奇偶性:A&1=1?奇:偶A\&1=1?奇:偶

  • 模2n2^n:A%2n=A&((1<<n)−1)A\%2^n=A\&((1<<n)-1)

  • 判断二进制码子集:若BB是AA的子集,则A&B=BA\&B=B

计11的个数

  • 调用库函数:C++:__builtin_popcount()、Java:Integer.bitCount()

  • 暴力算法:

    c++
    while (x) {
        ans += x & 1;
        x >>= 1;
    }
  • Brian Kernighan算法:跳过最右11和末位中间的零,直接对该一计数

    k=x&(x−1)k=x\&(x-1)的性质是:kk和xx相差最右边的11

    迭代直到x=0x=0,循环次数即一的个数

    c++
    while (x) {
        x &= x - 1;
        ++ans;
    }
  • 分治算法(Java bitCount()所用方法):

    • 问题难点在于不知道哪里有11,也就无从统计

      暴力算法是遍历所有位(优化后的BK算法也是O(n))使11加起来,换言之,就是将nn位数分成nn组

      那么,分治算法的思想是遍历所有两位,将其分为n2\frac n2组,使1/21/2加起来

      c++
      while (x) {
          // 这里需要先以每两位为一组, 求得1的个数, 覆盖回x
          ans += x & 3; // 3 == 0b0000....0011
          x >>= 2;
      }

      那么首先就要知道每个组是0/1/20/1/2中的哪个数:

    • 将整数以每两位为一组划分,要求出每组的11的个数,就是要求每组中所有位的加和

      其对应关系为原码(1的个数)20000010110011110\begin{matrix}原码&(1的个数)_2\\00&00\\01&01\\10&01\\11&10\end{matrix},那么如何将这个计量关系快速算出,且每组间互不影响?

      首先,取掩码0b0101..0101\rm 0b0101..0101,即0x55..5\rm 0x55..5,相与后能将每组中位于奇位置的11保留,偶位置全为零

      让原整数右移一位,则原本处于偶位置的11移到奇位置,原本处于奇位置的11移位相与后为零(故不影响其它组)

      将两次相与的结果相加,每组中11的个数就得到了,因为加和最大为22,故每组是独立的

    • 当然,分治算法不能满足于如此,这样还需遍历n2\frac n2次

      既然一次合并就能降低一半复杂度了,尝试继续合并,最后ans=x&(全1)=xans=x\&(全1)=x

      重复上述过程,但以每四位为一组划分:

      类似上述掩码,取0b0011..0011\rm 0b0011..0011,即0x33..3\rm 0x33..3,结果为x&M+(x>>2)&Mx\&M+(x>>2)\&M

      形象表达:将xx看作四进制数,每两位四进制数(即每四位为一组)为一组,求每组所有位的加和;即(原码)4(所有位的加和)4000001/100102/20/110221/12032210\begin{matrix}(原码)_4&(所有位的加和)_4\\00&00\\01/10&01\\02/20/11&02\\21/12&03\\22&10\end{matrix},因为加和最大为44,故每组间独立计算,互不影响

      重复每2n2n一组的划分,直到只剩下一组,对这一组完成最后的统计后,则剩余整数就是所求

      以3232位整数为例,最终一次运算应以3232位为一组,M=0x0000ffff、x=x&M+(x>>16)&MM={\rm 0x0000ffff}、x=x\&M+(x>>16)\&M,此时xx就是所求

    • c++
      // 对int/unsigned int  32位整数:
      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); // 此时x为所求
      
  • 分治算法在位运算上的优化(我也没想到这还能优化):

    • 对第一步:每组中原码和统计后码值的关系可优化为原码减去偶数位值:节省一次与运算

      原码计算过程1的个数0000−0000101−0011010−1011111−110\begin{matrix}原码&计算过程&1的个数\\00&00-0&00\\01&01-0&01\\10&10-1&01\\11&11-1&10\end{matrix},即x -= (x >> 1) & 0x55555555

    • 对第三步:因为经过前两步后,以每88位为一组时,组中所有位加和最多为88,最多占四位,因此可以先让高位和低位相加后再相与(相与为了去除组间的影响):

      x = (x + (x >> 4)) & 0x0f0f0f0f

      根据这个思路,第一、二步无法通过这种方法优化,第一步最多占两位,第二步最多占三位,因此不能先加再与;第三步以后均可通过该方法优化

    • 对更后面的步骤(第四步及以后):每组加和的最大值所需位数远小于每组位数的一半,例如第四步中,每组加和最大为1616,占五位;而每组位数的一半为88

      因此从上一个组移到下一个组的位不会有影响,例如:

      ​ 第三步后:0x 0a0 0a0..0a0\rm0x\ 0a_0\ 0a_0..0a_0,a0≤1000a_0\le1000,若后续不和掩码相与

      ​ 第四步后:0x 0a0bb0 aa1bb0..aa1bb0\rm0x\ 0a_0bb_0\ aa_1bb_0..aa_1bb_0

      ​ bb0=a0h+a0l≤10000、aa1=a0up+a0h≤10000bb_0=a_{0h}+a_{0l}\le10000、aa_1=a_{0up}+a_{0h}\le10000

      ​ 第五步后:0x 0a0bb0 aa2cc0 aa3cc0..aa3cc0\rm0x\ 0a_0bb_0\ aa_2cc_0\ aa_3cc_0..aa_3cc_0

      ​ cc0=bb0h+bb0l≤100000、aa2=0a0+aa1≤11000、aa3=aa1+aa1≤100000cc_0=bb_{0h}+bb_{0l}\le100000、aa_2=0a_0+aa_1\le11000、aa_3=aa_1+aa_1\le100000

      ​ 若和掩码相与:第四步0x00bb00bb..00bb\rm0x00bb00bb..00bb、第五步0x000000cc..000000cc\rm0x000000cc..000000cc

      上述cccc(有效位)只要在八位之内,多余位就可在最后一步和掩码相与后除去

      换言之,只要在计数之前除去多余位即可;由于3232位整数本就只用66位,因此第四、五步可以不与:

      c++
      x -= (x >> 1) & 0x55555555;
      x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
      x = (x + (x >> 4)) & 0x0f0f0f0f;
      x += x >> 8;
      x += x >> 16;
      x &= 0x3f; // 最后保留末6位即可
      // 推广到64位, 因为有效位为7, 故:
      // (在第五步之后)
      x += x >> 32;
      x &= 0x7f;
    • 和乘法的结合:和上述思想类似,因为有效位只有六位,在第三步后每组八位,就可在第三步后将末三组移到最高八位相加(末三组总和不超过六位,故不会影响最高组)

      即:(x+x<<8+x<<16+x<<24)>>24(x+x<<8+x<<16+x<<24)>>24,分配律到乘法

      即:(x×0x1010101)>>24(x\cross\rm0x1010101)>>24,乘法得出的高位应强制转换成int来舍去它们

      乘法效率其实不如上述优化强,但代码更简洁:

      c++
      unsigned int y = x;
      y -= (y >> 1) & 0x55555555;
      y = (y & 0x33333333) + ((y >> 2) & 0x33333333);
      y = (((y >> 4) & 0x0f0f0f0f) * 0x1010101) >> 24;

异或运算

基本性质

  • 异或运算后,两操作数相同位得零、不同位得一

  • 异或运算是一种对称加密:A=(A⊕B)⊕B、B=(A⊕B)⊕AA=(A\oplus B)\oplus B、B=(A\oplus B)\oplus A

    • 不通过第三个变量交换两个整数:A⊕B→A、A⊕B→B、A⊕B→AA\oplus B\rightarrow A、A\oplus B\rightarrow B、A\oplus B\rightarrow A
    • A⊕Aˉ=全1、A⊕A=0A\oplus\bar A=全1、A\oplus A=0
  • 满足差分性质:

汉明距离

  • 汉明距离:两个整数中不同位的个数

  • 计算一:调用库函数

    c++
    __builtin_popcount(x ^ y);