华容道自动求解 java版

以前自学程序设计时, 研究过华容道的自动求解,已经是几年前的事了。

当时找到一个高人写的程序,效率非常高,但是,是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;
		}
	}
}


ZBD,走步队,这个类不太容易理解

变量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


如何知道g1节点是怎么来的,通过hs 知道g2节点是 队列z的第6个节点的儿子,也就是c1, 又c1的父亲是第2个节点 a2, a2的父亲是初始布局 0


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;
	}
	
}







你可能感兴趣的:(华容道自动求解 java版)