DFS BFS
- 一、队列 BFS
- DFS(深度优先搜索)和 BFS(广度优先搜索)
- [690. 员工的重要性](https://leetcode-cn.com/problems/employee-importance/)`
- 方法一:深度优先搜索
- 方法二:广度优先搜索
- [★200. 岛屿数量](https://leetcode-cn.com/problems/number-of-islands/)
- 方法一:深度优先搜索
- 方法二:广度优先搜索
- [419. 甲板上的战舰](https://leetcode-cn.com/problems/battleships-in-a-board/)
- [2049. 统计最高分的节点数目](https://leetcode-cn.com/problems/count-nodes-with-the-highest-score/)
- [1926. 迷宫中离入口最近的出口](https://leetcode.cn/problems/nearest-exit-from-entrance-in-maze/)
- [2658. 网格图中鱼的最大数目](https://leetcode.cn/problems/maximum-number-of-fish-in-a-grid/)
- [851. 喧闹和富有](https://leetcode-cn.com/problems/loud-and-rich/)
- [1034. 边界着色](https://leetcode-cn.com/problems/coloring-a-border/)
- [1036. 逃离大迷宫](https://leetcode-cn.com/problems/escape-a-large-maze/)
- [1210. 穿过迷宫的最少移动次数](https://leetcode.cn/problems/minimum-moves-to-reach-target-with-rotations/)
- [395. 至少有 K 个重复字符的最长子串](https://leetcode-cn.com/problems/longest-substring-with-at-least-k-repeating-characters/)
- [2596. 检查骑士巡视方案](https://leetcode.cn/problems/check-knight-tour-configuration/)
- 二、双端队列 BFS 又称 0-1 BFS
- [2290. 到达角落需要移除障碍物的最小数目](https://leetcode.cn/problems/minimum-obstacle-removal-to-reach-corner/)
- [★ 1368. 使网格图至少有一条有效路径的最小代价](https://leetcode.cn/problems/minimum-cost-to-make-at-least-one-valid-path-in-a-grid/)
- [1263. 推箱子](https://leetcode.cn/problems/minimum-moves-to-move-a-box-to-their-target-location/)
- 三、优先队列 BFS
- [2577. 在网格图中访问一个格子的最少时间](https://leetcode.cn/problems/minimum-time-to-visit-a-cell-in-a-grid/)
- 方法一:Dijkstra 算法
- [778. 水位上升的泳池中游泳](https://leetcode.cn/problems/swim-in-rising-water/)
- 四、多源 BFS
- [994. 腐烂的橘子](https://leetcode.cn/problems/rotting-oranges/)
- [1162. 地图分析](https://leetcode-cn.com/problems/as-far-from-land-as-possible/)
- [1765. 地图中的最高点](https://leetcode-cn.com/problems/map-of-highest-peak/)
- [2258. 逃离火灾](https://leetcode.cn/problems/escape-the-spreading-fire/)
- 方法:两次 BFS
- 总结
一、队列 BFS
BFS 全称是 Breadth First Search,中文名是宽度优先搜索,也叫 广度优先搜索。
Tree 的 BFS,要把 root 结点先入队,然后再一层一层的遍历。
图的 BFS,从源点出发,一圈一圈的访问结点。
DFS(深度优先搜索)和 BFS(广度优先搜索)
BFS 算法找到的路径是从起点开始的 最短 合法路径。即这条路径所包含的边数最小。
在 BFS 结束时,每个节点都是通过从起点到该点的最短路径 访问的。
图上火苗传播的过程:最开始只有起点着火了,在每一时刻,有火的节点都向它相邻的所有节点传播火苗。
void bfs() {
Queue<Integer> q = new LinkedList<>();
q.offer(s);
visited[s] = true;
while (!q.isEmpty()) {
u = q.poll();
for (int v : egge[u]) {
if (!visited[v]) {
q.offer(v);
visited[v] = true; //
}
}
}
}
用一个队列 Q 来记录要处理的节点,布尔数组 visited[] 来标记是否已经访问过某个节点。
开始的时候,将所有节点的 visited 值设为 false,表示没有访问过;然后把起点 s 放入队列 Q 中并将 visited[s] 设为 true。之后,每次从队列 Q 中取出队首的节点 u,然后把与 u 相邻的所有节点 v 标记为已访问过并放入队列 Q。
循环直至当队列 Q 为空,表示 BFS 结束。
在 BFS 的过程中,也可以记录一些额外的信息。d 数组用于记录起点到某个节点的 最短距离(要经过的最少边数),p 数组记录是从哪个节点走到当前节点的,即相当记录 父结点。
690. 员工的重要性`
方法一:深度优先搜索
class Solution {
Map<Integer, Employee> map = new HashMap();
public int getImportance(List<Employee> employees, int id) {
for (Employee employee : employees) {
map.put(employee.id, employee);
}
return dfs(id);
}
public int dfs(int id) {
Employee employee = map.get(id);
int total = employee.importance;
List<Integer> subordinates = employee.subordinates;
for (int subId : subordinates) {
total += dfs(subId);
}
return total;
}
}
方法二:广度优先搜索
class Solution {
public int getImportance(List<Employee> employees, int id) {
Map<Integer, Employee> map = new HashMap();
for (Employee employee : employees) {
map.put(employee.id, employee);
}
int total = 0;
Queue<Integer> q = new LinkedList();
q.offer(id);
while (!q.isEmpty()) {
int curId = q.poll();
Employee employee = map.get(curId);
total += employee.importance;
List<Integer> subordinates = employee.subordinates;
for (int subId : subordinates) {
q.offer(subId);
}
}
return total;
}
}
★200. 岛屿数量
方法一:深度优先搜索
class Solution {
int[] dir = {1,0,-1,0,1};
public int numIslands(char[][] grid) {
int m = grid.length, n = grid[0].length, count = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == '1') {
count++;
dfs(grid, i, j);
}
}
}
return count;
}
void dfs(char[][] grid, int i, int j) {
int m = grid.length, n = grid[0].length;
for (int k = 0; k < 4; k++) {
int x = i + dir[k], y = j + dir[k + 1];
if (x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == '1') {
grid[x][y] = '0'; // ★入队时标记,每个结点不会重复入队。
dfs(grid, x, y);
}
}
}
}
方法二:广度优先搜索
class Solution {
int[] dir = {1,0,-1,0,1};
public int numIslands(char[][] grid) {
int m = grid.length, n = grid[0].length, count = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == '1') {
count++;
bfs(grid, i, j);
}
}
}
return count;
}
void bfs(char[][] grid, int i, int j) {
int m = grid.length, n = grid[0].length;
Deque<int[]> q = new LinkedList();
q.add(new int[]{i, j});
while (!q.isEmpty()) {
int[] p = q.poll();
i = p[0]; j = p[1];
for (int k = 0; k < 4; k++) {
int x = i + dir[k], y = j + dir[k + 1];
if (x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == '1') {
grid[x][y] = '0'; // ★入队时标记,每个结点不会重复入队。
q.add(new int[]{x, y});
}
}
}
}
}
419. 甲板上的战舰
横排或竖排连续的 ‘x’ 为一个战舰,和经典题目[岛屿数量]相同。
class Solution {
int[] dir = {1,0,-1,0,1};
char[][] board;
int m, n;
public int countBattleships(char[][] board) {
m = board.length; n = board[0].length;
this.board = board;
int res = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (board[i][j] == 'X') {
res++;
// dfs(i, j);
bfs(i, j);
}
}
}
return res;
}
void dfs(int i, int j) {
for (int k = 0; k < 4; k++) {
int x = i + dir[k], y = j + dir[k + 1];
if (x >= 0 && y >= 0 && x < m && y < n && board[x][y] == 'X') {
board[x][y] = '.';
dfs(x, y);
}
}
}
void bfs(int i, int j) {
Deque<int[]> q = new LinkedList();
q.add(new int[]{i, j});
board[i][j] = '.';
while (!q.isEmpty()) {
i = q.peek()[0]; j = q.poll()[1];
for (int k = 0; k < 4; k++) {
int x = i + dir[k], y = j + dir[k + 1];
if (x >= 0 && y >= 0 && x < m && y < n && board[x][y] == 'X') {
board[x][y] = '.';
q.add(new int[]{x, y});
}
}
}
}
}
2049. 统计最高分的节点数目
class Solution {
List<Integer>[] children;
int n, cnt = 0;
long maxScore = 0;
public int countHighestScoreNodes(int[] parents) {
n = parents.length;
// 邻接表
children = new ArrayList[n];
for (int i = 0; i < n; i++) children[i] = new ArrayList<Integer>();
for (int i = 1; i < n; i++){
int p = parents[i];
children[p].add(i);
}
dfs(0);
return cnt;
}
// 计算分数,返回子树的大小。
int dfs(int node){
long score = 1;
int size = n - 1; // n 个结点 - 当前结点,除去当前其它的所有结点数
for (int c : children[node]){
int t = dfs(c);
score *= t;
size -= t;
}
if (node != 0) score *= size;
if (score == maxScore) cnt ++;
else if (score > maxScore) {
maxScore = score;
cnt = 1;
}
return n - size; // 子结点数
}
}
1926. 迷宫中离入口最近的出口
class Solution {
private static int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
public int nearestExit(char[][] maze, int[] entrance) {
int m = maze.length, n = maze[0].length;
boolean[][] vis = new boolean[m][n];
vis[entrance[0]][entrance[1]] = true; // 防止重复入队
Deque<int[]> q = new LinkedList();
q.offer(entrance);
int step = 0; // 层
while(!q.isEmpty()) {
int w = q.size();
while(w-- > 0){
int[] t = q.poll();
for(int[] d : dirs) {
int x = t[0] + d[0], y = t[1] + d[1];
if(x < 0 || x >= m || y < 0 || y >= n) {
if (step == 0) continue;
return step;
}
if(!vis[x][y] && maze[x][y] == '.') {
q.offer(new int[]{x, y});
vis[x][y] = true; // ★入队时标记,每个结点不会重复入队。
}
}
}
step++;
}
return -1;
}
}
2658. 网格图中鱼的最大数目
DFS 统计每个包含正数的连通块的元素和,最大值即为答案。
class Solution {
private final static int[][] DIRS = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
public int findMaxFish(int[][] grid) {
int m = grid.length, n = grid[0].length, ans = 0;
for (int i = 0; i < m; ++i)
for (int j = 0; j < n; ++j)
ans = Math.max(ans, dfs(grid, i, j));
return ans;
}
private int dfs(int[][] grid, int x, int y) {
int m = grid.length, n = grid[0].length;
if (x < 0 || x >= m || y < 0 || y >= n || grid[x][y] == 0)
return 0;
int sum = grid[x][y];
grid[x][y] = 0; // 标记成访问过
for (var d : DIRS) // 四方向移动
sum += dfs(grid, x + d[0], y + d[1]);
return sum;
}
}
class Solution {
int[] dirs = {1,0,-1,0,1};
public int findMaxFish(int[][] grid) {
int m = grid.length, n = grid[0].length;
int res = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 0) continue;
res = Math.max(res, bfs(grid, i, j));
}
}
return res;
}
int bfs(int[][] grid, int i, int j) {
int res = 0;
Deque<int[]> q = new LinkedList();
q.offer(new int[]{i, j});
while (!q.isEmpty()) {
i = q.peek()[0]; j = q.poll()[1];
res += grid[i][j];
grid[i][j] = 0;
for (int k = 0; k < 4; k ++) {
int x = i + dirs[k], y = j + dirs[k + 1];
if (x < 0 || x >= grid.length || y < 0 || y >= grid[0].length || grid[x][y] == 0) continue;
q.offer(new int[]{x, y});
}
}
return res;
}
}
851. 喧闹和富有
搜索比自己有钱的最安静的一个
class Solution {
int[] res, quiet;
Map<Integer, List<Integer>> map = new HashMap();
public int[] loudAndRich(int[][] richer, int[] quiet) {
int n = quiet.length;
this.quiet = quiet;
res = new int[n];
Arrays.fill(res, -1);
// 记录直接比自己有钱的人
for (int[] r : richer) {
map.computeIfAbsent(r[1], v -> new ArrayList()).add(r[0]);
}
for (int i = 0; i < n; i++) {
dfs(i);
}
return res;
}
void dfs(int x) {
if (res[x] != -1) return; // 已经搜索过
res[x] = x;
if (map.get(x) == null) return;
// 搜索比自己有钱的最安静的一个
for (int other : map.get(x)) {
dfs(other);
if (quiet[res[other]] < quiet[res[x]])
res[x] = res[other];
}
}
}
若图为有向无环图,则可进行 拓扑排序。
拓扑排序 Topological sorting
class Solution {
public int[] loudAndRich(int[][] richer, int[] quiet) {
int n = quiet.length;
List<Integer>[] g = new List[n];
for (int i = 0; i < n; ++i)
g[i] = new ArrayList<Integer>();
int[] inDeg = new int[n];
for (int[] r : richer) {
g[r[0]].add(r[1]);
++inDeg[r[1]];
}
int[] res = new int[n];
Arrays.setAll(res, i -> i);
Queue<Integer> q = new ArrayDeque<Integer>();
for (int i = 0; i < n; ++i)
if (inDeg[i] == 0) q.offer(i);
while (!q.isEmpty()) {
int x = q.poll();
for (int y : g[x]) {
if (quiet[res[x]] < quiet[res[y]])
res[y] = res[x]; // 更新 x 的邻居的答案
if (--inDeg[y] == 0) q.offer(y);
}
}
return res;
}
}
一遍一遍更新走到更新完
class Solution {
public int[] loudAndRich(int[][] richer, int[] quiet) {
int n = quiet.length;
int[] res = new int[n];
Arrays.setAll(res, i -> i);
boolean flag = true;
while (flag) {
flag = false;
for (int[] r : richer) {
if (quiet[res[r[0]]] < quiet[res[r[1]]]){
res[r[1]] = res[r[0]];
flag = true;
}
}
}
return res;
}
}
1034. 边界着色
数字代表相邻格子颜色相同的数量,只有中间的 4 不是边界,其他有数字的格子都为边界。有点扫雷游戏的感觉。
class Solution {
int[][] grid;
boolean[][] vis;
int originalColor, color, m, n;
int[] dir = {1, 0, -1, 0, 1};
public int[][] colorBorder(int[][] grid, int row, int col, int color) {
this.grid = grid; this.color = color;
originalColor = grid[row][col];
if (originalColor == color) return grid;
m = grid.length; n = grid[0].length;
vis = new boolean[m][n];
vis[row][col] = true;
// bfs(row, col);
dfs(row, col);
return grid;
}
void bfs(int row, int col) {
Deque<int[]> q = new LinkedList();
q.add(new int[]{row, col});
while (!q.isEmpty()) {
int i = q.peek()[0], j = q.poll()[1];
boolean isBorder = false;
for (int k = 0; k < 4; k++) {
int x = i + dir[k], y = j + dir[k + 1];
if (x >= 0 && x < m && y >= 0 && y < n){
if (!vis[x][y]) {
if (grid[x][y] == originalColor) {
q.add(new int[]{x, y});
vis[x][y] = true;
} else isBorder = true; // 不同色
}
} else isBorder = true; // 越界
// 以下代码有问题,因为已经修改了 grid
// if (x < 0 || x >= m || y < 0 || y >= n || grid[x][y] != originalColor) isBorder = true;
// else if (!vis[x][y]){
// vis[x][y] = true;
// q.add(new int[]{x, y});
// }
}
if (isBorder) grid[i][j] = color;
}
}
private void dfs(int i, int j) {
boolean isBorder = false;
for (int k = 0; k < 4; k++) {
int x = i + dir[k], y = j + dir[k + 1];
if (x < 0 || x >= m || y < 0 || y >= n ) isBorder = true;
else if (!vis[x][y]){
if (grid[x][y] == originalColor) {
vis[x][y] = true;
dfs(x, y);
} else isBorder = true;
}
}
if (isBorder) grid[i][j] = color;
}
}
1036. 逃离大迷宫
从 source 出发向四周呈菱形状扩展,可以走出 n 步以外。说明 source 没有被局部包围,如果 target 同样没有被局部包围返回真。也就是最大搜索范围是 n <= 200 步。
class Solution:
def isEscapePossible(self, blocked: List[List[int]], source: List[int], target: List[int]) -> bool:
def bfs(s, t):
# vis, q = set(), [s]
vis, q = defaultdict(set), [s]
while q:
x, y = q.pop()
for a, b in ((x, y + 1), (x, y - 1), (x + 1, y), (x - 1, y)):
# if 0 <= a < 1000000 and 0 <= b < 1000000 and [a, b] not in blocked and (a, b) not in vis:
if 0 <= a < 1000000 and 0 <= b < 1000000 and b not in d[a] and b not in vis[a]:
if [a, b] == t or abs(a - s[0]) + abs(b - s[1]) > n:
return True
q.append([a, b])
# vis.add((a, b))
vis[a].add(b)
return False
# n = len(blocked)
d, n = defaultdict(set), len(blocked)
for x, y in blocked: d[x].add(y)
return bfs(source, target) and bfs(target, source)
1210. 穿过迷宫的最少移动次数
(x, y, s) 表示蛇尾在 (x, y),s 表示状态,0 水平/1竖直状态。这样初始位置为 (0, 0, 0),最终位置为 (n − 1, n − 2, 0)。
- 向下移动:x 增加 1,y 和 s 不变。用三元组 (1,0,0) 表示。
- 向右移动:y 增加 1,x 和 s 不变。用三元组 (0,1,0) 表示。
- 旋转:s 切换,即 0 变为 1,1 变为 0;x 和 y 不变。用三元组 (0,0,1) 表示。
判断:
- 移动后蛇身不能出界;
- 移动后蛇身不能在障碍物上;
- 对于旋转,还需要保证 (x + 1, y + 1) 没有障碍物。
蛇尾 (x, y),蛇头: (x, y + 1) // s = 0;(x + 1, y) // s = 1。
合并为一个公式表示蛇头:
(x + s, y + (s ⊕ 1)) // 其中 ⊕ 表示异或运算。
class Solution {
int[][] DIRS = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}};
public int minimumMoves(int[][] grid) {
int n = grid.length;
var vis = new boolean[n][n][2];
var q = new ArrayList<int[]>();
vis[0][0][0] = true;
q.add(new int[]{0, 0, 0}); // 初始位置
for (int step = 1; !q.isEmpty(); ++step) {
var tmp = q;
q = new ArrayList<>();
for (var t : tmp) {
for (var d : DIRS) {
int x = t[0] + d[0], y = t[1] + d[1], s = t[2] ^ d[2];
int a = x + s, b = y + (s ^ 1); // 蛇头
if (a < n && b < n && !vis[x][y][s] && grid[x][y] == 0 && grid[a][b] == 0 && (d[2] == 0 || grid[x + 1][y + 1] == 0)) {
if (x == n - 1 && y == n - 2) return step; // 此时蛇头一定在 (n-1,n-1)
vis[x][y][s] = true;
q.add(new int[]{x, y, s});
}
}
}
}
return -1;
}
}
395. 至少有 K 个重复字符的最长子串
class Solution {
public int longestSubstring(String s, int k) {
if (s.length() < k) return 0;
int[] cnt = new int[26];
for (char c : s.toCharArray()) cnt[c-'a']++;
for (int i = 0; i < 26; i++){
if (cnt[i] > 0 && cnt[i] < k){
int ans = 0;
String regex = String.valueOf((char)(i+'a'));
for (String t : s.split(regex)){
ans = Math.max(ans, longestSubstring(t, k));
}
return ans;
}
}
return s.length();
}
}
2596. 检查骑士巡视方案
class Solution {
int[][] dirs = {{-2, 1}, {-2, -1}, {2, 1}, {2, -1}, {-1, 2}, {-1, -2}, {1, 2}, {1, -2}};
public boolean checkValidGrid(int[][] grid) {
int n = grid.length;
if (grid[0][0] != 0) return false;
int count = 1, i = 0, j = 0;
while (count < n * n) {
boolean flag = true;
for (int[] d : dirs) {
int x = i + d[0], y = j +d[1];
if (x < 0 || x >= n || y < 0 || y >= n) continue;
if (grid[x][y] == count) {
flag = false;
i = x;
j = y;
count++;
break;
}
}
if (flag) return false;
}
return true;
}
}
二、双端队列 BFS 又称 0-1 BFS
在一个边权为 0/1 的图上求最短路
适用范围
边权值为可能有,也可能没有(由于 BFS 适用于权值为 1 的图,所以一般权值是 0 或 1),或者能够转化为这种边权值的最短路问题。
例如在走迷宫问题中,你可以花 1 个金币走 5 步,也可以不花金币走 1 步,这就可以用 0-1 BFS 解决。
实现
一般情况下,把没有权值的边扩展到的点放到队首,有权值的边扩展到的点放到队尾。这样即可保证像普通 BFS 一样整个队列队首到队尾权值单调不下降。
2290. 到达角落需要移除障碍物的最小数目
class Solution {
static final int[] dirs = {1, 0, -1, 0, 1};
int INF = Integer.MAX_VALUE;
public int minimumObstacles(int[][] grid) {
int m = grid.length, n = grid[0].length;
var dis = new int[m][n]; // 也可以附加到 q 中,但需要 vis 数组。
for (int[] d : dis) Arrays.fill(d, INF);
dis[0][0] = 0;
var q = new ArrayDeque<int[]>();
q.add(new int[]{0, 0});
while (!q.isEmpty()) {
// 出队时一定是最先到达 p 点的,用它更新四周。
int i = q.peek()[0], j = q.poll()[1];
if (i == m - 1 && j == n - 1) return dis[i][j];
for (int k = 0; k < 4; k++) {
int x = i + dirs[k], y = j + dirs[k + 1];
// 更新四周,dis 可能已经记录过。
if (x >= 0 && y >= 0 && x < m && y < n && dis[i][j] + grid[x][y] < dis[x][y]) {
dis[x][y] = dis[i][j] + grid[x][y];
// 相邻没有障碍物的点,代价为 0. 优先处理。
if (grid[x][y] == 0) q.addFirst(new int[]{x, y});
else q.add(new int[]{x, y});
}
}
}
return 0; // 不会执行到
}
}
★ 1368. 使网格图至少有一条有效路径的最小代价
如果当前点是正好由上一个点直接通过格子代表的方向过来的话,那么两点距离为 0,否则两点距离为 1,求起点 (0, 0) 到终点 (m - 1, n - 1) 的最短距离。
★ 假设 2 出队后,5 先入队(尾),3 后入队(首),然后 6 出队,9 先入队(尾),5 第二次入队(首)。5 出队,vis 标记为真,后面 5 再次出队时跳过,也可以通过距离过滤掉。
★ 只有点第一次出队后才用它更新四周。
0-1 BFS 和普通的 BFS 的区别:
- 普通的 BFS 一般使用 队列
- 0-1 BFS 队列和栈的结合
class Solution {
// ★注意顺序
int[][] dirs = {{0, 1},{0, -1}, {1, 0}, {-1, 0}};
int INF = Integer.MAX_VALUE;
public int minCost(int[][] grid) {
int m = grid.length, n = grid[0].length;
int[][] dis = new int[m][n];
for (int[] d : dis)
Arrays.fill(d, INF);
dis[0][0] = 0;
var q = new ArrayDeque<int[]>();
q.add(new int[]{0, 0});
while (!q.isEmpty()) {
int[] p = q.poll();
int i = p[0], j = p[1];
// if (i == m - 1 && j == n - 1) return dis[i][j];
for(int k = 0; k < 4; k++){
int x = i + dirs[k][0], y = j + dirs[k][1];
int cost = grid[i][j] == k + 1 ? 0 : 1;
// 通过距离剪枝
if (x < 0 || x >= m || y < 0 || y >= n || dis[i][j] + cost >= dis[x][y]) continue;
dis[x][y] = dis[i][j] + cost; // 更新
if (grid[i][j] == k + 1) q.addFirst(new int[]{x, y});
else q.add(new int[]{x, y});
}
}
return dis[m - 1][n - 1];
}
}
class Solution {
// 注意顺序
int[][] dir = new int[][]{{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
public int minCost(int[][] grid) {
int m = grid.length, n = grid[0].length;
boolean[][] vis = new boolean[m][n];
Deque<int[]> q = new LinkedList<>();
q.add(new int[]{0, 0, 0}); // 附加距离
while (!q.isEmpty()) {
int[] p = q.poll();
int i = p[0], j = p[1], cost = p[2];
if (i == m - 1 && j == n - 1) return cost;
if (vis[i][j]) continue; // 说明已经出过队
vis[i][j] = true; // ★出队后标记,用该结点更新其周围结点。结点可能多次入队。
int val = grid[i][j];
for (int k = 0; k < 4; k++) {
int x = i + dir[k][0];
int y = j + dir[k][1];
if (x < 0 || x >= m || y < 0 || y >= n || vis[x][y]) continue;
if (k == val - 1) q.addFirst(new int[]{x, y, cost});
else q.add(new int[]{x, y, cost + 1});
}
}
return 0;
}
}
1263. 推箱子
将箱子推到目标位置的最小推动次数与箱子和玩家的位置相关。把箱子和玩家的位置当成一个状态,那么状态的转移主要由玩家向上、下、左、右四个方向移动触发(如果玩家移动后的位置与箱子位置重叠,那么箱子也相应的作出同样的移动,即一次 “推动”)。把状态看成有向图的节点,状态的转移看成有向图的边,对应的边长与是否推动箱子有关(推动箱子时,边长为 1,否则为 0)。将箱子推到目标位置对应多个状态,这些状态中箱子位置等于目标位置。因此问题可以转化为:给定一个有向图,边长为 0 或 1,求某一节点到符合条件的任一节点的最短路径。
出队标记
class Solution {
int[] dir = {1, 0, -1, 0, 1};
public int minPushBox(char[][] grid) {
int m = grid.length, n = grid[0].length;
Deque<int[]> q = new ArrayDeque();
boolean[][][][] vis = new boolean[m][n][m][n];
int tx = 0, ty = 0, sx = 0, sy = 0, bx = 0, by = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 'S') {
sx = i;
sy = j;
} else if (grid[i][j] == 'B') {
bx = i;
by = j;
}
}
}
q.add(new int[]{sx, sy, bx, by, 0});
while (!q.isEmpty()) {
int[] p = q.poll();
int si = p[0], sj = p[1], bi = p[2], bj = p[3], d = p[4];
if (grid[bi][bj] == 'T') return d;
if (vis[si][sj][bi][bj]) continue;
vis[si][sj][bi][bj] = true;
for (int k = 0; k < 4; k++) {
sx = si + dir[k];
sy = sj + dir[k + 1];
if (!check(sx, sy, grid)) continue;
if (sx == bi && sy == bj) {
bx = bi + dir[k];
by = bj + dir[k + 1];
if (!check(bx, by, grid) || vis[sx][sy][bx][by]) continue;
q.add(new int[]{sx, sy, bx, by, d + 1});
} else if (!vis[sx][sy][bi][bj]) {
q.addFirst(new int[]{sx, sy, bi, bj, d});
}
}
}
return -1;
}
boolean check(int x, int y, char[][] grid) {
return x >= 0 && x < grid.length && y >= 0 && y < grid[0].length && grid[x][y] != '#';
}
}
入队标记
class Solution {
public int minPushBox(char[][] grid) {
int[] dirs = {-1, 0, 1, 0, -1};
int m = grid.length, n = grid[0].length;
int si = 0, sj = 0, bi = 0, bj = 0;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j] == 'S') {
si = i; sj = j;
} else if (grid[i][j] == 'B') {
bi = i; bj = j;
}
}
}
Deque<int[]> q = new ArrayDeque<>();
boolean[][][][] vis = new boolean[m][n][m][n];
q.offer(new int[]{si, sj, bi, bj, 0});
vis[si][sj][bi][bj] = true;
while (!q.isEmpty()) {
var p = q.poll();
int d = p[4];
si = p[0]; sj = p[1]; bi = p[2]; bj = p[3];
if (grid[bi][bj] == 'T') return d;
for (int k = 0; k < 4; ++k) {
int sx = si + dirs[k], sy = sj + dirs[k + 1];
if (!check(sx, sy, grid)) continue;
// 人箱重合
if (sx == bi && sy == bj) {
int bx = bi + dirs[k], by = bj + dirs[k + 1];
// 同向推箱子
if (!check(bx, by, grid) || vis[sx][sy][bx][by]) continue;
vis[sx][sy][bx][by] = true;
// 箱子动了,末尾加
q.offer(new int[]{sx, sy, bx, by, d + 1});
} else if (!vis[sx][sy][bi][bj]) {
vis[sx][sy][bi][bj] = true;
// 箱子没动,头部加
q.offerFirst(new int[]{sx, sy, bi, bj, d});
}
}
}
return -1;
}
private boolean check(int i, int j, char[][] grid) {
return i >= 0 && i < grid.length && j >= 0 && j < grid[0].length && grid[i][j] != '#';
}
}
三、优先队列 BFS
在基于优先队列的 BFS 中,每次从队首取出代价最小的结点进行进一步搜索。
每个结点可能会被入队多次,每次入队的代价不同。当该结点第一次从优先队列中取出,以后便无需再在该结点进行搜索,直接忽略即可。所以,优先队列的 BFS 当中,每个结点只会被处理一次。
这怎么听起来这么像堆优化的 Dijkstra 算法呢?事实上,堆优化 Dijkstra 就是优先队列 BFS。
图 与 Tree 的 BFS 区别:
1、tree 只有 1 个 root,而图可以是多源的,所以首先需要把多个源点加入队列。
2、tree 是有向的因此不需要标志是否访问过,而对于无向图来说,必须得标志是否访问过!防止某个节点多次入队。
2577. 在网格图中访问一个格子的最少时间
方法一:Dijkstra 算法
class Solution {
int[] dirs = {1, 0, -1, 0, 1};
public int minimumTime(int[][] grid) {
// 如果第一步走不出去返回 -1
if (grid[0][1] > 1 && grid[1][0] > 1) return -1;
int m = grid.length, n = grid[0].length;
boolean[][] vis = new boolean[m][n];
vis[0][0] = true; // 该题可以入队标记,也可以出队标记,前者更优。
// 因为有了约束条件,边权重不再是1,所以不能用队列,每次用最小值更新四周,因为它是确定的,不可能更小。
var q = new PriorityQueue<int[]>((a, b) -> a[2] - b[2]);
q.add(new int[]{0, 0, 0});
while (!q.isEmpty()){
int[] p = q.poll();
int i = p[0], j = p[1], w = p[2];
if (i == m - 1 && j == n - 1) return w;
w++; // 加一步
for (int k = 0; k < 4; k++){
int x = i + dirs[k], y = j + dirs[k + 1];
if (x < 0 || x >= m || y < 0 || y >= n || vis[x][y]) continue;
int v = grid[x][y];
// 不够时间,来回走补齐
if (w < v) {
if ((v - w) % 2 == 1) v++;
} else v = w;
q.add(new int[]{x, y, v});
vis[x][y] = true;
}
}
return -3;
}
}
778. 水位上升的泳池中游泳
并查集(Union-Find)
四、多源 BFS
994. 腐烂的橘子
class Solution {
int[] dirs = {1, 0, -1, 0, 1};
public int orangesRotting(int[][] grid) {
int time = 0, count = 0, m = grid.length, n = grid[0].length;
Queue<int[]> q = new LinkedList<>();
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
if (grid[i][j] == 2) q.add(new int[]{i, j, 0});
else if (grid[i][j] == 1) count++;
if (count == 0) return 0;
while (!q.isEmpty()) {
int i = q.peek()[0], j = q.peek()[1];
time = q.poll()[2];
for (int k = 0; k < 4; k++) {
int x = i + dirs[k], y = j + dirs[k + 1];
if (x >= 0 && y >= 0 && x < m && y < n && grid[x][y] == 1) {
grid[x][y] = 2;
q.add(new int[]{x, y, time + 1});
count--;
}
}
}
return count == 0 ? time : -1;
}
}
1162. 地图分析
由所有的陆地出发一圈一圈的标记海洋,最后被标记的一定是距离最大的。
class Solution {
int[] dir = {1,0,-1,0,1};
public int maxDistance(int[][] grid) {
int n = grid.length, manhanttan = -1;
boolean[][] vis = new boolean[n][n];
Deque<int[]> q = new LinkedList();
// 收集所有陆地
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
if (grid[i][j] == 1) {
q.add(new int[]{i, j});
vis[i][j] = true;
}
if (q.size() == n * n || q.isEmpty()) return -1;
// 一圈一圈扩展海洋
while (!q.isEmpty()) {
manhanttan++;
int m = q.size();
for (int w = 0; w < m; w++) {
int i = q.peek()[0], j = q.poll()[1];
for (int k = 0; k < 4; k++) {
int x = i + dir[k], y = j + dir[k + 1];
if (x >= 0 && y >= 0 && x < n && y < n && !vis[x][y]) {
q.offer(new int[]{x, y});
vis[x][y] = true;
}
}
}
}
return manhanttan == 0 ? -1 : manhanttan;
}
}
1765. 地图中的最高点
记录下所有水域的位置,然后从水域出发,广度优先搜索,计算出所有陆地格子的高度,即从每个水域格子向四周进行 BFS,每拓展一圈高度 + 1。
class Solution {
int[] dir = {1,0,-1,0,1};
public int[][] highestPeak(int[][] isWater) {
int m = isWater.length, n = isWater[0].length;
int[][] res = new int[m][n];
// 充当 vis
for (int[] row : res) Arrays.fill(row, -1);
Deque<int[]> q = new LinkedList();
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
if (isWater[i][j] == 1){
q.add(new int[]{i, j});
res[i][j] = 0; // 水域
}
while (!q.isEmpty()) {
int i = q.peek()[0], j = q.poll()[1];
for (int k = 0; k < 4; k++) {
int x = i + dir[k], y = j + dir[k + 1];
if (x >= 0 && y >= 0 && x < m && y < n && res[x][y] == -1) {
res[x][y] = res[i][j] + 1;
q.add(new int[]{x, y});
}
}
}
return res;
}
}
2258. 逃离火灾
方法:两次 BFS
通过 BFS 计算人到每个格子的最短时间 manTime,以及火到每个格子的最短时间 fireTime。
如果 manTime[m − 1][n − 1] < 0,人无法到达终点,返回 -1。
如果 fireTime[m − 1][n − 1] < 0,火无法到达终点,返回 109 。
记 res = fireTime[m − 1][n − 1] − manTime[m − 1][n − 1]。
如果 res < 0,说明火比人先到终点,返回 -1。
如果 res > 0,说明人比火先到终点。注意不会出现中途火把人烧到的情况,如果出现,那么火可以沿着人走的最短路到达终点,不会出现人比火先到的情况,与实际矛盾。
如果火和人是从不同方向到达终点的(左侧和上侧),那么答案可以是 res,即人可以等待 res 时间,最终与火同时到达终点。
如果火和人是从同一方向到达终点的,也就意味着火一直跟在人的后面,那么在中途不能出现人火重合的情况,所以答案应该是 res − 1。
class Solution {
int[] dir = {1, 0, -1, 0, 1};
int inf = 0x3f3f3f3f, m, n;
public int maximumMinutes(int[][] grid) {
m = grid.length; n = grid[0].length;
int[][] time = new int[m][n];
Deque<int[]> q = new LinkedList();
// 单源初始化 人到达安全屋的最短时间
for (int[] t : time) Arrays.fill(t, inf);
time[0][0] = 0;
q.add(new int[]{0, 0});
dfs(grid, q, time);
int people = time[m - 1][n - 1];
int upt = time[m - 2][n - 1], lpt = time[m - 1][n - 2];
if (people == inf) return -1; // 人无法到安全屋
// 多源初始化 火到达安全屋的最短时间
for (int[] t : time) Arrays.fill(t, inf);
q.clear();
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
if (grid[i][j] == 1) {
q.add(new int[]{i, j});
time[i][j] = 0;
}
dfs(grid, q, time);
// 记录最后三个单元格 安全屋 上 左
int fire = time[m - 1][n - 1];
int uft = time[m - 2][n - 1], lft = time[m - 1][n - 2];
if (fire == inf) return 1000000000; // 火无法到安全屋
int res = fire - people;
if (res < 0) return -1; // 火比人先到终点
// 上、左只要有一不通,或者火和人同时到达上、左,火只能跟随人到达安全屋。
// 火只会跟在人的后面,在到达终点前,人和火不能重合
if (uft == inf || lft == inf || uft - upt == res && lft - lpt == res) res--;
return res; // 人和火可以从不同方向同时到终点
}
void dfs(int[][] grid, Deque<int[]> q, int[][] time) {
while (!q.isEmpty()) {
int i = q.peek()[0], j = q.poll()[1];
for (int k = 0; k < 4; k++){
int x = i + dir[k], y = j + dir[k + 1];
if (x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == 0 && time[x][y] > time[i][j] + 1) {
time[x][y] = time[i][j] + 1;
q.add(new int[]{x, y});
}
}
}
}
}
总结
权重相同时,使用队列;
边权值为 0/1 时,使用双端队列,值为 1 队尾排队,值为 0 队首插队;
边权值(非负)多种情况时,使用优先队列,优先解决权值最小的边,因为它是最短的,确定的,以此更新四周。
第一种情况入队时标记,权重为 1 即距离相等;后两种情况,可能多次入队,出队时标记,出队时距离最短用它更新四周。
处理方式
- 队附加距离 + vis 数组
- 队 + dis 数组 通过距离过滤