关键思想
宽度优先遍历基本内容
Bfs 的特点是逐层扩散,从源头点到目标点扩散了几层,最短路就是多少
Bfs 可以使用的特征是任意两个节点之间的相互距离相同(无向图)
Bfs 开始时,可以是单个源头、也可以是多个源头
Bfs 频繁使用队列,形式可以是单点弹出或者整层弹出
Bfs 进行时,进入队列的节点需要标记状态,防止同一个节点重复进出队列
Bfs 进行时,可能会包含剪枝策略的设计
Bfs 是一个理解难度很低的算法,难点在于节点如何找到路、路的展开和剪枝设计
01 bfs
01bfs,适用于图中所有边的权重只有0和1两种值,求源点到目标点的最短距离 时间复杂度为 O(节点数量+边的数量),为什么不能用传统bfs? 流程如下:
1、distance[i]表示从源点到i点的最短距离,初始时所有点的distance设置为无穷大
2、源点进入双端队列,distance[源点]=0
3、双端队列 头部弹出 x,
A,如果x是目标点,返回distance[x]表示源点到目标点的最短距离
B,考察从x出发的每一条边,假设某边去y点,边权为w
1)如果 distance[y] > distance[x] + w,处理该边;否则忽略该边
2)处理时,更新distance[y] = distance[x] + w 如果w0,y从头部进入双端队列;如果w1,y从尾部进入双端队列
3)考察完x出发的所有边之后,重复步骤3
4,双端队列为空停止
模版代码
普通 bfs
class Solution {
public static int MAXM=101;
public static int MAXN=101;
public static int[][]queue=new int[MAXM*MAXN][2];
public static boolean[][]visit=new boolean[MAXN][MAXM];
public static int l,r;
// (x,y) i来到0位置 : x + move[i], y + move[i+1] -> x - 1, y // (x,y) i来到1位置 : x + move[i], y + move[i+1] -> x, y + 1 // (x,y) i来到2位置 : x + move[i], y + move[i+1] -> x + 1, y // (x,y) i来到3位置 : x + move[i], y + move[i+1] -> x, y - 1 public static int[]move=new int[]{-1,0,1,0,-1};
public int maxDistance(int[][] grid) {
int n=grid.length;
int m=grid[0].length;
l=0;
r=0;
int seas=0;
//初始化,寻找所有的陆地并标识,同时标识所有的海洋
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(grid[i][j]==1){
//标识并收集
visit[i][j]=true;
queue[r][0]=i;
queue[r++][1]=j;
}else{
visit[i][j]=false;
seas++;
}
}
}
//如果全是陆地或者海洋,直接按照题意返回-1
if(seas==0 || seas==n*m){
return -1;
}
//接下来开始bfs
int level=0;
int size=0;
while(l<r){
level++;
size=r-l;
for(int i=0,x,y,nx,ny;i<size;i++){
x=queue[l][0];
y=queue[l++][1];
//找到每块陆地旁边的海洋
for(int j=0;j<4;j++){
//上下左右分别遍历
nx=x+move[j];
ny=y+move[j+1];
if(nx>=0 && nx<n && ny>=0 && ny<m && !visit[nx][ny]){
visit[nx][ny]=true;
queue[r][0]=nx;
queue[r++][1]=ny;
}
}
}
}
return level-1;
}
}
01 bfs
/**
* @program: ZuoChengxunAlgorithmClass
* @ClassName Code03_MinimumObstacleRemovalToReachCorner
* @description:到达角落需要移除障碍物的最小数目
* 给你一个下标从 0 开始的二维整数数组 grid ,数组大小为 m x n
* 每个单元格都是两个值之一:
* 0 表示一个 空 单元格,
* 1 表示一个可以移除的 障碍物
* 你可以向上、下、左、右移动,从一个空单元格移动到另一个空单元格。
* 现在你需要从左上角 (0, 0) 移动到右下角 (m - 1, n - 1)
* 返回需要移除的障碍物的最小数目
* 测试链接 : https://leetcode.cn/problems/minimum-obstacle-removal-to-reach-corner/
* @author: zs宝
* @create: 2025-07-17 09:34
* @Version 1.0
**/
package main.java.class062;
import java.util.ArrayDeque;
public class Code03_MinimumObstacleRemovalToReachCorner {
class Solution {
public int minimumObstacles(int[][] grid) {
//四个方向前后左右的移动数组
int[] move ={ -1, 0, 1, 0, -1 };
int m = grid.length;
int n = grid[0].length;
//出发点到每个点的距离数组
int[][] distance = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
distance[i][j] = Integer.MAX_VALUE;
}
}
//创建一个队列,这个队列中存放层次遍历的点
ArrayDeque<int[]> queue = new ArrayDeque<>();
//初始时存放出发点
queue.addFirst(new int[] { 0, 0 });
distance[0][0] = 0;
//接下来就是01bfs的重点
/**
1,distance[i]表示从源点到i点的最短距离,初始时所有点的distance设置为无穷大
2,源点进入双端队列,distance[源点]=0
3,双端队列 头部弹出 x,
A,如果x是目标点,返回distance[x]表示源点到目标点的最短距离
B,考察从x出发的每一条边,假设某边去y点,边权为w
1)如果 distance[y] > distance[x] + w,处理该边;否则忽略该边
2)处理时,更新distance[y] = distance[x] + w
如果w==0,y从头部进入双端队列;如果w==1,y从尾部进入双端队列
3)考察完x出发的所有边之后,重复步骤3
4,双端队列为空停止
*/
while (!queue.isEmpty()) {
//头部弹出
int[] record = queue.pollFirst();
int x = record[0];
int y = record[1];
//A,如果x是目标点,返回distance[x]表示源点到目标点的最短距离
if (x == m - 1 && y == n - 1) {
return distance[x][y];
}
/**
B,考察从x出发的每一条边,假设某边去y点,边权为w
1)如果 distance[y] > distance[x] + w,处理该边;否则忽略该边
2)处理时,更新distance[y] = distance[x] + w
如果w==0,y从头部进入双端队列;如果w==1,y从尾部进入双端队列
3)考察完x出发的所有边之后,重复步骤3
*/ for (int i = 0; i < 4; i++) {
int nx = x + move[i];
int ny = y + move[i + 1];
if (nx >= 0 && nx < m && ny >= 0 && ny < n && distance[nx][ny] > distance[x][y] + grid[nx][ny]) {
distance[nx][ny] = distance[x][y] + grid[nx][ny];
if (grid[nx][ny] == 0) {
queue.addFirst(new int[] { nx, ny });
} else {
queue.addLast(new int[] { nx, ny });
}
}
}
}
return -1;
}
}
}
例题
1、地图分析
题目描述: 你现在手里有一份大小为 n x n
的 网格 grid
,上面的每个 单元格 都用 0
和 1
标记好了。其中 0
代表海洋,1
代表陆地。
请你找出一个海洋单元格,这个海洋单元格到离它最近的陆地单元格的距离是最大的,并返回该距离。如果网格上只有陆地或者海洋,请返回 -1
。
我们这里说的距离是「曼哈顿距离」( Manhattan Distance):(x0, y0)
和 (x1, y1)
这两个单元格之间的距离是 |x0 - x1| + |y0 - y1|
。
示例 1:
输入:grid = [[1,0,1],[0,0,0],[1,0,1]] 输出:2 解释: 海洋单元格 (1, 1) 和所有陆地单元格之间的距离都达到最大,最大距离为 2。
示例 2:
输入:grid = [[1,0,0],[0,0,0],[0,0,0]] 输出:4 解释: 海洋单元格 (2, 2) 和所有陆地单元格之间的距离都达到最大,最大距离为 4。
提示:
n == grid.length
n == grid[i].length
1 <= n <= 100
grid[i][j]
不是0
就是1
/**
* @program: ZuoChengxunAlgorithmClass
* @ClassName Code01_AsFarFromLandAsPossible
* @description: 地图分析
* // 你现在手里有一份大小为 n x n 的 网格 grid
* // 上面的每个 单元格 都用 0 和 1 标记好了其中 0 代表海洋,1 代表陆地。
* // 请你找出一个海洋单元格,这个海洋单元格到离它最近的陆地单元格的距离是最大的
* // 并返回该距离。如果网格上只有陆地或者海洋,请返回 -1。
* // 我们这里说的距离是「曼哈顿距离」( Manhattan Distance):
* // (x0, y0) 和 (x1, y1) 这两个单元格之间的距离是 |x0 - x1| + |y0 - y1| 。
* // 测试链接 : https://leetcode.cn/problems/as-far-from-land-as-possible/
* @author: zs宝
* @create: 2025-07-14 11:23
* @Version 1.0
**/
package main.java.class062;
public class Code01_AsFarFromLandAsPossible {
class Solution {
public static int MAXM=101;
public static int MAXN=101;
public static int[][]queue=new int[MAXM*MAXN][2];
public static boolean[][]visit=new boolean[MAXN][MAXM];
public static int l,r;
// (x,y) i来到0位置 : x + move[i], y + move[i+1] -> x - 1, y // (x,y) i来到1位置 : x + move[i], y + move[i+1] -> x, y + 1 // (x,y) i来到2位置 : x + move[i], y + move[i+1] -> x + 1, y // (x,y) i来到3位置 : x + move[i], y + move[i+1] -> x, y - 1 public static int[]move=new int[]{-1,0,1,0,-1};
public int maxDistance(int[][] grid) {
int n=grid.length;
int m=grid[0].length;
l=0;
r=0;
int seas=0;
//初始化,寻找所有的陆地并标识,同时标识所有的海洋
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(grid[i][j]==1){
//标识并收集
visit[i][j]=true;
queue[r][0]=i;
queue[r++][1]=j;
}else{
visit[i][j]=false;
seas++;
}
}
}
//如果全是陆地或者海洋,直接按照题意返回-1
if(seas==0 || seas==n*m){
return -1;
}
//接下来开始bfs
int level=0;
int size=0;
while(l<r){
level++;
size=r-l;
for(int i=0,x,y,nx,ny;i<size;i++){
x=queue[l][0];
y=queue[l++][1];
//找到每块陆地旁边的海洋
for(int j=0;j<4;j++){
//上下左右分别遍历
nx=x+move[j];
ny=y+move[j+1];
if(nx>=0 && nx<n && ny>=0 && ny<m && !visit[nx][ny]){
visit[nx][ny]=true;
queue[r][0]=nx;
queue[r++][1]=ny;
}
}
}
}
return level-1;
}
}
}
2、贴纸拼词
题目描述: 我们有 n
种不同的贴纸。每个贴纸上都有一个小写的英文单词。
您想要拼写出给定的字符串 target
,方法是从收集的贴纸中切割单个字母并重新排列它们。如果你愿意,你可以多次使用每个贴纸,每个贴纸的数量是无限的。
返回你需要拼出 target
的最小贴纸数量。如果任务不可能,则返回 -1
。
注意:在所有的测试用例中,所有的单词都是从 1000
个最常见的美国英语单词中随机选择的,并且 target
被选择为两个随机单词的连接。
示例 1:
输入: stickers = ["with","example","science"], target = "thehat" 输出:3 解释: 我们可以使用 2 个 "with" 贴纸,和 1 个 "example" 贴纸。 把贴纸上的字母剪下来并重新排列后,就可以形成目标 “thehat“ 了。 此外,这是形成目标字符串所需的最小贴纸数量。
示例 2:
输入:stickers = ["notice","possible"], target = "basicbasic" 输出:-1 解释:我们不能通过剪切给定贴纸的字母来形成目标“basicbasic”。
提示:
n == stickers.length
1 <= n <= 50
1 <= stickers[i].length <= 10
1 <= target.length <= 15
stickers[i]
和target
由小写英文单词组成
/**
* @program: ZuoChengxunAlgorithmClass
* @ClassName Code02_StickersToSpellWord
* @description: 贴纸拼词
* // 我们有 n 种不同的贴纸。每个贴纸上都有一个小写的英文单词。
* // 您想要拼写出给定的字符串 target ,方法是从收集的贴纸中切割单个字母并重新排列它们
* // 如果你愿意,你可以多次使用每个贴纸,每个贴纸的数量是无限的。
* // 返回你需要拼出 target 的最小贴纸数量。如果任务不可能,则返回 -1
* // 注意:在所有的测试用例中,所有的单词都是从 1000 个最常见的美国英语单词中随机选择的
* // 并且 target 被选择为两个随机单词的连接。
* // 测试链接 : https://leetcode.cn/problems/stickers-to-spell-word/
* @author: zs宝
* @create: 2025-07-16 17:26
* @Version 1.0
**/
package main.java.class062;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
public class Code02_StickersToSpellWord {
class Solution {
public static int MAXN = 401;
public static String[] queue = new String[MAXN];
public static int l, r;
//用来判断某个贴纸是否使用过
public static HashSet<String> visits = new HashSet<>();
//只有小写字母,创建一个图,其中存储包含改小写字母的所有贴纸
public static ArrayList<ArrayList<String>> graph = new ArrayList<>();
// 下标0 -> a
// 下标1 -> b
// 下标2 -> c
// ... // 下标25 -> z
static {
for (int i = 0; i < 26; i++) {
graph.add(new ArrayList<>());
}
}
public int minStickers(String[] stickers, String target) {
//初始化,图和判断贴纸是否使用的都清空
for (int i = 0; i < 26; i++) {
graph.get(i).clear();
}
visits.clear();
//将所有贴纸放进对应的图中
for (String str : stickers) {
//先将贴纸的所有字符排序
str = sort(str);
for (int i = 0; i < str.length(); i++) {
//将包含某个字母的贴纸加入对应的图中,但是当多个相同字符相邻时如aaa,贴纸不可以重复添加多次
if (i == 0 || str.charAt(i) != str.charAt(i - 1)) {
graph.get(str.charAt(i) - 'a').add(str);
}
}
}
target = sort(target);
visits.add(target);
//接下来开始bfs的过程
l = 0;
r = 0;
int level = 1;
queue[r++] = target;
while (l < r) {
int size = r - l;
for (int i = 0; i < size; i++) {
String cur = queue[l++];
//这里有一点剪枝的思想在其中,我们按照当前剩余字符cur的第一个字符来看graph中的字符,从而减少每一次的遍历贴纸数
for (String str : graph.get(cur.charAt(0) - 'a')) {
String next = next(cur, str);
if (next.equals("")) {
return level;
} else if (!visits.contains(next)) {
visits.add(next);
queue[r++] = next;
}
}
}
level++;
}
//bfs完后还没有返回,则说明先有贴纸不足以拼出target
return -1;
}
public static String sort(String s) {
char[] str = s.toCharArray();
Arrays.sort(str);
return String.valueOf(str);
}
//当t中的字符减掉s中的共同字符,还剩下那些字符
public String next(String t, String s) {
StringBuilder builder = new StringBuilder();
for (int i = 0, j = 0; i < t.length();) {
//如果s已经遍历玩,而t还没有,那就将t中剩余的字符全部加入builder中去
if (j == s.length()) {
builder.append(t.charAt(i++));
} else {
if (t.charAt(i) < s.charAt(j)) {
builder.append(t.charAt(i));
i++;
} else if (t.charAt(i) > s.charAt(j)) {
j++;
} else {
i++;
j++;
}
}
}
return builder.toString();
}
}
}
3、到达角落需要移除障碍物的最小数目
题目描述: 给你一个下标从 0 开始的二维整数数组 grid
,数组大小为 m x n
。每个单元格都是两个值之一:
0
表示一个 空 单元格,1
表示一个可以移除的 障碍物 。
你可以向上、下、左、右移动,从一个空单元格移动到另一个空单元格。
现在你需要从左上角 (0, 0)
移动到右下角 (m - 1, n - 1)
,返回需要移除的障碍物的 最小 数目。
示例 1:
输入:grid = [[0,1,1],[1,1,0],[1,1,0]] 输出:2 解释:可以移除位于 (0, 1) 和 (0, 2) 的障碍物来创建从 (0, 0) 到 (2, 2) 的路径。 可以证明我们至少需要移除两个障碍物,所以返回 2 。 注意,可能存在其他方式来移除 2 个障碍物,创建出可行的路径。
示例 2:
输入:grid = [[0,1,0,0,0],[0,1,0,1,0],[0,0,0,1,0]] 输出:0 解释:不移除任何障碍物就能从 (0, 0) 到 (2, 4) ,所以返回 0 。
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 105
2 <= m * n <= 105
grid[i][j]
为0
或1
grid[0][0] == grid[m - 1][n - 1] == 0
/**
* @program: ZuoChengxunAlgorithmClass
* @ClassName Code03_MinimumObstacleRemovalToReachCorner
* @description:到达角落需要移除障碍物的最小数目
* 给你一个下标从 0 开始的二维整数数组 grid ,数组大小为 m x n
* 每个单元格都是两个值之一:
* 0 表示一个 空 单元格,
* 1 表示一个可以移除的 障碍物
* 你可以向上、下、左、右移动,从一个空单元格移动到另一个空单元格。
* 现在你需要从左上角 (0, 0) 移动到右下角 (m - 1, n - 1)
* 返回需要移除的障碍物的最小数目
* 测试链接 : https://leetcode.cn/problems/minimum-obstacle-removal-to-reach-corner/
* @author: zs宝
* @create: 2025-07-17 09:34
* @Version 1.0
**/
package main.java.class062;
import java.util.ArrayDeque;
public class Code03_MinimumObstacleRemovalToReachCorner {
class Solution {
public int minimumObstacles(int[][] grid) {
//四个方向前后左右的移动数组
int[] move ={ -1, 0, 1, 0, -1 };
int m = grid.length;
int n = grid[0].length;
//出发点到每个点的距离数组
int[][] distance = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
distance[i][j] = Integer.MAX_VALUE;
}
}
//创建一个队列,这个队列中存放层次遍历的点
ArrayDeque<int[]> queue = new ArrayDeque<>();
//初始时存放出发点
queue.addFirst(new int[] { 0, 0 });
distance[0][0] = 0;
//接下来就是01bfs的重点
/**
1,distance[i]表示从源点到i点的最短距离,初始时所有点的distance设置为无穷大
2,源点进入双端队列,distance[源点]=0
3,双端队列 头部弹出 x,
A,如果x是目标点,返回distance[x]表示源点到目标点的最短距离
B,考察从x出发的每一条边,假设某边去y点,边权为w
1)如果 distance[y] > distance[x] + w,处理该边;否则忽略该边
2)处理时,更新distance[y] = distance[x] + w
如果w==0,y从头部进入双端队列;如果w==1,y从尾部进入双端队列
3)考察完x出发的所有边之后,重复步骤3
4,双端队列为空停止
*/
while (!queue.isEmpty()) {
//头部弹出
int[] record = queue.pollFirst();
int x = record[0];
int y = record[1];
//A,如果x是目标点,返回distance[x]表示源点到目标点的最短距离
if (x == m - 1 && y == n - 1) {
return distance[x][y];
}
/**
B,考察从x出发的每一条边,假设某边去y点,边权为w
1)如果 distance[y] > distance[x] + w,处理该边;否则忽略该边
2)处理时,更新distance[y] = distance[x] + w
如果w==0,y从头部进入双端队列;如果w==1,y从尾部进入双端队列
3)考察完x出发的所有边之后,重复步骤3
*/ for (int i = 0; i < 4; i++) {
int nx = x + move[i];
int ny = y + move[i + 1];
if (nx >= 0 && nx < m && ny >= 0 && ny < n && distance[nx][ny] > distance[x][y] + grid[nx][ny]) {
distance[nx][ny] = distance[x][y] + grid[nx][ny];
if (grid[nx][ny] == 0) {
queue.addFirst(new int[] { nx, ny });
} else {
queue.addLast(new int[] { nx, ny });
}
}
}
}
return -1;
}
}
}
4、使网格图至少有一条有效路径的最小代价
题目描述: 给你一个 m x n 的网格图 grid
。 grid
中每个格子都有一个数字,对应着从该格子出发下一步走的方向。 grid[i][j]
中的数字可能为以下几种情况:
1 ,下一步往右走,也就是你会从
grid[i][j]
走到grid[i][j + 1]
2 ,下一步往左走,也就是你会从
grid[i][j]
走到grid[i][j - 1]
3 ,下一步往下走,也就是你会从
grid[i][j]
走到grid[i + 1][j]
4 ,下一步往上走,也就是你会从
grid[i][j]
走到grid[i - 1][j]
注意网格图中可能会有 无效数字 ,因为它们可能指向 grid
以外的区域。
一开始,你会从最左上角的格子 (0,0)
出发。我们定义一条 有效路径 为从格子 (0,0)
出发,每一步都顺着数字对应方向走,最终在最右下角的格子 (m - 1, n - 1)
结束的路径。有效路径 不需要是最短路径 。
你可以花费 cost = 1
的代价修改一个格子中的数字,但每个格子中的数字 只能修改一次 。
请你返回让网格图至少有一条有效路径的最小代价。
示例 1:
输入:grid = [[1,1,1,1],[2,2,2,2],[1,1,1,1],[2,2,2,2]] 输出:3 解释:你将从点 (0, 0) 出发。 到达 (3, 3) 的路径为: (0, 0) --> (0, 1) --> (0, 2) --> (0, 3) 花费代价 cost = 1 使方向向下 --> (1, 3) --> (1, 2) --> (1, 1) --> (1, 0) 花费代价 cost = 1 使方向向下 --> (2, 0) --> (2, 1) --> (2, 2) --> (2, 3) 花费代价 cost = 1 使方向向下 --> (3, 3) 总花费为 cost = 3.
示例 2:
输入:grid = [[1,1,3],[3,2,2],[1,1,4]] 输出:0 解释:不修改任何数字你就可以从 (0, 0) 到达 (2, 2) 。
示例 3:
输入:grid = [[1,2],[4,3]] 输出:1
示例 4:
输入:grid = [[2,2,2],[2,2,2]] 输出:3
示例 5:
输入:grid = [[4]] 输出:0
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 100
/**
* @program: ZuoChengxunAlgorithmClass
* @ClassName Code04_MinimumCostToMakeAtLeastOneValidPath
* @description:使网格图至少有一条有效路径的最小代价
* // 给你一个 m * n 的网格图 grid 。 grid 中每个格子都有一个数字
* // 对应着从该格子出发下一步走的方向。 grid[i][j] 中的数字可能为以下几种情况:
* // 1 ,下一步往右走,也就是你会从 grid[i][j] 走到 grid[i][j + 1]
* // 2 ,下一步往左走,也就是你会从 grid[i][j] 走到 grid[i][j - 1]
* // 3 ,下一步往下走,也就是你会从 grid[i][j] 走到 grid[i + 1][j]
* // 4 ,下一步往上走,也就是你会从 grid[i][j] 走到 grid[i - 1][j]
* // 注意网格图中可能会有 无效数字 ,因为它们可能指向 grid 以外的区域
* // 一开始,你会从最左上角的格子 (0,0) 出发
* // 我们定义一条 有效路径 为从格子 (0,0) 出发,每一步都顺着数字对应方向走
* // 最终在最右下角的格子 (m - 1, n - 1) 结束的路径
* // 有效路径 不需要是最短路径
* // 你可以花费1的代价修改一个格子中的数字,但每个格子中的数字 只能修改一次
* // 请你返回让网格图至少有一条有效路径的最小代价
* // 测试链接 : https://leetcode.cn/problems/minimum-cost-to-make-at-least-one-valid-path-in-a-grid/
* @author: zs宝
* @create: 2025-07-18 11:00
* @Version 1.0
**/
package main.java.class062;
import java.util.ArrayDeque;
public class Code04_MinimumCostToMakeAtLeastOneValidPath {
class Solution {
public int minCost(int[][] grid) {
//表示索引1位置右走,2左,3下,4上
int[][]move={{},{0,1},{0,-1},{1,0},{-1,0}};
int m=grid.length;
int n=grid[0].length;
int[][]distace=new int[m][n];
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
distace[i][j]=Integer.MAX_VALUE;
}
}
//构建双端队列
ArrayDeque<int[]> queue=new ArrayDeque<>();
queue.addFirst(new int[]{0,0});
distace[0][0]=0;
while(!queue.isEmpty()){
int[]record=queue.pollFirst();
int x=record[0];
int y=record[1];
if(x==m-1 && y==n-1){
return distace[x][y];
}
for(int i=1;i<=4;i++){
//注意这一步,这里就是本题对应的01dfs的题目分析转化为抽象模型的而关键
//若当前方向与grid本来的方向同,那就是0,否则就为1
int weight=grid[x][y]!=i?1:0;
int nx=move[i][0]+x;
int ny=move[i][1]+y;
if(nx>=0 && nx<m && ny>=0 && ny<n && distace[nx][ny]>distace[x][y]+weight){
distace[nx][ny]=distace[x][y]+weight;
if(weight==0){
queue.addFirst(new int[]{nx,ny});
}else{
queue.addLast(new int[]{nx,ny});
}
}
}
}
return -1;
}
}
}
5、接雨水Ⅱ
题目描述: 给你一个 m x n
的矩阵,其中的值均为非负整数,代表二维高度图每个单元的高度,请计算图中形状最多能接多少体积的雨水。
示例 1:
输入: heightMap = [[1,4,3,1,3,2],[3,2,1,3,2,4],[2,3,3,2,3,1]] 输出: 4 解释: 下雨后,雨水将会被上图蓝色的方块中。总的接雨水量为1+2+1=4。
示例 2:
输入: heightMap = [[3,3,3,3,3],[3,2,2,2,3],[3,2,1,2,3],[3,2,2,2,3],[3,3,3,3,3]] 输出: 10
提示:
m == heightMap.length
n == heightMap[i].length
1 <= m, n <= 200
0 <= heightMap[i][j] <= 2 * 104
/**
* @program: ZuoChengxunAlgorithmClass
* @ClassName Code05_TrappingRainWaterII
* @description:二维接雨水
* // 给你一个 m * n 的矩阵,其中的值均为非负整数,代表二维高度图每个单元的高度
* // 请计算图中形状最多能接多少体积的雨水。
* // 测试链接 : https://leetcode.cn/problems/trapping-rain-water-ii/
* @author: zs宝
* @create: 2025-07-19 08:30
* @Version 1.0
**/
package main.java.class062;
import java.util.PriorityQueue;
public class Code05_TrappingRainWaterII {
class Solution {
public int trapRainWater(int[][] heightMap) {
int[]move={-1,0,1,0,-1};
int m=heightMap.length;
int n=heightMap[0].length;
//小根堆,0代表坐标x,1代表坐标y,2代表水线高度
PriorityQueue<int[]> heap=new PriorityQueue<>((a, b)->a[2]-b[2]);
boolean[][]visited=new boolean[m][n];
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
//如果是矩阵边缘,将其加入小顶堆,从外向里看,而不是从里向外看
//最开始的想法就是从里向外看,很复杂,不好判断某个位置到底能不能接水,而反过来从边缘最小处向内看,就容易思考很多
if(i==0 || i==m-1 || j==0 || j==n-1){
visited[i][j]=true;
heap.add(new int[]{i,j,heightMap[i][j]});
}else{
visited[i][j]=false;
}
}
}
int ans=0;
while(!heap.isEmpty()){
int[]record=heap.poll();
int x=record[0];
int y=record[1];
int w=record[2];
ans+=w-heightMap[x][y];
//开始bfs向四周扩散寻找
for(int i=0;i<4;i++){
int nx=x+move[i];
int ny=y+move[i+1];
if(nx>=0 && nx<m && ny>=0 && ny<n && !visited[nx][ny]){
//将四周的点加入小根堆中
//但是要注意其中小根堆里的水线需要当前位置的高度和引起这个点进堆的点的水线进行比较,选择大的那个
heap.add(new int[]{nx,ny,Math.max(w,heightMap[nx][ny])});
visited[nx][ny]=true;
}
}
}
return ans;
}
}
}
6、单词接龙Ⅱ
题目描述: 按字典 wordList
完成从单词 beginWord
到单词 endWord
转化,一个表示此过程的 转换序列 是形式上像 beginWord -> s1 -> s2 -> ... -> sk
这样的单词序列,并满足:
每对相邻的单词之间仅有单个字母不同。
转换过程中的每个单词
si
(1 <= i <= k
)必须是字典wordList
中的单词。注意,beginWord
不必是字典wordList
中的单词。sk == endWord
给你两个单词 beginWord
和 endWord
,以及一个字典 wordList
。请你找出并返回所有从 beginWord
到 endWord
的 最短转换序列 ,如果不存在这样的转换序列,返回一个空列表。每个序列都应该以单词列表 [beginWord, s1, s2, ..., sk]
的形式返回。
示例 1:
输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"] 输出:[["hit","hot","dot","dog","cog"],["hit","hot","lot","log","cog"]] 解释:存在 2 种最短的转换序列: "hit" -> "hot" -> "dot" -> "dog" -> "cog" "hit" -> "hot" -> "lot" -> "log" -> "cog"
示例 2:
输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"] 输出:[] 解释:endWord "cog" 不在字典 wordList 中,所以不存在符合要求的转换序列。
提示:
1 <= beginWord.length <= 5
endWord.length == beginWord.length
1 <= wordList.length <= 500
wordList[i].length == beginWord.length
beginWord
、endWord
和wordList[i]
由小写英文字母组成beginWord != endWord
wordList
中的所有单词 互不相同
/**
* @program: ZuoChengxunAlgorithmClass
* @ClassName Code06_WordLadderII
* @description:单词接龙 II
* // 按字典 wordList 完成从单词 beginWord 到单词 endWord 转化
* // 一个表示此过程的 转换序列 是形式上像
* // beginWord -> s1 -> s2 -> ... -> sk 这样的单词序列,并满足:
* // 每对相邻的单词之间仅有单个字母不同
* // 转换过程中的每个单词 si(1 <= i <= k)必须是字典 wordList 中的单词
* // 注意,beginWord 不必是字典 wordList 中的单词
* // sk == endWord
* // 给你两个单词 beginWord 和 endWord ,以及一个字典 wordList
* // 请你找出并返回所有从 beginWord 到 endWord 的 最短转换序列
* // 如果不存在这样的转换序列,返回一个空列表
* // 每个序列都应该以单词列表 [beginWord, s1, s2, ..., sk] 的形式返回
* // 测试链接 : https://leetcode.cn/problems/word-ladder-ii/
* @author: zs宝
* @create: 2025-07-19 10:24
* @Version 1.0
**/
package main.java.class062;
import java.util.*;
public class Code06_WordLadderII {
class Solution {
//记录给的所有字典里的单词,加速查找过程
public static HashSet<String> dic;
//当前层有哪些词典里的单词
public static HashSet<String> curLevel = new HashSet<>();
//下一层可以由当前层的单词改变一个字母,在词典中有哪些单词
public static HashSet<String> nextLevel = new HashSet<>();
//建图,反向图
public static HashMap<String, ArrayList<String>> graph = new HashMap<>();
//记录可以达成转换的一条路径
public static LinkedList<String> path = new LinkedList<>();
//记录最后的答案
public static List<List<String>> ans = new ArrayList<>();
public List<List<String>> findLadders(String beginWord, String endWord, List<String> wordList) {
build(wordList);
//若最终的结果单词都不在词典中,返回空的答案
if (!dic.contains(endWord)) {
return ans;
}
//若能找到从begin到end的路径
if (bfs(beginWord, endWord)) {
dfs(endWord, beginWord);
}
return ans;
}
public static void build(List<String> wordList) {
dic = new HashSet<>(wordList);
graph.clear();
curLevel.clear();
nextLevel.clear();
ans.clear();
}
public static boolean bfs(String beginWord, String endWord) {
boolean find = false;
curLevel.add(beginWord);
//宽度优先遍历,寻找路径,找到的只要保证每层的单词没有一样的,就必定是最短的
while (!curLevel.isEmpty()) {
//移除已经出现遍历过的单词
dic.removeAll(curLevel);
//对当前层的每个单词寻找改变一个字母后,在词典中可以找到的单词,注意反向建图
for (String world : curLevel) {
char[] w = world.toCharArray();
//这里有个小技巧,对于每个单词由于1 <= beginWord.length <= 5
//所以我们让每个位置的字母分别在26个小写字母中尝试,而不是与词典中的单词比较判定,从而减少计算量
for (int i = 0; i < w.length; i++) {
char old = w[i];
for (char j = 'a'; j <= 'z'; j++) {
w[i] = j;
String str = String.valueOf(w);
if (dic.contains(str) && !str.equals(world)) {
if (str.equals(endWord)) {
find = true;
}
graph.putIfAbsent(str, new ArrayList<>());
//反向建图,很关键
graph.get(str).add(world);
nextLevel.add(str);
}
}
w[i] = old;
}
}
//如果在这一层找到了目标,则最短路径已经出现了,可以直接返回结果了,不用再继续向下找
if (find) {
return true;
} else {
HashSet<String> temp = curLevel;
curLevel = nextLevel;
nextLevel = temp;
nextLevel.clear();
}
}
return false;
}
//由于我们已经反向建图,现在开始从后往前寻找路径
public static void dfs(String endWord, String beginWord) {
path.addFirst(endWord);
if (endWord.equals(beginWord)) {
ans.add(new ArrayList<>(path));
} else if (graph.containsKey(endWord)) {
for (String next : graph.get(endWord)) {
dfs(next, beginWord);
}
}
//递归状态复原
path.removeFirst();
}
}
}