【dawn·資料結構】迷宮問題(C++)
簡要說明:
(1)題目來源:課程。
(2)由於作者水平限制和時間限制,程式碼本身可能仍有一些瑕疵,仍有改進的空間。也歡迎大家一起來討論。
——一個大二剛接觸《資料結構》課程的菜雞留
題目簡介
給定一個m×n(3≤ m,n <1001)迷宮,其中1表示牆壁,0表示通路。你可以使用一個二維陣列maze[m][n]來表示這個迷宮,其中起點和終點固定在maze[1][0]和maze[m-2][n-1]的位置。
你可以有8種前進方向,即分別是正北、正南、正西、正東、西北、西南、東北、東南(假設行座標自大到小為正北方向)。
要求寫出一個程式,能夠判斷給定迷宮是否存在一條從起點到終點的通路(即在路徑上的maze[i][j]=0)。
輸入樣例要求:
第一行輸入一個正整數m,表示迷宮陣列maze的行數。
第二行輸入一個正整數n,表示迷宮陣列maze的列數。
接下去m行分別輸入由0和1組成的字串,字串的長度應恰等於n,即表示maze陣列第i行。
參考樣例1:2
輸入:
5
7
1111111
0010001
1101111
1010000
1111111
輸出:
TRUE
參考樣例2:3
輸入:
5
7
1111111
0010001
1111111
1010000
1111111
輸出:
FALSE
思路分析
注:僅代表個人思路。
(1) 首先要確定maze[][]的存放形式。由於一個格子只可能有兩種情況,或是牆壁,或是路徑,因此可設為bool陣列。設定為int陣列也沒有任何問題。
(2) 確定尋找路徑的方法。由於在尋找路徑的過程中,有可能會沿著非牆壁格子走到“死衚衕”,因此此時需要有一個“回退”的過程。考慮到這麼一個過程實則是不斷試探和回退,因此命名為“回溯法”。每次回溯將重新訪問在到達當前格子前的一個格子,這樣的訪問方式與棧(stack)十分相似。因此,教材中也把這個問題放在了棧這一資料結構的相關例項中。
(3) 確定回溯法後又遇到一個問題,棧中我們需要儲存什麼資料。如果僅儲存位置,那麼在回退後的訪問的方向應該如何確定?如果是隨機決定,那麼能否得出正確的結果將是一個不確定的結果,這有悖於我們解決這一問題的初衷。因此棧中還需要存放在上一次試探中我們選擇的方向,回退後方向進行改變。如果當前格子已經試探過了所有方向,那麼就需要進一步回退。
(4) 但同樣還會引申出一個問題,如果迷宮中有環(圈)呢?如果迷宮設計得正好那麼巧妙,按照上面的試探方法,就極有可能無法走出這個圈——這就對應程式的“死迴圈”。因此在每次試探格子時,除了要與之前試探過的方向不同,我們還應該判斷這個格子我們在已走路徑中是否走到過。如果是,那麼就構成一個圈,我們就不應該試探這個方向。因此除了單單儲存迷宮格子是牆壁還是道路,還需要有這麼一個標記——就另外需要一個bool型的二維陣列,大小與maze一致,我們記作mark[m][n]。
(5) 來審視一下mark[m][n]的設計:如果在當前已走過的路徑中已經走過這個格子,就設mark[i][j]=true。在具體實現時我們需要這麼一個步驟:將mark[m][n]的所有元素初始化為false。在試探到格子maze[i][j]時,如果maze[i][j]=false,就不必擔心構成圈的問題;如果maze[i][j]=false,表明無需進行這一步試探,那麼需要更改方向。此外,由於是“已走路徑”的標記,由退出maze[i1,j1]回退到maze[i2,j2]時,需要更改mark[i1,j1]=false。
(6) 進一步考察mark[m][n]=true帶來的意義。設想這麼一個情景:在回退時不重設mark為false時,如果我們試探到maze[i][j],發現mark[i][j]=true,會得出什麼結論。首先,maze[i][j]一定在之前就被試探過,並且已經回退過。換句話說,由maze[i][j]為起點,之後的所有路徑是都無法到達終點的。因此,mark[i][j]的第二個含義就是會暗示這個格子是否可以到達終點。由此在實現mark[m][n]時,基於(5)的討論,我們並不需要在由maze[i1, j1]回退到maze[i2,j2]時重新設定mark[i1,j1]=false。mark[i1,j1]=true給我們提供的資訊,遠比是否會構成一個圈多得多——雖然這看上去有違我們給每個maze[i][j]一個標記的初衷。在補充部分中,會簡單討論這樣的改進會有怎樣的好處。
(7) 來回顧一下整個過程:從起點開始,從最初方向開始,每次從當前所在格子由上一次記錄的方向確定下一個試探的方向,判斷下一個格子是否可以到達(需要滿足兩個條件:maze[i][j]=true,mark[i][j]=false)。如果可以到達,那麼以新的格子為起點繼續試探。如果不可以,需要再次改變方向。當方向不能再次改變時,需要回退到到達當前格子前的最後一個格子,以那個格子為起點繼續試探。無法回退表示沒有路徑。如果下一個可到達的格子是終點,那麼就找到路徑。接下去的程式碼就是對這一個回顧的具體實現。
程式碼部分
#include <iostream>
#include <fstream>
#include <string>
#include <stack>
using namespace std;
struct node { //棧中需要儲存這樣的一個資料型別
node(int ix=0, int iy=0, int id=0):x(ix),y(iy),d(id) {}
int x;
int y;
int d;
};
struct move { //由方向體現具體的移動方式,為了簡便程式碼
move(int ia=0, int ib=0):a(ia),b(ib) {}
int a;
int b;
};
class Maze {
public:
//建構函式
Maze(int im, int in, string *is);
//判斷是否存在路徑
bool GetPath();
private:
int m;
int n;
bool *maze; //1表示牆壁,0表示通路
bool *mark; //1表示走過,0表示為走過
move mv[8];
bool& maze_node(int i, int j);
bool& mark_node(int i, int j);
};
//Maze類的建構函式
Maze::Maze(int im, int in, string *is) {
m=im;
n=in;
maze=new bool[m*n];
mark=new bool[m*n];
for (int i=0;i<m*n;i++) mark[i]=false;
//構造move陣列, 表示8個方向
mv[0]=move(-1,0); //向左
mv[1]=move(-1,1); //左上
mv[2]=move(0,1); //向上
mv[3]=move(1,1); //右上
mv[4]=move(1,0); //向右
mv[5]=move(1,-1); //右下
mv[6]=move(0,-1); //向下
mv[7]=move(-1,-1); //左下
//將string陣列儲存的資訊放入maze中.
for (int i=0;i<n;i++)
for (int j=0;j<m;j++) maze[j*n+i]=!(is[j][i]-'0');
}
//獲取行座標i, 列下標j的maze結點資訊
bool& Maze::maze_node(int i, int j) {
return maze[i*n+j];
}
//獲取行座標i, 列下標j的mark標記資訊
bool& Maze::mark_node(int i, int j) {
return mark[i*n+j];
}
//尋找路徑
bool Maze::GetPath() {
int c_m, c_n; //表示當前格子的下標
int e_m, e_n; //表示預期格子的下標
int dir; //表示方向
node current(1,0,-1); //請結合程式碼考慮為什麼第三個引數傳入-1?
stack<node> s;
if (!maze_node(1,0)||!maze_node(m-2,n-1)) { //起點與終點需符合規範
cerr<<"輸入不合規範."<<endl;
return false;
}
s.push(current);
mark_node(1,0)=true;
while (!s.empty()) {
s.pop(¤t); //current=s.top(); s.pop();
c_m=current.x;
c_n=current.y;
dir=current.d;
while (dir<7) {
e_m=current.x+mv[++dir].a;
e_n=current.y+mv[dir].b;
if (e_m==m-2&&e_n==n-1) return true; //到達終點
if (e_m>=0&&e_n>=0&&e_m<m&&e_n<n&&maze_node(e_m,e_n)&&!mark_node(e_m,e_n)) { //預期格子是路徑並且沒有走過(避免構成迴路)
mark_node(e_m,e_n)=true;
current.x=e_m;
current.y=e_n;
current.d=dir;
s.push(current);
c_m=e_m;
c_n=e_n;
dir=-1;
}
}
}
return false;
}
//將至多2個字元長的字串S解讀為對應的int值. 不合規範情況下將返回-1.
//假設輸入的m,n都滿足3≤m,n<100.
int STRtoINT(const string& S) {
switch (S.length()) {
case 0: return -1; //不允許輸入字串為空
case 1: return S[0];
case 2: return 10*(S[0]-'0')+S[1]-'0';
default: return -1;
}
}
//主函式
int main() {
int m, n;
string s, *sa;
ifstream ifs;
ifs.open("MazeCreate.txt");
getline(ifs,s);
m=STRtoINT(s);
getline(ifs,s);
n=STRtoINT(s);
if (m==-1||n==-1) { //以防輸入空string
cerr<<"輸入不合規範."<<endl;
return -1;
}
sa=new string[m];
for (int i=0;i<m;i++) getline(ifs,sa[i]);
Maze mz(m,n,sa);
if(mz.GetPath()) cout<<"TRUE";
else cout<<"FALSE";
}
補充部分
(1) 在GetPath()函式內需要注意幾點:首先,下一步探測的格子是否可能越界,以免帶來在陣列訪問等上的一些問題。再者,一些細節需要注意,如while(dir<7)中,為什麼是“<”而不是更符合認知的“≤”4。
(2) 程式碼本身出於精簡性的考慮,並沒有完全按照在思路分析(7)中討論到的那樣來安排。譬如,在方向的試探上使用的是while()迴圈,而沒有不斷地push()和pop()。5可以根據註腳內容重新編排一下迴圈體。
(3) 回到思路分析(6)的結束部分,我們來討論一下賦予mark[i][j]一個“有悖於初衷的、新的”含義的好處。如果不加以改進,我們可以設想到程式中會有基於maze[i][j]為起點進行無法到達終點的無用探測的重複,重複次數會取決於maze[i][j]與周圍聯通的程度,效率也會更低。因此,來考察一下改進後的演算法的時間複雜度。迷宮一共有m×n個格子,在最壞的情況下,每一個格子需要探查8個方向,即一共需要8mn次試探。而由於mark[][]的改進,事實上每一個格子只會被訪問到一次,因此時間複雜度為mn,這是一個不錯的改進。6
(4) 說點比較主觀的:這一篇是應室友提議寫出來的,於是乎也寫到挺晚的,不過個人覺得還是很有意義的。至少在寫程式碼時是我個人自己手把手地寫一遍,相當於實踐了一遍。雖然竊以為之前的數獨問題會比這個問題更難一些,但這也是在《資料結構》中第一次遇到回溯法的題目,加上思路分析中的(5)~(6)也確實可能是我在獨立地解決相關問題時會缺乏考慮的一點,因此也是一次很好的鞏固和學習了。
由於C++提供new分配空間,此處設定m和n過大可能會導致申請失敗,讀者可根據情況自行調整程式碼中的maze[][]的定義方式(即預先開設好固定大小的空間,然後由兩個int型資料管理意義上的大小)。 ↩︎
即存在有這麼一條通路:(x表示通路;b表示起點,e表示終點)
1 1 1 1 1 1 1
b x 1 0 0 0 1
1 1 x 1 1 1 1
1 0 1 x x x e
1 1 1 1 1 1 1 ↩︎不難驗證根據圖中的輸入我們無法找到一條通路。 ↩︎
這是由於dir的自增在while迴圈的迴圈體中實現。可以自行模擬程式的操作流程,以方便對這裡的迴圈條件的設定的理解。 ↩︎
相對應地,在訪問新的格子時,需要重新設定c_m, c_n與dir,而沒有采取更合乎討論過程中的步驟,即在到達新格子時將新格子壓入棧後,重新外層迴圈,並在外層迴圈最初讀取棧頂node的資料,然後繼續試探。 ↩︎
我個人還有一個想法。在探討到這個問題時,讓我聯想到了《資料結構》講到串的模式匹配時的最樸素的BF(Brute force)演算法時,用到的一個再合適不過的形容詞——健忘。我想在這個過程中,如果每次回退就重新設定mark[i][j],這似乎也是一種健忘的體現。不過我在此仍有一個小的疑問,在解數獨問題中,在每次回退時我都重新設定了一下格子的狀態(對應rePosition()函式),這個過程是否也“過於”健忘。目前我的想法是,格子對應的_position_[]確實有可能在回退的過程中需要更改。如果一成不變,可能會引起錯誤的結果。因此這樣的“健忘”是保證正確所需要的。 ↩︎
相關文章
- (C++)資料結構實驗二——迷宮問題C++資料結構
- 迷宮問題
- c++迷宮問題回溯法遞迴演算法C++遞迴演算法
- 回溯法求迷宮問題
- POJ3984-迷宮問題
- POJ3984 迷宮問題【BFS】
- 洛谷 p1605 迷宮問題 詳解
- 迷宮問題——最短程式碼,不到70行
- 解密迷宮問題:三種高效演算法Java實現,讓你輕鬆穿越未知迷宮解密演算法Java
- 使用A*演算法解迷宮最短路徑問題演算法
- 用python深度優先遍歷解迷宮問題Python
- 資料結構括號匹配問題資料結構
- 03.Java資料結構問題Java資料結構
- 資料結構——RMQ(ST表)問題資料結構MQ
- 【資料結構】停車場問題資料結構
- 回溯和遞迴實現迷宮問題(C語言)遞迴C語言
- C++實現迷宮的生成與解決C++
- c++基本資料結構C++資料結構
- 走迷宮
- 1744 迷宮
- 509迷宮
- [SDOI2012] 走迷宮 題解
- 【ybtoj】【BFS】【例題1】走迷宮
- C++資料結構和pb資料結構的轉換C++資料結構
- 我花了一夜用資料結構給女朋友寫個H5走迷宮遊戲資料結構H5遊戲
- C++資料結構-佇列C++資料結構佇列
- 資料結構之堆(c++)資料結構C++
- 3090 走迷宮
- 3089 探索迷宮
- dfs深度優先搜尋解決迷宮類問題(遍歷)
- 資料結構初階--堆排序+TOPK問題資料結構排序TopK
- 資料結構練習題(順序表和單連結串列)C++資料結構C++
- [20190930]關於資料結構設計問題.txt資料結構
- leetcode演算法資料結構題解---資料結構LeetCode演算法資料結構
- 簡單介紹Python迷宮生成和迷宮破解演算法Python演算法
- c/c++資料對齊問題C++
- 資料結構 - 單連結串列 C++ 實現資料結構C++
- C++資料結構連結串列的基本操作C++資料結構