四種迷宮生成算法的實現和可視化

Generation

(上圖是使用隨機化Prim算法生成一個200x200的迷宮的過程)

Github項目地址:maze

前言

本文中的迷宮指的是最常見的那種迷宮:迷宮整體輪廓是二維矩形,迷宮裏的格子是正方形的,格子上下左右各相鄰另外一個格子(邊和角除外),迷宮內部沒有環路,也沒有無法到達的格子,起點在一個角(本文中爲左上角),終點在另一個角(本文中爲右下角),從起點到終點有且僅有一條路徑:

Maze

每個格子都可以抽象成圖上的一個點,而相鄰且連通的格子間的路徑可以抽象成圖像的一個邊,不難發現這實際上是一棵樹。

Path

常見的迷宮生成算法有4種:深度優先算法、隨機化Kruskal算法、隨機化Prim算法和遞歸分割算法。對這4種算法再分類可以分爲2類:前3種歸爲一類,最後1種自成一類。爲什麼我在這裏將前3種歸爲一類呢?因爲前3種算法有着相似的流程:

  1. 將所有相鄰格子連接起來,形成一個無向圖G={V,E}
  2. 構造G的一個子圖G'={V',E'}G'要求是一顆樹,且V'=V

前3種算法的不同之處在於第2步構造G'時所使用的方法不同。

數據結構

前面我們已經看到了,迷宮可以抽象爲一顆樹,在此我們使用鄰接表來存儲樹,鄰接表類AdjacencyList定義如下(省略了實現和非關鍵代碼):

class AdjacencyList
{
public:
    AdjacencyList(int row = 0, int column = 0);

    void connect(int i, int j);
    void disconnect(int i, int j);

    int row() const { return m_row; }
    int column() const { return m_column; }

    std::vector<int> &neighbor(int i);
    std::vector<int> &surround(int i);
    
private:
    int m_row;
    int m_column;

    std::vector<std::vector<int>> m_index2neighbor;
    std::vector<std::vector<int>> m_index2surround;
};

迷宮中的格子抽象成的點按照從左向右,從上向下進行編號,row()column()用來獲得迷宮的大小,connect()用來連接兩個相鄰的點,disconnect()用來斷開兩個連接的點,neighbor()用來獲取與某個點相連接的點,surround()用來獲取與某個點相鄰(不一定連接在一起)的點。

深度優先算法

算法過程圖示:

DFS

使用深度優先算法構造得到的G'實際上就是G的深度優先搜索樹,與普通的深度優先算法不同的是,選擇下一個要染灰的點時需要加入一些隨機性,否則迷宮每次都會生成得一摸一樣。算法代碼爲:

AdjacencyList DeepFirstSearch::generate()
{
    enum Color
    {
        White,
        Gray,
        Black
    };

    AdjacencyList result(m_row, m_column);

    vector<int> color(static_cast<size_t>(m_row * m_column), White);
    vector<int> current;
    current.reserve(static_cast<size_t>(m_row * m_column));

    color[0] = Gray;
    current.push_back(0);

    while (current.size())
    {
        int last = current.back();
        random_shuffle(result.surround(last).begin(), result.surround(last).end());

        auto iter = result.surround(last).cbegin();

        for (; iter != result.surround(last).cend(); iter++)
        {
            if (color[static_cast<size_t>(*iter)] == White)
            {
                color[static_cast<size_t>(*iter)] = Gray;
                result.connect(last, *iter);
                current.push_back(*iter);
                break;
            }
        }

        // all adjacent points are found
        if (iter == result.surround(last).cend())
        {
            current.pop_back();
            color[static_cast<size_t>(last)] = Black;
        }
    }

    return result;
}

隨機化Kruskal算法

算法過程圖示:

Kruskal

Kruskal原本是構造最小生成樹的算法,但迷宮中的邊都沒有權重(或者說權重都是0),因此在把新邊加入樹中時隨機選擇一個就可以。在選擇邊時需要引入隨機性,否則每次都會得到相同的結果。算法代碼爲:

AdjacencyList Kruskal::generate()
{
    AdjacencyList result(m_row, m_column);

    UnionFind uf(m_row * m_column);

    vector<pair<int, int>> edges;
    for (int i = 0; i < m_row * m_column; i++)
    {
        for (auto iter : result.surround(i))
        {
            // avoid duplicate edge
            if (i > iter)
            {
                edges.push_back(pair<int, int>(i, iter));
            }
        }
    }
    random_shuffle(edges.begin(), edges.end());
    for (auto iter : edges)
    {
        if(!uf.connected(iter.first, iter.second))
        {
            uf.connect(iter.first, iter.second);
            result.connect(iter.first, iter.second);
        }
    }
    return result;
}

隨機化Prim算法

算法過程圖示:

Prim

Prim同樣是構造最小生成樹的算法,注意事項和Kruskal相同。算法代碼爲:

AdjacencyList Prim::generate()
{
    AdjacencyList result(m_row, m_column);

    vector<bool> linked(static_cast<size_t>(m_row * m_column), false);
    linked[0] = true;

    set<pair<int ,int>> paths;
    paths.insert(pair<int, int>(0, 1));
    paths.insert(pair<int, int>(0, m_column));

    static default_random_engine e(static_cast<unsigned>(time(nullptr)));

    while (!paths.empty())
    {
        // random select a path in paths
        int pos = static_cast<int>(e() % paths.size());
        auto iter = paths.begin();
        advance(iter, pos);

        // connect the two node of path
        result.connect(iter->first, iter->second);

        // get the node not in linked
        int current = 0;
        if (!linked[static_cast<size_t>(iter->first)])
        {
            current = iter->first;
        }
        else
        {
            current = iter->second;
        }

        // add the node to linked
        linked[static_cast<size_t>(current)] = true;

        // add all not accessed path to paths, and delete all invalid path from paths
        for (auto i : result.surround(current))
        {
            pair<int, int> currentPath = makeOrderedPair(i, current);
            if (!linked[static_cast<size_t>(i)])
            {
                paths.insert(currentPath);
            }
            else
            {
                paths.erase(paths.find(currentPath));
            }
        }
    }

    return result;
}

遞歸分割算法

算法過程圖示:

Recursive division

如果說前面3種算法是通過“拆牆”來構造迷宮,那麼遞歸分割算法就是通過“建牆”來構造迷宮了。在當前要處理的矩形中隨機選擇一個點,然後以這個點爲中心向上下左右4個方向各建一堵牆,其中3堵牆都要留門,不然就會出現無法到達的區域。對被這4堵牆劃分成的4個矩形遞歸執行這個過程,就能得到一個迷宮。算法代碼爲:

AdjacencyList RecursiveDivision::generate()
{
    AdjacencyList result(m_row, m_column);
    result.connectAllSurround();

    divide(result, 0, 0, m_column - 1, m_row - 1);
    return result;
}

void RecursiveDivision::divide(AdjacencyList &list, int left, int top, int right, int bottom)
{
    // the x range of input is [left, right]
    // the y range of input is [top, bottom]

    if ((right - left < 1) || (bottom - top < 1))
    {
        return;
    }

    static default_random_engine e(static_cast<unsigned>(time(nullptr)));
    int x = static_cast<int>(e() % static_cast<unsigned>(right - left)) + left;
    int y = static_cast<int>(e() % static_cast<unsigned>(bottom - top)) + top;

    vector<pair<int, int>> toDisconnect;

    for (int i = left; i <= right; i++)
    {
        int p = y * m_column + i;
        int q = (y + 1) * m_column + i;
        toDisconnect.emplace_back(p, q);
    }

    for (int i = top; i <= bottom; i++)
    {
        int p = i * m_column + x;
        int q = i * m_column + x + 1;
        toDisconnect.emplace_back(p, q);
    }

    // the position of no gap wall relative to (x, y), 0:top 1:bottom 2:left 3:right
    int position = e() % 4;

    int gapPos[4] = {0};

    // get the gap position
    gapPos[0] = static_cast<int>(e() % static_cast<unsigned>(y - top + 1)) + top;
    gapPos[1] = static_cast<int>(e() % static_cast<unsigned>(bottom - y)) + y + 1;
    gapPos[2] = static_cast<int>(e() % static_cast<unsigned>(x - left + 1)) + left;
    gapPos[3] = static_cast<int>(e() % static_cast<unsigned>(right - x)) + x + 1;

    for (int i = 0; i <= 3; i++)
    {
        if (position != i)
        {
            int p = 0;
            int q = 0;
            if (i <= 1) // the gap is in top or bottom
            {
                p = gapPos[i] * m_column + x;
                q = gapPos[i] * m_column + x + 1;
            }
            else // the gap is in left or right
            {
                p = y * m_column + gapPos[i];
                q = (y + 1) * m_column + gapPos[i];
            }
            pair<int, int> pair(p, q);
            toDisconnect.erase(find(toDisconnect.begin(), toDisconnect.end(), pair));
        }
    }

    for (auto &pair : toDisconnect)
    {
        list.disconnect(pair.first, pair.second);
    }

    divide(list, left, top, x, y);
    divide(list, x + 1, top, right, y);
    divide(list, left, y + 1, x, bottom);
    divide(list, x + 1, y + 1, right, bottom);
}

參考鏈接

Maze generation algorithm:https://en.wikipedia.org/wiki/Maze_generation_algorithm

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