广度优先搜索-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;
}
有两种情况你不需要使用哈希集
- 你完全确定没有循环,例如,在树遍历中;
- 你确实希望多次将结点添加到队列中。
下面是几个利用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的模型,剩下的都是套路,路漫漫其修远兮,在刷题练内功的道路上,我辈还需继续努力。