📢📢📢哈喽大家好啊,我是浅夜,这几天学了bfs,刷了几道bfs题,因为这种题相对模板一些,自己学了害怕忘了,那么就小小的总结一下子吧,也是跟大家分享分享,如果能帮助到大家就太荣幸啦!
A.迷宫🎯
迷宫类的题目是典型的BFS问题,我们一般通过BFS实现对于上下左右移动的不同情形的搜索,不同题目的区别就在于结束搜索的条件和最后要求的答案如何处理了,以及处理不同问题时用到的小技巧了,这正是我们列举多道题目的目的。
📋问题描述
❓思路分享
小明每次可以选择上下左右移动,另外他还可以通过传送门(传送门是双向的)直接传送到另一个格子,小明的目的是从他初始的格子(随机)走到迷宫最右下角的格子。当然,他在迷宫中行走,肯定不能超出迷宫的范围,有的题目也会跨越障碍物,也就是说,我们bfs其实是一个笨且考虑周到的方法,他将所有可能的路线全部列举出来,而本题目bfs结束的条件就是到走迷宫终点。
对于上下左右移动的迷宫模型,我们需要用到偏移量的小技巧(代码中有注释)。
题目要求的是最短步数期望,所以小明是不能回头的,我们如何在代码中体现小明不能回头呢,那就是用一个“迷宫”大小的boolean数组来表示每个格子的状态,初始都为false,小明每经过一个,就给它标记为true。
另外我们如何处理传送门呢?首先,每个传送门是一个二维坐标来表示,我们既需要处理不同传送门多对多的关系,又需要将传送门的横纵坐标一并维护,这貌似不能简单的通过某个数据结构来实现,这里我们就可以通过映射将二维坐标统一变成一个一维的值,即(x,y) ==>x*n + y,这个n就是每一行元素的个数,简单的理解这个式子就是我们看他是这个数组的第几个元素(从上往下,从左往右的顺序数)。接下来我们就可以用一个HashMap来保存传送门了,key值为某个点(Integer),value为一个集合,保存着这个点可以传送到的其他点。前面也提到了,传送门是双向的且我们是用key-value模型来保存的,所以我们在维护这些传送门的时候应该注意将他们还要倒过来再保存一下。
说完了一些值得注意的小技巧,我们来分析一下问题的处理思路:
先定义一个步数变量(也是bfs层数),每走一步+1。然后将输入的传送门维护起来,从初始位置开始bfs,标记该点为已经到过,该点入队(入队表示到达了该点),队不为空,队中元素出队,首先判断是不是有传送门,如果有,就传送到传送门另一头,再入队,并标记。如果没有传送门,那么就在不超出迷宫范围且没有到过格子上下左右移动,新到一个格子,再入队......这样一直进行,直到终点。这样我们每个起点都得到一种走迷宫的方案步数,我们累加每种起点的步数得到总步数,用总步数/总格子数 就得到了步数期望。
📗参考代码
import java.util.*;
/**
* @ClassName 迷宫_bfs
* @Author @浅夜
* @Date 2023/3/14 16:52
* @Version 1.0
*/
public class 迷宫_bfs {
static int N = 2010;
static Map<Integer, List<int[]>> map = new HashMap<>();
static boolean[][] st = new boolean[N][N];
//偏移量预处理
static int[] dx = {1, -1, 0, 0};
static int[] dy = {0, 0, 1, -1};
static int n, m;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
for (int i = 0; i < m; i++) {
int x1 = sc.nextInt() - 1;
int y1 = sc.nextInt() - 1;
int x2 = sc.nextInt() - 1;
int y2 = sc.nextInt() - 1;
add(x1, y1, x2, y2);
add(x2, y2, x1, y1);//传送门可以相互传送
}
Queue<int[]> q = new LinkedList<>();
q.offer(new int[]{n - 1, n - 1});
st[n - 1][n - 1] = true;
int ans = 0; //累计答案
int x = 0; //记录层数
while (!q.isEmpty()) {
int size = q.size();
while (size-- > 0) {
int[] curr = q.poll();
int a = curr[0], b = curr[1];
//累加答案
ans += x;
//先看有没有传送门
if (map.containsKey(a * n + b)) {
List<int[]> list = map.get(a * n + b);
for (int[] g : list) {
if (!st[g[0]][g[1]]) {
q.offer(g);
st[g[0]][g[1]] = true;
}
}
}
//没有传送门的情况,只能上下左右走
for (int i = 0; i < 4; i++) {
int newX = a + dx[i];
int newY = b + dy[i];
if (newX >= 0 && newX < n && newY >= 0 && newY < n && !st[newX][newY]) {
q.offer(new int[]{newX, newY});
st[newX][newY] = true;
}
}
}
x++;
}
System.out.printf("%.2f", ans * 1.0 / (n * n));
}
//添加(x1,y1)到(x2,y2)的传送门
static void add(int x1, int y1, int x2, int y2) {
//二维映射成一维简化计算:(x1,y1)=>x1*n+y1
if (!map.containsKey(x1 * n + y1)) map.put(x1 * n + y1, new ArrayList<>());
map.get(x1 * n + y1).add(new int[]{x2, y2});
}
}
B.🎁大胖子走迷宫
📋问题描述
❓思路分享
走迷宫的原理同上,不同点有:起点固定,增加了障碍物,而且小明的体型不是一直占用一个格子了,另外,小明可以选择上下左右移动而且可以选择不移动。
这道题的难点在于对小明体型的处理,他如果处在肥胖状态下的话,他其实占用的是一个正方形的区域,如下图:
小明的身体 | 小明的身体 | 小明的身体 |
小明的身体 | 小明 | 小明的身体 |
小明的身体 | 小明的身体 | 小明的身体 |
小明的身体 | 小明的身体 | 小明的身体 | 小明的身体 | 小明的身体 |
小明的身体 | 小明的身体 | 小明的身体 | 小明的身体 | 小明的身体 |
小明的身体 | 小明的身体 | 小明 | 小明的身体 | 小明的身体 |
小明的身体 | 小明的身体 | 小明的身体 | 小明的身体 | 小明的身体 |
小明的身体 | 小明的身体 | 小明的身体 | 小明的身体 | 小明的身体 |
我们可以看到,小明要想移动,那么要满足下一个格子的周围一定的范围不能有障碍物,其实我们可以用一个变量tmp来表示小明的肥胖程度。tmp = 2时,表示他还要占掉四周2格,就是体型为5 * 5的情况。我们每次判断边界和障碍物时,应该将小明的体型考虑进去。
另外,小明可以选择上下左右移动而且可以选择不移动,可能小明 会遇到一种情况,他发现他只有一种走法而且这个路口不允许他的体型通过,他又不能先走别的格子再等瘦下去回头来通过这里,那么他就只能原地不动等到瘦下去再通过。那么我们就要将原位置继续入队。
📗参考代码
import java.util.LinkedList;
import java.util.Queue;
import java.util.Scanner;
/**
* @ClassName 大胖子走迷宫
* @Author @浅夜
* @Date 2023/3/18 11:20
* @Version 1.0
*/
public class 大胖子走迷宫 {
static int tmp = 2;
static int N = 310;
static char[][] a = new char[N][N];
static boolean[][] visit = new boolean[N][N]; //标记某个点是不是已经走过了
static int[] dx = {1, -1, 0, 0};
static int[] dy = {0, 0, 1, -1};
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int k = sc.nextInt();
for (int i = 0; i < n; i++) {
a[i] = sc.next().toCharArray();
}
//bfs
int count = 0; //表示时间。也是bfs的层数
Queue<int[]> queue = new LinkedList<>();
queue.offer(new int[]{2, 2});
visit[2][2] = true;
while (!queue.isEmpty()) {
int size = queue.size();
while (size-- > 0) {
int curr[] = queue.poll();
int x = curr[0];
int y = curr[1];
if (x == n - 3 && y == n - 3) {
System.out.println(count);
return;
}
for (int i = 0; i < 4; ++i) {
int a = x + dx[i];
int b = y + dy[i];
if (a - tmp >= 0 && a + tmp < n && b - tmp >= 0 && b + tmp < n && check(a, b) && !visit[a][b]) {
queue.offer(new int[]{a, b});
visit[a][b] = true;
}
}
//有可能体型还是太大,只能等待体型变小
queue.offer(new int[]{x, y}); //原位置继续入队 等count足够体型变小
}
count++;
if (count == k) tmp = 1;
if (count == 2 * k) tmp = 0;
}
}
//(x,y)是小明的中心位置 判断小明是不是真的能待在这个位置
//因为他可能占的不止一个格子 而是一个正方形区域! 所以需要遍历这个矩阵内的所有点
static boolean check(int x, int y) {
for (int i = x - tmp; i <= x + tmp; ++i) {
for (int j = y - tmp; j <= y + tmp; ++j) {
if (a[i][j] == '*') return false;
}
}
return true;
}
}
C.🍭染色时间
📋问题描述
❓思路分享
虽然这次不是走迷宫,但是与走迷宫有异曲同工之妙,它们都是向上下左右的格子来搜索。
这道题目的核心在于我们我们不用状态数组表示某个点是否被染色过,要知道这道题的每个格子都有一个染色时间(a数组保存)我们用一个染过和没染过的状态来表示肯定是不够滴,我们就需要动态的维护一个数组b,这个数组来保存给某一个格子染色的时间,注意这个染色时间是从起始时间(0时刻)来计算的。
那我们在bfs判断某个格子的时候,除了判断格子是否在棋盘范围之内,还应该判断这个点的染色时间(b)是否大于它前面的格子染色的时间 + 它本身的染色时间(a),如果大于说明它已经被触发过了但是显然当前的触发才应该是有效的触发(bfs是一层一层进行的),或者染色时间(b)还是0,说明还没有被触发过,那么就触发,入队,然后给这个格子的b赋值,应该是前面格子的染色时间 (b) + 当前格子的染色时间(a)。
因为b数组是可以理解成类似前缀和数组的,我们只需要找出b数组的最大值,就是染完所有格子的用时了。
📗参考代码
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;
/**
* @ClassName 染色时间
* @Author @浅夜
* @Date 2023/3/15 22:06
* @Version 1.0
*/
public class 染色时间 {
static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
static int [][]d={{-1,0},{1,0},{0,-1},{0,1}};
static int[][] a; //保存每个格子染色需要的时间
static int[][] b; //给某个格子染上色需要的时间(开始到染上的时间)
static int n, m;
public static void main(String[] args) throws IOException {
String[] v = br.readLine().split(" ");
n = Integer.parseInt(v[0]);
m = Integer.parseInt(v[1]);
a = new int[n][m];
b = new int[n][m];
for (int i = 0; i < n; i++) {
v = br.readLine().split(" ");
for (int j = 0; j < m; j++) {
a[i][j] = Integer.parseInt(v[j]);
}
}
b[0][0] = a[0][0]; //第一个格子
int max = 0;
bfs();
for (int i = 0; i < n; i++) { //找二维数组最大值,这样写更快,否则会超时
Arrays.sort(b[i]);
max = Math.max(max, b[i][m-1]);
}
// for(int i = 0; i < n; i++){
// for (int j = 0; j < m ; j++) {
// if(b[i][j]>max) max = b[i][j];
// }
// }
// System.out.print(max);
}
static void bfs() {
Queue<int[]> queue = new LinkedList<>();
queue.add(new int[]{0, 0});
int newX, newY, t;
int[] cur = new int[2];
while (!queue.isEmpty()) {
cur = queue.poll();
int x = cur[0], y = cur[1];
t = b[x][y]; //染色需要的时间
for (int i = 0; i < 4; i++) {
newX = x + d[i][0];
newY = y + d[i][1];
if (check(newX, newY, t)) {
queue.add(new int[]{newX,newY});
b[newX][newY] = b[x][y] + a[newX][newY]; //当前格染色时间 = 染前面格子花费的时间 + 该格染色时间
}
}
}
}
static boolean check(int x, int y, int t) {
if (x >= 0 && x < n && y < m && y >= 0) {
return t + a[x][y] < b[x][y] || b[x][y] == 0;
}
return false;
}
}
📢📢📢~
好啦,以上就是我想给大家分享的题目啦~
刚学bfs,本文主要是自己的学习记录,如果因为我的理解偏差对题目的解释有问题还请各位佬批评指正!
最后大家如果还有好的bfs的题目欢迎来分享讨论,我学会了还可以补在本文结尾奥
大家觉得有收获的话可以点个赞古力一下嘿嘿😋