題目描述:
迷宮是一個二維矩陣,其中1爲牆,0爲路,入口在第一列,出口在最後一行。要求從入口開始,從出口結束,按照 上,下,左,右 的順序來搜索路徑.。
思路:
回溯法 + 試探法。回溯法可用棧或遞歸,每次將走過的座標進行標記,防止再次回頭造成死循環。
準備工作:位置信息
struct Pos
{
int _row;
int _col;
Pos(const int& x, const int& y)
:_row(x)
,_col(y)
{}
};
bool CheckAccess(Pos pos)
{
if ( (pos._row < N && pos._row >= 0 ) && ( pos._col < N && pos._col >= 0 )
&& _maze[pos._row][pos._col] == 0)
{
return true;
}
return false;
}
一、遞歸寫法(同時創建一個棧保存了通路的座標)
思路:首先需要將整個問題劃分爲子問題。如果子問題能通,那該問題也能通。轉換爲代碼就是,先給定入口點,以該點爲中心,分別試探上下左右四個放下,一旦某個方向能通,則遞歸爲子問題,再以子問題爲中心,分別探測;再回溯的時候,加入子問題四個位置都不能通,則函數棧幀銷燬,自動回到該子問題的父問題。
bool GetPathR(Pos entry, stack<Pos>& path)
{
path.push(entry);
//標記該點,防止回頭
_maze[entry._row][entry._col] = 2;
if (entry._row == N - 1)
return true;
//分別探測上,下,左右
Pos next = entry;
next._row -= 1;
if (CheckAccess(next))
{
if (GetPathR(next, path))
return true;
}
next = entry;
next._row += 1;
if (CheckAccess(next))
{
if (GetPathR(next, path))
return true;
}
next = entry;
next._col -= 1;
if (CheckAccess(next))
{
if (GetPathR(next, path))
return true;
}
next = entry;
next._col += 1;
if (CheckAccess(next))
{
if (GetPathR(next, path))
return true;
}
//該位置走不通
path.pop();
return false;
}
二、用棧來改造遞歸的實現:
bool GetPathWithStack(Pos entry,stack<Pos>& path)
{
path.push(entry);
while (!path.empty())
{
Pos cur = path.top();
//標記該位置
_maze[cur._row][cur._col] = 2;
if (cur._row == N - 1)
return true;
//判斷上
Pos next = cur;
next._row -= 1;
if (CheckAccess(next))
{
path.push(next);
continue;
}
//判斷下
next = cur;
next._row += 1;
if (CheckAccess(next))
{
path.push(next);
continue;
}
//判斷左
next = cur;
next._col -= 1;
if (CheckAccess(next))
{
path.push(next);
continue;
}
//判斷右
next = cur;
next._col += 1;
if (CheckAccess(next))
{
path.push(next);
continue;
}
//上下左右都不可同,回溯pop掉該位置
path.pop();
}
return false;
}
對於例一、二的測試:
void Test1()
{
int mz[10][10] =
{
{ 1,1,1,1,1,1,1,1,1,1 },
{ 1,1,1,1,1,1,1,1,1,1 },
{ 0,0,0,1,1,1,1,1,1,1 },
{ 1,1,0,1,1,1,1,1,1,1 },
{ 1,1,0,0,0,0,0,0,1,1 },
{ 1,1,1,1,1,0,1,0,1,1 },
{ 1,1,1,1,1,0,1,0,1,1 },
{ 1,1,1,1,1,0,1,0,1,1 },
{ 1,1,1,1,1,0,1,0,1,1 },
{ 1,1,1,1,1,1,1,0,1,1 }
};
Maze<10> maze(mz);
stack<Pos> path;
cout << maze.GetPathR(Pos(2, 0), path) << endl;
maze.PrintMaze();
maze.PrintPath(path);
}
輸出截圖(遞歸寫法和用棧寫法結果相同):
三、多出口求出最短路徑(改造上面代碼)
思路:多使用一個用來存最短路徑的棧,當該條路徑已經到出口的時候,path中存放了此路徑上的座標,與此同時和用來存放最短路徑的shortPath的元素個數比,shortPath棧始終存放的是步數最短的路徑。
void GetShotPath(Pos entry, stack<Pos>& path, stack<Pos>& shortPath)
{
path.push(entry);
_maze[entry._row][entry._col] = 2;
if (entry._row == N - 1)
{
if (path.size() < shortPath.size() || shortPath.empty())
{
shortPath = path;
return;
}
}
Pos next = entry;
next._row -= 1;
if (CheckAccess(next))
GetShotPath(next,path,shortPath);
next = entry;
next._row += 1;
if (CheckAccess(next))
GetShotPath(next,path,shortPath);
next = entry;
next._col -= 1;
if (CheckAccess(next))
GetShotPath(next, path, shortPath);
next = entry;
next._col += 1;
if (CheckAccess(next))
GetShotPath(next, path, shortPath);
path.pop();
}
void Test2()
{
int mz[10][10] =
{
{ 1,1,1,1,1,1,1,1,1,1 },
{ 1,1,1,1,1,1,1,1,1,1 },
{ 0,0,0,0,0,0,1,1,1,1 },
{ 1,1,1,1,1,0,1,1,1,1 },
{ 1,1,0,0,0,0,0,0,1,1 },
{ 1,1,0,1,1,0,1,0,1,1 },
{ 1,1,0,1,1,0,1,0,1,1 },
{ 1,1,0,1,1,0,1,0,1,1 },
{ 1,1,0,1,1,0,1,0,1,1 },
{ 1,1,0,1,1,0,1,0,1,1 }
};
Maze<10> maze(mz);
stack<Pos> path;
stack<Pos> shortPath;
maze.GetShotPath(Pos(2,0), path, shortPath);
maze.PrintMaze();
maze.PrintPath(shortPath);
}
四、帶環迷宮
注:之前我們用的方法都是將走過的點標記爲2,但是對於帶環問題如果還是像之前一樣,就會出現錯誤。比如像下面的圖:本來應該是兩條路徑到出口點,但是如果按照之前的標記方式走的話,第二條路就會出現問題。下圖中按照上下左右的探測方式來走:首先先完成第一幅圖的內容;此時對[9,5]該進行回溯,一直回溯到[4,5],遞歸走右,一直遞歸到[2,3],到此圖二走完;再對[2,3]進行回溯,一直回到[2,2],本來最初[2,2]直接按照順序向下探測進行遞歸,右側還有一條屬於自己的路,但是此時由於之前[4,5]的向右遞歸,影響了自己的路徑。這時候如果兩條路徑長短不一致,恰好又要找到最短路徑,那麼就會出問題。
通過上面的描述,顯然之前的標記方法已經不使用於當前的情景,所以需要使用另外一種方法,不能造成因爲是別人走過我的路,我就不能走我自己的路的慘劇。這種方法就是:一開始可以用2來標記,以便區別開0和1,之後每次都將子問題的值標記爲當前位置的值加1,這樣兩條路就都會遍歷到,從而找到最短路徑。實現如下:
void CricleMaze(Pos entry,stack<Pos>& path, stack<Pos>& shortPath,int count)
{
_maze[entry._row][entry._col] = ++count;
path.push(entry);
if (entry._row == N - 1)
{
printf("出口點爲[%d,%d]\n", entry._row, entry._col);
if (path.size() < shortPath.size() || shortPath.empty())
{
shortPath = path;
return;
}
}
Pos next = entry;
next._row -= 1;
if (CheckAccess(entry,next))
CricleMaze(next,path,shortPath,count);
next = entry;
next._row += 1;
if (CheckAccess(entry, next))
CricleMaze(next, path, shortPath, count);
next = entry;
next._col -= 1;
if (CheckAccess(entry, next))
CricleMaze(next, path, shortPath, count);
next = entry;
next._col += 1;
if (CheckAccess(entry, next))
CricleMaze(next, path, shortPath, count);
path.pop();
}
bool CheckAccess(Pos cur, Pos next)
{
if ((next._row < N && next._row >= 0) && (next._col < N && next._col >= 0)
&& ( (_maze[next._row][next._col] == 0) || (_maze[cur._row][cur._col] < _maze[next._row][next._col])))
{
return true;
}
return false;
}
void Test3()
{
Maze<10> maze(mz);
stack<Pos> path;
stack<Pos> shortPath;
maze.CricleMaze(Pos(2, 0),path,shortPath,1);
maze.PrintMaze();
maze.PrintPath(shortPath);
}
輸出結果及分析:
由於給的圖中,第二次遍歷的路徑走到[4,7]的時候,發現左側要比自己要就停止了繼續遞歸下去。這是由於我們的判斷條件導致,只有當第二條路徑比第一條短的時候纔會接着遞歸,再更新最短路徑。所以在遇到問題是,儘量採用最後一種標記方法來實現,以免出現帶環路徑,