C++抽象编程——回溯算法(3)——解决迷宫问题

编码解决迷宫问题的算法

尽管递归的分解和simple case是我们需要在概念层面上解决问题的所在,但编写完整的程序来导航迷宫需要考虑一些实现细节。例如,我们需要确定一个迷宫本身的表示,例如,怎么确定墙壁在哪里,跟踪当前位置,指示特定正方形被标记,并确定是否已经转义从迷宫在为迷宫设计合适的数据结构时,本身就是一个有趣的编程挑战,它与理解递归算法无关,这是本次讨论的焦点。如果有的话,数据结构的细节可能会阻碍你的整体理解算法的难度。幸运的是,可以通过引入隐藏一些复杂性的新接口来设置这些细节。所以我们可以设计一个maze.h接口导出一个名为Maze的类,其中包含了所有必要的信息,以便跟踪迷宫中的通道,并在图形窗口中显示该迷宫。
一旦你访问了迷宫课程,编写一个程序来解决一个迷宫变得简单得多。 这次的目的是写一个函数:

bool solveMaze(Maze & maze, Point pt);

solveMaze的参数是(1)保存数据结构的Maze对象,(2)起始位置,每个递归子问题都会发生变化。 为了确保在找到解决方案时递归可以终止,如果已经找到解决方案,solveMaze函数将返回true,否则返回false。

PS:事实上,这个实现用到了太多的Stanford的学校库函数。所以我们就学习思路。具体的实现我会在了解后他们的库函数再做,以下的内容或者出口的方法,我们只要知道它是做什么的就好了。
point.h 文件参看:C++抽象编程——面向对象(5)——最终版的point文件
direction.h文件参看:C++抽象编程——回溯算法(2)——准备Direction文件

maze.h文件:

/*
* 文件: maze.h
* ------------
* 这个接口出口maze类.
*/
#ifndef _maze_h
#define _maze_h

#include "grid.h" //这个库函数只是提供了一些二维数组的简便算法 
#include "point.h"
/*
* 类: Maze
* -----------
*这个类代表一个二维迷宫,它包含在矩形的正方形网格中。 
*这个迷宫是从一个数据文件中读取的,其中字符'+',' - '
*和'|' 分别代表角落,水平墙壁和垂直墙壁; 空格表示开放
*的通道正方形起始位置由字符“S”表示。 例如,以下数据
*文件定义了一个简单的迷宫:
*
* +-+-+-+-+-+
* | |
* + +-+ + +-+
* |S | |
* +-+-+-+-+-+
*/
class Maze {
public:
/*
* 构造函数: Maze
* 用法: Maze maze(filename);
* ---------------------------
* 通过读取特定的文件来构造迷宫.
*/
Maze(string filename);
/*
* 方法: showInGraphicsWindow
* 用法: showInGraphicsWindow();
* showInGraphicsWindow(x, y);
* ----------------------------------
* 将迷宫展示在窗口上,不过我们这个时候不用考虑这个问题 
*/
void showInGraphicsWindow();
void showInGraphicsWindow(double x, double y);

/*
* 方法: getStartPosition
* 用法: Point start = maze.getStartPosition();
* ---------------------------------------------
* 返回一个表示开始方块坐标的点。
*/
Point getStartPosition();
/*
* 方法: isOutside
* 用法: if (maze.isOutside(pt)) . . .
* ------------------------------------
* 当 方块已经在迷宫外面的时候,返回true 
*/
bool isOutside(Point pt);
/*
* 方法: wallExists
* 用法: if (maze.wallExists(pt, dir)) . . .
* ------------------------------------------
* 如果在pt位置上的有正方形有方向dir上的墙,则返回true。
*/
bool wallExists(Point pt, Direction dir);
/*
* 方法: markSquare
* 用法: maze.markSquare(pt);
* ---------------------------
* 在迷宫中指定标记的方块.
*/
void markSquare(Point pt);
/*
* 方法: unmarkSquare
* 用法: maze.unmarkSquare(pt);
* -----------------------------
* 在迷宫中取消标记指定的方形。
*/
void unmarkSquare(Point pt);
/*
* 方法: isMarked
* 用法: if (maze.isMarked(pt)) . . .
* -----------------------------------
* 如果指定的方块已经被标记,那么返回true 
*/
bool isMarked(Point pt);

#include "mazepriv.h"
}; 

实现文件

/*
* 函数: solveMaze
* 用法: solveMaze(maze, start);
* ------------------------------
* 尝试从指定的起点生成当前迷宫的
*解决方案。 如果迷宫有解决方案,
*则解决方法返回true,否则返回false。
*实现使用递归来解决由标记当前正方形导致的子迷宫,
*并沿着每个开放的通道移动一步。
*/
bool solveMaze(Maze & maze, Point start) {
    if (maze.isOutside(start)) return true; //simple case 
    if (maze.isMarked(start)) return false; //simple case
    /*下面为递归分解*/ 
    maze.markSquare(start);
        for (Direction dir = NORTH; dir <= WEST; dir++) {
            /*如果在这个方位上没有产生墙壁*/ 
            if (!maze.wallExists(start, dir)) {
                /*那么以这个点为起始,再执行一次,就是移动一步*/ 
                if (solveMaze(maze, adjacentPoint(start, dir))) {
                    return true;
                }
            }
        }
    maze.unmarkSquare(start);
    return false;
}
/*
* 函数: adjacentPoint  adjacent,邻近,附近的意思 
* 用法: Point finish = adjacentPoint(start, dir);
* ------------------------------------------------
* 返回由dir指定的方向从一开始移动一个正方形的点。
* 例如,如果pt是点(1,1),则调用neighborPoint(pt,EAST)返回点(2,1)。
* 为了保持与图形包的一致性,y坐标随着屏幕向下移动而增加。 
* 因此,移动NORTH减小y分量,并且移动SOUTH增加它.
*/
Point adjacentPoint(Point start, Direction dir) {
    switch (dir) {
        case NORTH: return Point(start.getX(), start.getY() - 1);
        case EAST: return Point(start.getX() + 1, start.getY());
        case SOUTH: return Point(start.getX(), start.getY() + 1);
        case WEST: return Point(start.getX() - 1, start.getY());
    }
    return start;
}

说服你自己这个方案是可行的

为了有效地使用递归,在某些时候,您必须能够看到一个递归函数,如上面的中的solveMaze示例,并对自己说:“我明白这是如何工作的。问题越来越简单,因为每次都标记更多的方块。simple case显然是正确的。这个代码必须做这个工作。”然而,对于大多数人来说,建立对递归的信任不会很容易。人的天生怀疑使我们想要看到解决方案中的步骤。问题是,即使是像我们前面所示的迷宫一样简单的迷宫,解决方案中涉及的步骤的完整内容太大,无法舒适地思考。例如,解决迷宫需要66个调用来解决迷宫,当解决方案终于被发现时嵌套在27级的深度。 如果你尝试详细跟踪代码,那么肯定会迷失。
如果您还没有准备好接收leap of faith,那么最好的方法就是在更广泛的意义上跟踪代码的运行。 您知道代码首先尝试通过将一个正方形移动到北方来解决迷宫,因为for循环按照“方向”枚举定义的顺序遍历方向。因此,解决过程的第一步是进行递归开始于以下位置:

在这一点上,再次发生相同的过程。 该程序再次尝试向北移动,并在以下位置进行新的递归调用:

在这个递归级别,向北移动不再可能,所以for循环循环到其他方向。在循环到南边之后,程序遇到一个标记的方块(就是X),也不能移动。继续循环程序发现向西开放,并继续生成一个新的递归调用。 同样的过程发生在这个新的地方,这又导致以下配置:

在这个位置上,for循环中的任何一个方向都不可行; 每个方向都被墙壁封闭或已被标记。因此,此时此级别的for循环从底部退出时,它会取消当前正方形的标记并返回到上一级(就是上一个X)。事实证明,所有的路径也在这个位置进行了探索,所以程序再次取消了Square的标记,并在递归中返回到下一个更高级别。这个时候程序一直追溯到初始调用,完全耗尽了从北移开始的可能性(此时方块回到原点)。然后,for循环尝试向东方向,发现它被阻止,并继续探索向南方向,从以下配置中的递归调用开始:

从这里开始,同样的过程。递归系统地探索沿着这条路径的每个方向,当它到达一个死胡同的时候通过递归调用又回到这个未尝试完的方向的点(注意这个时候不是回到原点)。程序在以下位置进行递归调用:

我们注意这个红色的点,因为在这个方向上,我们刚刚是往北边走发现走不通的。但是我们还没有尝试往南边走,所以我们返回的级别是这里而不是原点。最后重复这个过程,最终找到一条通路(图中X的路径):

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值