數據結構-迷宮問題(回溯法)

題目描述:

迷宮是一個二維矩陣,其中1爲牆,0爲路,入口在第一列,出口在最後一行。要求從入口開始,從出口結束,按照 上,下,左,右 的順序來搜索路徑.。

思路:

回溯法 + 試探法。回溯法可用棧或遞歸,每次將走過的座標進行標記,防止再次回頭造成死循環。

準備工作:位置信息

struct Pos
{
    int _row;
    int _col;

    Pos(const int& x, const int& y)
        :_row(x)
        ,_col(y)
    {}
};

    //一、二、三通用
    bool CheckAccess(Pos pos)
    {
        //1、邊界條件
        //2、判斷是否能通
        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()
{
    //0爲可以走得路
    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.GetPathWithStack(Pos(2, 0), path) << endl;
    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)
        {
            //判斷size,若小於shortpath則該路徑爲新的最短路徑
            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)
    {
        //1.檢查邊界
        //2.是否是通路 0
        //3.比我大可以走,因爲是別人走過的我沒走過
        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]的時候,發現左側要比自己要就停止了繼續遞歸下去。這是由於我們的判斷條件導致,只有當第二條路徑比第一條短的時候纔會接着遞歸,再更新最短路徑。所以在遇到問題是,儘量採用最後一種標記方法來實現,以免出現帶環路徑,

這裏寫圖片描述


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章