C/C++中__builtin_popcount()的使用及原理


__builtin_popcount()用於計算一個 32 位無符號整數有多少個位為1

Counting out the bits
    可以很容易的判斷一個數是不是2的冪次:清除最低的1位(見上面)並且檢查結果是不是0.盡管如此,有的時候需要直到有多少個被設置了,這就相對有點難度 了。

    GCC有一個叫做__builtin_popcount的內建函數,它可以精確的計算1的個數。盡管如此,不同於__builtin_ctz,它並沒有被 翻譯成一個硬件指令(至少在x86上不是)。相反的,它使用一張類似上面提到的基於表的方法來進行位搜索。這無疑很高效並且非常方便。

    其他語言的使用者沒有這個選項(盡管他們可以重新實現這個算法)。如果一個數只有很少的1的位,另外一個方法是重復的獲取最低的1位,並且清除它。

查看引用來源

為了在 VC 上實現 __builtin_popcount (unsigned u) 的功能,自己寫了兩個函數,分別是 popcnt (unsigned u), popcount (unsigned u) 。
前者是通過清除 u 最低的 bit 1 ,直至 u 為 0 ,每次都為計數器加 1 。時間復雜度為 O (m) , m 為 bit 1 的個數。
后者是使用二分法,比較巧妙,跟蹤調試一下就知道原理了。時間復雜度為 O (lg N) , N 為位數。
不過最高效的還是使用查表的方式來計算。
但是需要弄一個很大的表,不然隨着位數的增長,查表的速度還是比不上二分法的速度。例如 64 位整數,保存 所有 8 位整數的結果 (256 個)。查表需要操作 8 次,而二分法需要 6 次而已。

測試代碼如下:

// 計算一個 32 位無符號整數有多少個位為 1

#include <iostream>
#include <ctime>
using namespace std;

unsigned popcnt (unsigned u)
{
    unsigned ret = 0;
    while (u)
        {
        u = (u & (u - 1));    // 將 u 最右邊的 1 清除
        ret ++;
        }
    return ret;
}

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


int main ()
{
     // 先測試一下函數是否正常工作
    for (int i = 0; i <= 1000; i ++)
        {
        int k = popcnt (i);
        cout << k << " ";
        }
    cout << endl;
   
    for (int i = 0; i <= 1000; i ++)
        {
        int k = popcount (i);
        cout << k << " ";
        }
    cout << endl;
   
    // 比較速度    
    int test;
    test = (1U << 31) - 1;
    //test = 1000000;
    clock_t start, finish;

    start = clock ();
    for (int i = 0; i < test; i ++)
        {
        popcnt (i);
        }
    finish = clock ();
    cout << (double)(finish - start) / (double)(CLOCKS_PER_SEC) << " s" << endl;

    start = clock ();
    for (int i = 0; i < test; i ++)
        {
        popcount (i);
        }
    finish = clock ();
    cout << (double)(finish - start) / (double)(CLOCKS_PER_SEC) << " s" << endl;

    start = clock ();
    for (int i = 0; i < test; i ++)
        {
        //__builtin_popcount (i);    // G++內建函數, Dev-C++ 可支持
        }
    finish = clock ();
    cout << (double)(finish - start) / (double)(CLOCKS_PER_SEC) << " s" << endl;
   
    system ("pause");

    return 0;
}


/*
使用 VC 2005 ,在 test = (1U << 31) - 1 時, release 狀態是 0 s 出結果的,但是 debug 狀態就很慢, 換 test = 1000000 試試。
使用其他 IDE, 即使是 release 狀態下,速度也不理想。但是可以測得 __builtin_popcount (通過查表來計算)比較快。
*/

說到底,這個函數到底有什么實際用處呢?當然有了,使用一個二進制數字表示一個集合的時候,枚舉一個組合(子集),需要判斷這個數字里面的 1 的個數是不是和子集的大小相等。這種方法通常是屬於暴力法,如果不是使用二進制數字表示集合,很可能就計算超時了。
下面有個例子(sicily 1158),期待更好的解法,但是暴力法的效果還不錯^_^而且實現簡單。。

====================================帥氣的分割 線====================================

Pick numbers

Total Submit : 371    Accepted Submit : 135

Problem
Given a matrix of size M*N, the elements of which are integer numbers from -10 to 10. You task is to Go from
the top-left corner (1, 1) to the bottom-right corner (M, N). You can only move right or down, and you can
not go out of the matrix. The number in the grid you passed must be picked. The sum of numbers you picked must
be positive and as minimal as possible.
Input

The input contains several test cases.

      The first line of each test case are two integer numbers M, N (2<=M<=10, 2<=N<=10), indicating the number
      of rows and columns of the matrix. The following M lines, each contains N integer numbers.
Output
For each test case, output the sum of numbers you picked on a single line. If you can't get a positive sum,
output -1.

Sample input
2 2

0 2
1 0

3 3

0 0 0
0 0 0
0 0 0

Sample output
1
-1

Problem Source
2006 Algorithm Course Examination

====================================無聊的分割 線====================================

分析:
用一個二進制數字 path 表示所走的路徑,從低位開始,如果某位為 1 ,表示向下走,否則向右走。
path 中的位 1 必須有 m - 1 個,使用 G++ 內建函數 __builtin_popcount (path) 來計算。
path 的起始值為 first = (1 << (m - 1)) - 1 ,表示先向下走 m - 1 步。
path 的終止值為 last = first << (n - 1) ,表示先向右走 n - 1 步。
從 first 到 last ,枚舉所有可行的 path ,然后計算對應的 sum ,目標是找一個最小的 sum > 0 ,
如果找不到,sum = -1 。
注意此題不能用動態規划來求最小值,因為該最小值可能是負數。
枚舉的次數是 2m + n - 2 - 2n - 1, 當 m, n <= 10 時為 512 。

#include <cstdio>
using namespace std;

const int M = 10;
const int N = 10;
int d [M] [N];
int m, n;

void f ()
...{
    for (int i = 0; i < m; i ++)
        ...{
        for (int j = 0; j < n; j ++)
            ...{
            scanf ("%d", &d [i] [j]);
            }
        }
    int first = (1 << (m - 1)) - 1;
    int last = first << (n - 1);
    int cnt = m - 1;
    int MASK = 1 << (m + n - 2);
    int max = (1U << 31) - 1;

    for (int path = first; path <= last; path ++)
        ...{
        if (__builtin_popcount (path) == cnt)
            ...{
            int sum = d [0] [0];
            for (int i = 0, j = 0, mask = 1; mask < MASK; mask <<= 1)
                ...{
                if (mask & path)
                    ...{
                    i ++;                   
                    } else    ...{
                        j ++;
                        }
                sum += d [i] [j];
                }
            if (sum > 0 && sum < max)
                ...{
                max = sum;
                }
            }
        }
    printf ("%d ", (max == ((1U << 31) - 1) ? -1 : max));
}

int main ()
...{
    while (scanf ("%d%d", &m, &n) != EOF)
        ...{
        f ();
        }
    return 0;
}


提交結果:
Run ID      User Name      Problem      Language      Status      Run Time      Run Memory      Submit Time
84848       rappizit     1158     C++     Accepted     0.01 sec     260 KB     2007-09-17 13:38:57

===================================遺忘的分割 線=====================================

以下內容為新添加的:
經測試,該函數的速度是 G++ 的一半,所以有理由認為 G++ 保存的表的大小是 65536 的(或者使用匯編)。但是不可能在自己的代碼里面加上那么一大段數據吧?權衡一下,表的大小設為 256 是適當的,可以在 VC 里面使用以下代碼。

char poptable [256] =
{
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
};
unsigned __builtin_popcount (unsigned u)
{
    return poptable [u & 0xFF] + poptable [(u >> 8) & 0xFF] + poptable [(u >> 16) & 0xFF] + poptable [(u >> 24) & 0xFF];
}=====================20080725添加========================
unsigned popcount (unsigned u)
{
    u = (u & 0x55555555) + ((u >> 1) & 0x55555555);
    u = (u & 0x33333333) + ((u >> 2) & 0x33333333);
    u = (u & 0x0F0F0F0F) + ((u >> 4) & 0x0F0F0F0F);
    u = (u & 0x00FF00FF) + ((u >> 8) & 0x00FF00FF);
    u = (u & 0x0000FFFF) + ((u >> 16) & 0x0000FFFF);
    return u;
}
這個二分法的原理:
用8位二進制數來做示范好了,例如 u = 10110011。
10110011
00010001    //每兩位取1位,即取偶數位, u & 01010101
01010001    //取奇數位並右移一位, (u >> 1) & 01010101
---------------
01100010    //上面兩數相加,賦值給u,注意每兩列相加的結果不會進位到第三列
00100010    //每四位取低兩位, u & 00110011
00010000    //每四位取高兩位並右移兩位, (u >> 2) & 00110011
---------------
00110010    //上面兩數相加,賦值給u
00000010    //每八位取低四位, u & 00001111
00000011    //每八位取高四位並右移四位,(u >> 4) & 00001111
---------------
00000101    //上面兩數相加,賦值給u
最終結果 u = 5。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM