首先,本人声明一下,这篇博客是看了其他人写的博客之后,对这个面试题十分感兴趣,所以才想分享一下。
在明白这个面试题的时候,先通过一个简单的迷宫问题,明白一下几个问题:
什么是“搜索”?什么是“剪枝”?什么是“回溯”?
“搜索”:
每个点都可以按照右下左上的方向进行尝试,如果是“墙壁”,就换一个方向,如果可以走,就往前走到下一点,然后再接着尝试。
“剪枝”:
之前走过的路,就不在往那边走了,因为回去的话,下一步还要回来。不在搜索一些明显不对的地方,剪掉没用的分支,提高效率。
“回溯”:
如果我们正在往前搜索,前面没有路的时候,我们就需要返回来,找到之前有可能出现岔路口的地方,再去下一个方向搜索
明白以上问题以后,来看这个类似华容道的问题:
空格可以和上下左右的数字进行交换,你可以认为空格在移动。如果移动成:
如图所示,空格不能往右走,被墙挡住了,不过可以往上、下、左走,这个状态,空格就有上、下、左三个方向可以进行搜索,空格往下走一步,这时候可以往左和往上,往上的话又回到原来状态了,所以应该“剪枝”,那么什么时候应该进行“回溯”呢?就是在当前状态无论哪个方向都行不通的时候,但是这时候的“回溯”,不是像迷宫问题中走不通的时候才用,二十前面的路是之前走过的,也不应该再往前走了,应该“回溯”,像这个华容道,如果是按照右上方的方向移动,确实可以无线进行。但是转到第三圈的时候,会发现和刚开始的状态一直,所以不应该再继续转下去了,所以这个时候应该考虑“回溯”。
那么明白搜索过程后,应该怎么写代码实现“回溯”算法呢?
看到这里我们会想到用栈去操作,但是这个问题直接用栈的话,编码比较难,所以我们可以使用递归来实现这个搜索过程。意思是把搜索定义成一个函数,然后里面又调用搜索这个函数自身,当搜索到“穷途末路”时,搜索函数返回,会自动会到上一层的调用中,从而完成回溯。
那么什么时候判断九宫格能否达到游戏胜利的状态呢?
如果每条路都被堵住,回溯算法就会回溯到起点,如果回溯到起点状态,还没有找到游戏胜利的话,就是不能达到胜利状态。
正确的移动步骤应该如何记录呢?
在search开始的时候把步骤记录下来,如果search未成功,则将之前的记录下来,如果search未成功,则将之前记录的步骤去除
那么我们为了保证得到的解法是最优解的,还要了解一下什么“深搜”和“广搜”
深搜:
深度优先搜索,会在一个方向一直搜下去,直到这条路走不通,才会考虑第二个方向
广搜:
广度优先搜索,会优先搜索所有方向的第一步,然后再接着搜索每一个可行的方向的第二部,以此类推
那么什么时候使用广搜?什么时候使用深搜呢?
广搜:广搜比深搜多了一个队列来存储要搜索的状态,但是它却能够得到最优解,空间换时间,所以要得到最优解的时候,要使用广搜
深搜:当要得到所有解的时候,深搜的空间更少,编码更简单,所以适合使用深搜
要将搜索的初始状态加到一个队列里,然后每次从队列中取出一个状态,往可以前进的方向前进一步,然后再将该状态放到队列。利用队列先进先出的特点,就可以实现广搜的效果,若果队列空了, 还没有找到出口,说明所有的方向都找不到,游戏自然无法胜利。
记录游戏胜利:利用链表的思想,每一步记录上一步的状态和这次的方向,这样在达到最终胜利状态的时候,可以找到这个状态的上一步,而上一步又可以找到上上步把每一步都和它的上一步链起来
具体的代码编写:
package huarongdao;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
public class HuaRongDao {
// 定义方向
private static final int LEFT = 1;
private static final int RIGHT = 2;
private static final int UP = 3;
private static final int DOWN = 4;
// 3x3的九宫格
private int[][] arr;
// 记录空格的位置
private int x;
private int y;
// 定义移动的数组
private List<Integer> moveArr = new LinkedList<Integer>();
// 定义终点状态
private static final Integer WIN_STATE = 123456780;
// 保存已经搜索过的状态
private Set<Integer> statusSet = new HashSet<Integer>();
// 初始化,数字0代表空格,先遍历,找出空格的位置
public HuaRongDao(int[][] arr) {
this.arr = arr;
for(int i=0; i<arr.length; i++) {
for(int j=0; j<arr.length; j++) {
if(arr[i][j] == 0) {
x = i;
y = j;
}
}
}
}
// 判断是否可以朝某个方向进行移动
private boolean canMove(int direction) {
switch (direction) {
// y > 0才能左移
case LEFT:
return y > 0;
// y < 2才能右移
case RIGHT:
return y < 2;
// x > 0才能上移
case UP:
return x > 0;
// x < 2才能下移
case DOWN:
return x < 2;
}
return false;
}
// 朝某个方向进行移动,该函数不作判断,直接移动
// 调用前请自行用canMove先行判断
private void move(int direction) {
int temp;
switch (direction) {
// 空格和左侧数字交换
case LEFT:
temp = arr[x][y - 1];
arr[x][y - 1] = 0;
arr[x][y] = temp;
y = y - 1;
break;
// 空格和右侧数字交换
case RIGHT:
temp = arr[x][y + 1];
arr[x][y + 1] = 0;
arr[x][y] = temp;
y = y + 1;
break;
// 空格和上方数字交换
case UP:
temp = arr[x - 1][y];
arr[x - 1][y] = 0;
arr[x][y] = temp;
x = x - 1;
break;
// 空格和下方数字交换
case DOWN:
temp = arr[x + 1][y];
arr[x + 1][y] = 0;
arr[x][y] = temp;
x = x + 1;
break;
}
// 该方向记录
moveArr.add(direction);
}
// 某个方向的回退,该函数不作判断,直接移动
// 其操作和move方法正好相反
private void moveBack(int direction) {
int temp;
switch (direction) {
// 空格和左侧数字交换
case LEFT:
temp = arr[x][y + 1];
arr[x][y + 1] = 0;
arr[x][y] = temp;
y = y + 1;
break;
// 空格和右侧数字交换
case RIGHT:
temp = arr[x][y - 1];
arr[x][y - 1] = 0;
arr[x][y] = temp;
y = y - 1;
break;
// 空格和上方数字交换
case UP:
temp = arr[x + 1][y];
arr[x + 1][y] = 0;
arr[x][y] = temp;
x = x + 1;
break;
// 空格和下方数字交换
case DOWN:
temp = arr[x - 1][y];
arr[x - 1][y] = 0;
arr[x][y] = temp;
x = x - 1;
break;
}
// 记录的移动步骤出栈
moveArr.remove(moveArr.size() - 1);
}
// 获取状态,这里把9个数字按顺序组成一个整数来代表状态
// 方法不唯一,只要能区分九宫格状态就行
private Integer getStatus() {
int status = 0;
for(int i=0; i<arr.length; i++) {
for(int j=0; j<arr.length; j++) {
status = status * 10 + arr[i][j];
}
}
return status;
}
// 搜索方法
private boolean search(int direction) {
// 如果能够朝该方向行走
if(canMove(direction)) {
// 往该方向移动
move(direction);
// 移动后的状态
Integer status = getStatus();
// 如果已经是胜利状态,返回true
if(WIN_STATE.equals(status)) {
return true;
}
// 如果是之前走过的状态了,返回false
if(statusSet.contains(status)) {
// 这一步走错了,回退
moveBack(direction);
return false;
}
// 将当前状态存入set
statusSet.add(status);
// 继续朝四个方向进行搜索
boolean searchFourOk = search(RIGHT) || search(DOWN) || search(LEFT) || search(UP);
if(searchFourOk) {
return true;
} else {
// 这一步走错了,把它的记录去除
moveBack(direction);
return false;
}
}
return false;
}
// 解题入口方法
public boolean solve() {
Integer status = getStatus();
// 如果已经是胜利状态,返回true
if(WIN_STATE.equals(status)) {
return true;
}
// 初始状态先记录
statusSet.add(status);
// 朝4个方向进行搜索
return search(RIGHT) || search(DOWN) || search(LEFT) || search(UP);
}
// 打印路径
public void printRoute() {
for(int i=0; i<moveArr.size(); i++) {
System.out.print(getDirString(moveArr.get(i)));
System.out.print(" ");
}
}
// 方向与其对应的字符串
private String getDirString(int dir) {
switch (dir) {
case LEFT:
return "左";
case RIGHT:
return "右";
case UP:
return "上";
case DOWN:
return "下";
}
return null;
}
// 打印当前华容道的状态
public void print() {
for(int i=0; i<arr.length; i++) {
for(int j=0; j<arr.length; j++) {
System.out.print(arr[i][j]);
System.out.print(" ");
}
System.out.println();
}
}
}
1 2 3
4 5 6
8 7 0
无法胜利
1 2 3
4 0 6
7 5 8
可以胜利,路径为:下 右
3 4 1
5 6 0
8 2 7
可以胜利,路径为:左 左 上 右 下 左 下 右 右 上 左 左 下 右 上 上 右 下 左 左 上 右 下 右 下
Process finished with exit code 0
之所以喜欢这个面试题,是因为这个面试题用到了栈、队列、链表等结构的思想,对他们的理解也更加深刻了,如果还觉得不错,点个关注支持下呗。