1、什么广度优先搜索(Breadth-First-Search)
广度优先搜索(Breadth-First-Search),简称 BFS。直观地讲,它其实就是一种地毯式或水波纹层层推进的搜索策略,即先查找离起始顶点最近的,然后是次近的,依次往外搜索。
2、代码模板
2.1、python 模板
# 从 start 到 end
def bfs(graph, start, end):
# 已访问顶点
visited = set()
# LIFO
queue = []
queue.append[start])
# record visited node
visited.add(start)
while queue:
node = queue.pop()
visited.add(node)
process(node)
nodes = generate_related_nodes(node)
queue.push(nodes)
# other processing work
...
2.2、Java 模板
// 计算从起点 start 到终点 target 的最近距离
int bfs(Node start, Node target) {
Queue<TreeNode> queue;
Set<Node> visited; // 记录已访问的顶点,避免走回头路,陷入死循环0
// 起点 start 入队
queue.offer(start);
// start 入队,则代表 start 被访问过了,需要记录
visited.add(start);
// 记录扩散的层数
int step = 0;
while (queue is not empty) {
int size = queue.size();
// 将当前队列中所有的节点分别出队,向外扩散一层
while (size-- > 0) {
Node curr = queue.poll();
// 判断当前节点是否到达终点 target
if (curr is target) {
return step;
}
// 将当前节点的子节点入队
for (Node next : curr.children) {
if (next not in visited) {
queue.offer(next);
visited.add(x);
}
}
}
// 更新步数
step++;
}
return step;
}
代码中的变量解释:
queue用于存储距起始节点stargetn 层的所有节点,是BFS的核心数据结构;curr.children获取curr所有的直接孩子节点;visited主要记录被访问过的节点,防止走回头路,陷入死循环,做剪枝用;大部分时候都是必须的,如果像n叉树的数据结构,只有next指针的,则不需要visited
3、实战
3.1、二叉树最小深度
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int minDepth(TreeNode root) {
if (root == null) {
return 0;
}
Queue<TreeNode> queue = new LinkedList<>();
// 根节点 root 入队
queue.offer(root);
// depth 初始化为 1,因为 root 为第一层
int depth = 1;
while (!queue.isEmpty()) {
int size = queue.size();
// 将当前队列中所有的节点分别出队,向外扩散
while (size-- > 0) {
TreeNode curr = queue.poll();
// 判断当前节点是否到达叶节点
if (curr.left == null && curr.right == null) {
return depth;
}
// 将当前节点的子节点入队
if (curr.left != null) {
queue.offer(curr.left);
}
if (curr.right != null) {
queue.offer(curr.right);
}
}
depth++;
}
return depth;
}
}
3.2、打开转盘锁
解法一、BFS
class Solution {
public int openLock(String[] deadends, String target) {
// 需要跳过的死亡密码
Set<String> deads = Stream.of(deadends).collect(Collectors.toSet());
// 记录被访问的节点
Set<String> visited = new HashSet<>();
Queue<String> queue = new LinkedList<>();
int step = 0;
queue.offer("0000");
visited.add("0000");
while (!queue.isEmpty()) {
int size = queue.size();
// 将当前队列中所有的节点分别出队,向外扩散一层
while (size-- > 0) {
String curr = queue.poll();
if (deads.contains(curr)) {
// 遇到死亡密码跳过
continue;
}
// 判断当前节点是否到达终点 target
if (curr.equals(target)) {
return step;
}
// 每次波动转盘锁的四个位置的概率一样
// 每位密码有可能会左旋或右旋
for (int i = 0; i < 4; i++) {
// 右旋
String plus = plusOne(curr, i);
if (!visited.contains(plus)) {
queue.offer(plus);
visited.add(plus);
}
// 左旋
String minus = minusOne(curr, i);
if (!visited.contains(minus)) {
queue.offer(minus);
visited.add(minus);
}
}
}
// 更新步数
step++;
}
// 如果无论如何不能解锁,返回 -1。
return -1;
}
public String plusOne(String pwd, int index) {
char[] ch = pwd.toCharArray();
if (ch[index] == '9') {
ch[index] = '0';
} else {
ch[index]++;
}
return new String(ch);
}
public String minusOne(String pwd, int index) {
char[] ch = pwd.toCharArray();
if (ch[index] == '0') {
ch[index] = '9';
} else {
ch[index]--;
}
return new String(ch);
}
}
解法二、双向 BFS
class Solution {
public int openLock(String[] deadends, String target) {
// 需要跳过的死亡密码
Set<String> deads = Stream.of(deadends).collect(Collectors.toSet());
// 记录被访问的节点
Set<String> visited = new HashSet<>();
Set<String> start = new HashSet<>();
Set<String> end = new HashSet<>();
int step = 0;
start.add("0000");
end.add(target);
while (!start.isEmpty() && !end.isEmpty()) {
// 哈希集合在遍历的过程中不能修改,用 temp 存储扩散结果
Set<String> temp = new HashSet<>();
// 将当前队列中所有的节点分别出队,向外扩散一层
for (String curr : start) {
if (deads.contains(curr)) {
// 遇到死亡密码跳过
continue;
}
// 判断当前节点是否到达终点 target
if (end.contains(curr)) {
return step;
}
visited.add(curr);
// 每次波动转盘锁的四个位置的概率一样
// 每位密码有可能会左旋或右旋
for (int i = 0; i < 4; i++) {
// 右旋
String plus = plusOne(curr, i);
if (!visited.contains(plus)) {
temp.add(plus);
}
// 左旋
String minus = minusOne(curr, i);
if (!visited.contains(minus)) {
temp.add(minus);
}
}
}
// 更新步数
step++;
// temp 相当于 q1start
// 这里交换 start end,下一轮 while 就是扩散 end
// 重复此步骤,其实就是,分别从 start 和 end 扩散
start = end;
end = temp;
}
// 如果无论如何不能解锁,返回 -1。
return -1;
}
public String plusOne(String pwd, int index) {
char[] ch = pwd.toCharArray();
if (ch[index] == '9') {
ch[index] = '0';
} else {
ch[index]++;
}
return new String(ch);
}
public String minusOne(String pwd, int index) {
char[] ch = pwd.toCharArray();
if (ch[index] == '0') {
ch[index] = '9';
} else {
ch[index]--;
}
return new String(ch);
}
}
4、小结
BFS 需要借助队列来实现,遍历得到的路径就是起始顶点到终止顶点的最短路径。