十年前,我还没有找到程序员的工作,大学时,没有自主编写过多少代码,毕业后一段时间找不到工作,经常去面试,有一次去一个民营公司应聘,除我之外,还有个人也和我同时去了,这人虽然专业能力不强,但是做过学生会会长,非常能说,硬是给我们争取到了一个机会,负责应聘的这个面试官,其实就是这个小公司的老板,这位面试官说,我给你们一个机会,只要你们三天内可以用C语言,实现一个万年历,我就给你们工作。
若是换个人,比如只有我,别人都不会和你多说,直接就要被拒,不会像这个学生会会长,明明专业能力不行,还可以和老板掰扯半天,然后争取到一个机会。
后来我倒是一天之内,把这个C语言的万年历给完成了,虽然最后还是没有得到这个工作,但是给了我信心,连续多次被拒,然后又没有足够的反馈,别人一般不会告诉你,为什么会被拒的,非常打击信心。
后来我又自己独立思考,不上网找任何帮助,用C语言独立实现了n皇后,理解了回溯算法,再后来又自己独立思考,不上网找代码,用C语言独立实现了华容道,当然效率很低,要三分钟才能自动求解完成,思路也不对,居然用的是深度优先,也不对盘面布局进行编码,速度当然慢。
我也知道自己的这个华容道程序,只能说勉强可用,网上肯定有更好的,于是找到了一个论坛,其中一个关于华容道的贴子,下面可是热闹非凡,长达十几页的回复,讨论,学术氛围很浓厚,灌水的很少,每个人都在分享自己的思路,给出相关的代码,或者伪代码,其中比较有名的是吕震宇,许剑伟等人给出的算法和思路,我把这些评论一个个都看玩了,确实受益匪浅,完全掌握了广度优先算法。
所以华容道这个游戏与我如此有缘,通过它我更好的掌握了编程和算法,所以我要写点东西来纪念一下。之前已经写了一篇,《华容道自动求解 java版》,但是其中并没有提到如何用哈希算法来实现华容道的自动求解,只用到编码的方法实现了。于是这篇文章就了说说怎么用java来实现哈希算法版的华容道自动求解。
不得不说这是一个我给自己挖的坑,哈希算法版的,很麻烦的,因为许剑伟的C语言版中用了uint来实现PmHx这个类,而java可没有uint类型的,只能用long来代替,还有其中用到了C语言特有的指针转换,把char * 类型的指针转换为了 int * 类型的指针,另外ZBJ 走步分析类也要重写, ZBD走步队也要重写,因为布局变化了。表示布局的20个int组成的数组变化了。
求盘面哈希值,这个类折腾很久,才搞对,首先是java的int类型,占4个字节,其中能表示的最大整数Integer.MAX_INT的值为2的31次方减去1,最小的负数为 负的Integer.MAX_INT,负数都是补码来表示的,0xff ff ff ff 等于-1
package game.hrd;
public class PmHx {
private long[] hsb;
private long[] hsb2;
public int cht; //哈希冲突次数
//使用128k(17位)哈希表,如果改用更大的表,相应的哈希计算位数也要改
static private final int hSize = 128 * 1024;
public int count = 0;
public PmHx() {
hsb = new long[hSize + hSize/4 + 64];
hsb2 = new long[hsb.length / 4];
cht = 0;
}
public int check(int[] layout) {
count++;
//生成散列参数n1,n2,m0
//以下参数生成算法不保证参数与棋盘的唯一对应关系,因此理论上存在哈希表冲突判断错误的可能
//只不过产生错误的可能性几乎可能完全忽略
int[] p = ZBD.layoutCompress(layout);
long n1 = ( ( p[1] << 3 )+ (p[2] << 5) + p[0]); //每次折叠时都应充分发挥各子作用,增强散列
long n2 = ( ( p[3] << 1) + (p[4] << 4) );
long m0 = (n2 << 6) ^ (n1 << 3); //增强散列参数
//第一哈希处理
long h1 = ( n1 + n2 + m0 ) & 0x0000ffffffffL;
long h = (( h1 & 0x0001FFFF ) ^ ( h1 >> 17 )) & 0x0000ffffffffL;
int h0 = (int) h;
// if (count < 1000)
// System.out.println("PmHx check count: " + count + ", h: " + h0);
for (int i = 0; i < 2; i++) {
if (hsb[h0] == 0) {
hsb[h0] = h1;
return 1;
}
if (hsb[h0] == h1) {
return 0;
}
h0++;
}
//多次查表,最多32次
//第二哈希处理
h1 = (n1 - n2 + m0) & 0x0000ffffffffL;
h = (( h1 & 0x00007FFF ) ^ ( h1 >> 19 )) & 0x0000ffffffffL;
h0 = (int) h;
for(int i = 0; i < 10; i++) {
if (hsb2[h0] == 0) {
hsb2[h0] = h1;
return 1;
}
if (hsb2[h0] == h1) {
return 0;
}
h0++;
} //首次查表
cht++; //哈希冲突计数(通过5次哈希,一般情况下冲突次数为0)
return 1;
}
//按左右对称规则考查棋盘,并记录到哈希表
public void check2(int[] q20) {
int[] p20 = new int[20];
for (int i = 0; i < 20; i+= 4) {
p20[i] = q20[i+3];
p20[i+1] = q20[i+2];
p20[i+2] = q20[i+1];
p20[i+3] = q20[i];
}
check(p20);
}
static public String layoutToString(int[] q) {
String layout = "";
for (int i = 0; i < 20; i++) {
layout += q[i];
if (i % 4 == 3) {
layout += ",";
}
}
return layout;
}
}
走步分析
package game.hrd;
/**
* Created by huangcongjie on 2017/12/17.
*/
public class PMZB_Hx {
//原位置,目标位置,最多只会有10步
int[] src = new int[10];
int[] dst = new int[10];
//总步数
int n;
public void analyze(int[] qiPan, PMZB_Hx zb) {
int i=0,k1=0,k2=0,h=0; //i,列,空格1位置,空格2位置,h为两空格的联合类型
zb.n=0; //计步复位
for(i=0; i<20; i++){
if(qiPan[i] == 0) {
k1=k2;
k2=i; //查空格的位置
}
}
i = k2;
if (k1 + 4 == k2) {
h = 1; //空格竖联合
}
if (k1 + 1 == k2 && LocalConst.COL[k1] < 3) {
h = 2; //空格横联合
}
int col1 = LocalConst.COL[k1];
int col2 = LocalConst.COL[k2];
if (col1 > 0) {
i = zinb(zb, k1, -1, qiPan, h, k1, k2);
if (qiPan[i] == 3) {
if (h == 1)
zin0(zb, i, k1);
}
if (qiPan[i] == 5) {
if (h == 1)
zin1(zb, i, k1);
}
if (qiPan[i] == 4) {
if (h == 2) {
zin1(zb, i, k2);
}
zin1(zb, i, k1);
}
}
if (col1 < 3) {
i = zinb(zb, k1, 1, qiPan, h, k1, k2);
if (qiPan[i] == 3) {
if (h == 1)
zin0(zb, i, k1);
}
if (qiPan[i] == 5) {
if (h == 1)
zin0(zb, i, k1);
}
if (qiPan[i] == 4) {
zin0(zb, i, k1);//如果横联合,k1不是第一空,所以不用判断h
}
}
if (k1 > 3) {
i = zinb(zb, k1, -4, qiPan, h, k1, k2);
if (qiPan[i] == 4 && qiPan[i+1] == 4 &&
(col1 != 1 || qiPan[i-1] != 4) ) {
if (h == 2)
zin0(zb, i, k1);
}
if (qiPan[i] == 1) {
if (qiPan[i-4] == 3) {
if (h == 1) {
zin4(zb, i, k2);
}
zin4(zb, i, k1);
}
if (qiPan[i-4] == 5 && qiPan[i-3] == 5) {
if (h == 2)
zin4(zb, i, k1);
}
}
}
if (k1 < 16) {
i = zinb(zb, k1, 4, qiPan, h, k1, k2);
if (qiPan[i] == 3)
zin0(zb, i, k1);
if (qiPan[i] == 4 && i < 19 && qiPan[i+1] == 4 &&
(col1 != 1 || qiPan[i-1] != 4) ) {
if (h == 2) {
zin0(zb, i, k1);
}
}
if (qiPan[i] == 5 && qiPan[i+1] == 5) {
if (h == 2)
zin0(zb, i, k1);
}
}
if (col2 > 0) {
i = zinb(zb, k2, -1, qiPan, h, k1, k2);
if (qiPan[i] == 4) {
zin1(zb, i, k2);
}
}
if(k2>3) {
i = zinb(zb, k2,-4, qiPan, h, k1, k2);
if(qiPan[i]==1 && qiPan[i-4] == 3) {
zin4(zb, i, k2);
}
}
if(col2<3) {
i = zinb(zb, k2,1, qiPan, h, k1, k2);
if(qiPan[i]==4) {
if(h==2) {
zin0(zb, i, k1);
}
zin0(zb, i, k2);
}
}
if(k2<16) {
i = zinb(zb, k2,4, qiPan, h, k1, k2);
if(qiPan[i]==3) {
if(h==1) {
zin0(zb, i, k1);
}
zin0(zb, i, k2);
}
}
}
//保存步法 (左移1列), 走一步
private void zin0(PMZB_Hx zb, int src, int dst) {
zb.src[zb.n] = src;
zb.dst[zb.n] = dst;
zb.n++;
}
//保存步法 (左移1列),竖将
private void zin1(PMZB_Hx zb, int src, int dst) {
zb.src[zb.n] = src - 1;
zb.dst[zb.n] = dst - 1;
zb.n++;
}
//保存步法 (上移1行), 横将
private void zin4(PMZB_Hx zb, int src, int dst) {
zb.src[zb.n] = src - 4;
zb.dst[zb.n] = dst - 4;
zb.n++;
}
/**
* 走小兵
* @param zb 走步对象
* @param dst 走步结束位置
* @param fx 走步方向
* @param qiPan 棋盘数组,20个元素,没有经过压缩的
* @param h 空格结合类型,1=空格竖联合, 2=空格横联合
* @param k1 第一个空格的位置
* @param k2 第二个空格的位置
* @return 返回新的开始位置
*/
private int zinb(PMZB_Hx zb, int dst, int fx, int[] qiPan, int h, int k1, int k2) {
int src = dst + fx; //走步开始位置
if (qiPan[src] == 2) {
//小兵
if (h > 0) {
zin0(zb, src, k1);
zin0(zb, src, k2);
} else {
zin0(zb, src, dst);
}
}
return src;
}
//走一步函数
void zb(int[] qiPan, int src, int dst) {
int c = qiPan[src];
int lx = c;
if (c == 1) {
lx = qiPan[src-4];
}
switch(lx) {
case 2: //兵
qiPan[src] = 0;
qiPan[dst] = c;
break;
case 3: //竖
qiPan[src] = qiPan[src+4] = 0;
qiPan[dst] = c;
qiPan[dst+4] = 1;
break;
case 4: //横
qiPan[src] = qiPan[src+1]=0;
qiPan[dst] = qiPan[dst+1]=c;
break;
case 5: //王
qiPan[src] = qiPan[src+1]= qiPan[src+4]=qiPan[src+5]=0;
qiPan[dst] = qiPan[dst+1]= c;
qiPan[dst+4]=qiPan[dst+5]= 1;
break;
}
}
}
工具类
package game.hrd.game.hrd.refactor;
/**
* Created by huangcongjie on 2017/12/20.
*/
public class HrdUtil {
/**
*
* @param layout int array which contains 20 integers
* @return an array only contains 5 integers
*/
static public int[] layoutCompress5(int[] layout) {
int[] ret = new int[5];
for (int i = 0; i < 5; i++) {
int a = i * 4;
ret[i] = layout[a] | (layout[a+1] << 8) |
(layout[a+2] << 16) | (layout[a+3] << 24);
}
return ret;
}
// int[] layout = {
// 6, 15, 15, 7,
// 6, 15, 15, 7,
// 8, 11, 11, 5,
// 8, 3, 4, 5,
// 2, 0, 0, 1
// };
// int[] ret = {
// 3, 5, 5, 3,
// 1, 1, 1, 1,
// 3, 4, 4, 3,
// 1, 2, 2, 1,
// 2, 0, 0, 2
// };
//layout 用1-15表示各棋子,空位用0表示,兵1-4,竖将5-9,横将10-14,大王15
static public int[] convertToHashLayout(int[] layout) {
int[] ret = new int[20];
//用1-15表示各棋子,空位用0表示,兵1-4,竖将5-9,横将10-14,大王15
for (int i = 0; i < layout.length; i++) {
int cur = layout[i];
if (cur == 15) {
if (i-4 >= 0 && ret[i-4] == 5) {
ret[i] = 1;
} else {
ret[i] = 5;
}
} else if (cur >= 10 && cur <= 14) {
ret[i] = 4;
} else if (cur >= 5 && cur <= 9) {
if (i-4 >= 0 && ret[i-4] == 3) {
ret[i] = 1;
} else {
ret[i] = 3;
}
} else if (cur >= 1 && cur <= 4) {
ret[i] = 2;
} else {
ret[i] = cur; //空格
}
}
return ret;
}
static public int[] hashLayoutConvertToNormal(int[] hashLayout) {
int[] ret = new int[20];
int sn = 0, hn = 0, bn = 0;
for (int i = 0; i < hashLayout.length; i++) {
if (hashLayout[i] == 5) {
ret[i] = ret[i+1] = ret[i+4] = ret[i+5] = 15;
}
if (hashLayout[i] == 4) {
hn++;
ret[i] = ret[i+1] = 9 + hn;
i++;
}
if (hashLayout[i] == 3) {
sn++;
ret[i] = ret[i+4] = 4 + sn;
}
if (hashLayout[i] == 2) {
bn++;
ret[i] = bn;
}
}
return ret;
}
static public int[] convertToScreen(int[] hashLayout) {
int[] ret = new int[20];
for (int i = 0; i < hashLayout.length; i++) {
int cur = hashLayout[i];
if (cur == 1) {
ret[i] = hashLayout[i-4];
} else {
ret[i] = hashLayout[i];
}
}
return ret;
}
static public int[] copy(int[] layout) {
int[] ret = new int[layout.length];
for (int i = 0; i < layout.length; i++) {
ret[i] = layout[i];
}
return ret;
}
static public String toLayoutString(int[] layout2) {
String layout = "";
for (int i = 0; i < 20; i++) {
layout += layout2[i];
if (i % 4 == 3) {
layout += ",|";
} else {
layout += ", ";
}
}
layout = layout.substring(0, layout.length() - 2);
return layout;
}
}
package game.hrd;
import java.util.Scanner;
public class ZBD_HX {
public int[][] z; //队列
PMZB_Hx 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_HX(int k) {
zbj = new PMZB_Hx();
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];
}
}
//打印棋盘
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 = {
3, 5, 5, 3,
1, 1, 1, 1,
3, 4, 4, 3,
1, 2, 2, 1,
2, 0, 0, 2
};
int ret = bfs(qp, 200);
}
public static int bfs(int[] qiPan, int dep) {
long current = System.currentTimeMillis();
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_HX worker = new ZBD_HX(40 * 1024);
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[13] == 5 && q[14] == 5) {
//大王出来了
worker.hui(i);
long cost = System.currentTimeMillis() - current;
System.out.println(String.format("%d层有解,遍历%d节点,哈希%d节点,队长%d,哈希冲突%d次,用时%dms\n",
worker.ren, js, js2, worker.n, hx.cht, cost));
Scanner input=new Scanner(System.in);
for (int h = 0; h < worker.ren; h++) {
System.out.println("第"+ h +"步(ESC退出)");
prt(worker.getre(h)); //输出结果
if (input.nextLine().equals("q")) {
break;
}
}
prt(q);
return 1; //有解
}
if (i < dep && hx.check(q) == 1) {
//js2遍历的实结点个数,js2不包括起始点,出队时阻止了回头步,使节点不可能再遍历
js2++;
//对称节点做哈希处理
hx.check2(q);
worker.zbrd(q);
}
}
}
return 0;
}
}