以前自学程序设计时, 研究过华容道的自动求解,已经是几年前的事了。
当时找到一个高人写的程序,效率非常高,但是,是C语言的代码,代码可读性不好,以前弄明白过这个程序,现在又忘记了,故而这次把C语言的代码改成java版的,有重新理解一遍,并记下来,以后不怕在忘了。
C 和 javascript版的代码
http://www.fjptsz.com/xxjs/xjw/rj/110.htm
对以上代码,我着重理解了盘面是如何编码的,还有原作者如何用数组实现广度优先搜索,如何找到最终答案的。
package com.global; public class LocalConst { //用1-15表示各棋子,空位用0表示,兵1-4,竖将5-9,横将10-14,大王15 //大王只能1个,将必须5个(横竖合计),兵必须为4个 // static final public String U = "ABBBBCCCCCHHHHHM"; static final public int[] U = {'A','B','B','B','B','C','C','C','C','C','H','H','H','H','H','M'}; static final public int[] COL = {0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3}; //列号表 static final public int[] ROW = {0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4}; //行号表 static final public int[] WQ2 = {1,2,4,8,16,32,64,128,256,512,1024,2048,4096}; //二进制位权表(12个) //使用128k(17位)哈希表,如果改用更大的表,相应的哈希计算位数也要改 static final public int hsize = 128 * 1024; }
盘面的状态(节点)数量是十分有限的,状态总数不会超过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。
组合序号表是什么意思?
比如小兵只有4个, 可以占用6个位置,前面的14个位置已经被大王,横将,竖将占了
小兵的组合序号表Bz的内容为 001111 (序号为0), 011101 (序号为1), 011110 (序号为2), ...
package com.gamedata; import com.global.LocalConst; //盘面编码类 public class PmBm { //组合序号表 short[] Hz; //横将 short[] Sz; //竖将 short[] Bz; //小兵 //权值表 int[] Hw; int[] Sw; int[] Mw; public int bmTotal; public PmBm(int[] qiPan) { Hz = new short[4096 * 3]; Sz = new short[4096]; Bz = new short[128]; //C12取5=792 Hw = new int[792*2]; Sw = new int[792]; int i,j,k; int Hn=0, Bn=0, Sn=0; //各类子数目,大王默认为1不用计数 for(i=0;i<20;i++){ //计算各种棋子的个数 if(LocalConst.U[qiPan[i]]=='B') Bn++; if(LocalConst.U[qiPan[i]]=='H') Hn++; if(LocalConst.U[qiPan[i]]=='C') Sn++; } Hn /= 2; Sn /= 2; int[] WQ2 = LocalConst.WQ2; int Hmax=WQ2[11],Smax=WQ2[12-Hn*2],Bmax=WQ2[16-(Hn+Sn)*2]; //各种子的最大二进位数 short Hx=0,Sx=0,Bx=0; //各种棋子组合的最大序号 //初始化组合序号表 for(i=0; i<4096; i++){ for(j=0,k=0;j<12;j++) { if((i & WQ2[j]) > 0) { 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; //竖将位权,横将位权,王位权 Mw = new int[12]; Hw = new int[Hx]; Sw = new int[Sx]; 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; //初始化竖将权值表 bmTotal = Mq*12; } //盘面编码 public int Bm(int[] qiPan) { int Bb=0,Bd=-1; //空位序号记录器 int Sb=0,Sd=-1; //竖条序号记录器 int Hb=0,Hd=-1; //横条序号记录器 int Mb = 0; //大王序号记录器 int c,lx; int[] f = new int[16]; for(int i = 0; i < 20; i++){ c=qiPan[i]; lx = LocalConst.U[c]; //当前的值 if(lx=='M') { //大王定序 if(f[c] == 0) { Mb = i - LocalConst.ROW[i]; f[c] = 1; } continue; } if (LocalConst.COL[i]<3 && LocalConst.U[qiPan[i+1]] <= 'H') { Hd++; //横条位置序号(编号) } if (lx == 'H') {//横将定序,转为二进制进行询址得Hb if(f[c] == 0) { Hb += LocalConst.WQ2[Hd]; f[c]=1; } continue; } if (LocalConst.ROW[i]<4 && LocalConst.U[qiPan[i+4]]<='C') { Sd++; //竖将位置序号(编号) } if (lx=='C') { //竖条定序,转为二进制进行询址得Sb if(f[c] == 0) { Sb += LocalConst.WQ2[Sd]; f[c]=1; } continue; } if(lx<='B') Bd++; //小兵位置序号(编号) if(lx=='B') Bb += LocalConst.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 } //按左右对称规则考查棋盘,对其编码 public int dcBM(int[] q){ char i; int[] q2 = new int[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); } }
PMZB 这个类是负责盘面走步的,非常简单,就是一些判断,容易理解
package com.gamelogic; import com.global.LocalConst; public class PMZB { //原位置,目标位置,最多只会有10步 int[] src = new int[10]; int[] dst = new int[10]; //总步数 int n; private void zbIn(PMZB zb, int s, int d) { zb.src[n] = s; zb.dst[n] = d; zb.n++; } public void analyze(int[] qiPan, PMZB zb) { int i,col,k1=0,k2=0,h=0; //i,列,空格1位置,空格2位置,h为两空格的联合类型 int c,lx; //f[]记录已判断过的棋字 int[] f = new int[16]; zb.n=0; //计步复位 for(i=0; i<20; i++){ if(qiPan[i] == 0) { k1=k2; k2=i; //查空格的位置 } } if (k1 + 4 == k2) { h = 1; //空格竖联合 } if (k1 + 1 == k2 && LocalConst.COL[k1] < 3) { h = 2; //空格横联合 } for (i = 0; i < 20; i++) { c = qiPan[i]; lx = LocalConst.U[c]; col = LocalConst.COL[i]; if (f[c] == 1) { continue; } switch (lx) { case 'M'://曹操可能的走步 if (i + 8 == k1 && h == 2) { //向下 zbIn(zb, i, i + 4); } if (i - 4 == k1 && h == 2) { //向上 zbIn(zb, i, i - 4); } if (col < 2 && i + 2 == k1 && h == 1) { //向右 zbIn(zb, i, i + 1); } if (col > 0 && i - 1 == k1 && h == 1) { //向左 zbIn(zb, i, i - 1); } f[c] = 1; break; case 'H': //关羽可能的走步 if (i + 4 == k1 && h == 2) { zbIn(zb, i, i+4); } if (i - 4 == k1 && h == 2) { zbIn(zb, i, i - 4); } if (col < 2 && (i + 2 == k1 || i + 2 == k2)) { zbIn(zb, i, i + 1); if (h == 2) { zbIn(zb, i, k1); } } if (col > 0 && (i - 1 == k1 || i - 1 == k2)) { zbIn(zb, i, i - 1); if (h == 2) { zbIn(zb, i, k1); } } f[c] = 1; break; case 'C': //张飞,马超,赵云,黄忠可能的走步 if (i + 8 == k1 || i + 8 == k2) { zbIn(zb, i, i + 4); if (h == 1) { zbIn(zb, i, k1); } } if (i - 4 == k1 || i - 4 == k2) { zbIn(zb, i, i - 4); if (h == 1) { zbIn(zb, i, k1); } } if (col < 3 && i + 1 == k1 && h == 1) { zbIn(zb, i, i+1); } if (col > 0 && i - 1 == k1 && h == 1) { zbIn(zb, i, i-1); } f[c] = 1; break; case 'B': if (i + 4 == k1 || i + 4 == k2) { if (h > 0) { zbIn(zb, i, k1); zbIn(zb, i, k2); } else { zbIn(zb, i, i + 4); } } if (i - 4 == k1 || i - 4 == k2) { if (h > 0) { zbIn(zb, i, k1); zbIn(zb, i, k2); } else { zbIn(zb, i, i - 4); } } if (col < 3 && (i + 1 == k1 || i + 1 == k2)) { if (h > 0) { zbIn(zb, i, k1); zbIn(zb, i, k2); } else { zbIn(zb, i, i + 1); } } if (col > 0 && (i - 1 == k1 || i - 1 == k2)) { if (h > 0) { zbIn(zb, i, k1); zbIn(zb, i, k2); } else { zbIn(zb, i, i - 1); } } break; } } } //走一步函数 void zb(int[] qiPan, int src, int dst) { int c = qiPan[src]; int lx = LocalConst.U[c]; switch(lx) { case 'B': qiPan[src] = 0; qiPan[dst] = c; break; case 'C': qiPan[src] = qiPan[src+4] = 0; qiPan[dst] = qiPan[dst+4] = c; break; case 'H': qiPan[src] = qiPan[src+1]=0; qiPan[dst] = qiPan[dst+1]=c; break; case 'M': qiPan[src] = qiPan[src+1]= qiPan[src+4]=qiPan[src+5]=0; qiPan[dst] = qiPan[dst+1]= qiPan[dst+4]=qiPan[dst+5]=c; break; } } }
变量n 表示当前队的长度
变量m 表示当前入队的布局,它的父亲布局在队列z 中的索引
比如,我们有一个初始布局 0, 布局0 通过PMZB类的分析, 有 3个儿子节点,分别命名为a1, a2, a3
首先把初始布局 0 放入队列z 中,这时m=0, 把m放入数组 hs 中, 接着把儿子 a1, a2, a3 都放入 队列z中,这时
z = [0, a1, a2, a3], hs = [0, 0, 0, 0], 然后m = m + 1; 假设 a1 有儿子节点b1, b2, a2 有儿子 c1, a3 有儿子d1, d2,
b1有儿子 e1, e2, b2 有儿子 f1, f2, f3, c1有儿子 g1, g2, 如果搞清了队列z, 数组hs, n, m的变化过程,就理解了代码
最终
0 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
z= | 0 | a1 | a2 | a3 | b1 | b2 | c1 | d1 | d2 | e1 | e2 | f1 | f2 | f3 | g1 | g2 |
hs= | 0 | 0 | 0 | 0 | 1 | 1 | 2 | 3 | 3 | 4 | 4 | 5 | 5 | 5 | 6 | 6 |
package com.gamelogic; import com.gamedata.PmBm; import com.gamedata.PmHx; public class ZBD { public int[][] z; //队列 PMZB zbj; int n; //队长度 int[] hs;//回溯用的指针及棋子 int[] hss; int m,cur; //队头及队头内步集游标,用于广度搜索 int max; //最大队长 int[] res;//结果 int ren; private void reset() { n=0; m=0; cur=-1; hss[0]=-1; ren=0; } public ZBD(int k) { zbj = new PMZB(); z = new int[k][20]; hs = new int[k*2 + 500]; hss = new int[k]; res = new int[k]; max = k; reset(); } //走步出队 int zbcd(int[] qiPan) { if (cur == -1) { zbj.analyze(z[m], zbj); } cur++; if (cur >= zbj.n) { m++; cur = -1; return 1; } if (hss[m] == zbj.src[cur]) { //和上次移动同一个棋子时不搜索,可提速20%左右 return 1; } qpcpy(qiPan, z[m]); zbj.zb(qiPan, zbj.src[cur], zbj.dst[cur]); return 0; } //走步入队 void zbrd(int[] qiPan) { if (n >= max) { System.out.println("对溢出"); return; } qpcpy(z[n], qiPan); //出队 if (cur >= 0) { hss[n] = zbj.dst[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]]; //回溯 } } //取第n步盘面 int[] getre(int n) { return z[res[n]]; } private static void qpcpy(int[] q1, int[] q2) { for (int i = 0; i < q1.length; i++) { q1[i] = q2[i]; } } //--广度优先-- public static int bfs(int[] qiPan, int dep) { int i,j,k,bm,v; int js = 0; int js2 = 0; PmBm coder = new PmBm(qiPan); int[] JD = new int[coder.bmTotal];//建立节点数组 ZBD z = new ZBD(coder.bmTotal / 10); for (z.zbrd(qiPan), i=1; i <= dep; i++) { k = z.n; while (z.m < k) { if (z.zbcd(qiPan) == 1) { continue;//返回1说明是步集出队,不是步出队 } js++; if (qiPan[17] == 15 && qiPan[18] == 15) { //大王出来了 z.hui(i); for (int h=0; h<z.ren; h++) { prt(z.getre(h)); //输出结果 } prt(qiPan); return 1; } if (i == dep) { //到了最后一层可以不再入队了 continue; } bm = coder.Bm(qiPan); if (JD[bm] == 0) { js2++; //js搜索总次数计数和js2遍历的实结点个数 JD[bm] = 1; JD[coder.dcBM(qiPan)] = 1; z.zbrd(qiPan); } } } return 0; } //打印棋盘 static private void prt(int[] qipan) { for (int i = 0; i < 5; i++) { for (int j = 0; j < 4; j++) { System.out.print(qipan[i*4+j] + "\t"); } System.out.println(); } System.out.println(); } public static void test() { int[] qp = { 6, 15, 15, 7, 6, 15, 15, 7, 8, 11, 11, 5, 8, 3, 4, 5, 2, 0, 0, 1 }; int ret = bfs(qp, 200); if (ret == 1) { } } public static int bfs_hx(int[] qiPan, int dep) { int all = 0; if (dep > 500 || dep <= 0) { dep = 200; } int[] q = new int[20]; qpcpy(q, qiPan); int i,k; int js = 0; int js2 = 0; PmHx hx = new PmHx(); ZBD worker = new ZBD(50000); for (worker.zbrd(q), i=1; i<=dep; i++) { //一层一层的搜索 k = worker.n; if (worker.m == k) { return -1; } while (worker.m < k) { //广度优先 if (worker.zbcd(q) == 1) { continue; //返回1说明是步集出队,不是步出队 } js ++; //遍历总次数计数 if (q[17] == 5 && q[18] == 5) { //大王出来了 worker.hui(i); for (int h = 0; h < worker.ren; h++) { prt(worker.getre(h)); //输出结果 } prt(q); return 1; } if (i < dep && hx.check(q) == 1) { //js2遍历的实结点个数,js2不包括起始点,出队时阻止了回头步,始节点不可能再遍历 js2++; //对称节点做哈希处理 hx.check2(q); worker.zbrd(q); } } } return 0; } }