關於用棧和佇列分別解決走迷宮問題的方法討論(參與者:陳卓,毛敏磊)

取名字比写博客还难發表於2024-04-01

關於用棧和佇列分別解決走迷宮問題

對於生活中最常見的小遊戲——走迷宮,相信大家都不陌生,人為走相信大家都會走,但能不能用程式碼實現,我們認為是可以的,以下是我們對如何走迷宮的一些看法和程式碼實現(cz負責佇列解決,mml負責用棧解決)

1.關於用佇列解決:

先簡單介紹一下佇列:佇列是一種操作受限的線性表,只允許在表的一端進行插入,在表的另一端進行刪除。可進行插入的一段稱為隊尾,可進行刪除的一端稱為隊頭。佇列的主要特點就是先進先出。依照儲存結構可
分為順序隊和鏈式隊。
解決思路:對於一個迷宮,我們將起點設定為(1,1),終點設定為(M,N),通路為0,不通路為1。對於當前位置我們知道可以往上,下,左,右四個方向行走,我們預設按照上,右,下,左的順序依次查詢可走的方向,將可以走的方格存入佇列中,當四個方向查詢完畢後,彈出隊首元素從而開始對存入佇列中的下一個元素進行可通路查詢,當目前的隊首元素恰好等於終點座標時,即為找到終點(M,N)。如果佇列已空且還沒有找到終點,則該迷宮沒有終點。那麼到此已經解決了如何找到迷宮出路的大體方法,既然找到,又該如何輸出呢?對此,我們認為可以在儲存方格座標的資料型別中加一個用來儲存上一方格在佇列中的位置下標pre(預設起點下標為-1),那麼輸出路徑時就可以透過尋找上一方格的位置下標輸出相應佇列元素。例如:起點(1,1)的右方向假如是通路,那麼存入佇列中的元素為(1,2)且該元素pre=0.為什麼可以採用這種方法?相信會有人問佇列元素不是被彈出了嗎?怎麼輸出佇列中元素?其實對於這種問題佇列中的順序隊儲存方式就能很好解決:在對順序隊進行操作時彈出元素只是將頭位置加1,而並非真正意義上的彈出,在這種情況下就能很好的保留之前所存入通路座標,進而解決迷宮問題,如下圖(圖片引用自PPT——棧與佇列):

以下是程式碼的具體實現:

#include<iostream>
#define Maxsize 10000   //定義一個儲存資料最大區間
using namespace std;

typedef struct
{
	int x;		//橫座標
	int y;		//縱座標
	int pre;
}Box2;`	//定義一個資料型別儲存位置資訊

typedef struct
{
	Box2 data[Maxsize];		//佇列儲存的資料型別
	int front;					//佇列頭
	int rear;					//佇列尾
}Queue;		//定義一個佇列,該佇列為順序佇列

int mg[1002][1002];				//定義一個存放迷宮的二維陣列

void CreateQueue(Queue*& q)
{
	q = new Queue;
	q->front = q->rear = -1;
}		//為新佇列申請儲存空間

void EnQueue(Queue*& q, Box2 x)
{
	if (q->rear == Maxsize - 1)
	{
		cout << "儲存失敗!" << endl;
		return;
	}			//判斷佇列是否已滿

	q->rear++;
	q->data[q->rear] = x;

	return;
}		//將新的可通方格存入佇列中

void OutQueue(Queue*& q, Box2& x)
{
	if (q->front == q->rear)
	{
		cout << "讀取失敗!" << endl;
		return;
	}		//判斷佇列是否為空

	q->front++;
	x = q->data[q->front];

	return;
}

int QueueEmpty(Queue* q)
{
	return (q->front == q->rear);
}		//判斷佇列是否為空,若為空則返回1,否則返回0

void Cout(Queue* q, int front)
{
	int x = front;
	int i;

	do {
		i = x;
		x = q->data[x].pre;
		q->data[i].pre = -1;
	} while (x != 0);		//透過迴圈分別將從終點開始的前一位置記為-1

	x = 0;

	while (x < Maxsize)
	{
		if (q->data[x].pre == -1)
		{
			cout << "( " << q->data[x].x << " , " << q->data[x].y << " )" << endl;
		}
		x++;
	}		//遍歷佇列,將佇列中資料為-1的座標輸出
}

void SearchPath(int x1, int y1, int x2, int y2)
{
	Box2 e;
	Queue *q;
	int i, j;

	CreateQueue(q);

	e.x = x1;
	e.y = y1;
	e.pre = -1;			//由於起點無前一位置,因此將起點前一位置定為-1
	EnQueue(q, e);		//將迷宮起點存入佇列中

	mg[x1][y1] = 1;     //將起點值改為1,避免重複進入

	while (!QueueEmpty(q))
	{
		OutQueue(q, e);		//判斷佇列中下一方格的通路情況

		i = e.x;
		j = e.y;

		if (i == x2 && j == y2)
		{

			Cout(q, q->front);
			delete q;

			return;
		}		//判斷該方格是否為終點

		for (int m = 0; m < 4; m++)
		{
			int x, y;
			switch (m)
			{
			case 0:
				x = i - 1;
				y = j;
				break;
			case 1:
				x = i;
				y = j + 1;
				break;
			case 2:
				x = i + 1;
				y = j;
				break;
			case 3:
				x = i;
				y = j - 1;
				break;
			}

			if (mg[x][y] == 0)
			{
				e.x = x;
				e.y = y;
				e.pre = q->front;		//將可通方格的前一位置記為當前方格的存入佇列中順序
				EnQueue(q, e);
				mg[x][y] = 1;
			}		//將當前方格四個方向所有可通路存入佇列中
		}
	}

	putchar('\n');
	cout << "Not Found";		//迷宮查詢結束,未能找到終點
	delete q;
}

int main()
{
	int M, N, i, j;
	cin >> M >> N;		//輸入迷宮的行,列

	for (i = 0; i < M + 2; i++)
	{
		mg[i][0] = 1;
		mg[i][M + 1] = 1;
	}
	for (i = 0; i < N + 2; i++)
	{
		mg[0][i] = 1;
		mg[M + 1][i] = 1;
	}
	for (i = 1; i < M + 1; i++)
	{
		for (j = 1; j < N + 1; j++)
		{
			cin >> mg[i][j];
		}
	}		//將迷宮初始化,在外圍建立一堵牆

	SearchPath(1, 1, M, N);		//查詢迷宮起點到終點是否有通路

	return 0;
}

執行結果如下:

2.關於用棧解決:

對於迷宮問題用棧解決主要基於棧的特性Fist in Last(先進後出),可以很好的儲存走迷宮時的中間狀態——經過的路徑。根據先進後出的特點可以大概想到看先將走過的路徑存入棧內,當路走不通時將棧中的該路徑彈出。同時根據迷宮的特點我們想到用二維陣列來儲存我們的迷宮。那麼大概的思路便是遍歷迷宮中的路徑,將路徑存入棧內,當所在路徑沒有新的路可走時開始回退也就是將棧內的元素彈出直到棧頂元素可以找到新的路徑。
根據大概的思路先來定義會使用到的資料結構。

(1)對於迷宮中位置的儲存定義一個資料結構,基於我們是用二維陣列來儲存迷宮,那麼可以採用橫座標和縱座標來描述位置。同時遍歷路徑我們需要一個能反映當前方向的變數所以有以下定義:

typedef struct
{
   int x, y;
   int di;//按照東南西北的順序,di從1-4.
}Box;

(2)根據用橫縱座標來儲存位置資訊,可以使用橫座標和縱座標的增減來表示移動因此可以定義一個結構陣列來表示增減量所以有以下定義:

typedef struct
{
    int x, y;//用x,y的增量來表示移動
}Direction;
Direction direct[5]{ {0,0},{1,0},{0,-1},{-1,0},{0,1} };//設定按東南西北的順序來尋找出口

現在開始正式思考如何尋找迷宮的出口。首先找迷宮有兩大步驟:1)在沒路時能將棧內的元素彈出。2)能遍歷迷宮能走的位置。那麼我們可以設定雙層迴圈巢狀,將遍歷迷宮位置的步驟作為內層迴圈,彈出棧內元素作為外層迴圈。當遍歷迷宮位置無路可走時退出內層迴圈進入外層迴圈將元素彈出直到有新的路徑出現時。
同時對於走過的路徑也應進行標記使系統能識別該位置走過,那麼我們採用將走過的路的值賦為-1(用1表示迷宮的牆,0來表示可走的路)。
以下是程式碼的具體實現:

int findPath( int M,int N)
{
    Box temp;
    stack<Box> s;
    int x, y, di;//儲存當前地點的橫座標和縱座標
    int line, col;//儲存下一個地點的橫座標和縱座標
    Direction direct[5]{ {0,0},{1,0},{0,-1},{-1,0},{0,1} };//設定按東南西北的順序來尋找出口
    mg[1][1] = -1;//將記過的地方設定為-1
    temp={ 1,1,0 };
    s.push(temp);//為迴圈同一先將入口存入棧內
    while (!s.empty())
    {
        temp = s.top();
        s.pop();
        x = temp.x;
        y = temp.y;
        di = temp.di + 1;
        while (di <= 4)//方向未嘗試完
        {
            line = x + direct[di].x;//根據di的取值來進行相應方向的橫縱座標的增減
            col = y + direct[di].y;
            if (mg[line][col] == 0)
            {
                temp = { x,y,di };
                s.push(temp); //當有新路徑時將上個位置存入棧內
                x = line;//表示移動到下一個地點
                y = col;
                mg[line][col] = -1;
                if (x == M && y == N) {//迷宮有路
                    cout<< '(' << M << ',' << N << ')' << endl;
                    while (!s.empty())
                    {
                        cout << '(' << s.top().x << ',' << s.top().y << ')' << endl;
                        s.pop();
                    }
                    return 1;
                }
                else {
                    di = 1;
                }
            }
            else {
                di++;
            }

        }
    }
    cout<<"迷宮沒有出路";//返回0說明迷宮沒有路
    return 0;
}

執行結果如下

總結:對於用棧和佇列解決迷宮問題本質上是源於對棧和佇列優點的利用,FIFO和FILO,基於此優點還可以解決更多同類問題如:查詢檔案指定名,對撲克牌排序和蜘蛛紙牌等,感興趣的人也可去加以嘗試,此後也將分享有關這方面的程式碼和知識,感謝閱覽。

相關文章