队列和广度优先搜索
Posted 清梅煮酒
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了队列和广度优先搜索相关的知识,希望对你有一定的参考价值。
广度优先搜索(BFS
)的一个常见应用是找出从根结点到目标结点的最短路径。
思路
1. 结点的处理顺序
在第一轮中,我们处理根结点。在第二轮中,我们处理根结点旁边的结点;在第三轮中,我们处理距根结点两步的结点;等等等等。
与树的层序遍历类似,越是接近根结点的结点将越早地遍历。
如果在第 k
轮中将结点 X
添加到队列中,则根结点与 X
之间的最短路径的长度恰好是 k
。也就是说,第一次找到目标结点时,你已经处于最短路径中。
2. 队列的入队和出队顺序
首先将根结点排入队列。然后在每一轮中,我们逐个处理已经在队列中的结点,并将所有邻居添加到队列中。值得注意的是,新添加的节点不会立即遍历,而是在下一轮中处理。
结点的处理顺序与它们添加到队列的顺序是完全相同的顺序,即先进先出(FIFO
)。这就是我们在 BFS
中使用队列的原因。
模版
标准模版
int BFS(Node root, Node target) {
Queue<Node> queue = new LinkedList<>(); // 存储所有等待处理的节点
int step = 0; // 深度
// 初始化,将根节点存入队列
queue.offer(root);
// BFS
while (!queue.isEmpty()) {
step = step + 1;
// 迭代
int size = queue.size();
for (int i = 0; i < size; ++i) {
Node cur = queue.peek();
// 第一次找到该节点,将深度返回
if (cur == target)
return step
for (Node next : the neighbors of cur) {
queue.offer(next);
}
queue.remove();
}
}
return -1;
}
1.如代码所示,在每一轮中,队列中的结点是等待处理的结点。
2.在每个更外一层的while
循环之后,我们距离根结点更远一步。变量step
指示从根结点到我们正在访问的当前结点的距离。
避免重复访问节点
有时,确保我们 永远不会访问一个结点两次 很重要。否则,我们可能陷入无限循环。如果是这样,我们可以在上面的代码中添加一个哈希集来解决这个问题。这是修改后的伪代码:
int BFS(Node root, Node target) {
Queue<Node> queue = new LinkedList<>(); // 存储所有等待处理的节点
Set<Node> used = new HashSet<>(); // 存储已经访问了的节点
int step = 0; // 深度
// 初始化,将根节点存入队列和 HashSet 中
queue.offer(root);
used.add(root);
// BFS
while (!queue.isEmpty()) {
step = step + 1;
// 迭代
int size = queue.size();
for (int i = 0; i < size; ++i) {
Node cur = queue.peek();
// 第一次找到该节点,将深度返回
if (cur == target)
return step
for (Node next : the neighbors of cur) {
if(!used.contains(next)) {
queue.offer(next);
used.add(next);
}
}
queue.remove();
}
}
return -1;
}
有两种情况你不需要使用哈希集:
1.你完全确定没有循环,例如,在树遍历中;
2.你确实希望多次将结点添加到队列中。
例题
1.岛屿数量
难度:
Medium
题目描述
给定一个由 '1'
(陆地)和 '0'
(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。
示例 1:
输入:
11110
11010
11000
00000
输出: 1
示例 2:
输入:
11000
11000
00100
00011
输出: 3
解题思路及实现
这道题思路如下:线性扫描整个二维网格,如果一个结点包含 1
,则以其为根结点启动广度优先搜索。将其放入队列中,并将值设为 0
以标记访问过该结点。迭代地搜索队列中的每个结点,直到队列为空。
实现代码:
class Solution {
public int numIslands(char[][] grid) {
int result = 0;
if (grid == null || grid.length == 0 || grid[0].length == 0) return result;
int nr = grid.length;
int nc = grid[0].length;
for (int c = 0; c < nc; c++) {
for (int r = 0; r < nr; r++) {
// 如果是岛,返回结果加1,并将默认的坐标点标记为0
if (isIsland(grid, c, r)) {
result++;
makeVisited(grid, c, r);
// 开始广度优先搜索算法
final LinkedList<Integer> queue = new LinkedList<>();
queue.add(r * nc + c);
while (!queue.isEmpty()) {
final int removed = queue.remove();
final int removedCol = removed % nc;
final int removedRow = removed / nc;
// top
// left removed right
// bottom
// top
final int removeTop = removedRow - 1;
if (removeTop >= 0 && isIsland(grid, removedCol, removeTop)) {
queue.add(removeTop * nc + removedCol);
makeVisited(grid, removedCol, removeTop);
}
// left
final int removeLeft = removedCol - 1;
if (removeLeft >= 0 && isIsland(grid, removeLeft, removedRow)) {
queue.add(removedRow * nc + removeLeft);
makeVisited(grid, removeLeft, removedRow);
}
// bottom
final int removeBottom = removedRow + 1;
if (removeBottom <= nr - 1 && isIsland(grid, removedCol, removeBottom)) {
queue.add(removeBottom * nc + removedCol);
makeVisited(grid, removedCol, removeBottom);
}
// right
final int removeRight = removedCol + 1;
if (removeRight <= nc - 1 && isIsland(grid, removeRight, removedRow)) {
queue.add(removedRow * nc + removeRight);
makeVisited(grid, removeRight, removedRow);
}
}
}
}
}
return result;
}
// 返回二维数组对应的位置是否为岛
private boolean isIsland(char[][] grid, int c, int r) {
return grid[r][c] == '1';
}
// 标记二维数组对应的位置非岛
private void makeVisited(char[][] grid, int c, int r) {
grid[r][c] = '0';
}
}
2.打开转盘锁
难度:
Medium
题目描述
你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字:'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
。每个拨轮可以自由旋转:例如把 '9'
变为 '0'
,'0'
变为 '9'
。每次旋转都只能旋转一个拨轮的一位数字。
锁的初始数字为 '0000'
,一个代表四个拨轮的数字的字符串。
列表 deadends
包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。
字符串 target
代表可以解锁的数字,你需要给出最小的旋转次数,如果无论如何不能解锁,返回 -1
。
解题思路及实现
我们可以将 0000
到 9999
这 10000
状态看成图上的 10000
个节点,两个节点之间存在一条边,当且仅当这两个节点对应的状态只有 1 位不同,且不同的那位相差 1
(包括 0
和 9
也相差 1
的情况),并且这两个节点均不在数组 deadends
中。那么最终的答案即为 0000
到 target
的最短路径。
我们用广度优先搜索来找到最短路径,从 0000
开始搜索。对于每一个状态,它可以扩展到最多 8
个状态,即将它的第 i = 0, 1, 2, 3
位增加 1
或减少 1
,将这些状态中没有搜索过并且不在 deadends
中的状态全部加入到队列中,并继续进行搜索。注意 0000
本身有可能也在 deadends
中。
public class B752OpenLock {
public int openLock(String[] deadends, String target) {
Set<String> dead = new HashSet<>();
for (String d : deadends) dead.add(d);
// 用于bfs的队列
Queue<String> queue = new LinkedList<>();
queue.add("0000");
queue.add(null);
// 额外的一个哈希集,确保我们永远不会访问一个结点两次。
// 否则,我们可能陷入无限循环
Set<String> seen = new HashSet<>();
seen.add("0000");
int depth = 0;
while (!queue.isEmpty()) {
final String node = queue.remove();
if (node == null) {
// null 作为单次广度搜索的尾巴标记
depth++;
if (queue.peek() != null) {
// 这意味着单轮搜索结束,队列末尾继续追加一个null
// 当轮训到下一个null时,意味着下轮的搜索结束
queue.offer(null);
}
} else if (node.equals(target)) {
return depth;
} else if (!dead.contains(node)) { // 如果某个节点不是死亡数字
// 对于每一个状态,它可以扩展到最多 8 个状态,即将它的第 i = 0, 1, 2, 3 位增加 1 或减少 1
for (int i = 0; i < 4; i++) { // 四个位置
for (int j = -1; j <= 1; j += 2) { // 每个位置分别 -1 , + 1
// 这里用到一个小技巧避免对字符进行额外的负数判断
// 这里统一加10求余,值一定是期望的值,否则结果会返回负数
// 比如,当node.charAt(i) = '0', j = -1 时,这时候结果仍然是期望的9,而不是 -1
int y = ((node.charAt(i) - '0') + j + 10) % 10;
// 取得对应的邻居节点,比如 i = 1, j = 1 时, '0000' -> '0100'
String nei = node.substring(0, i) + String.valueOf(y) + node.substring(i + 1);
if (!seen.contains(nei)) {
seen.add(nei);
queue.offer(nei);
}
}
}
}
}
return -1;
}
}
3.完全平方数
难度:
Medium
题目描述
给定正整数 n
,找到若干个完全平方数(比如 1, 4, 9, 16, ...
)使得它们的和等于 n
。你需要让组成和的完全平方数的个数最少。
示例 1:
输入: n = 12
输出: 3
解释: 12 = 4 + 4 + 4.
示例 2:
输入: n = 13
输出: 2
解释: 13 = 4 + 9.
解题思路及实现
如下图,详细实现参考代码:
class Solution {
public int numSquares(int n) {
boolean[] visited = new boolean[n];
int step = 1;
Queue<Integer> queue = new LinkedList<>();
queue.add(n);
queue.add(null);
while (!queue.isEmpty()) {
Integer removed = queue.remove();
// bfs
for (int i = 1; ; i++) {
if (removed == null) {
step++;
if (queue.peek() != null) {
queue.offer(null);
}
break;
}
int nextValue = removed - (i * i);
if (nextValue == 0)
return step;
if (nextValue < 0) {
break;
}
// 关键点!
// 当再次出现时没有必要加入,因为在该节点的路径长度肯定不小于第一次出现的路径长
if (!visited[nextValue]) {
visited[nextValue] = true;
queue.add(nextValue);
}
}
}
return -1;
}
}
参考 & 感谢
文章绝大部分内容节选自LeetCode
,概述:
https://leetcode-cn.com/explore/learn/card/queue-stack/217/queue-and-bfs/869/
例题:
https://leetcode-cn.com/problems/number-of-islands
https://leetcode-cn.com/problems/open-the-lock
https://leetcode-cn.com/problems/perfect-squares
以上是关于队列和广度优先搜索的主要内容,如果未能解决你的问题,请参考以下文章