可以先refer to这篇C++版文章
广度优先搜索(Breadth First Search , BFS)遍历类似于树的按层次遍历的过程,则是以广度为第一关键词,当碰到岔道口时,总是先依次访问从该岔道口能直接到达的所有结点,然后再按这些结点被访问的顺序去依次访问它们能直接到达的所有结点,以此类推,直到所有结点都被访问为止。
广度优先搜索的整个算法过程很像一个队列,因此,广度优先搜索一般由队列实现,且总是按层次进行遍历,其基本写法如下(可用作模板用):
void bfs(int s){
Queue<Integer> queue=new LinkedList<Integer>();
queue.offer(s);
while(!queue.isEmpty()){
//取出队首元素top
//访问队首元素top
//将队首元素出队
//将top的下一层结点中未曾入队的结点全部入队,并设置为已入队
}
}
下面是对该模板中每一个步骤的说明,请结合代码一起看:
①定义队列q,并将起点s入队。
②写一个 while循环,循环条件是队列q非空。
③在while循环中,先取出队首元素top,然后访问它(访问可以是任何事情,例如将其输出)。访问完后将其出队。
④将top的下一层结点中所有未曾入队的结点入队,并标记它们的层号为now的层号加1,同时设置这些入队的结点已入过队。
⑤返回②继续循环。
BFS要用到队列(Queue,可用LinkedList来实现,它继承了Queue接口)
队列示例代码:
import java.util.LinkedList;
import java.util.Queue;
public class Main {
public static void main(String[] args) {
//add()和remove()方法在失败的时候会抛出异常(不推荐)
Queue<String> queue = new LinkedList<String>();
//添加元素
queue.offer("a");
queue.offer("b");
queue.offer("c");
queue.offer("d");
System.out.println(queue.size());
for(String q : queue){
System.out.println(q);
}
System.out.println("===");
System.out.println("poll="+queue.poll()); //返回第一个元素,并在队列中删除
for(String q : queue){
System.out.println(q);
}
System.out.println("===");
System.out.println("element="+queue.element()); //返回第一个元素
for(String q : queue){
System.out.println(q);
}
System.out.println("===");
System.out.println("peek="+queue.peek()); //返回第一个元素
for(String q : queue){
System.out.println(q);
}
}
}
BFS原理刨析:
BFS就是一层一层的搜索,即先把根节点入队,判断队列不为空,根据队首元素(刚开始就是根节点)再把队首元素下一层的节点全部入队,弹出队首元素,再重新判断队列是否为空,根据队首元素再将队首的下一层入队,一次反复。
因为BFS问题是一层一层的搜索,所以它非常适合搜索最短路,找到的第一个合法解就是最优解(即层数)!
下面举两个例子,希望读者能从中学习BFS的思想是如何通过队列来实现的:
岛屿问题(块问题):
给出一个n*m的矩阵,矩阵中的元素为0或1。称位置(x,y)与其上下左右四个位置(x,y+1)、(x,y-1)、(x+1,y)、(x-1,y)是相邻的。如果矩阵中有若干个1是相邻的(不必两两相邻),那么称这些1构成了一个“块”。求给定的矩阵中“块”的个数。
0 1 1 1 0 0 1
0 0 1 0 0 0 0
0 0 0 0 1 0 0
0 0 0 1 1 1 0
1 1 1 0 1 0 0
1 1 1 1 0 0 0
上面的6*7矩阵中,“块”的个数为4
对这个问题,求解的基本思想是:枚举每一个位置的元素,如果为0,则跳过;如果为1,则使用BFS查询与该位置相邻的4个位置(前提是不出界),判断它们是否为1(如果某个相邻的位置为1,则同样去査询与该位置相邻的4个位置,直到整个“1”块访问完毕)。而为了防止走回头路,一般可以设置一个bool型数组inq(即In queue的简写)来记录每个位置是否在BFS中已入过队。
一个小技巧是:对当前位置(x,y)来说,由于与其相邻的四个位置分别为(x,y+1)、(x,y-1)、(x+1,y)、(x-1,y),那么不妨设置下面两个增量数组,来表示四个方向(竖着看即为(0,1)、(0,-1)、(1,0)、(-1,0))。
int X[] = {0,0,1,-1};
int Y[] = {1,-1,0,0};
这样就可以使用for循环来枚举4个方向,以确定与当前坐标(nowX,nowY)相邻的4个位置,如下所示:
for(int i=0;i<4;i++) {
newX = nowX+X[i];
newY = nowY+Y[i];
}
输入:
6 7
0 1 1 1 0 0 1
0 0 1 0 0 0 0
0 0 0 0 1 0 0
0 0 0 1 1 1 0
1 1 1 0 1 0 0
1 1 1 1 0 0 0
输出:
4
代码:
疑问:为什么bfs的判断里每次都要生成新结点??????,如果不生成新结点输出是9
解决:
因为如果继续用同名的node会造成队列里前面同名的node的覆盖,所以需要新生成结点。如下:
Queue<Node> q=new LinkedList<Node>();
Node node=new Node();
node.x=1;node.y=2;
q.offer(node);
node.x=3;node.y=3;
q.offer(node);
for(Node qq:q){
System.out.println(qq.x+","+qq.y+" ");
}
输出:
明显看到队首元素也成了(3,3),但不知道是为什么。。
3,3
3,3
correct code:
import java.util.LinkedList;
import java.util.Queue;
import java.util.Scanner;
class Node{
int x,y;//位置(x,y)
}
public class 岛屿问题_块问题 {
static Node node=new Node();
//static int n,m;//矩阵大小
static int[][] a;//01矩阵
static Boolean[][] inq;//记录是否入过队
//增量数组,组合分别代表上下右左
static int X[] = {0,0,1,-1};
static int Y[] = {1,-1,0,0};
//判断坐标(x,y)是否需要访问
static Boolean judge(int x,int y,int n,int m){
//越界
if(x>=n||x<0||y>=m||y<0){
return false;
}
//当前位置为0或已入过队
if(a[x][y]==0||inq[x][y]==true){
return false;
}
return true;
}
//BFS函数访问位置(x,y)所在的块,将该块中所有 "1" 的inq都设置为true
static void bfs(int x,int y,int n,int m){
Queue<Node> queue=new LinkedList<Node>();//定义队列
node.x=x;
node.y=y;
queue.offer(node);//结点入队
inq[x][y]=true;//设置(x,y)已入过队
while(!queue.isEmpty()){
Node top=queue.poll();//返回队首元素并将其出队
for(int i=0;i<4;i++){
int newX=top.x+X[i];
int newY=top.y+Y[i];
if(judge(newX,newY,n,m)){//如果新位置(newX,newY)需要访问
//设置node的坐标为(newX,newY)
Node node1=new Node();//为什么每次都得生成新结点???
node1.x = newX;
node1.y = newY;
queue.offer(node1);//将node加入对列
inq[newX][newY]=true;//设置位置(newX,newY)已入过队
}
}
}
}
public static void main(String[] args) {
// TODO 自动生成的方法存根
Scanner in=new Scanner(System.in);
int n=in.nextInt();
int m=in.nextInt();
a=new int[n][m];
inq=new Boolean[n][m];
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
inq[i][j]=false;
a[i][j]=in.nextInt();
}
}
int ans=0;//岛屿个数
for(int i=0;i<n;i++){//枚举每一个位置
for(int j=0;j<m;j++){
//如果元素为1且未入队
if(a[i][j]==1&&inq[i][j]==false){
ans++;//岛屿个数加1
bfs(i,j,n,m);//访问整个块,将该块所有 "1"的inq都标记为true
}
}
}
System.out.println(ans);
}
}
再举一个例子:
迷宫(最短路问题)
给定一个n*m大小的迷宫,其中*代表不可通过的墙壁,而“.”代表平地,S表示起点,T代表终点。移动过程中,如果当前位置是(x,y)(下标从0开始),且每次只能前往上下左右、(x,y+1)、(x,y-1)、(x-1,y)、(x+1,y)四个位置的平地,求从起点S到达终点T的最少步数。
.....
.*.*.
.*S*.
.***.
...T*
上面样例S为(2,2),T的坐标为(4,3)。
思路:
在本题中,由于求的是最少步数,而BFS是通过层次的顺序来遍历的,因此可以从起点S开始计数遍历的层数,那么在到达终点T时的层数就是需要求解的起点S到达终点T的最少步数。
输入:
5 5
.....
.*.*.
.*S*.
.***.
...T*
2 2 4 3
输出:
11
代码:
没理解node在bfs里的位置。。。这样前面的不是又被后面的覆盖了吗
import java.util.LinkedList;
import java.util.Queue;
import java.util.Scanner;
public class 迷宫_最短路问题 {
static String[][] a;//迷宫
static Boolean[][]inq;
static int[] X={0,0,1,-1};//增量数组
static int[] Y={1,-1,0,0};
static Node1 S=new Node1();
static Node1 T=new Node1();
//检测位置(x,y)是否有效
static Boolean test(int x,int y,int n,int m){
if(x>=n||x<0||y>=m||y<0){ //越界
return false;
}
if(a[x][y]=="*"){ //墙壁
return false;
}
if(inq[x][y]==true){ //已入过队
return false;
}
return true;
}
static int bfs(int n,int m){
Queue<Node1> q=new LinkedList<Node1>();
q.offer(S);
while(!q.isEmpty()){
Node1 top=q.poll();
if(top.x==T.x&&top.y==T.y){
return top.step;
}
Node1 node=new Node1();//?????????
for(int i=0;i<4;i++){//循环四次,得到四个相邻位置
int newX=top.x+X[i];
int newY=top.y+Y[i];
if(test(newX,newY,n,m)){//位置 (newX,newY)有效
node.x=newX;node.y=newY;//设置Node的坐标是(newX,newY)
node.step=top.step+1;//Node层数为top的层数+1
q.offer(node);//将结点Node加入队列
inq[newX][newY]=true;//设置位置 (newX,newY) 已经入过队
}
}
}
return -1;//无法到达终点T时返回-1
}
public static void main(String[] args) {
// TODO 自动生成的方法存根
Scanner in=new Scanner(System.in);
int n=in.nextInt();
int m=in.nextInt();
in.nextLine();
a=new String[n][m];
inq=new Boolean[n][m];
for(int i=0;i<n;i++){
String[] str=in.nextLine().split("");
for(int j=0;j<m;j++){
a[i][j]=str[j+1];
inq[i][j]=false;
}
}
//起点和重点的坐标
S.x=in.nextInt();
S.y=in.nextInt();
T.x=in.nextInt();
T.y=in.nextInt();
S.step=0;//初始化起点的层数为0,即S到S的最小步数为0
System.out.println(bfs(n,m));
}
}
class Node1{
int x,y;//位置(x,y)
int step;//step为从起点S到达该位置的最少步数(即层数)
}
再强调一点,在BFS中设置的inq数组的含义是,判断结点是否已入过队,而不是结点是否己被访问。区别在于:如果设置成是否已被访问,有可能在某个结点正在队列中(但还未访问)时由于其他结点可以到达它而将这个结点再次入队,导致很多结点反复入队,计算量大大增加。因此BFS中让每个结点只入队一次,故需要设置inq数组的含义为结点是否已入过队而非结点是否已被访问。