基於迴圈佇列的BFS的原理及實現

幾何思維發表於2021-01-27

文章首發於微信公眾號:幾何思維

1.故事起源

有一隻螞蟻出去尋找食物,無意中進入了一個迷宮。螞蟻只能向上、下、左、右4個方向走,迷宮中有牆和水的地方都無法通行。這時螞蟻犯難了,怎樣才能找出到食物的最短路徑呢?

基於迴圈佇列的BFS的原理及實現

2.思考

螞蟻在起點時,有4個選擇,可以向上、下、左、右某一個方向走1步。
如果螞蟻走過了一段距離,此時也依然只有4個選擇。
當然要排除之前走過的地方(不走回頭路,走了也只會更長)和無法通過的牆和水。

基於迴圈佇列的BFS的原理及實現

螞蟻想,還好我會影分身。如果每一步都分身成4個螞蟻,向4個方向各走1步,這樣最先找到食物的肯定就是最短的路徑了(因為每一步都把能走的地方都走完了,肯定找不出更短的路徑了)。

基於迴圈佇列的BFS的原理及實現

而且還能看出,第1步會到達所有到起點距離為1的地方,第2步也會到達所有距離為2的地方。
如此類推,第n步會覆蓋所有到起點最短距離為n的地方。

基於迴圈佇列的BFS的原理及實現

3.問題建模

把迷宮地圖放在二維陣列中,能通行的地方為0,牆和水的地方為負數。

基於迴圈佇列的BFS的原理及實現

每一步向4個方向走,可以通過當前座標\((x,y)\)加上一個方向向量。

基於迴圈佇列的BFS的原理及實現

這個其實就是寬度優先搜尋(BFS)的思想。

4.寬度優先搜尋(BFS)

又稱廣度優先搜尋,優先向四周擴充套件子節點,是最簡便的圖的搜尋演算法之一,一般通過佇列來實現。

4.1 佇列

是一種特殊的線性表,它只允許在表的前端進行刪除操作,而在表的後端進行插入操作,即先進先出。

基於迴圈佇列的BFS的原理及實現

佇列一般通過陣列實現,對該陣列增加一些操作上的限制。

基於迴圈佇列的BFS的原理及實現

但上面的實現有一些缺陷,當佇列滿時,也就是tail指標移動到隊尾,這時就無法再插入資料,但前面的元素已經出隊了,可能還有空缺的位置。

為了能高效利用空間,對該佇列增加一點改進,也就是迴圈佇列的產生。

4.2 迴圈佇列

把佇列想象成一個首尾相接的環形。

基於迴圈佇列的BFS的原理及實現

陣列實現,需要多預留一個空間。如果head=tail時,無法判斷是隊空還是隊滿,所以佔用一個空間,通過tail+1與head的關係來判斷是否隊滿。

基於迴圈佇列的BFS的原理及實現

4.3 佇列實現BFS

實現步驟如下:

  • 將起點加入佇列。
  • 從隊首取出一個節點,通過該節點向4個方向擴充套件子節點,並依次加入隊尾。
  • 重複以上步驟,直至隊空或已找到目標位置。

迴歸迷宮問題,到起點的距離為1,2,3...的點會依次入隊。

基於迴圈佇列的BFS的原理及實現

當head指標遍歷到距離為2的點時,向4周擴充套件距離為3的節點,並繼續入隊。

基於迴圈佇列的BFS的原理及實現

5.程式碼實現

5.1 變數定義

// 方向向量
const int direction[4][2] = {{0,  1},
                             {-1, 0},
                             {0,  -1},
                             {1,  0}};

const int MAXM = 100, MAXN = 100, QUEUE_LENGTH = 5;
// 佇列中的節點
struct Node {
    int x, y, distance;
    Node() {}
    Node(int xx, int yy, int d) : x(xx), y(yy), distance(d) {}
};

int n, m, step = 0, map[MAXM][MAXN], visit[MAXM][MAXN];
Node start, target;

5.2 BFS標準模板

void bfs() {
    Node queue[QUEUE_LENGTH];
    int head = 0, tail = 1;
    queue[0] = Node(start.x, start.y, 0);
    visit[start.x][start.y] = 0;

    while (head != tail) {
        int x = queue[head].x;
        int y = queue[head].y;
        int distance = queue[head].distance;
        head = (head + 1) % QUEUE_LENGTH;
        for (int i = 0; i < 4; ++i) {
            int dx = x + direction[i][0];
            int dy = y + direction[i][1];
            if (dx >= 0 && dx < m && dy >= 0 && dy < n && visit[dx][dy] == -1 && map[dx][dy] >= 0) {
                // 表示從i方向走過來的,方便後續回溯路徑
                visit[dx][dy] = i;
                if (dx == target.x && dy == target.y) {
                    cout << "已到目標點,最短距離為" << distance + 1 << endl;
                    step = distance + 1;
                    return;
                }
                if ((tail + 1) % QUEUE_LENGTH == head) {
                    cout << "佇列滿" << endl;
                    return;
                }
                // 新座標入隊
                queue[tail] = Node(dx, dy, distance + 1);
                tail = (tail + 1) % (QUEUE_LENGTH);
            }
        }
    }
}

5.3 路徑回溯

void printPath() {
    int x, y, d, path[MAXM][MAXN] = {0};
    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < n; ++j) {
            path[i][j] = -1;
        }
    }
    x = target.x;
    y = target.y;
    path[start.x][start.y] = 0;
    // 路徑回溯
    while (!(x == start.x && y == start.y)) {
        path[x][y] = step--;
        d = visit[x][y];
        x -= direction[d][0];
        y -= direction[d][1];
    }
    // 路徑列印
    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < n; ++j) {
            if (path[i][j] >= 0) {
                cout << path[i][j];
            } else {
                cout << "-";
            }
        }
        cout << endl;
    }
}

5.4 資料輸入

int main() {
    cin >> m >> n;
    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < n; ++j) {
            cin >> map[i][j];
            visit[i][j] = -1;
        }
    }
    cin >> start.x >> start.y >> target.x >> target.y;
    bfs();
    if (step > 0) printPath();
    return 0;
}

5.5 測試結果

輸入資料:
5 5
0 0 -1 0 0
0 0 0 0 -2
-1 0 -2 0 0
0 0 0 -1 0
0 -2 0 0 0
1 1 4 3

輸出:
已到目標點,最短距離為5

路徑列印:
-----
-0---
-1---
-23--
--45-

掃描下方二維碼關注公眾號,第一時間獲取更新資訊!

基於迴圈佇列的BFS的原理及實現

相關文章