计算机软件技术实习日志项目三(二) 迷宫项目实现
前言
大家好,我将为大家介绍我实现迷宫的具体细节,做软件是我之前没有接触过的领域,做得不好,大家不要见怪。本文章介绍prim生成迷宫和A星自动寻路。本文着重介绍项目实现,原理知识请参考《计算机软件技术实习日志项目三(一) 迷宫项目准备》。这篇文章我本来写过了,但是失误删掉了,所以我又重写了一遍。太南了
一、参数定义
我们定义地图块结构体
#include <utility>
using namespace std;
typedef pair<int, int> prii;
struct node {
CRect rec; //用于画图
int x, y, id, flag,dir; //坐标,用于生成地图通路的id,用于判断该方块是什么类型,方向(最后好像并没有用到)
int f, g, h,ida; //(A星相关的参数)f=g+h,ida用于表示它在A星生成顺序
bool in_open, in_close; //用于判断它是否在open队列和close队列
prii fa,son; //prii其实是pair<int,int> prii
node() { //初始化函数
flag = 1;
x = y = id = dir = 0;
f = g = h = in_open = in_close = ida = 0;
}
bool operator<(const node& rhs) const {
/*
因为用到了优先队列,所以自定义结构体要重载运算符,优先队列默认是大根堆,
所以重载小于号,返回true时表示左边的优先级小于右边的。
*/
if (f == rhs.f) {
return ida < rhs.ida; //旧点优先级低
}
else {
return f > rhs.f; //f大的点优先级低
}
}
void update(node tmp); //由父节点更新的更新的函数
void up();
void down();
void left();
void right();
};
void node::update(node tmp) {
g = tmp.g+1, h = abs(39-x)+abs(39-y); //哈密顿距离
f = g + h;
in_open = 1; //表示在open队列里
ida = ++acnt; //ida号
fa = {tmp.x,tmp.y}; //指向父节点
open_queue.push(*this); //进队
}
void node::up() {
dir = 1;
x -= 1;
}
void node::down() {
dir = 2;
x += 1;
}
void node::left() {
dir = 3;
y -= 1;
}
void node::right() {
dir = 4;
y += 1;
}
struct Edge { //链式向前星表示图
int v, next,w;
};
void addedge(int u, int v, int w);
void add(int u, int v);
void addedge(int u, int v, int w) {
edge[++cnt].v = v, edge[cnt].w = w, edge[cnt].next = head[u], head[u] = cnt;
}
void add(int u, int v) {
edgem[++cntm].v = v, edgem[cntm].next = headm[u], headm[u] = cntm;
}
struct tnode { //prim结点,此节点表示点b到点c的距离为a
int a, b, c;
bool operator<(const tnode& rhs) const { //重载运算符
return a > rhs.a;
}
};
二、迷宫生成
我门生成迷宫的大致思路是,从(1,1)地图块开始间隔产生通路结点,然后将相邻的结点之间建边边权随机,这样我们用prim生成最小生成树的时候就是随机的了,然后我们用完prim是逻辑上建立了最小生成树,实际上并没有,我们还要深搜遍历一遍最小生成树将地图块的flag改为2。
地图生成函数
void CmazeDlg::productmaze() {
mcnt = 0;
for (int i = 0; i < 41; ++i) { //为了懒省事直接暴力初始化地图块的每个参数了
for (int j = 0; j < 41; ++j) {
maze[i][j].rec.left = 0 + j * 20;
maze[i][j].rec.right = 20 + j * 20;
maze[i][j].rec.top = 0 + i * 20;
maze[i][j].rec.bottom = 20 + i * 20;
maze[i][j].flag = 1;
maze[i][j].id = -1;
maze[i][j].x = i, maze[i][j].y = j;
maze[i][j].f = 0;
maze[i][j].g = 0;
maze[i][j].h = 0;
maze[i][j].ida = 0;
maze[i][j].in_close = 0;
maze[i][j].in_open = 0;
maze[i][j].fa.first = 0;
maze[i][j].fa.second = 0;
}
}
for (int i = 1; i < 41; i += 2) { //间隔生成通路节点
for (int j = 1; j < 41; j += 2) {
maze[i][j].id = ++mcnt;
maze[i][j].flag = 2;
ma[mcnt] = maze[i][j];
ma[mcnt].x = maze[i][j].x;
ma[mcnt].y = maze[i][j].y;
}
}
srand((unsigned)time(NULL)); //设置随机数种子
cnt = cntm = 0; //初始化通路节点的连通图的相关参数
memset(head, 0, sizeof head);
memset(vis, 0, sizeof vis);
memset(headm, 0, sizeof headm);
int tdis = 0;
for (int i = 1; i < 41; i += 2) { //相邻的通路结点建立随机权值的边
for (int j = 1; j < 39; j += 2) {
tdis = rand() % 100 + 1;
addedge(maze[i][j].id, maze[i][j + 2].id, tdis);
addedge(maze[i][j + 2].id, maze[i][j].id, tdis);
}
}
for (int i = 1; i < 41; i += 2) {
for (int j = 1; j < 39; j += 2) {
tdis = rand() % 100 + 1;
addedge(maze[j][i].id, maze[j + 2][i].id, tdis);
addedge(maze[j + 2][i].id, maze[j][i].id, tdis);
}
}
prim(); //开始在连通图上生成最小生成树
dfs(1); //最小生成树逻辑逻辑上是建好了,但是现实的话还是孤立的
//深搜遍历最小生成树,把两个树节点(即通路结点)间的障碍结点变为通路节点。
//paintnow();
}
此时我们建立了连通图,如下图。我们用细黄线表示建边了,没画完。但它实际上是孤立的通路节点(黄色)。
我们建立完连通图后,就要在连通图上建立最小生成树了
prim函数
void CmazeDlg::prim() { //这里使用了优先队列优化
for (int i = 1; i <= mcnt; ++i) { //左右点首先设为B类
dis[i] = inf;
}
dis[1] = 0; //一开始将点1设为距离A类点集距离最小的B类点。
int tcnt = 0, pre=0, now=0;
tnode tmp;
q.push({ 0,0,1});
while (++tcnt <= mcnt) {
while (!q.empty()) {
tmp = q.top(); //找到距离A类点集最小的点
q.pop();
pre = tmp.b, now = tmp.c;
if (!vis[now]) break; //这个点必须是B类点,也就是之前没访问过
}
vis[now] = 1; //现在他是A类点了
add(pre, now); //与他前导结点相连,一个点的前导结点就是,将该节点到A类点集的距离更新为最小的点。
for (int i = head[now], v; i; i = edge[i].next) { //更新与新A类结点相连的点。
v = edge[i].v;
if (!vis[v] && dis[v] > edge[i].w) { //如果距离小于之前的距离切实B类点,就更新入队。
dis[v] = edge[i].w;
q.push({ dis[v],now,v });
}
}
}
}
我们调用完最小生成树后,只是建立的逻辑上的最小生成树。他的显示效果还是如下图所示
但是逻辑上他是这样的,如下图。黄线代表连接的最小生成树。没画完整,简单演示一下。
现在我们就深搜遍历生成树,然后将连边上的障碍结点改为通路节点
void CmazeDlg::dfs(int id) {
for (int i = headm[id],v; i; i = edgem[i].next) {
v = edgem[i].v;
int mi1 = min(ma[id].x, ma[v].x), mi2 = max(ma[id].x, ma[v].x), ma1 = min(ma[id].y, ma[v].y), ma2 = max(ma[id].y, ma[v].y);
for (int i = mi1; i <= mi2; ++i) {
for (int j = ma1; j <= ma2; ++j) {
maze[i][j].flag = 2;
}
}
dfs(v);
}
}
这样我们就生成了一个迷宫
三、A*自动寻路
我们在建立好的迷宫即一个最小生成树上用A*搜索出起点到终点的通路。
priority_queue<node> open_queue;
void CmazeDlg::astar() {
node tmp;
int tx, ty, ttx, tty;
open_queue.push(maze[1][1]);
maze[1][1].f = maze[1][1].g = maze[1][1].h = 0; //起点初始化,本迷宫固定了起点和终点,但是其实可以自己随机设置的,因为并没有什么实现的难度,所以大家可以自己实现一下。
maze[1][1].in_open = 1;
while (!maze[39][39].in_open) { //当终点不在open队列时,说明终点还不可达,所以我们继续搜索。
tmp = open_queue.top(); //读取当前open队列的f值最小的点
tx = tmp.x, ty = tmp.y;
maze[tx][ty].in_close = 1; //将它取出方法close队列里
open_queue.pop();
for (int i = 1; i <= 4; ++i) {
ttx = tx + dr[i][0], tty = ty + dr[i][1]; //遍历周围四个结点
if (maze[ttx][tty].flag != 1 && !maze[ttx][tty].in_close) { //如果他不是障碍,且他不在close队列里
if (!maze[ttx][tty].in_open || maze[ttx][tty].g > tmp.g + 1) { //四周的点如果不在open队列里,或者新的g值更小
maze[ttx][tty].update(maze[tx][ty]); //我们就更新它
}
}
}
}
while (!open_queue.empty()) { //清空open队列为下次做准备
open_queue.pop();
}
node nodep = maze[39][39]; //我们从终点开始找父节点,
while (!(nodep.x == 1 && nodep.y == 1)) { //直到找到了起点
tx = nodep.fa.first,ty=nodep.fa.second;
maze[tx][ty].son.first = nodep.x; //在找的时候我们更新该节点的父节点的子节点(这个子节点也就是该点)
maze[tx][ty].son.second = nodep.y;
maze[tx][ty].flag = 0; //flag设为0方便绘图
nodep = maze[tx][ty]; //向上遍历
}
paintnow();
}
我们通过设定定时器可以让小人自动走
void CmazeDlg::OnBnClickedauto()
{
// TODO: 在此添加控件通知处理程序代码
astar();
SetTimer(1, 150, NULL);
}
void CmazeDlg::OnTimer(UINT_PTR nIDEvent)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
if ((mplayer.x != 39 || mplayer.x != 39)) {
int sx = maze[mplayer.x][mplayer.y].son.first; //下一跳结点坐标
int sy = maze[mplayer.x][mplayer.y].son.second; //
mplayer.x = sx, mplayer.y = sy;
paintnow();
}
CDialogEx::OnTimer(nIDEvent);
}
四、演示
手动操作
自动操作
五、总结
这一次通过学习迷宫,更新了我对prim算法的理解,对他的应用有了新的认知,我还学会了以前听说过的A*,对于定时器的运用也更加熟练。