1. 程式人生 > >華容道搜尋演算法研究

華容道搜尋演算法研究

轉載地址已經廢棄,這裡做個備份。

作者: 許劍偉 2006五一節於福建莆田十中

轉載:http://www.fjptsz.com/xxjs/xjw/rj/110.htm
一、演算法的由來:
也不知寫了多少行程式,累了,寫點輕鬆的吧,可千萬不要和作業系統打交了!還有那該死的Socket,真討厭,什麼“緩衝區不足”,我不是有2G記憶體,怎麼就不足了?
志雄正潛心研究24點演算法,向我要全排列的演算法,我給他一個利用堆疊解決方法,寥寥幾行,志雄叫好。看他挺專心的,於是我也加入他的24點專案。花了一個晚上考慮演算法,第二天早上,完全實現了我的演算法,用時0.75秒,解出1900道24點題目的完全解,比較滿意。第二天再和李雄討論演算法並最後定稿。後來李志雄和我談起華容道的問題。想來頗為感慨,大概6年前,林清霞老師給我“華容道”時,我花費了整整一個下午時間,也沒能走出來,一惱火,開啟電腦程式設計來解。用TC2.0編寫一個程式,花了一天,終於寫好,並得到結果。可如今再提“華容道”時,我竟然對當時的演算法很模糊,我知道寫那種程式有一定難度,可多年後,在我更加熟悉程式設計之後,怎麼突然沒頭緒了,只記得當時寫象棋程式花費了不少時間,華容道應是小問題,當時的我到底有用了什麼招術?我想,我應再闖“華容道”。
可是要用什麼演算法呢?在網路上找了很久,看了幾篇,沒找到我滿意的,仔細分析,這些演算法效率不行,我不想採用。於是我又坐下來考慮演算法,3個小時過去了,終於有眉目了。
本文演算法可在40毫秒內解出“橫刀立馬”(P2.4G),其它棋局耗時略有不同。本文程式利用雜湊技術優化後速度提高3倍,約12ms/題。
二、棋局:
橫刀立馬
橫刀立馬布局

圖中棋子共10個,滑動棋子,把曹操移正下方出口。
有數學家指出,此問題單靠數學方法很難求解。
“華容道”開局佈陣有數百種,以上僅是一種。
三、前人研究:
引網文:“華容道”是世界著名的智力遊戲。在國外和魔方、獨粒鑽石並列,被譽為”智力遊戲界三大不可思議”並被編入學校的教科書。日本藤村幸三朗曾在《數理科學》雜誌上發表華容道基本佈局的最少步法為85步。後來清水達雄找出更少的步法為83步。美國著名數學家馬丁·加德納又進一步把它減少為81步。此後,至今還未曾見到打破這一記錄的報道。

網路上可找到幾個有效的演算法例程,一個是PASCAL的,一個是VB的,一個是C的,還有一個針對手機的java原始碼,都指明使用廣度優先演算法及一些剪枝辦法。但演算法效率仍然不高。天津師範大李學武《華容道遊戲的搜尋策略》說到使用雙向搜尋可提高效率,但本文未採用這種方法,我覺得目標結點不好選擇。有篇文章說對稱的節點可以不搜尋,想了想確實有道理,本文采用了。後來又在網路上找到幾個華容道遊戲程式,其中李智廣的程式效率較高(V2.0) ,本想細仔研究它,可是很遺憾,未能找到它的演算法說明,只好自已動手設計演算法,經過2天努力,本文的搜尋效率已遠遠超過它,足以證實演算法的有效性。
四、演算法:
(一)、廣度優先搜尋:這裡簡單介紹,不明白的話自己查查圖、樹相關資料吧。
一個盤面狀態理解為一個節點,在程式設計時表示節點的方法是多樣的,可用一串數字來表示盤面狀態節點,通過壓縮處理,甚至可用一個int32整型數來表示一個節點。
首先考查起始盤面(節點)的所有走法,然後逐一試走,設當前有n1種走法,則生成n1個兒子節點。
接下來,考查這n1個兒子節點的所有走法,並分別試走,生成n2個孫子節點。
接下來,考查這n2個孫子節點的所有走法,並分別試走,生成n3個曾孫節點。
再接下,就不多說了,依上迴圈,一代一代的往下生成節點,直到找到目標節點。
以上搜索思想樸素、簡單,這也正是程式設計所需要的!可是擺在我們面前的問題有兩個: a、代代生成子節點,將會有很多個節點,如何存取這些節點呢,也就是說如何有序的排放節點而不至於造成混亂。b、程式大概結構應是怎樣的。

第1個問題可這樣解決:設第一代節點放在A[1]中,第二代節點放在A[2]中,第三代節點放在A[3]……注意A[1]中含有多個節點,A[2]也是這樣的……。
第2個問題可用以下虛擬碼解決:
//———————
展開首節點得所有兒子節點A[1]
for( i=1;i<=n層;I++){ //查詢n代(層)
P1=A[i],P2=A[i+1]
for(j=1;j<=P1內節點個數;j++){
B=P1[j] //讀取P中的第j個節點
檢查B是否為目標節點,如果是,結束搜尋
展開B並將所有節點追加到P2中 //P2為P1下一代的節點集
}
}
//———————
以上程式碼基本上給出了搜尋演算法,這種搜尋本質上是廣度優先演算法。接下個我們來優化這個程式。
把第一代兒子節點放在A[1]中,那麼A[1]要有多大空間來放節點所,顯然第一代只需能放10個節點就夠了,因為最多可能的走步不會超過10步,那第二代呢,肯定就多了,第三代還會更多……,每代所需空間都不一樣,那我們要如何分配空間,如果使用javascript、PHP等指令碼語言來程式設計,記憶體空間分配問題基本不用管,但用C語言呢。假如每代最多10000個節點,然後,您想搜尋200代,為了簡化程式,您可以簡單的分配一個200*10000即可解決問題。現在電腦記憶體很多,用這些空間雖不算奢侈,並且會取得很高的搜尋速度,但本著求精、節約的精神,有必要優化A[][]陣列問題。基本思想方法就是將這個二維陣列壓入一個一維陣列中去。這個特殊的一維資料,稱為隊。隊和陣列還有些區別,構成一個隊一般還需要隊頭指標和隊尾指標,當我們讀寫節點資料時,高度有序的移動這兩個指標進行存取節點而不至於混亂。
偽程式中看到,第一代n1個兒子節點放在A[1]中,第二代放在A[2]中,這時A[1]中的很多空間就浪費了,不妨這樣吧,讀第一代節點時,把生成的第二代節點資料接在A[1]中最後一個節點之後,當第一代讀完時,下一個就是第二代了;讀第二代時,生成第三代節點,同樣第三代也接往A[1]裡的最後一節點之後,讀的過程稱出隊,寫過程過程稱為入隊。我們統一從隊頭開始讀(出隊),隊尾處開始寫(入隊)。由於搜尋時是一代代有序往下搜尋,則隊裡的節點也是一代一代的往下接。
為了有序進行,讀取兒子節點時,我們將隊頭指標指向兒子節點開始處,然後讀取節點,讀完後隊頭指標往後移動一步,當移動n1次後,就讀完n1個兒子節點。在讀兒子節點的過程中,我們同時試走兒子節點,生成孫子節點併入隊。如此過程,在隊頭指標讀完兒子節點後,隊頭指標就會指向第一個孫子節點上。虛擬碼如下一步
//———————
展開首節點A得所有兒子節點D陣列(隊)中
P=1,P2=最後一個; //P指向D的第一個(隊頭指標),P2指向D的最後一個(隊尾指標)
for(i=1;i<=n層;I++){ //查詢n代(層)
k=P2-P //當前層節點個數
for(j=1;j<=k;j++){
B=D[P] //讀取D中的第P個節點
檢查B是否為目標節點,如果是,結束搜尋
展開B並將所有節點追加到D[P2]中
P++,P2+=B展開的個數
}
}
//———————

剪枝問題:
第n層(代)的某一節點M,往前試走一步生成Q,當然Q就是n+1層節點。Q有沒有可能同以前走過的節點相同呢,當然有可能,走象棋時很明顯,不同走法可能產生相同結果!設以前的走過節點都沒有重複,Q不可能與小於n-1層的節點重複,如果重複會有什麼會結果?Q到M只一步,M到Q也只需一步,Q與n-2層重複,則Q為n-2層而不是n+1層,Q可生成M,M就會在n-2+1=n-1層出現過,這時n和n-1層都有M,與題設矛盾。因此,每走一步,一直往前查到n-1層,如果Q沒有重複即為新生節點,否則應剪枝(即這樣的節點不能入隊)。剛才說“往前查”,即使只限定在n-1層之後一個一個查,肯定還是慢,怎麼辦能,乾脆每生成一個節點,就對這個節點建立索引,以後哪怕節點有萬億個也不怕。如何建索引表,下文再敘。
再細想,單是以上演算法還是不夠快。如:父親A在移動曹操時,生了兒子P。兒子P生孫子時也移動曹操得到R,從棋局中發現這時R和A是同一節點,即父親A等於孫子P,這是明顯的節點重複,為這樣的R去檢查重複也是浪費時間。因此,發現要移動的棋子與父節點當時移動的子是同一個棋子就別試走,事實證明這樣可少測試節點達1/3,使遍歷速度提高20%。華容道棋局與象棋棋局不同,連續走兩步移動同一個子,那麼第二步是多餘的,如果你要追求數學上的證明就自己動手證明吧。
我們還可進一步優化,某一盤面A1必存在與之左右對稱的的盤面A2,目標節點M1必存在與之左右對稱的盤面節點M2,設兩節點最短路徑為min(點1,點2),則min(A1,M1)==min(A2,M2),當M1為目標結點並且M2也為目標節點時,搜尋A1和搜A2得到的最優結果是等價的,只需搜結果A1。華容道只要求曹操從正下方移出,所以M1,M2都是目標節點,因此,搜尋了A1就沒必要搜尋其對稱節點A2,這樣可使程式效率提高近一倍。
通過以上剪枝優化,生成的樹已接近最小樹。
回朔問題:
當我寫完程式時,發現把曹操移出後,卻不知道是具體的移動過程,也就是說不知道具體的路徑。原因在哪裡呢?虛擬碼中沒有記錄走過的路徑,當找到目標節點卻不知道是如何走來的,因此產生兒子節點的過程中還應告訴兒子:它的父親是誰。當我們得到目標結點後,我們就問目標結點:你的父親是誰,然後我們找其父,再問:你的父親是誰,如此一層一層往上追問,最後就知道了全路徑。這個過程我常稱“回溯”(也許不準確)。
(二)上文提到索引,如何索引。要解決索引問題,可能有很多種方法,首先想到的是使用雜湊表的辦法,雜湊表是棋類遊戲常用的方法,演算法原理不難,不過實現起來也挺麻煩的,使用雜湊表時一般使用隨機數來建立索引,因此一定要選擇有足夠雜湊度隨機數(或準隨機演算法),以免造成大量雜湊衝突而降底效率。以下介紹的方法是通過編碼及查表方法來完成節點索引,建立一種不會發生重複的索引。總之,就是要建立一個索引,雜湊表是個有重複的索引(碰到衝突的一般要做二次雜湊),編碼方法是建立一個無重複索引。本文講述的的編碼方法得到的速度不會很快,如果你追求速度,最好使用雜湊表建立索引,並且在計算雜湊值時採用增量技術,這樣計算索引號的速度可提高10至20倍,程式在10ms內可搜尋出最優解。
盤面的狀態(節點)數量是十分有限的,狀態總數不會超過50萬種。(橫刀立馬為例)
曹操的走法只有12種,任你如何排放,只有12種,不是20種。
橫將(關羽)的排法最多隻有11種
接下來對4個豎將排列(組合),排列第一個豎將的排法最多10種,第二個8種,第三個6種,第四個4種。組合數是10*8*6*4/4!=80,後來為來程式設計方便,做了更多冗於,組合數用C10取4,即C(10,4)=10*9*8*7/4!=210,這樣,4個豎將的某一排列組合必對應0—209中的一個數,這個數就是我們所要的豎將組合編碼值。
同理小兵的組合為C(6,4)=15,編碼範圍在0—14
因此對這4種(10個)棋子全排列,種數最多為12*11*210*15=415800,即4百多K。
最後易得盤面編碼:各種棋子的編碼值乘以碼權,然後取和。
碼權只需遵照排列規律,隨你定,是比較簡單的。可設兵的碼權為1,豎將則是15,橫將則為15*210,曹操為15*210*11。
要如何對各種棋子高速有效的編碼呢?如“橫刀立馬”開局,如何編碼?
這又變成一個組合問題。
我們一個一個的排放“橫刀立馬”棋子並演示編碼過程。
曹操有12個可排放位置,這12個位置編號為0-11,曹操位置在1,注意,首個是0。
關羽有11個可排放位置,這11個位置編號為0-10,關羽位置在1個。
豎將有10個可排放的位置,編號為0-9,一將是0,二將是1,三將是4,四將是5。
小兵有6個可排放的位置,編號為0-5,一兵是0,二兵是1,三兵是2,四兵是5。
豎將編號序列為0,1,4,5,這一組合對應的組合序號(編碼)是多少呢,如何定義?真還有點不好處理,有人說這與群論有關。我不太清楚,我就用了一些笨辦法解決問題。0,1,4,5表示的是各個將的位置,豎將在位用1表示,不在位用0表示,則0,1,4,5可示意為11001100000,這不就成了二進位制數,不妨把0145轉為二進數,用查表法轉換是很快的,只需4個加法語名即可完成,再用這個二進數當作陣列的下標來查組合的編號表,從表中得到它的編號,設表為Sb,則編號值為Sb[11001100000]或Sb[816],這樣就完成了高速編碼。這個編號表如何建立呢?這也好辦,事前把0000000000—1111111111共1024個數中含4個1的數按順序編號,其中只有C(10,4)=210個數被編號,其餘不用。由此建立一個1024長的有序表Sb[],表中位置下標含4個1的被編號,共210個。
豎將編碼過程表示為:0145=>1100110000=>Sb[100110000]即Sb[816]
小兵同樣方式編碼0125=>111001=>Bb[111001]即Bb[57]

上述,編碼後盤面總數最多為415800,當我們記錄每一個節點是否已經遍歷時,最多隻需415800個位元組,如果是廣度搜索,還可按位元方法壓縮8倍,只需415800/8=51975個位元組,現在的計算機,記憶體很存都很強,隨便一個new,就可得幾兆記憶體,幾百兆也沒問題,哪裡在乎這4百多K,跟本無需壓縮,壓縮也是在浪費時間。
有了上述排列組合的關係,便可很輕鬆的寫一個編碼函式,從而建立與節點相關的表或索引表等。如,可用編號做為陣列的下標來詢址,找到相應的節點記錄。這樣速度就會很快,在檢查節點是否已經遍歷過的時候,無需一個一個的往前查!速度要快幾十倍!用廣度搜索時,每層一般有數百個甚至上千個節點,一個一個的查過去是很費時的,如果再用解釋型語言這麼查,解一題,給2分鐘也未必有結果!
編碼也很費時,完成一個節點編碼需可能需200個指令。一個一個查節點,比對二節點是否相同就不費時嗎?也挺費時的,比對二個節點是否相同,要查遍4*5個方格,至少需要60條指令(優化後也需10),遍歷檢驗重複節點時平均要查2.5層,每層平均有200個節點,設平均查了半數就可知道是否重複,因此判斷一個節是否重複需要10*200*2.5/2+500(迴圈產生的)=7000多個指令。有人說對節點壓縮可提速,其實不見得,因為壓縮是需要時間的,別幹吃力不討好的事。當在DOS下程式設計,只能用640K記憶體時經常考慮壓縮。總之編碼比較費時。不編碼則更費時,相差6000/200=30倍。
如上說,編碼很費時,所以這200條指令應避免乘除指令,如何避免呢?儘量用查表法!如:要多次使用2的n次方,千萬不要每次n個2相乘,應該在程式前端充分考慮執行過程中可能使用到的2的各種次方,先用乘法都算出來並列表(一般用陣列),在需要多次使用時(如迴圈中語句中使用時),只需查表即可。
有了編碼函式,也可用來對棋盤壓縮。“橫刀立馬”最大編號為415800,只佔用24bit(3個位元組)。壓縮棋盤後,就還要有個解壓縮,用的當然是相應的解碼演算法。在DOS模式下,記憶體不夠用多考慮壓縮,用VC就沒必要壓縮了。
五、下一步工作:
通過以上演算法,可得知幾十步乃至百步以後演變的棋局,因此華容道問題已解決。下一步該考慮象棋的演算法了。幾年前寫的程式沒有打敗自己,也有必要重寫。打算使用深度優先演算法,做更有效的剪枝和盤面估值。考慮加入開局庫和殘局庫。

六、利用編碼技術搜尋華容道原始碼:
文中程式碼使用C++編寫,定義了一些類,使用純C編寫難度可能會更大一些,因為程式實現過程中使用了很多變數,不封裝為類,變數使用會比較亂。
文中棋盤被定義為一個長度為20的一維字元型陣列,定義為一維陣列的有一定的好處,內層的迴圈可完全避免產生乘法指令。而且陣列使用也更簡單。
以下程式碼可再做優化,如使用行內函數等,由於行內函數中不能使用迴圈,寫出來的程式會比較難看,所以未給出。做仔細優化,速度還可提高近一倍。
幾個核心程式碼也可用匯程式設計序優化,按筆者經驗,速度還可提高至少一倍,但程式會變得不知所云,我覺得並不可取。
程式支援的棋盤型別:含一個曹操,橫將及豎將個數之和為5個,兵為4個

//---------------------------------------------------------------------------
//----本程式在C++Builder6.0及VC++6.0中除錯通過----
//----程式名稱:"華容道"搜尋----
//----程式設計:許劍偉----
//----最後修改時間:2006.5.3----
//----速度:橫刀立馬40多毫秒(P2.4G機器)
//----如果優化走法生成器,速度為35毫秒,由於速度瓶脛在編碼器上,所以為了讓程式可讀性好些不做優化
//----要徹底提高速度,請查閱下文中利用雜湊技術的華容道算
//---------------------------------------------------------------------------
#include <stdio.h>
#include <conio.h>
//---------------------------------------------------------------------------
//--以下定義一些常數或引數--
//---------------------------------------------------------------------------
//棋盤表示使用char一維陣列,例:char q[20];
//1-15表示各棋子,空位用0表示,兵1-4,豎將5-9,橫將10-14,大王15
//大王只能1個,將必須5個(橫豎合計),兵必須為4個
const char U[]="ABBBBCCCCCHHHHHM";; //棋子型別表
const COL[20]={0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3};   //列號表
const ROW[20]={0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4};   //行號表
const WQ2[13]={1,2,4,8,16,32,64,128,256,512,1024,2048,4096}; //二進位制位權表(12個)
//---------------------------------------------------------------------------
//--以下定義幾函式--
//---------------------------------------------------------------------------
//以下define用於棋盤複製,不要用環循,實地址直接引用要快8#define qpcpy(q1,q2) {/*複製棋盤*/\
  int *ls1=(int*)q1,*ls2=(int*)q2;\
  ls1[0]=ls2[0],ls1[1]=ls2[1],ls1[2]=ls2[2],ls1[3]=ls2[3],ls1[4]=ls2[4];\
}
//memset(JD,0,Bm.bmtot);
void qkmem(void *ps,int n){ //記憶體塊置0
  register int *p=(int*)ps,*p2=p+n/4;
  while(p<p2) *p++=0;
  char *p3=(char *)p,*p4=(char *)ps+n;
  while(p3<p4) *p3++=0;
}
void prt(char *q){ //列印棋盤
  int i,j;
  for(i=0;i<5;i++){
    for(j=0;j<4;j++) printf("%2d ",q[i*4+j]);
    printf("\r\n");
  }
  printf("\r\n");
}
//---------------------------------------------------------------------------
//--以下是搜尋演算法之一(解決編碼問題)--
//---------------------------------------------------------------------------
class PmBm{ //盤面編碼類
 public:
 short int *Hz,*Sz,*Bz;  //豎將,橫將,小兵,組合序號表
 int *Hw,*Sw,Mw[12]; //權值表:橫條,豎條,大王
 int bmtot;
 PmBm(char *q){//初始化編碼表
   Hz=new short int[4096*3]; Sz=Hz+4096; Bz=Hz+4096*2;
   Hw =new int[792*2];  Sw=Hw+792;  //C12取5=792
   int i,j,k;
   int Hn=0,Bn=0,Sn=0; //各類子數目,大王預設為1不用計數
   for(i=0;i<20;i++){   //計算各種棋子的個數
     if(U[q[i]]=='B') Bn++;
     if(U[q[i]]=='H') Hn++;
     if(U[q[i]]=='C') Sn++;
   }
   Hn/=2,Sn/=2;
   int Hmax=WQ2[11],Smax=WQ2[12-Hn*2],Bmax=WQ2[16-(Hn+Sn)*2]; //各種子的最大二進位數
   int Hx=0,Sx=0,Bx=0; //各種棋子組合的最大序號
   for(i=0;i<4096;i++){  //初始化組合序號表
     for(j=0,k=0;j<12;j++) if(i&WQ2[j]) k++; //計算1的個數
     if(k==Hn&&i<Hmax) Hz[i]=Hx++;
     if(k==Sn&&i<Smax) Sz[i]=Sx++;
     if(k==Bn&&i<Bmax) Bz[i]=Bx++;
   }
   int Sq=Bx,Hq=Bx*Sx,Mq=Bx*Sx*Hx; //豎將位權,橫將位權,王位權
   for(i=0;i<12;i++) Mw[i]=i*Mq; //初始化大王權值表
   for(i=0;i<Hx;i++) Hw[i]=i*Hq; //初始化橫將權值表
   for(i=0;i<Sx;i++) Sw[i]=i*Sq; //初始化豎將權值表
   bmtot=Mq*12;
 }
 ~PmBm(){ delete[] Hz,Hw; }
 int BM(char *q){ //盤面編碼
   int Bb=0,Bd=-1; //空位序號記錄器
   int Sb=0,Sd=-1; //豎條序號記錄器
   int Hb=0,Hd=-1; //橫條序號記錄器
   int Mb;         //大王序號記錄器
   char c,lx,f[16]={0};   //其中f[]標記幾個棋子是否已確定位置序號
   int i;
   for(i=0;i<20;i++){
     c=q[i],lx=U[c]; //當前的值
     if(lx=='M') { //大王定序
       if(!f[c]) Mb=i-ROW[i],f[c]=1;
       continue;
     }
     if(COL[i]<3&&U[q[i+1]]<='H') Hd++; //橫條位置序號(編號)
     if(lx=='H') {//橫將定序,轉為二進位制進行詢址得Hb
       if(!f[c]) Hb+=WQ2[Hd],f[c]=1;
       continue;
     }
     if(ROW[i]<4&&U[q[i+4]]<='C') Sd++; //豎將位置序號(編號)
     if(lx=='C') { //豎條定序,轉為二進位制進行詢址得Sb
       if(!f[c]) Sb+=WQ2[Sd],f[c]=1;
       continue;
     }
     if(lx<='B') Bd++;  //小兵位置序號(編號)
     if(lx=='B') Bb+=WQ2[Bd]; //小兵定序,轉為二進位制進行詢址得Bb
   }
   //Hb,Sb,Bb為組合序號,"橫刀立馬"最大值為小兵C(6,4)-1=15-1,豎條C(10,4)-1=210-1
   Bb=Bz[Bb],Sb=Sz[Sb],Hb=Hz[Hb];//詢址後得得Bb,Sb,Hb組合序號
   return Bb+Sw[Sb]+Hw[Hb]+Mw[Mb]; //用位權編碼,其中Bb的位權為1
 }
 int dcBM(char *q){ //按左右對稱規則考查棋盤,對其編碼
   char i,q2[20];
   for(i=0;i<20;i+=4) q2[i]=q[i+3],q2[i+1]=q[i+2],q2[i+2]=q[i+1],q2[i+3]=q[i];
   return BM(q2);
 }
};
//---------------------------------------------------------------------------
//以下定義搜尋過程使用的核心資料結構
//---------------------------------------------------------------------------
struct PMZB{ //盤面走步集結構
  char s[10],d[10];//原位置,目標位置,最多隻會有10int n;           //總步數
};
//以下是走法生成器函式
#define kgpd(i)  (i==k1||i==k2) //空格判斷巨集
#define kgpd1(i) (i==k1&&h==1)  //豎聯空格判斷巨集
#define kgpd2(i) (i==k1&&h==2)  //橫聯空格判斷巨集
#define zin(des) z->s[z->n]=i,z->d[z->n]=des,z->n++ //儲存步法巨集
void zbFX(char *q,PMZB *z){ //分析當前可能的步法,並將所有可能的步法儲存在z中
  int i,col,k1=0,k2=0,h=0; //i,列,空格1位置,空格2位置,h為兩空格的聯合型別
  char c,lx,f[16]={0}; //f[]記錄已判斷過的棋字
  z->n=0; //計步復位

  for(i=0;i<20;i++){
    if(!q[i]) k1=k2,k2=i; //查空格的位置
  }
  if(k1+4==k2) h=1;            //空格豎聯合
  if(k1+1==k2&&COL[k1]<3) h=2; //空格橫聯合
  for(i=0;i<20;i++){
    c=q[i],lx=U[c],col=COL[i];
    if(f[c]) continue;
    switch(lx){
     case 'M': //曹操可能的走步
       if(kgpd2(i+8))        zin(i+4);  //向下
       if(kgpd2(i-4))        zin(i-4);  //向上
       if(col<2&&kgpd1(i+2)) zin(i+1);  //向右
       if(col  &&kgpd1(i-1)) zin(i-1);  //向左
       f[c]=1; break;
     case 'H': //關羽可能的走步
       if(kgpd2(i+4))        zin(i+4);  //向下
       if(kgpd2(i-4))        zin(i-4);  //向上
       if(col<2&&kgpd(i+2)) {zin(i+1); if(h==2) zin(k1); }  //向右
       if(col  &&kgpd(i-1)) {zin(i-1); if(h==2) zin(k1); }  //向左
       f[c]=1; break;
     case 'C': //張飛,馬超,趙雲,黃忠可能的走步
       if(kgpd(i+8))        {zin(i+4); if(h==1) zin(k1); }  //向下
       if(kgpd(i-4))        {zin(i-4); if(h==1) zin(k1); }  //向上
       if(col<3&&kgpd1(i+1)) zin(i+1);  //向右
       if(col  &&kgpd1(i-1)) zin(i-1);  //向左
       f[c]=1; break;
     case 'B': //小兵可能的走步
       if(kgpd(i+4))        { if(h){zin(k1);zin(k2);} else zin(i+4); } //向上
       if(kgpd(i-4))        { if(h){zin(k1);zin(k2);} else zin(i-4); } //向下
       if(col<3&&kgpd(i+1)) { if(h){zin(k1);zin(k2);} else zin(i+1); } //向右
       if(col  &&kgpd(i-1)) { if(h){zin(k1);zin(k2);} else zin(i-1); } //向右
       break;
    }
  }
}
void zb(char *q,int s,int d){ //走一步函式
  char c=q[s],lx=U[c];
  switch(lx){
    case 'B': {q[s]=0;        q[d]=c;          break; }
    case 'C': {q[s]=q[s+4]=0; q[d]=q[d+4]=c;   break; }
    case 'H': {q[s]=q[s+1]=0; q[d]=q[d+1]=c;   break; }
    case 'M': {q[s]=q[s+1]=q[s+4]=q[s+5]=0; q[d]=q[d+1]=q[d+4]=q[d+5]=c; break; }
  }
}
//---------------------------------------------------------------------------
//--以下是搜尋過程(廣度優先)--
//---------------------------------------------------------------------------
class ZBD{ //走步隊
 public:
 char (*z)[20];     //佇列
 PMZB zbj;
 int n;       //隊長度
 int *hs,*hss;//回溯用的指標及棋子
 int m,cur;   //隊頭及隊頭內步集遊標,用於廣度搜索
 int max;     //最大隊長
 int *res,ren;//結果
 ZBD(int k){ z=new char[k][20]; hs=new int[k*2+500]; hss=hs+k; res=hss+k; max=k; reset(); }
 ~ZBD(){ delete[] z; delete[] hs;}
 void reset() { n=0; m=0,cur=-1; hss[n]=-1; ren=0;}
 int zbcd(char *q){ //走步出隊
   if(cur==-1) zbFX(z[m],&zbj);
   cur++; if(cur>=zbj.n) {m++,cur=-1; return 1;} //步集遊標控制
   if(hss[m]==zbj.s[cur]) return 1;//和上次移動同一個棋子時不搜尋,可提速20%左右
   qpcpy(q,z[m]); zb(q,zbj.s[cur],zbj.d[cur]); //走步後產生新節點,結果放在qreturn 0;
 }
 void zbrd(char *q){ //走步入隊
   if(n>=max) { printf("隊溢位.\r\n"); return; }
   qpcpy(z[n],q); //出隊
   if(cur>=0) hss[n]=zbj.d[cur]; //記錄移動的子(用於回溯)
   hs[n++]=m; //記錄回溯點
 }
 void hui(int cs){ //引數:層數
   int k=cs-2; ren=cs,res[cs-1]=m;
   for(;k>=0;k--) res[k]=hs[res[k+1]]; //回溯
 }
 char* getre(int n){ return z[res[n]];} //取第n步盤面

};
//--廣度優先--
void bfs(char *q,int dep){ //引數為棋盤及搜尋最大深度
  int i,j,k,bm,v; //ok表示是否找到
  int js=0,js2=0;
  PmBm Bm(q); //建立編碼器
  char *JD=new char[Bm.bmtot]; qkmem(JD,Bm.bmtot); //建立節點陣列
  ZBD Z=ZBD(Bm.bmtot/10); //建立隊
  for(Z.zbrd(q),i=1;i<=dep;i++){ //一層一層的搜尋
    k=Z.n;
    //printf("本層%d %d\r\n",i,k-Z.m);
    while(Z.m<k){ //廣度優先
      if(Z.zbcd(q)) continue;     //返回1說明是步集出隊,不是步出隊
      js++;
      if(q[17]==15&&q[18]==15) { Z.hui(i); goto end; }//大王出來了
      if(i==dep) continue; //到了最後一層可以不再入隊了
      bm=Bm.BM(q);
      if(!JD[bm]){
        js2++ ;  //js搜尋總次數計數和js2遍歷的實結點個數
        JD[bm]=1, JD[Bm.dcBM(q)]=1;//對節點及其對稱點編碼
        Z.zbrd(q);
      }
    }
  }
  end:delete JD;
  printf("共遍歷%d個節點,其中實結點%d.隊長%d,搜尋層數%d,任意鍵...\r\n",js,js2,Z.n,Z.ren);
  if(!Z.ren) { printf("此局%d步內無解",dep); return; }
  for(i=0;i<Z.ren;i++) { getch();clrscr(); prt(Z.getre(i)); } //輸出結果
}
//---------------------------------------------------------------------------
void main(int argc, char* argv[])
{//華榮道棋盤引數,須留二個空位,兵41-4,豎將5-9,橫將10-14,大王15(1個)
 char qp[20]={
   6,15,15,7,
   6,15,15,7,
   8,11,11,5,
   8,3, 4, 5,
   2,0, 0, 1
 };
 int i,dep=81;
 bfs(qp,dep);
 getch();
}
//---------------------------------------------------------------------------
//===============================================
//===============================================

七、利用雜湊技術
用棋盤摺疊方法計算雜湊值
棋盤可看做5個int32,分別對它移位並求和,取得雜湊值,移位應適當,充分利用各子,增強雜湊
讓hash衝突變為零的要點:
1.利用盤面生成一個足夠亂的數,你可能考慮各種各樣的雜湊演算法(類似MD5的功能)
摺疊計算hash時,注意各子的值儘量少被直相加(異或等),摺疊計算時通過適當移位後再相加,移位的作用是各子的數值部分儘量少重疊在一起,充分利用各子數值,產生足夠的雜湊度.
2.利用隨機函式(rand)來產生,當然隨機數應與盤面產生關聯,每一盤面對應的隨機數(一個或一組)應是唯一的。
3.雜湊表的大小應是表中記錄的節點總數的4倍以上.
4.設雜湊表為int hsb[128K+10],總節點數為m=30K
某一盤面節點的雜湊值為n,n是17位的,那麼n在hash表中位置為hsb[n],
hsb[n]裡存放的是這個節點的另一個32位的雜湊值,用於判斷雜湊衝突.
當出現衝突時,n在表中的位置改為n+1,這樣可充分利用雜湊表,節約空間
經過這樣處理後,雜湊衝突次數約為:
第一次雜湊突次數:(1+2+…+30)/128=(n^2)/2/128=3.5k
第二次雜湊突次數:(1*1+2*2+…+30*30)/(128*128)=(n^3)/3/128/128=0.55K
接下來我們再建第二個15位雜湊表hsb2[32k]
同樣上述處理
第三次雜湊突次數:(1+2+…+0.55k)/32=(n^2)/2/32k=0.005k=5次
第四次雜湊突次數:(1*1+2*2+…+0.55*0.55k)/(32k*32k)=0次
以上分析是在雜湊值n的雜湊度理想情況下的結果,如果你的n的雜湊度不是很理想,衝突次數可乘上2,即:
第一次:3.5k*2=7k
第二次:0.55k*2=1.1
第三次:[(1+2+…+1.1k)/32]*2=40次
第四次:[(1*1+2*2+…+1.1*1.1k)/(32*32k)]*2=0.88次(約1次)

//===============================================
//===============================================
//===============================================
//下文是利用雜湊技術優化後的程式碼
//===============================================

//---------------------------------------------------------------------------
//----本程式在C++Builder6.0及VC++6.0中除錯通過----
//----程式名稱:"華容道之雜湊演算法"搜尋----
//----程式設計:許劍偉----
//----最後修改時間:2006.10.22----
//----速度:橫刀立馬12毫秒(P2.4G機器)
//---------------------------------------------------------------------------
#include <time.h>
#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
//---------------------------------------------------------------------------
//--棋盤定義說明及搜尋過程使用的核心資料結構--
//---------------------------------------------------------------------------
//棋盤表示使用char一維陣列,例:char q[20];
//大王是5(大王只能1個),橫將是4,豎將是3,兵是2,空位用0表示
//大王與橫將前兩個須相同,餘佔位填充1,豎將第二個佔位同樣填充1
//棋盤上只能為2個空格,不能多也不能少
//---------------------------------------------------------------------------
const COL[20]={0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3};//列號表
struct PMZB{       //盤面走步集結構
  char s[10],d[10];//原位置,目標位置,最多隻會有10int n;           //總步數
};
typedef char QP[20];
//---------------------------------------------------------------------------
//--以下定義幾函式--
//---------------------------------------------------------------------------
//以下define用於棋盤複製,不要用環循,實地址直接引用要快8#define qpcpy(q,p) {int *a=(int*)q,*b=(int*)p;a[0]=b[0],a[1]=b[1],a[2]=b[2],a[3]=b[3],a[4]=b[4];}/*複製棋盤*/
void qkmem(void *ps,int n){ //記憶體塊置0,同memset(mem,0,memsize);
  register int *p=(int*)ps,*p2=p+n/4;
  while(p<p2) *p++=0;
  char *p3=(char *)p,*p4=(char *)ps+n;
  while(p3<p4) *p3++=0;
}
//---------------------------------------------------------------------------
//--以下是搜尋演算法之一(解決雜湊表問題)--
//---------------------------------------------------------------------------
#define hsize 128*1024//使用128k(17位)雜湊表,如果改用更大的表,相應的雜湊計算位數也要改
//以下這兩個雜湊計算是對棋盤摺疊計算,注意異或與加法相似,不會提高雜湊度,適當的移位則會提高雜湊度
#define hs17(h1,h) h=(h1&0x0001FFFF)^(h1>>17) //17位雜湊值計算(摺疊式計算)
#define hs15(h1,h) h=(h1&0x00007FFF)^(h1>>19) //15位雜湊值計算(摺疊式計算)
#define phs(h1,h,b){if(!b[h]){b[h]=h1;return 1;} if(b[h]==h1)return 0;h++;} //雜湊值測試,返回1是新節點
class PmHx{ //盤面雜湊計算
 public:
 unsigned int *hsb,*hsb2; //雜湊表
 int cht; //雜湊衝突次數
 PmHx(){//初始化編碼表
   int i;
   hsb=new unsigned int[hsize+hsize/4+64];hsb2=hsb+hsize+32; //第二雜湊表大小為第一雜湊表的1/4
   reset();
 }
 ~PmHx(){ delete[] hsb; }
 void reset(){ cht=0; qkmem(hsb,(hsize+hsize/4+64)*sizeof(unsigned int));}
 int check(char *q){ //盤面編碼
   //生成雜湊引數n1,n2,m0
   //以下引數生成演算法不保證引數與棋盤的唯一對應關係,因此理論上存在雜湊表衝突判斷錯誤的可能
   //只不過產生錯誤的可能性幾乎可能完全忽略
   unsigned int i,n1,n2,m0,h,h1,*p=(unsigned int*)q;
   n1=(p[1]<<3)+(p[2]<<5)+p[0]; //每次摺疊時都應充分發揮各子作用,增強雜湊
   n2=(p[3]<<1)+(p[4]<<4);
   m0=(n2<<6)^(n1<<3); //增強雜湊引數
   int a=1;
   //第一雜湊處理
   h1=n1+n2+m0; hs17(h1,h);//h1為雜湊和,h為第一雜湊索引
   for(i=0;i<2;i++) phs(h1,h,hsb); //多次查表,最多32次
   //第二雜湊處理
   h1=n1-n2+m0; hs15(h1,h);//h1為雜湊差,h為第二雜湊值
   for(i=0;i<10;i++) phs(h1,h,hsb2); //首次查表
   cht++; //雜湊衝突計數(通過5次雜湊,一般