搜索算法学问不小...
总结:
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两手抓, 两手都要硬...