前言
有時候,當你並不了解很多高級算法的時候,搜索不失為一種解決問題的好方法,而且很多高級算法有或多或少的會用到搜索或者搜索的思想。可見,搜索是一個基礎並且必須要掌握的算法。
在這篇文章中,會對BFS進行一次系統的總結。好了,廢話不多說,趕緊開始。
搜索里面包含了一下內容:
- 列表
- 線性搜索
- 二分搜索
- 樹/圖
- 廣度優先搜索
- 最良優先搜索
- 均一開銷搜索
- A*算法
- 深度優先搜索
- 迭代深化深度優先搜索
- 深度限制搜索
- 雙方向探索
- 分支限定法
- 廣度優先搜索
- 字符串
- KMP算法
- BM算法
- AC自動機
- Rabin-Karp算法
- 位圖算法
但是這里直講關於BFS的內容,涉及到BFS的有樹和圖;
實現
樹
注意,樹里面分為二叉樹和多叉樹,處理方式有些不一樣。
樹的結構
/**
* 樹
*/
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode *prev;
TreeNode(int x) : val(x), left(NULL), right(NULL), prev(NULL) {}
};
struct MoreTreeNode {
int val;
std::vector<MoreTreeNode*> nexts;
MoreTreeNode(int x) : val(x) {}
};
樹的bfs實現
/**
* 樹的最基本的BFS
* @param root
*/
void Solution::DanXiangTreeBFS(TreeNode* root) {
if (root == NULL) {
return ;
}
std::queue<TreeNode*> queues;
queues.push(root);
while (!queues.empty()) {
TreeNode* node = queues.front();
queues.pop();
std::cout << "value:" << node->val << std::endl;
if (node->left) {
queues.push(node->left);
}
if (node->right) {
queues.push(node->right);
}
}
}
/**
* 多插樹的最基本的BFS
* @param root
*/
void Solution::MoreTreeBFS(MoreTreeNode* root) {
if (root) {
return ;
}
std::queue<MoreTreeNode*> queues;
queues.push(root);
std::unordered_map<MoreTreeNode*, bool> visited;
visited[root] = true;
while (!queues.empty()) {
MoreTreeNode* node = queues.front();
queues.pop();
// 訪問所有子結點
for (int i = 0; i < node->nexts.size(); i++) {
MoreTreeNode* next = node->nexts[i];
if (next && !visited[next]) {
visited[next] = true;
std::cout << "value:" << next->val << std::endl;
queues.push(next);
}
}
}
}
總結
有沒有發現,樹的BFS遍歷必然會存在一個隊列,一個紀錄是否訪問過該結點的數組,總而言之,就是如果沒訪問過就加入,一直循環直到全部結點訪問完。
圖
圖的表示方式有好幾種,這里只用了鄰接表的表達方式。
圖的結構
/**
* 圖
*/
// 圖的最大頂點數目
#define MaxGraphSize 10
// 定義邊表結點
struct ArcNode {
int val;
ArcNode* next;
};
// 定義頂點表結點
template <class T>
struct VertexNode {
T vertex;
ArcNode* firstNode;
};
// 圖
struct Graph {
int edgeNum;
int vertexNum;
VertexNode<int> vertes[MaxGraphSize];
};
圖的bfs實現
/**
* 圖的BFS
*/
void Solution::GraphBFS(Graph* graph) {
int value[MaxGraphSize];
int head = 0, rear = 0;
int queue[MaxGraphSize];
int visited[MaxGraphSize];
memset(visited, 0, sizeof(int)*MaxGraphSize);
// 遍歷所有的結點,並且以該結點作為起始點,迭代遍歷起始結點的下一個結點
for (int i = 0; i < graph->vertexNum; i++) {
if (!visited[i]) {
visited[i] = 1;
queue[rear++] = i; //結點i入隊列
}
while(head != rear) {
int j = queue[head++]; //結點j出隊列
ArcNode* node = graph->vertes[j].firstNode; //找到對應的起始結點
while (node != NULL) {
int k = node->val; //結點k
if (!visited[k]) {
visited[k] = 1;
queue[rear++] = k;
}
node = node->next;
}
}
}
}
總結
是不是感覺有些不一樣了呢,nono,實質還是一樣的,只不過這里並沒有用到stl,而是用下標head和rear去紀錄出度和入度,實現一個隊列,因為當head和rear相等的時候就是訪問完所有結點的時候。
雙向廣搜
什么是雙向廣搜呢,就是說使用兩個隊列,一個從頭用BFS搜,另一個從尾也用BFS搜,直到雙方存在重疊的結點,則返回,至於返回什么根據題目的要求,可以是層數balabala;因為將搜索分為兩個部分,搜索的時間自然會變得很快。
/**
* 雙向廣搜,存在兩個方向上的BFS,當兩個方向的搜索生成同一子結點時終止此搜索
* 通常存在兩種方法:1.兩個方向交替擴展,2.選擇結點個數較少的那個方向先擴展
* @param root
*/
void Solution::ShuangXiangBFS(MoreTreeNode* begin, MoreTreeNode* target) {
// 假設是圖
std::queue<MoreTreeNode*> min, max;
min.push(begin);
max.push(target);
bool found = false;
std::unordered_map<MoreTreeNode*, int> visited;
visited[begin] = 1, visited[target] = 2; //初始狀態下默認設置成1(正向)和2(反向)
// flag作為區分兩個隊列的標志
auto extend_node = [&](std::queue<MoreTreeNode*> queues, bool flag) {
MoreTreeNode* node = queues.front();
queues.pop();
// 訪問所有子結點
for (int i = 0; i < node->nexts.size(); i++) {
MoreTreeNode* next = node->nexts[i];
if (flag) {
if (visited[next] != 1) { // 沒在正向隊列中出現
if (visited[next] == 2) { // 在反向隊列中出現過,說明已經找到了
found = true;
return;
}
visited[next] = 1; //做標記
queues.push(next);
}
}
else {
if (visited[next] != 2) { // 同上
if (visited[next] == 1) {
found = true;
return;
}
visited[next] = 2;
queues.push(next);
}
}
}
};
while (!min.empty() || !max.empty()) {
if (!min.empty()) {
extend_node(min, true);
}
if (found) {
return ;
}
if (!max.empty()) {
extend_node(max, false);
}
if (found) {
return ;
}
}
}
其實上面的代碼並不完全正確,仍然需要進行優化,比如說會遇到這樣的情況
求S-T的最短路,交替節點搜索(一次正向節點,一次反向節點)時
while(1):
S –> 1
T –> 3
while(2):
S -> 5
T -> 4
while(3):
1 -> 5
3 -> 5 返回最短路為4,錯誤的,事實是3,S-2-4-T
正確做法的是交替逐層搜索,保證了不會先遇到非最優解就跳出,而是檢查完該層所有節點,得到最優值。也即如果該層搜索遇到了對方已經訪問過的,那么已經搜索過的層數就是答案了,可以跳出了,以后不會更優的了。
下面的代碼進行優化:優先去訪問結點少的層。
#include <stdio.h>
#include <stdlib.h>
#include <queue>
#include <iostream>
using namespace std;
int l; //4<=l<=300
int sx,sy,tx,ty;
int a[305][305]; //正向搜索層次
int b[305][305]; //反向搜索層次
struct point{
int x,y;
};
struct point dir[]=
{
{1,2},{1,-2},{-1,2},{-1,-2},
{2,1},{2,-1},{-2,1},{-2,-1}
};
bool check(int x,int y){
if(x>=0 && x<l && y>=0 && y<l)
return true;
else
return false;
}
void dbfs(){
memset(a,-1,sizeof(a));
memset(b,-1,sizeof(b));
a[sx][sy]=0;
b[tx][ty]=0;
queue<point> forQ,backQ;
point p1,p2;
p1.x=sx;
p1.y=sy;
p2.x=tx;
p2.y=ty;
forQ.push(p1);
backQ.push(p2);
//正反向隊列至少還有一個可以擴展
while(forQ.empty()==false || backQ.empty()==false){
//優化:優先擴展元素少的隊列(如果只有一個隊列非空,則擴展非空隊列)
int forSize=forQ.size();
int backSize=backQ.size();
if(backSize==0 || forSize<backSize){
//擴展正向隊列一層
int i;
for(i=0;i<forSize;i++){
point cur=forQ.front();
forQ.pop();
if(b[cur.x][cur.y]!=-1){
printf("%d\n",a[cur.x][cur.y]+b[cur.x][cur.y]);
return;
}
int j;
for(j=0;j<8;j++){
if(check(cur.x+dir[j].x, cur.y+dir[j].y)){
point next={cur.x+dir[j].x, cur.y+dir[j].y}; //!注意struct的創建方式
if(a[next.x][next.y]!=-1)//以前已經正向擴展過
continue;
a[next.x][next.y]=a[cur.x][cur.y]+1;
forQ.push(next);
}
}
}
}else{
//擴展反向隊列一層
int i;
for(i=0;i<backSize;i++){
point cur=backQ.front();
backQ.pop();
if(a[cur.x][cur.y]!=-1){
printf("%d\n",a[cur.x][cur.y]+b[cur.x][cur.y]);
return;
}
int j;
for(j=0;j<8;j++){
if(check(cur.x+dir[j].x, cur.y+dir[j].y)){
point next={cur.x+dir[j].x, cur.y+dir[j].y};
if(b[next.x][next.y]!=-1)
continue;
b[next.x][next.y]=b[cur.x][cur.y]+1;
backQ.push(next);
}
}
}
}
}//end while
printf("0");
}
int main(void){
int time;
scanf("%d",&time);
while(time-->0){
scanf("%d%d%d%d%d\n",&l,&sx,&sy,&tx,&ty);
dbfs();
}
return 0;
}
練習
網絡上有這么一道題,騎士移動
這道題就可以用到BFS,雙向BFS,A*(有目的的BFS)來解決
BFS
/**
* 騎士題目
* 給定起點和終點,按照騎士的走法,求解最短路數
*/
// 使用bfs實現(核心代碼)
void qishi_bfs() {
typedef struct Node {
int x, y;
int step;
};
int dis[8][2]={{-2,1},{-2,-1},{-1,-2},{-1,2},{2,-1},{2,1},{1,-2},{1,2}};
std::vector< std::vector<int>> visited(8, std::vector<int>(8, 0)); // 8*8
Node start, end;
// 邊界判斷
auto isValid = [&](Node node) {
if (node.x < 0 || node.y < 0 || node.x > 7 || node.y > 7) {
return false;
}
return true;
};
// 是否為結果
auto isTarget = [&](Node node1, Node node2) {
if (node1.x == node2.x && node1.y == node2.y) {
return true;
}
return false;
};
std::queue<Node> queue;
queue.push(start);
visited[start.x][start.y] = 1;
auto state_bfs = [&]() {
while(!queue.empty()) {
Node next = queue.front();
queue.pop();
if (isTarget(start, next)) {
std::cout << next.step << std::endl;
break;
}
// 方可可以到達的哈什湖上
for (int i = 0; i < 8; ++i) {
Node t;
t.x = next.x + dis[i][0];
t.y = next.y + dis[i][1];
if (isValid(t) && visited[t.x][t.y] == 0) {
visited[t.x][t.y] = 1;
t.step = next.step + 1;
queue.push(t);
}
}
}
};
state_bfs();
}
雙向BFS
// 使用雙隊列的bfs實現(核心代碼)
void qishi_double_bfs() {
typedef struct Node {
int x, y;
int step;
};
int dis[8][2]={{-2,1},{-2,-1},{-1,-2},{-1,2},{2,-1},{2,1},{1,-2},{1,2}};
std::vector< std::vector<int>> level(8, std::vector<int>(8, 0)); // 8*8
std::vector< std::vector<int>> color(8, std::vector<int>(8, 0)); // 區分隊列
Node start, end;
std::queue<Node> queue_start;
queue_start.push(start);
level[start.x][start.y] = 0;
color[start.x][start.y] = 1;
std::queue<Node> queue_end;
queue_start.push(end);
level[end.x][end.y] = 1; //因為起點和終點必然不是重合的,它們之間至少存在1的距離
color[start.x][start.y] = 2;
// 邊界判斷
auto isValid = [&](Node node) {
if (node.x < 0 || node.y < 0 || node.x > 7 || node.y > 7) {
return false;
}
return true;
};
auto state_bfs = [&](std::queue<Node> queue, bool flag) {
while(!queue.empty()) {
Node next = queue.front();
queue.pop();
// 方可可以到達的哈什湖上
for (int i = 0; i < 8; ++i) {
Node t;
t.x = next.x + dis[i][0];
t.y = next.y + dis[i][1];
if (isValid(t)) {
continue;
}
if (flag) {
if (color[t.x][t.y] == 0) {
color[t.x][t.y] = 1;
level[t.x][t.y] = level[next.x][next.y] + 1;
queue.push(t);
}
else if (color[t.x][t.y] == 2) {
return level[t.x][t.y] + level[next.x][next.y];
}
}
else {
if (color[t.x][t.y] == 0) {
color[t.x][t.y] = 2;
level[t.x][t.y] = level[next.x][next.y] + 1;
queue.push(t);
}
else if (color[t.x][t.y] == 1) {
return level[t.x][t.y] + level[next.x][next.y];
}
}
}
}
};
// 避免出現起始或者結束中沒有下一步的結點,也就是可能中間斷掉了
while (!queue_start.empty() || !queue_end.empty()) {
if (!queue_start.empty()) {
state_bfs(queue_start, true);
}
if (!queue_end.empty()) {
state_bfs(queue_end, true);
}
}
}
A*
因為這個稍微有些復雜,所以會單獨寫一篇文章來描述。
其他
當我們已經獲取到了圖的鄰接表的時候,要求從起始結點到目標結點的路徑,應該怎么求呢?
/**
* 反向生成路徑,但是需要注意的地方在於,其只能生成一條路徑
* 要么你每次在主程序中得到目標結點后,就收斂然后使用這個函數
* @tparam state_t
* @param father
* @param target
* @return
*/
template <typename state_t>
std::vector<state_t> gen_path(const std::unordered_map<state_t, state_t> father, const state_t& target) {
std::vector<state_t> path;
path.push_back(target);
// 從葉結點一直到訪問到根結點(不再存在父結點)
for (state_t cur = target; father.find(cur) != father.end(); cur = father.at(cur)) {
path.push_back(cur);
}
std::reverse(path.begin(), path.end());
return path;
}
/**
* 很顯然,對於多條路徑的計算方法,DFS再合適不過了,
* 此時nexts為鄰接表,紀錄每個結點的相關的狀態數組
* @tparam state_t
*/
template <typename state_t>
std::vector<std::vector<state_t>> results;
template <typename state_t>
void get_more_path(const std::unordered_map<state_t, std::vector<state_t>> nexts, std::vector<state_t> path, state_t cur, state_t target) {
// 達到目標后收斂
if (cur == target) {
results.push_back(path);
return ;
}
else {
std::vector<state_t> next = nexts[cur];
for (int i = 0; i < next.size(); i++) {
state_t now = next[i];
path.push_back(now);
get_more_path(nexts, path, now, target);
path.pop_back();
}
}
}