55

如何通过编程解决华容道问题?

 5 years ago
source link: https://blog.csdn.net/csdnsevenn/article/details/82782947?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

点击上方“ 程序人生 ”,选择“置顶公众号”

第一时间关注程序猿(媛)身边的故事

Fn2mIrU.jpg!web

小史是一个应届生,虽然学的是电子专业,但是自己业余时间看了很多互联网与编程方面的书,一心想进 BAT 互联网公司。

ZBB3qe6.jpg!web

作者

channingbreeze

已获原作者授权,如需转载,请联系原作者。

今天他就去一家外企面试了。

yiYfQ3N.jpg!web

【面试前】

面试前,小史就收到了中英文的面试邀请。

ERvIzir.jpg!web

去外企面试,最好要能够和面试官英语对话。小史除了复习算法之外,赶紧练起了口语。

aQby2i6.jpg!web

jYzyEnV.jpg!web

IvMfmaa.jpg!web

【面试现场】

be6BfmA.jpg!web

7ZBJj2v.jpg!web

fmEniui.jpg!web

面试官给了小史一个问题。(题目已翻译成中文,请自行脑补英文现场)

题目:我 有1到8八个数字,放在一个3x3的九宫格里面,那么会留下一个空格。

MnmI32I.jpg!web

空格可以和上下左右的数字进行交换,你可以认为空格在移动。如果移动成

qEJB3uq.jpg!web

则游戏胜利。

你需要完成以下2件事情:

1、给出数据结构来描述这个过程。

2、给你一个初始状态,告诉我能不能胜利,并给出如何移动才能胜利。

这有点像咱们中国的华容道游戏。

QfamInY.jpg!web

2eiMfa6.jpg!web

2yAvEzF.jpg!web

ayyyeii.jpg!web

mmQRruN.jpg!web

小史把他能想到的都写了下来。

import java.util.LinkedList;
import java.util.List;

/**
 * @author xiaoshi on 2018/9/8.
 */
public class HuaRongDao {

    // 定义方向
    public static final int LEFT = 1;
    public static final int RIGHT = 2;
    public static final int UP = 3;
    public static final int DOWN = 4;

    // 3x3的九宫格
    private int[][] arr;

    // 记录空格的位置
    private int x;
    private int y;

    // 定义移动的数组
    private List<Integer> moveArr = new LinkedList<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;
                }
            }
        }
    }

    // 判断是否可以朝某个方向进行移动
    public 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先行判断
    public 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;
        }
    }

    // 打印当前华容道的状态
    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();
        }
    }

}

aiE7ve6.jpg!web

UNjy6nb.jpg!web

AZFjmm7.jpg!web

r2aQvmv.jpg!web

Z3eMjmQ.jpg!web

2miiEjR.jpg!web

【请教大神】

小史回到学校,把面试的情况和计算机学院的吕老师说了一下。

FjUJv26.jpg!web

7veqyq2.jpg!web

6nA7Fb3.jpg!web

【迷宫问题】

2qamue6.jpg!web

iErEJbr.jpg!web

zMJzYvU.jpg!web

yYN3uy2.jpg!web

V3QbQvE.jpg!web

by6fqqQ.jpg!web

小史:每个点都可以按照右下左上的方向来进行尝试,如果是墙壁,就换一个方向,如果可以走,就往前走到下一点,然后再接着尝试。直到到达终点为止。

ju6fa22.jpg!web

AzEnQzE.jpg!web

viyq6vb.jpg!web

b2qYNrZ.jpg!web

632MbaN.jpg!web

nmMRVrV.jpg!web

If67FjI.jpg!web

吕老师随手又画了一个迷宫。

quyIbuZ.jpg!web

ZvUNN3u.jpg!web

UNvqIz3.jpg!web

AfyiuyV.jpg!web

EZN7Bva.jpg!web

iyq6FbJ.jpg!web

NbEzayu.jpg!web

vInqyyz.jpg!web

e2ABnuU.jpg!web

吕老师:小史,这块并不是往左走,而是回退,退回到上一步。如果我们正在往前搜索,当然不能走回头路。但是当前面没有路的时候,我们就需要返回来,找到之前有可能出现岔路口的地方,再去下一个方向进行搜索。

JRfUneR.jpg!web

zaEbyq2.jpg!web

2iyiaya.jpg!web

【华容道问题】

r2iAfaZ.jpg!web

B77FZ3i.jpg!web

NBfmei6.jpg!web

YVVnAj2.jpg!web

BbeuQru.jpg!web

jeA7bqR.jpg!web

yiU7buY.jpg!web

小史:吕老师,我明白了,空格在华容道中移动,就好像我在迷宫里走动一样,每次到一个新的状态,就有几个方向可以搜索,如果是之前碰到过的状态,那就不搜索。

NRZFj2b.jpg!web

eq2MBjB.jpg!web

my6Fzyb.jpg!web

2IN3Inb.jpg!web

IBvYBjz.jpg!web

【递归实现回溯】

bMfmQjA.jpg!web

小史:“回溯”的过程有点像栈的操作。往前走一步就像是入栈,到了死胡同,要往回退,就像是出栈。

Ifeey2y.jpg!web

吕老师:这个过程确实是栈的过程,但是直接用栈的话,对于你刚刚接触搜索算法,可能编码比较难。其实你可以用递归来实现这个搜索过程。

fMnyqym.jpg!web

Anu6NbQ.jpg!web

MzU3men.jpg!web

RN3iyqZ.jpg!web

aMVZbyR.jpg!web

ENb2QnE.jpg!web

MfAV32R.jpg!web

nYnQFje.jpg!web

QVbQVfn.jpg!web

R3iMBbu.jpg!web

小史:我在走迷宫的时候,每走一步,就把这一步是往哪走的记录下来,但是碰到了死胡同,往回退的时候,我又把之前记录的步骤最后一步去掉。这样一来,达到终点的时候,我记下来的步骤就是一条从起点到终点的路径了。

AzIBn2Q.jpg!web

j2iQVnq.jpg!web

小史:记录移动路径,其实就是在真正搜索之前,把方向记录下来,而搜索如果要返回了,则说明该次搜索已经结束,没有结果,应该把该记录去除。

UVZ7riA.jpg!web

MnAJJfV.jpg!web

IJBJFvv.jpg!web

qiaQRn7.jpg!web

mIB36nR.jpg!web

【小史的努力】

吃完烤串,喝完小酒,小史和吕老师休闲地走在回学校的路上。

BFNBjuz.jpg!web

EzeaAnM.jpg!web

吕老师笑而不语。

回到宿舍,小史就打开了电脑,手在键盘上飞快地敲了起来。

理解了算法之后,小史的代码写起来也是非常快,不一会儿就写好了:

import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

/**
 * @author xiaoshi on 2018/9/8.
 */
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();
        }
    }

}

几个测试用例下来,小史眉头一皱,发现事情并不简单。

bmYvuib.jpg!web

小史经过缜密的分析,找到了原因。

26zAvuJ.jpg!web

我可以判断一下,如果某条路走的步数超过100步,就不再走了,赶紧回退。

小史在search函数中增加了moveArr.size()<100的判断,得到了最终结果。

uiui63v.jpg!web

【深搜和广搜】

第二天,小史得意洋洋地拿着自己的代码去找吕老师秀起来。

qY7BBzq.jpg!web

VN7VriU.jpg!web

JNVjAvI.jpg!web

小史:现在的算法,没办法保证得到的解法就是最优解,并且它很容易进入复杂的死胡同出不来,有点像一个死钻牛角尖的人。

3mYVVnu.jpg!web

NR3iYvr.jpg!web

FBN3Ifq.jpg!web

吕老师:深度优先搜索,会在一个方向一直搜下去,直到这条路走不通,才会考虑第二个方向。

BvmQNf3.jpg!web

吕老师:广度优先搜索,是先搜索每一个可行方向的第一步,然后再接着搜索每一个可行方向的第二步。以此类推。

zaemymv.jpg!web

YRvmMnI.jpg!web

VRZbuyE.jpg!web

RRBvYbN.jpg!web

小史:这个算法似乎没有“回溯”的必要了,没办法再用递归了吧?而且分头搜索这个方式应该怎么实现呢?

rIjqmyF.jpg!web

吕老师:你可以将要搜索的初始状态加到一个队列里,然后每次从队列中取出一个状态,往可以前进的方向前进一步,然后再将该状态放到队列。利用队列先进先出的特点,就可以实现广搜的效果。

zueEB3U.jpg!web

eAJrYri.jpg!web

mAnmQjE.jpg!web

2eINfyA.jpg!web

vmyym27.jpg!web

N7Z7jmm.jpg!web

EbQVb2f.jpg!web

小史:每一步都记录上一步的状态和这次的方向。这样在达到最终胜利状态的时候,可以找到这个状态的上一步。而上一步又可以找到上上步,这不就是链表么?

q6fIvmU.jpg!web

rAVjAr3.jpg!web

fYvaaaf.jpg!web

emauYvQ.jpg!web

理解了算法之后,小史的代码写起来也是非常快,不一会儿就写好了:

import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

/**
 * @author xiaoshi on 2018/9/9.
 */
public class HuaRongDao {

    // 定义方向
    private static final int LEFT = 0;
    private static final int RIGHT = 1;
    private static final int UP = 2;
    private static final int DOWN = 3;

    // 定义辅助数组
    private static final int[][] dxdy = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}};

    // 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>();

    // 代表广搜的每一步,通过lastItem链起来
    private class SearchItem {
        private Integer status;
        private Integer dir;
        private SearchItem lastItem;
        SearchItem(Integer status, Integer dir, SearchItem lastItem) {
            this.status = status;
            this.dir = dir;
            this.lastItem = lastItem;
        }
        public Integer getStatus() {return status;}
        public Integer getDir() {return dir;}
        public SearchItem getLastItem() {return lastItem;}
    }

    // 广搜的存储队列
    private List<SearchItem> statusToSearch = new LinkedList<SearchItem>();

    // 初始化,数字0代表空格,先遍历,找出空格的位置
    public HuaRongDao(int[][] arr) {
        this.arr = arr;
        getXY();
    }

    // 获取空格的x和y坐标
    private void getXY() {
        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;
    }

    // 找出该方向的相反方向
    private int getBackDir(int direction) {
        switch (direction) {
            // y > 0才能左移
            case LEFT:
                return RIGHT;
            // y < 2才能右移
            case RIGHT:
                return LEFT;
            // x > 0才能上移
            case UP:
                return DOWN;
            // x < 2才能下移
            case DOWN:
                return UP;
        }
        return 0;
    }

    // 朝某个方向进行移动,该函数不作判断,直接移动
    // 调用前请自行用canMove先行判断
    private void move(int direction) {
        int temp;
        temp = arr[x + dxdy[direction][0]][y + dxdy[direction][1]];
        arr[x + dxdy[direction][0]][y + dxdy[direction][1]] = 0;
        arr[x][y] = temp;
        x = x + dxdy[direction][0];
        y = y + dxdy[direction][1];
    }

    // 某个方向的前进,该函数不作判断,直接移动
    private void moveForward(int direction) {
        move(direction);
        // 该方向记录
        moveArr.add(direction);
    }

    // 某个方向的回退,该函数不作判断,直接移动
    // 其操作和moveForward方法正好相反
    private void moveBack(int direction) {
        move(getBackDir(direction));
        // 记录的移动步骤出栈
        moveArr.remove(moveArr.size() - 1);
    }

    // 获取状态,这里把9个数字按顺序组成一个整数来代表状态
    // 方法不唯一,只要能区分九宫格状态就行
    public 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;
    }

    // 根据状态还原九宫格数组
    // 该方法是getStatus的逆过程
    public void recoverStatus(Integer status) {
        for(int i=0; i<arr.length; i++) {
            for(int j=0; j<arr.length; j++) {
                arr[2 - i][2 - j] = status % 10;
                status = status / 10;
            }
        }
        getXY();
    }

    // 搜索方法
    private boolean search() {
        // 如果还有要搜索的状态
        while(statusToSearch.size() > 0) {
            // 队首出列
            SearchItem item = statusToSearch.remove(0);
            Integer status = item.getStatus();
            // 搜到了
            if(status.equals(WIN_STATE)) {
                // 找到路径
                recordRoute(item);
                return true;
            }
            // 根据status还原arr和x,y
            recoverStatus(status);
            // 4个方向进行遍历
            for(int i=0; i<4; i++) {
                // 如果能够朝该方向行走
                if(canMove(i)) {
                    // 向前一步
                    moveForward(i);
                    status = getStatus();
                    // 之前搜过的状态
                    if (statusSet.contains(status)) {
                        moveBack(i);
                        // 放弃
                        continue;
                    }
                    // 新状态加入待搜索
                    statusSet.add(status);
                    statusToSearch.add(new SearchItem(status, i, item));
                    moveBack(i);
                }
            }
        }
        return false;
    }

    // 解题入口方法
    public boolean solve() {
        Integer status = getStatus();
        // 如果已经是胜利状态,返回true
        if(WIN_STATE.equals(status)) {
            return true;
        }
        // 初始状态先记录
        statusSet.add(status);
        // 初始状态进入搜索队列
        statusToSearch.add(new SearchItem(status, null, null));
        return search();
    }

    // 根据链表顺藤摸瓜,找到路径
    private void recordRoute(SearchItem item) {
        while(null != item.getLastItem()) {
            moveArr.add(0, item.getDir());
            item = item.getLastItem();
        }
    }

    // 打印路径
    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

zUZB3mv.jpg!web

BRrYJjn.jpg!web

iyqUryQ.jpg!web

一个问题一顿饭,吕老师不亏的。

【饭桌上的闲聊】

vqUnMjY.jpg!web

umMF7br.jpg!web

FNjqYzf.jpg!web

qyimInb.jpg!web

7bEBj2Q.jpg!web

rYFBVnF.jpg!web

zu26Nf7.jpg!web

QjMFBvu.jpg!web

qMnaum3.jpg!web

PS:这次这篇花费了两周时间以及小史两顿饭钱。如果你看到了这里,并且有所收获的话,可以动动手指转发一下哦,小史和吕老师都会感谢你。

- The End -

「若你有原创文章想与大家分享,欢迎投稿。」

加编辑微信ID,备注#投稿#:

程序 丨 druidlost  

小七 丨 duoshangshuang

程序人生地区交流群,了解一下

根据你所在的城市,回复相应关键词“北京”,“沪杭”或“广东”,我会拉你进相应地区群哦~(目前只有该三个地区群)

(注意:进你所在城市或者最希望进的地区群,以上3个地区群不必重复进入,谢谢配合。)

FZBz6bB.jpg!web

上期精彩内容

YZFb22Z.png!web

neIvUbQ.gif


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK