迷宫寻路
应用情景
例如如图所示迷宫,黄色方格代表起点,橙色方格代表终点,绿色方格代表可走路径,蓝色方格代表障碍物。已知这是一个 M × N 大小的迷宫,可以用 0 表示可走路径,1 表示障碍,算法要求实现从迷宫的任意一点出发,试探出一条通向终点的路径。
应用解析
刚看到这个情景,我们是一头雾水的,因此在开始解析之前,我们先把迷宫的构成说明白。如图是一个我已经鸽了很久的 RPG 游戏制作页面,我们发现这个页面和游戏的界面是不一样的,游戏的舞台被一个个方格所切割,制作这类的游戏时,我无论是绘制地图、设置事件还是踩雷遇怪,都是通过对这些方格填充内容实现的,而这些方格在一张确定大小的地图上都是有对应坐标的。
我们是怎么定位可控角色在地图的位置的?其实也是通过这些坐标,确定好角色所在的方格之后就把角色的贴图填充进去。当角色进行移动时,我们先获取这个角色的坐标,确定移动到哪个方格之后,播放设置好的行走图即可实现。不知道爱玩游戏的你是否想过这些问题呢?(笑)
深度优先
现在我们已经把背景说明白了,再来思考一下,我们可以把需求的迷宫描述为一个二维数组,二维数组抽象成几何图形的时候是一个矩阵,因此我们的问题就变成了描述起点到终点坐标的问题了。这个时候我们就要模拟一个玩家,这个玩家要标记他走过的坐标,由于要自动寻路,有东南西北四个维度,因此我们就以这个顺序先作为玩家的寻路顺序。当玩家遇到死路时,就要退回之前走过的路,因此我们就发现了栈结构“后进先出”的特性很适合用于描述走回头路的过程。
代码实现
#include<iostream>
#include<stack>
using namespace std;
#define M 8
#define N 8
typedef struct
{
int x; //路径的 x 坐标
int y; //路径的 y 坐标
int next_direction = 1; //表示方位,由 1 到 4 分别表示东南西北
} unit;
void Labyrinth(int xi, int yi, int xe, int ye); //走迷宫函数
void exploreWay(int x, int y, stack<unit>& path, unit& a_unit); //探路函数
int a_maze[M + 2][N + 2] =
{
{1,1,1,1,1,1,1,1,1,1},
{1,0,0,1,0,0,0,1,0,1},
{1,0,0,1,0,0,0,1,0,1},
{1,0,0,0,0,1,1,0,0,1},
{1,0,1,1,1,0,0,0,0,1},
{1,0,0,0,1,0,0,0,0,1},
{1,0,1,0,0,0,1,0,0,1},
{1,0,1,1,1,0,1,1,0,1},
{1,1,0,0,0,0,0,0,0,1},
{1,1,1,1,1,1,1,1,1,1}
};
int main()
{
Labyrinth(1, 1, M, N);
for (int i = 0; i < M + 2; i++) //打印迷宫路径
{
for (int j = 0; j < N + 2; j++)
{
if (a_maze[i][j] == 4)
cout << " ";
else
cout << a_maze[i][j] << " ";
}
cout << endl;
}
return 0;
}
void exploreWay(int x, int y, stack<unit>& path, unit& a_unit)
{
if (a_maze[x][y] == 0) //该方向路可以走
{
a_unit.x = x;
a_unit.y = y;
a_maze[x][y] = 2;
path.push(a_unit); //新路径入栈
}
else //改变方向,准备下一次探路
path.top().next_direction++;
}
void Labyrinth(int xi, int yi, int xe, int ye) //探查下一个可走的路径
{
stack<unit> path;
unit a_unit;
exploreWay(xi, yi, path, a_unit);
while (!path.empty())
{
if (path.top().x == xe && path.top().y == ye)
break;
switch (path.top().next_direction)
{
case 1: //向东探路
exploreWay(path.top().x + 1, path.top().y, path, a_unit);
break;
case 2: //向南探路
exploreWay(path.top().x, path.top().y + 1, path, a_unit);
break;
case 3: //向西探路
exploreWay(path.top().x - 1, path.top().y, path, a_unit);
break;
case 4: //向北探路
exploreWay(path.top().x, path.top().y - 1, path, a_unit);
break;
default: //走到死路
a_maze[path.top().x][path.top().y] = 9; //标记为死路
path.pop(); //栈顶退栈
}
}
while (!path.empty()) //为了打印路径,给迷宫挖空
{
a_maze[path.top().x][path.top().y] = 4;
path.pop();
}
}
运行效果
- 虽然这段代码是以深度优先搜索为基础写出来的,但是这并不完整,因为理论上深度优先搜索是可以找到所有路径的。解决方案是找到终点之后仍然走回头路,在有岔路的地方再次进行试探,直到没有岔路可走为止。此处只是为了展示栈结构的应用,深度优先搜索并不是我们当前要谈的问题,因此我简化的操作,并且将路径挖空,帮助我们更好地理解。
迷宫寻路(广度优先)
应用解析
在这里我们想利用广度优先的思想来实现,本算法的思想是从 (xi,yi) 开始,利用队列的特点,一层一层扩大搜索的直径,把可走的点都导入到队列中,直到搜索到终点。
不过我这里主要是为了展示队列的应用,因此对于广度优先我在这里不过多阐述,可以自行查阅相关资料理解。这里需要强调的是,由于我们需要得到完整的路径,也就是说搜索过的路径不能够真出队列,以便于我们得到答案,因此就不能使用 STL 库的 queue 容器来实现,自建队列的话就要使用顺序队列来描述。不过我认为此处最适合的是 STL 库的 vector 容器,我们只需要定义两个游标来描述 vector 对象的队列头和尾的位置,就可以使用 vector 的方法来实现插入等操作,我们在解决银行排队问题的时候也是这么做的。
代码实现
#include<iostream>
#include<vector>
using namespace std;
#define M 8
#define N 8
typedef struct
{
int x;
int y; //路径的坐标
int pre; //表示该路径前驱的游标
} unit;
int exploreWay(int x, int y, vector<unit>& path, unit& a_unit, int front, int& rear); //添加单个路径
void Labyrinth(int xi, int yi, int xe, int ye, vector<unit>& min_path); //路径搜索函数
int a_maze[M + 2][N + 2] =
{
{1,1,1,1,1,1,1,1,1,1},
{1,0,0,1,0,0,0,1,0,1},
{1,0,0,1,0,0,0,1,0,1},
{1,0,0,0,0,1,1,0,0,1},
{1,0,1,1,1,0,0,0,0,1},
{1,0,0,0,1,0,0,0,0,1},
{1,0,1,0,0,0,1,0,0,1},
{1,0,1,1,1,0,1,1,0,1},
{1,1,0,0,0,0,0,0,0,1},
{1,1,1,1,1,1,1,1,1,1}
};
int main()
{
vector<unit> min_path; //存储最短路径
Labyrinth(1,1, M, N,min_path);
cout << "最短路径为:" << endl;
for (int i = min_path.size() - 1; i >= 0; i--)
{
cout << "(" << min_path[i].x << " , " << min_path[i].y << ") ";
if ((min_path.size() - i) % 5 == 0)
cout << endl;
}
return 0;
}
int exploreWay(int x, int y, vector<unit>& path, unit& a_unit, int front, int& rear)
{
if (a_maze[x][y] == 0) //若路径可走
{
a_unit.x = x;
a_unit.y = y;
a_unit.pre = front;
path.push_back(a_unit); //添加路径
rear++; //移动尾指针
a_maze[x][y] = 2;
return 1;
}
return 0;
}
void Labyrinth(int xi, int yi, int xe, int ye, vector<unit>& min_path) //搜索路径为:(xi,yi)->(xe,ye)
{
vector<unit> path; //存储所有可走路径
unit a_unit;
int front, rear = -1;
front = rear;
exploreWay(xi, yi, path, a_unit, front, rear); //添加起点路径,同时初始化头尾指针
while (rear != front)
{
front++; //判断路径东侧结点是否添加
if (exploreWay(path[front].x + 1, path[front].y, path, a_unit, front, rear) == 1
&& a_unit.x == xe && a_unit.y == ye)
break; //判断路径南侧结点是否添加
if (exploreWay(path[front].x, path[front].y + 1, path, a_unit, front, rear) == 1
&& a_unit.x == xe && a_unit.y == ye)
break; //判断路径西侧结点是否添加
if (exploreWay(path[front].x - 1, path[front].y, path, a_unit, front, rear) == 1
&& a_unit.x == xe && a_unit.y == ye)
break; //判断路径北侧结点是否添加
if (exploreWay(path[front].x, path[front].y - 1, path, a_unit, front, rear) == 1
&& a_unit.x == xe && a_unit.y == ye)
break;
}
while (rear != -1) //将搜索到的最短路径移动到 min_path
{
min_path.push_back(path[rear]);
rear = path[rear].pre;
}
}
运行效果
参考资料
《大话数据结构》—— 程杰 著,清华大学出版社
《数据结构教程》—— 李春葆 主编,清华大学出版社
《数据结构(C语言版|第二版)》—— 严蔚敏 李冬梅 吴伟民 编著,人民邮电出版社