---
title: 廣度優先搜尋(BFS)
description: 本文介紹了BFS的實做,並提供了一個範例題目和程式碼實作,以及一些引導思考和參考資料。
tags: BFS, 演算法, C++
type: article
lang: zh-tw
---
[Toc]
# 目錄
- 何謂BFS
- 範例題目
- 解決方法
- 程式落地
# 何謂BFS
BFS,全稱Breadth-first search,廣度優先搜尋,常用於搜尋最佳解(例如最短路徑),與DFS適合窮舉可能性不同。
特點為如水波般一圈圈擴散。優先搜尋最近的所有地點再慢慢擴大移動範圍。
# 範例題目
### 走迷宮問題
給一個 $n\times m$ 大小的迷宮,以「`#`」代表不可通行的障礙物,
「`.`」代表可通行的空間,「`S`」代表起點、「`E`」代表終點,
每花 $1$ 分鐘可走到「上、右、下、左」四個方向其中一個相鄰、且可通行的地方。
絕對不能走到地圖外面。
求從起點走到終點最少要花多少分鐘,如果不可能達成則輸出「$-1$」。
#### 範例輸入
```
5 6
S....#
##.##.
...#..
..#...
....#E
```
#### 範例輸出
```
13
```
#### 範例輸入
```
3 5
#...S
#.##.
.#.E#
```
#### 範例輸出
```
-1
```
# 概念
## Q1:目的:如何找到起終點間最小的路徑?
1. 一條路走到底,把所有的可能都走一次([[DFS]])
2. 找一個起點,慢慢往外擴散(BFS)
1. 這就像是水珠的漣漪一樣,只要測算漣漪靠岸的時間,就能得到離岸邊的距離
2. 或是把一團水丟進往四面八方的水管,這樣水會從起點慢慢蔓延到周圍
我們將地圖上每一個位置(無論是否可通行)稱為一個「點」。
因此,設起點為Sx,Sy,終點為Ex,Ey
另外更嚴謹的說法是:
我們假設某個位離起點dis遠,則我們需要從dis = 0 開始,先將dis < i 所有位置都找出來後,才開始找 dis = i 的位置。
所以我們能確定,第一個解若在第 i 步找到,則表示 dis < i 不存在任何解,故 dis = i 為最佳解。
## 特性
一圈圈擴散、不走相同點的特性同時也保證了每個點只須被考慮過一次,因此複雜度為O(nm),o為節點數,m為某個節點搜尋周圍所花的計算輛。
不過也因為如此,不像 DFS 會保留到達同個地點的各自不同走法,BFS 只在乎花了幾步走到這個地點,會得到一個純量,不在乎怎麼走到的,也不在乎不同走法的差異。
## 實作目標
==選定一個起點,使其向水波一圈一圈擴散出去,若是碰到終點則停止==
這句話包含了幾個目標:
1. 選定起點
2. 一圈一圈擴散
3. 碰到終點停止
# 程式碼實作
## 總說
三部分:
1. 起點
2. 搜尋周圍,同時記錄,作為下一個待搜尋的點
3. 轉移到下一個待搜尋的點。
## 如何向外擴散?
用跟中心的距離來分辨在第幾圈,確保每一圈都完全記錄本身距離與周圍,再前往下一圈。
一樣,我們從想法提取要實作的部分
1. 以距離來判斷圈數
2. 一圈結束再前往下一圈
3. 紀錄本身距離
4. 搜尋周圍
## 以距離判斷圈數
起點為0。在可以走的路徑填上距離起點的最小距離,起點周圍是1、再周圍是2......。
因此,我們定義一個struct(一個能包含很多資料的東西),讓struct能同時表達座標與距離起點距離。
```cpp
struct node{
int x,y,dis;//x座標, y座標, 與起點距離
};
```
## 如何保證一圈接著一圈而不錯亂?
就要保證一圈的所有點都被記錄再前往下一圈,並保證搜過的不再搜,使每一格的dis都是最小。
這是由於當後續展開相同時,從起點走到現在位置的步數越小,位還越好。這說明了只有當前最佳時會得到未來最佳。
而因為BFS一圈一圈的特性,初次抵達時的步數必定最小,因此第二次抵達必不為最佳,這與最佳解無關,則可忽略。
下面就要解決:
1. 搜過的不要再搜
2. 一圈所有點都被記錄才能往下一圈
## 搜過的不要再搜
建一個
```cpp
bool used[1024][1024];//判斷二維陣列上的某個點是否被紀錄過
```
並用[[memset]]
```cpp
memset(used,0,sizeof(used))
```
把used的每一格(used所佔的所有記憶體空間)都初始化為0
==起點也要初始化==
否則就會出現從起點往外找,發現起點本人沒used就往回走的情況。
```cpp
used[sx][sy] = true;
```
並且在丟進queue之前就令現在搜尋到的這格對應的used為true
```cpp
used[nxt.x][nxt.y] = true;
```
### 一圈所有點都被記錄才能往下一圈
可以用[[queue]]來解決。可以把他想成待辦清單,先發布的任務會先處理;同樣的,先儲存的點會先處理。(後進先出)
先令我現在所身在的點為cur(current),搜尋的點為nxt(next)。
```cpp
node cur, nxt;//定義現在點與搜尋點
```
接下來我們要把步驟分成紀錄與搜尋。紀錄是指更新當前節點(cur)的資訊;而搜尋,則是向周圍尋找沒找過的,並放入代辦清單。當queue清空(沒有點需要搜尋),搜尋就可以結束(代表地圖被填滿或沒找到)。
另外queue的長度也代表了可通行點的數量。
用陣列實作queue:
```cpp
node qq[1048576];//2^20
```
如果會的話,用vector也可以。
#### 如何紀錄
==把起點存進去queue!!!==這是我們第一個紀錄的點,並希望從這個點向四周搜尋。
```cpp
qq[0].x=Sx;
qq[0].y=Sy;
qq[0].dis=0;
```
維護兩個指針i,j,分別代表queue的head跟尾巴,重複直到i=j或i>j(等價於在i<j時執行),也就是重複到queue空掉。
```cpp
for (i=0, j=1; i<j; i++)
{
cur=qq[i];
//...nxt初始化(與cur有關)與條件檢查
if(搜到新的點nxt){
qq[j] = nxt;
j++;
}
}
```
小結:
queue的性質(first in first out 排隊的感覺)可以保證會先把dis=n的所有點紀錄一遍,同時把搜到的丟進queue,再搜dis=n+1的所有點。qq[0]記得初始化。
### 如何搜尋?
1. 檢查是否為終點
2. 搜尋上下左右
1. nxt.dis = cur.dis + 1(要比上一個dir+1)
2. 檢查邊界條件
#### 1
直接跟終點比較就好
```cpp
if (cur.x == ex && cur.y == ey) {
return cur.dis;
}
```
#### 2-1
以本題而言,要搜尋上下左右可以建表(後面有宣告具體值就不用給陣列長度),來表達上下左右的位移值,兩個陣列剛好會是sin跟cos的90\*n度的值相同。
```cpp
int dx[] = {0, 1, 0, -1};
int dy[] = {1, 0, -1, 0};
```
再用迴圈把四個方向都做一次,並且記得更新dis
```cpp
for (k=0; k<4; k++)
{
nxt = cur;
nxt.x += dx[k];
nxt.y += dy[k];
nxt.dis++;//important!!!這個沒做會直接暴死
//.....其他的東東
}
```
#### 2-2
##### 2-2-1 nxt是否在範圍內
n,m根據題意為二維陣列的大小。
```cpp
if(nxt.x >= 0 && nxt.x < n
&& nxt.y >= 0 && nxt.y < m)
```
很美的對齊。
##### 2-2-2 是否為牆壁&&是否使用過
```cpp
if( board[nxt.x][nxt.y] != '#'
&& !used[nxt.x][nxt.y] ) {
//...確認後加入點
}
```
## Q3:如何停下?
本題為有終點的題目,所以只要比較cur跟終點是否相同即可得知最短距離dis(上面有code了)。
找不到就
```cpp
return -1
```
## Q4:Init
### 全域
```cpp
int dx[] = {0, 1, 0, -1};
int dy[] = {1, 0, -1, 0};
//建表
int n, m;
string board[1024];
//io
struct node
{
int x, y;
int dis;
};
node qq[1048576];
bool used[1024][1024];
```
### BFS函數內
```cpp
int i, j, k;
node cur, nxt;
```
used要初始化為0
起點要對qq[0]跟used初始化
### 開始搜尋的時候
cur要對nxt初始化
cur要根據建表的位移量位移
dis++!!!
## Q5:IO
隨便做一做。(指input&output)
記得一邊檢查是否為起終點,用變數維護
### 檢查
沒有起、終點(Sx等,先設成-1)
另有一些細節需要前往[廣度優先搜尋 - HackMD](https://hackmd.io/@sa072686/cp/%2FVXHUmlgOTPGNxV9FWbdukA)收看
# 程式碼結構
## init
```cpp
#include<bits/stdc++.h>
using namespace std;
//
int n,m;
string board[1024];
//io
int dx[4]={0,1,0,-1};
int dy[4]={1,0,-1,0};
//方向位移
struct node{
int x,y,dis;
};
node qq[1048576];
bool used[1024][1024];
//搜尋要用的東西 可以背起來
```
## bfs
#### 參數
```cpp
(int_sx, int _sy, int _ex, int _ey, int n, int m)
```
#### init
##### 宣告
```cpp
int i, j, k;
node cur, nxt;
```
##### qq
```cpp
// 將所有起點放進 queue 中
qq[0].x = _sx;
qq[0].y = _sy;
qq[0].dis = 0;
```
##### used
```cpp
//把所有的used設為0
memset(used,0,sizeof(used));
// 先將所有起點標記為「已探索」
used[_sx][_sy] = true;
```
#### queue
```cpp
for(i=0,j=1;i<j;i++){
//...
}
```
##### init
```cpp
cur=qq[i];
```
##### 檢查終點
```cpp
if(cur.x == _ex && cur.y == _ey){
return cur.dis;
}
```
##### 向四個方向搜尋
###### init
```cpp
nxt=cur;
nxt.x+=dx[k];
nxt.y+=dy[k];
nxt.dis++;//important
```
###### 條件檢查+紀錄
```cpp
if(nxt.x >= 0 && nxt.x <n
&& nxt.y >= 0 && nxt.y <m){
if(board[nxt.x][nxt.y]!='#'
&& !used[nxt.x][nxt.y]){
used[nxt.x][nxt.y]=true;
qq[j] = nxt;
j++;
}
}
```
## main
```cpp
int main(){
cin>>n>>m;//n直m橫
int i,j;
int Sx=-1,Sy=-1,Ex=-1,Ey=-1;//條件檢查
for(i=0;i<n;i++){
for(j=0;j<m;j++){
cin>>board[i][j];
if(board[i][j]=='S'){
Sx=i;
Sy=j;
}
else if(board[i][j]=='E'){
Ex=i;
Ey=j;
}
}
}
if(Sx==-1||Ex==-1){//條件檢查
cout<<-1;
}
else cout<<bfs(Sx,Sy,Ex,Ey,n,m);
}
```
![[BFS_Code]]
# 參考資料
- [Ch19 BFS 廣度優先搜尋 - HackMD](https://hackmd.io/@CLKO/BkxmExS8J4?type=view)
- 圖很讚
- [搜尋 - HackMD](https://hackmd.io/@tw20000807/search#/12/19)
- [廣度優先搜尋 - HackMD](https://hackmd.io/@sa072686/cp/%2FVXHUmlgOTPGNxV9FWbdukA)
## 原本講義裡的code(含註解)
```cpp
int n, m;
int dx[] = {0, 1, 0, -1};
int dy[] = {1, 0, -1, 0};
string board[1024];
struct node
{
int x, y;
int dis;
};
node qq[1048576];
bool used[1024][1024];
int bfs(int sx, int sy, int ex, int ey)
{
int i, j, k;
node cur, nxt;
memset(used, 0, sizeof(used));//將used陣列填滿false
// 將所有起點放進 queue 中
qq[0].x = sx;
qq[0].y = sy;
qq[0].dis = 0;
// 先將所有起點標記為「已探索」
used[sx][sy] = true;
// 依序看過 queue 中所有的點
// i 為目前要看的點;j 為新的點進來時要放的空白位置
for (i=0, j=1; i<j; i++)
{
cur = qq[i];
// 檢查是否為終點
if (cur.x == ex && cur.y == ey)
{
// 如果是,回報最小步數
return cur.dis;
}
// 窮舉上下左右四個方向,尋找可通行地點
for (k=0; k<4; k++)
{
nxt = cur;
nxt.x += dx[k];
nxt.y += dy[k];
nxt.dis++;
// 最優先檢查是否在範圍內,以防存取到不正常的陣列 index
if (nxt.x >= 0 && nxt.x < n
&& nxt.y >= 0 && nxt.y < m)
{
// 確定在範圍內,才檢查是否可通行和是否已探索
if (board[nxt.x][nxt.y] != '#'
&& !used[nxt.x][nxt.y])
{
// 設為已探索並加入 queue 中
used[nxt.x][nxt.y] = true;
qq[j] = nxt;
j++;
}
}
}
}
// 如果找過所有可探索範圍,未能發現終點,即為無解
return -1;
}
```
{%hackmd @sa072686/__style %}