1. 程式人生 > >簡單易懂的迷宮走法--棧應用(c++)

簡單易懂的迷宮走法--棧應用(c++)

哈嘍~小夥伴們,你們帥氣的阿俊又回來啦,他有好東西和你們分享哦,那就是困擾了他好久的迷宮問題。感覺自己走迷宮時簡單得很,可讓計算機走咋這麼難哩,我明白了,一定是因為計算機太笨了,真是笨死了,啥都要人家一步步教他咋做,還想取代人?路漫漫其修遠兮哦,嘿嘿嘿,那我們看看咋教他走迷宮吧!

如果學習過資料結構或圖論的小夥伴應該聽過深度/廣度優先搜尋演算法,這兩個演算法可是圖論中大多數演算法的基礎,非常重要哦。

圖可以用線代利器–鄰接矩陣儲存,而迷宮地圖也是可以用矩陣儲存喲,1表示牆,0表示通路。這樣迷宮求解問題就等價於指定深搜起點(迷宮入口),在深搜搜索過程中遇到指定終點(迷宮出口)就立即停止,因為它找到一條通路啦

深搜思想比較簡單,就一句話,當前點只要有鄰接點,就走下去;無鄰接點,往回退。重複判斷,直到遍歷完所有點。

這是典型的遞迴,不過遞迴雖然寫起來簡潔明瞭,但是卻不是太好寫,而且出錯時難以除錯,效率也不是太高,於是我們想到了用棧來實現深搜,彌補了遞迴的三個缺點,但可能程式碼量大一些,不過沒關係,黑貓,白貓,抓到老鼠就是好貓,那咱們看看如何用棧實現迷宮求解吧
先給你們看看區域性效果圖,饞哭你們,嘿嘿嘿
在這裡插入圖片描述


演算法描述

多說無益,咱們直接上圖吧
核心處理流程圖
在這裡插入圖片描述

-1表示牆;0表示通路;-2表示已走過;-3表示此路不通;

1,假設最初以出發點為當前點p
2,判斷p是否為出口,
2.1若p為出口,p入棧,並在地圖相應位置賦-2,表示已經過,然後結束;
2.2若p不為出口,判斷p是否有空白鄰接點,
2.2.1若有,p入棧,在地圖相應位置賦-2,同時p更新為下一個空白鄰接點;
2.2.2若無,在p對應的地圖位置上賦-3,同時彈出棧頂作為p
3,棧非空,重複步驟2

資料結構

棧的資料結構我就不貼了,想看可以翻我部落格,有具體實現,這裡就直接用STL的棧啦

typedef struct
{
	int x,y;
}Pos;//點x,y座標 
typedef struct
{
	int n;//迷宮大小
	Pos start,end;//起止座標 
	int maze[N][N]; //迷宮分佈圖。-1-->牆;0-->通路 
}Maze; 

1 建迷宮咯

打死我我也不用檔案讀取資料,嘿嘿嘿,真香

//從檔案讀取迷宮 
void CreateMaze(Maze &m)
{
	fstream inFile("迷宮.txt",ios:
:in); if(!inFile)cout<<"檔案開啟失敗!"<<endl; inFile>>m.n; inFile>>m.start.x>>m.start.y; inFile>>m.end.x>>m.end.y; for(int i = 0; i < m.n; i++) { for(int j = 0; j < m.n; j++) { inFile>>m.maze[i][j]; } } inFile.close(); /*測試是否讀取成功 cout<<m.n<<endl; cout<<m.start.x<<m.start.y<<endl; cout<<m.end.x<<m.end.y<<endl; for(int i = 0; i < m.n; i++) { for(int j = 0; j < m.n; j++) { cout<<m.maze[i][j]<<" "; } cout<<endl; }*/ }

檔案迷宮.txt內容

10
1 4
1 8
-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

2 查通路呀

看看流程圖是不是發現是否存在通路這個判斷位於邏輯中心,沒有他真滴啥都做不了了呢,於是我們專門為他建立了一個函式,以示尊敬,希望他保佑我別出Bug

  • 查詢通路按照我的習慣是東南西北順時針查詢,一找到立刻返回;但由於矩陣是x朝下,y朝右,所以大家寫的時候注意下x,y的處理;其實只要你囊括了四個方向,順序無所謂,最後都能達到目標,這裡只是個人編碼習慣問題
    我希望它做兩件事,一是有通路返回最先找到的鄰接點;二是無通路返回(-1,-1)表示無通路
//-2->已走過;-3->不通 
//判斷當前點是否存在通路,通,返回第一個可通的座標;不通,返回(-1,-1) 
Pos Pass(Maze m,Pos curpos)
{
	Pos nextpos;
	//初始化表示無通路 
	nextpos.x = -1;
	nextpos.y = -1;
	
	int x,y;
	x = curpos.x;
	y = curpos.y;
	//每次都按順序查詢,走過的路被標記為 -2,所以不會再走 
	//按照我們人的東西南北為參照,二維陣列x朝下,y朝右,所以應注意方向 
	if(m.maze[x][y+1] == 0)//東 
	{
		nextpos.x = x;
		nextpos.y = y+1;
	}
	else if(m.maze[x+1][y] == 0)//南 
	{
		nextpos.x = x+1;
		nextpos.y = y;
	}
	else if(m.maze[x][y-1] == 0)//西 
	{
		nextpos.x = x;
		nextpos.y = y-1;
	}else if(m.maze[x-1][y] == 0)//北 
	{
		nextpos.x = x-1;
		nextpos.y = y;
	}
	return nextpos; 
}

3 找活路啦

看著流程圖敲一敲,機智的你肯定沒問題啦

程式碼後半部分僅僅是為列印時的美觀,不寫也行,不過人家看臉啦,還是比較注重形式的呀,看著可舒服啦。處理也很簡單,再開一個棧(反正是STL他們家的,又不要錢)把從路徑棧中彈出的點壓入新棧,這樣順序就負負得正啦。然後把編號賦給彈出的棧頂在地圖中對應的位置。然後輸出地圖時令值大於0的輸出相應數值,其餘皆輸出#。這也是我為啥讓-2,-3表示走過的路,不同的路,因為他們大於0,阻擋了我最求美的道路,於是無情地將他們打入負數的冷宮

void MazePath(Maze m)
{
	Pos curpos,nextpos;//當前位置,下個位置 
	stack<Pos> path_stack;//路徑棧 
	
	curpos = m.start;
	do{
		nextpos = Pass(m,curpos); 
	//超級大bug,當終點為當前點且其四周都被沒空白時,當前的的下一個點自然返回空,表示不可通,也就找不到終點了
	//所以應該先判斷當前點是否為終點,是的話立刻退出,否則判斷是否存在通路,存在當前點就入棧;否則將當前置為-3,彈出棧頂作為當前點 
		if(curpos.x == m.end.x && curpos.y == m.end.y)//當前點為終點
		{
			path_stack.push(curpos);//當前點入路徑棧
			m.maze[curpos.x][curpos.y] = -2;//走過標記為-2 
			break;
		}
		if(nextpos.x != -1)//表示可通 
		{

			path_stack.push(curpos);//當前點入路徑棧
			m.maze[curpos.x][curpos.y] = -2;//更新迷宮地圖,表示該點已經過
			curpos = nextpos;//當前點更新為下一個點 
		}
		else
		{
			m.maze[curpos.x][curpos.y] = -3;//表示當前點不通
			curpos = path_stack.top();//當前點更新為棧頂元素 
			path_stack.pop();//刪除棧頂 
		} 
	}while(!path_stack.empty());
	
	//=================以下僅僅是為了展示效果(人家看臉嘛)======================= 
	if(path_stack.empty())cout<<"死路一條!"<<endl;
 	else
 	{
 		stack<Pos>tmp_stack;
		 int ord = 0; 
 		//被標記為1的不全是路徑上的點,但在棧中一定是路徑上的點 
 		while(!path_stack.empty())
 		{
 			curpos = path_stack.top();
 			path_stack.pop();
 			tmp_stack.push(curpos);	
		}
		
		while(!tmp_stack.empty())
 		{
 			ord++;
 			curpos = tmp_stack.top();
 			tmp_stack.pop();
 			m.maze[curpos.x][curpos.y] = ord;
		}
 		//列印輸出
 		cout<<endl;
		for(int i = 0; i < m.n; i++)
		{
			for(int j = 0; j < m.n; j++)
			{
				if(m.maze[i][j] > 0)
				{
					cout<<setw(2)<<m.maze[i][j]<<" ";
				}
				else cout<<setw(2)<<" # ";
				
			}
			cout<<endl;
		}
	}
	
} 

說說心裡話

  • 到這裡我的分享快到尾聲啦,我知道自己講得很糟糕,但是我會努力的,總有一天我也會變得像你一樣強

  • 主體邏輯一定要清晰,這次一開始將當前點是否存在通路作為第一個判斷條件,在某些點作為出口時是得不出答案的,因為存在一種情況,當前點確實為終點,但是他舉目四望,發現走投無路啦,誰知,路就在他腳下,可他沒機會低頭就被丟向無路可走的深淵,只要他肯低頭,立馬就踏破鐵鞋無覓處,這告訴我們要腳踏實地,過好當下呀。好在我知錯就改,立刻將是否為終點作為第一判斷條件,避免了慘劇發生,不過還是付出些許代價的

  • 今天收穫不小,關鍵是賊有成就感,對棧的理解有深了一步,有句話說得好,紙上得來終覺淺,覺知此事要躬行吶。

  • 書看再多,不實踐,難以內化;實踐再多,不總結,難以深化;總結再多,不寫下來,難以昇華;寫得再多,你不點贊,難以開心


好啦,好啦,別依依不捨啦(我知道不會有人不捨),咳咳,我要回去畫電路圖啦,小夥伴們,回見,不要太想我哦


完整Code

#include<iostream>
using namespace std;
#include<stdlib.h>
#include<stack>
#include<fstream>
#include<iomanip> 
#define N 20
typedef struct
{
	int x,y;
}Pos;//x,y座標 
typedef struct
{
	int n;//迷宮大小
	Pos start,end;//起止座標 
	int maze[N][N]; //迷宮分佈圖。-1-->牆;0-->通路 
}Maze; 
//從檔案讀取迷宮 
void CreateMaze(Maze &m)
{
	fstream inFile("迷宮.txt",ios::in);
	if(!inFile)cout<<"檔案開啟失敗!"<<endl;
	
	inFile>>m.n;
	inFile>>m.start.x>>m.start.y;
	inFile>>m.end.x>>m.end.y;
	
	for(int i = 0; i < m.n; i++)
	{
		for(int j = 0; j < m.n; j++)
		{
			inFile>>m.maze[i][j];
		}
	} 
	inFile.close();
	cout<<m.n<<endl;
	cout<<m.start.x<<m.start.y<<endl;
	cout<<m.end.x<<m.end.y<<endl;
	
	for(int i = 0; i < m.n; i++)
	{
		for(int j = 0; j < m.n; j++)
		{
			cout<<m.maze[i][j]<<" ";
		}
		cout<<endl;
	}
}
//-2->已走過;-3->不通 
//判斷當前點是否存在通路,通,返回第一個可通的座標;不通,返回(-1,-1) 
Pos Pass(Maze m,Pos curpos)
{
	Pos nextpos;
	//初始化表示無通路 
	nextpos.x = -1;
	nextpos.y = -1;
	
	int x,y;
	x = curpos.x;
	y = curpos.y;
	//每次都按順序查詢,走過的路被標記為 1,所以不會再走 
	//按照我們人的東西南北為參照,二維陣列x朝下,y朝右,所以應注意方向 
	if(m.maze[x][y+1] == 0)//東 
	{
		nextpos.x = x;
		nextpos.y = y+1;
	}
	else if(m.maze[x+1][y] == 0)//南 
	{
		nextpos.x = x+1;
		nextpos.y = y;
	}
	else if(m.maze[x][y-1] == 0)//西 
	{
		nextpos.x = x;
		nextpos.y = y-1;
	}else if(m.maze[x-1][y] == 0)//北 
	{
		nextpos.x = x-1;
		nextpos.y = y;
	}
	return nextpos; 
}
void MazePath(Maze m)
{
	Pos curpos,nextpos;//當前位置,下個位置 
	stack<Pos> path_stack;//路徑棧 
	
	curpos = m.start;//cout<<m.end.x<<m.end.y;
	do{//cout<<"curpos: "<<curpos.x<<" "<<curpos.y<<endl;
		nextpos = Pass(m,curpos); //cout<<"nextpos: "<<nextpos.x<<" "<<nextpos.y<<endl;
	
	//超級大bug,當終點為當前點且其四周都被沒空白時,當前的的下一個點自然返回空,表示不可通,也就找不到終點了
	//所以應該先判斷當前點是否為終點,是的話立刻退出,否則判斷是否存在通路,存在當前點就入棧;否則將當前置為-3,彈出棧頂作為當前點 
		if(curpos.x == m.end.x && curpos.y == m.end.y)//當前點為終點
		{
			path_stack.push(curpos);//當前點入路徑棧
			m.maze[curpos.x][curpos.y] = -2;//走過標記為-2 
			break;
		}
		if(nextpos.x != -1)//表示可通 
		{

			path_stack.push(curpos);//當前點入路徑棧
			m.maze[curpos.x][curpos.y] = -2;//更新迷宮地圖,表示該點已經過
			curpos = nextpos;//當前點更新為下一個點 
		}
		else
		{
			m.maze[curpos.x][curpos.y] = -3;//表示當前點不通
			curpos = path_stack.top();//當前點更新為棧頂元素 
			path_stack.pop();//刪除棧頂 
		} 
	}while(!path_stack.empty());
	
	//以下僅僅是為了展示而處理效果 
	if(path_stack.empty())cout<<"死路一條!"<<endl;
 	else
 	{
 		stack<Pos>tmp_stack;
		 int ord = 0; 
 		//被標記為1的不全是路徑上的點,但在棧中一定是路徑上的點 
 		while(!path_stack.empty())
 		{
 			curpos = path_stack.top();
 			path_stack.pop();
 			tmp_stack.push(curpos);	
 		//	m.maze[curpos.x][curpos.y] = 5;
		}
		
		while(!tmp_stack.empty())
 		{
 			ord++;
 			curpos = tmp_stack.top();
 			tmp_stack.pop();
 		//	tmp_stack.push(curpos);
 			m.maze[curpos.x][curpos.y] = ord;
		}
 		
 		cout<<endl;
		for(int i = 0; i < m.n; i++)
		{
			for(int j = 0; j < m.n; j++)
			{
				if(m.maze[i][j] > 0)
				{
					cout<<setw(2)<<m.maze[i][j]<<" ";
				}
				else cout<<setw(2)<<" # ";
				
			}
			cout<<endl;
		}
	}
} 
int main()
{
	Maze m;
	CreateMaze(m);
	for(int i = 0; i < 10; i++)//測試:以所有點為終點
	{
		for(int j = 0; j < 10; j++)
		{
			m.end.x = i;
			m.end.y = j;
			cout<<"i: "<<i<<" j: "<<j<<endl;
			MazePath(m);
		}
	}
//	MazePath(m);
	return 0;
}