1. 程式人生 > 其它 >廣度優先搜尋(BFS)

廣度優先搜尋(BFS)

技術標籤:演算法筆記

廣度優先搜尋

廣度優先搜尋:當碰到岔道口時,總是先依次訪問從該岔道口能直接到達的所有結點,然後再按這些結點被訪問的順序去依次訪問它們能直接到達的所有結點,以此類推,直到所有結點都被訪問為止。

廣度優先搜尋(BFS)一般由佇列實現,且總是按層次的順序進行遍歷。

  1. 定義佇列q,並將起點s入隊
  2. 寫一個while迴圈,迴圈條件時佇列q非空
  3. 在while迴圈中,先取出隊首元素top,然後訪問它(訪問可以時任何事情,例如將其輸出)。訪問完後將其出隊
  4. 將top的下一層結點中所有未曾入隊的結點入隊,並標記它們的層號為now的層號加1,同時設定這些入隊的結點已入過隊
  5. 返回2繼續迴圈
void
BFS(int s) { queue<int> q; q.push(s); while(!q.empty()) { 取出隊首元素top; 訪問隊首元素top; 將隊首元素出隊; 將top的下一層結點中未曾入隊的結點全部入隊,並設定為已入隊; } }

例題1

給出一個 mxn 的矩陣,矩陣中的元素為0或1。稱位置(x, y)與其上下左右四個位置(x, y+1)、(x, y-1)、(x+1, y)、(x-1, y)是相鄰的。如果矩陣中有若干個1是相鄰的(不必兩兩相鄰),那麼稱這些 1 構成了一塊“塊”。求給定的矩陣中“塊”的個數。
0 1 1 1 0 0 1
0 0 1 0 0 0 0

0 0 0 0 1 0 0
0 0 0 1 1 1 0
1 1 1 0 1 0 0
1 1 1 1 0 0 0
例如上面 6x7 的矩陣中,“塊”的個數為4。

分析

列舉每一個位置的元素
如果為0,則跳過
如果為1,則使用BFS查詢與該位置相鄰的4個位置
判斷它們是否為1(如果某個相鄰的位置為1,則同樣去查詢與該位置相鄰的4個位置,直到整個“1”塊訪問完畢)。

為了防止走回頭路,設定一個bool型陣列 inq 來記錄每個位置是否在BFS中已入過隊

小技巧:對當前位置(x, y)來說,由於與其相鄰的四個位置分別為(x, y+1)、(x, y-1)、(x+1, y)、(x-1, y),那麼不妨設定下面兩個增量陣列,來表示四個方向(0, 1)、(0, -1)、(1, 0)、(-1, 0)

int X[] = {0, 0, 1, -1};
int Y[] = {1, -1, 0, 0};

使用for迴圈來列舉4個方向,以確定當前座標(nowX, nowY)相鄰的4個位置。

for(int i = 0; i < 4; i++)
{
	newX = nowX + X[i];
	newY = nowY + Y[i];
}

BFS

#include <cstdio>
#include <queue>
using namespace std;
const int maxn = 100;
struct node
{
	int x, y; //位置(x, y)
}Node;

int n, m; //矩陣大小為n*m
int matrix[maxn][maxn]; //01矩陣
bool inq[maxn][maxn] = {false}; //記錄位置(x, y)是否已入過隊
int X[4] = {0, 0, 1, -1}; //增量陣列
int Y[4] = {1, -1, 0, 0};
//判斷座標(x, y)是否需要訪問
bool judge(int x, int y)
{
    //越界返回false
	if(x >= n || x < 0 || y >= m || y < 0)
		return false;
    //當前位置為0,或(x, y)已入過隊,返回false
	if(matrix[x][y] == 0 || inq[x][y] == true)
		return false;
    //以上都不滿足,返回true
	return true;
}
//BFS函式訪問位置(x, y)所在的塊,將該塊中所有“1”的inq都設定為true
void BFS(int x, int y)
{
	queue<node> Q; //定義佇列
	Node.x = x, Node.y = y; //當前結點的座標為(x, y)
	Q.push(Node); //將結點Node入隊
	inq[x][y] = true; //設定(x, y)已入隊
	while(!Q.empty())
	{
		node top = Q.front(); //取出隊首元素
		Q.pop(); //隊首元素出隊
		for(int i = 0; i < 4; i++) //迴圈4次,得到4個相鄰位置
		{
			int newX = top.x + X[i];
			int newY = top.y + Y[i];
			if(judge(newX, newY)) //如果新位置(newX, newY)需要訪問
			{
			    //設定Node的座標為(newX, newY)
				Node.x = newX, Node.y = newY;
				Q.push(Node); //將結點Node加入佇列
				inq[newX][newY] = true; //設定位置(newX, newY)已入過隊
			}
		}
	}
}

int main()
{
	scanf("%d%d", &n, &m);
	for(int x = 0; x < n; x++)
	{
		for(int y = 0; y < m; y++)
		{
			scanf("%d", &matrix[x][y]); //讀入01矩陣
		}
	}
	int ans = 0; //存放塊數
	for(int x = 0; x < n; x++) //列舉每一個位置
	{
		for(int y = 0; y < m; y++)
		{
		    //如果元素為1,且未入過隊
			if(matrix[x][y] == 1 && inq[x][y] == false)
			{
				ans++; //塊數加1
				BFS(x, y); //訪問整個塊,將該塊所有“1”的inq都標記為true
			}
		}
	}
	printf("%d\n", ans);
	return 0;
}

例題2

給定一個n*m大小的迷宮,其中 * 代表不可通過的牆壁,而 . 代表平地,S表示起點,T代表終點。移動過程中,如果當前位置是(x, y)(下標從0開始),且每次只能前往上下左右(x, y+1)、(x, y-1)、(x-1, y)、(x+1, y)四個位置的平地,求從起點S到達終點T的最少步數。
在這裡插入圖片描述
在上面的樣例中,S的座標為(2, 2),T的座標為(4, 3)

分析

由於求最少步數,而BFS是通過層次的順序來遍歷的,因此可以從起點S開始計數遍歷的層數,那麼在到達終點T時的層數就是需要求解的起點S到達終點T的最少步數。

#include <cstdio>
#include <queue>
#include <cstring>
using namespace std;
const int maxn = 100;
struct node
{
	int x, y; //位置(x, y)
	int step; //step為從起點S到達該位置的最少步數(即層數)
}S, T, Node; //S為起點,T為終點,Node為臨時結點

int n, m; //n為行,m為列
char maze[maxn][maxn]; //迷宮資訊
bool inq[maxn][maxn] = {false}; //記錄位置(x, y)是否已入過隊
int X[4] = {0, 0, 1, -1}; //增量陣列
int Y[4] = {1, -1, 0, 0};
//檢測位置(x, y)是否有效
bool test(int x, int y)
{
	if(x >= n || x < 0 || y >= m || y < 0)
		return false; //超過邊界
	if(maze[x][y] == '*')
		return false; //牆壁*
	if(inq[x][y] == true)
		return false; //已入過隊
	return true; //有效位置
}

int BFS()
{
	queue<node> q; //定義佇列
	q.push(S); //將起點S入隊
	while(!q.empty())
	{
		node top = q.front(); //取出隊首元素
		q.pop(); //隊首元素出隊
		if(top.x == T.x && top.y == T.y)
		{
			return top.step; //終點,直接返回最少步數
		}
		for(int i = 0; i < 4; i++) //迴圈4次,得到4個相鄰位置
		{
			int newX = top.x + X[i];
			int newY = top.y + Y[i];
			if(test(newX, newY)) //位置(newX, newY)有效
			{
				//設定Node的座標為(newX, newY)
				Node.x = newX, Node.y = newY;
				Node.step = top.step + 1; //Node層數為top的層數加1
				q.push(Node); //將結點Node加入佇列
				inq[newX][newY] = true; //設定位置(newX, newY)已入過隊
			}
		}
	}
	return -1; //無法到達終點T返回-1
}

int main()
{
	scanf("%d%d", &n, &m);
	for(int i = 0; i < n; i++)
	{
		getchar(); //過濾掉每行後面的換行符
		for(int j = 0; j < m; j++)
		{
			maze[i][j] = getchar();
		}
		maze[i][m + 1] = '\0';
	}
	scanf("%d%d%d%d", &S.x, &S.y, &T.x, &T.y); //起點和終點的座標
	S.step = 0; //初始化起點的層數為0,即S到S的最少步數為0
	printf("%d\n", BFS());
	return 0;
}
//輸入資料
5 5 //5行5列
..... //迷宮資訊
.*.*.
.*S*.
.***.
...T*
2 2 4 3 //起點S的座標與終點T的座標
11 //輸出資料

在BFS中設定的inq陣列的含義是判斷結點是否已入過隊,而不是結點是否已被訪問。
區別:如果設定成是否被訪問,有可能在某個結點正在佇列中(但還未訪問)時由於其他結點可以到達它而將這個結點再次入隊,導致很多結點反覆入隊,計算量增加。
因此BFS中讓每個結點只入隊一次,則需要設定inq陣列的含義為結點是否已入隊。