MIT HAKM求二进制形式1的个数

求32位无符号数的二进制形式中1的个数,这是个经典的题目。求解这个题目,我们常规的做法就是将此二进制数右移31次,每次移位后判断最低位的那个是否为1,如果是1,那么计数加1。
那么还有没有更优的解法来减少移位和运算次数呢?
在1972年MIT人工智能实验室发布的一本备忘录中,提到了一个非常有趣的算法来解决这个问题,下面我们先来看一下这个算法是怎么做的?

unsigned int count(unsigned int n)
{
    unsigned int temp= n - ((n>>1)&033333333333) - ((n>>2)&011111111111)
    return ((temp+(temp>>3))&030707070707) %63;
}

刚刚看到上面的这个一脸懵逼,这是怎么来的?
其实就是分治法。

1. 一个自然数的多项式表示。

只要是一个自然数不会导致32位无符号数溢出,那么这个自然数就可以用以2为底的多项式表示。
p(n)2 = an2n+an-12n-1+an-22n-2+...+a222+a121+a020
ai取值[0,2)。根据计算机二进制表示,sum{ai}就是1的个数。
那么如果以4为底呢?
p(n)4 = an4n+an-14n-1+an-24n-2+...+a242+a141+a040
ai取值范围[0,4)。根据计算机二进制表示,相当于把其每两位分割成一组,单独将每一组数取出,其值就是ai,比如说假设一个8位的二进制数(01101011)2
p(n)4 =1*43+2*42+2*41+3*40
这样我们就将这个二进制以每份2位进行分割,我们只需要知道每一个系数的数字对应的二进制1的个数,然后累加即可。
以此类推,
p(n)2x=an(2x)n+an-1(2x)n-1+an-2(2x)n-2+...+a2(2x)2+a1(2x)1+a0(2x)0
此式就表示将二进制数以x位为一组进行分割,下面我们只需要知道每个系数对应的二进制1的个数,累加即可。
我们完成了分割。
那么我们应该怎样计算每个系数对应的二进制个数呢?
假设以3位一组分割,
对于其中一个系数ai,我们只需要向右移动两次,每次与(001)2,将结果累加。
ai=ai&(001)2+ai>1&(001)2+ai>2&(001)2
对于32位无符号整数。以3位为一组分割
temp=p(n)23&010101010101+p(n)23>1&010101010101+p(n)23>2&010101010101式子中的常数为八进制数
那么这个temp所表示的数也可以表示为3位一组分割的多项式形式,并且temp的系数ai就是p(n)23中对应第i项系数二进制形式1的个数。
我们只需要将temp 求的∑0nai
也就是说我们现在需要利用某种手段消掉每一项中底的幂次方。
怎么办?巧了,还真有这样的公式

2. nN%(n−1)=1

对于任意一个自然数n,其N次方始终可以用n-1取模,结果为1

证明:
1.假设n(k-1)% (n-1) = 1成立
2.那么nk% (n-1)=[(n-1)*n(k-1)+n(k-1)]%(n-1)=0+nk−1%(n−1) = nk−1%(n−1) = 1
依然成立
3.又有 n(1-1)% (n-1) = 1
顾nN%(n−1)=1结论成立。

3. 多项式求系数和

下面我们回到多项式消去底,对系数求和的问题上来。
p(n)N=anNn+an-1Nn-1+an-2Nn-2+...+a2N2+a1N1+a0N0
下面用到的是(a+b)%c = (a%c+b%c)%c
p(n)N%(N-1) = [anNn%(N-1)+an-1Nn-1%(N-1)+an-2Nn-2%(N-1)+...+a2N2%(N-1)+a1N1%(N-1)+a0N0%(N-1) ]%(N-1)
=(an+an-1+an-2+...+a2+a1+a0)%(N-1)
如果能保证0nai < N-1
那么p(n)N%(N-1) = an+an-1+an-2+...+a2+a1+a0
由本文章节1中说得出的结论temp变量的多项式中,系数和是二进制数1的个数,那么这个系数和一定<=32
也就是说N-1>32,N>33
N是一个以2的幂次方,最小的分割单位是6位,才可以满足当前多项式求系数和的公式。
所以我们对原数P(n)的二进制数以6位为间隔进行分割,那么我们只需要通过>,&将P(n)每一组中的系数转换成二进制1的个数保存起来,这样我们就得到一个多项式temp,temp和P(n)都是以6位为一组分割的,不同的是P(n)的系数是表示的是这一组的实际值,而temp的系数表示的是P(n)的系数中二进制1的个数,那么temp多项式中的系数和表示的是P(n)二进制中1的个数。
代码如下

//注意:0开头的常数均为8进制数
void count(unsigned int n)
{
    unsigned int temp= 
          (n &010101010101)
     +((n>>1)&010101010101)
     +((n>>2)&010101010101)
     +((n>>3)&010101010101)
     +((n>>4)&010101010101)
     +((n>>5)&010101010101);
 return (temp%63);
}

我们再对这段代码进行优化。
4.代码优化
我们知道temp变量中系数是原数中1的个数,元素中1的个数最大为6,即(000110)2,所以只需呀3位有效位即可,所以我们在计算位数时,先以每3位为一组计算temp,然后再将temp转化成以6位为一组的形式,即前三位向后移3位+后3位.

//注意:0开头的常数均为8进制数
void count(unsigned int n)
{
    unsigned int temp= 
         (n &011111111111)
    +((n>>1)&011111111111)
    +((n>>2)&011111111111);
 temp = (temp + (temp>>3)) &030707070707;
 return (temp%63);
}

此方法减少了运算过程。
我们还可以优化每3位中组内的计算
(a22+b21+c20)-(a21+b20)-(a20) = a+b+c
相当于
n-(n>1&03)-(n>2&01) = a+b+c; 03,01 均为八进制数
又简化了一次运算

void count(unsigned int n)
{
   unsigned int temp= n - ((n>>1)&033333333333) - ((n>>2)&011111111111)
  temp= (temp+(temp>>3))&030707070707;
    return temp%63;
}

经过一次分割,利用系数保存原始数据中每一组1的个数,然后多项式求系数和,然后优化求解每组求1个数的过程,最终得到了这个内有乾坤的算法。