一、问题描述
- 以一个m*n(1≤m,n≤100)的长方阵表示迷宫,0和1分别表示迷宫中的通路和障碍。设计一个程序,对任意设定的迷宫,求出一条从入口到出口的通路,或得出没有通路的结论。
- 具体需求:
- 找出迷宫中的一条通路。求得的通路以三元组(i,j,d)的形式输出,其中(i,j)表示 迷宫中的一个坐标,d 表示走到下一坐标的方向。若迷宫中没有通路,则输出“无解”。
- 找出迷宫中的所有可能的通路。
- 如果有通路,找出最短通路。
- 以方阵形式输出迷宫及其通路。
- 补充设定:
- 假定位置(1,1)/* (行,列) */为整个迷宫的起点(m,n)为整个迷宫的终点。
- 假设迷宫中每一个位置只能走一次。
二、结果展示
先上结果,请各位看官自行判断有没有继续往下读的必要。
规则介绍界面:
功能选择界面:
由系统创建的迷宫求解:
已经创建的迷宫展示:
全部解法展示:
一共有40种解法。
其中一条最短路径解法展示:
自定义迷宫求解:
迷宫有解结果展示:
迷宫无解结果展示:
三、求解思路
1.题目分析
- 首先根据题目要求,我们可以将这个规整的长方阵上的每一个点对应到一个自定义的坐标系中,坐标系的大小就是迷宫的大小。
- 任何一个迷宫中都存在两种元素——通路和阻碍。当我们在迷宫中每走一步一定会遇到这两种情况的其中一种,所以在坐标系中的每一个点将会有属性表示其是否是通路或者阻碍。
- 从结果分析,因为我们要求得一条完整的路径,所以我们需要存储走过的一条成功的路线里,分别经历了哪些位置和在该位置上走向下一位置的方向。
- 最后到了最关键的问题,如何找到迷宫的所有解呢?
2.找到迷宫的所有解——左/右手法则
- 我最先想到的是常用的左/右手法则。顾名思义,就是进入迷宫后,选择一个方向,然后在每一个分岔口选择最朝向这个方向的路线一直走下去。若遇到无法走出来的死路时,则退回到上一个分岔口,按照原来选择的方向,重新选择除刚刚退回来的路线以外的其他路线继续走。这样的话总有一个时刻是可以走到终点的。
- 但是这种方法只能保证我们走到终点,而不可以求出全部解,所以我们需要对它进行一些小小的变形。如果学过算法的可以知道,以下方法是深度优先搜索,但是在搜索到结果后还进行了持续遍历以求得最好的结果。
- 改进思路:每走到一个分岔口,记录下该分岔口的位置和接下来可以走的方向。选择其中一个方向向下走,直到死路或是终点,则记录下成功的路线,向后退到离最后位置最近的岔路口,选择其他可以走的方向向下走。当记录的所有分岔口可以走的方向走完为止,于是我们便找到了迷宫所有的通路。
- 这里的可以走的方向是指除了从上一个位置走到该位置的其他所有方向。但是此时还忽略了一个问题,就是当迷宫中存在回路的时候,我们会回来在可能存在回路的地方兜圈子,这显然也是我们在假设中拒绝的,所以在后文中我将提供两种解决回路问题的思路。
3.解决回路问题
- 回路问题的本质是不可以走已经走过的路,假设二的内容也包含在其中。所以有如下两种方法:
- 第一,在地图中记录走过的点,判断将要走的位置不可以是已经走过的点。
- 第二,将要走的位置与已经走过路线中的所有位置进行比较,如果已经走过该位置,则不可以重复前行。
- 如果考虑到后面如何设计存储地图和路线,通过对比我们可以知道第一种方法的优势在于需要对比的位置可以直接找到,但是在定义中需要增加地图的属性;第二种方法的优势在于只用更改路线,而不用对地图有任何的操作,难点是遍历时间长。
- 本文选择的是第二种策略。
四、详细设计
1.数据结构设计
根据面向对象程序设计思想,根据需求分为三类:
- 地图类
- a) 属性:地图数组(储存行列数和数组指针)
- b) 操作:查找单个坐标点的上、下、左、右方向是否为通路,输出地图,输出解谜路线,找到解谜路线。
//地图类:用一维数组储存地图的函数列数以及迷宫
class Map{
public:
int m,n;//m表示列数,n表示行数
int *map;
Map(int m1, int n1, int x[]){
m=m1;
n=n1;
map= new int[m*n];
for(int i=0;i<m*n;i++){
map[i]=x[i];
}
}
~Map(){
delete []map;
}
void printoutMap();
friend int searchright(int &i, int &j, int &d,Map &m);
friend int searchdown(int &i, int &j, int &d,Map &m);
friend int searchleft(int &i, int &j, int &d,Map &m);
friend int searchup(int &i, int &j, int &d,Map &m);
friend int findroute(int i, int j, int d, LinkQueue &last, LinkStack &sel, Map &ma);
friend void figureoutMaze(LinkQueue &route,Map &m);
};
- 路径类(链队)
- a) 属性:结点(储存坐标和移动方向,指向下一个结点的指针),头指针,尾指针
- b) 操作:初始化,队列置空,返回队列的长度,入队操作,出队操作,取队头,判断队是否为空,判断队里已有的点,遍历查找坐标是否存在,寻找解谜路线,将一个队列的所有数据赋值到另一个队列,将一个队列直到一定数据指前的所有数据赋值到另一个队里,输出解谜路线。
//结点结构体
struct Node{
int i,j,d;
struct Node *next;
};
//队列:存储每一个可行的全部链路
class LinkQueue{
private:
Node *front, *rear;
public:
LinkQueue(){ //构造函数
front = new Node;
front->next = NULL;
rear = front;
}
~LinkQueue(){ //析构函数
Node *i;
i = front->next;
for(;front != rear;){
delete front;
front = i;
i = i->next;
}
delete rear;
}
void makeEmpty(){ //置空
Node *i=front->next;
if(front->next==NULL){
return;
}
for(;i != NULL;i=front->next){
front->next = i->next;
delete i;
}
rear=front;
}
int size(){ //队列的长度
int i=0;
Node *p;
for(p=front->next;p!=NULL;p=p->next){
i++;
}
return i;
}
void enQueue(int &i1,int &j1,int &d1){ //入队操作
Node *s = new Node;
s->i = i1;
s->j = j1;
s->d = d1;
s->next = NULL;
rear->next = s;
rear = s;
}
int deQueue(int &i2,int &j2,int &d2){ //出队操作
if(isEmpty()){
return 0;
}
Node *p = front->next;
i2 = p->i;
j2 = p->j;
d2 = p->d;
front->next = p->next;
if(p->next == NULL){
rear = front;
}
delete p;
return 1;
}
void getQueue(int &i3,int &j3,int &d3){ //取队头
i3 = front->next->i;
j3 = front->next->j;
d3 = front->next->d;
}
bool isEmpty(){ //判断队是否为空
return (front == rear)? true:false;
}
int search(int &i4,int &j4,int &d4){ //判断队里已有的点
for(Node *p=front->next;p!=NULL;p=p->next){
if((p->i==i4)&&(p->j==j4)){
return 1;
}
}
return 0;
}
int traverse(int &i5,int &j5,int &d5){ //遍历查找
Node *p=front->next;
for(;p!=NULL;p=p->next){
if((p->i==i5)&&(p->j==j5)){
d5=p->d;
return 1;
}
}
return 0;
}
friend int findroute(int i, int j, int d, LinkQueue &last, LinkStack &sel, Map &ma);
friend void copyto(LinkQueue &pas,LinkQueue &ini,int i,int j, int d);
friend void copyall(LinkQueue &pas,LinkQueue &ini);
friend void figureoutMaze(LinkQueue &route,Map &m);
};
- 多路选择点类(链栈)
- a) 属性:结点(储存坐标和移动方向,指向下一个结点的指针)
- b) 操作:初始化,入栈,出栈,取栈顶,判断栈是否为空,寻找解谜路线。
//栈:存储查找过程中所遇到的多路选择
class LinkStack{
private:
Node* top;
public:
LinkStack(){ //构造函数
top = NULL;
}
~LinkStack(){ //析构函数
if(isEmpty()){
}
else{
Node* i=top->next;
for(;i!=NULL;i=top->next){
top->next= i->next;
delete i;
}
delete top;
}
}
void Push(int &i1,int &j1,int &d1){ //入栈
Node* p;
p=new Node;
p->i = i1;
p->j = j1;
p->d = d1;
if(top==NULL){
top=p;
p->next=NULL;
}
else{
p->next=top;
top=p;
}
}
int Pop(int &i2,int &j2,int &d2){ //出栈
Node* p;
if(top==NULL){
return 0;
}
p=top;
top=p->next;
i2 = p->i;
j2 = p->j;
d2 = p->d;
delete p;
return 1;
}
void getTop(int &i3,int &j3,int &d3){ //取栈顶
i3 = top->i;
j3 = top->j;
d3 = top->d;
}
bool isEmpty(){ //判断队是否为空
return (top == NULL)? true:false;
}
friend int findroute(int i, int j, int d, LinkQueue &last, LinkStack &sel, Map &ma);
};
2.算法设计
-
主函数:先进行界面函数调用并且进入功能选择,确定是使用系统已经定义的地图还是让用户自行输入地图。接着进入循环,最初在栈不空时先出栈,调用寻找路线的函数,如果成功找到路线,则调用输出函数并判断该路线是否为最短通路,如果此时栈不为空则重复循环。最后输出迷宫的最短解。
-
寻找最短路径函数:以0和1分别表示迷宫地图中可以走的通路和路障。从(1,1)点开始分别判断上下左右中除了来时的方向以外的其他三个方向是否可以通过,按照一定顺序将所有可选方向可选则入栈,再出栈顶按照此方向一直向下进行寻路。如果寻路到终点或是无路可走则退出,判断栈是否为空,若不为空则将重复出栈寻路过程,直至所有路线都被寻找到。其中每当判断出一条完整的通路以后,将它与最短的通路作比较,如果它短于最短的路线则赋值给储存最短路线的对象。
五、问题与反思
涉及的问题以及解决方式:
- 第一类:寻找路线的判断条件逻辑问题——在设计时需要考虑到一次的路线不可以与原有的路线重复,不可以走出死循环的情况。
- 第二类:对出现多种情况时的代码的判断考虑不全面——按照线性顺序需要判断多次入栈,出栈入队再更改坐标,十分复杂,所以需要更加清楚退出循环的条件,也就是寻找到路线或者进入死胡同无法向下进行查找。
- 第三类:编译运行时的代码错误——定义类的析构函数的时候不可以析构被全局变量赋值的指针变量指向的空间。
- 第四类:需要让用户自定义迷宫——在最初设计时,我们需要用一维数组进行定义二维平面,以方便用户的输入并传递到地图类中。当然此种设计会使判断条件复杂化,同时也会使坐标和数组的照应变得不明显。不过,它方便了我们完成更复杂的操作。
六、作者的碎碎念
- 虽然我平时做作业经常参考CSDN上的内容,但是这还是我第一次将自己完成课设的思路和经历记录下来。完成迷宫问题求解时我只学习到线性表的内容,水平有限,若有错误和问题请指正,欢迎交流讨论。
PS:
如果需要的人多的话我就上传完整的代码资源吧,感谢你的阅读,祝各位看官好运。