1. 程式人生 > >基礎搜尋演算法,華容道小遊戲解法

基礎搜尋演算法,華容道小遊戲解法

首先,本人宣告一下,這篇部落格是看了其他人寫的部落格之後,對這個面試題十分感興趣,所以才想分享一下。 在明白這個面試題的時候,先通過一個簡單的迷宮問題,明白一下幾個問題: 迷宮 什麼是“搜尋”?什麼是“剪枝”?什麼是“回溯”? “搜尋”: 每個點都可以按照右下左上的方向進行嘗試,如果是“牆壁”,就換一個方向,如果可以走,就往前走到下一點,然後再接著嘗試。 “剪枝”: 之前走過的路,就不在往那邊走了,因為回去的話,下一步還要回來。不在搜尋一些明顯不對的地方,剪掉沒用的分支,提高效率。 “回溯”: 如果我們正在往前搜尋,前面沒有路的時候,我們就需要返回來,找到之前有可能出現岔路口的地方,再去下一個方向搜尋 明白以上問題以後,來看這個類似華容道的問題: 在這裡插入圖片描述

空格可以和上下左右的數字進行交換,你可以認為空格在移動。如果移動成:

在這裡插入圖片描述 如圖所示,空格不能往右走,被牆擋住了,不過可以往上、下、左走,這個狀態,空格就有上、下、左三個方向可以進行搜尋,空格往下走一步,這時候可以往左和往上,往上的話又回到原來狀態了,所以應該“剪枝”,那麼什麼時候應該進行“回溯”呢?就是在當前狀態無論哪個方向都行不通的時候,但是這時候的“回溯”,不是像迷宮問題中走不通的時候才用,二十前面的路是之前走過的,也不應該再往前走了,應該“回溯”,像這個華容道,如果是按照右上方的方向移動,確實可以無線進行。但是轉到第三圈的時候,會發現和剛開始的狀態一直,所以不應該再繼續轉下去了,所以這個時候應該考慮“回溯”。 那麼明白搜尋過程後,應該怎麼寫程式碼實現“回溯”演算法呢?

看到這裡我們會想到用棧去操作,但是這個問題直接用棧的話,編碼比較難,所以我們可以使用遞迴來實現這個搜尋過程。意思是把搜尋定義成一個函式,然後裡面又調用搜索這個函式自身,當搜尋到“窮途末路”時,搜尋函式返回,會自動會到上一層的呼叫中,從而完成回溯。 那麼什麼時候判斷九宮格能否達到遊戲勝利的狀態呢? 如果每條路都被堵住,回溯演算法就會回溯到起點,如果回溯到起點狀態,還沒有找到遊戲勝利的話,就是不能達到勝利狀態。 正確的移動步驟應該如何記錄呢? 在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

之所以喜歡這個面試題,是因為這個面試題用到了棧、佇列、連結串列等結構的思想,對他們的理解也更加深刻了,如果還覺得不錯,點個關注支援下唄。