BFS-基礎簡單的算法


前言

有時候,當你並不了解很多高級算法的時候,搜索不失為一種解決問題的好方法,而且很多高級算法有或多或少的會用到搜索或者搜索的思想。可見,搜索是一個基礎並且必須要掌握的算法。

在這篇文章中,會對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();
        }
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM