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

原文:http://www.matrix67.com/blog/archives/266php

 

今天咱們來看兩個稍微複雜一點的例子。html

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

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

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的遍歷順序以下:3d

00
01
11
10code

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

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)。

&nbsp
;   今年四月份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.

Matrix67原創轉貼請註明出處

相關文章
相關標籤/搜索