华容道搜索算法研究

华容道搜索算法研究

许剑伟 2006五一节于福建莆田十中

 

转载:http://www.fjptsz.com/xxjs/xjw/rj/110.htm

一、算法的由来:

也不知写了多少行程序,累了,写点轻松的吧,可千万不要和操作系统打交了!还有那该死的Socket,真讨厌,什么“缓冲区不足”,我不是有2G内存,怎么就不足了?

志雄正潜心研究24点算法,向我要全排列的算法,我给他一个利用堆栈解决方法,寥寥几行,志雄叫好。看他挺专心的,于是我也加入他的24点项目。花了一个晚上考虑算法,第二天早上,完全实现了我的算法,用时0.75秒,解出190024点题目的完全解,比较满意。第二天再和李雄讨论算法并最后定稿。后来李志雄和我谈起华容道的问题。想来颇为感慨,大概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  //P2P1下一代的节点集

  }

}

//---------------------

以上代码基本上给出了搜索算法,这种搜索本质上是广度优先算法。接下个我们来优化这个程序。

把第一代儿子节点放在A[1]中,那么A[1]要有多大空间来放节点所,显然第一代只需能放10个节点就够了,因为最多可能的走步不会超过10步,那第二代呢,肯定就多了,第三代还会更多……,每代所需空间都不一样,那我们要如何分配空间,如果使用javascriptPHP等脚本语言来编程,内存空间分配问题基本不用管,但用C语言呢。假如每代最多10000个节点,然后,您想搜索200代,为了简化程序,您可以简单的分配一个200*10000即可解决问题。现在电脑内存很多,用这些空间虽不算奢侈,并且会取得很高的搜索速度,但本着求精、节约的精神,有必要优化A[][]数组问题。基本思想方法就是将这个二维数组压入一个一维数组中去。这个特殊的一维数据,称为队。队和数组还有些区别,构成一个队一般还需要队头指针和队尾指针,当我们读写节点数据时,高度有序的移动这两个指针进行存取节点而不至于混乱。

伪程序中看到,第一代n1个儿子节点放在A[1]中,第二代放在A[2]中,这时A[1]中的很多空间就浪费了,不妨这样吧,读第一代节点时,把生成的第二代节点数据接在A[1]中最后一个节点之后,当第一代读完时,下一个就是第二代了;读第二代时,生成第三代节点,同样第三代也接往A[1]里的最后一节点之后,读的过程称出队,写过程过程称为入队。我们统一从队头开始读(出队),队尾处开始写(入队)。由于搜索时是一代代有序往下搜索,则队里的节点也是一代一代的往下接。

为了有序进行,读取儿子节点时,我们将队头指针指向儿子节点开始处,然后读取节点,读完后队头指针往后移动一步,当移动n1次后,就读完n1个儿子节点。在读儿子节点的过程中,我们同时试走儿子节点,生成孙子节点并入队。如此过程,在队头指针读完儿子节点后,队头指针就会指向第一个孙子节点上。伪代码如下一步

//---------------------

展开首节点A得所有儿子节点D数组()

P=1P2=最后一个; //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层的节点重复,如果重复会有什么会结果?QM只一步,MQ也只需一步,Qn-2层重复,则Qn-2层而不是n+1层,Q可生成MM就会在n-2+1=n-1层出现过,这时nn-1层都有M,与题设矛盾。因此,每走一步,一直往前查到n-1层,如果Q没有重复即为新生节点,否则应剪枝(即这样的节点不能入队)。刚才说“往前查”,即使只限定在n-1层之后一个一个查,肯定还是慢,怎么办能,干脆每生成一个节点,就对这个节点建立索引,以后哪怕节点有万亿个也不怕。如何建索引表,下文再叙。

再细想,单是以上算法还是不够快。如:父亲A在移动曹操时,生了儿子P。儿子P生孙子时也移动曹操得到R,从棋局中发现这时RA是同一节点,即父亲A等于孙子P,这是明显的节点重复,为这样的R去检查重复也是浪费时间。因此,发现要移动的棋子与父节点当时移动的子是同一个棋子就别试走,事实证明这样可少测试节点达1/3,使遍历速度提高20%。华容道棋局与象棋棋局不同,连续走两步移动同一个子,那么第二步是多余的,如果你要追求数学上的证明就自己动手证明吧。

我们还可进一步优化,某一盘面A1必存在与之左右对称的的盘面A2,目标节点M1必存在与之左右对称的盘面节点M2,设两节点最短路径为min(1,2),则min(A1,M1)==min(A2,M2),当M1为目标结点并且M2也为目标节点时,搜索A1和搜A2得到的最优结果是等价的,只需搜结果A1。华容道只要求曹操从正下方移出,所以M1M2都是目标节点,因此,搜索了A1就没必要搜索其对称节点A2,这样可使程序效率提高近一倍。

通过以上剪枝优化,生成的树已接近最小树。

回朔问题:

当我写完程序时,发现把曹操移出后,却不知道是具体的移动过程,也就是说不知道具体的路径。原因在哪里呢?伪代码中没有记录走过的路径,当找到目标节点却不知道是如何走来的,因此产生儿子节点的过程中还应告诉儿子:它的父亲是谁。当我们得到目标结点后,我们就问目标结点:你的父亲是谁,然后我们找其父,再问:你的父亲是谁,如此一层一层往上追问,最后就知道了全路径。这个过程我常称“回溯”(也许不准确)

(二)上文提到索引,如何索引。要解决索引问题,可能有很多种方法,首先想到的是使用哈希表的办法,哈希表是棋类游戏常用的方法,算法原理不难,不过实现起来也挺麻烦的,使用哈希表时一般使用随机数来建立索引,因此一定要选择有足够散列度随机数(或准随机算法),以免造成大量哈希冲突而降底效率。以下介绍的方法是通过编码及查表方法来完成节点索引,建立一种不会发生重复的索引。总之,就是要建立一个索引,哈希表是个有重复的索引(碰到冲突的一般要做二次哈希),编码方法是建立一个无重复索引。本文讲述的的编码方法得到的速度不会很快,如果你追求速度,最好使用哈希表建立索引,并且在计算哈希值时采用增量技术,这样计算索引号的速度可提高1020倍,程序在10ms内可搜索出最优解。

盘面的状态(节点)数量是十分有限的,状态总数不会超过50万种。(横刀立马为例)

曹操的走法只有12种,任你如何排放,只有12种,不是20种。

横将(关羽)的排法最多只有11

接下来对4个竖将排列(组合),排列第一个竖将的排法最多10种,第二个8种,第三个6种,第四个4种。组合数是10*8*6*4/4!=80,后来为来编程方便,做了更多冗于,组合数用C104,即C104=10*9*8*7/4=210,这样,4个竖将的某一排列组合必对应0209中的一个数,这个数就是我们所要的竖将组合编码值。

同理小兵的组合为C64=15,编码范围在014

因此对这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],这样就完成了高速编码。这个编号表如何建立呢?这也好办,事前把000000000011111111111024个数中含41的数按顺序编号,其中只有C104=210个数被编号,其余不用。由此建立一个1024长的有序表Sb[],表中位置下标含41的被编号,共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条指令应避免乘除指令,如何避免呢?尽量用查表法!如:要多次使用2n次方,千万不要每次n2相乘,应该在程序前端充分考虑运行过程中可能使用到的2的各种次方,先用乘法都算出来并列表(一般用数组),在需要多次使用时(如循环中语句中使用时),只需查表即可。

有了编码函数,也可用来对棋盘压缩。“横刀立马”最大编号为415800,只占用24bit3个字节)。压缩棋盘后,就还要有个解压缩,用的当然是相应的解码算法。在DOS模式下,内存不够用多考虑压缩,用VC就没必要压缩了。

五、下一步工作:

通过以上算法,可得知几十步乃至百步以后演变的棋局,因此华容道问题已解决。下一步该考虑象棋的算法了。几年前写的程序没有打败自己,也有必要重写。打算使用深度优先算法,做更有效的剪枝和盘面估值。考虑加入开局库和残局库。

 

六、利用编码技术搜索华容道源代码:

文中代码使用C++编写,定义了一些类,使用纯C编写难度可能会更大一些,因为程序实现过程中使用了很多变量,不封装为类,变量使用会比较乱。

文中棋盘被定义为一个长度为20的一维字符型数组,定义为一维数组的有一定的好处,内层的循环可完全避免产生乘法指令。而且数组使用也更简单。

以下代码可再做优化,如使用内联函数等,由于内联函数中不能使用循环,写出来的程序会比较难看,所以未给出。做仔细优化,速度还可提高近一倍。

几个核心代码也可用汇编程序优化,按笔者经验,速度还可提高至少一倍,但程序会变得不知所云,我觉得并不可取。

程序支持的棋盘类型:含一个曹操,横将及竖将个数之和为5个,兵为4

//---------------------------------------------------------------------------

//----本程序在C++Builder6.0VC++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;  //C125=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];//原位置,目标位置,最多只会有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<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();

}

//---------------------------------------------------------------------------

//===============================================

//===============================================

 

 

 

七、利用哈希技术

用棋盘折叠方法计算哈希值

棋盘可看做5int32,分别对它移位并求和,取得哈希值,移位应适当,充分利用各子,增强散列

hash冲突变为零的要点:

1.利用盘面生成一个足够乱的数,你可能考虑各种各样的杂凑算法(类似MD5的功能)

折叠计算hash,注意各子的值尽量少被直相加(异或等),折叠计算时通过适当移位后再相加,移位的作用是各子的数值部分尽量少重叠在一起,充分利用各子数值,产生足够的散列度.

2.利用随机函数(rand)来产生,当然随机数应与盘面产生关联,每一盘面对应的随机数(一个或一组)应是唯一的。

3.哈希表的大小应是表中记录的节点总数的4倍以上.

4.设哈希表为int hsb[128K+10],总节点数为m=30K

某一盘面节点的哈希值为n,n17位的,那么nhash表中位置为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.0VC++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];//原位置,目标位置,最多只会有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<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次哈希,一般情况下冲突次数为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<k){ //广度优先

      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(i<dep&&Hx.check(q)){ //到了最后一层可以不再入队了

        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;i<S.ren;i++) {

   printf("%d(ESC退出)\r\n",i);

   prt(S.getre(i));

   if(getch()==27) break;

   clrscr();

 }

 getch();

}

//---------------------------------------------------------------------------

 

八、利用javascript进行华荣道搜索

 

//===============================================

//===============================================

//===============================================

//下文是利用javascript进行华容道搜索

//速度:约25(P2.4G)

//设计:许剑伟,200655日下午及晚上—200656日上午

//===============================================

<html>

<head>

<title>华容道</title>

</head>

 

<body>

<script language=javascript>

var QZLX=Array();//棋子类型表

QZLX.A="A";

QZLX.B="B";

QZLX.X="B";

QZLX.Y="B";

QZLX.Z="B";

QZLX.C="C";QZLX.D="C";QZLX.E="C";QZLX.F="C";QZLX.G="C";

QZLX.H="H";QZLX.I="H";QZLX.J="H";QZLX.K="H";QZLX.L="H";

QZLX.M="M";

var COL=Array(0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3); //列号表

var ROW=Array(0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4); //行号表

qp=Array(

 "C","M","M","D",

 "C","M","M","D",

 "E","H","H","F",

 "E","X","Y","F",

 "B","A","A","Z"

// "B","M","M","B",

// "E","M","M","F",

// "E","H","H","F",

// "B","I","I","B",

// "A","J","J","A"

// "M","M","A","C",

// "M","M","A","C",

// "D","E","H","H",

// "D","E","B","F",

// "B","B","B","F"

);

 

function qpToS(q){ //棋盘转为串

  var i,j,s="";

  for(i=0;i<5;i++,s+="<br>")

    for(j=0;j<4;j++) s+=q[i*4+j]+" ";

  return s.replace(/[A]/g,"&nbsp;");

}

function BM(q){   //快速编码

  return q.join("").replace(/[DEFG]/g,"C").replace(/[IJKL]/g,"H").replace(/[XYZ]/g,"B");

}

function dcBM(q){ //对称编码

  var s=q[3]+q[2]+q[1]+q[0]+q[7]+q[6]+q[5]+q[4]+q[11]+q[10]+q[9]+q[8]+q[15]+q[14]+q[13]+q[12]+q[19]+q[18]+q[17]+q[16];

  return s.replace(/[DEFG]/g,"C").replace(/[IJKL]/g,"H").replace(/[XYZ]/g,"B");

}

function zbFX(q){ //走步分析

  var i,r="";

  for(i in q){ //i是字符型

    if(q[i-0]!="A") continue;

    i=i-0;

    if(ROW[i]<4){

      switch(QZLX[q[i+4]]){ //下棋子

        case "B": { r+=i+4+" "+i+" "; break; }

        case "C": { r+=i+4+" "+i+" "; break; }

        case "H": if(q[i+4]==q[i+5]&&q[i+1]=="A") { r+=i+4+" "+i+" "; break; }

        case "M": if(q[i+4]==q[i+5]&&q[i+1]=="A") { r+=i+4+" "+i+" "; }

      }

      if(q[i+4]=="A"&&ROW[i]<3){ //处理下二棋子

        switch(QZLX[q[i+8]]){

          case "C": { r+=i+8+" "+i+" "; break; }

          case "B": { r+=i+8+" "+i+" "; }

        }

      }

    }

    if(ROW[i]){

      switch(QZLX[q[i-4]]){ //上棋子

        case "B": { r+=i-4+" "+i+" "; break; }

        case "C": { r+=i-8+" "+(i-4)+" "; break; }

        case "H": if(q[i-4]==q[i-3]&&q[i+1]=="A") { r+=i-4+" "+i+" "; break; }

        case "M": if(q[i-4]==q[i-3]&&q[i+1]=="A") { r+=i-8+" "+(i-4)+" "; }

      }

      if(q[i-4]=="A"&&ROW[i]>1){ //处理上二棋子

        switch(QZLX[q[i-8]]){

          case "C": { r+=i-12+" "+(i-4)+" "; break; }

          case "B": { r+=i-8+" "+i+" "; }

        }

      }

    }

    if(COL[i]){ //处理左边棋子

      switch(QZLX[q[i-1]]){ //左棋子

        case "B": { r+=i-1+" "+i+" "; break; }

        case "H": { r+=i-2+" "+(i-1)+" "; break; }

        case "C": if(q[i-1]==q[i+3]&&q[i+4]=="A") { r+=i-1+" "+i+" "; break; }

        case "M": if(q[i-1]==q[i+3]&&q[i+4]=="A") { r+=i-2+" "+(i-1)+" "; }

 

      }

      if(q[i-1]=="A"&&COL[i]>1){ //处理左二棋子

        switch(QZLX[q[i-2]]){

          case "H": { r+=i-3+" "+(i-1)+" "; break; }

          case "B": { r+=i-2+" "+i+" "; }

        }

      }

      if(QZLX[q[i-5]]=="B"&&(q[i-1]=="A"||q[i-4]=="A")) { r+=i-5+" "+i+" "; } //左上方棋子

      if(QZLX[q[i+3]]=="B"&&(q[i-1]=="A"||q[i+4]=="A")) { r+=i+3+" "+i+" "; } //左下方棋子

    }

    if(COL[i]<3){ //处理右边棋子

      switch(QZLX[q[i+1]]){ //右棋子

        case "B": { r+=i+1+" "+i+" "; break; }

        case "H": { r+=i+1+" "+i+" "; break; }

        case "C": if(q[i+1]==q[i+5]&&q[i+4]=="A") { r+=i+1+" "+i+" "; break; }

        case "M": if(q[i+1]==q[i+5]&&q[i+4]=="A") { r+=i+1+" "+i+" "; }

 

      }

      if(q[i+1]=="A"&&COL[i]<2){ //处理右二棋子

        switch(QZLX[q[i+2]]){

          case "H": { r+=i+2+" "+i+" "; break; }

          case "B": { r+=i+2+" "+i+" "; }

        }

      }

      if(QZLX[q[i-3]]=="B"&&(q[i+1]=="A"||q[i-4]=="A")) { r+=i-3+" "+i+" "; } //右上方棋子

      if(QZLX[q[i+5]]=="B"&&(q[i+1]=="A"||q[i+4]=="A")) { r+=i+5+" "+i+" "; } //右下方棋子

    }

  }

  return q.join(" ")+" "+r; //为防止对象过多,干脆用一个单串返回

}

function zb(q,s,d){

  var c=q[s];

  switch(QZLX[c]){

    case "B": {q[s]="A";                      q[d]=c;        break; }

    case "C": {q[s]=q[s+4]="A";               q[d]=q[d+4]=c; break; }

    case "H": {q[s]=q[s+1]="A";               q[d]=q[d+1]=c; break; }

    case "M": {q[s]=q[s+1]=q[s+4]=q[s+5]="A"; q[d]=q[d+1]=q[d+4]=q[d+5]=c; break; }

  }

  return q;

}

//------------------------------------

function ZBD(){ //构造走步队对象

  this.z=Array();  //队列,this为当前对象,常在构造时使用,在没有用new分配一个实体前,this不明确

  this.hs=Array(); //回朔指针

  this.hsb=Array(); //回朔步

  this.hd=0;       //队头指针

  this.cur=Array();//队头信息

  this.Rd=zbrd;    //入队方法

  this.Cd=zbcd;    //出队方法

  this.getQP=getqp;

  this.cur.n="null"; //cur无内容标记

 

}

function zbrd(q,fla){ //走步入队,第二个参数为是否回朔

  var n=this.z.length;

  this.z[n]=zbFX(q);

  if(fla==-1) this.hs[n]=0,this.hsb[n]="无棋子";//回朔指针

  else this.hs[n]=this.hd,this.hsb[n]=this.cur[this.cur[this.cur.p-2]];

}

function zbcd(){ //走步出队

  if(this.cur.n=="null"){

    this.cur=this.z[this.hd].split(" ");

    this.cur.n=this.cur.length-1;//原串中最后一个是空格所以多减1

    this.cur.p=20; //棋步游标

  }

  if(this.cur.p>=this.cur.n) {this.hd++; this.cur.n="null"; return "";}

  var p=this.cur.p;

  this.cur.p+=2;

  if(this.cur[this.cur[p]]==this.hsb[this.hd]) return "";

  return zb(this.cur.slice(0,20),this.cur[p]-0,this.cur[p+1]-0); //使用拷贝盘传入

}

function getqp(n){ //从队中取出棋盘

  var s=this.z[n].split(" ");

  return s.slice(0,20);

}

//------------------------------------

//广度优先搜索

//------------------------------------

 

function GDsearch(q,dep){ //参数为棋盘及搜索最大深度

  var i,k,ok=0,bm,v; //ok表示是否找到

  var js=0,js2=0;

  var D=new ZBD(); //建立走步队

  var JD=Array();  //结点记录器

  for(D.Rd(q,-1),i=1;i<=dep&&!ok;i++){ //一层一层的搜索

    k=D.z.length;

    while(D.hd<k){ //广度优先

      q=D.Cd(); //出队

      if(q=="") continue;     //返回空说明是步集出队,不是步出队

      if(q[17]=="M"&&q[18]=="M") { ok=1;break;} //大王出来了

      if(i==dep) continue; //到了最后一层就不再入队了

      bm=BM(q); v=JD[bm];

      js++;

      if(!v){

        js2++ ;  //js搜索总次数计数和js2遍历的实结点个数

        JD[bm]=i, JD[dcBM(q)]=i;//对节点及其对称点编码并计录结点深度

        D.Rd(q,0);  //入队

      }

    }

  }

  k=i-1; //实际计算的层数

  var s="共遍历"+js+"个节点,其中实结点"+js2+"搜索步数"+k+"<hr>";

  var hs=Array();

  if(ok){

    for(i=1,hs[0]=D.hd;i<k;i++) hs[i]=D.hs[hs[i-1]]; //回塑

    for(i=k-1;i>=0;i--)  s+=qpToS(D.getQP(hs[i]))+"<hr>";

    s+=qpToS(q);

  }

  else s+="此局"+dep+"步内无解";

  return s;

}

document.write(GDsearch(qp,130));

 

</script>

</body>

 

</html>

 

 

猜你喜欢

转载自int420.iteye.com/blog/1688409