搜索算法学问不小...
总结:
1. 状态表示用整数最快, 可是转化状态的代码不好写, 用字符串挺爽的, 可有些地方涉及到数字运算, 代码又不自然, 整来整去, 还是用byte[]好了...性能没比字符串强多少...
2. open表用LinkedList就挺好, 支持队列和堆栈两种模型, 这点在双向广度优先搜索时候挺方便, closed表千万别用List类型, 用HashMap或者HashSet性能上才可接受, 而且前者优于后者...
3. 完美哈希函数, 也就是那个全排列的哈希函数, 能够不浪费一点儿空间, 实现一一映射, 函数的设计涉及到变进制数的概念, 数学的力量还是无比强大地...
4. 双向广度优先搜索区分方向要用两个open和closed, 相遇时刻拿出另一个方向上状态相同的节点比较麻烦, 需要考虑究竟用什么当做键, 用什么当做值, 把两个方向上的搜索代码抽取到一个方法中会使性能降低很多, 且需要抛异常来结束算法, 不抽取则代码又挺难看, 难办. 双向广度优先搜索路径输出是个麻烦事儿, 索性砍掉State类中记录动作的属性, 在输出grid的时候现算更挺好, 方便且免转化...
5. 编程上的细节, 什么该预先算出, 用什么方法实现节点扩展最爽, 怎么实现demo最酷, 怎么来考察运行性能, 这些都需要加强...
下面给出代码...
普通广度优先搜索:
LinkedList<State> open = new LinkedList<State>(); Map<State, Integer> closed = new HashMap<State, Integer>(); State now; List<State> nodes; public void search() { open.add(EightNum.src); while (!open.isEmpty()) { now = open.poll(); closed.put(now, 0); if (now.equals(EightNum.des)) break; nodes = EightNum.expend(now); for (State n : nodes) if (!closed.containsKey(n)) open.add(n); } EightNum.showPath(now, null); }
双向广度优先搜索:
LinkedList<State> open1 = new LinkedList<State>(); LinkedList<State> open2 = new LinkedList<State>(); Map<Integer, State> closed1 = new HashMap<Integer, State>(); Map<Integer, State> closed2 = new HashMap<Integer, State>(); State now, s; List<State> nodes; public void search() { State end1 = EightNum.src; State end2 = EightNum.des; open1.add(end1); open2.add(end2); while (!open1.isEmpty() || !open2.isEmpty()) { if (!open1.isEmpty()) { do { now = open1.poll(); closed1.put(now.hashCode(), now); nodes = EightNum.expend(now); for (State n : nodes) if ((s = closed2.get(n.hashCode())) != null) { EightNum.showPath(now, s); return; } else if (!closed1.containsKey(n.hashCode())) open1.add(n); } while (!now.equals(end1)); end1 = open1.peekLast(); } if (!open2.isEmpty()) { do { now = open2.poll(); closed2.put(now.hashCode(), now); nodes = EightNum.expend(now); for (State n : nodes) if ((s = closed1.get(n.hashCode())) != null) { EightNum.showPath(s, now); return; } else if (!closed2.containsKey(n.hashCode())) open2.add(n); } while (!now.equals(end2)); end2 = open2.peekLast(); } } }
A*算法:
PriorityQueue<State> open = new PriorityQueue<State>(256, new Comparator<State>() { public int compare(State s1, State s2) { return s1.G + s1.H - s2.G - s2.H; } }); Map<State, Integer> closed = new HashMap<State, Integer>(); State now; List<State> nodes; public void search() { EightNum.src.G = 0; EightNum.src.H = manhattan(EightNum.src.grid); open.add(EightNum.src); while (!open.isEmpty()) { now = open.poll(); closed.put(now, 0); if (now.equals(EightNum.des)) break; nodes = EightNum.expend(now); for (State n : nodes) if (!closed.containsKey(n)) { n.G = now.G + 1; n.H = manhattan(n.grid); open.add(n); } } EightNum.showPath(now, null); } private int manhattan(byte[] bs) { byte[] tmp = EightNum.getXYs(bs); int sum = 0; for (int i = 0; i < 8; i++) sum += Math.abs(tmp[i] / 3 - EightNum.XYs[i] / 3) + Math.abs(tmp[i] % 3 - EightNum.XYs[i] % 3); return sum; }
这里的open表要用优先级队列, 可是A*算法框架要更新open表和closed表中的某些节点的F值, PriorityQueue对遍历的支持不太理想, 索性就去掉了更新值的部分, 每次直接插入算了. A*的启发式函数要用曼哈顿距离, 那个"不在家"个数不太理想, 扩展的节点有时竟然比普通的广度优先搜索还多...
状态空间元素的表示:
class State { byte[] grid; State prev; int G, H; int hash = -1; public State(byte[] g, State p) { grid = g; prev = p; } public State(byte[] g, State p, int G, int H) { this(g, p); this.G = G; this.H = H; } public int hashCode() { if (hash != -1) return hash; int sum = 0; byte[] invs = EightNum.getInvNums(grid); for (int i = 0; i < 7; i++) sum += invs[i] * EightNum.facts[i]; sum += (8 - grid[8]) * EightNum.facts[7]; return hash = sum; } public boolean equals(Object o) { byte[] bs = ((State) o).grid; for (byte i = 0; i < bs.length; i++) if (bs[i] != grid[i]) return false; return true; } }
这个全排列的完美哈希函数在性能上好像没帮多大忙, 尤其用字符串表示grid的时候, 可能是我不会用吧. 缓存一下hash值, 貌似能快个几十毫秒...
辅助数据, 计算与测试...
static final byte rules[][] = {{1,3},{-1,1,3},{-1,3}, {-3,1,3},{-1,-3,1,3},{-1,-3,3}, {-3,1},{-1,-3,1},{-1,-3}}; static final char moves[] = {'U','.','L','.','R','.','D'}; static final int facts[] = {1,2,6,24,120,720,5040,40320}; static byte[] XYs = new byte[8]; static State src, des; static long count, timer; public static List<State> expend(State now) { List<State> nodes = new LinkedList<State>(); byte[] bs = now.grid; byte d, k = bs[8]; byte[] ops = rules[k]; for (byte i = 0; i < ops.length; i++) { byte[] grid = Arrays.copyOf(bs, 9); byte op = ops[i]; d = (byte) (k + op); if (op == -3) { byte b = grid[k-3]; grid[k-3] = grid[k-2]; grid[k-2] = grid[k-1]; grid[k-1] = b; } else if (op == 3) { byte b = grid[k+2]; grid[k+2] = grid[k+1]; grid[k+1] = grid[k]; grid[k] = b; } grid[8] = d; nodes.add(new State(grid, now)); } count += nodes.size(); return nodes; } public static void shuffle() { byte[] g2, g1 = getRandom(); do { g2 = getRandom(); } while (!canSolve(g1, g2)); System.out.println("SRC"); showGrid(g1); System.out.println("DES"); showGrid(g2); src = new State(g1, null); des = new State(g2, null); XYs = getXYs(g2); } private static byte[] getRandom() { List<Byte> bs = new ArrayList<Byte>(9); for (byte i = 1; i < 9; i++) bs.add(i); Collections.shuffle(bs); bs.add((byte) (new Random().nextInt(9))); byte[] ret = new byte[9]; for (byte i = 0; i < 9; i++) ret[i] = bs.get(i); return ret; } private static boolean canSolve(byte[] src, byte[] des) { byte[] in1 = getInvNums(src); byte[] in2 = getInvNums(des); int sum = 0; for (byte b : in1) sum += b; for (byte b : in2) sum += b; return sum % 2 == 0; } public static byte[] getInvNums(byte[] grid) { byte[] invs = new byte[7]; for (byte i = 1; i < 8; i++) { byte sum = 0; for (byte j = 0; j < i; j++) if (grid[j] > grid[i]) sum++; invs[i-1] = sum; } return invs; } public static byte[] getXYs(byte[] bs) { byte[] xys = new byte[8]; for (byte i = 0; i < bs[8]; i++) xys[bs[i]-1] = i; for (byte i = bs[8]; i < 8; i++) xys[bs[i]-1] = (byte) (i + 1); return xys; } public static void showPath(State now, State next) { LinkedList<State> path = new LinkedList<State>(); for ( ; now.prev != null; now = now.prev) path.push(now); for ( ; next != null; next = next.prev) path.add(next); for (int i = 0, s = now.grid[8], t; i < path.size(); i++, s = t) { now = path.get(i); t = now.grid[8]; //System.out.print(moves[t-s+3]); //showGrid(now.grid); } System.out.format(" | %7d | %3d | %5d\n", count, path.size(), System.currentTimeMillis() - timer); count = 0; } private static void showGrid(byte[] grid) { char[] cs = new char[9]; for (byte i = 0, j = 0; i < 8; i++) { if (i == grid[8]) cs[j++] = ' '; cs[j++] = (char) (grid[i] + '0'); } System.out.println("-------"); System.out.format("%c %c %c\n", cs[0], cs[1], cs[2]); System.out.format("%c %c %c\n", cs[3], cs[4], cs[5]); System.out.format("%c %c %c\n", cs[6], cs[7], cs[8]); System.out.println("-------"); } public static void main(String[] args) { shuffle(); timer = System.currentTimeMillis(); System.out.println("name | nodes | len | time"); System.out.print("BFS "); new BFS().search(); timer = System.currentTimeMillis(); System.out.print("BiBFS"); new BiBFS().search(); timer = System.currentTimeMillis(); System.out.print("AStar"); new AStar().search(); } /* 随机数据运行结果: SRC DES ------- ------- 6 1 6 2 2 5 3 ---> 5 4 1 4 7 8 8 3 7 ------- ------- 相关统计: ----------------------------- name | nodes | len | time BFS | 155132 | 20 | 401 BiBFS | 2562 | 20 | 10 AStar | 646 | 20 | 0 ----------------------------- */
PS: 算法与API两手抓, 两手都要硬...