MIT HAKMEM算法

今天學習了一種很有趣的BitCount算法——MIT HAKMEM算法。

 

本文中^表示乘方

 

問題需求:計算32位整型數中的'1'的個數

 

思路分析:

 

1.整型數 i 的數值,實際上就是各位乘以權重——也就是一個以2爲底的多項式:

 

i = A0*2^0+A1*2^1+A2*2^2+...

 

因此,要求1的位數,實際上只要將各位消權:

 

i = A0+A1+A2+...

 

所得的係數和就是'1'的個數。

 

2.對任何自然數n的N次冪,用n-1取模得數爲1,證明如下:

 

若 n^(k-1) % (n-1) = 1 成立

 

則 n^k % (n-1) = ((n-1)*n^(k-1) + n^(k-1)) % (n-1) = 0 + n^(k-1) % (n-1)  = 1 也成立

 

又有 n^(1-1) % (n-1) = 1

 

故對任意非負整數N, n^N %(n-1)=1

 

3.因此,對一個係數爲{Ai}的以n爲底的多項式P(N), P(N)%(n-1) = (sum({Ai})) % (n-1) ;

 

如果能保證sum({Ai}) < (n-1),則 P(N)%(n-1) = (sum({Ai}))  ,也就是說,此時只要用n-1對多項式取模,就可以完成消權,得到係數和。

 

於是,問題轉化爲,將以2爲底的多項式轉化爲以n爲底的多項式,其中n要足夠大,使得n-1 > sum({Ai})恆成立。

 

32位整型數中Ai=0或1,sum({Ai})<=32。n-1 > 32 ,n需要大於33。

 

因此取n=2^6=64>33作爲新多項式的底。

 

4.將32位二進制數的每6位作爲一個單位,看作以64爲底的多項式:

 

i = t0*64^0 + t1*64^1 + t2*64^2 + t3*64^3 + ...

 

各項的係數ti就是每6位2進制數的值。

 

這樣,只要通過運算,將各個單位中的6位數變爲這6位中含有的'1'的個數,再用63取模,就可以得到所求的總的'1'的個數。

 

5.取其中任意一項的6位數ti進行考慮,最簡單的方法顯然是對每次對1位進行mask然後相加,即

 

(ti>>5)&(000001) + (ti&>>4)(000001) + (ti>>3)&(000001) + (ti>>2)&(000001) + (ti>>1)&(000001) + ti&(000001)

 

其中000001位2進制數

 

由於ti最多含有6個1,因此上式最大值爲000110,絕不會產生溢出,所以上式中的操作完全可以直接對整型數 i 進行套用,操作過程中,t0~t6將並行地完成上式的運算。

 

注意:不能將&運算提取出來先+後&,想想爲什麼。

 

因此,bit count的實現代碼如下:

 

[cpp] view plain copy

    int bitcount(unsigned int n)  
    {  
        unsigned int tmp;  
      
        tmp = (n &010101010101)  
         +((n>>1)&010101010101)  
         +((n>>2)&010101010101)  
         +((n>>3)&010101010101)  
         +((n>>4)&010101010101)  
         +((n>>5)&010101010101);  
      
        return (tmp%63);  
    }  

 

但MIT HAKMEM最終的算法要比上面的代碼更加簡單一些。

 

爲什麼說上面的式子中不能先把(ti>>k)都先提取出來相加,然後再進行&運算呢?

因爲用&(000001)進行MASK後,產生的有效位只有1位,只要6位數中的'1'個數超過1位,那麼在"先加"的過程中,得數就會從最低位中向上溢出。

 

但是我們注意到,6位數中最多隻有6個'1',也就是000110,只需要3位有效位。上面的式子實際上是以1位爲單位提取出'1'的個數再相加求和求出6位中'1'的總個數的,所以用的是&(000001)。如果以3位爲單位算出'1'的個數再進行相加的話,那麼就完全可以先加後MASK。算法如下:

 

tmp = (ti>>2)&(001001) + (ti>>1)&(001001) + ti&(001001)

(tmp + tmp>>3)&(000111)

 

C代碼:

[cpp] view plain copy

    int bitcount(unsigned int n)  
    {  
        unsigned int tmp;  
      
        tmp = (n &011111111111)  
         +((n>>1)&011111111111)  
         +((n>>2)&011111111111);  
           
        tmp = (tmp + (tmp>>3)) &030707070707;  
      
        return (tmp%63);  
    }  

 

注:代碼中是使用8進制數進行MASK的,11位8進制數爲33位2進制數,多出一位,因此第一位八進制數會把最高位捨去(7->3)以免超出int長度。

 

從第一個版本到第二個實際上是一個“提取公因式”的過程。用1組+, >>, &運算代替了3組。並且已經提取了"最大公因式"。然而這仍然不是最終的MIT HAKMEM算法,不過已經非常接近了,看看代碼吧。

 

MIT HAKMEM算法:

[cpp] view plain copy

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

 

又減少了一組+, >>, &運算。被優化的是3位2進制數“組”內的計算。再回到多項式,一個3位2進制數是4a+2b+c,我們想要求的是a
+b+c,n>>1的結果是2a+b,n>>2的結果是a。

 

於是: (4a+2b+c) - (2a+b) - (a) = a + b + c

 

中間的MASK是爲了屏蔽"組間""串擾",即屏蔽掉從左邊組的低位移動過來的數。

 

總結

 

第一次自己寫算法分析,感覺收穫還是蠻大的。

 

看算法的時候都是看這個算法是如何工作的,寫的過程則是去模擬發現者的思路,看這個算法是如何被推導出來的,把思路反過來又重新梳理了一遍。

 

最大的感慨是數學對於算法的重要性。這個算法最開始的思路就是多項式消權,並且貫穿了整個算法推導和優化的過程。而第二步的必要條件則是對取模和冪運算關係的瞭解。優化的第一步用到了“提取公因式”思想,第二步則迴歸到了多項式的基本運算。

 

其中除了取模和冪運算的知識不算太基礎以外,其他無一例外都是初中以內的數學知識與思想。特別是對2進制數本質的理解,所有人都清楚如何求得一個二進制整型數的數值,但是卻很少有人對其多項式本質始終保持着清晰的認識。

 

就是這些小得不能再小,基礎得不能再基礎的地方,決定了我們的水平。
---------------------
作者:msquare
來源:CSDN
原文:https://blog.csdn.net/msquare/article/details/4536388
版權聲明:本文爲博主原創文章,轉載請附上博文鏈接!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章