深度/广度优先搜索

65 阅读7分钟

在开始正文之前,先讨论一下数据结构是什么?数据结构就是程序运行时数据存放的结构,在万物皆对象的java里面,对象就是存放在堆内存里面。
而有一些对象很特别:
比如数组,是有一片连续的内存组成的,可以通过数组的下标来访问到数组里面的每一个对象。
再比如链表,在自己的内存空间中不仅存储值内容,还有一个指向下一个元素指针/引用,一般都叫个next或者pre之类的名字。
再比如二叉树,有两个引用,比如left和right。
照着这个逻辑一个对象可以有三个引用吗?当然可以比如二叉树再维护一下父元素的引用,left,right和parent。
那四个呢,也可以up down left right。
五个六个七八个呢,个人觉得也是可以的,大不了next1,next2,next3,next4,next5,next6但是代码如果写到这个程度,多少有点不优雅。
一个引用叫链表,两个可以维护一颗二叉树,多个拼起来也是一种数据结构叫做图。图里面的每一个元素叫做顶点。
个人理解觉得链表和二叉树都是图的范畴。都是用引用跟另外的元素连接起来。那么如果一个元素跟另外的100个元素都能连接我是不是要维护100个引用呢。
前辈告诉我们不需要。图有专门的记录方式,最经典的就是两种:
一种是矩阵,比如我有100个元素(顶点),那么维护一个长度是100的数组来记录顶点。然后在维护一个边长是100的矩阵(正方形),用一种标志标记两个顶点是否相连比如1,另外的标志标记连个顶点不相连,比如无穷大或者0。
一种是链表,比如还是100个顶点,还是长度100的数组记录顶点,然后维护一个长度100的链表桶,就跟java里面HashMap的存储结构一样,每个链表记录与该顶点相连的其他顶点。
好了终于进入今天的正文了,深度/广度优先搜索就是为遍历图数据结构的算法。
首先是深度优先,我不关心这个顶点有几个相连元素,我每次只取一个,并标记已访问的元素,直到没有下一个或者所有的都访问到。
然后是广度优先,我把一个定点所有的元素全部访问到(第一层),然后再去访问第二层,第三层直到所有的元素访问完毕。
深度优先搜索不好理解的地方在于回溯。就是如果一条路走不通,需要重新回溯回来重新搜索。
而广度优先搜索需要借助一种数据结构队列,才能完成。大体的逻辑就是先把第一个顶点元素入队列,然后搜索这个顶点所有的节点入队之后第一个定点出队,在队列中再取出另外的元素继续这个过程直到队列元素为空,利用的队列先进先出的特性。
一个算法例子:
宝宝和妈妈参加亲子游戏,在一个二维矩阵(N*N)的格子地图上,
宝宝和妈妈抽签决定各自的位置,地图上每个格子有不同的糖果数量,部分格子有障碍物。
游戏规则是妈妈必须在最短的时间(每个单位时间只能走一步)到达宝宝的位置,
路上的所有糖果都可以拿走,不能走障碍物的格子,只能上下左右走。
请问妈妈在最短到达宝宝位置的时间内最多拿到多少糖果(优先考虑最短时间到达的情况下尽可能多拿糖果)。
表数如下:
// -3:妈妈
// -2:宝宝
// -1:障碍
// ≥0:糖果数(0表示没有糖果,但是可以走)
// 4
// 3 2 1 -3
// 1 -1 1 1
// 1 1 -1 2
// -2 1 2 3
这个题目要求最短时间到达,第一反应是使用广度优先搜索,一层一层的搜索,直到为妈妈找到一条最短的路径。
代码说明,首先看Pos对象注意其visit属性,这个是必须要维护的,网上你凡是能搜索到的免费答案,几乎都是错的,付费的咱不知道。
维护一个全局的是否访问属性,能叫广度搜索吗?稍微给一个复杂点的例子都跑不通。

import java.util.*;  
  
/**  
* 糖果游戏 广度优先搜索  
*/  
public class CandyGameBfs {  
// <p> 宝宝和妈妈参加亲子游戏,在一个二维矩阵(N*N)的格子地图上,宝宝和妈妈抽签决定各自的位置,地图上每个格子有不同的糖果数量,部分格子有障碍物。 </p> <p>  
// 游戏规则是妈妈必须在最短的时间(每个单位时间只能走一步)到达宝宝的位置,路上的所有糖果都可以拿走,不能走障碍物的格子,只能上下左右走。 </p> <div>  
// 请问妈妈在最短到达宝宝位置的时间内最多拿到多少糖果(优先考虑最短时间到达的情况下尽可能多拿糖果)。 </div>  
// -3:妈妈  
// -2:宝宝  
// -1:障碍  
// ≥0:糖果数(0表示没有糖果,但是可以走)  
// 4  
// 3 2 1 -3  
// 1 -1 1 1  
// 1 1 -1 2  
// -2 1 2 3  
  
static int mi = 0,mj = 0,si = 0,sj = 0;  
static Queue<Pos> queue = null; //记录已访问节点  
static List<Pos> posList = null; //记录所有的路线  
  
static Pos end = null;//宝宝的位置  
static int[][] arr;  
public static void main(String[] args) {  
//处理输入  
Scanner scanner = new Scanner(System.in);  
final int n = scanner.nextInt();  
arr = new int[n][n];  
  
for(int i = 0; i < n; i++){  
for(int j = 0; j < n; j++){  
arr[i][j] = scanner.nextInt();  
if(arr[i][j] == -3){  
mi = i;  
mj = j;  
}else if(arr[i][j] == -2){  
si = i;  
sj = j;  
}  
}  
}  
//初始化数据  
queue = new LinkedList<>(); //记录已访问节点  
posList = new LinkedList<>(); //记录所有的路线  
Pos start = new Pos(mi,mj,0,new boolean[n][n]); //开始位置  
end = new Pos(si,sj,0);  
queue.offer(start);  
bfs(queue.poll());  
  
  
//打印结果  
System.out.println(posList.size());  
for(Pos pos : posList){  
int size = 0;  
int sum = 0;  
while (pos != null){  
System.out.println(pos.x + " " + pos.y + " "+ pos.count);  
pos = pos.pre;  
if(pos != null && pos.count > 0){  
sum+=pos.count;  
size++;  
}  
}  
System.out.println(size + 1 + " "+ sum);  
System.out.println("===");  
}  
}  
  
/**  
* //1,将第一个元素加入队列  
* //2,搜索这个元素相邻的所有元素加入队列。之后第一个元素可以出队列  
* //3,拿出队首的一个元素重复1,2  
* //4,如果找到了孩子,记录当前路径,  
* //5,队列为空,搜索结束  
* 广度搜索不需要回溯  
* @param start  
*/  
public static void bfs(Pos start){  
  
if(start == null){ //队列空了 结束方法  
return;  
}  
if(start.equals(end)){ //找到了,加入结果集  
posList.add(start);  
}  
  
start.visit[start.x][start.y] = true; //已访问 这个地方还是值得考虑一下的。  
  
  
//向上  
if(start.x > 0 && !start.visit[start.x - 1][start.y] && arr[start.x - 1][start.y] != -1){  
Pos tmp = new Pos(start.x-1,start.y,arr[start.x-1][start.y], start.visit);  
tmp.pre = start; //维护路径链表  
queue.offer(tmp);  
}  
  
//向下  
if(start.x < arr.length-1 && !start.visit[start.x + 1][start.y] && arr[start.x + 1][start.y] != -1){  
Pos tmp = new Pos(start.x+1,start.y,arr[start.x+1][start.y], start.visit);  
tmp.pre = start;  
queue.offer(tmp);  
}  
  
//向左  
if(start.y > 0 && !start.visit[start.x][start.y - 1] && arr[start.x][start.y-1] != -1){  
Pos tmp = new Pos(start.x,start.y - 1,arr[start.x][start.y-1], start.visit);  
tmp.pre = start;  
queue.offer(tmp);  
}  
  
//向右  
if(start.y < arr.length - 1 && !start.visit[start.x][start.y+1] && arr[start.x][start.y+1] != -1){  
Pos tmp = new Pos(start.x,start.y+1,arr[start.x][start.y+1], start.visit);  
tmp.pre = start;  
queue.offer(tmp);  
}  
  
//到这里 搜有的相邻元素都加入队列了。  
bfs(queue.poll());  
}  
  
  
static class Pos{  
int x;  
int y; //当前位置  
int count; //糖果数量  
Pos pre; //路径链表  
boolean[][] visit; //标记是否已访问  
int size; //路径链表长度,因为要寻找最短路径,记录该字段可判断是否大于该字段,大于即可结束搜索,该示例未维护。  
public Pos(int x,int y,int count){  
this.x = x;  
this.y = y;  
this.count = count;  
}  
public Pos(int x,int y,int count,boolean[][] visited){  
this.x = x;  
this.y = y;  
this.count = count;  
boolean[][] visit = new boolean[visited.length][visited[0].length];  
for(int i = 0; i < visited.length; i++){  
System.arraycopy(visited[i], 0, visit[i], 0, visited[0].length);  
}  
this.visit = visit;  
}  
  
/**  
* 重写该方法 只要x,y相等该对象就相等  
*/  
@Override  
public boolean equals(Object o) {  
if (this == o) return true;  
if (o == null || getClass() != o.getClass()) return false;  
Pos pos = (Pos) o;  
return x == pos.x && y == pos.y;  
}  
  
@Override  
public int hashCode() {  
return Objects.hash(x, y);  
}  
}  
}