迷宫问题
给定一张迷宫地图和一个迷宫入口,和出口 找到一条可以通过的道路
迷宫地图
我们使用一个二维数组代表迷宫,其中
- 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个栈实现。基础知识还是很重要的,环环相扣,前面学习的数组可以作为队列的基石,数组和链表又可以用来实现栈,紧接着 栈又可以用来实现回溯算法。
所以以前跳过这些基础知识 直接看树,搞不懂是有道理的 。