位運算簡介及實用技巧(二):進階篇(1)

轉載連接:http://www.matrix67.com/blog/archives/264算法

=====   真正強的東西來了!   =====編程

二進制中的1有奇數個仍是偶數個
    咱們能夠用下面的代碼來計算一個32位整數的二進制中1的個數的奇偶性,當輸入數據的二進制表示裏有偶數個數字1時程序輸出0,有奇數個則輸出1。例如,1314520的二進制101000000111011011000中有9個1,則x=1314520時程序輸出1。
var
   i,x,c:longint;
begin
   readln(x);
   c:=0;
   for i:=1 to 32 do
   begin
      c:=c + x and 1;
      x:=x shr 1;
   end;
   writeln( c and 1 );
end.

    但這樣的效率並不高,位運算的神奇之處尚未體現出來。
    一樣是判斷二進制中1的個數的奇偶性,下面這段代碼就強了。你能看出這個代碼的原理嗎?
var
   x:longint;
begin
   readln(x);
   x:=x xor (x shr 1);
   x:=x xor (x shr 2);
   x:=x xor (x shr 4);
   x:=x xor (x shr 8);
   x:=x xor (x shr 16);
   writeln(x and 1);
end.

    爲了說明上面這段代碼的原理,咱們仍是拿1314520出來講事。1314520的二進制爲101000000111011011000,第一次異或操做的結果以下:函數

    00000000000101000000111011011000
XOR  0000000000010100000011101101100
—————————————
    00000000000111100000100110110100測試

    獲得的結果是一個新的二進制數,其中右起第i位上的數表示原數中第i和i+1位上有奇數個1仍是偶數個1。好比,最右邊那個0表示原數末兩位有偶數個1,右起第3位上的1就表示原數的這個位置和前一個位置中有奇數個1。對這個數進行第二次異或的結果以下:編碼

    00000000000111100000100110110100
XOR   000000000001111000001001101101
—————————————
    00000000000110011000101111011001code

    結果裏的每一個1表示原數的該位置及其前面三個位置中共有奇數個1,每一個0就表示原數對應的四個位置上共偶數個1。一直作到第五次異或結束後,獲得的二進制數的最末位就表示整個32位數裏有多少個1,這就是咱們最終想要的答案。blog

計算二進制中的1的個數
    一樣假設x是一個32位整數。通過下面五次賦值後,x的值就是原數的二進制表示中數字1的個數。好比,初始時x爲1314520(網友抓狂:能不能換一個數啊),那麼最後x就變成了9,它表示1314520的二進制中有9個1。
x := (x and $55555555) + ((x shr 1) and $55555555); 
x := (x and $33333333) + ((x shr 2) and $33333333); 
x := (x and $0F0F0F0F) + ((x shr 4) and $0F0F0F0F); 
x := (x and $00FF00FF) + ((x shr 8) and $00FF00FF); 
x := (x and $0000FFFF) + ((x shr 16) and $0000FFFF);

    爲了便於解說,咱們下面僅說明這個程序是如何對一個8位整數進行處理的。咱們拿數字211(咱們班某MM的生日)來開刀。211的二進制爲11010011。遞歸

+—+—+—+—+—+—+—+—+
| 1 | 1 | 0 | 1 | 0 | 0 | 1 | 1 |   <—原數
+—+—+—+—+—+—+—+—+
|  1 0  |  0 1  |  0 0  |  1 0  |   <—第一次運算後
+——-+——-+——-+——-+
|    0 0 1 1    |    0 0 1 0    |   <—第二次運算後
+—————+—————+
|        0 0 0 0 0 1 0 1        |   <—第三次運算後,得數爲5
+——————————-+get

    整個程序是一個分治的思想。第一次咱們把每相鄰的兩位加起來,獲得每兩位裏1的個數,好比前兩位10就表示原數的前兩位有2個1。第二次咱們繼續兩兩相加,10+01=11,00+10=10,獲得的結果是00110010,它表示原數前4位有3個1,末4位有2個1。最後一次咱們把0011和0010加起來,獲得的就是整個二進制中1的個數。程序中巧妙地使用取位和右移,好比第二行中$33333333的二進制爲00110011001100….,用它和x作and運算就至關於以2爲單位間隔取數。shr的做用就是讓加法運算的相同數位對齊。it

二分查找32位整數的前導0個數
    這裏用的C語言,我直接Copy的Hacker's Delight上的代碼。這段代碼寫成C要好看些,寫成Pascal的話會出現不少begin和end,搞得代碼很難看。程序思想是二分查找,應該很簡單,我就不細說了。
int nlz(unsigned x)
{
   int n;

   if (x == 0) return(32);
   n = 1;
   if ((x >> 16) == 0) {n = n +16; x = x <<16;}
   if ((x >> 24) == 0) {n = n + 8; x = x << 8;}
   if ((x >> 28) == 0) {n = n + 4; x = x << 4;}
   if ((x >> 30) == 0) {n = n + 2; x = x << 2;}
   n = n - (x >> 31);
   return n;
}

只用位運算來取絕對值
    這是一個很是有趣的問題。你們先本身想一想吧,Ctrl+A顯示答案。
    答案:假設x爲32位整數,則x xor (not (x shr 31) + 1) + x shr 31的結果是x的絕對值
    x shr 31是二進制的最高位,它用來表示x的符號。若是它爲0(x爲正),則not (x shr 31) + 1等於$00000000,異或任何數結果都不變;若是最高位爲1(x爲負),則not (x shr 31) + 1等於$FFFFFFFF,x異或它至關於全部數位取反,異或完後再加一。

高低位交換
    這個題其實是我出的,作爲學校內部NOIp模擬賽的第一題。題目是這樣:

    給出一個小於2^32的正整數。這個數能夠用一個32位的二進制數表示(不足32位用0補足)。咱們稱這個二進制數的前16位爲「高位」,後16位爲「低位」。將它的高低位交換,咱們能夠獲得一個新的數。試問這個新的數是多少(用十進制表示)。
  例如,數1314520用二進制表示爲0000 0000 0001 0100 0000 1110 1101 1000(添加了11個前導0補足爲32位),其中前16位爲高位,即0000 0000 0001 0100;後16位爲低位,即0000 1110 1101 1000。將它的高低位進行交換,咱們獲得了一個新的二進制數0000 1110 1101 1000 0000 0000 0001 0100。它便是十進制的249036820。

 

    當時幾乎沒有人想到用一句位操做來代替冗長的程序。使用位運算的話兩句話就完了。
var
   n:dword;
begin
   readln( n );
   writeln( (n shr 16) or (n  shl 16) );
end.

    而事實上,Pascal有一個系統函數swap直接就能夠用。

二進制逆序
    下面的程序讀入一個32位整數並輸出它的二進制倒序後所表示的數。

    輸入: 1314520    (二進制爲00000000000101000000111011011000)
    輸出: 460335104  (二進制爲00011011011100000010100000000000)
var
   x:dword;
begin
   readln(x);
   x := (x and $55555555) shl  1 or (x and $AAAAAAAA) shr  1;
   x := (x and $33333333) shl  2 or (x and $CCCCCCCC) shr  2;
   x := (x and $0F0F0F0F) shl  4 or (x and $F0F0F0F0) shr  4;
   x := (x and $00FF00FF) shl  8 or (x and $FF00FF00) shr  8;
   x := (x and $0000FFFF) shl 16 or (x and $FFFF0000) shr 16;
   writeln(x);
end.

    它的原理和剛纔求二進制中1的個數那個例題是大體相同的。程序首先交換每相鄰兩位上的數,之後把互相交換過的數當作一個總體,繼續進行以2位爲單位、以4位爲單位的左右對換操做。咱們再次用8位整數211來演示程序執行過程:
+---+---+---+---+---+---+---+---+
| 1 | 1 | 0 | 1 | 0 | 0 | 1 | 1 |   <---原數
+---+---+---+---+---+---+---+---+
|  1 1  |  1 0  |  0 0  |  1 1  |   <---第一次運算後
+-------+-------+-------+-------+
|    1 0 1 1    |    1 1 0 0    |   <---第二次運算後
+---------------+---------------+
|        1 1 0 0 1 0 1 1        |   <---第三次運算後
+-------------------------------+

-------------------------------------------------------------------------------------------------------------------------------------

n皇后問題位運算版(sgu題目連接

    n皇后問題是啥我就不說了吧,學編程的確定都見過。下面的十多行代碼是n皇后問題的一個高效位運算程序,看到過的人都誇它牛。初始時,upperlim:=(1 shl n)-1。主程序調用test(0,0,0)後sum的值就是n皇后總的解數。拿這個去交USACO,0.3s,暴爽。
procedure test(row,ld,rd:longint);
var
      pos,p:longint;
begin

{ 1}  if row<>upperlim then
{ 2}  begin
{ 3}     pos:=upperlim and not (row or ld or rd);
{ 4}     while pos<>0 do
{ 5}     begin
{ 6}        p:=pos and -pos;
{ 7}        pos:=pos-p;
{ 8}        test(row+p,(ld+p)shl 1,(rd+p)shr 1);
{ 9}     end;
{10}  end
{11}  else inc(sum);

end;

    乍一看彷佛徹底摸不着頭腦,實際上整個程序是很是容易理解的。這裏仍是建議你們本身單步運行一探究竟,實在沒研究出來再看下面的解說。

  
    和普通算法同樣,這是一個遞歸過程,程序一行一行地尋找能夠放皇后的地方。過程帶三個參數,row、ld和rd,分別表示在縱列和兩個對角線方向的限制條件下這一行的哪些地方不能放。咱們以6x6的棋盤爲例,看看程序是怎麼工做的。假設如今已經遞歸到第四層,前三層放的子已經標在左圖上了。紅色、藍色和綠色的線分別表示三個方向上有衝突的位置,位於該行上的衝突位置就用row、ld和rd中的1來表示。把它們三個並起來,獲得該行全部的禁位,取反後就獲得全部能夠放的位置(用pos來表示)。前面說過-a至關於not a + 1,這裏的代碼第6行就至關於pos and (not pos + 1),其結果是取出最右邊的那個1。這樣,p就表示該行的某個能夠放子的位置,把它從pos中移除並遞歸調用test過程。注意遞歸調用時三個參數的變化,每一個參數都加上了一個禁位,但兩個對角線方向的禁位對下一行的影響須要平移一位。最後,若是遞歸到某個時候發現row=111111了,說明六個皇后全放進去了,此時程序從第1行跳到第11行,找到的解的個數加一。

    ~~~~====~~~~=====   華麗的分割線   =====~~~~====~~~~

Gray碼
    假如我有4個潛在的GF,我須要決定最終到底和誰在一塊兒。一個簡單的辦法就是,依次和每一個MM交往一段時間,最後選擇給我帶來的「滿意度」最大的MM。但看了dd牛的理論後,事情開始變得複雜了:我能夠選擇和多個MM在一塊兒。這樣,須要考覈的狀態變成了2^4=16種(固然包括0000這一狀態,由於我有多是玻璃)。如今的問題就是,我應該用什麼順序來遍歷這16種狀態呢?
    傳統的作法是,用二進制數的順序來遍歷全部可能的組合。也就是說,我須要以0000->0001->0010->0011->0100->...->1111這樣的順序對每種狀態進行測試。這個順序很不科學,不少時候狀態的轉移都很耗時。好比從0111到1000時我須要暫時甩掉當前全部的3個MM,而後去把第4個MM。同時改變全部MM與個人關係是一件何等巨大的工程啊。所以,我但願知道,是否有一種方法可使得,從沒有MM這一狀態出發,每次只改變我和一個MM的關係(追或者甩),15次操做後剛好遍歷完全部可能的組合(最終狀態不必定是1111)。你們本身先試一試看行不行。
    解決這個問題的方法很巧妙。咱們來講明,假如咱們已經知道了n=2時的合法遍歷順序,咱們如何獲得n=3的遍歷順序。顯然,n=2的遍歷順序以下:

00
01
11
10

    你可能已經想到了如何把上面的遍歷順序擴展到n=3的狀況。n=3時一共有8種狀態,其中前面4個把n=2的遍歷順序照搬下來,而後把它們對稱翻折下去並在最前面加上1做爲後面4個狀態:

000
001
011
010  ↑
--------
110  ↓
111
101
100

    用這種方法獲得的遍歷順序顯然符合要求。首先,上面8個狀態剛好是n=3時的全部8種組合,由於它們是在n=2的所有四種組合的基礎上考慮選不選第3個元素所獲得的。而後咱們看到,後面一半的狀態應該和前面一半同樣知足「相鄰狀態間僅一位不一樣」的限制,而「鏡面」處則是最前面那一位數不一樣。再次翻折三階遍歷順序,咱們就獲得了剛纔的問題的答案:

0000
0001
0011
0010
0110
0111
0101
0100
1100
1101
1111
1110
1010
1011
1001
1000

    這種遍歷順序做爲一種編碼方式存在,叫作Gray碼(寫個中文讓蜘蛛來抓:格雷碼)。它的應用範圍很廣。好比,n階的Gray碼至關於在n維立方體上的Hamilton迴路,由於沿着立方體上的邊走一步,n維座標中只會有一個值改變。再好比,Gray碼和Hanoi塔問題等價。Gray碼改變的是第幾個數,Hanoi塔就該移動哪一個盤子。好比,3階的Gray碼每次改變的元素所在位置依次爲1-2-1-3-1-2-1,這正好是3階Hanoi塔每次移動盤子編號。若是咱們能夠快速求出Gray碼的第n個數是多少,咱們就能夠輸出任意步數後Hanoi塔的移動步驟。如今我告訴你,Gray碼的第n個數(從0算起)是n xor (n shr 1),你能想出來這是爲何嗎?先本身想一想吧。

    下面咱們把二進制數和Gray碼都寫在下面,能夠看到左邊的數異或自身右移的結果就等於右邊的數。

二進制數   Gray碼
   000       000
   001       001
   010       011
   011       010
   100       110
   101       111
   110       101
   111       100

    從二進制數的角度看,「鏡像」位置上的數便是對原數進行not運算後的結果。好比,第3個數010和倒數第3個數101的每一位都正好相反。假設這兩個數分別爲x和y,那麼x xor (x shr 1)和y xor (y shr 1)的結果只有一點不一樣:後者的首位是1,前者的首位是0。而這正好是Gray碼的生成方法。這就說明了,Gray碼的第n個數確實是n xor (n shr 1)。

二進制轉格雷碼:

從最右邊一位起,依次將每一位與左邊一位異或(XOR),做爲對應格雷碼該位的值,最左邊一位不變(至關於左邊是0);

 

G:格雷碼 B:二進制碼     G(N) = B(n+1) XOR B(n)

 

從左邊第二位起,將每位與左邊一位解碼後的值異或,做爲該位解碼後的值(最左邊一位依然不變)。

二進制碼第n位 = 二進制碼第(n+1)位+格雷碼第n位,由於二進制碼和格雷碼皆有相同位數,因此二進制碼可從最高位的左邊位元取0,以進行計算


    今年四月份 mashuo 給我看了 這道題 ,是二維意義上的Gray碼。題目大意是說,把0到2^(n+m)-1的數寫成2^n * 2^m的矩陣,使得位置相鄰兩數的二進制表示只有一位之差。答案其實很簡單,全部數都是由m位的Gray碼和n位Gray碼拼接而成,須要用左移操做和or運算完成。完整的代碼以下: 
var
   x,y,m,n,u:longint;
begin
   readln(m,n);
   for x:=0 to 1 shl m-1 do begin
      u:=(x xor (x shr 1)) shl n; //輸出數的左邊是一個m位的Gray碼
      for y:=0 to 1 shl n-1 do
         write(u or (y xor (y shr 1)),' '); //並上一個n位Gray碼
      writeln;
   end;
end.

Gray碼解決"九連環問題":每次操做只容許更改1個環的狀態,第k個環可更改狀態的前提是前k-1個環都被取下且第k+1個環未被取下,第一個環可隨意更改狀態。

Gray碼自後向前第i爲表明第i個換的狀態(1表明未解開0表明解開),從a到b狀態最少須要B[a]-B[b]步操做。

題目:poj 1090 Chain   poj 1832 連環鎖

 

Matrix67原創轉貼請註明出處

相關文章
相關標籤/搜索