算法:广度优先搜索(BFS)与队列

落爺英雄遲暮 提交于 2019-12-14 19:49:17

广度优先搜索-BFS的一个常见应用是找出从根结点到目标结点的最短路径,通常这发生在树或图中。我们提供了一个示例来解释在 BFS 算法中是如何逐步应用队列的。

示例:这里我们提供一个示例来说明如何使用 BFS 来找出根结点 A 和目标结点 G 之间的最短路径。
在这里插入图片描述
1. 结点的处理顺序是 什么 ?
在第一轮中,处理根结点
在第二轮中,处理根结点旁边的结点;
在第三轮中,处理距根结点两步的结点;


与树的层序遍历类似,越是接近根结点的结点将越早地遍历。
如果在第 k 轮中将结点 X 添加到队列中,则根结点与 X 之间的最短路径的长度恰好是 k。也就是说,第一次找到目标结点时,你已经处于最短路径中。

2. 队列的入队和出队顺序是什么?
如上图,我们首先将根结点排入队列。然后在每一轮中,我们逐个处理已经在队列中的结点,并将所有邻居添加到队列中。值得注意的是,新添加的节点不会立即遍历,而是在下一轮中处理。结点的处理顺序与它们添加到队列的顺序是完全相同的顺序,即先进先出(FIFO)。这就是我们在 BFS 中使用队列的原因。

BFS代码模板

int BFS(Node root, Node target) {
	queue<Node> queue;   // store all nodes which are waiting to be processed
    unordered_set<Node> used;     // store all the used nodes
    int step = 0;       // number of steps neeeded from root to current node
    
    /* 初始化 */
    add root to queue;
    add root to used;
    /* BFS */
    while (!queue.empty()) {
        step = step + 1;
        /* 遍历上一轮已经在队列中的节点 */
        int size = queue.size();
        for (int i = 0; i < size; ++i) {
            Node cur = queue.front();
            queue.pop();
            return step if cur is target;
            for (Node next : the neighbors of cur) {
                if (next is not in used) {
                    add next to queue;
                    add next to used;
                }
            }
            remove the first node from queue;
        }
    }
    return -1;   
}

有两种情况你不需要使用哈希集

  1. 你完全确定没有循环,例如,在树遍历中;
  2. 你确实希望多次将结点添加到队列中。

下面是几个利用BFS解决的问题,leetcode AC

例题1:岛屿数量。给定一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。
示例 1:
输入:
11110
11010
11000
00000
输出: 1

示例 2:
输入:
11000
11000
00100
00011
输出: 3

AC解答

class Solution {
public:
	int numIslands(vector<vector<char>>& grid) {
	int row = grid.size();
	if (!row) {
		return 0;
	}
	int col = grid[0].size();
	int num = 0;
	queue<pair<int, int>> q;
	/* BFS:广度优先搜索 */
	for (int i = 0; i < row; i++) {
		for (int j = 0; j < col; j++) {
			/* 1.碰到1就放入队列,然后以这个1为起点,采取广度优先搜索 */
			if (grid[i][j] == '1') {
				num++;
				grid[i][j] = '0';
				q.push({ i, j });
				/* 2.以当前根节点为起点, 把直接或者间接相连的1全部找出来,这就是一个岛屿 */
				while (!q.empty()) {
					auto node = q.front();
					q.pop();
					int r = node.first;
					int c = node.second;
					/* 3.开始搜索所有的"1"节点 */
					/* 上边 */
					if (r - 1 >= 0 && grid[r - 1][c] == '1') {
						q.push({ r - 1, c });
						grid[r - 1][c] = '0';
					}

					/* 下边 */
					if (r + 1 < row && grid[r + 1][c] == '1') {
						q.push({ r + 1, c });
						grid[r + 1][c] = '0';
					}
					/* 左边 */
					if (c - 1 >= 0 && grid[r][c - 1] == '1') {
						q.push({ r, c - 1 });
						grid[r][c - 1] = '0';
					}
					/* 右边 */
					if (c + 1 < col && grid[r][c + 1] == '1') {
						q.push({ r, c + 1 });
						grid[r][c + 1] = '0';
					}		
				}	
			}
		}
	}
		return num;
	}
};

例题2:打开锁盘。你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字: ‘0’, ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’ 。每个拨轮可以自由旋转:例如把 ‘9’ 变为 ‘0’,‘0’ 变为 ‘9’ 。每次旋转都只能旋转一个拨轮的一位数字。锁的初始数字为 ‘0000’ ,一个代表四个拨轮的数字的字符串。列表 deadends 包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。字符串 target 代表可以解锁的数字,你需要给出最小的旋转次数,如果无论如何不能解锁,返回 -1。
示例 1:
输入:deadends = [“0201”,“0101”,“0102”,“1212”,“2002”], target = “0202”
输出:6
解释:
可能的移动序列为 “0000” -> “1000” -> “1100” -> “1200” -> “1201” -> “1202” -> “0202”。
注意 “0000” -> “0001” -> “0002” -> “0102” -> “0202” 这样的序列是不能解锁的,
因为当拨动到 “0102” 时这个锁就会被锁定。

示例 2:
输入: deadends = [“8888”], target = “0009”
输出:1
解释:把最后一位反向旋转一次即可 “0000” -> “0009”。

示例 3:
输入: deadends = [“8887”,“8889”,“8878”,“8898”,“8788”,“8988”,“7888”,“9888”], target = “8888”
输出:-1
解释:无法旋转到目标数字且不被锁定。

示例 4:
输入: deadends = [“0000”], target = “8888”
输出:-1

class Solution {
public:
	int addOne(char &ch){
		if (isdigit(ch)){
			if (ch != '9') {
				ch++;
			}
			else {
				ch = '0';
			}
			return 0;
		}
		return -1;
	}
	int minOne(char &ch){
		if (isdigit(ch)){
			if (ch != '0') {
				ch--;
			}
			else {
				ch = '9';
			}
			return 0;
		}
		return -1;
	}

	int openLock(vector<string>& deadends, string target) {
		int step = 0;
		unordered_set<string> deadset(deadends.begin(), deadends.end());
		unordered_set<string> s; //存放被访问过的节点
		queue<string> q;		//待访问的节点
		string begin = "0000";	//起始节点

		if (deadset.count(begin) != 0){
			return -1;
		}
		/* bfs search algorithm*/
		q.push(begin);
		s.insert(begin);
		while (!q.empty()) {
			int size = q.size();
			step++;
			//遍历第step轮队列
			for (int i = 0; i < size; i++){
				string cur = q.front();
				q.pop();
				for (int i = 0; i < 4; i++) {
					string tmp1 = cur;
					string tmp2 = cur;
					addOne(tmp1[i]);
					minOne(tmp2[i]);
					if (tmp1 == target || tmp2 == target){
						return step;
					}
					//没有被访问,且
					if (s.count(tmp1) == 0 && deadset.count(tmp1) == 0){
						s.insert(tmp1);
						q.push(tmp1);
					}
					if (s.count(tmp2) == 0 && deadset.count(tmp2) == 0){
						s.insert(tmp2);
						q.push(tmp2);
					}
				}
			}
		}
		return -1;
	}
};

此题目中,因为要去高频率查询vector,如果这样不做变化,超时,用对数据结构很重要啊!按照一般的BFS来写这道题目,审查了一遍,感觉没问题,提交后虽然AC,一看时间,1300ms!!!简直不能忍受。后来看看了别人的写法,因为这个过程我们要反复的去vector中去查找当前节点是不是dead,所以相当耗时,如果将这个vector一把转成无序set,那相当快,直接160ms。看来用对数据结构很重要啊!

例题3:完全平方数给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
示例 1:
输入: n = 12
输出: 3
解释: 12 = 4 + 4 + 4.

示例 2:
输入: n = 13
输出: 2
解释: 13 = 4 + 9.

在这里插入图片描述

#include<iostream>
#include<set>
#include<queue>
using namespace std;

class Solution {
public:
    int numSquares(int n) {
	    int step = 0;
	    queue<int> q;
	    set<int> visited;

	    q.push(n);
	    visited.insert(n);
	    while (!q.empty()) {
            step++;
            int size = q.size();
            /* 第step轮需要访问的节点 */
            for (int i = 0; i < size; i++) {
                int res = 0;
                int cur = q.front();
                q.pop();

                /* 查看平方数小于cur的数是哪几个,从1开始统计 */
                for (int j = 1; ; j++) {
                    res = cur - j*j;
                    /* 如果当前res小于0,代表j平方大于cur */
                    if (res < 0) {
                        break;
                    }
                    if (res == 0) {
                        return step;
                    }

                    /* 前面没有出现过,就需要加入队列 */
                    if (visited.count(res) == 0) {
                        q.push(res);
                        visited.insert(res);
                    }
                }
            }
	    }

	    return -1;
    }
};

其实BFS不是很难,难的是建立出模型,如果能够很快的建立出BFS的模型,剩下的都是套路,路漫漫其修远兮,在刷题练内功的道路上,我辈还需继续努力。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!