資料結構學習5-使用遞迴解決迷宮問題
阿新 • • 發佈:2021-02-19
迷宮問題
給定一張迷宮地圖和一個迷宮入口,和出口 找到一條可以通過的道路
迷宮地圖
我們使用一個二維陣列代表迷宮,其中
- 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個棧實現。基礎知識還是很重要的,環環相扣,前面學習的陣列可以作為佇列的基石,陣列和連結串列又可以用來實現棧,緊接著 棧又可以用來實現回溯演算法。
所以以前跳過這些基礎知識 直接看樹,搞不懂是有道理的 。