3. 深度优先搜索

现在我们用堆栈解决一个有意思的问题,定义一个二维数组:

  1. int maze[5][5] = {
  2. 0, 1, 0, 0, 0,
  3. 0, 1, 0, 1, 0,
  4. 0, 0, 0, 0, 0,
  5. 0, 1, 1, 1, 0,
  6. 0, 0, 0, 1, 0,
  7. };

它表示一个迷宫,其中的1表示墙壁,0表示可以走的路,只能横着走或竖着走,不能斜着走,要求编程序找出从左上角到右下角的路线。程序如下:

例 12.3. 用深度优先搜索解迷宫问题

  1. #include <stdio.h>
  2.  
  3. #define MAX_ROW 5
  4. #define MAX_COL 5
  5.  
  6. struct point { int row, col; } stack[512];
  7. int top = 0;
  8.  
  9. void push(struct point p)
  10. {
  11. stack[top++] = p;
  12. }
  13.  
  14. struct point pop(void)
  15. {
  16. return stack[--top];
  17. }
  18.  
  19. int is_empty(void)
  20. {
  21. return top == 0;
  22. }
  23.  
  24. int maze[MAX_ROW][MAX_COL] = {
  25. 0, 1, 0, 0, 0,
  26. 0, 1, 0, 1, 0,
  27. 0, 0, 0, 0, 0,
  28. 0, 1, 1, 1, 0,
  29. 0, 0, 0, 1, 0,
  30. };
  31.  
  32. void print_maze(void)
  33. {
  34. int i, j;
  35. for (i = 0; i < MAX_ROW; i++) {
  36. for (j = 0; j < MAX_COL; j++)
  37. printf("%d ", maze[i][j]);
  38. putchar('\n');
  39. }
  40. printf("*********\n");
  41. }
  42.  
  43. struct point predecessor[MAX_ROW][MAX_COL] = {
  44. {{-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}},
  45. {{-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}},
  46. {{-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}},
  47. {{-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}},
  48. {{-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}},
  49. };
  50.  
  51. void visit(int row, int col, struct point pre)
  52. {
  53. struct point visit_point = { row, col };
  54. maze[row][col] = 2;
  55. predecessor[row][col] = pre;
  56. push(visit_point);
  57. }
  58.  
  59. int main(void)
  60. {
  61. struct point p = { 0, 0 };
  62.  
  63. maze[p.row][p.col] = 2;
  64. push(p);
  65.  
  66. while (!is_empty()) {
  67. p = pop();
  68. if (p.row == MAX_ROW - 1 /* goal */
  69. && p.col == MAX_COL - 1)
  70. break;
  71. if (p.col+1 < MAX_COL /* right */
  72. && maze[p.row][p.col+1] == 0)
  73. visit(p.row, p.col+1, p);
  74. if (p.row+1 < MAX_ROW /* down */
  75. && maze[p.row+1][p.col] == 0)
  76. visit(p.row+1, p.col, p);
  77. if (p.col-1 >= 0 /* left */
  78. && maze[p.row][p.col-1] == 0)
  79. visit(p.row, p.col-1, p);
  80. if (p.row-1 >= 0 /* up */
  81. && maze[p.row-1][p.col] == 0)
  82. visit(p.row-1, p.col, p);
  83. print_maze();
  84. }
  85. if (p.row == MAX_ROW - 1 && p.col == MAX_COL - 1) {
  86. printf("(%d, %d)\n", p.row, p.col);
  87. while (predecessor[p.row][p.col].row != -1) {
  88. p = predecessor[p.row][p.col];
  89. printf("(%d, %d)\n", p.row, p.col);
  90. }
  91. } else
  92. printf("No path!\n");
  93.  
  94. return 0;
  95. }

运行结果如下:

  1. 2 1 0 0 0
  2. 2 1 0 1 0
  3. 0 0 0 0 0
  4. 0 1 1 1 0
  5. 0 0 0 1 0
  6. *********
  7. 2 1 0 0 0
  8. 2 1 0 1 0
  9. 2 0 0 0 0
  10. 0 1 1 1 0
  11. 0 0 0 1 0
  12. *********
  13. 2 1 0 0 0
  14. 2 1 0 1 0
  15. 2 2 0 0 0
  16. 2 1 1 1 0
  17. 0 0 0 1 0
  18. *********
  19. 2 1 0 0 0
  20. 2 1 0 1 0
  21. 2 2 0 0 0
  22. 2 1 1 1 0
  23. 2 0 0 1 0
  24. *********
  25. 2 1 0 0 0
  26. 2 1 0 1 0
  27. 2 2 0 0 0
  28. 2 1 1 1 0
  29. 2 2 0 1 0
  30. *********
  31. 2 1 0 0 0
  32. 2 1 0 1 0
  33. 2 2 0 0 0
  34. 2 1 1 1 0
  35. 2 2 2 1 0
  36. *********
  37. 2 1 0 0 0
  38. 2 1 0 1 0
  39. 2 2 0 0 0
  40. 2 1 1 1 0
  41. 2 2 2 1 0
  42. *********
  43. 2 1 0 0 0
  44. 2 1 0 1 0
  45. 2 2 2 0 0
  46. 2 1 1 1 0
  47. 2 2 2 1 0
  48. *********
  49. 2 1 0 0 0
  50. 2 1 2 1 0
  51. 2 2 2 2 0
  52. 2 1 1 1 0
  53. 2 2 2 1 0
  54. *********
  55. 2 1 2 0 0
  56. 2 1 2 1 0
  57. 2 2 2 2 0
  58. 2 1 1 1 0
  59. 2 2 2 1 0
  60. *********
  61. 2 1 2 2 0
  62. 2 1 2 1 0
  63. 2 2 2 2 0
  64. 2 1 1 1 0
  65. 2 2 2 1 0
  66. *********
  67. 2 1 2 2 2
  68. 2 1 2 1 0
  69. 2 2 2 2 0
  70. 2 1 1 1 0
  71. 2 2 2 1 0
  72. *********
  73. 2 1 2 2 2
  74. 2 1 2 1 2
  75. 2 2 2 2 0
  76. 2 1 1 1 0
  77. 2 2 2 1 0
  78. *********
  79. 2 1 2 2 2
  80. 2 1 2 1 2
  81. 2 2 2 2 2
  82. 2 1 1 1 0
  83. 2 2 2 1 0
  84. *********
  85. 2 1 2 2 2
  86. 2 1 2 1 2
  87. 2 2 2 2 2
  88. 2 1 1 1 2
  89. 2 2 2 1 0
  90. *********
  91. 2 1 2 2 2
  92. 2 1 2 1 2
  93. 2 2 2 2 2
  94. 2 1 1 1 2
  95. 2 2 2 1 2
  96. *********
  97. (4, 4)
  98. (3, 4)
  99. (2, 4)
  100. (1, 4)
  101. (0, 4)
  102. (0, 3)
  103. (0, 2)
  104. (1, 2)
  105. (2, 2)
  106. (2, 1)
  107. (2, 0)
  108. (1, 0)
  109. (0, 0)

这次堆栈里的元素是结构体类型的,用来表示迷宫中一个点的x和y座标。我们用一个新的数据结构保存走迷宫的路线,每个走过的点都有一个前趋(Predecessor)点,表示是从哪儿走到当前点的,比如predecessor[4][4]是座标为(3, 4)的点,就表示从(3, 4)走到了(4, 4),一开始predecessor的各元素初始化为无效座标(-1, -1)。在迷宫中探索路线的同时就把路线保存在predecessor数组中,已经走过的点在maze数组中记为2防止重复走,最后找到终点时就根据predecessor数组保存的路线从终点打印到起点。为了帮助理解,我把这个算法改写成伪代码(Pseudocode)如下:

  1. 将起点标记为已走过并压栈;
  2. while (栈非空) {
  3. 从栈顶弹出一个点p;
  4. if (p这个点是终点)
  5. break;
  6. 否则沿右、下、左、上四个方向探索相邻的点
  7. if (和p相邻的点有路可走,并且还没走过)
  8. 将相邻的点标记为已走过并压栈,它的前趋就是p点;
  9. }
  10. if (p点是终点) {
  11. 打印p点的座标;
  12. while (p点有前趋) {
  13. p = p点的前趋;
  14. 打印p点的座标;
  15. }
  16. } else
  17. 没有路线可以到达终点;

我在while循环的末尾插了打印语句,每探索一步都打印出当前迷宫的状态(标记了哪些点),从打印结果可以看出这种搜索算法的特点是:每次探索完各个方向相邻的点之后,取其中一个相邻的点走下去,一直走到无路可走了再退回来,取另一个相邻的点再走下去。这称为深度优先搜索(DFS,Depth First Search)。探索迷宫和堆栈变化的过程如下图所示。

图 12.2. 深度优先搜索

深度优先搜索

图中各点的编号表示探索顺序,堆栈中保存的应该是座标,我在画图时为了直观就把各点的编号写在堆栈里了。可见正是堆栈后进先出的性质使这个算法具有了深度优先的特点。如果在探索问题的解时走进了死胡同,则需要退回来从另一条路继续探索,这种思想称为回溯(Backtrack),一个典型的例子是很多编程书上都会讲的八皇后问题。

最后我们打印终点的座标并通过predecessor数据结构找到它的前趋,这样顺藤摸瓜一直打印到起点。那么能不能从起点到终点正向打印路线呢?在上一节我们看到,数组支持随机访问也支持顺序访问,如果在一个循环里打印数组,既可以正向打印也可以反向打印。但predecessor这种数据结构却有很多限制:

  1. 不能随机访问一条路线上的任意点,只能通过一个点找到另一个点,通过另一个点再找第三个点,因此只能顺序访问。

  2. 每个点只知道它的前趋是谁,而不知道它的后继(Successor)是谁,所以只能反向顺序访问。

可见,有什么样的数据结构就决定了可以用什么样的算法。那为什么不再建一个successor数组来保存每个点的后继呢?从DFS算法的过程可以看出,虽然每个点的前趋只有一个,后继却不止一个,如果我们为每个点只保存一个后继,则无法保证这个后继指向正确的路线。由此可见,有什么样的算法就决定了可以用什么样的数据结构。设计算法和设计数据结构这两件工作是紧密联系的。

习题

1、修改本节的程序,要求从起点到终点正向打印路线。你能想到几种办法?

2、本节程序中predecessor这个数据结构占用的存储空间太多了,改变它的存储方式可以节省空间,想想该怎么改。

3、上一节我们实现了一个基于堆栈的程序,然后改写成递归程序,用函数调用的栈帧替代自己实现的堆栈。本节的DFS算法也是基于堆栈的,请把它改写成递归程序,这样改写可以避免使用predecessor数据结构,想想该怎么做。