演算法競賽——BFS廣度優先搜尋

時間最考驗人發表於2022-01-06

BFS

廣度優先搜尋:一層一層的搜尋(類似於樹的層次遍歷)

BFS基本框架

基本步驟:

  1. 初始狀態(起點)加到佇列裡

  2. while(佇列不為空)

    隊頭彈出

    擴充套件隊頭元素(鄰接節點入隊)

  3. 最後隊為空,結束

BFS難點所在(最短路問題):

  1. 儲存的資料結構:佇列

    狀態如何儲存到佇列裡邊(以什麼形式)?

  2. 狀態怎麼表示,怎麼轉移?

  3. dist

    如何記錄每一個狀態的距離

最短路問題:寬搜的優勢是能找到最短(最小)路!(所有邊權重都一樣才可以用!)——一層一層的搜尋(類似於樹的層次遍歷)。深搜可以保證我們走到終點,但不能確保是最短路。

搜尋過程(層次遍歷)如下:

(1)從圖中的某個頂點出V發,訪問V

(2)依次訪問V的各個未曾訪問過的鄰接點

(3)分別從這些鄰接點出發依次訪問它們的鄰接點,並使“先被訪問的頂點的鄰接點”先於“後被訪問的頂點的鄰接點”被訪問

(4)重複步驟(3)直至所有已被訪問的頂點的鄰接點都被訪問到

image

圖的BFS和樹幾乎一模一樣,唯一的區別是樹有根節點,而圖沒有,因此在遍歷圖時要選一個根節點。下圖以A作為根節點:

image

D和E是不能顛倒過來的,因為我們先遍歷到的頂點是B,下一次展開的時候必須找與B直接相連的節點,即必須在找與C相連的節點之前把所有與B相連的節點找出來,由於A和C都走過了,因此唯一能走的點就是D。因此B先走完!

image

BFS的資料結構實現形式是佇列,通過佇列儲存已被訪問過的節點,利用其先進先出的特點:保證了先訪問的頂點的鄰接點亦先被訪問

即佇列保證了下圖中B的鄰接點比C的鄰接點要先出現:

image

1.走迷宮(acwing 844)

給定一個 n×mn×m 的二維整數陣列,用來表示一個迷宮,陣列中只包含 00 或 11,其中 00 表示可以走的路,11 表示不可通過的牆壁。

最初,有一個人位於左上角 (1,1)(1,1) 處,已知該人每次可以向上、下、左、右任意一個方向移動一個位置。

請問,該人從左上角移動至右下角 (n,m)(n,m) 處,至少需要移動多少次。

資料保證 (1,1)(1,1) 處和 (n,m)(n,m) 處的數字為 00,且一定至少存在一條通路。

輸入格式

第一行包含兩個整數 nn 和 mm。

接下來 nn 行,每行包含 mm 個整數(00 或 11),表示完整的二維陣列迷宮。

輸出格式

輸出一個整數,表示從左上角移動至右下角的最少移動次數。

資料範圍

1≤n,m≤1001≤n,m≤100

輸入樣例:

5 5
0 1 0 0 0
0 1 0 1 0
0 0 0 0 0
0 1 1 1 0
0 0 0 1 0

輸出樣例:

8

【參考程式碼】

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>

using namespace std;

typedef pair<int, int>PII;//pair存放點的座標

const int N = 110;

int g[N][N];//存放地圖
int d[N][N];//存放點到起點的距離

// 四個方向!
int dx[4] = {-1, 0, 1, 0};
int dy[4] = {0, 1, 0, -1};

int n, m;

int bfs()
{
    queue<PII> q;//佇列儲存訪問過的點以及該頂點的鄰接點
    memset(d, -1, sizeof d);//初始化各個點要起點的距離為-1,表示該點沒有被訪問過的
    
    //1.起點入隊
    q.push({0, 0});
    d[0][0] = 0;// 起點到自己的距離為0
    
    //2.while(...)
    while(q.size())
    {
        //2.1 獲取隊頭元素 並彈出
        auto t = q.front();// 拿到隊頭元素
        q.pop();// 彈出隊頭元素
        
        //2.2 擴充套件隊頭元素(鄰接點入隊)
        for(int i = 0; i < 4; i ++)// 列舉所有鄰接點
        {
            int x = t.first + dx[i];
            int y = t.second + dy[i];
            if(x >= 0 && y >= 0 && x < n && y < m && d[x][y] == -1 && g[x][y] == 0)// 判斷是否滿足條件
            {
                //該鄰接點入隊,距離增加 
                q.push({x, y});
                d[x][y] = d[t.first][t.second] + 1;
            }
        }
    }
    
    return d[n - 1][m - 1];
    
}

int main()
{
    cin >> n >> m;
    //輸入地圖
    for (int i = 0; i < n; i ++ )
        for(int j = 0; j < m; j ++)
            cin >> g[i][j];
    
    cout << bfs();        
    
    return 0;
}

陣列模擬佇列:

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 110; 
typedef pair<int, int> PII;
int n, m;
int g[N][N];//存放地圖
int d[N][N];//存 每一個點到起點的距離
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};//x 方向的向量和 y 方向的向量組成的上、右、下、左
PII q[N * N];//手寫佇列
int bfs()
{
    int hh = 0, tt = 0;
    q[0] = {0, 0};

    memset(d, - 1, sizeof d);//距離初始化為- 1表示沒有走過

    d[0][0] = 0;//表示起點走過了

    while(hh <= tt)//佇列不空
    {
        PII t = q[hh ++ ];//取隊頭元素

        for(int i = 0; i < 4; i ++ )//列舉4個方向
        {
            int x = t.first + dx[i], y = t.second + dy[i];//x表示沿著此方向走會走到哪個點
            if(x >= 0 && x < n && y >= 0 && y < m && g[x][y] == 0 && d[x][y] == -1)//在邊界內 並且是空地可以走 且之前沒有走過
            {
                d[x][y] = d[t.first][t.second] + 1;//到起點的距離
                q[ ++ tt ] = {x, y};//新座標入隊
            }
        }
    }
    return d[n - 1][m - 1]; //輸出右下角點距起點的距離即可
}
int main() 
{
    cin >> n >> m;
    for(int i = 0; i < n; i ++ )
        for(int j = 0; j < m; j ++ )
            cin >> g[i][j];

    cout << bfs() << endl;

    return 0;
}

2.八數碼

在一個 3×33×3 的網格中,1∼81∼8 這 88 個數字和一個 x 恰好不重不漏地分佈在這 3×33×3 的網格中。

例如:

1 2 3
x 4 6
7 5 8

在遊戲過程中,可以把 x 與其上、下、左、右四個方向之一的數字交換(如果存在)。

我們的目的是通過交換,使得網格變為如下排列(稱為正確排列):

1 2 3
4 5 6
7 8 x

例如,示例中圖形就可以通過讓 x 先後與右、下、右三個方向的數字交換成功得到正確排列。

交換過程如下:

1 2 3   1 2 3   1 2 3   1 2 3
x 4 6   4 x 6   4 5 6   4 5 6
7 5 8   7 5 8   7 x 8   7 8 x

現在,給你一個初始網格,請你求出得到正確排列至少需要進行多少次交換。

輸入格式

輸入佔一行,將 3×33×3 的初始網格描繪出來。

例如,如果初始網格如下所示:

1 2 3 
x 4 6 
7 5 8 

則輸入為:1 2 3 x 4 6 7 5 8

輸出格式

輸出佔一行,包含一個整數,表示最少交換次數。

如果不存在解決方案,則輸出 −1−1。

輸入樣例:

2  3  4  1  5  x  7  6  8

輸出樣例

19

1、題目的目標

image

求最小步數 -> 用BFS

2、移動情況

image

移動方式:

轉以後:a = x + dx[i], b = y + dy[i].

思想:把每一個狀態看作圖論的一個節點(節點a到節點b的距離為1——狀態能轉移)——> 起點到終點最少需要多少步

從初始狀況移動到目標情況 —> 求最短路

3、問題

第一點:狀態如何儲存到佇列裡?

第二點:如何記錄每一個狀態的“距離”(即需要移動的次數)?

第三點:佇列怎麼定義(佇列儲存的是狀態,狀態可以用字串表示),dist陣列怎麼定義(每一個狀態到起始狀態的距離——兩個關鍵字:狀態(字串)、距離(int))——key-value?——雜湊表

4、解決方案

將 “3*3矩陣” 轉化為 “字串”

如:

image

所以:

佇列可以用 queue<string>
//直接存轉化後的字串
dist陣列用 unordered_map<string, int>
//將字串和數字聯絡在一起,字串表示狀態,數字表示距離

5、矩陣與字串的轉換方式

image

【參考程式碼】

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#include<unordered_map>

using namespace std;

//狀態轉移
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};


int bfs(string strat)
{
    //定義目標狀態、
    string end = "12345678x";
    //定義佇列和dist陣列
    queue<string> q;
    unordered_map<string, int> d;
    
    //初始化佇列和dist陣列
    q.push(strat);
    d[strat] = 0;
    
    while(q.size())
    {
        // 獲取隊頭元素,彈出佇列
        auto t = q.front();
        q.pop();
        //記錄當前狀態的距離,如果為最終狀態則返回距離結果
        int distance = d[t];
        if(t == end) return d[t];
        
        //查詢x在一位陣列中的下標,進行狀態轉換
        int k = t.find('x');
        int x = k / 3, y = k % 3;
        
        //擴充套件隊頭元素(鄰接節點入隊)
        for(int i = 0; i < 4; i ++)
        {
            //轉移後的x的座標(鄰接節點)
            int a = x + dx[i], b = y + dy[i];
            //沒有越界
            if(a >= 0 && b >= 0 && a < 3 && b < 3)
            {
                //轉移x
                swap(t[k], t[a * 3 + b]);
                //如果當前狀態是第一次遍歷,記錄距離,入隊
                if(!d.count(t))
                {
                    d[t] = distance + 1;
                    q.push(t);
                }
                //還原狀態,為下一種轉換情況做準備!
                swap(t[k], t[a * 3 + b]);
            }
        }
    }
    
    //無法轉換到目標狀態,返回-1
    return -1;
    
}

int main()
{
    
    string strat;
    // 輸入起始狀態
    for (int i = 0; i < 9; i ++ )
    {
        char c;
        cin >> c;
        strat += c;
    }
    
    cout << bfs(strat);
    
}

總結常用技巧:一維陣列與二維陣列座標的轉換

設[一維陣列]下標為index(從0開始),二維陣列長度為m * n,則:

一維陣列轉換為二維陣列

x = index / n 
y = index % n

二維陣列轉換為一維陣列

index = x + y * n

學習內容部分轉載自:

1.​ acwing演算法基礎課
2.作者:四谷夕雨
連結:https://www.acwing.com/solution/content/15149/

注:如果文章有任何錯誤或不足,請各位大佬盡情指出,評論留言留下您寶貴的建議!如果這篇文章對你有些許幫助,希望可愛親切的您點個贊推薦一手,非常感謝啦

相關文章