图论基础
1 什么是图
1.1 基础定义
图(Graph)是一个用于描述一组对象之间关系的数学结构。这些对象被称为顶点(Vertex),也称为节点(Node)或点(Point),而对象之间的关系则通过边(Edge)来表示,边也称为链接(Link)或线(Line)。图通常以图解形式描绘为顶点的一组点或环,并通过边的线或曲线连接。
1.2 图的基本术语
- 顶点 (Vertex)
- 顶点是图的基本元素,它们代表图中的对象或实体。在图形表示中,顶点通常以点或圆圈的形式出现。
- 一个图G的顶点集合通常用V(G)表示,其中每个元素都是图中的一个顶点。
- 边 (Edge)
- 边是连接图中两个顶点的线段或弧线,表示顶点之间的某种关系或连接。
- 在无向图中,边没有方向,连接的两个顶点是对称的。
- 在有向图中,边具有方向,从一个顶点指向另一个顶点,表示一种单向关系。
- 图的边集合通常用E(G)表示,其中每个元素都是一条边,由它所连接的两个顶点定义。
- 度 (Degree)
- 一个顶点的度是指与该顶点直接相连的边的数量。
- 在无向图中,顶点的度就是与其相连的边的数量。
- 在有向图中,顶点的度可以细分为入度(指向该顶点的边的数量)和出度(从该顶点出发的边的数量)。
- 路径 (Path)
- 路径是图中顶点和边交替出现的一个序列,其中序列中的每对连续顶点都由一条边相连。
- 路径的起点和终点可以是同一个顶点,此时称为环(Cycle)。
- 路径的长度是指路径中边的数量。
- 连通性 (Connectivity)
- 在无向图中,如果图中任意两个顶点之间都存在路径,则称该图为连通图。
- 在有向图中,如果对于每一对顶点u和v,都存在从u到v的路径(或反之),则称该图为强连通图。
- 如果一个图不是连通的(或强连通的),但它可以分成几个连通(或强连通)的子图,则这些子图称为连通分量(或强连通分量)。
- 子图 (Subgraph)
- 如果图G'的顶点集合V'是G的顶点集合V的子集,且G'的边集合E'中的每条边都是G的边集合E中的边,并且与V'中的顶点相关联,则称G'是G的子图。
- 环 (Cycle)
- 环是一种特殊的路径,它的起点和终点是同一个顶点,并且除了起点和终点外,路径中的顶点和边都不重复。
- 权重 (Weight)
- 在带权图中,每条边都关联一个权重值,这个值可以表示两个顶点之间关系的某种度量,如距离、成本、时间等。
- 邻接性 (Adjacency)
- 如果两个顶点由一条边相连,则称它们是相邻的。在无向图中,邻接是对称的;在有向图中,邻接是有方向的。
1.3 图的分类
一、按边有无方向分类
-
无向图(Undirected Graph)
- 定义:图中的边没有方向,即如果顶点v到顶点w有一条边,那么顶点w到顶点v也存在同一条边。
- 特点:边表示顶点之间的对称关系。
- 示例:社交网络中两个人的好友关系,通常视为无向的,因为一个人将另一个人视为好友,则另一个人也将前者视为好友。
-
有向图(Directed Graph)
- 定义:图中的边具有方向,即如果顶点v到顶点w有一条边,但并不意味着顶点w到顶点v也存在同一条边。
- 特点:边表示顶点之间的非对称关系,有向边通常带有箭头,表示方向。
- 示例:微博中用户之间的关注关系,是单向的,用户A关注用户B,并不意味着用户B也关注用户A
二、按节点和边的类别数量分类
- 同构图(Homogeneous Graph)
- 定义:图中只有一种类型的节点和一种类型的边。
- 特点:结构简单,易于处理和分析。
- 示例:传统的社交网络图,其中所有节点都代表用户,所有边都代表用户之间的关系(如好友关系)。
- 异构图(Heterogeneous Graph)
- 定义:图中存在多种类型的节点和/或多种类型的边。
- 特点:结构复杂,但能够更准确地表示现实世界中的复杂关系。
- 示例:知识图谱,其中节点可能代表不同类型的实体(如人、地点、组织等),边可能代表不同类型的关系(如出生地、工作于、隶属于等)。
三、按边是否带有权重分类
- 无权图(Unweighted Graph)
- 定义:图中的边没有权重,仅表示顶点之间是否存在关系。
- 特点:简单直观,适用于不需要考虑边的重要性或强度的场景。
- 示例:简单的社交网络图,其中边仅表示两个人是否是好友,而不考虑他们之间关系的紧密程度。
- 有权图(Weighted Graph)
- 定义:图中的边带有权重,权重表示边的重要性或强度。
- 特点:能够更准确地表示顶点之间的关系,适用于需要考虑边的重要性或强度的场景。
- 示例:交通网络图,其中边表示道路,权重可能表示道路的长度、行驶时间或交通流量等。
2 图的表示
2.1 邻接矩阵
什么是邻接矩阵
邻接矩阵是一个二维数组(矩阵),其大小为n×n,其中n是图中顶点的数量。矩阵中的元素a[i][j]表示顶点i与顶点j之间的连接关系。根据图的不同类型(如无向图、有向图、有权图等),a[i][j]的取值和含义也会有所不同。
- 对于无向图,如果顶点i与顶点j相连,则a[i][j]和a[j][i]都为1(或边的权重,如果图是有权图的话);如果顶点i与顶点j不相连,则a[i][j]和a[j][i]都为0。由于无向图的邻接矩阵是对称的,因此在实际存储时,可以只存储上(或下)三角矩阵的元素,以节省空间。
- 对于有向图,如果顶点i到顶点j有一条有向边,则a[i][j]为1(或边的权重);如果顶点i到顶点j没有有向边,则a[i][j]为0。此时,邻接矩阵不一定是对称的。
- 对于有权图,无论是有向还是无向,a[i][j]的值都表示顶点i与顶点j之间边的权重。如果顶点i与顶点j不相连,则a[i][j]通常用一个特殊的值来表示,如无穷大(∞)或某个足够大的数,以便在算法中区分不相连的顶点对。
示例说明:
假设有一个无向图G,其顶点集合为V={A, B, C, D},边集合为E={(A,B), (A,C), (B,C), (B,D)}。则图G的邻接矩阵可以表示为:
A | B | C | D | |
---|---|---|---|---|
A | 0 | 1 | 1 | 0 |
B | 1 | 0 | 1 | 1 |
C | 1 | 1 | 0 | 0 |
D | 0 | 1 | 0 | 0 |
在这个邻接矩阵中,元素a[i][j]为1表示顶点i与顶点j相连,为0表示不相连。由于是无向图,所以矩阵是对称的。
优缺点
优点:
- 直观易懂:通过矩阵的形式,可以直观地看出图中顶点之间的连接关系。
- 便于计算:在邻接矩阵中,可以方便地通过索引来访问和修改顶点之间的连接关系。
缺点:
- 空间复杂度高:对于大型稀疏图来说,邻接矩阵会浪费大量的存储空间来存储不存在的边。
- 访问效率低:在稀疏图中,访问某个顶点的所有邻接点时需要遍历整行(或整列),效率较低。
2.2 邻接表
什么是邻接表
邻接表的基本思想是为图中的每个顶点维护一个单链表,链表中存储的是与该顶点直接相连的其他顶点。对于无向图而言,由于边是双向的,所以每个顶点的邻接链表中需要包含所有与之相连的其他顶点;而对于有向图,邻接链表则只包含该顶点的出边所指向的顶点。
邻接表基础实现思想:
- 顶点存储:通常,我们可以使用一个一维数组来存储图中的顶点信息。数组的每个元素对应图中的一个顶点,其索引值可以作为顶点的标识符。
- 邻接链表:对于每个顶点,都维护一个单链表,链表中的节点表示与该顶点直接相连的其他顶点。链表节点的结构一般包含至少两个部分:一个存储邻接顶点在顶点数组中的索引(或直接存储邻接顶点对象),另一个是指向下一个链表节点的指针。
- 头指针数组:为了方便地访问每个顶点的邻接链表,我们可以使用一个额外的数组来存储每个顶点邻接链表的头指针。这样,我们就可以通过顶点的索引直接找到其对应的邻接链表。
示例说明:
假设有一个无向图G,其顶点集合为V={A, B, C, D},边集合为E={(A,B), (A,C), (B,C), (B,D)}。使用邻接表来表示该图的结构如下:
顶点 | 邻接链表 |
---|---|
A | B → C |
B | A → C → D |
C | A → B |
D | B |
在这个示例中,每个顶点都对应一个邻接链表,链表中存储与该顶点直接相连的其他顶点(通过顶点在数组中的索引表示)。例如,顶点A的邻接链表包含顶点B和C,表示顶点A与顶点B和C相连。
优缺点
优点:
- 节省空间:对于稀疏图而言,邻接表能够显著减少存储空间的浪费,因为它只存储实际存在的边和顶点信息。
- 添加和删除边方便:在邻接表中添加或删除边通常只需要在对应的邻接链表中添加或删除节点,操作的时间复杂度较低。
- 便于查找顶点的邻接点:通过邻接表,我们可以快速地找到与给定顶点直接相连的所有顶点。
缺点:
- 查找边较慢:在邻接表中查找两个顶点之间是否存在边可能需要遍历其中一个顶点的邻接链表,时间复杂度较高。
- 表示有向图时可能不够直观:对于有向图而言,邻接表只能表示顶点的出边,如果需要表示入边,则需要额外的数据结构(如逆邻接表)。
2.3 代码示例
2.3.1 邻接矩阵
java
public class MatrixGraph {
// 顶点数量
private int V;
// 邻接矩阵
private int[][] adjMatrix;
// 构造函数,初始化顶点数量和邻接矩阵
public MatrixGraph(int V) {
this.V = V;
adjMatrix = new int[V][V];
}
// 添加边
// 对于无向图,你还需要调用addEdge(v, w);
// 对于有权图,你可以将int[][] adjMatrix改为Integer[][],并将addEdge中的值设置为边的权重
public void addEdge(int v, int w) {
adjMatrix[v][w] = 1; // 对于无权图,我们使用1表示存在边,0表示不存在边
// 注意:对于有向图,我们不需要adjMatrix[w][v] = 1;
}
// 打印邻接矩阵
public void printGraph() {
for (int i = 0; i < V; i++) {
for (int j = 0; j < V; j++) {
System.out.print(adjMatrix[i][j] + " ");
}
System.out.println();
}
}
// 主函数,用于测试Graph类
public static void main(String[] args) {
MatrixGraph g = new MatrixGraph(4);
g.addEdge(1, 2);
g.addEdge(1, 5);
g.addEdge(2, 4);
g.addEdge(3, 1);
g.addEdge(4, 3);
System.out.println("邻接矩阵为:");
g.printGraph();
}
}
2.3.2 邻接表
java
package cn.zxc.demo.leetcode_demo.advanced_data_structure.graph;
import java.util.ArrayList;
import java.util.List;
public class LinkGraph {
private int V; // 图的顶点数
private List<List<Integer>> adjList; // 邻接表
// 构造函数
public LinkGraph(int V) {
this.V = V;
adjList = new ArrayList<>(V);
for (int i = 0; i < V; i++) {
adjList.add(new ArrayList<>());
}
}
// 添加边
// 对于无向图,需要调用addEdge(v, w)和addEdge(w, v)
public void addEdge(int v, int w) {
adjList.get(v).add(w); // 在顶点v的邻接链表中添加顶点w
}
// 打印图(仅打印邻接表)
public void printGraph() {
for (int i = 0; i < V; i++) {
System.out.print("顶点 " + i + " 的邻接顶点: ");
List<Integer> list = adjList.get(i);
for (int neighbor : list) {
System.out.print(neighbor + " ");
}
System.out.println();
}
}
// 主函数,用于测试Graph类
public static void main(String[] args) {
LinkGraph g = new LinkGraph(6);
g.addEdge(1, 2);
g.addEdge(1, 5);
g.addEdge(2, 4);
g.addEdge(3, 1);
g.addEdge(4, 3);
System.out.println("图的邻接表表示:");
g.printGraph();
}
}
3 图的遍历
3.1 Dfs(深度优先遍历)
基本思想
深度优先搜索算法从根节点(或选定的起始节点)开始,尽可能深地搜索树的分支,直到达到叶子节点。然后,它回溯到前一个节点,继续搜索下一个分支。这个过程一直进行到所有节点都被访问为止。如果图中存在未访问的节点,算法会选择其中一个作为新的起始节点,并重复上述过程。
深度优先搜索的原理是递归或迭代实现回溯。递归方式更为直观和常用,它使用函数调用栈来隐式地维护一个节点栈;迭代方式则需要显式地维护一个栈来存储待访问的节点。
实现步骤
- 初始化
- 对于递归实现,初始化通常意味着设置起始节点和必要的辅助数据结构(如访问标记数组)。
- 对于迭代实现,还需要初始化一个栈来存储待访问的节点。
- 搜索过程
- 递归实现中,算法通过递归调用自身来遍历节点的所有未访问子节点。
- 迭代实现中,算法不断地从栈中取出节点,并遍历其所有未访问的邻接节点,将未访问的邻接节点加入栈中。
- 回溯
- 当一个节点的所有邻接节点都被访问过,且没有未访问的分支时,算法会回溯到该节点的父节点,继续搜索其他分支。
- 结束条件
- 当所有节点都被访问过,或者栈为空且没有未访问的节点时,算法结束。
优缺点
优点:
- 内存消耗小:与广度优先搜索相比,深度优先搜索的空间复杂度较低,特别是在树或稀疏图中。
- 实现简单:深度优先搜索的实现相对简单,只需要使用栈(隐式或显式)和访问标记数组。
- 适用于深度探索:在需要深度探索的场景中(如迷宫问题、生成树的构造等),深度优先搜索非常有效。
缺点:
- 难以寻找最优解:在需要找到最优解的场景中(如最短路径问题),深度优先搜索可能不是最佳选择,因为它会深入探索一个分支而忽略其他可能更优的分支。
- 可能陷入无限循环:在非连通图或包含环的图中,如果算法设计不当,可能会导致无限循环。因此,需要设置适当的终止条件或使用访问标记数组来避免重复访问节点。
代码示例
java
package cn.zxc.demo.leetcode_demo.advanced_data_structure.graph;
/**
* 有向图深度优先遍历
* 1、使用邻接矩阵存储图结构
* 2、对邻接矩阵进行深度优先遍历
* 从 第一个 顶点开始遍历,往下遍历,如果遇到新的顶点,则继续往下遍历,直到
*/
public class Dfs {
// 顶点个数 0 ~ (v - 1)
private int v;
// 邻接矩阵
private int[][] graph;
// a点 到 b点 的最小步数
private int MIN_STEP = Integer.MAX_VALUE;
public Dfs(int v) {
this.v = v;
this.graph = new int[v][v];
// 为每一个顶点赋值1,例如 0-0 1-1 2-2 3-3
for (int i = 0; i < v; i++) {
this.graph[i][i] = 1;
}
}
// 初始化邻接矩阵
// 输入的二维数组,表示的是可以联通的顶点 例如:{{1,2},{1,5},{2,4},{3,1},{4,3}}
// {1,2} 表示顶点1和顶点2可以联通
public void init(int[][] inits){
for (int[] init : inits) {
if (init[0] > v || init[1] > v)
throw new RuntimeException("顶点个数超出范围");
graph[init[0]][init[1]] = 1;
}
}
// 通过深度搜索的方式,判断是否存在从startX 到 endX 的路径
public boolean find_dfs(int startX, int endX){
boolean[] visited = new boolean[v];
visited[startX] = true;
return dfs(startX, endX, visited);
}
/**
* 递归实现深度搜索
* @param startX 起点
* @param endX 终点
* @param visited 访问过的顶点【可以防止当图出现环时不断递归(死循环--栈溢出)】
* @return
*/
private boolean dfs(int startX, int endX,boolean[] visited){
// 到达目标地点
if (startX == endX){
return true;
}
// 由于是有向图,所以直接以起点为横坐标遍历起点可以到达的所有顶点
for (int i = 0; i < v; i++) {
// 如果可以联通,并且没有被访问过
if (graph[startX][i] == 1 && !visited[i]){
visited[i] = true;
if (dfs(i, endX, visited)){
return true;
}
visited[i] = false;
}
}
return false;
}
// 通过深度搜索的方式,查找到从startX 到 endX 的最小步数
public Integer getMinStep(int startX, int endX){
boolean[] visited = new boolean[v];
visited[startX] = true;
return dfs(startX, endX, 0, visited);
}
/**
* 递归实现深度搜索
* @param startX 起点
* @param endX 终点
* @param step 步长
* @param visited 访问过的顶点【可以防止当图出现环时不断递归(死循环--栈溢出)】
* @return
*/
public Integer dfs(int startX, int endX, int step,boolean[] visited){
if (startX == endX){
if (step + 1 < MIN_STEP){
MIN_STEP = step + 1;
}
return MIN_STEP;
}
for (int i = 0; i < v; i++) {
if (graph[startX][i] == 1 && !visited[i]){
visited[i] = true;
dfs(i, endX,step + 1, visited);
visited[i] = false;
}
}
return MIN_STEP;
}
public static void main(String[] args) {
int[][] inits = {{1,2},{1,5},{2,4},{3,1},{4,3}};
Dfs dfs = new Dfs(6);
dfs.init(inits);
System.out.println(dfs.find_dfs(5,3));
System.out.println(dfs.getMinStep(1,5));
}
}
3.2 Bfs(广度优先遍历)
基本思想
广度优先搜索(Breadth-First Search, BFS)从根节点(或选定的起始节点)开始,逐层地向外扩展搜索,即先访问起始节点的所有相邻节点,然后再访问这些节点的相邻节点,依此类推,直到找到目标节点或遍历完所有节点。
实现步骤
- 初始化
- 创建一个队列,用于存储待访问的节点。
- 创建一个访问标记数组或集合,用于记录哪些节点已被访问过。
- 将起始节点加入队列,并标记为已访问。
- 搜索过程
- 当队列不为空时,执行以下步骤:
- 从队列中取出一个节点。
- 访问该节点(如打印节点值、进行某种计算等)。
- 遍历该节点的所有邻接节点:
- 如果邻接节点未被访问过,则将其加入队列,并标记为已访问。
- 当队列不为空时,执行以下步骤:
- 结束条件
- 当队列为空时,算法结束。此时,所有可达的节点都已被访问过。
优缺点
优点:
- 找到最短路径:在无权图中,广度优先搜索能够找到从起始节点到目标节点的最短路径(按边的数量计算)。
- 实现简单:广度优先搜索的实现相对简单,只需要使用队列和访问标记数组。
- 适用于层次遍历:广度优先搜索特别适用于需要按层次遍历的场景,如二叉树的层序遍历。
缺点:
- 空间复杂度较高:在最坏情况下(即图为完全图时),广度优先搜索需要存储大量的节点,导致空间复杂度较高。
- 不适用于有权图:在有权图中,广度优先搜索找到的路径可能不是最短路径(按边的权重和计算)。
代码示例
java
package cn.zxc.demo.leetcode_demo.advanced_data_structure.graph;
import java.util.concurrent.ArrayBlockingQueue;
/**
* 有向图广度搜索
* 基础说明:使用邻接矩阵存储有向图
* 搜索步骤:
* 1、创建一个队列,将起点加入队列
* 2、从队列中取出一个点,判断是否是终点,如果是则返回,否则将点加入已访问集合,将点邻接点加入队列
* 3、重复步骤2,直到队列为空
*/
public class Bfs {
// 顶点数
private int v;
// 邻接矩阵
private int[][] adjMatrix;
public Bfs(int v)
{
this.v = v;
this.adjMatrix = new int[v][v];
// 为每一个顶点赋值1,例如 0-0 1-1 2-2 3-3
for (int i = 0; i < v; i++) {
this.adjMatrix[i][i] = 1;
}
}
// 初始化邻接矩阵
// 输入的二维数组,表示的是可以联通的顶点 例如:{{1,2},{1,5},{2,4},{3,1},{4,3}}
// {1,2} 表示顶点1和顶点2可以联通
public void init(int[][] inits){
if (inits == null || inits.length > v){
throw new RuntimeException("输入的二维数组不合法");
}
for (int[] init : inits) {
adjMatrix[init[0]][init[1]] = 1;
}
}
/**
* 判断 起点 到 终点 是否存在路径
* @param src 起点
* @param dest 终点
* @return
* @throws InterruptedException
*/
public boolean find_bfs(int src, int dest) throws InterruptedException {
boolean[] visited = new boolean[v];
return bfs(src,dest,visited);
}
/**
* 广度搜索
* @param src 起点
* @param dest 终点
* @param visited 访问过的顶点
* @return
* @throws InterruptedException
*/
public boolean bfs(int src, int dest,boolean[] visited) throws InterruptedException {
if (src == dest){
return true;
}
ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(v);
queue.put(src);
visited[src] = true;
while (!queue.isEmpty()){
Integer poll = queue.poll();
if (poll == dest){
return true;
}
for (int i = 0; i < v; i++) {
if (adjMatrix[poll][i] == 1 && !visited[i]){
queue.put(i);
visited[i] = true;
}
}
}
return false;
}
public static void main(String[] args) throws InterruptedException {
int[][] inits = {{1,2},{1,5},{2,4},{3,1},{4,3}};
Bfs dfs = new Bfs(6);
dfs.init(inits);
System.out.println(dfs.find_bfs(1,3));
}
}
4 例题
解救美女1:有一天,小美和你去玩迷宫。但是方向感不好的小美很快就迷路了,你得知后便去解救无助的小美,你已经弄清楚了迷宫的地图,现在你要知道从你当前位置出发你是否能够达到小美的位置?
思路
使用邻接矩阵表示迷宫地图
1:表示地图上的障碍物,0表示有路可走邻接矩阵:
邻接矩阵:
0(你) 0 1 0
0 0 0 0
0 0 1 0
0 1 0(小美) 0
0 0 0 1
4.1 是否能够到达
使用BFS遍历实现
java
package cn.zxc.demo.leetcode_demo.advanced_data_structure.graph;
import java.util.concurrent.ArrayBlockingQueue;
/**
* 有一天,小美和你去玩迷宫。但是方向感不好的小美很快就迷路了,你得知后便去解救无助的小美,你已经弄清楚了迷宫的地图,现在你要知道从你当前位置出发你是否能够达到小美的位置?
* 起点默认为 {0, 0}
*/
public class MazeBfsDemo {
// 地图:邻接矩阵
private int[][] map;
// 可以走动的方向
private int[][] dir = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
/**
* 初始化迷宫地图
* @param weight 宽
* @param height 高
* @param obstacle 障碍点
*/
public MazeBfsDemo(int x, int y, int[][] obstacle) {
this.map = new int[x][y];
for (int[] ints : obstacle) {
this.map[ints[0]][ints[1]] = 1;
}
}
/**
* 使用Bfs找到目标地点
* @param destX 目标地点的x坐标
* @param destY 目标地点的y坐标
* @return
* @throws InterruptedException
*/
public boolean find(int destX,int destY) throws InterruptedException {
boolean[][] visited = new boolean[map.length][map[0].length];
return find(0,0,destX,destY,visited);
}
private boolean find(int srcX, int srxY, int destX, int destY, boolean[][] visited) throws InterruptedException {
if (srcX == destX && srxY == destY){
return true;
}
// 将第一个节点入队
ArrayBlockingQueue<int[]> queue = new ArrayBlockingQueue<>(map.length * map[0].length);
queue.put(new int[]{srcX, srxY});
visited[srcX][srxY] = true;
while (!queue.isEmpty()){
// 弹出节点
int[] cur = queue.poll();
// 到达目标点则返回true
if (cur[0] == destX && cur[1] == destY){
return true;
}
// 获取弹出的节点的四个方向 判断是否可以走,可以走则将节点入队
for (int i = 0; i < dir.length; i++) {
int nextX = cur[0] + dir[i][0];
int nextY = cur[1] + dir[i][1];
if (nextX >= 0 && nextX < map.length && nextY >= 0 && nextY < map[0].length && map[nextX][nextY] == 0 && !visited[nextX][nextY]){
queue.put(new int[]{nextX, nextY});
visited[nextX][nextY] = true;
}
}
}
return false;
}
public static void main(String[] args) throws InterruptedException {
int[][] obstacle = {{0, 2}, {2, 2}, {3, 1}, {4, 3}};
MazeBfsDemo mazeBfsDemo = new MazeBfsDemo(5, 4, obstacle);
System.out.println(mazeBfsDemo.find(0, 2));
}
}
4.2 获取最短路径
使用DFS遍历实现
java
package cn.zxc.demo.leetcode_demo.advanced_data_structure.graph;
import java.util.ArrayList;
import java.util.Arrays;
/**
* 获取最短路径
* 有一天,小美和你去玩迷宫。但是方向感不好的小美很快就迷路了,你得知后便去解救无助的小美,你已经弄清楚了迷宫的地图,现在你要知道从你当前位置出发你是否能够达到小美的位置?
* 起点默认为 {0, 0}
*/
public class MazeDfsDemo {
// 地图:邻接矩阵
private int[][] map;
// 可以走动的方向
private int[][] dir = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
// 最短步数
private int minStep = Integer.MAX_VALUE;
// 记录路径
private ArrayList<String> path;
/**
* 初始化迷宫地图
* @param weight 宽
* @param height 高
* @param obstacle 障碍点
*/
public MazeDfsDemo(int x, int y, int[][] obstacle) {
this.map = new int[x][y];
for (int[] ints : obstacle) {
this.map[ints[0]][ints[1]] = 1;
}
}
/**
* 查找所有可能到达的路径,并记录最短路径
* @param destX 目标点x
* @param destY 目标点y
*/
public void find(int destX,int destY) {
if (map[destX][destY] == 1) {
System.out.println("该点为障碍点,无法到达");
return;
}
boolean[][] visited = new boolean[map.length][map[0].length];
ArrayList<String> tempPath = new ArrayList<>();
find(0, 0, destX, destY, 0, visited,tempPath);
}
/**
* 查找所有可能到达的路径,并记录最短路径
* @param srcX 起点x
* @param srcY 起点y
* @param destX 目标点x
* @param destY 目标点y
* @param step 步数
* @param visited 节点访问标识
* @param tempPath 临时路径
*/
private void find(int srcX, int srcY, int destX, int destY, int step, boolean[][] visited,ArrayList<String> tempPath) {
// 到达目标点
if (srcY == destY && srcX == destX){
// 当前步数小于最小步数 ,记录最小步数 & 当前路径
if (step < minStep){
minStep = step;
path = new ArrayList<>(tempPath);
}
return;
}
// 每个节点走4个方向
for (int i = 0; i < dir.length; i++) {
int nextX = srcX + dir[i][0];
int nextY = srcY + dir[i][1];
// 判断当前方向是否可以走
if (nextX < 0 || nextX >= map.length || nextY < 0 || nextY >= map[0].length || visited[nextX][nextY] || map[nextX][nextY] == 1){
continue;
}
// 可以走:记录路径 & 标识节点
tempPath.add("(" + nextX + "," + nextY + ")");
visited[nextX][nextY] = true;
find(nextX, nextY, destX, destY, step + 1, visited,tempPath);
// 回溯:移除路径 & 节点标识
tempPath.remove(tempPath.size() - 1);
visited[nextX][nextY] = false;
}
}
public static void main(String[] args) {
int[][] obstacle = {{0, 2}, {2, 2}, {3, 1}, {4, 3}};
MazeDfsDemo mazeDfsDemo = new MazeDfsDemo(5, 4, obstacle);
mazeDfsDemo.find(3, 2);
System.out.println(Arrays.toString(mazeDfsDemo.path.toArray()));
}
}
[(1,0), (2,0), (3,0), (4,0), (4,1), (4,2), (3,2)]