基础搜索算法,华容道小游戏解法

首先,本人声明一下,这篇博客是看了其他人写的博客之后,对这个面试题十分感兴趣,所以才想分享一下。
在明白这个面试题的时候,先通过一个简单的迷宫问题,明白一下几个问题:
迷宫
什么是“搜索”?什么是“剪枝”?什么是“回溯”?
搜索”:
每个点都可以按照右下左上的方向进行尝试,如果是“墙壁”,就换一个方向,如果可以走,就往前走到下一点,然后再接着尝试。
剪枝”:
之前走过的路,就不在往那边走了,因为回去的话,下一步还要回来。不在搜索一些明显不对的地方,剪掉没用的分支,提高效率。
回溯”:
如果我们正在往前搜索,前面没有路的时候,我们就需要返回来,找到之前有可能出现岔路口的地方,再去下一个方向搜索
明白以上问题以后,来看这个类似华容道的问题:
在这里插入图片描述
空格可以和上下左右的数字进行交换,你可以认为空格在移动。如果移动成:

在这里插入图片描述
如图所示,空格不能往右走,被墙挡住了,不过可以往上、下、左走,这个状态,空格就有上、下、左三个方向可以进行搜索,空格往下走一步,这时候可以往左和往上,往上的话又回到原来状态了,所以应该“剪枝”,那么什么时候应该进行“回溯”呢?就是在当前状态无论哪个方向都行不通的时候,但是这时候的“回溯”,不是像迷宫问题中走不通的时候才用,二十前面的路是之前走过的,也不应该再往前走了,应该“回溯”,像这个华容道,如果是按照右上方的方向移动,确实可以无线进行。但是转到第三圈的时候,会发现和刚开始的状态一直,所以不应该再继续转下去了,所以这个时候应该考虑“回溯”。
那么明白搜索过程后,应该怎么写代码实现“回溯”算法呢?
看到这里我们会想到用栈去操作,但是这个问题直接用栈的话,编码比较难,所以我们可以使用递归来实现这个搜索过程。意思是把搜索定义成一个函数,然后里面又调用搜索这个函数自身,当搜索到“穷途末路”时,搜索函数返回,会自动会到上一层的调用中,从而完成回溯。
那么什么时候判断九宫格能否达到游戏胜利的状态呢?
如果每条路都被堵住,回溯算法就会回溯到起点,如果回溯到起点状态,还没有找到游戏胜利的话,就是不能达到胜利状态。
正确的移动步骤应该如何记录呢?
在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

之所以喜欢这个面试题,是因为这个面试题用到了栈、队列、链表等结构的思想,对他们的理解也更加深刻了,如果还觉得不错,点个关注支持下呗。

猜你喜欢

转载自blog.csdn.net/weixin_42719412/article/details/82817866
今日推荐