转载地址已经废弃,这里做个备份。
作者: 许剑伟 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
#include
//---------------------------------------------------------------------------
//--以下定义一些常数或参数--
//---------------------------------------------------------------------------
//棋盘表示使用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*p++=0;
char *p3=(char *)p,*p4=(char *)ps+n;
while(p3*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&&iif(k==Sn&&iif(k==Bn&&iint Sq=Bx,Hq=Bx*Sx,Mq=Bx*Sx*Hx; //竖将位权,横将位权,王位权
for(i=0;i<12;i++) Mw[i]=i*Mq; //初始化大王权值表
for(i=0;i*Hq; //初始化横将权值表
for(i=0;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];//原位置,目标位置,最多只会有10步
int 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]); //走步后产生新节点,结果放在q中
return 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//广度优先
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;iint argc, char* argv[])
{//华荣道棋盘参数,须留二个空位,兵4个1-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
#include
#include
#include
//---------------------------------------------------------------------------
//--棋盘定义说明及搜索过程使用的核心数据结构--
//---------------------------------------------------------------------------
//棋盘表示使用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];//原位置,目标位置,最多只会有10步
int 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*p++=0;
char *p3=(char *)p,*p4=(char *)ps+n;
while(p3*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次哈希,一般情况下冲突次数为0)
return 1;
}
void check2(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];
check(q2);
//check2()执行次数较少,是实节点的次数,约为check()执行次数的1/3,所以不必过份求其速度
}
}; //建立哈希表索引器
//---------------------------------------------------------------------------
//以下设计走法生成器
//---------------------------------------------------------------------------
#define zin0(des) z->s[z->n]=i,z->d[z->n++]=des //保存步法宏
#define zin1(des) z->s[z->n]=i-1,z->d[z->n++]=des-1 //保存步法宏(左移1列)
#define zin4(des) z->s[z->n]=i-4,z->d[z->n++]=des-4 //保存步法宏(上移1行)
#define zinb(des,fx) i=des+(fx); if(q[i]==2) {if(h){zin0(k1);zin0(k2);}else zin0(des);}
void zbFX(char *q,PMZB *z){ //分析当前可能的步法,并将所有可能的步法保存在z中
int i,k1=-1,k2,h=0;z->n=0; //k1空格1位置,k2空格2位置,h为两空格的联合类型,计步复位
for(i=0;i<20;i++){ if(!q[i]){if(k1==-1) k1=i; else { k2=i; break; }} } //查空格的位置
int col1=COL[k1],col2=COL[k2];
if(k1+4==k2) h=1; //空格竖联合
if(k1+1==k2&&col1<3) h=2; //空格横联合
if(col1>0){zinb(k1,-1);
if(q[i]==3) {if(h==1) zin0(k1);}
if(q[i]==5) {if(h==1) zin1(k1);}
if(q[i]==4) {if(h==2) zin1(k2); zin1(k1);}
}
if(col1<3){zinb(k1,1);
if(q[i]==3) {if(h==1) zin0(k1);}
if(q[i]==5) {if(h==1) zin0(k1);}
if(q[i]==4) {zin0(k1);} //如果横联合,k1不是第一空,所以不用判断h
}
if(k1>3){zinb(k1,-4);
if(q[i]==4&&q[i+1]==4&&(col1!=1||q[i-1]!=4)){ if(h==2) zin0(k1); }
if(q[i]==1){
if(q[i-4]==3) {if(h==1) zin4(k2); zin4(k1);}
if(q[i-4]==5&&q[i-3]==5){if(h==2) zin4(k1);}
}
}
if(k1<16){zinb(k1,4);
if(q[i]==3) zin0(k1);
if(q[i]==4&&q[i+1]==4&&(col1!=1||q[i-1]!=4)){ if(h==2) zin0(k1); }
if(q[i]==5&&q[i+1]==5){ if(h==2) zin0(k1); }
}
if(col2>0){zinb(k2,-1); if(q[i]==4) zin1(k2); }
if(k2>3) {zinb(k2,-4); if(q[i]==1&&q[i-4]==3)zin4(k2);}
if(col2<3){zinb(k2,1); if(q[i]==4) {if(h==2) zin0(k1); zin0(k2);}}
if(k2<16) {zinb(k2,4); if(q[i]==3) {if(h==1) zin0(k1); zin0(k2);}}
}
//---------------------------------------------------------------------------
//--以下是搜索过程(广度优先)--
//---------------------------------------------------------------------------
class ZBD{ //走步队(搜索器)
public:
QP *z; //棋盘队列
int dn,dm,cur;//队(队尾),队头及队头内走步集游标
PMZB zbj; //队头走步集
int *hs; //回溯用的指针,指向父亲(对应的父亲)
char*hss; //对应的源步
int max; //最大队长
int *res,ren;//结果
int js,js2; //搜索情况计数
PmHx Hx; //希希处理类
ZBD(int k){ z=new QP[k]; hs=new int[k*2+500]; res=hs+k; hss=new char[k]; max=k; reset(); }
~ZBD(){ delete[] z; delete[] hs; delete[] hss; }
void reset() { dn=0; dm=0,cur=-1; ren=0; js=js2=0; hss[dn]=-1;}
void zb(char *q,char s,char d){ //走一步函数
char c=q[s];q[s]=0;
switch(c){
case 3: q[s+4]=0; q[d+4]=1; break; //竖,余位填充1
case 4: q[s+1]=0; q[d+1]=c; break; //横
case 5: q[s+1]=q[s+4]=q[s+5]=0; q[d+1]=c,q[d+4]=q[d+5]=1; break;
}q[d]=c;
}
int zbcd(char *q){ //走步出队
if(cur==-1) zbFX(z[dm],&zbj);
cur++; if(cur>=zbj.n) {dm++,cur=-1; return 1;} //步集游标控制
if(hss[dm]==zbj.s[cur]) return 1;//和上次移动同一个棋子时不搜索,可提速20%左右
qpcpy(q,z[dm]); zb(q,zbj.s[cur],zbj.d[cur]); //走步后产生新节点,结果放在q中(出队)
return 0;
}
void zbrd(char *q){ //走步入队
if(dn>=max) { printf("队溢出.\r\n"); return; }
qpcpy(z[dn],q); //出队
if(cur>=0) hss[dn]=zbj.d[cur]; //记录下移动的目标位置(用于剪枝)
hs[dn++]=dm; //记录回溯点
}
char* getre(int n){ return z[res[n]];} //取第n步盘面
int bfs(char *qp,int dep,int all=0){ //广度优先搜索,参数为棋盘及搜索最大深度,all为穷举节点总数
if(dep>500||dep<=0) dep=200;
reset(); Hx.reset(); //哈希表及队复位
char q[20]; qpcpy(q,qp);
int i,k;
for(zbrd(q),i=1;i<=dep;i++){ //一层一层的搜索
k=dn; if(dm==k) return -1;
if(all)printf("第%d层共%d节点\r\n",i-1,k-dm);
while(dm//广度优先
if(zbcd(q)) continue; //返回1说明是步集出队,不是步出队
js++; //遍历总次数计数
if(q[13]==5&&q[14]==5&&!all) { //大王出来了
for(ren=i,k=ren-2,res[ren-1]=dm;k>=0;k--) res[k]=hs[res[k+1]]; //回溯
return 1;
}
if(iq)){ //到了最后一层可以不再入队了
js2++; //js2遍历的实结点个数,js2不包括起始点,出队时阻止了回头步,始节点不可能再遍历
Hx.check2(q);//对称节点做哈希处理
zbrd(q);
}
}
}
return 0;
}
}S(40*1024); //建立搜索引擎
//---------------------------------------------------------------------------
//----输入输出部分----
//---------------------------------------------------------------------------
void prt(char *q){ //打印棋盘
int i,j,k;
char y[20],x[20],xy[20],p[20],c1,c2;
for(i=0;i<20;i++){
y[i]='|',x[i]='-',xy[i]='+';
if(q[i]) p[i]=q[i]+48; else p[i]=' ';
if(q[i]==1) p[i]=p[i-4];
}
for(i=0;i<20;i++){
if(q[i]==0) {if(COL[i]<3&&q[i+1]==0) y[i]=' '; if(i<16&&q[i+4]==0) x[i]=' ';}
if(q[i]==3) { x[i]='.'; }
if(q[i]==4) { y[i]='.'; i++; }
if(q[i]==5) { y[i]=' '; y[i+4]=' '; x[i]=' '; x[i+1]=' '; xy[i]='.'; i++; }
}
printf("+-+-+-+-+\r\n");
for(i=0;i<5;i++){
k=i*4;
printf("|");
for(j=0;j<4;j++){ printf("%c%c",p[k+j],y[k+j]); }
printf("\r\n|");
for(j=0;j<4;j++){ printf("%c%c",x[k+j],xy[k+j]); }
printf("\r\n");
}
}
//---------------------------------------------------------------------------
void main(int argc, char* argv[]){
int i,ok,t1=clock();
QP qp={
3,5,5,3,
1,1,1,1,
3,4,4,3,
1,2,2,1,
2,0,0,2
};
ok=S.bfs(qp,500);
if(!ok) printf("此局500层内无解.\r\n");
if(ok==-1) printf("此局无解.\r\n",200);
printf("%d层有解,遍历%d节点,哈希%d节点,队长%d,哈希冲突%d次,用时%dms\r\n",S.ren,S.js,S.js2,S.dn,S.Hx.cht,clock()-t1);
for(i=0;iprintf("第%d步(ESC退出)\r\n",i);
prt(S.getre(i));
if(getch()==27) break;
clrscr();
}
getch();
}
//---------------------------------------------------------------------------
八、利用javascript进行华荣道搜索
//===============================================
//===============================================
//===============================================
//下文是利用javascript进行华容道搜索
//速度:约25秒(P2.4G)
//设计:许剑伟,2006年5月5日下午及晚上—2006年5月6日上午
//===============================================
华容道</title>
>