华容道搜索算法研究

转载地址已经废弃,这里做个备份。

作者: 许剑伟 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次哈希,一般情况下冲突次数为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>
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值