迷宫问题-回溯法-栈的使用-溢出问题

本文详细解析了使用栈解决迷宫问题的过程,包括算法设计思路、代码实现及调试经验分享。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

辣鸡小玲的解题报告。
这学期上数据结构,学到栈,冯向阳老师教,放了一题“迷宫问题”到OJ上,让做。我照着PPT给的伪代码写,然后改了几个小时,终于在电脑快没电要自动关机的时候改完了,今天是第二天,乘着还没忘,赶紧把解题报告写出来。

题目

7 顺序栈ADT模板简单应用算法设计:迷宫问题
作者: 冯向阳 时间限制: 1S章节: DS:栈
问题描述 :
目的:使用C++模板设计顺序栈的抽象数据类型(ADT)。并在此基础上,使用顺序栈ADT的基本操作,设计并实现简单应用的算法设计。
内容:(1)请参照顺序表的ADT模板,设计顺序栈的抽象数据类型。(由于该环境目前仅支持单文件的编译,故将所有内容都集中在一个源文件内。在实际的设计中,推荐将抽象类及对应的派生类分别放在单独的头文件中。参考教材、课件,以及网盘中的顺序表ADT原型文件,自行设计顺序栈的ADT。)
(2)ADT的简单应用:使用该ADT设计并实现若干应用顺序栈的算法设计。
应用:在迷宫中找出从入口到出口的路径是一个经典的程序设计问题。最简单的迷宫可以表示为一个由方块组成的矩阵,其中每个方块或为墙,或为通道。要求应用顺序栈,设计一个算法,在给定的迷宫矩阵maze中,找出从入口到出口的一条简单路径,即同一个通道在路径上不能出现两次以上。迷宫在计算机内可以用一个二维数组表示,每个数组元素表示一个方块。如果是通道,值为0,反之为1。
提示:
(1)可以用如图所示的方块表示迷宫。其中,图中的空白方块为通道;图中的斜线方块为墙;所求路径必须是简单路径,即在求得的路径上不能重复出现同一通道块。
在这里插入图片描述
(2)算法的基本思想为:
1)探索到出口的路径,具有递归性质:
若当前位置是出口,则问题已解决;
若当前位置不可通,则探索失败;
向可行的方向走一步,从那里出发探索到出口的路径。
2)本问题的特点:
在每个位置上可能有多个可行选择,有分支,需要逐一试探;
只需要找到一条路经(而不是所有可能路径)。
3)要解决这个问题,需要:
为问题找一种数据表示;
一种确定可行方向的方式;
防止出现兜圈子的情况(设法纪录已试探过的位置)。

(3)问题表示:

1)用整数矩阵(二维数组)表示迷宫。

2)初始时,通路上的点用0表示,非通路点用1。

3)入口和出口都是数组下标对。

4)为避免陷入无限循环,在探索中把试探过的点标记为2。

(4)方向处理:捕获.jpg

1)找一种方便形式,表示从一个位置出发的可能探索位置。

 迷宫中任一位置(i,j)有4个可能方向。

2)用数组direction表示可能方向(4个)。数组元素是计算4个方向下一点的偏移值,便于计算各方向的下一位置。

 int direction[4][2]={-1,0,0,1,1,0,0,-1};

在这里插入图片描述
(4)算法的基本思想:

1)从入口出发,采用试探方法,搜索到目标点(出口)的路径,遇到出口则成功结束。

2)遇到分支点时选一个方向向前探索,这时需纪录当时的分支点和在这里已试探过的分支(和尚未试探过的分支)。

3)若遇到死路(所有方向都不能走或已试探过),就退回前一分支点,换一方向再探索。直到找到目标,或者所有可能通路都探索到为止。这类方法称为回溯法。

4)每次回退(回溯)时总是去考虑最近纪录的那个分支点,如果最近分支点已经没有其它选择,就把它删除;
5)纪录和删除具有后进先出性质,可以用栈保存分支点信息;
6)遇到分支点将相关信息压入栈,删除分支点时将它弹出。
要求:入口、出口坐标为输入参数。如找到路径,则正向输出路径经过的每个矩阵元素的位置信息,矩阵元素之间用“->”分隔。每行超过4个矩阵元素则换行输出。如找不到路径,则输出"No Path"。为保证和测试数据一致,探索顺序应按照提示的顺序进行。
参考函数原型:

template

void maze_path( SqStack &S, int row, int col, node enter, node outer, int **maze );

辅助函数:

(1)bool make2DArray(int row,int col, int ** &maze); //二位数组空间申请

(2)bool InputMaze(int row, int col, int ** &maze); //迷宫矩阵输入

(3)顺序栈ADT基本操作函数:若栈不空,则用e返回S的指定位置元素,并返回TRUE;否则返回FALSE。

template

bool SqStack::GetElement(int pos, ElemType &e) const;

(4)模板参数ElemType设定为node(struct类型)

//迷宫专用结点类型

struct node{

int x,y;

int dir;

};

输入说明 :

第一行:迷宫矩阵的行数row 列数col

第二行:入口位置信息

第三行:出口位置信息

第四行-第(4+row)行:迷宫矩阵

输出说明 :

路径信息:每一行最多包含路径上4个点的位置信息,点与点之间用"->“分隔。如未找到路径,则输出"No Path”。

输入范例 :
10 10
0 0
9 9
0 0 1 1 1 1 1 0 0 0
1 0 1 0 0 0 0 0 1 1
1 0 0 0 1 1 0 1 1 1
1 1 0 1 1 1 0 1 1 0
1 1 0 1 1 1 0 0 0 1
1 0 0 1 1 1 0 1 1 1
1 1 0 0 0 0 0 1 1 1
1 1 0 1 1 0 1 1 1 1
1 1 1 1 1 0 0 0 0 0
1 1 1 1 1 0 1 1 0 0
输出范例 :
(0,0)->(0,1)->(1,1)->(2,1)->
(2,2)->(2,3)->(1,3)->(1,4)->
(1,5)->(1,6)->(2,6)->(3,6)->
(4,6)->(5,6)->(6,6)->(6,5)->
(7,5)->(8,5)->(8,6)->(8,7)->
(8,8)->(8,9)->(9,9)

代码

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <string>
#include <sstream>
using namespace std;
const int MAXLISTSIZE = 1000;
int const direction[4][2]= {-1,0,0,1,1,0,0,-1};
//全局数组、方向数组 路线按照<左上右下>遍历
struct node
{
    //迷宫专用结点类型
    int x,y;
    int dir;
    node()
    {
        x=0,y=0,dir=-1;
    }
    node(int x,int y,int dir=-1)
    {
        this->x=x;
        this->y=y;
        this->dir=dir;
    }
};
template<class ElemType>
class SqStack
{
private:
    ElemType *base;   // 栈底指针
    ElemType *top;   // 栈顶指针(栈顶元素的下一个位置)
    int maxSize;        // 允许的最大存储容量(以sizeof(ElemType)为单位
public:
    //构造函数,初始化顺序栈为空栈
    SqStack(int ms = 200)
    {
        if(ms==0)
            ms=MAXLISTSIZE;
        base=new ElemType[ms];
        if(!base)
        {
            cout<<"Falure to get space"<<endl;
            exit(1);
        }
        maxSize=ms;
        top=base;//设置为空栈
    }
    //基于输入输出流的构造函数,忽略空字符版
    SqStack(string s,int ms=200)
    {
        if(ms==0)
            ms=MAXLISTSIZE;
        base=new ElemType[ms];
        if(!base)
        {
            cout<<"Fail to get space"<<endl;
            exit(1);
        }
        maxSize=ms;
        top=base;//设置为空栈
        stringstream sin(s);
        ElemType date;
        while(sin>>date)
        {
            this->push(date);
        }
    }
    //析构函数,删除顺序栈
    ~SqStack()
    {
        StackDestroy();
    }
    //将顺序栈置为空表
    bool StackClear( )
    {
        if(!base)
        {
            cout<<"The stack hasn't be built"<<endl;
            return false;
        }
        top=base;
        return true;
    }
    //返回顺序栈的长度
    int StackLength() const
    {
        return top - base;
    }
    //判断顺序栈是否为空栈
    bool StackisEmpty() const
    {
        return top == base;
    }
    //判断顺序栈是否为满栈
    bool StackFull() const
    {
        if(this->StackLength()==maxSize)
            return true;
        return false;
    }
    //用e返回栈顶元素
    bool GetTop(ElemType &e) const
    {
        if(this->StackLength()==0)//如果已经栈空
        {
            cout<<"Stack is empty!"<<endl;
            return false;
        }
        e=*(top-1);
        return true;
    }
    //入栈
    bool push(ElemType &e)
    {
        if(top-base>=maxSize)//如果栈满,双倍扩容
        {
            DoubleSpace();
        }
        *top++=e;
        return true;
    }
    //出栈
    bool pop(ElemType &e)
    {
        if(top==base)//如果已经栈空
        {
            cout<<"Stack is empty!"<<endl;
            return false;
        }
        e=*(--top);
        return true;
    }
    //销毁顺序栈
    bool StackDestroy()
    {
        if(!base)//若栈未建立,返回
        {
            cout<<"The stack hasn't build"<<endl;
            return false;
        }
        delete [] base;// 回收栈空间
        base=NULL;
        top=NULL;
        maxSize=0;
        return true;
    }
    //遍历顺序栈
    void StackTraverse() const//就当你要逆置输出了
    {
        ElemType *p=top-1;
        for(int i=0; i<this->StackLength(); i++)
        {
            cout<<'('<<p->x<<','<<p->y<<")->";
            p--;
        }
        cout<<endl;
    }
    //栈空间加倍
    bool DoubleSpace()
    {
        ElemType *newbase;   // 栈底指针
        ElemType *newtop;    // 栈顶指针(栈顶元素的下一个位置)
        int newmaxSize=2*maxSize;// 允许的最大存储容量(以sizeof(ElemType)为单位
        newbase=new ElemType[newmaxSize];
        newtop=newbase;
        if(!newbase)
        {
            cout<<"Fail to get space of doublestack!"<<endl;
            return false;
        }
        int i=0;
        for(i=0; i<maxSize; i++)
        {
            *newtop=*top;
            newtop++;
            top++;
        }
        top=newtop;
        base=newbase;
        return true;
    }
    //输入函数,空字符也算一个元素
    void input(string str)
    {
        stringstream sin(str);
        ElemType date;
        while(sin.get(date))
        {
            this->push(date);
        }
    }
    //bool GetElement(int pos, ElemType &e) const;
};
bool make2DArray(int row,int col, int ** &maze)//二位数组空间申请
{
    int **m=new int *[row];
    if(!m)
    {
        cout<<"fail to get space of m"<<endl;
        exit(1);
    }
    for(int i=0; i<col; i++)
    {
        m[i]=new int [col];
        if(!m[i])
        {
            cout<<"fail to get space of m[i]"<<endl;
            exit(1);
        }
    }
    maze=m;
    return true;
}
bool InputMaze(int row, int col, int ** &maze)//迷宫矩阵输入
{
    int i=0,j=0;
    for(i=0; i<row; i++)
    {
        for(j=0; j<col; j++)
        {
            cin>>maze[i][j];
        }
    }
    return true;
}
//顺序栈ADT基本操作函数:若栈不空,则用e返回S的指定位置元素,并返回TRUE;否则返回FALSE。
/*template<class ElemType>
bool SqStack<ElemType>::GetElement(int pos, ElemType &e) const
{
    return true;
}*/
template<class ElemType>
void print_path(SqStack<ElemType> &S)
{
    //用等量空间逆置S,再输出
    SqStack<ElemType> A;
    while(!S.StackisEmpty())
    {
        ElemType e;
        S.pop(e);
        A.push(e);
    }
    int i=0;
    int len=A.StackLength();
    while(!A.StackisEmpty())
    {
        i++;
        ElemType e;
        A.pop(e);
        cout<<'('<<e.x<<','<<e.y<<')';
        if(i!=len)//这个是len还是len-1等会试试就知道
            cout<<"->";
        if(i%4==0)//16个元素要不要换行,这种可能会CE
            cout<<endl;
    }
}
template<class ElemType>
void maze_path( SqStack<ElemType> &S, int &row, int &col, node &enter, node &outer, int **maze )
{
    //在迷宫maze中求从enter到outer的路径
    int i,j,k,g,h;
    maze[enter.x][enter.y]=2;//标记初始点防止折回 MARK II
    S.push(enter);//入口点进栈
    while(!S.StackisEmpty())//走不通时一步一步回退
    //如果是空栈就代表回溯到了起点,连起点都pop了,就是无解的迷宫。
    {
        node ele,nod;
        S.GetTop(ele);//取出前一分支点,不是pop噢 MARK III
        i=ele.x;
        j=ele.y;
        for(k=ele.dir+1; k<=3; k++) //依次检查还未试探的方向
        {//MARK IV
            g=i+direction[k][0];//算出下一节点
            h=j+direction[k][1];
            if(g<0||g>=row||h<0||h>=col)//如果算出的这个结点跳出边界外了
                continue;
            else if(maze[g][h]!=0)//如果算出的这个结点是不能走的结点
                continue;
            if(g==outer.x&&h==outer.y)//如果到达出口,打印路径
            {
                nod.x=g;
                nod.y=h;
                S.push(nod);
                print_path(S);
                return;
            }
            if(maze[g][h]==0)//如果是一个没试探过的结点
            {
                nod.x=g;
                nod.y=h;
                S.push(nod);//新结点进栈
                maze[g][h]=2;//标记这个新节点走过
                i=g;
                j=h;
                k=-1;//这样一完成,就会更新k值,导致新一轮的for循环
            }
        }
        if(k==4)
        {
            S.pop(ele);
        }
    }
    cout<<"No Path"<<endl;
}
int main()
{
    int row,col;
    while(cin>>row>>col)//读入迷宫大小
    {
        int **maze;//创建二位数组存取迷宫
        int beg_i,beg_j;//起点
        int end_i,end_j;//终点
        cin>>beg_i>>beg_j>>end_i>>end_j;//读入起点终点
        make2DArray(row,col,maze);//创建二位数组
        InputMaze(row,col,maze);//读入二位数组(迷宫)
        SqStack<node> S;//创建栈   MARK I
        node enter(beg_i,beg_j);
        node outer(end_i,end_j);
        maze_path(S,row,col,enter,outer,maze);
    }
    return 0;
}

回溯法

回溯法的意思就是,一步一步走,如果这条路不通,就回到上一步。

在这里插入图片描述
比如图中绿点开始,按照冯向阳老师的算法设计,是路线按照<左上右下>遍历。
绿点按照蓝线走,走到头发现不对,于是一步一步往回撤,撤回绿点,再向下走。

MARK I 316行

迷宫问题创建栈的作用是保存路径结点,每走可以走的一步都将当前的点压入栈,如果回溯,就把这点pop出栈,这样,在遇到终点的时候(有解),栈中储存的就是每一步走过来的路径结点。
同时,如果一直回溯,回溯到起点去了,连起点都被pop了,那栈就空了,空栈就代表这个迷宫无解。

MARK II 262行

用二位数组保存迷宫图谱,(也可以用map[][]噢),0表示可以走的路,1表示是墙。
用2表示走过的格子,防止来回走来回走来回走变成死循环。

MARK III 268行

在这里插入图片描述
取结点的时候一定是getTop,不是pop噢,一旦用pop,首结点就直接出去,回不来了。图中绿色的结点更好理解一点:从红线走过来的时候,绿结点入栈,走蓝线,发现不通,反复pop,直到把绿结点也pop出来,然后重新走上正确的红线。这样会导致栈中没有绿结点。
正确的做法应该是getTop,然后如果某个结点四面都走不通,就pop这个结点。
298-301行对于K的判定就是这个意思。

MARK IV 271行

这个for循环也是迷宫算法很精妙的地方。
当一个结点可以走的时候,更新当前坐标,然后这个for循环就被更新了(k又重新等于-1了),这样不会跳出for循环。如果跳出for循环,只有一种可能——k=4,就是四个面都试过了,走不了,那这个结点是要被pop的,所以298行pop了这个结点。

溢出问题

Debug的后两个小时其实是被这个溢出问题(35行的int ms=200)和非法中断(128行的 return false)拖得死死的。

先说非法中断 128行

所谓非法中断,是我自己创的词(狗头),就是程序在遇到非法操作的时候(空栈仍然执行pop),程序猿设置语句对程序的中断。
原本我的中断是这样写的:

if(top==base)//如果已经栈空
        {
            cout<<"Stack is empty!"<<endl;
            exit(1);
        }

直接让程序结束了。
但是这样一结束的话,后面的程序(打印函数)就无法执行了,所以还是把他改成了return false;

然后说溢出问题 35行

最开始的时候,ms是等于20的。这是按照冯向阳给的ADT原型写的ADT。
ms=20造成的问题就是,这个迷宫最后是不止走了20步的,一旦超过20,指针就指到天上去了。也可能是我的DoubleSpace方法没写好,不然应该自动扩容的。

最后一点点总结

1.以后写ADT一定要,写一个方法测试一个方法。

这次不仅pop有问题噢,DoubleSpace、StackTraverse也有问题,说不定方法也有问题,只是我没测试出来。
遇上大问题的时候,要是ADT不好使,那真是抓破头皮噢。

2.提示语句一定要写。

比如127行的 cout<<“Stack is empty!”<<endl;就是Debug时候的救命稻草。

3.大程序多往程序里插标志输出

cout<<“mark”<<endl;这样的语句可以清晰的知道程序可以执行到哪一行。配合使用遍历还可以知道栈里存的是啥。这次Debug,标志输出也是救命稻草。不用标志输出StackTraverse的话,我连溢出都看不出来,指针指到天上去了,我人也被指去天上了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值