# 計算機軟體技術實習日誌專案三(二) 迷宮專案實現

傑克·奈皮爾發表於2020-12-28

計算機軟體技術實習日誌專案三(二) 迷宮專案實現



前言

大家好,我將為大家介紹我實現迷宮的具體細節,做軟體是我之前沒有接觸過的領域,做得不好,大家不要見怪。本文章介紹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*,對於定時器的運用也更加熟練。

相關文章