迷宫
配套视频讲解
关于dfs和bfs的区别讲解。
对于上图,假设我们要找从1到5的最短路,那么我们用dfs去找,并且按照编号从大到小的顺序去找,首先找到的路径如下,
从节点1出发,我们发现节点2可以走,于是我们就走向了节点2,然后又发现节点2可以走向节点4,于是走向了节点4,然后从节点4走向了节点5,我们只是找到了一条从节点1到节点5的路径,但是我们并不能确定是否是最短路径,所以我们还需要继续去找。我们回退到节点4,与节点4相连的节点3和节点5都已经被遍历过了,所以我们回退到节点2,与节点2相连的节点1和节点4都已经被遍历过了,所以我们回退到节点1,此时节点3也与节点1相连,于是我们走向节点3,后面的路径如下图所示,
我们通过节点3走向了节点5。然后返回到节点3,与节点3相连的节点1和节点5都已经被遍历过了,所以我们回退到节点1,此时与节点1相连的节点2和节点3都已经被遍历过了,dfs退出。
我们找到了两条路径,通过对比这两天路径的长度,我们可以找到最短路径,可以看出,我们穷尽了所有从节点1到达节点5的路径。
接下来看一看bfs是怎么找的。
我们是用队列实现的bfs过程,一开始队列里面只有起始点,即[1]。从队列里面拿出一个节点,即节点1,然后去走此时节点1能够到达的所有节点。如上图所示,bfs会同时向多个方向拓展,节点1与节点2,节点3相连,那么第一步它会从节点1走向节点,也会从节点1走向节点3。这时分别将节点2和节点3加入队列,并且此时,节点1已经出队,即[2,3]。然后去走所有与节点2相连且没有被遍历过的节点,再去走所有与节点3相连且没有被遍历过的节点,如下图所示,
第二步从节点3走到节点5,也从节点2走向了节点4,但是此时我发现已经走到终点了,并且长度为2,即便在后续遍历中我可以走到终点,但是现在所到达的所有点长度已经是2了,那么后续继续走长度一定比2大,所以我可以确定,此时的路径一定是最短路。
dfs题目分析
这一道题我们可以用dfs也可以用bfs,但是对于迷宫最短路类题目,最好的方法是bfs。通过这道题如果采用dfs来做,需要用到dfs里的回溯和剪枝。
首先分析题意,我们要找从起点到终点的最短路,并且这个最短路的行走路线是字典序最小,其实字典序最小好操作,我们只需要在遍历上下左右四个方向的时候按照字典序从小到大的顺序遍历就行了。即
static char[] direct = {'D','L','R','U'};
static int[] nexty = {0,-1,1,0};
static int[] nextx = {1,0,0,-1};
接下来考虑如何找最短路,对于dfs而言,要找最短路,我必须把当前可走的所有路都遍历结束后,才能确定哪一条路是最短路,对于某一个位置,我当前向左走,标记左边的节点为已遍历过,并且把这个’L’存入,走到终点后我再回退,那么再次回到这个位置时,我会选择其它可以走的方向,那么我们要把左边的节点已遍历过的标记清空,表示没有遍历过,并且把在这里存入的’L’取出。即
for (int i = 0; i < 4; i++) {
int xx = x + nextx[i];
int yy = y + nexty[i];
if(xx >= 0 && xx < n && yy >= 0 && yy < m &&visit[xx][yy] == 0 && map[xx][yy] == '0') {
visit[xx][yy] = 1;
path[s] = direct[i];//更新路径
......
dfs(xx, yy, s+1);
visit[xx][yy] = 0;//回溯 dfs返回的时候,往往需要对之前做的标记进行重置
}
}
解释一下上述代码,for (int i = 0; i < 4; i++)
表示四个方向遍历,它遍历的顺序由nextx和nexty数组决定,而我们在最开始就给他规定了遍历的顺序是按字典序从小到大遍历的。然后if语句一是判断下一个位置的坐标是否越界,二是判断下一个位置是否之前被遍历过,三是判断下一个位置是否可以走,都没问题的话我就去走这个位置,然后标记这个位置被走过,并且存入此时走的方向,visit[xx][yy] = 1;path[s] = direct[i];
,然后就是进入dfs,那么这里dfs三个参数分别表示下一个位置的x坐标,y坐标,以及走到下一个位置走了几步。dfs结束后,我要给visit标记复原,即visit[xx][yy] = 0;
上述是正常dfs以及回溯的过程,接下来我们要加入剪枝。什么情况下我可以确定这条路一定不会成为答案?也就是这条路的长度超过了我们此时记录的最短路的长度,即
if(s >= step) {
return;
}
s表示我走到当前节点的步数,step表示此时记录的最短路的长度,我还没有走到终点步数就比最短路长,那么它必然不会成为最短路,所以后面就不需要遍历了,直接返回。
还有第二种剪枝,这个不太好想,也是比较最短路的长度,我们比较的是到达当前位置的长度以及在之前我走到该位置的最短长度。比如有一个点A,我走到这里耗费了s步,但是我之前记录我走到这里耗费了s1步,而s1<s,那么说明我之前走到这里再向后走的路径一定比我现在走到这里再向后走的路径短,那么此时我就没有必要遍历了。即
if(s > dp[x][y]) {
return;
}
s表示我走到当前节点(x,y)的步数, d p [ x ] [ y ] dp[x][y] dp[x][y]表示我之前走到该节点的最短路。那么在dfs的过程中我们要更新dp数组,
for (int i = 0; i < 4; i++) {
int xx = x + nextx[i];
int yy = y + nexty[i];
if(xx >= 0 && xx < n && yy >= 0 && yy < m &&visit[xx][yy] == 0 && map[xx][yy] == '0') {
visit[xx][yy] = 1;
dp[xx][yy] = Math.min(dp[xx][yy], s+1);//更新dp
path[s] = direct[i];//更新路径
dfs(xx, yy, s+1);
visit[xx][yy] = 0;//回溯 dfs返回的时候,往往需要对之前做的标记进行重置
//path[s] = '';
}
}
在回溯的时候我们只回溯了visit数组,为什么呢?dp数组不需要回溯,因为它记录的就是一个全局的值,即在我所有到达(x,y)点的路径中最短路径的长度。而path数组虽然需要回溯,但是我们在下一个遍历的时候,下一个的值会直接覆盖之前的值,所以不需要特意给他回溯。那么你也可以理解有回溯,即注释的那个地方//path[s] = '';
,这里写和不写效果是一样的。
然后我们看当走到终点时,如何处理,
//判断是否走到终点
if(x == n-1 && y == m-1) {
if(s < step) {//当前步数小于之前的最优值,对结果进行一下记录
step = s;
String string = "";
for (int i = 0; i < s; i++) {
//System.out.print(" "+path[i] + " ");
string += path[i];
}
//set.add(string);
result = string;
}
//System.out.println();
}
判断一下此时走到终点的路径长度是否小于我之前记录的长度,如果小于,我要更新最短路径长度,以及这条路径每一步走的方向。
最后这道题,给我们的图是一个字符串,我们可以把它转化成二维字符数组,转化细节在shuju函数里。
dfs题目代码
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Scanner;
import java.util.Set;
public class Main{
/*
* 在步数最少的前提下,请找出字典序最小的一个作为答案
* 如何保证找到的第一个路径一定是字典序最小的
* 遍历的时候,按照字典序从小到大的顺序去遍历
* D<L<R<U
* DDRURRDDDR
* DRRURRDDDR
* 如何记录我们的路径
* bfs
* node{
* x,y,path//走到当前节点的路径
* }
* path
*/
static int n = 30;
static int m = 50;
static char[][] map = new char[n][m];
static char[] direct = {'D','L','R','U'};
static int[] nexty = {0,-1,1,0};
static int[] nextx = {1,0,0,-1};
static int[][] visit = new int[n][m];
//dfs
static char[] path = new char[n*m+1];
static int step = n*m+1;
static String result = "";
static Set<String> set = new HashSet<String>();
static int[][] dp = new int[n][m];
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
shuju();
for(int i=0;i<n;i++){
Arrays.fill(dp[i], n*m+1);
}
sc.close();
visit[0][0] = 1;
dfs(0,0,0);
System.out.println(result);
}
private static void shuju() {
// TODO Auto-generated method stub
String string = "01010101001011001001010110010110100100001000101010"
+"00001000100000101010010000100000001001100110100101"
+"01111011010010001000001101001011100011000000010000"
+"01000000001010100011010000101000001010101011001011"
+"00011111000000101000010010100010100000101100000000"
+"11001000110101000010101100011010011010101011110111"
+"00011011010101001001001010000001000101001110000000"
+"10100000101000100110101010111110011000010000111010"
+"00111000001010100001100010000001000101001100001001"
+"11000110100001110010001001010101010101010001101000"
+"00010000100100000101001010101110100010101010000101"
+"11100100101001001000010000010101010100100100010100"
+"00000010000000101011001111010001100000101010100011"
+"10101010011100001000011000010110011110110100001000"
+"10101010100001101010100101000010100000111011101001"
+"10000000101100010000101100101101001011100000000100" //D -> L -> R -> U
+"10101001000000010100100001000100000100011110101001"
+"00101001010101101001010100011010101101110000110101"
+"11001010000100001100000010100101000001000111000010"
+"00001000110000110101101000000100101001001000011101"
+"10100101000101000000001110110010110101101010100001"
+"00101000010000110101010000100010001001000100010101"
+"10100001000110010001000010101001010101011111010010"
+"00000100101000000110010100101001000001000000000010"
+"11010000001001110111001001000011101001011011101000"
+"00000110100010001000100000001000011101000000110011"
+"10101000101000100010001111100010101001010000001000"
+"10000010100101001010110000000100101010001011101000"
+"00111100001000010000000110111000000001000000001011"
+"10000001100111010111010001000110111010101101111000";
//将上面的一大串字符转为数组中存储,且数组为int型,仅存0 和 1两个值
int[][] moze= new int[30][50];
for (int i = 0 ; i < 30 ; i++) {
for(int j = 0 ; j < 50 ; j++) {
map[i][j] = string.charAt(i*50+j); //
}
}
}
private static void dfs(int x, int y, int s) {//s当前已经走的步数 走的路径的方向path[i] 表示第i步走的方向
/* 剪枝
* 1.s走到x,y所需要的步数,step当前记录的走完迷宫所需要的最短的步数,s>step return
* 2.dp[x][y] 当前走到x,y点所需要的最短距离,s>dp[x][y] return
*
*
* dfs{
* 1.剪枝
* 2.回溯
* 3.递归
*
* }
*/
//剪枝操作
if(s >= step) {
return;
}
if(s > dp[x][y]) {
return;
}
//判断是否走到终点
if(x == n-1 && y == m-1) {
//System.out.print("---" + x + " " + y + " ");
if(s < step) {//当前步数小于之前的最优值,对结果进行一下记录
step = s;
String string = "";
for (int i = 0; i < s; i++) {
//System.out.print(" "+path[i] + " ");
string += path[i];
}
//set.add(string);
result = string;
}
//System.out.println();
}
for (int i = 0; i < 4; i++) {
int xx = x + nextx[i];
int yy = y + nexty[i];
if(xx >= 0 && xx < n && yy >= 0 && yy < m &&visit[xx][yy] == 0 && map[xx][yy] == '0') {
visit[xx][yy] = 1;
dp[xx][yy] = Math.min(dp[xx][yy], s+1);//更新dp
path[s] = direct[i];//更新路径
dfs(xx, yy, s+1);
visit[xx][yy] = 0;//回溯 dfs返回的时候,往往需要对之前做的标记进行重置
}
}
}
}
bfs题目分析
迷宫最短路问题最简单的做法就是使用bfs去做,bfs与dfs的区别是bfs第一次走到终点的路径一定是最短路,他不用把所有可行路径都遍历一次。接下来大致讲一下bfs的过程。
在bfs的过程我是同时多个方向扩展路径,所以每一个点,当前我走到这里需要的步数以及这条路上的方向我都要记录,所以我要自己写一个节点类,里面存了节点坐标和走到该点的一个路径,这个路径既表示了这条路上每一步走的方向也表明了路的长度。x和y表示当前点的坐标,pathString表示走到当前点的路径。
static class node{
int x;
int y;
String pathString;
public node(int x, int y, String pathString) {
super();
this.x = x;
this.y = y;
this.pathString = pathString;
}
}
首先把起点加入队列,并标记起点已经被访问过,
LinkedList<node> queue = new LinkedList<>();//申请一个队列
queue.add(new node(0, 0, ""));//把起点放入队列
visit[0][0] =1;
String shunxv ="";//记录最短路径
然后依次从队列里面取出点,直到队列为空。取出点后我们先判断该点是否为终点节点,如果是说明我们找到了一条最短路,后续不需要遍历了,直接退出。否则我要向四个方向去遍历。通过if语句判断下一个位置的坐标是否越界,下一个位置是否可走,下一个位置是否已经被遍历过,都满足的话我就可以走向下一个位置,把它对应的信息添加到队列里面,然后标记该位置已经被走过。
while(!queue.isEmpty()){
node t = queue.poll();
int x1 = t.x;
int y1 = t.y;
String str1 = t.pathString;
if(x1==n-1&&y1==m-1){//判断是否走到终点
shunxv = str1;
break;
}
for(int i=0;i<4;i++){//向四个方向去遍历
int x2= x1+nextx[i];
int y2= y1+nexty[i];
if(x2>=0&&x2<=n-1&&y2>=0&&y2<=m-1&&map[x2][y2]=='0'&&visit[x2][y2]!=1){
queue.add(new node(x2, y2, str1+direct[i]));
visit[x2][y2]=1;
}
}
}
bfs题目代码
import java.util.LinkedList;
import java.util.Scanner;
public class Main{
/*
* 在步数最少的前提下,请找出字典序最小的一个作为答案
* 如何保证找到的第一个路径一定是字典序最小的
* 遍历的时候,按照字典序从小到大的顺序去遍历
* D<L<R<U
* DDRURRDDDR
* DRRURRDDDR
* 如何记录我们的路径
* bfs
* node{
* x,y,path//走到当前节点的路径
* }
* path
*/
static class node{
int x;
int y;
String pathString;
public node(int x, int y, String pathString) {
super();
this.x = x;
this.y = y;
this.pathString = pathString;
}
}
static int n = 30;
static int m = 50;
static char[][] map = new char[n][m];
static char[] direct = {'D','L','R','U'};
static int[] nexty = {0,-1,1,0};
static int[] nextx = {1,0,0,-1};
static int[][] visit = new int[n][m];
static String res;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
shuju();
sc.close();
bfs();
}
private static void shuju() {
// TODO Auto-generated method stub
String string = "01010101001011001001010110010110100100001000101010"
+"00001000100000101010010000100000001001100110100101"
+"01111011010010001000001101001011100011000000010000"
+"01000000001010100011010000101000001010101011001011"
+"00011111000000101000010010100010100000101100000000"
+"11001000110101000010101100011010011010101011110111"
+"00011011010101001001001010000001000101001110000000"
+"10100000101000100110101010111110011000010000111010"
+"00111000001010100001100010000001000101001100001001"
+"11000110100001110010001001010101010101010001101000"
+"00010000100100000101001010101110100010101010000101"
+"11100100101001001000010000010101010100100100010100"
+"00000010000000101011001111010001100000101010100011"
+"10101010011100001000011000010110011110110100001000"
+"10101010100001101010100101000010100000111011101001"
+"10000000101100010000101100101101001011100000000100" //D -> L -> R -> U
+"10101001000000010100100001000100000100011110101001"
+"00101001010101101001010100011010101101110000110101"
+"11001010000100001100000010100101000001000111000010"
+"00001000110000110101101000000100101001001000011101"
+"10100101000101000000001110110010110101101010100001"
+"00101000010000110101010000100010001001000100010101"
+"10100001000110010001000010101001010101011111010010"
+"00000100101000000110010100101001000001000000000010"
+"11010000001001110111001001000011101001011011101000"
+"00000110100010001000100000001000011101000000110011"
+"10101000101000100010001111100010101001010000001000"
+"10000010100101001010110000000100101010001011101000"
+"00111100001000010000000110111000000001000000001011"
+"10000001100111010111010001000110111010101101111000";
//将上面的一大串字符转为数组中存储,且数组为int型,仅存0 和 1两个值
int[][] moze= new int[30][50];
for (int i = 0 ; i < 30 ; i++) {
for(int j = 0 ; j < 50 ; j++) {
map[i][j] = string.charAt(i*50+j); //
}
}
}
private static void bfs() {
// TODO Auto-generated method stub
LinkedList<node> queue = new LinkedList<>();//申请一个队列
queue.add(new node(0, 0, ""));//把起点放入队列
visit[0][0] =1;
String shunxv ="";//记录最短路径
while(!queue.isEmpty()){
node t = queue.poll();
int x1 = t.x;
int y1 = t.y;
String str1 = t.pathString;
if(x1==n-1&&y1==m-1){//判断是否走到终点
shunxv = str1;
break;
}
for(int i=0;i<4;i++){//向四个方向去遍历
int x2= x1+nextx[i];
int y2= y1+nexty[i];
if(x2>=0&&x2<=n-1&&y2>=0&&y2<=m-1&&map[x2][y2]=='0'&&visit[x2][y2]!=1){
queue.add(new node(x2, y2, str1+direct[i]));
visit[x2][y2]=1;
}
}
}
System.out.println(shunxv);
}
}