本文接续上文: Lozyue:迷宫问题之打印和通路求解 一次编程实现的探索之旅(上)
讨论求解迷宫问题中的所有解问题和用广度优先遍历实现最短路径求解。
代码小处理
工欲善其事必先利其器,我们先来对代码做点小处理。
在最初的实现中,我们把代码和类都写在了一个文件中(你也可以不用类来写)。但其实,有更好的方式,就是把这个类单独拆分出来放到一个头文件中,在主文件中包含进来:
后缀为.h的头文件和cpp后缀的源文件没有什么特别不同,但头文件的引入更灵活,可以通过一个#include
再加引号包含路径的语句把头文件的代码包含进来,就像头文件所有内容被写在了引用(#include)的那个位置一样。
但为了防止重复引用,我们只需简单的在需要做成头文件的前后加上预编译命令:
#pragma once
#ifndef MAZE
#define MAZE // 唯一标识的名称,这里就用MAZE
// 这里是文件内容
// codes ……
#endif
然后将源文件`maze.cpp`的后缀改成 .h ,将main
函数移动出来,再新建一个叫maze.cpp
的文件,将主入口函数main
放入其中,并在顶部加上:
#include "./maze.h"
然后就是 GitHub 仓库中`Maze-final`的样子了。
实现链式调用
之前我们定义的方法中全部都是void类型,这其实有些浪费。我们不妨用点小技巧:类的方法中都有一个`this`指针指向自己这个类实例本身,而这个this
就是类指针,我们完全可以把它`return`出去,相应的,函数类型要改成类指针类型,在本文中是`Maze *`也即:
Maze * printMaze(){
// .....
return this;
}
这样做的好处是什么呢?就是链式调用,一种很舒服的调用方法方式。当我们把类中所有的void方法修改成上面的亚子,像上文中的main函数实现,我们完全可以给它升级一下,实例化的时候使用 new
关键字分配内存获得一个类指针类型变量,然后可以对这个变量进行链式调用,写出更优雅的代码。 这也就是常说笑的,没有对象?那就去new一个呗。
// main函数升级版
int main(){
// 简易6阶迷宫
int mazeMatrix[][MaxRank] = {
{1, 1, 1, 1, 1, 1},
{0, 0, 0, 0, 1, 1},
{1, 0, 1, 0, 0, 1},
{1, 0, 0, 0, 1, 1},
{1, 1, 0, 0, 0, 0},
{1, 1, 1, 1, 1, 1},
};
position entrance = { 1, 0, -2},
exit = {4, 5, -2};
Maze * maze = new Maze(mazeMatrix, sizeof(mazeMatrix)/sizeof(mazeMatrix[0]), entrance, exit); // new关键字申请内存返回 Maze * 的指针类型
maze->printMaze(); // 预先打印,maze变量是个指针,需要使用指针运算符访问其方法
maze->findSolution()->setAccess()->clearMapMark()->printMaze()->clearSolution(); // 链式调用,效果和之前一样
深度优先遍历输出迷宫的所有通路
接下来我们来完成实现输出迷宫的所有通路,其中用了深度优先遍历的思想。
深度优先遍历出所有通路虽然听起来难度很大,实际上难度也不小。
喂喂,别这么快就放弃,其实它的代码在形式上和查找一条通路的方法有很多相似之处。
但是这里我将引导你从之前的遍历思维转换到我求解需要的深度优先遍历思想,你会发现,其实也不过就那么一回事儿~。
为了能打印出所有的解,我们又需要一个变量存储一组solution
这样的数据。所以我们利用<list>定义一个存储solution
类型的链表。
回忆之前保存position
类型的链表`solution`的类型对应为 list<position>
那么现在我们定义一个存放一组解的链表就要这么写:
list<list<position>> solutions; // 没错,就是这么疯狂
如果你绝这么写看起来实在是别扭的话,那么typedef绝对能助你一臂之力
typedef list<position> oneway;
list<oneway> solutions; // 是不是对数据类型有了更高的抽象认识ヾ(≧▽≦*)o
由于递归需要一个栈来回退,显然需要是更高定义域的,那么放到类的成员数据中也是合适的。再定义一个辅助数据middleStack
这是现在的类成员数据:(多嘴一句,考虑后续的拓展继承的话,这些成员数据更适合声明在protected:下面)
typedef list<position> oneway;
class Maze{
private:
int mazeMatrix[MaxRank][MaxRank] = {0};
int mazeRank = 0;
position entrance = {0,0,-2};
position exit;
// 存储求解结果, 存储一个解
list<position>solution;
// 存储求解结果集,可存储多个解
list<oneway> solutions; // list<list<position>> solutions; 这样也是可以的
// 求解辅助数据
stack<position> middleStack;
// .....
现在准备工作已经做好了,我们可以开始着手写遍历出所有同路的方法了。在这种需要遍历出所有通路的情况,可以想到把之前求出一条通路的方法改造成递归也许就可以实现。
但这次的遍历和之前的有什么不同呢?
如果找不同之处比较难,我们可以先列举一下他们的相同之处:
一、都要输出通路,故都需要一个栈来保存从而回溯,而递归中的保存信息的栈要么作为参数在递归的函数方法内部递归时传递,要么就放到全局共享访问,这也就是我们定义了一个辅助数据栈`middleStack`的原因。
二、都需要判断通路已经作访问记录。通路可能是上右下左,不保存访问记录很容易就死亡循环。所以之前`findAccess`函数中的判断四个方向的if条件仍是需要的,访问记录也是需要的,那我们还是将记录标记到地图上,以值2作为访问标记。
提示到这里,应该也很快就能发现不同之处,要输出所有通路的递归访问至少要把迷宫地图上的所有宫格都访问一遍,而不是找到路能走通就行,上右下左四个方向都要访问,故不是if ... else if ... else的单逻辑通路了,应该是满足是未被访问的通路的条件即可,也即几个if并列。所以判断成功找到通路后不是程序的终止,而是另一条通路寻找的开始。而有了递归,我们也不需要用while循环进行驱动了,所以可以摸出算法的大概框架了:
Maze * DeepFirstSearch_solution(position current){
// 成功的标识
if(已经达到出口){
保存结果到 this->solutions;
return this;
}
// 查找上方未被访问的可通行宫格
if(conditions){
// 对该方向进行记录访问路径并递归驱动
// ...
}
// 查找右方未被访问的可通行宫格
if(conditions){
// 对该方向进行记录访问路径并递归驱动
// ...
}
// 查找下方未被访问的可通行宫格
if(conditions){
// 对该方向进行记录访问路径并递归驱动
// ...
}
// 查找左方未被访问的可通行宫格
if(conditions){
// 对该方向进行记录访问路径并递归驱动
// ...
}
return this;
}
而在上面的每个if语句块中要执行的操作几乎是固定的:
1. 获取下一步前进的宫格
2. 先增加访问标记
3. 入栈 先记录访问路径
4. 访问目标宫格,递归驱动
5. 访问完成后退栈,清除访问标记供其他查找通路访问
但其实上面的逻辑是有点问题的,请仔细思考一下第五点:同一轮中我们会访问四个方向进行递归驱动,如果一个方向的递归访问完成后就立马清除访问标记,那么这轮中其他方向的递归驱动的时候不又可以访问到这个方向的宫格,这就会造成死亡递归。所以,我们应该要把递归访问后的清除标记放到最后,每轮清除一次就可以了。
真正的算法代码已经要呼之欲出了!但不急,还有几个小细节:
第一次递归时显然需要把初始宫格也就是入口进栈做记录,而之后不用,那么我们就给递归方法增加一个counts参数传递,每次传递时自增,这样就可以用counts的值来判断其递归的次数,甚至可以用它来防止出错时死亡递归卡机,设定一个比较大的递归上限就可以了。(当然设置断点调试是更好的)
先来看下现在能实现的半伪代码吧:(最佳的阅读方式是先看判断四个方向的伪代码到结束再从头看起)
Maze * DeepFirstSearch_solution(position current, counts){
if(counts==0){ // counts为0说明第一次递归
this->middleStack.push(current); // 最初的位置(起点)当然要保存一下啦
}
// 成功的标识
if(已经达到出口){
保存结果到 this->solutions;
clearVisit(nextOne); // 成功也是一轮,执行完也要清除访问标记
return this;
}
// 查找上方未被访问的可通行宫格
if(conditions){
current = nextOne; // 获取下一步前进的宫格
visit(nextOne); // 先增加访问标记
this->middleStack.push(current); // 访问了就入栈 记录访问路径
this->DeepFirstSearch_solution(current, ++counts); // 递归驱动,同时counts自增传递计数
this->middleStack.pop(); // 递归完毕说明该方向通路已经探索完毕 退栈一次
}
// 查找右方未被访问的可通行宫格
if(conditions){
current = nextOne; // 获取下一步前进的宫格
visit(nextOne); // 增加访问标记
this->DeepFirstSearch_solution(current, ++counts); // 递归驱动
this->middleStack.pop(); // 递归完毕说明该方向通路已经探索完毕 退栈一次
}
// 查找下方未被访问的可通行宫格
if(conditions){
current = nextOne; // 获取下一步前进的宫格
visit(nextOne); // 增加访问标记
this->DeepFirstSearch_solution(current, ++counts); // 递归驱动
this->middleStack.pop(); // 递归完毕说明该方向通路已经探索完毕 退栈一次
}
// 查找左方未被访问的可通行宫格
if(conditions){
current = nextOne; // 获取下一步前进的宫格
visit(nextOne); // 增加访问标记
this->DeepFirstSearch_solution(current, ++counts); // 递归驱动
this->middleStack.pop(); // 递归完毕说明该方向通路已经探索完毕 退栈一次
}
clearVisit(nextOne); // 每轮执行完清除访问标记
return this;
}
接下来就是完成具体的实现了,访问标记和获取下一步的宫格什么的,就沿用之前的方式即可。但是这里多了一步保存所有结果,我们需要遍历一个保存position链表的链表。这里经过请教 @ZaxTyson 最终用了C++11标准中的auto for循环大法,其中的i就是链表对象中的数据项实例,我不是专业学习C++的,所以也不明白也不必去明白背后的原理了
// auto遍历法 i 就是实例对象 one_solution是我们要遍历的链表对象
for(auto && i : one_solution){
// 遍历完成后栈还原 栈middleStack中由栈顶到栈底是终点到起点的顺序
this->middleStack.push(i);
}
在函数开头,因为作用是存储所有结果,我们还需要清除一下之前的保存,这样就又需要一个方法,干脆就重载一下`clearAccess`方法,传递一个整型参数时清空this->solutions
不传递参数时清空`this->solution`
// 清空`solutions`中的第solutionIndex个数据存储,传入-1 时全部清空
Maze * clearAccess(int solutionIndex){
if(solutionIndex == -1){
this->solutions.clear();
return this;
}
int index = 0;
for(auto && i: this->solutions){
if(index==solutionIndex){
i.clear(); //
}
index++;
}
return this;
}
来不及解释了更多了,这是求所有通路的函数源码了:
// 用深度优先遍历 查找迷宫的所有解 存储至`solutions`中
// 传递的参数需要是起点的position结构体
Maze * DeepFirstSearch_solution(position current, int counts = 0){
if(counts==0){
this->clearAccess(-1); // 万万不可!! 先清空solutions中的所有数据,否则递归中记录不了结果
// 首次执行先把起点压入栈中
this->middleStack.push(current);
}
// 成功找到一条通路
if(current.row==this->exit.row && current.column==this->exit.column){
int size = this->middleStack.size();
list<position> one_solution;
for(int i=0;i<size;i++){
// 栈middleStack中由栈顶到栈底是终点到起点的顺序
one_solution.push_front(this->middleStack.top() ); // 头插法 倒序存储 栈中元素
this->middleStack.pop(); // 一边出栈一个元素
}
// 此时栈空了,重复利用栈,再遍历list把数据存储进去供下次回溯
for(auto && i : one_solution){
// 遍历完成后栈还原 栈middleStack中由栈顶到栈底是终点到起点的顺序
this->middleStack.push(i);
}
// 同时将当前访问点标记清除,与整体保持一致
this->mazeMatrix[current.row][current.column] = 0;
current.passable = 0; // current周围的通路设为 未知状态
this->solutions.push_back(one_solution);
return this;
}
position nextOne;
// 查找current的相邻且为通路的宫格,需要对所有满足条件的宫格进行搜索,所以if并列代替单个选择的if...else if...else结构
// 上方
if(current.row - 1 > 0 && this->mazeMatrix[current.row - 1][current.column]==0){
current.passable = -1;
this->mazeMatrix[current.row - 1][current.column] = 2; // 下一步方向设置访问标记,置为特殊不可访问状态
nextOne = {current.row - 1, current.column, -1}; // 获取下一步方向的宫格数据
this->middleStack.push(nextOne); // 下一步的宫格数据入栈
// 访问成功的情况需要 再次递归 完成后 退栈还原
this->DeepFirstSearch_solution(nextOne, ++counts); // 递归驱动,访问下一步方向
this->middleStack.pop(); // 递归完毕,该方向通路已经探索完成 退栈供查找其他通路时访问
}
// 右方
if(current.column +1 < this->mazeRank && this->mazeMatrix[current.row][current.column+1]==0){
current.passable = -2;
this->mazeMatrix[current.row][current.column+1] = 2;
nextOne = {current.row, current.column+1, -2};
this->middleStack.push(nextOne);
this->DeepFirstSearch_solution(nextOne, ++counts);
this->middleStack.pop();
}
// 下方
if(current.row + 1 < this->mazeRank && this->mazeMatrix[current.row+1][current.column]==0){
current.passable = -3;
this->mazeMatrix[current.row+1][current.column] = 2;
nextOne = {current.row+1, current.column, -3};
this->middleStack.push(nextOne);
this->DeepFirstSearch_solution(nextOne, ++counts);
this->middleStack.pop();
}
// 左方
if(current.column -1 > 0 && this->mazeMatrix[current.row][current.column - 1]==0){
current.passable = -4;
this->mazeMatrix[current.row][current.column - 1] 2= ;
nextOne = {current.row, current.column - 1, -4};
this->middleStack.push(nextOne);
this->DeepFirstSearch_solution(nextOne, ++counts);
this->middleStack.pop();
}
this->mazeMatrix[current.row][current.column] = 0; // 还原当前宫格的普通通路状态
current.passable = 0; // 下一步通路方向设为 未知状态
return this;
}
篇幅有限,main 函数中的调用我就不放了,仓库中都有,看下运行结果吧:
剩下一个求最短路径的问题拓展,这个用广度优先遍历的思想就可以实现,比求所有通路要简单很多,我都有实现放在代码中了,有兴趣去完整实现中看下。
另外我还准备了一个大一点的迷宫供玩耍:
// 11阶升级版迷宫
int mazeMatrixUpgrate[][MaxRank] = {
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
{0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1},
{1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1},
{1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1},
{1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1},
{1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1},
{1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1},
{1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1},
{1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1},
{1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0},
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
};
position entrance = { 1, 0, -2 },
upgrateExit={9, 10, -2};
完整代码实现见GitHub仓库:
https://github.com/lozyue/MazeSolutiongithub.com好了,本文到这里就结束了,感谢你的耐心阅读,看到这里的你一定是最棒的,感谢你的喜欢点赞和收藏,我们下次再见啦~