Các kĩ thuật xử lí bít I-- Bitwise tricks I

Như Donald Knuth [2] đã nói, thao tác với bít là "một thành phần không thể thiếu trong toolkit của các lập trình viên tốt". Trong bài này chúng ta sẽ tìm hiểu tại sao thao tác với bít lại quan trong như vậy. Sử dụng thao tác bít, chúng ta có thể thực hiện nhiều thứ rất "ảo" bằng một vài câu lệnh đơn giản.

Xét một ví dụ "đơn giản" sau: cho một số nguyên 64 bít không dấu $x > 0$, kiểm tra xem $x$ có phải là một lũy thừa của 2 hay không, i.e, kiểm tra xem có tồn tại một số $y \geq 0$ sao cho $x= 2^y$? Có lẽ một số trong chúng ta sẽ tiếp cận như sau: với mỗi $y \in \{0,1,\ldots,63\}$, ta tính $2^y$ và so sánh với $x$:

IsPowerOf2($x$):
    $yy = 1$
    for $y \leftarrow 0$ to $63$
        if $yy = x$
            return Yes
        $yy \leftarrow 2yy$
    return No

 
Tất nhiên bạn có thể thêm một vài heuristic để thuật toán nhanh hơn, còn về mặt ý tưởng thì là như vậy. Tuy nhiên, nếu bạn biết xử lí bít thì hàm trên có thể được thực hiện nhanh và ngắn gọn như sau:

IsPowerOf2B($x$):
    if $x\&(x-1) = 0$
        return Yes
    else
        return No

 
Code C:

typedef unsigned long long int LLU;
/*
* Note: this method is only correct for x != 0
* For x = 0, we need to modify this method a bit.
*/
int is_power_of_2B(LLU x){
if( (x&(x-1)) == 0){
return TRUE;
}else {
return FALSE;
}
}

Trong đó $\&$ là phép AND. Hi vọng ví dụ trên đủ để thuyết phục bạn rằng nắm chắc các kĩ thuật xử lí bít là một điều không thể thiếu trong các kĩ năng của bạn. Trong các phần tiếp theo chúng ta sẽ tìm hiểu tại sao thuật toán IsPowerOf2B là đúng.

Khái niệm cơ bản

Ta sẽ kí hiệu số nguyên có dấu là sgnk và số nguyên không dấu là usgnk trong đó $k$ là số bít. Ví dụ số nguyên 64 bít không dấu là usgn64 và số nguyên 32 bít có dấu là sgn32.

Các thao tác bít cơ bản và kí hiệu toán học dùng trong các bài viết này được cho bởi bảng dưới:

Thao tác Kí hiêu Ví dụ
AND $\&$ $00111110 \& 10010011 = 00010010 $
OR $|$ $00111110 | 10010011 = 10111111 $
NOT $\overline{.}$ $\overline{00111110} = 11000001 $
XOR $\oplus$ $00111110 \oplus 10010011 = 10101101 $
SHIFT LEFT $\ll$ $10111110 \ll 2 = 11111000 $
SHIFT RIGHT $\gg$ $00111110 \gg 3 = 00000111 \quad \\ 10111110 \gg_u 3 = 00010111 \quad \\ 10111110 \gg_s 3 = 11110111 $

 

Các ngôn ngữ lập trình thường cung cấp 2 phép SHIFT RIGHT là LOGICAL SHIFT RIGHT và ARITHMETIC SHIFT RIGHT. Sự khác nhau nằm ở chỗ bít dấu sẽ được shift như thế nào. Trong LOGICAL SHIFT RIGHT, kí hiệu $\gg_u$, bít dấu không được giữ nguyên khi shift Trong ARITHMETIC SHIFT RIGHT, kí hiệu $\gg_s$, bít dấu sẽ được shift lan về bên phải khi shift. Hai ví dụ cuối cùng trong bảng trên của hàng SHIFT RIGHT minh họa cho sự khác nhau này. Nếu số được shift là số không dấu thì ARITHMETIC SHIFT RIGHT và LOGICAL SHIFT RIGHT là như nhau. Một số ngôn ngữ có hai phép SHIFT LEFT là ARITHMETIC SHIFT LEFT và LOGICAL SHIFT LEFT. Tuy nhiên trong khuôn khổ những bài viết ở đây, ta chỉ xét LOGICAL SHIFT LEFT mà ta gọi là SHIFT LEFT cho ngắn gọn.

Một trong những điều chúng ta phải hết sức chú ý đó là thứ tự ưu tiên của các thao tác. Thứ tự thông thường như sau:

NOT > nhân, chia > cộng, trừ > SHIFT LEFT, SHIFT RIGHT > AND > XOR > OR

Tính chất

Một số tính chất của các thao tác như sau:

$\begin{array} {l} x\&y = y\&x, \qquad x|y = y|x, \qquad x\oplus y = y\oplus x & \qquad(1) \\ (x\&y)\&z = x\&(y\&z) , \quad (x|y)|z = x|(y|z), \quad (x\oplus y)\oplus z = x\oplus (y\oplus z) & \qquad(2) \\ (x|y)\&z = (x\&z)|(y\&z), \qquad (x\&y)|z = (x|z)\&(y|z) & \qquad (3) \\ (x\oplus y)\&z = (x \& z)\oplus (y \&z) & \qquad (4) \\ (x\&y)|x = x, \qquad (x|y)\&x = x & \qquad (5) \\ (x\&y) \oplus (x|y) = x \oplus y & \qquad (6) \\ x\& 0 = 0, \qquad x|0 = x, \qquad x\oplus 0 = x & \qquad (7) \\ x\&x = x, \qquad x|x = x, \qquad x\oplus x = 0 & \qquad (8) \\ x\&-1 = x, \qquad x|-1 = -1, \qquad x\oplus-1 = \overline{x} & \qquad (9) \\ x\& \overline{x} = 0, \qquad x|\overline{x} = -1, \qquad x\oplus \overline{x} = -1 &\qquad (10) \\ \overline{x\&y} = \overline{x} | \overline{y}, \qquad \overline{x|y} = \overline{x} \&\overline{y}, \qquad \overline{x \oplus y} = \overline{x} \oplus y = x \oplus \overline{y} & \qquad (11) \\ x \ll k = 2^k x ,\qquad x \gg k = \lfloor \frac{x}{2^k} \rfloor &\qquad (12) \end{array}$

Các tính chất $(1-11)$ không quá khó để chứng minh. Do các thao tác áp dụng kiểu bitwise (không có nhớ), để chứng minh, ta chỉ cần kiểm tra các thao tác trên đúng với trường hợp $x,y,z$ chỉ có 1 bít. Chi tiết coi như bài tập cho bạn đọc. Chú ý $-1$ có biểu diễn nhị phân là $(\ldots111)_2$ (chỉ gồm các bít 1). Tính chất $(12)$ ($x$ không dấu) được suy ra từ định nghĩa của các phép shift.

Ta dễ thấy: $x + \overline{x} = (\ldots 1111)_2 = -1$, từ đó suy ra:

$-x = \overline{x}+ 1 \qquad (13)$

Thay $x$ bởi $x-1$, ta cũng suy ra:

$-x = \overline{x-1} \qquad (14)$

Kết hơp $(13), (14)$ ta có $\overline{x-1} = \overline{x} + 1$. Bằng phép quy nạp, ta suy ra:

$\overline{x-y} = \overline{x} + y \qquad (15)$

Các ví dụ

Trong các ví dụ dưới đây, các giá trị boolean TRUE/FALSE sẽ được coi là các giá trị $1/0$.

Kiểm tra hai số nguyên khác dấu

Cho hai số nguyên $x,y$ kiểu sgnk, trả về $1$ nếu $x,y$ khác dấu và 0 nếu ngược lại. Các thủ tục dưới đây thực hiện hàm này. Tính đúng đắn coi như bài tập cho bạn đọc.

IsDiffSignA(sgnk $x$, sgnk $y$):
    return $ (x\oplus y) < 0$

 

IsDiffSignB(sgnk $x$, sgnk $y$):
    return $(x\oplus y) \gg_u (k-1)$

 

Code C:

int is_diff_signA(int x, int y){
return (x^y) &lt; 0;
}
int is_diff_signB(int x, int y){
return ((unsigned)(x^y))&gt;&gt; 31;
}

Tính giá trị tuyệt đối

Tính giá trị tuyệt đối của số nguyên $x$ có độ dài $k$ bít:

AbsA(sgnk $x$):
    $m \leftarrow x \gg_s (k-1) $         [[ $m = 0$ if $x \geq 0$ and $-1$ otherwise]]
    return $x - (2x\&m)$

 

AbsB(sgnk $x$):
    $m \leftarrow x \gg_s (k-1) $
    return $(x \oplus m) - m$

 

AbsC(sgnk $x$):
    $m \leftarrow x \gg_s (k-1) $
    return $(x + m) \oplus m$

 
Code C:

int absa(int x){
int m = x &gt;&gt; (31); // the sign mask
return x - ((2*x)&amp;m);
}

int absb(int x){
int m = x &gt;&gt; (31); // the sign mask
return (x^m) - m;
}

int absc(int x){
int m = x &gt;&gt; (31); // the sign mask
return (x+m)^m;
}

Chứng minh AbsA trả lại giá trị tuyệt đối của $x$ coi như bài tập cho bạn đọc. Ta sẽ chứng minh thủ tục AbsB là đúng:

  1. Nếu $x \geq 0$, ta suy ra $m = 0$. Do đó $(x\oplus 0) - 0 = x$.
  2. Nếu $x < 0$, ta suy ra $m = -1 = (\ldots 1111)_2$. Ta có:

    $\begin{array} {l} (x \oplus m) - m & = (x\oplus -1) -1 \\ & = \overline{x} - 1 \qquad (\mbox{ by } (9)) \\ &= -\overline{\overline{x}} = -x \qquad (\mbox{ by } (14))\end{array}$

Chứng minh AbsC đúng tương tự như chứng minh AbsB đúng. Chi tiết coi như bài tập cho bạn đọc.

Mở rộng bít dấu

Giả sử ta có một số nguyên có dấu $x$ độ dài $k$ bít, ta muốn mở rộng bít thứ $b$ của $x$ ra thành bít dấu:

  1. Nếu bít thứ $b$ là $1$ thì ta sẽ chuyển $k-b$ bít cao nhất thành $1$.
  2. Nếu bít thứ $b$ là $0$ thì ta sẽ chuyển $k-b$ bít cao nhất thành $0$.

Ví dụ $x = (10001001)_2, k = 8, b = 4$, giá trị sau khi mở rộng là $x = (1111 1001)_2$. Ví dụ khác, $x = (10000001)_2, k = 8, b = 4$, giá trị sau khi mở rộng là $x = (0000 0001)_2$. Ta có thể làm như sau:

SignExtA(sgnk $x, b$):
    return $(x \ll (k-b)) \gg_s (k-b)$

 

SignExtB(sgnk $x, b$):
    $m \leftarrow 1 \ll (b-1)$
    $x \leftarrow x\&((1 \ll b)-1)$        [[clear $k-b$ leading bits]]
    return $(-(x \&m)) | x$

 

SignExtC(sgnk $x, b$):
    $m \leftarrow 1 \ll (b-1)$
    $x \leftarrow x\&((1 \ll b)-1)$
    return $(x \oplus m) - m$

 

Code C:

int sign_extA(int x, int  b){
return (x &lt;&lt; (32-b)) &gt;&gt; (32-b);
}

int sign_extB(int x, int  b){
int m = 1 &lt;&lt; (b-1); // the signed bit
x = x&amp;((1&lt;&lt;b)-1); // clear k-b leading bits
return (-(x&amp;m))|x;
}

int sign_extC(int x, int b){
int m = 1 &lt;&lt; (b-1);
x = x&amp;((1&lt;&lt;b)-1); // clear k-b leading bits
return (x^m)-m;
}

Ta sẽ chứng minh SignExtB đúng. Các thủ tục khác chứng minh tương tự. Gọi $x = (\beta x_b\alpha)_2$ với $|\alpha| = b-1$ và $|\beta| = k -b$. Ta cần chứng minh thủ tục sẽ trả về $(0^{k-b}0\alpha)_2$ nếu $x_b = 0$ và $(1^{k-b}1\alpha)_2$ nếu $x_b = 1$.

Thật vậy, sau khi đã xóa $k-b$ bít đầu tiên (lệnh $x\&(1 \ll b)$), ta có $x = 0^{k-b}x_b\alpha$. Nếu $x_b = 0$, ta suy ra $x\&m = 0$ và thủ tục sẽ trả lại $0 | x = x = (0^{k-b}0\alpha)_2$. Nếu $x_b = 1$, ta có $x\&m = (0^{k-b}10^{|\alpha|})_2$ và $-x = (1^{k-b}1\alpha)_2$. Do đó, thủ tục trả lại $-x|x = (1^{k-b}10^{|\alpha|})_2 | (0^{k-b}1\alpha)_2 = 1^{k-b}1\alpha$.

Đổi giá trị hai biến nguyên không dùng biến thứ ba

Một trong những thủ tục trong các thuật toán sắp xếp là đổi chỗ hai biến. Thông thường, ta sẽ dùng một biến trung gian để lưu một trong hai giá trị trong quá trình đổi chỗ. Nếu hai biến cần đổi chỗ là các số nguyên ta không cần phải sử dụng biến trung gian đó.

ExchangeAdd(x,y):
    $x \leftarrow x+y$
    $y \leftarrow x-y$
    $x \leftarrow x-y$

 

ExchangeXor(x,y):
    $x \leftarrow x \oplus y$
    $y \leftarrow x\oplus y$
    $x \leftarrow x\oplus y$

 
Code C:

void exchange_add(int *x, int *y){
*x = *x + *y;
*y = *x - *y;
*x = *x - *y;
}

void exchange_xor(int *x, int *y){
*x = *x^*y;
*y = *x^*y;
*x = *x^*y;
}

Chứng minh tính đúng đắn coi như bài tập cho bạn đọc.

Tìm giá trị lớn nhất, nhỏ nhất

Cho hai giá trị nguyên $x,y$, tìm giá trị lớn nhất, nhỏ nhất của hai số $x,y$.

MinA(sgnk $x$,sgnk $y$):
    $m \leftarrow -(x< y)$         [[$m=-1$ if $x
    return $y\oplus ((x\oplus y)\&m)$

 

MinB(sgnk $x$, sgnk $y$):
    $m \leftarrow (x-y) \gg (k-1)$         [[$m=-1$ if $x
    return $y + ((x-y)\&m)$

 
Code C:

int minA(int x, int y ){
int m = -(x&lt;y);
return y^((x^y)&amp;m);
}

int minB(int x, int y ){
int m = -(x&lt;y);
return y + ((x-y)&amp;m);
}

Tính đúng đắn coi như bài tập cho bạn đọc. Thủ tục tính max tương tự như tính min. Chi tiết coi như bài tập cho bạn đọc.

Đổi dấu có điều kiện

Giả sử ta có một số nguyên có dấu $x$ và một biến $f$. Nếu $f = 1$ thì ta phải đổi dấu của $x$. Nếu $f = 0$ ta giữ nguyên giá trị $x$.

ConditionalNegationA(signk x, f):
    return $(x\oplus (-f))+f$

 

ConditionalNegationB(signk x, f):
    return $(x-f)\oplus(-f)$

 
Code C:

/*x is negated or not depends on the flag f that only has value 0/1*/
int cond_negationA(int x, int f){
return (x^(-f))+f;
}

int cond_negationB(int x, int f){
return (x-f)^(-f);
}

Ta chứng minh ConditionalNegationA đúng. Nếu $f = 0$, dễ dàng kiểm tra được thủ tục trả lại giá trị $x$. Nếu $f = 1$, ta có:

$\begin{array} {l} (x \oplus -1) +1 & = \overline{x} +1 \qquad (\mbox{ by } (9)) \\ &= -x \qquad (\mbox{ by } (13))\end{array}$

Tương tự như vậy, ta có thể chứng minh được ConditionalNegationB đúng.

Thao tác với bít 1 tận cùng bên phải

Trong phần này, giả sử số nguyên $x$ có biểu diễn nhị phân như sau:

$x = (\alpha 01^a10^b)_2 \qquad (16)$

trong đó $a,b$ có thể bằng $0$. Ta dễ dàng suy ra:

$\begin{array} {l} \overline{x} & = (\overline{\alpha}10^a01^b)_2 \\ x-1 & = (\alpha11^a01^b)_2 \qquad (17) \\ -x &= (\overline{\alpha}10^a10^b)_2 \end{array}$

Từ đó suy ra $\overline{x}+1 = -x = \overline{x-1}$. Ta lại thu được các đẳng thức ở trên.

Một số thao tác với bít 1 tận cùng bên phải quan trọng nên nhớ:

$\begin{array} {l} x\&(x-1) & = (\alpha01^a00^b)_2 \quad \mbox{[Remove the rightmost 1]} \qquad (18) \\ x\&-x &= (0^{|\alpha|}00^a10^b)_2 \quad \mbox{[Extract the rightmost 1]} \qquad (19) \\ x|-x &= (1^{|\alpha|}11^a10^b)_2 \quad \mbox{[Smear the rightmost 1 to the left]} \qquad (20) \\ x\oplus-x &= (1^{|\alpha|}11^a00^b)_2 \quad \mbox{[Remove and smear it to the left]} \qquad (21) \\ x | (x-1) &= (\alpha01^a11^b)_2 \quad \mbox{[Smear the right most 1 to the right]} \qquad (22) \\ x \oplus (x-1) &= (0^{|\alpha|}00^a11^b)_2 \quad \mbox{[Extract and smear it to the right]} \qquad (23) \\ \overline{x} \& (x-1) &= (0^{|\alpha|}00^a01^b)_2 \quad \mbox{[Extract, remove and smear it to the right]} \qquad (24) \end{array}$

Các mặt nạ thần kì

Các mặt nạ "thần kì" (magic mask) được sử dụng rất rất nhiều trong các thủ tục thao tác bít. Các mặt nạ thần kì được định nghĩa như sau:

$\begin{array} {l} \mu_0 & = (\ldots 01010101010101010101010101010101)_2 \\ \mu_1 & = (\ldots 00110011001100110011001100110011)_2 \qquad (25) \\ \mu_2 & = (\ldots 00001111000011110000111100001111)_2 \\ \ldots \ldots \ldots \end{array}$

Về mặt toán học, $\mu_k$ là khai triển nhị phân của $-\frac{1}{2^{2^k}+1}$ do

$(2^{2^k}+1)\mu_k = \mu_k \ll 2^k + \mu_k = (\ldots 1111111)_2 = -1 $

Ở đây, bạn đọc chỉ cần nhớ $\mu_k$ gồm mỗi dãy lặp lại $2^k$ số $0$ liên tiếp sau đó là $2^k$ số 1 liên tiếp.

Các ví dụ (tiếp)

Kiểm tra lũy thừa 2

Thủ tục IsPowerOf2 ở trên kiểm tra xem có tồn tại $y$ sao cho $x = 2^y$ hay không? Dễ thấy, nếu $x = 2^y$ thì $x$ có đúng 1 bít 1. Theo $(18)$, $x\&(x-1)$ sẽ xóa bít 1 tận cùng bên phải. Do đó, $x\&(x-1)$ trả về 0 khi và chỉ khi $x \not= 0$ và $x = 2^y$ với một số $y$ nào đó.

Đếm số bít 1
Cho một số nguyên $x$ (không dấu), đếm số bít $1$ của $x$. Ta có thể thực hiện sử dụng $2k$ thao tác bít trong đó $k$ là số bít biểu diễn của $x$ như sau:

CountOnesA(usgnk $x$):
    $c \leftarrow 0$
    for $i \leftarrow 1$ to $k$
        $c \leftarrow c + (x\&1)$
        $x \leftarrow x \gg_u 1$

 
Code C:

int count_onesA(unsigned int x){
int c  = 0, i = 0;
for(i = 0; i &lt; 32; i++){
c += (x&amp;1);
x  &gt;&gt;= 1;
}
return c;
}

Sử dụng thuật toán trên trong trường hợp $x$ có $32$ bít cần khoảng $96$ phép tính. Để tăng tốc thuật toán, ta sẽ sử dụng một bảng $B$, gọi là bảng nhớ, để tính trước. Giả sử ta có bảng $B[0,1,\ldots, 255]$ trong đó $B[i] = j,$ $0 \leq i \leq 255$ nếu trong biểu diễn nhịn phân cuả $i$ có $j$ bít 1. Nếu bảng $B$ đã được tính sẵn, ta có thể đếm số bít 1 của $x$ trong 11 phép tính như sau:

CountOnesB(usgn32 $x$):
    $B[0,1,\ldots, 255] \leftarrow$ the table as described above
    $c \leftarrow c + B[x\&\mathrm{0x000000ff}]$
    $x \leftarrow x \gg_u 8$
    $c \leftarrow c + B[x\&\mathrm{0x000000ff}]$
    $x \leftarrow x \gg_u 8$
    $c \leftarrow c + B[x\&\mathrm{0x000000ff}]$
    $x \leftarrow x \gg_u 8$
    $c \leftarrow c + B[x\&\mathrm{0x000000ff}]$

 
Code C:

int B[255];// the lookup table for counting bits

void build_lookup_table(){
B[0] = 0; B[1] = 1;
int i = 0, j = 0,s = 0, t = 0;
for( i = 1; i &lt;= 8; i++){
s = 1 &lt;&lt;i; t = (1 &lt;&lt; (i+1))-1;
for(j = s; j &lt;= t; j++){
B[j] =B[j&amp; ((1 &lt;&lt;i)-1)] + (j &gt;&gt; i);
}
}
}
/* Remember to build the lookup table before using this method for counting 1s */
int count_onesB(unsigned int x){
int c  = 0;
c+= B[x&amp;0x000000ff];
x &gt;&gt;= 8;
c+= B[x&amp;0x000000ff];
x &gt;&gt;= 8;
c+= B[x&amp;0x000000ff];
x &gt;&gt;= 8;
c+= B[x&amp;0x000000ff];
return c;
}

Trong trường hợp số lượng bít 1 của $x$ không nhiều, ta có thể sử dụng $(18)$. Cụ thể, nếu $x$ có $t$ bít 1 thì ta chỉ cần $3t$ phép tính.

CountOnesC(usgnk $x$):
    $c \leftarrow 0$
    while $x \not= 0$
        $c \leftarrow c +1$
        $x \leftarrow x \& (x-1)$

 

Code C:

int count_onesC(unsigned int x){
int c  = 0;
while(x != 0){
c++;
x = x&amp;(x-1);
}
return c;
}

Chứng minh thủ tục CountOneC đúng coi như bài tập cho bạn đọc.

Đếm số bít 0 cuối cùng

Cho một số nguyên $x$, gọi $\rho(x)$ là số lượng bít 0 cuối cùng (trailing zeros) của $x$. Tính $\rho(x)$. Ví dụ $x = 120 = (01111000)_2$, ta có $\rho(x) = 3$. Dễ thấy nếu $x$ lẻ thì $\rho(x) = 0$. Ta có công thức đệ quy sau của $\rho(x)$:

$\rho(2x+1) = 0 \qquad \rho(2x) = \rho(x) + 1 \qquad \rho(0) = k$

Trong đó $k$ là số bít biểu diễn của $x$.

Cách đầu tiên ta có thể nghĩ đến là duyệt các bít của $x$ từ trái sang phải để đếm số lượng bít 0 cho đến khi ta bắt gắp bít 1 đầu tiên thì dừng. Cách này cần khoảng $4t$ phép tính trong đó $t$ là số lượng bít 0 cuối cùng. Trong trường hợp tồi nhất, đếm số bít 0 cuối cùng cuả số nguyên không dấu 32 bít cần hơn 150 phép tính. Có thể tăng tốc thuật toán này bằng cách sử dụng bảng nhớ như trên.

Cách khác ta có thể nghĩ tới là sử dụng chia để trị để tìm bít 1 phải nhất. Cách này áp dụng cho mọi số nguyên 32 bít sử dụng 19 phép tính.

DCCountZeros(usgn32 $x$):
    if $x\&1 = 0$
        return 0
    $c \leftarrow 0$
    if $(x\& \mathrm{0x0000ffff}) = 0$
        $c \leftarrow c+ 16$
        $x \leftarrow x \gg_u 16$
    if $(x\& \mathrm{0x000000ff}) = 0$
        $c \leftarrow c+ 8$
        $x \leftarrow x \gg_u 8$
    if $(x\& \mathrm{0x0000000f}) = 0$
        $c \leftarrow c+ 4$
        $x \leftarrow x \gg_u 4$
    if $(x\& \mathrm{0x00000003}) = 0$
        $c \leftarrow c+ 2$
        $x \leftarrow x \gg_u 2$
    $c \leftarrow c + 1 - (x\&1)$
    return $c$

 
Code C:

/* this method doesn't work for x = 0, readers must be careful when using it*/

int dc_count_zeros(unsigned int x){
if( (x&amp;1) != 0){
return 0;
}
int c = 0;
if((x&amp; 0x0000ffff) == 0){
c+= 16;
x &gt;&gt;= 16;
}
if((x &amp; 0x000000ff )== 0){
c+= 8;
x &gt;&gt;= 8;
}
if((x &amp; 0x0000000f) == 0){
c += 4;
x &gt;&gt;= 4;
}
if((x &amp; 0x00000003) == 0){
c += 2;
x &gt;&gt;= 2;
}
c+= 1 - (x&amp;1);
return c;
}

Tính đúng đắn coi như bài tập cho bạn đọc.

Ngoài ra ta có thể sử dụng mặt nạ thần kì để tính $\rho(x)$. Nhận thấy nếu $x = 2^y$ với một số $y$ nào đó thì:

$\rho(x) = [x\&\mu_0 = 0] + 2[x\&\mu_1 = 0] + 4[x\&\mu_2 = 0] + 8 [x\&\mu_3 = 0] + \ldots \quad (26)$

trong đó hàm $[I(x)] = 1$ nếu $I(x)$ là TRUE và $0$ nếu ngược lại. Do đó, để áp dụng $(26)$, ta sẽ áp dụng $(19)$ trước để xóa hết các bít 1 bên trái của bít 1 cuối cùng. Nếp áp dụng phương pháp này cho số nguyên có $32$ bít thì mất cỡ 16 phép tính. Giả mã như sau:

MMCountZeros(usgn32 $x$):
    $x \leftarrow x\&(-x)$         [[$x = 2^y$]]
    $\mu_0 \leftarrow \mathrm{0x55555555}, \mu_1 \leftarrow \mathrm{0x33333333}$
    $\mu_2 \leftarrow \mathrm{0x0f0f0f0f},\mu_3 \leftarrow \mathrm{0x00ff00ff}$
    $\mu_4 \leftarrow \mathrm{0x0000ffff}$
    $c \leftarrow 0$
    if $x \& \mu_4 = 0$
        $c \leftarrow c + 16$
    if $x \& \mu_3 = 0$
        $c \leftarrow c + 8$
    if $x \& \mu_2 = 0$
        $c \leftarrow c + 4$
    if $x \& \mu_1 = 0$
        $c \leftarrow c + 2$
    if $x \& \mu_0 = 0$
        $c \leftarrow c + 1$
    return $c$

 

Code C:

unsigned int m0 = 0x55555555;
unsigned int m1 = 0x33333333;
unsigned int m2 = 0x0f0f0f0f;
unsigned int m3 = 0x00ff00ff;
unsigned int m4 = 0x0000ffff;

/* this method doesn't work for x = 0, readers must be careful when using it*/

int mm_count_zeros(unsigned int x){
x &amp;= -x;
int c  = 0;
if((x&amp;m4) == 0) c+= 16;
if((x&amp;m3) == 0) c+= 8;
if((x&amp;m2) == 0) c+= 4;
if((x&amp;m1) == 0) c+= 2;
if((x&amp;m0) == 0) c+= 1;
return c;
}

Đảo ngược bít

Cho số nguyên $x$, ví dụ $8$ bít $x = (x_7x_6\ldots x_0)_2$, đảo ngược biểu diễn nhị phân của $x$, i.e, trả lại $(x_0x_1\ldots x_7)_2$.

Cách đơn giản nhất là copy các bít của $x$ sang một số $y$ theo thứ tự đảo ngược. Cách này mất $6t$ phép tính trong đó $t$ là vị trí của bít 1 tận cùng bên trái của $x$. Do đó, khi áp dụng cho số 32 bít, trong trường hợp xấu nhất, tổng số thao tác ta cần sử dụng là hơn 180 phép tính.

BitReversalA(usgnk $x$):
    $s \leftarrow k $
    $y \leftarrow 0$
    while $x \not= 0$
        $y \leftarrow y| (x\&1)$
        $y \leftarrow y\ll 1$
        $x \leftarrow x \gg_u 1$
        $s \leftarrow s-1$
    $y \leftarrow y \ll s$
    return $y$

 
Code C:

unsigned int bit_reversalA(unsigned int x){
int s = 32;
unsigned int y = 0;
while( x!= 0){
y &lt;&lt;=1;
y |= (x&amp;1);
x &gt;&gt;= 1;
s--;
}
y &lt;&lt;= s;
return y;
}

Trong bài toàn này ta cũng có thể áp dụng chia để trị. Ví dụ với $x$ có 8 bít như trên, ta có thể đảo ngược các bít của $x$ trong 3 bước như sau:

$\begin{array} {l} x_6x_7x_4x_5x_2x_3x_0x_1 \\ x_4x_5x_6x_7x_0x_1x_2x_3 \\ x_0x_1x_2x_3x_4x_5x_6x_7 \end{array} $

Để thực hiện bước 1, ta sẽ làm như sau:

$\begin{array} {l} (x \gg_u 1)\& \mu_0 &= (0x_70x_50x_30x_1)_2 \\ (x \& \mu_0) \ll 1 &= (x_60x_40x_20x_00)_2 \\ ((x \gg_u 1)\& \mu_0) | ( (x \& \mu_0) \ll 1) &= (x_6x_7x_4x_5x_2x_3x_0x_1)_2 \end{array} $

Tương tự như vậy các bước tiếp theo ta thực hiện với mặt nạ thần kì $\mu_1,\mu_2, \ldots$. Giả mã với $x$ là số nguyên 32 bít không dấu như sau:

BitReversalB(usgn32 $x$):
    $\mu_0 \leftarrow \mathrm{0x55555555}, \mu_1 \leftarrow \mathrm{0x33333333}$
    $\mu_2 \leftarrow \mathrm{0x0f0f0f0f},\mu_3 \leftarrow \mathrm{0x00ff00ff}$
    $\mu_4 \leftarrow \mathrm{0x0000ffff}$
    $x \leftarrow ((x \gg_u 1)\& \mu_0) | ( (x \& \mu_0) \ll 1)$
    $x \leftarrow ((x \gg_u 2)\& \mu_1) | ( (x \& \mu_1) \ll 2)$
    $x \leftarrow ((x \gg_u 4)\& \mu_2) | ( (x \& \mu_2) \ll 4)$
    $x \leftarrow ((x \gg_u 8)\& \mu_3) | ( (x \& \mu_3) \ll 8)$
    $x \leftarrow (x \gg_u 16)| ( x \ll 16)$
    return $x$

 

Code C:

unsigned int m0 = 0x55555555; // a magic mask
unsigned int m1 = 0x33333333; // a magic mask
unsigned int m2 = 0x0f0f0f0f; // a magic mask
unsigned int m3 = 0x00ff00ff; // a magic mask
unsigned int m4 = 0x0000ffff; // a magic mask

unsigned int bit_reversalB(unsigned int x){
x = ((x&gt;&gt;1)&amp;m0)|(((x&amp;m0)&lt;&lt;1));
x = ((x&gt;&gt;2)&amp;m1)|(((x&amp;m1)&lt;&lt;2));
x = ((x&gt;&gt;4)&amp;m2)|(((x&amp;m2)&lt;&lt;4));
x = ((x&gt;&gt;8)&amp;m3)|(((x&amp;m3)&lt;&lt;8));
x = (x &gt;&gt; 16)|(x &lt;&lt; 16);
return x;
}

Dễ thấy sử dụng các mặt nạ thần kì, ta có thể đảo bít số nguyên 32 bít trong $23$ phép tính và số nguyên 64 bít trong 26 phép tính. Chứng minh tính đúng đắn của thủ tục trên coi như bài tập cho bạn đọc. Trong ví dụ tiếp theo, ta sẽ áp dụng phương pháp đổi chỗ hai bít để đảo bít số nguyên 64 bít sử dụng 26 phép tính.

Đổi chỗ hai bít

Cho số nguyên không dấu $x = (x_k\ldots x_i \ldots x_j \ldots x_0)_2$, đổi chỗ hai bít $x_i$ và $x_j$ của $x$ ($j$ < $i$).

Đặt $\delta = i - j$, ta tiến hành đổi chỗ như sau:

SwapBitsA(usgnk $x, i, j$):
    $\delta \leftarrow i - j$
    $y \leftarrow (x \gg_u \delta)\& 2^j$
    $z \leftarrow (x \ll \delta)\& 2^i$
    $m \leftarrow \overline{2^i | 2^j}$
    $x \leftarrow (x\&m) | y | z$
    return $x$

 
Code C:

/* Swap x_i and x_j (32 &gt; i &gt; j) of an unsigned integer x
* Note x = x_31 x_30 .... x_1 x_0*/
unsigned int swap_bitsA(unsigned int x, int i, int j){
int delta = i - j;
unsigned int y = (x &gt;&gt; delta)&amp; (1 &lt;&lt; j);
unsigned int z = (x &lt;&lt; delta)&amp;(1&lt;&lt; i);
unsigned int m = ~((1&lt;&lt;i) | (1&lt;&lt;j));
x = (x&amp;m)|y|z;
return x;
}

Trong giả mã trên, bước đầu tiên, ta tách $x_i$ ra khỏi $x$ và đặt vào vị trí thứ $j$. Sau đó, ta tách $x_j$ ra khỏi $x$ và đặt vào bít thứ $i$. Mặt nạ $m$ ở trên có tất cả các bít là 1 ngoại trừ bít thứ $i$ và $j$. Suy ra, $x\&m$ sẽ thiết lập bít $x_i$ và $x_j$ về 0. Như vậy, kết quả cuối cùng sẽ là $(x\&m )| y | z$.

Chú ý trong ví dụ đổi chỗ hai số nguyên không dùng biến thứ 3, ta sử dụng phép XOR. Ở bài toán này, ta có thể thực hiện ý tưởng tương tự.

SwapBitsB(usgnk $x, i, j$):
    $\delta \leftarrow i - j$
    $y \leftarrow (x \oplus (x \gg_u \delta))\& 2^j$
    $x \leftarrow x \oplus y \oplus (y \ll \delta)$
    return $x$

 
Code C:

/* Swap x_i and x_j (32 &gt; i &gt; j) of an unsigned integer x
* Note x = x_31 x_30 .... x_1 x_0*/

unsigned int swap_bitsB(unsigned int x, int i, int j){
int delta = i - j;
unsigned int y = (x ^(x&gt;&gt;delta))&amp;(1&lt;&lt;j);
x = x^y^(y&lt;&lt; delta);
return x;
}

Đầu tiên ta tính $x_i \oplus x_j$ và đưa kết qủa vào vị trí thứ $j$. sau đó đổi $x_i$ thành $x_i \oplus (x_i \oplus x_j) = x_j$ và $x_j$ thành $x_j \oplus (x_i \oplus x_j) = x_i$. Dễ thấy cách này sử dụng ít thao tác bít hơn cách ở trên.

Ý tưởng của thuật toán SwapBitsB còn có thể được dùng để đổi chỗ vài cặp bít cùng một lúc (giống như cách chúng ta đảo bít ở trên). Thông thường, để thực hiện đảo nhiều cặp bít, ta sẽ dùng mặt nạ $\theta$ thay vì $2^j$ như trên.

$\begin{array} {l} y & \leftarrow (x \oplus (x \gg_u \delta))\&\theta \\ x &\leftarrow x \oplus y \oplus (y \ll \delta)\end{array} \qquad (27)$

Ta sẽ gọi thủ tục này là $\delta$-swap. Sử dụng $\delta$-swap, ta có thể thực hiện đảo bít số nguyên 64 bít không dấu trong 26 phép tính (thay vì 28) như sau:

BitReversalC(usgn32 $x$):
    $\mu_0 \leftarrow \mathrm{0x55555555}$
    $x \leftarrow ((x \gg_u 1)\& \mu_0) | ( (x \& \mu_0) \ll 1)$
    $y \leftarrow (x \oplus(x \gg_u 4))\& \mathrm{0x0300c0303030c303}$
    $x \leftarrow x \oplus y \oplus (y \ll 8)$
    $y \leftarrow (x \oplus(x \gg_u 4))\& \mathrm{0x00c0300c03f0003f}$
    $x \leftarrow x \oplus y \oplus (y \ll 8)$
    $y \leftarrow (x \oplus(x \gg_u 20))\& \mathrm{0x00000ffc00003fff}$
    $x \leftarrow x \oplus y \oplus (y \ll 20)$
    $x \leftarrow (x \gg_u 34) | (x \ll 30)$
    return $x$

 

Chi tiết chứng minh tính đúng đắn của thuật toán trên coi như bài tập cho bạn đọc.

Code: bit-tricks-all-in-one

Tham khảo

[1] Donald E. Knuth. The Art of Computer Programming, Volume 4, Fascicle 1: Bitwise Tricks & Techniques; Binary Decision Diagrams (12th ed.). Addison-Wesley Professional, 2009.
[2] http://graphics.stanford.edu/~seander/bithacks.html
[3] Warren, Henry S. Hacker's Delight. Pearson Education, 2012.

Facebook Comments

Tags: , ,