游戏定义
整个六边形用二维数组保存(逻辑上),为了方便计算
游戏状态
使用数组保存游戏状态会占用大量的内存空间,运算速度也慢,所以使用位压缩的方式保存游戏的状态,这可以节省内存空间以及提高运算速度
游戏操作
游戏只有顺时针、原点对称、逆时针三种方式交换黑点,定义为 L、C、R。
大概整个编码中最难的部分就是这里。
分析
先来张图
让游戏只用代码运行,就要摒弃一切无关信息,保留我们所需的关键信息。
所以只保留游戏开始、结束的两个状态,三个简单的操作。
游戏的状态与黑点位置有关,三个操作中受影响的也是黑点,黑点的变化是游戏的关键。
问题一
别嫌弃图丑
假设现在用L的操作方式点击图中的红点(2行 3列)的位置,通过计算可以确定红点周围一圈的点,从0的位置开始判断,大致可以写出以下方式
long state = 1149501712;
for (int i = 0; i < 6; i++) {
int index = getIndex(i); // 对应一维数组的索引
if (isExist(state, index)) { // 该点是否存在黑点
state -= getValue(i);
state += getValue((i + 1) % 6);
}
}
以上图的状态执行L操作,可以得到想要的结果(state = 1149501697),现在以下图的状态再执行一次相同的操作
结果state = 1149504883,对应的游戏状态是这样的
为什么会这样?多了5个点?哪来的?
逐步分析下:
i = 0,1 的位置会有黑点,这没错,接下来
i = 1,因为i = 0的操作 1 的位置有了黑点,而在执行上个步骤前 1 的位置没有黑点,所以 2 不该有黑点,但是i = 0的步骤影响了接下来的所有步骤,其余两个也有这样的问题(点之间的关联性)
问题二
由于这不是规则的矩形,计算黑点的一维数组索引就必需知道前面的点的个数和,如果每走一步都计算一次则很浪费时间
解决问题思路
对于问题二的解决很简单,先计算一遍每行前面的点的数量和存放在一维数组里,用对应的行数去取就行了
重点是问题二
因为点之间的关联性,前面的方式在计算下一个节点的状态时,会影响其它的点,原因是在同一个状态下操作,所以要分离下一个节点的状态计算。计算下一个状态时,只关心被点击的点的周围一圈,与其它黑点无关,代码如下
private long originSymmetric(long parentState, int row, int col) {
long childState = 0;
int choiceCol = getColIndex(row);
for (int i = 0; i < 6; i++) {
int index = previousLength[row + offsetRow[i]] + col + offsetCol[choiceCol][i];
if (isExist(parentState, index)) {
childState |= getValue(previousLength[row + offsetRow[(i + 3) % 6]] + col + offsetCol[choiceCol][(i + 3) % 6]);
parentState ^= getValue(index);
}
}
return childState | parentState;
}
父节点移除被移动的黑点
parentState ^= getValue(index);
子节点增加移动后的黑点
childState |= getValue(previousLength[row + offsetRow[(i + 3) % 6]] + col + offsetCol[choiceCol][(i + 3) % 6]);
以上运算后父节点被点击的点的周围一圈黑点全部存到子节点上,该点周围信息丢失,子节点的情况则相反,最后合并两个节点的信息,得到新的节点,即真正所需的子节点
基本问题都解决了,接下来的工作就只是挑选可能最快达到目标的节点,在可点击的范围内重复L、C、R操作了
代码
Node.java
public class Node {
private Node parent;
private long state, weight;
private int row, col, step;
private char direction;
public Node getParent() {
return parent;
}
public void setParent(Node parent) {
this.parent = parent;
}
public long getState() {
return state;
}
public void setState(long state) {
this.state = state;
}
public long getWeight() {
return weight;
}
public void setWeight(long weight) {
this.weight = weight;
}
public int getRow() {
return row;
}
public void setRow(int row) {
this.row = row;
}
public int getCol() {
return col;
}
public void setCol(int col) {
this.col = col;
}
public int getStep() {
return step;
}
public void setStep(int step) {
this.step = step;
}
public char getDirection() {
return direction;
}
public void setDirection(char direction) {
this.direction = direction;
}
}
NodeTree.java
public class NodeTree {
private long targetState, sum;
private int[] range, previousLength; // 六边形次外圈的范围、六边形对应的行前面的点数量之和
private Node[] nodes;
private int nodesIndex, depth;
public NodeTree(int[][] startState, int[][] endState, int depth) {
this.targetState = NodeUtil.getStateValue(endState);
this.range = NodeUtil.getRange(endState);
this.previousLength = NodeUtil.getPreviousLength(endState);
this.depth = depth;
this.nodes = new Node[40];
Node root = new Node();
root.setState(NodeUtil.getStateValue(startState));
nodes[nodesIndex++] = root;
sum++;
}
private char[] direction = {'L', 'C', 'R'}; // 对应顺时针、原点交换、逆时针
public void search() {
long startTime = System.currentTimeMillis();
while (nodesIndex > 0) {
Node parent = nodes[0];
// 找到目标,停止搜索
if (parent.getState() == targetState) {
printSearchInfo(startTime, "success");
printPath(parent);
return;
}
shiftDown(--nodesIndex);
// 节点达到最大深度,不再搜索
if (parent.getStep() < depth) {
for (int i = 0; i < range.length; i++) {
for (int j = 1; j <= range[i]; j++) {
for (int d = 0; d < direction.length; d++) {
if (nodesIndex == nodes.length) {
Node[] newNodes = new Node[nodesIndex * 2];
System.arraycopy(nodes, 0, newNodes, 0, nodesIndex);
nodes = newNodes;
}
Node child = nextStep(parent, direction[d], i + 1, j);
if (child != null) {
nodes[nodesIndex++] = child;
shiftUp(nodesIndex - 1);
sum++;
}
}
}
}
}
}
printSearchInfo(startTime, "fail");
}
// 走一步
private Node nextStep(Node parent, char direction, int row, int col) {
long childState = 0;
switch (direction) {
case 'L':
childState = clockwiseRotate(parent.getState(), row, col);
break;
case 'C':
childState = originSymmetric(parent.getState(), row, col);
break;
case 'R':
childState = anticlockwiseRotate(parent.getState(), row, col);
break;
}
if (childState != parent.getState()) {
Node child = new Node();
child.setParent(parent);
child.setRow(row);
child.setCol(col);
child.setDirection(direction);
child.setState(childState);
child.setStep(parent.getStep() + 1);
child.setWeight(getEstimatedValue(child));
return child;
}
return null;
}
private int[] offsetRow = {-1, -1, 0, 1, 1, 0};
private int[][] offsetCol = { // 行数对应的列变化规则
{-1, 0, 1, 1, 0, -1}, // 上
{-1, 0, 1, 0, -1, -1}, // 中
{0, 1, 1, 0, -1, -1} // 下
};
// 左边,顺时针旋转
private long clockwiseRotate(long parentState, int row, int col) {
long childState = 0;
int choiceCol = getColIndex(row);
for (int i = 0; i < 6; i++) {
int index = previousLength[row + offsetRow[i]] + col + offsetCol[choiceCol][i];
if (isExist(parentState, index)) {
childState |= getValue(previousLength[row + offsetRow[(i + 1) % 6]] + col + offsetCol[choiceCol][(i + 1) % 6]);
parentState ^= getValue(index);
}
}
return childState | parentState;
}
// 原点对称交换
private long originSymmetric(long parentState, int row, int col) {
long childState = 0;
int choiceCol = getColIndex(row);
for (int i = 0; i < 6; i++) {
int index = previousLength[row + offsetRow[i]] + col + offsetCol[choiceCol][i];
if (isExist(parentState, index)) {
childState |= getValue(previousLength[row + offsetRow[(i + 3) % 6]] + col + offsetCol[choiceCol][(i + 3) % 6]);
parentState ^= getValue(index);
}
}
return childState | parentState;
}
// 右边,逆时针旋转
private long anticlockwiseRotate(long parentState, int row, int col) {
long childState = 0;
int choiceCol = getColIndex(row);
for (int i = 5; i >= 0; i--) {
int index = previousLength[row + offsetRow[i]] + col + offsetCol[choiceCol][i];
if (isExist(parentState, index)) {
if (i > 0)
childState |= getValue(previousLength[row + offsetRow[i - 1]] + col + offsetCol[choiceCol][i - 1]);
else
childState |= getValue(previousLength[row + offsetRow[5]] + col + offsetCol[choiceCol][5]);
parentState ^= getValue(index);
}
}
return childState | parentState;
}
// 选择行数对应的列变化规则
private int getColIndex(int row) {
if (row < previousLength.length / 2) {
return 0;
} else if (row == previousLength.length / 2) {
return 1;
} else {
return 2;
}
}
// 点是否存在
private boolean isExist(long state, int index) {
return (state >> index & 1) == 1;
}
// 2的index次方
private long getValue(int index) {
return (long) Math.pow(2, index);
}
// 估值函数
private long getEstimatedValue(Node node) {
return node.getState() + Math.abs(node.getState() - targetState);
}
// 节点下沉
private void shiftDown(int index) {
nodes[0] = nodes[index];
for (int parentIndex = 0, childIndex = (parentIndex << 1) + 1;
childIndex < index;
parentIndex = childIndex, childIndex = (parentIndex << 1) + 1) {
if (childIndex + 1 < index &&
nodes[childIndex + 1].getWeight() <= nodes[childIndex].getWeight()) {
++childIndex;
}
if (nodes[childIndex].getWeight() > nodes[parentIndex].getWeight()) {
break;
}
Node node = nodes[parentIndex];
nodes[parentIndex] = nodes[childIndex];
nodes[childIndex] = node;
}
}
// 节点上浮
private void shiftUp(int index) {
for (int parentIndex = index - 1 >> 1;
parentIndex >= 0 && nodes[parentIndex].getWeight() > nodes[index].getWeight();
index = parentIndex, parentIndex = index - 1 >> 1) {
Node node = nodes[parentIndex];
nodes[parentIndex] = nodes[index];
nodes[index] = node;
}
}
// 打印搜索信息
private void printSearchInfo(long startTime, String result) {
System.out.println("搜索时间:" + (System.currentTimeMillis() - startTime) + "毫秒");
System.out.println(result);
System.out.println("搜索节点数量:" + sum);
System.out.format("节点数组长度:%d 有效长度:%d 存储空间利用率:%f%%%n", nodes.length, nodesIndex, (double) nodesIndex * 100 / nodes.length);
}
// 打印路径
private void printPath(Node node) {
while (node.getParent() != null) {
System.out.format("step:%d direction:%c row:%d col:%d%n", node.getStep(), node.getDirection(), node.getRow(), node.getCol());
node = node.getParent();
}
}
}
NodeUtil.java
public class NodeUtil {
// 点击的边界,六边形的次外圈大小
public static int[] getRange(int[][] state) {
int[] range = new int[state.length - 2];
for (int i = 1; i < state.length - 1; i++) {
range[i - 1] = state[i].length - 2;
}
return range;
}
// 状态值
public static long getStateValue(int[][] state) {
int sum = 0;
long value = 0;
for (int row = 0; row < state.length; row++) {
for (int col = 0; col < state[row].length; col++) {
if (state[row][col] == 1) {
value |= (long) Math.pow(2, sum + col);
}
}
sum += state[row].length;
}
return value;
}
// 六边形对应的行前面的点数量之和
public static int[] getPreviousLength(int[][] state) {
int[] previousLength = new int[state.length];
int sum = 0;
for (int row = 0; row < previousLength.length; row++) {
previousLength[row] = sum;
sum += state[row].length;
}
return previousLength;
}
}
NodeTreeTest.java
public class NodeTreeTest {
public static void main(String[] args) {
int[][] startState = {
{0, 0, 0, 0},
{1, 0, 0, 0, 1},
{0, 0, 0, 0, 0, 0},
{0, 0, 0, 1, 0, 0, 0},
{0, 1, 0, 0, 1, 0},
{0, 0, 1, 0, 0},
{0, 0, 0, 0}
};
int[][] endState = {
{0, 0, 0, 0},
{0, 0, 0, 0, 0},
{0, 0, 1, 1, 0, 0},
{0, 0, 0, 1, 0, 0, 0},
{0, 0, 1, 0, 0, 0},
{0, 1, 1, 0, 0},
{0, 0, 0, 0}
};
int depth = 5;
System.out.println("startState:" + NodeUtil.getStateValue(startState));
System.out.println("endState:" + NodeUtil.getStateValue(endState));
System.out.println("depth:" + depth);
NodeTree nodeTree = new NodeTree(startState, endState, depth);
nodeTree.search();
}
}
测试结果
总结
不要以为这个结果的时间很慢或者很快,有些情况内存溢出也没算出来(起始状态和目标状态差值的绝对值大),有些情况则在毫秒级别之内找到答案(起始状态和目标状态差值的绝对值小),我想这和估值函数有关,如果估值函数写的更好,这些情况发生可能性就会很小。