1. 程式人生 > 其它 >資料結構學習5-使用遞迴解決迷宮問題

資料結構學習5-使用遞迴解決迷宮問題

技術標籤:基礎知識java演算法

迷宮問題

給定一張迷宮地圖和一個迷宮入口,和出口 找到一條可以通過的道路
在這裡插入圖片描述

迷宮地圖

我們使用一個二維陣列代表迷宮,其中

  • 0 代表沒有走過
  • 1 代表障礙物
  • 2表示路可以走
  • 3 標識已走過,但是不通
  • 9 代表終點

關鍵程式碼

  • go方法 根據執行的選擇方向模式,探測下一步應該往哪走,當4個方向都不滿足的時候,就需要對回滾到上一個操作點,這個地方使用到了棧,

  • goWithDynamic方法 根據當前點和終點的位置進行動態判定4個方向的優先順序,但是針對於很多障礙物的地圖,它表現的並沒有想象中那麼好,甚至有點差

完整程式碼

public class Maze {

    /**
     * 地圖
     * 0 代表沒有走過 1標識牆  2表示路可以走 3 標識已走過,但是不通
     * 9 終點
     */
private int[][] map; /** * 初始位置 */ private int[] cur; /** * 終點的位置 */ private int[] end; /** * 用於計數 */ private int count; /** * 存放軌跡路徑 用於回溯 */ private Stack<Integer[]> track; public Maze(int row, int col) { initMap
(row, col); // 初始化一個軌跡路徑 track = new Stack<>(); } private void initMap(int row, int col) { // 設定一個邊框 輔助程式執行 row += 2; col += 2; map = new int[row][col]; // 初始化牆 for (int i = 0; i < col; i++) { map[0][i] = 1; map[
row - 1][i] = 1; } for (int i = 0; i < row; i++) { map[i][0] = 1; map[i][col - 1] = 1; } } /** * 設定柵欄 */ public Maze setBarrier(int row, int col) { map[row][col] = 1; return this; } public void print() { // 將迷宮輸出 System.out.println("地圖:"); for (int i = 0; i < map.length; i++) { for (int j = 0; j < map[i].length; j++) { System.out.printf("\t%s", map[i][j]); } System.out.print("\n"); } } public Maze start(int row, int col) { if (map[row][col] != 0) { throw new RuntimeException("起點座標設定有誤"); } // 起點預設走了 map[row][col] = 2; cur = new int[]{row, col}; return this; } /** * 設定終點 */ public Maze end(int row, int col) { // 檢查終點位置 if (map[row][col] != 0) { throw new RuntimeException("終點座標設定有誤"); } end = new int[]{row, col}; map[row][col] = 9; return this; } /** * 開始執行 */ public void go(int goMode) { // 計數 count++; boolean result = false; if (goMode == 1) { // 順時針 result = goWithClockwise(); } else if (goMode == 2) { // 逆時針 result = goWithAnticlockwise(); } else if (goMode == 3) { result = goWithDynamic(); } if (!result) { // 回溯 if (track.isEmpty()) { throw new RuntimeException("當前位置已不可達,需要重新規劃路線"); } else { System.out.println("執行回溯...."); // 標記當前點為可達但是道路不通 map[cur[0]][cur[1]] = 3; Integer[] pop = track.pop(); cur[0] = pop[0]; cur[1] = pop[1]; System.out.printf("當前位置:(%d,%d)\n", cur[0], cur[1]); } } // 列印 print(); // 繼續往下走 if (map[cur[0]][cur[1]] != 9) { go(goMode); } else { // 終點入棧 track.push(new Integer[]{cur[0], cur[1]}); System.out.println("抵達終點...共執行:" + count + "次,實際路徑長度:" + track.size()); // 列印軌跡 StringBuilder resultTrack = new StringBuilder(); while (!track.isEmpty()) { Integer[] pop = track.pop(); resultTrack.append("(" + pop[0] + "," + pop[1] + ")").append(" <- "); } resultTrack.append("開始"); System.out.println(resultTrack); } } /** * 順時針的走 上右下左 */ private boolean goWithClockwise() { return goTo(new int[]{1, 2, 3, 4}); } /** * 逆時針走 下 右 上 左 */ private boolean goWithAnticlockwise() { return goTo(new int[]{3, 2, 1, 4}); } /** * 根據當前的位置和終點的位置 動態計算4個方向的順序 * 每次都會重新計算該怎麼走 * * @return */ private boolean goWithDynamic() { int[] dir; // 1 2 3 4 上右下左 if (cur[0] < end[0]) { // 終點在下半局 if (cur[1] < end[1]) { // 終點在右邊 dir = new int[]{2, 3, 1, 4}; } else { // 左邊 dir = new int[]{4, 3, 1, 2}; } } else { // 終點在上半局 if (cur[1] < end[1]) { // 終點在右邊 dir = new int[]{1, 2, 3, 4}; } else { // 左邊 dir = new int[]{1, 4, 2, 3}; } } return goTo(dir); } /** * 按照指定順序前往下一個點 * * @param dir 1 2 3 4 上右下左 * @return */ private boolean goTo(int[] dir) { boolean result = false; for (int i = 0; i < 4; i++) { if (dir[i] == 1) { // 往上走 result = goTo(-1, 0); } else if (dir[i] == 2) { // 往右邊走 result = goTo(0, 1); } else if (dir[i] == 3) { // 往下邊走 result = goTo(1, 0); } else { // 往左邊走 result = goTo(0, -1); } if (result) { return result; } } return result; } /** * 去下一個可達的點 */ private boolean goTo(int rowOffset, int colOffset) { int row = cur[0] + rowOffset, col = cur[1] + colOffset; boolean canGo = false; if (map[row][col] == 9) { canGo = true; } else if (map[row][col] == 0) { // 下個點沒有走過 map[row][col] = 2; canGo = true; } if (canGo) { // 當前位置入棧 track.push(new Integer[]{cur[0], cur[1]}); // 將下一個點的座標作為新的位置 cur[0] = row; cur[1] = col; } return canGo; } public static void main(String[] args) { Maze maze = new Maze(6, 5); // // 順時針 (6,5) <- (5,5) <- (4,5) <- (3,5) <- (2,5) <- (1,5) <- (1,4) <- (1,3) <- (2,3) <- (3,3) <- (4,3) <- 開始 // maze.setBarrier(3, 1).setBarrier(3, 2).start(4, 3).end(6, 5).go(1); // // // // 逆時針結果 (6,5) <- (6,4) <- (6,3) <- (5,3) <- (4,3) <- 開始 // maze.setBarrier(3, 1).setBarrier(3, 2).start(4, 3).end(6, 5).go(2); // 測試回溯 使用逆時針 終點放在 1 1 上面 // maze.setBarrier(3, 1).setBarrier(3, 2).start(4,3).end(1, 1).go(3); // 順時針結果 // (1,1) <- (2,1) <- (2,2) <- (1,2) <- (1,3) <- (2,3) <- (3,3) <- (4,3) <- 開始 // 逆時針結果 // (1,1) <- (2,1) <- (2,2) <- (1,2) <- (1,3) <- (2,3) <- (3,3) <- (3,4) <- (2,4) <- (1,4) <- (1,5) <- (2,5) <- (3,5) <- (4,5) <- (5,5) <- (6,5) <- (6,4) <- (6,3) <- (5,3) <- (4,3) <- 開始 // 動態計算 // (1,1) <- (1,2) <- (1,3) <- (2,3) <- (3,3) <- (4,3) <- 開始 // 多設定一些柵欄 使用動態判斷方向優先順序的時候考慮不到柵欄存在的情況 因為對於判定方向優先順序而言 它感知不到柵欄的存在 maze .setBarrier(3, 1) .setBarrier(3, 2) .setBarrier(3, 3) .setBarrier(3, 4) .setBarrier(1,2) .setBarrier(5, 5) .start(4, 3).end(1, 1).go(3); } }

使用遞迴需要遵守的重要守則

在這裡插入圖片描述

總結

回溯其實就是將每一步有效的操作都壓入一個棧中,然後通過出棧和入棧實現對操作的撤銷和重新執行。所以redo和undo 可以通過2個棧實現。基礎知識還是很重要的,環環相扣,前面學習的陣列可以作為佇列的基石,陣列和連結串列又可以用來實現棧,緊接著 棧又可以用來實現回溯演算法。
所以以前跳過這些基礎知識 直接看樹,搞不懂是有道理的 。