摘要: 使用栈的数据结构及相应的回溯算法实现迷宫创建及求解,带点JavaGUI 的基础知识。

难度: 中级

  迷宫问题是栈的典型应用,栈通常也与回溯算法连用。 回溯算法的基本描述是:

  (1)  选择一个起始点;

(2)  如果已达目的地, 则跳转到 (4); 如果没有到达目的地, 则跳转到 (3) ;

  (3)  求出当前的可选项;

a.  若有多个可选项,则通过某种策略选择一个选项,行进到下一个位置,然后跳转到 (2);

b.  若行进到某一个位置发现没有选项时,就回退到上一个位置,然后回退到 (2) ;

(4) 退出算法。

在回溯算法的实现中,通常要使用栈来保存行进中的位置及选项。本文给出自己写的迷宫回溯算法实现及简要说明。

  1.  首先给出栈的抽象数据结构 StackADT<T> : 主要是入栈、出栈、判断空、长度、展示操作;

  1. package zzz.study.javagui.maze;
  2.  
  3. public interface StackADT<T> {
  4.  
  5. /* 判断是否为空栈;若为空,返回TRUE, 否则返回FALSE */
  6. public boolean isEmpty();
  7.  
  8. /* 入栈操作: 将元素 e 压入栈中 */
  9. public void push(T e);
  10.  
  11. /* 出栈操作: 若栈非空,将栈顶元素弹出并返回;若栈空,则抛出异常 */
  12. public T pop();
  13.  
  14. /* 返回栈顶元素,但并不将其弹出 */
  15. public T peek();
  16.  
  17. /* 返回栈长度,即栈中元素的数目 */
  18. public int size();
  19.  
  20. /* 遍历操作: 若栈非空,遍历栈中所有元素 */
  21. public String toString();
  22.  
  23. }

  2.  可变长的栈的实现 DyStack<T>: 借助 ArrayList 及一个指针来实现。注意到这里使用泛型来保证栈的通用性。

  1. package zzz.study.javagui.maze;
  2.  
  3. import java.util.ArrayList;
  4.  
  5. public class DyStack<T> implements StackADT<T> {
  6.  
  7. private final int INIT_STACK_SIZE = 20;
  8.  
  9. ArrayList<T> ds; // 栈元素列表
  10. private int top; // 栈顶索引:当前栈顶元素的下一个元素的索引
  11.  
  12. /*
  13. * 构造器:
  14. * 使用默认容量创建一个空栈
  15. *
  16. */
  17. public DyStack() {
  18. top = 0;
  19. ds = new ArrayList<T>(INIT_STACK_SIZE);
  20. }
  21.  
  22. /*
  23. * 构造器:
  24. * 使用指定容量创建一个空栈
  25. *
  26. */
  27. public DyStack(int capacity) {
  28.  
  29. top = 0;
  30. ds = new ArrayList<T>(capacity);
  31. }
  32.  
  33. public boolean isEmpty() {
  34.  
  35. if (top == 0)
  36. return true;
  37. else
  38. return false;
  39. }
  40.  
  41. public void clear() {
  42. top = 0;
  43. ds.clear();
  44. }
  45.  
  46. public void push(T e) {
  47.  
  48. ds.add(top, e);
  49. top++;
  50. }
  51.  
  52. public T pop() {
  53.  
  54. if (ds.isEmpty())
  55. throw new StackEmptyException("The stack has been empty!");
  56. top--;
  57. T result = ds.get(top);
  58. ds.set(top, null);
  59. return result;
  60. }
  61.  
  62. public T peek() {
  63.  
  64. if (ds.isEmpty())
  65. return null;
  66. return ds.get(top - 1);
  67. }
  68.  
  69. public int size() {
  70. return top;
  71. }
  72.  
  73. public String toString() {
  74.  
  75. StringBuilder content = new StringBuilder(" ");
  76. for (int i = 0; i < top; i++) {
  77. content.append(" --> ");
  78. content.append(ds.get(i));
  79. }
  80. return content.toString();
  81. }
  82.  
  83. public int getTop() {
  84. return top;
  85. }
  86.  
  87. }
  88.  
  89. class StackEmptyException extends RuntimeException {
  90.  
  91. public StackEmptyException(String msg) {
  92. super(msg);
  93. }
  94. }

3.  迷宫位置的数据结构 Position: 这里为了"节约内存"方向选择了 byte 类型,实际上在小型程序里是不必要的,带来了繁琐的类型转换,也带来了一些隐藏的问题。比如 Set<Byte> dirs 包含 byte 1;  但 dirs.contains(1) 会返回 false , 而 dirs.contains((byte)1) 才会返回 true.  Position 要在集合中使用,最好实现 equals 和 hashCode 方法。注意 equals 不要写成了 equal,  hashCode 不要写成 hashcode , 这些我都犯过错的 :)

  1. package zzz.study.javagui.maze;
  2.  
  3. /*
  4. * 迷宫中的通道位置模型:
  5. * row: 所处位置的行坐标
  6. * col: 所处位置的列坐标
  7. * dir: 将要进行的搜索方向: 正东 1; 正南 2; 正西3; 正北 4;
  8. */
  9.  
  10. public class Position {
  11.  
  12. private int row;
  13. private int col;
  14. private byte dir;
  15.  
  16. public Position() {
  17. row = 0;
  18. col = 0;
  19. dir = 0;
  20. }
  21.  
  22. public Position(int row, int col, byte dir) {
  23. this.row = row;
  24. this.col = col;
  25. this.dir = dir;
  26. }
  27.  
  28. public Position(int row, int col) {
  29. this(row, col, 0);
  30. }
  31.  
  32. public Position(int row, int col, int dir) {
  33. this(row, col, (byte)dir);
  34. }
  35.  
  36. public int getRow() {
  37. return row;
  38. }
  39.  
  40. public int getCol() {
  41. return col;
  42. }
  43.  
  44. public short getDir() {
  45. return dir;
  46. }
  47.  
  48. public String toString() {
  49. String dirStr = "";
  50. switch (dir) {
  51. case 1: dirStr = "正东"; break;
  52. case 2: dirStr = "正南"; break;
  53. case 3: dirStr = "正西"; break;
  54. case 4: dirStr = "正北"; break;
  55. default: dirStr = "UNKNOWN"; break;
  56. }
  57. return "(" + row + "," + col + "," + dirStr + ")";
  58. }
  59.  
  60. public boolean equals(Object obj)
  61. {
  62. if (obj == this) { return true; }
  63. if (obj instanceof Position) {
  64. Position p = (Position) obj;
  65. if (p.row == this.row && p.col == this.col && p.dir == this.dir) {
  66. return true;
  67. }
  68. }
  69. return false;
  70. }
  71.  
  72. public int hashCode()
  73. {
  74. int result = 17;
  75. result = result * 31 + row;
  76. result = result * 31 + col;
  77. result = result * 31 + dir;
  78. return result;
  79. }
  80.  
  81. }

4.  迷宫的核心实现 Maze :

 里面注释说的比较清楚了,有五点说明一下:

  (1)  由于要使用回溯算法,必须使用一个 Map<Position, List<triedDirs>> 来记录每个位置已经尝试过的方向,使用一个栈 stackForCreate 记录当前行进的位置轨迹; 当回溯到前面的位置时,不再重复已经尝试过的方向,避免重复尝试陷入无限循环;

  (2) 方向选择上,耗费了一点空间,简单地实现了方向选择的概率设置;也就是将未尝试的方向列表按概率次数扩展成新的方向列表,然后随机从这个新的方向列表中选择;

  (3) 在抵达迷宫边界时,对方向加以限制,只允许往出口方向走;否则,回走会形成环路,由于回溯的特性,会将环路里面的墙全部"吃掉"!

  (4) 在迷宫展示上,为了简便使用了字符 IIII 完美等于 5 个空格完美等于 2 个 G, 实现了对齐问题; 虽然使用等宽字体,但似乎未起作用, 也尝试过 T, [T], [I], 这样的符号,但与空格难以对齐。写这个程序还是费了不少心思的 ^_^ 注意到 Maze 继承了 Observable , 支持 GUI 展示, 可以展示迷宫生成的过程, 也可以看到空格是如何一步步"吃掉"由 IIII 组成的墙的, interesting ~~

  (5) 为了展示出误导路径, 采用分治策略将迷宫切分成若干个子迷宫矩阵分别求解, 并将上一个子迷宫矩阵的终止点与下一个子迷宫矩阵的起始点衔接起来确保一定有一条通路从入口抵达出口。

  (6) 为什么创建迷宫的代码比求解迷宫的代码更多呢?因为求解迷宫可以尽可能地朝正东或正南即出口方向走,但创建迷宫必须选择随机方向。

  1. package zzz.study.javagui.maze;
  2.  
  3. import java.util.*;
  4. import java.util.concurrent.TimeUnit;
  5.  
  6. public class Maze extends Observable {
  7.  
  8. // 定义迷宫大小:行数 rows 和列数 cols
  9. private final int rows;
  10. private final int cols;
  11.  
  12. // 定义迷宫出口点位置: 行坐标 EXIT_ROW 和 列坐标 EXIT_COL
  13. private final int EXIT_ROW;
  14. private final int EXIT_COL;
  15.  
  16. // 定义迷宫矩阵mazeMatrix 和 标记数组 mark
  17. private boolean[][] mazeMatrix; // true: 可通行; false: 不可通行
  18. private short[][] mark;
  19.  
  20. private String mazeStr = ""; // 迷宫的字符串表示
  21. private String solution = ""; // 迷宫的解的字符串表示
  22.  
  23. // 定义移动方向表
  24. private byte[][] move = {
  25. {0, 1}, // 正东 , move[0] 方向一
  26. {1, 0}, // 正南 , move[1] 方向二
  27. {0, -1}, // 正西 , move[2] 方向三
  28. {-1, 0}, // 正北 , move[3] 方向四
  29. };
  30.  
  31. // 存放所有方向, 使用该集合与某个位置已尝试方向的差集来获取其未尝试的方向
  32. private static final Set<Byte> allDirs = new HashSet<Byte>(Arrays.asList(new Byte[] {0, (byte)1, (byte)2, (byte)3}));
  33.  
  34. private DyStack<Position> stack; // 使用栈存放迷宫通路路径
  35.  
  36. private boolean isCreatedFinished; // 迷宫是否创建完成
  37.  
  38. private Random rand = new Random();
  39.  
  40. public Maze(int rows, int cols) {
  41. this.rows = rows;
  42. this.cols = cols;
  43. EXIT_ROW = rows - 1;
  44. EXIT_COL = cols - 1;
  45. mazeMatrix = new boolean[rows][cols];
  46. mark = new short[rows][cols];
  47. }
  48.  
  49. /**
  50. * 迷宫求解:求解迷宫并设置解的表示
  51. */
  52. public void solve() {
  53. if (hasPath()) {
  54. setSolutionStr();
  55. } else {
  56. noSolution();
  57. }
  58. }
  59.  
  60. /**
  61. * 迷宫矩阵的字符串表示
  62. */
  63. public String toString() {
  64.  
  65. StringBuilder mazeBuf = new StringBuilder("\n");
  66. String mazeCell = "";
  67. for (int i = 0; i < rows; i++) {
  68. if (i == 0) {
  69. mazeBuf.append("Entrance => ");
  70. } else {
  71. // the width of "Entrance => " is Equal to the width of 20 spaces.
  72. mazeBuf.append(indent(20));
  73. }
  74. mazeBuf.append('|');
  75. for (int j = 0; j < cols; j++) {
  76. if (mazeMatrix[i][j] == false) {
  77. mazeCell = String.format("%4s", "IIII");
  78. } else { // 存在通路
  79. if (mark[i][j] == 1) {
  80. mazeCell = String.format("%2s", "GG");
  81. }
  82. else {
  83. mazeCell = String.format("%5s", "");
  84. }
  85. }
  86. mazeBuf.append(mazeCell);
  87. }
  88. if (i == rows - 1) {
  89. mazeBuf.append("| => Exit\n");
  90. } else {
  91. mazeBuf.append("|\n");
  92. }
  93. }
  94. mazeStr = mazeBuf.toString();
  95. return mazeStr;
  96. }
  97.  
  98. /**
  99. * 监听按钮事件后发生改变,并通知观察者此变化的发生
  100. */
  101. public void change() {
  102. setChanged();
  103. notifyObservers();
  104. }
  105.  
  106. public String getSolution() {
  107. return solution;
  108. }
  109.  
  110. public boolean isCreatedFinished() { return isCreatedFinished; }
  111.  
  112. /**
  113. * 将迷宫还原为初始状态
  114. */
  115. public void reset() {
  116. for (int i = 0; i < rows; i++) {
  117. for (int j = 0; j < cols; j++) {
  118. mazeMatrix[i][j] = false;
  119. }
  120. }
  121. isCreatedFinished = false;
  122. }
  123.  
  124. public void createMaze() {
  125. for (int i = 0; i <= EXIT_ROW; i++) {
  126. for (int j = 0; j <= EXIT_COL; j++) { // 初始无通路
  127. mazeMatrix[i][j] = false;
  128. }
  129. }
  130. if (rows < 10 && cols < 10) {
  131. StackADT<Position> createPaths = new DyStack<Position>(rows+cols);
  132. createMaze(0,0, EXIT_ROW, EXIT_COL, createPaths);
  133. isCreatedFinished = true;
  134. change();
  135. }
  136. else {
  137. StackADT<Position> createPaths = new DyStack<Position>(rows+cols);
  138.  
  139. List<int[][]> smallParts = divideMaze(rows, cols, 4);
  140. for (int[][] parts: smallParts) {
  141. createMaze(parts[0][0], parts[0][1], parts[1][0], parts[1][1], createPaths);
  142. if (parts[0][1] != 0) {
  143. // 衔接点打通, 保证总是有一条从入口到出口的通路
  144. mazeMatrix[parts[0][0]][parts[0][1]-1] = true;
  145. }
  146. }
  147.  
  148. isCreatedFinished = true;
  149. change();
  150. }
  151. }
  152.  
  153. /*
  154. * divide [1:rows-1] into n sectors
  155. */
  156. private static List<Integer> divideN(int rows, int n) {
  157. int each = rows/n;
  158. int start = 0;
  159. List<Integer> divs = new ArrayList<Integer>();
  160. for (int i=0; i<n;i++) {
  161. divs.add(start + i*each);
  162. }
  163. divs.add(rows-1);
  164. return divs;
  165. }
  166.  
  167. private static List<int[][]> divideMaze(int rows, int cols, int n) {
  168. List<Integer> nrowParts = divideN(rows, n);
  169. List<Integer> ncolParts = divideN(cols, n);
  170. System.out.println("nrowParts: " + nrowParts);
  171. List<int[][]> results = new ArrayList<int[][]>();
  172. int rowsize = nrowParts.size();
  173. int colsize = ncolParts.size();
  174. for (int i=0; i<rowsize-1; i++) {
  175. for (int j=0; j<colsize-1; j++) {
  176. int[][] smallParts = new int[2][2];
  177. int startRow = nrowParts.get(i);
  178. int exitRow = (i == rowsize-2) ? (nrowParts.get(i+1)) : (nrowParts.get(i+1)-1);
  179. int startCol = ncolParts.get(j);
  180. int exitCol = (j == colsize-2) ? (ncolParts.get(j+1)) : (ncolParts.get(j+1)-1);
  181. smallParts[0][0] = startRow;
  182. smallParts[0][1] = startCol;
  183. smallParts[1][0] = exitRow;
  184. smallParts[1][1] = exitCol;
  185. System.out.println("div: " + startRow + " " + startCol + " " + exitRow + " " + exitCol);
  186. results.add(smallParts);
  187. }
  188. }
  189. return results;
  190. }
  191.  
  192. /*
  193. * 生成迷宫, 采用 Recursive Backtracking. Refer to:
  194. * <a href="http://weblog.jamisbuck.org/2010/12/27/maze-generation-recursive-backtracking"/>
  195. */
  196. public void createMaze(int startRow, int startCol, int exitRow, int exitCol, StackADT<Position> stackForCreate) {
  197.  
  198. mazeMatrix[startRow][startCol] = true;
  199.  
  200. int currRow = startRow;
  201. int currCol = startCol;
  202. byte nextdir = 0; // 当前可能选择的方向
  203. int nextRow = currRow; // 下一个可能到达的相邻位置
  204. int nextCol = currCol;
  205.  
  206. // 每个位置已经尝试过的方向,用于回溯时确定有效的下一个方向
  207. Map<Position, Set<Byte>> triedPaths = new HashMap<Position, Set<Byte>>();
  208.  
  209. List<Byte> allDirsWalked = new ArrayList<Byte>();
  210.  
  211. while (currRow != exitRow || currCol != exitCol) {
  212.  
  213. do {
  214. nextdir = getNextRandDir(currRow, currCol, startRow, startCol, exitRow, exitCol, triedPaths);
  215. System.out.println("nextdir: " + nextdir);
  216. allDirsWalked.add(nextdir);
  217. while (nextdir == -1) {
  218. Position p = stackForCreate.pop();
  219. currRow = p.getRow();
  220. currCol = p.getCol();
  221. nextdir = getNextRandDir(currRow, currCol, startRow, startCol, exitRow, exitCol, triedPaths);
  222. allDirsWalked.add(nextdir);
  223. System.out.println("Back to: " + p);
  224. }
  225.  
  226. nextRow = currRow + move[nextdir][0]; // 取得下一个可能到达的相邻位置
  227. nextCol = currCol + move[nextdir][1];
  228. addTriedPaths(currRow, currCol, nextdir, triedPaths);
  229.  
  230. System.out.println(currRow + " " + currCol + " " + nextdir + " " + nextRow + " " + nextCol);
  231.  
  232. } while (!checkBound(nextRow, nextCol, startRow, startCol, exitRow, exitCol));
  233.  
  234. // 已尝试过的路径, 分两种情况: 所有方向都尝试过或仍有方向没有尝试过
  235. // 如果所有方向都尝试过, 那么需要回退到上一个位置再尝试
  236. if (mazeMatrix[nextRow][nextCol]) {
  237. if (hasAllPathTried(currRow, currCol, triedPaths)) {
  238. Position p = stackForCreate.pop();
  239. currRow = p.getRow();
  240. currCol = p.getCol();
  241. System.out.println("Back to: " + p);
  242. }
  243. continue;
  244. }
  245.  
  246. mazeMatrix[nextRow][nextCol] = true;
  247. stackForCreate.push(new Position(currRow, currCol, nextdir));
  248. currRow = nextRow;
  249. currCol = nextCol;
  250.  
  251. // 更新 UI 界面, 显示迷宫当前状态
  252. try {
  253. change();
  254. TimeUnit.MILLISECONDS.sleep(300);
  255. } catch (InterruptedException ie) {
  256. System.err.println("pause between maze-creating steps interrupted");
  257. }
  258. }
  259.  
  260. mazeMatrix[exitRow][exitCol] = true;
  261. statDirWalked(allDirsWalked);
  262. }
  263.  
  264. /*
  265. * 当前位置的所有方向是否都已经尝试过
  266. */
  267. private boolean hasAllPathTried(int currRow, int currCol, Map<Position, Set<Byte>> triedPaths) {
  268. Set<Byte> triedDirs = triedPaths.get(new Position(currRow, currCol));
  269. if (triedDirs == null) {
  270. triedDirs = new HashSet<Byte>();
  271. }
  272. Set<Byte> allDirsCopy = new HashSet<Byte>(allDirs);
  273. allDirsCopy.removeAll(triedDirs);
  274. return allDirsCopy.isEmpty();
  275. }
  276.  
  277. /*
  278. * 记录当前位置已经尝试过的方向, 避免后续走重复路子
  279. */
  280. private void addTriedPaths(int currRow, int currCol, byte nextdir, Map<Position, Set<Byte>> triedPaths) {
  281. Position currPos = new Position(currRow, currCol);
  282. Set<Byte> triedDirs = triedPaths.get(currPos);
  283. if (triedDirs == null) {
  284. triedDirs = new HashSet<Byte>();
  285. }
  286. triedDirs.add(nextdir);
  287. triedPaths.put(currPos, triedDirs);
  288. }
  289.  
  290. // 抵达迷宫最上边界时, 仅允许往东或往南走
  291. private static final byte[] firstRowAllowedDirs = new byte[] { (byte)0, (byte)1 };
  292.  
  293. // 抵达迷宫最下边界时, 仅允许往东或往北走
  294. private static final byte[] lastRowAllowedDirs = new byte[] { (byte)0, (byte)3 };
  295.  
  296. // 抵达迷宫最左边界时, 仅允许往东或往南走
  297. private static final byte[] firstColAllowedDirs = new byte[] { (byte)0, (byte)1 };
  298.  
  299. // 抵达迷宫最右边界时, 仅允许往南或往西走
  300. private static final byte[] lastColAllowedDirs = new byte[] { (byte)1, (byte)2 };
  301.  
  302. /*
  303. * 获取下一个随机的方向, [0,1,2,3] , 若均已尝试, 返回 -1
  304. */
  305. private byte getNextRandDir(int currRow, int currCol,
  306. int startRow, int startCol,
  307. int exitRow, int exitCOl,
  308. Map<Position, Set<Byte>> triedPaths) {
  309. Set<Byte> triedDirs = (Set<Byte>) triedPaths.get(new Position(currRow, currCol));
  310. if (triedDirs == null) {
  311. triedDirs = new HashSet<Byte>();
  312. }
  313.  
  314. // 如果抵达迷宫边界, 则优先向出口方向走, 避免回走会形成环路, 破坏所有的墙
  315. if (reachUpBound(currRow, startRow, exitRow)) {
  316. if (triedDirs.contains(firstRowAllowedDirs[0]) && triedDirs.contains(firstRowAllowedDirs[1])) {
  317. return -1;
  318. }
  319. return firstRowAllowedDirs[rand.nextInt(2)];
  320. }
  321.  
  322. if (reachLowBound(currRow, startRow, exitRow)) {
  323. if (triedDirs.contains(lastRowAllowedDirs[0]) && triedDirs.contains(lastRowAllowedDirs[1])) {
  324. return -1;
  325. }
  326. return lastRowAllowedDirs[rand.nextInt(2)];
  327. }
  328.  
  329. if (reachLeftBound(currCol, startCol, exitCOl)) {
  330. if (triedDirs.contains(firstColAllowedDirs[0]) && triedDirs.contains(firstColAllowedDirs[1])) {
  331. return -1;
  332. }
  333. return firstColAllowedDirs[rand.nextInt(2)];
  334. }
  335.  
  336. if (reachRightBound(currCol, startCol, exitCOl)) {
  337. if (triedDirs.contains(lastColAllowedDirs[0]) && triedDirs.contains(lastColAllowedDirs[1])) {
  338. return -1;
  339. }
  340. return lastColAllowedDirs[rand.nextInt(2)];
  341. }
  342.  
  343. Set<Byte> allDirsCopy = new HashSet<Byte>(allDirs);
  344. allDirsCopy.removeAll(triedDirs);
  345. List<Byte> possibleDirs = getRandomDirs(allDirsCopy);
  346. Byte[] nonTriedDirs = possibleDirs.toArray(new Byte[possibleDirs.size()]);
  347. if (nonTriedDirs.length == 0) {
  348. return -1;
  349. }
  350. else {
  351. byte nextdir = nonTriedDirs[rand.nextInt(nonTriedDirs.length)];
  352. return nextdir;
  353. }
  354. }
  355.  
  356. /*
  357. * 抵达迷宫上边界
  358. */
  359. private boolean reachUpBound(int currRow, int startRow, int exitRow) {
  360. if (startRow < exitRow) {
  361. return currRow == startRow;
  362. }
  363. else {
  364. return currRow == exitRow;
  365. }
  366. }
  367.  
  368. /*
  369. * 抵达迷宫下边界
  370. */
  371. private boolean reachLowBound(int currRow, int startRow, int exitRow) {
  372. if (startRow > exitRow) {
  373. return currRow == startRow;
  374. }
  375. else {
  376. return currRow == exitRow;
  377. }
  378. }
  379.  
  380. /*
  381. * 抵达迷宫左边界
  382. */
  383. private boolean reachLeftBound(int currCol, int startCol, int exitCol) {
  384. if (startCol < exitCol) {
  385. return currCol == startCol;
  386. }
  387. else {
  388. return currCol == exitCol;
  389. }
  390. }
  391.  
  392. /*
  393. * 抵达迷宫右边界
  394. */
  395. private boolean reachRightBound(int currCol, int startCol, int exitCol) {
  396. if (startCol > exitCol) {
  397. return currCol == startCol;
  398. }
  399. else {
  400. return currCol == exitCol;
  401. }
  402. }
  403.  
  404. /*
  405. * 统计随机选择的方向出现的比例
  406. */
  407. private void statDirWalked(List<Byte> allDirWalked) {
  408. int[] counts = new int[4];
  409. int backCount = 0;
  410. for (Byte b: allDirWalked) {
  411. if (b != -1) {
  412. counts[b] += 1;
  413. }
  414. else {
  415. backCount++;
  416. }
  417. }
  418. int total = allDirWalked.size();
  419. for (int i=0; i < counts.length; i++) {
  420. System.out.printf("P[%d]=%g ", i, (double)counts[i] / total);
  421. }
  422. System.out.println("back count: " + backCount);
  423. System.out.println(allDirWalked);
  424. }
  425.  
  426. // 各方向出现的概率设置,
  427. private static final int P0 = 36;
  428. private static final int P1 = 36;
  429. private static final int P2 = 14;
  430. private static final int P3 = 14;
  431.  
  432. /*
  433. * 扩展 nonTriedDirs 使得 0 (向前) , 1 (向下) 出现的概率更大一些, 减少回退的几率
  434. */
  435. private List<Byte> getRandomDirs(Set<Byte> nonTriedDirs) {
  436.  
  437. List<Byte> selectDirs = new ArrayList<Byte>();
  438. if (nonTriedDirs.contains((byte)0)) {
  439. selectDirs.addAll(createNnums((byte) 0, P0));
  440. }
  441. if (nonTriedDirs.contains((byte)1)) {
  442. selectDirs.addAll(createNnums((byte) 1, P1));
  443. }
  444. if (nonTriedDirs.contains((byte)2)) {
  445. selectDirs.addAll(createNnums((byte) 2, P2));
  446. }
  447. if (nonTriedDirs.contains((byte)3)) {
  448. selectDirs.addAll(createNnums((byte) 3, P3));
  449. }
  450. return selectDirs;
  451. }
  452.  
  453. private static List<Byte> createNnums(byte b, int num) {
  454. List<Byte> occurs = new ArrayList<Byte>();
  455. for (int i=0; i<num; i++) {
  456. occurs.add(b);
  457. }
  458. return occurs;
  459. }
  460.  
  461. private boolean checkBound(int row, int col,
  462. int startRow, int startCol,
  463. int exitRow, int exitCol) {
  464. boolean rowBound = false;
  465. if (startRow < exitRow) {
  466. rowBound = (row <= exitRow && row >= startRow);
  467. }
  468. else {
  469. rowBound = (row <= startRow && row >= exitRow);
  470. }
  471. boolean colBound = false;
  472. if (startCol < exitCol) {
  473. colBound = (col <= exitCol && col >= startCol);
  474. }
  475. else {
  476. colBound = (col <= startCol && col >= exitCol);
  477. }
  478.  
  479. return rowBound && colBound;
  480. }
  481.  
  482. /*
  483. * 求解迷宫路径
  484. */
  485. private boolean hasPath() {
  486. int row = 0, col = 0, dir = 0; // 当前位置的行列位置坐标及搜索移动方向
  487. int nextRow, nextCol; // 下一步移动要到达的位置坐标
  488. boolean found = false;
  489. Position position = new Position(0, 0, (byte) 0); // 通道的临时存放点
  490. stack = new DyStack<Position>(rows + cols); // 创建指定容量的空栈
  491. mark[0][0] = 1;
  492. stack.push(position);
  493.  
  494. while (!stack.isEmpty() && !found) {
  495. try {
  496. position = stack.pop(); // 如四个搜索方向的相邻位置都无通道,则出栈并退回到最近一次经过的位置
  497. row = position.getRow();
  498. col = position.getCol();
  499. dir = position.getDir();
  500. while (dir < 4 && !found) {
  501. nextRow = row + move[dir][0]; // 取得下一个可能到达的相邻位置
  502. nextCol = col + move[dir][1];
  503.  
  504. if (nextRow == EXIT_ROW && nextCol == EXIT_COL) { // 找到出口点,即存在通路径
  505. found = true;
  506. position = new Position(row, col, (byte) ++dir);
  507. stack.push(position);
  508. position = new Position(EXIT_ROW, EXIT_COL, (byte) 1);
  509. stack.push(position);
  510. } else if (checkBound(nextRow, nextCol, 0, 0, EXIT_ROW, EXIT_COL)
  511. && mazeMatrix[nextRow][nextCol] == true && mark[nextRow][nextCol] == 0) {
  512. // 没有找到出口点,但当前搜索方向的相邻位置为通道,则前进到相邻位置,并在相邻位置依序按照前述四个方向进行搜索移动
  513. mark[nextRow][nextCol] = 1;
  514. position = new Position(row, col, (byte) ++dir);
  515. stack.push(position);
  516. row = nextRow;
  517. col = nextCol;
  518. dir = 0;
  519. } else {
  520. /* 没有找到出口点,且当前搜索方向的相邻位置为墙,或已搜索过,或超出迷宫边界,
  521. * 则向当前位置的下一个搜索方向进行搜索移动 */
  522. ++dir;
  523. }
  524. }
  525. } catch (Exception e) {
  526. System.out.print("栈空!");
  527. e.printStackTrace();
  528. }
  529. }
  530. mark[EXIT_ROW][EXIT_COL] = 1;
  531.  
  532. if (found)
  533. return true;
  534. else
  535. return false;
  536.  
  537. }
  538.  
  539. private void setSolutionStr() {
  540. solution = "\n所找到的通路路径为: \n" + stack + "\n\n";
  541. solution += "其中,(x,y,z)表示从坐标点(x,y)向z方向移动\n\n";
  542. }
  543.  
  544. private void noSolution() { // 迷宫无解的字符串表示
  545. solution = "迷宫无解!\n";
  546. }
  547.  
  548. /*
  549. * 显示迷宫时,为美观起见, 缩进 n 个字符
  550. */
  551. private String indent(int n) {
  552. StringBuilder indentBuf = new StringBuilder();
  553. while (n-- > 0) {
  554. indentBuf.append(' ');
  555. }
  556. return indentBuf.toString();
  557. }
  558.  
  559. }  

  5.  GUI 界面: 主要使用 "观察者" 模式来实现。其中 Maze 是被观察者, 当 Maze 发生变化时,会去通知后面会给出的观察者 MazePanel ; 而 Maze 的触发是在 Model 类里。

  Model 类: 监听按钮点击事件, 获取输入来触发生成迷宫。 注意到这里另起了线程去执行, 使得在 maze.CreateMaze 方法里的 sleep 不会阻塞 Ui 线程更新界面; 写 GUI 记住两句话: (1) 更新组件和界面相关的事情一定要在事件分发线程里做; 与界面无关的计算和 IO 不要在事件分发线程里做,因为那样会阻塞 UI 线程,导致界面无法更新(假死); (2) 事件处理方法 actionPerformed 和 SwingUtilities.invokeLater 里的代码是在事件分发线程里做的;

  1. package zzz.study.javagui.maze;
  2.  
  3. import java.awt.event.ActionEvent;
  4. import java.awt.event.ActionListener;
  5. import java.util.Observer;
  6. import java.util.regex.Pattern;
  7.  
  8. import javax.swing.JOptionPane;
  9. import javax.swing.JPanel;
  10. import javax.swing.JTextField;
  11.  
  12. /**
  13. * 监听按钮事件,并改变 Maze 对象(被观察者)
  14. */
  15. public class Model implements ActionListener {
  16.  
  17. private MazeGUI app; // 用于从中获取数据的 GUI 界面
  18.  
  19. public void actionPerformed(ActionEvent event) {
  20.  
  21. JTextField inputRow = app.getInputRow();
  22. JTextField inputCol = app.getInputCol();
  23. JPanel mazePanel = app.getMazePanel();
  24.  
  25. String rowStr = inputRow.getText();
  26. String colStr = inputCol.getText();
  27. String regex = "^\\s*[1-9][0-9]?\\s*$";
  28. if (rowStr.matches(regex) && colStr.matches(regex)) {
  29. int rows = Integer.parseInt(inputRow.getText());
  30. int cols = Integer.parseInt(inputCol.getText());
  31. final Maze maze = new Maze(rows,cols);
  32. maze.addObserver((Observer) mazePanel);
  33.  
  34. new Thread(new Runnable() {
  35. public void run() {
  36. if (maze.isCreatedFinished()) {
  37. maze.reset();
  38. maze.change();
  39. }
  40. maze.createMaze();
  41. }
  42. }).start();
  43.  
  44. }
  45. else {
  46. JOptionPane.showMessageDialog(null, "对不起,您的输入有误, 请输入 [1-99] 之间的任意数字!", "警告", JOptionPane.WARNING_MESSAGE);
  47. }
  48.  
  49. }
  50.  
  51. public void setGUI(MazeGUI app) {
  52. this.app = app;
  53. }
  54.  
  55. }

  6.  MazePanel :  Maze 的观察者, 当 Maze 状态发生变化时,调用 change 方法时,就会通知该面板更新其显示。注意到更新 UI 在 SwingUtilities.invokeLater 方法中完成;其它事情则在外面做。

  1. package zzz.study.javagui.maze;
  2.  
  3. import java.awt.BorderLayout;
  4. import java.awt.Font;
  5. import java.util.Observable;
  6. import java.util.Observer;
  7.  
  8. import javax.swing.*;
  9. import javax.swing.border.TitledBorder;
  10.  
  11. /**
  12. * 迷宫面板: 按钮事件的观察者
  13. */
  14. public class MazePanel extends JPanel implements Observer {
  15.  
  16. private String title;
  17. private String text;
  18. private JTextArea infoArea;
  19.  
  20. public MazePanel(String title, Font font) {
  21. this(title, "");
  22. infoArea.setFont(font);
  23. }
  24.  
  25. public MazePanel(String title, String text) {
  26. this.title = title;
  27. this.text = text;
  28. infoArea = new JTextArea(text);
  29. //infoArea.setEnabled(false);
  30. setLayout(new BorderLayout());
  31. setBorder(new TitledBorder(title));
  32. add(infoArea, BorderLayout.CENTER);
  33. add(new JScrollPane(infoArea));
  34. }
  35.  
  36. public String getTitle() {
  37. return title;
  38. }
  39.  
  40. public void setTitle(String title) {
  41. this.title = title;
  42. }
  43.  
  44. public String getText() {
  45. return text;
  46. }
  47.  
  48. public void setText(String text) {
  49. this.text = text;
  50. }
  51.  
  52. public void update(Observable o, Object arg) {
  53.  
  54. Maze m = (Maze)o;
  55. if (m.isCreatedFinished()) {
  56. m.solve();
  57. text = "" + m + "\n" + m.getSolution();
  58. }
  59. else {
  60. text = "" + m + "\n";
  61. }
  62. infoArea.setText(text);
  63.  
  64. SwingUtilities.invokeLater(new Runnable() {
  65. public void run() {
  66. updateUI();
  67. }
  68. });
  69.  
  70. }
  71.  
  72. }

  7.   MazeGUI 界面: 组装界面组件, 启动应用。

  1. package zzz.study.javagui.maze;
  2.  
  3. import java.awt.BorderLayout;
  4. import java.awt.Container;
  5. import java.awt.FlowLayout;
  6. import java.awt.Font;
  7. import java.util.Enumeration;
  8.  
  9. import javax.swing.*;
  10. import javax.swing.border.TitledBorder;
  11. import javax.swing.plaf.FontUIResource;
  12.  
  13. /**
  14. * 迷宫程序的主界面
  15. * @author shuqin1984
  16. */
  17. public class MazeGUI extends JFrame {
  18.  
  19. private JTextField inputRow; // 用户输入行数
  20. private JTextField inputCol; // 用户输入列数
  21.  
  22. private JPanel mazePanel; // 显示迷宫的面板
  23.  
  24. public MazeGUI() {
  25. super("程序演示:模拟走迷宫");
  26. }
  27.  
  28. private final Font font = new Font("Monospaced",Font.PLAIN, 14);
  29.  
  30. public static void main(String[] args) {
  31.  
  32. SwingUtilities.invokeLater(new Runnable() {
  33. public void run() {
  34. MazeGUI app = new MazeGUI();
  35. app.launch();
  36. }
  37. });
  38. }
  39.  
  40. private static void InitGlobalFont(Font font) {
  41. FontUIResource fontRes = new FontUIResource(font);
  42. for (Enumeration<Object> keys = UIManager.getDefaults().keys();
  43. keys.hasMoreElements(); ) {
  44. Object key = keys.nextElement();
  45. Object value = UIManager.get(key);
  46. if (value instanceof FontUIResource) {
  47. UIManager.put(key, fontRes);
  48. }
  49. }
  50. }
  51.  
  52. /**
  53. * 启动应用程序
  54. */
  55. public void launch()
  56. {
  57. JFrame f = new MazeGUI();
  58. f.setBounds(100, 100, 800, 600);
  59. f.setVisible(true);
  60. f.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
  61.  
  62. InitGlobalFont(font);
  63.  
  64. Container contentPane = f.getContentPane();
  65. contentPane.setLayout(new BorderLayout());
  66.  
  67. JPanel inputPanel = createInputPanel();
  68. contentPane.add(inputPanel, BorderLayout.NORTH);
  69.  
  70. mazePanel = new MazePanel("显示迷宫和迷宫的解", font);
  71. contentPane.add(mazePanel, BorderLayout.CENTER);
  72.  
  73. f.setContentPane(contentPane);
  74. }
  75.  
  76. /**
  77. * 创建并返回输入面板
  78. */
  79. public JPanel createInputPanel() {
  80.  
  81. JPanel inputPanel = new JPanel(new FlowLayout());
  82. inputPanel.setBorder(new TitledBorder("用户输入信息提示"));
  83.  
  84. JLabel labelInfo = new JLabel("请输入迷宫大小:",null,SwingConstants.LEFT);
  85.  
  86. JLabel labelRow = new JLabel("行");
  87. JLabel labelCol = new JLabel("列");
  88. JLabel labelSpace = new JLabel(" ");
  89. if (inputRow == null)
  90. inputRow = new JTextField(3);
  91. if (inputCol == null)
  92. inputCol = new JTextField(3);
  93.  
  94. inputPanel.add(labelInfo);
  95. inputPanel.add(inputRow);
  96. inputPanel.add(labelRow);
  97. inputPanel.add(inputCol);
  98. inputPanel.add(labelCol);
  99. inputPanel.add(labelSpace);
  100.  
  101. JButton button = new JButton("生成迷宫");
  102. inputPanel.add(button);
  103.  
  104. Model m = new Model();
  105. m.setGUI(this);
  106. button.addActionListener(m);
  107.  
  108. return inputPanel;
  109.  
  110. }
  111.  
  112. public JTextField getInputRow() {
  113. return inputRow;
  114. }
  115.  
  116. public JTextField getInputCol() {
  117. return inputCol;
  118. }
  119.  
  120. public JPanel getMazePanel() {
  121. return mazePanel;
  122. }
  123.  
  124. }

  8.  截图,无图无真相~~

     

  

【本文完】

Java求解迷宫问题:栈与回溯算法的更多相关文章

  1. 回溯算法之n皇后问题

    今天在看深度优先算法的时候,联想到DFS本质不就是一个递归回溯算法问题,只不过它是应用在图论上的.OK,写下这篇博文也是为了回顾一下回溯算法设计吧. 学习回溯算法问题,最为经典的问题我想应该就是八皇后 ...

  2. 剑指Offer——回溯算法解迷宫问题(java版)

    剑指Offer--回溯算法解迷宫问题(java版)   以一个M×N的长方阵表示迷宫,0和1分别表示迷宫中的通路和障碍.设计程序,对任意设定的迷宫,求出从入口到出口的所有通路.   下面我们来详细讲一 ...

  3. 8皇后以及N皇后算法探究,回溯算法的JAVA实现,非递归,循环控制及其优化

    上两篇博客 8皇后以及N皇后算法探究,回溯算法的JAVA实现,递归方案 8皇后以及N皇后算法探究,回溯算法的JAVA实现,非递归,数据结构“栈”实现 研究了递归方法实现回溯,解决N皇后问题,下面我们来 ...

  4. java实现回溯算法

    最近有在leetcode上面做算法题,已经遇到了两道回溯算法的题目,感觉一点思路都没有,现决定将java如何实现回溯算法做一次总结. 什么叫做回溯算法 (摘抄于百度百科) 回溯算法实际上一个类似枚举的 ...

  5. 8皇后以及N皇后算法探究,回溯算法的JAVA实现,递归方案

    八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例.该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行.同 ...

  6. 8皇后问题SQL求解(回溯算法)

    问题 八皇后问题是一个古老而著名的问题,是回溯算法的典型例题.该问题是十九世纪著名的数学家高斯1850年提出:在8X8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行.同一 ...

  7. 八皇后问题求解java(回溯算法)

    八皇后问题 八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例.该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处 ...

  8. 剑指Offer——回溯算法

    剑指Offer--回溯算法 什么是回溯法 回溯法实际是穷举算法,按问题某种变化趋势穷举下去,如某状态的变化用完还没有得到最优解,则返回上一种状态继续穷举.回溯法有"通用的解题法"之 ...

  9. 深度实战玩转算法, Java语言7个经典应用诠释算法精髓

    深度实战玩转算法,以Java语言主讲,通过7款经典好玩游戏,真正将算法用于实际开发,由算法大牛ACM亚洲区奖牌获得者liuyubobobo主讲,看得见的算法,带领你进入一个不一样的算法世界,本套课程共 ...

随机推荐

  1. [APP] Android 开发笔记 006-使用短信验证SDK进行短信验证

    1. 下载SDK (http://www.mob.com/#/download) SMS For Android Studio: http://www.mob.com/download/sms/and ...

  2. (转)关于如何学好游戏3D引擎编程的一些经验

    此篇文章献给那些为了游戏编程不怕困难的热血青年,它的神秘要我永远不间断的去挑战自我,超越自我,这样才能攀登到游戏技术的最高峰 ——阿哲VS自己 QQ79134054多希望大家一起交流与沟通 这篇文章是 ...

  3. INTRO: THE DAWN (亡灵序曲) 中独白

    As the last ship sailed towards the distant horizon I sat there watching on a rock My mind slowly dr ...

  4. windows下的zookeeper安装

    先在官网下载安装包(https://www.apache.org/dyn/closer.cgi/zookeeper/),单机安装非常简单,只要获取到 Zookeeper 的压缩包并解压到某个目录如:C ...

  5. Python3设置在shell脚本中自动补全功能的方法

    本篇博客将会简短的介绍,如何在ubuntu中设置python自动补全功能. 需求:由于python中的内建函数较多,我们在百纳乘时,可能记不清函数的名字,同时自动补全功能,加快了我们开发的效率. 方法 ...

  6. [转]Android中attr自定义标签详解

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"     xmlns:wen= ...

  7. python---修改编辑器的配色和字体大小

    因为习惯黑色的背景,所以必须修改成对应的配色: 在这里设置theme: 设置字体大小: 找到Font,这里设置字体大小,首先要Scheme 后 进行 Save as 操作后,才能设置 Size ,设置 ...

  8. 微信小程序---示例DEMO

    转:CSDN的文章: https://blog.csdn.net/rolan1993/article/details/73467867 不错的DEMO: https://github.com/skyv ...

  9. MyEclipse无法创建servers视图:Could not create the view: An unexpected exception was thrown

    今天上班刚打开MyEclipse,就发现servers视图无法打开了,显示:Could not create the view: An unexpected exception was thrown. ...

  10. 服务器推技术研究Comet

    服务器推技术 最近参与的一个关于股票的项目,有这样一个需求.服务器需要主动推送给客户端消息.这和传统的Web模式不同.传统的Web系统,客户端和服务器的交互是这样的: 客户端先和服务器建立一个TCP连 ...