华容道与哈希散列

十年前,我还没有找到程序员的工作,大学时,没有自主编写过多少代码,毕业后一段时间找不到工作,经常去面试,有一次去一个民营公司应聘,除我之外,还有个人也和我同时去了,这人虽然专业能力不强,但是做过学生会会长,非常能说,硬是给我们争取到了一个机会,负责应聘的这个面试官,其实就是这个小公司的老板,这位面试官说,我给你们一个机会,只要你们三天内可以用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;
    }

}

猜你喜欢

转载自blog.csdn.net/mu399/article/details/78908266