八数码问题

搜索算法学问不小...

总结:

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两手抓, 两手都要硬...

你可能感兴趣的:(C++,c,算法,C#,J#)