图算法(普利姆算法、克鲁斯卡尔算法、迪杰斯特拉算法、弗洛伊德算法、骑士周游算法)
本篇博文通过学习尚硅谷韩顺平老师的《Java数据结构与算法》课程所做,在此非常感谢!!!
文章目录
普利姆算法
概述
普利姆(Prim)算法求最小生成树,也就是在包含n个顶点的连通图中,找出只有(n-1)条边包含所有n个顶点的连通子图,也就是所谓的极小连通子图;普利姆的算法如下:
- 设
G=(V,E)
是连通网,T=(U,D)
是最小生成树,V,U
是顶点集合,E,D
是边的集合 ; - 若从
顶点u
(任意一个点)开始构造最小生成树,则从集合V
中取出顶点u
放入集合U
中,标记顶点v
的visited[u]=1
; - 若集合
U
中顶点ui
与集合V-U
中的顶点vj
之间存在边,则寻找这些边中权值最小的边,但不能构成回路,将顶点vj
加入集合U
中,将边(ui,vj)
加入集合D
中,标记visited[vj]=1
; - 重复步骤②,直到
U
与V
相等,即所有顶点都被标记为访问过,此时D
中有n-1
条边;
最小生成树
最小生成树(Minimum Cost Spanning Tree,简称MST。
- 给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树
- N个顶点,一定有N-1条边
- 包含全部顶点
- N-1条边都在图中
应用
普利姆算法解决修路问题
- 有7个村庄(A, B, C, D, E, F, G) ,现在需要修路把7个村庄连通;
- 各个村庄的距离用边线表示(权) ,比如 A – B 距离 5公里;
- 问:如何修路保证各个村庄都能连通,并且总的修建公路总里程最短?
- 思路: 将10条边,连接即可,但是总的里程数不是最小?正确的思路,就是尽可能的选择少的路线,并且每条路线最小,保证总里程数最少;
分析
如下图所示
克鲁斯卡尔算法
概述
- 克鲁斯卡尔(Kruskal)算法,是用来求加权连通图的最小生成树的算法。
- 基本思想:按照权值从小到大的顺序选择n-1条边,并保证这n-1条边不构成回路;
- 具体做法:首先构造一个只含n个顶点的森林,然后依权值从小到大从连通网中选择边加入到森林中,并使森林中不产生回路,直至森林变成一棵树为止
应用
克鲁斯卡尔算法解决公交站问题
- 有北京有新增7个站点(A, B, C, D, E, F, G) ,现在需要修路把7个站点连通;
- 各个站点的距离用边线表示(权) ,比如 A – B 距离 12公里;
- 问:如何修路保证各个站点都能连通,并且总的修建公路总里程最短?
什么是终点?
就是将所有顶点按照从小到大的顺序排列好之后;某个顶点的终点就是"与它连通的最大顶点",如下图中:
在将<E,F> <C,D> <D,E>加入到最小生成树R中之后,这几条边的顶点就都有了终点:
- C的终点是F。
- D的终点是F。
- E的终点是F。
因此,接下来,虽然<C,E>是权值最小的边。但是C和E的终点都是F,即它们的终点相同,因此,将<C,E>加入最小生成树的话,会形成回路。这就是判断回路的方式。也就是说,我们加入的边的两个顶点不能都指向同一个终点,否则将构成回路。
分析
如下图所示
上述两个算法的代码实现
逻辑代码
package edu.hebeu.bfs_dfs_prim_kruskal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
/**
* 图
* @author 13651
*
*/
public class MyGraph {
/**
* 存放图的顶点的集合
*/
private ArrayList<String> vertexList;
/**
* 存放图的所有直接连通边的集合
*/
private ArrayList<Edge> edgeList;
/**
* 存放图对应的邻接矩阵
*/
private int[][] adjacencyMatrix;
/**
* 存放图的邻接矩阵中最大的元素值(最大边的权值),在生成最小生成树时使用
*/
private int adjacencyMatrix_MAX;
/**
* 创建图对象的构造器
* @param n 图的顶点个数
*/
public MyGraph(int n) {
adjacencyMatrix = new int[n][n]; // 初始化邻接矩阵为n*n
vertexList = new ArrayList<String>(n); // 初始化存放图的顶点的集合
edgeList = new ArrayList<>(); // 初始化存放图的所有直接连通边的集合
}
/**
* 通过克鲁斯卡尔算法得到最小生成树的所有边
* @return 图对象的最小生成树的所有边
*/
public ArrayList<Edge> prim(int v) {
if (v >= vertexList.size() || v < 0) {
System.out.println("不存在此顶点!");
return null;
}
return new MinTree().primCreate(v);
}
/**
* 通过克鲁斯卡尔算法得到最小生成树的所有边
* @return 图对象的最小生成树的所有边
*/
public ArrayList<Edge> kruskal() {
return new MinTree().kruskalCreate();
}
/* 外部调用的广度优先遍历方法
* 对bfs广度优先遍历的重载,遍历所有的节点并进行bfs广度优先遍历
*/
public void bfs() {
boolean[] isVisiteds = new boolean[getVertexNum()]; // 初始化存放每个节点访问情况的数组
// TODO 遍历所有的节点,进行广度优先遍历bfs,相当于回溯
for (int i = 0; i < vertexList.size(); i++) {
if (!isVisiteds[i]) { // 如果该节点没有被访问
bfs(isVisiteds, i);
}
}
}
/** 外部调用的深度优先遍历方法
* 对dfs深度优先遍历的重载,遍历所有的节点并进行dfs深度优先遍历
*/
public void dfs() {
boolean[] isVisiteds = new boolean[getVertexNum()]; // 初始化存放每个节点访问情况的数组
// TODO 遍历所有的节点,进行深度优先遍历dfs,相当于回溯
for (int i = 0; i < getVertexNum(); i++) {
if (!isVisiteds[i]) { // 如果该节点没有访问
dfs(isVisiteds, i);
}
}
}
/** 广度优先遍历算法
* 对一个节点v进行广度优先的方法
* @param isVisiteds
* @param v 当前进行广度优先遍历的节点
*
* 算法步骤:
1.访问初始节点V并标记节点V为已访问;
2.节点V入队列;
3.当队列非空时,继续执行,否则算法结束(节点V的算法结束);
4.出队列,取得头结点U;
5.查找节点U的第一个邻接节点W;
6.若节点U的邻接节点W不存在,则转到3步骤;否则循环执行下面的3步;
6.1.若节点W尚未被访问,则访问节点W并标记为已访问;
6.2.节点W入队列;
6.3.查找节点U的继W邻接节点后的一个邻接节点W,转到6步骤;
*/
private void bfs(boolean[] isVisiteds, int v) {
int u; // 存放队列头结点对应的下标
int w; // 邻接节点w
LinkedList<Integer> queue = new LinkedList<>(); // 队列(使用LinkedList模拟),记录节点访问的顺序
// TODO 1. 访问初始节点V并标记节点V为已访问;
System.out.print(getVertexValue(v) + " -> "); isVisiteds[v] = true;
// TODO 2. 节点V入队列
queue.addLast(v);
// TODO 3. 当节点非空时,继续执行,否则算法结束(节点V的算法结束);
while (!queue.isEmpty()) {
// TODO 4. 出队列,取得头结点U;
u = queue.removeFirst();
// TODO 5. 查找节点U的第一个邻接节点W;
w = getFirstAdjacencyVertexIndex(u);
// TODO 6. 若节点U的邻接节点W不存在,则转到3步骤;否则循环执行下面的3步;
while (w != -1) { // W存在(找到了)
// TODO 6.1. 若节点W尚未被访问,则访问节点W并标记为已访问;
if (!isVisiteds[w]) {
System.out.print(getVertexValue(w) + " -> "); isVisiteds[w] = true;
// TODO 6.2. 节点W入队列;
queue.addLast(w);
}
// TODO 以U为前驱节点,找W后面的下一个邻接节点
w = getNextAdjacencyVertexIndex(u, w); // 这条语句体现出我们的广度优先
}
}
}
/**
* 深度优先遍历算法
* 对一个节点v进行广度优先的方法
* @param isVisiteds
* @param i 当前进行深度优先遍历的节点
*
* 算法步骤:
1.访问初始节点V,并标记节点V为已访问;
2.查找节点V的第一个邻接节点W;
3.若W存在,则继续执行下一步;若W不存在,则回到第1步,将从V的下一个节点继续;
4.若W未被访问,对W进行深度优先遍历递归(即把W当作另一个V,再依次执行上面的1、2、3步);
5.(执行到此,说明W已经被访问了)查找结点V的W邻接点的下一个邻接节点,转至第3步;
*/
private void dfs(boolean[] isVisiteds, int v) {
// TODO 1. 首先我们访问该节点,然后将该节点设置为已访问
System.out.print(getVertexValue(v) + " -> "); isVisiteds[v] = true;
// TODO 2. 查找节点V的第一个邻接节点W
int w = getFirstAdjacencyVertexIndex(v);
// TODO 3. 若W存在,则继续执行下一步;若W不存在,则回到第1步,将从V的下一个节点继续;
while (w != -1) { // w存在
if (!isVisiteds[w]) { // w没有被访问
dfs(isVisiteds, w); // 对w进行深度优先遍历递归
}
// TODO 5. (执行到此,说明W已经被访问了)查找结点V的W邻接点的下一个邻接节点,转至第3步;
w = getNextAdjacencyVertexIndex(v, w);
}
}
/**
* 获取当前节点(i索引对应的节点)的第一个邻接节点的下标值
* @param i 当前节点
* @return 当前节点(i索引对应的节点)的第一个邻接节点的下标值,存在返回其下标值,不存在返回-1
*/
private int getFirstAdjacencyVertexIndex(int i) {
for (int j = 0; j < vertexList.size(); j++) {
if (adjacencyMatrix[i][j] > 0) { // 当邻接矩阵此时的节点值大于0,即i与j节点连通
return j;
}
}
return -1;
}
/**
* 根据前一个邻接节点的下标来获取下一个邻接节点的下标
* @param v1
* @param v2
* @return 存在返回其下标值,不存在返回-1
*/
private int getNextAdjacencyVertexIndex(int v1, int v2) {
for (int j = v2 + 1; j < vertexList.size(); j++) {
if (adjacencyMatrix[v1][j] > 0) {
return j;
}
}
return -1;
}
/**
* 插入顶点的方法
* @param vertex 顶点
*/
public void insertVertex(String vertex) {
vertexList.add(vertex);
}
/** 插入边的方法
* @param v1 连接边的一个顶点的下标
* @param v2 连接边的另一个顶点的下标
* @param weight 边的权重
*/
public void insertEdges(int v1, int v2, int weight) {
// TODO 因为这个图是无向的,所以要如下形式添加边
adjacencyMatrix[v1][v2] = weight;
adjacencyMatrix[v2][v1] = weight;
// 通过此时插入到邻接矩阵的元素创建边对象并加入到edges中
edgeList.add(new Edge(vertexList.get(v1), vertexList.get(v2), adjacencyMatrix[v1][v2]));
// TODO 保存插入边后的最大边的权值,在之后使用普利姆算法生成最小生成树使用
if (adjacencyMatrix_MAX == 0 || adjacencyMatrix_MAX < weight) {
adjacencyMatrix_MAX = weight;
}
}
/**
* 获取图中的顶点个数
* @return
*/
public int getVertexNum() {
return vertexList.size();
}
/**
* 获取图中边的个数
* @return
*/
public int getEdgesNum() {
return edgeList.size();
}
/**
* 返回下标为i的顶点的数据
* @param i 顶点对应的下标
* @return
*/
public String getVertexValue(int i) {
return vertexList.get(i);
}
/**
* 获取名为data 的顶点在顶点集合中的下标位置
* @param data 顶点的值
* @return data顶点在顶点集合中的下标位置
*/
public int getVertexPostiton(String data) {
return vertexList.indexOf(data);
}
/**
* 获取边(连接顶点v1和v2的边)的权值
* @param v1 边的一个顶点v1的下标
* @param v2 边的另一个顶点v2的下标
* @return
*/
public int getEdgeWeight(int v1, int v2) {
return adjacencyMatrix[v1][v2];
}
/**
* 获取该图对象的全部顶点
* @return
*/
public ArrayList<String> getVertexs() {
return vertexList;
}
/**
* 获取该图对象的全部直接连通边,类似于:{ "A", "B", 12 }、{"B", "F", 18}、...
* @return
*/
public ArrayList<Edge> getEdges() {
return edgeList;
}
/**
* 显示打印这个图对应的邻接矩阵
*/
public void showAdjacencyMatrix() {
for (int[] tmp : adjacencyMatrix) {
for (int i = 0; i < tmp.length; i++) {
System.out.printf("%5d", tmp[i]);
} System.out.println();
}
}
/**
* 获取下标为i的顶点 的终点的下标(克鲁斯卡尔算法使用)
* @param ends 数组记录了各个顶点对应的终点是哪个,ends数组是在遍历过程中逐步形成的
* @param i 顶点对应的下标
* @return 下标为i的顶点 的终点的下标
*/
private int getEndIndex(int[] ends, int i) {
while (ends[i] != 0) {
i = ends[i];
}
return i;
}
/**
* 图对象对应的最小生成树
* @author 13651
*
*/
private class MinTree {
/**
* 通过普利姆算法选择边
* @param v 从图的哪个顶点(图中的下标)开始
*/
private ArrayList<Edge> primCreate(int v) {
ArrayList<Edge> res = new ArrayList<>(); //
boolean[] isVisiteds = new boolean[vertexList.size()]; // 标记图中的所有顶点的访问情况
isVisiteds[v] = true; // 将v顶点设置为已经访问过的
// 用来记录选择的边的两个顶点
int v1 = -1;
int v2 = -1;
int minWeight = adjacencyMatrix_MAX + 1; // 初始化最小值为 邻接矩阵中的最大值+1
for (int num = 1; num < vertexList.size(); num++) { // 表示循环 顶点个数-1次(即获取 顶点个数-1 条边)
// TODO 确定本次循环要选择的边
for (int i = 0; i < vertexList.size(); i++) {
for (int j = 0; j < vertexList.size(); j++) {
if (isVisiteds[i] && !isVisiteds[j] && adjacencyMatrix[i][j] != 0 &&
adjacencyMatrix[i][j] < minWeight) {
minWeight = adjacencyMatrix[i][j];
v1 = i;
v2 = j;
}
}
}
// System.out.println("选择边《" + vertexList.get(v1) + "-" + vertexList.get(v2) + "》,权值:" + adjacencyMatrix[v1][v2] + "----------MAX = " + adjacencyMatrix_MAX);
res.add(new Edge(vertexList.get(v1), vertexList.get(v2), adjacencyMatrix[v1][v2])); // 将入选的边保存到edges中
isVisiteds[v2] = true; // 将选择的边对应的点设置为已访问
minWeight = adjacencyMatrix_MAX + 1; // 重新初始化最小值
}
return res;
}
/**
* 通过克鲁斯卡尔算法选择最小生成树的边
*/
private ArrayList<Edge> kruskalCreate() {
ArrayList<Edge> res = new ArrayList<>(); // 保存入选(该图对象对应的最小生成树)的边
int[] ends = new int[getEdgesNum()]; // 保存"当前最小生成树"中的每个顶点在"已有最小生成树"中的终点
ArrayList<Edge> edgeList2 = new ArrayList<>(); // 新建一个集合用来存放该图对象的所有边(防止排序后原先的边存储位置该边)
edgeList2.addAll(edgeList); // 将该图对象的所有边加入到该集合
Collections.sort(edgeList2); // 对当前图对象所有的直接连通边进行排序
// TODO 遍历(从小到大遍历)edges(图对象的 所有的直接连通边),将每条边添加到最小生成树中时,判断该边是否构成了回路,如果没有就加入,反之就不加入
for (int i = 0; i < edgeList2.size(); i++) {
int v1Index = getVertexPostiton(edgeList2.get(i).v1); // 获取该边的v1点在顶点集合中的下标
int v2Index = getVertexPostiton(edgeList2.get(i).v2); // 获取该边的v2点在顶点集合中的下标
/* TODO 判断该边如果加入当前"最小生成树"后是否会构成回路
* 如果不构成回路就加入,反之就不加入
*/
// 获取v1Index点在"已有最小生成树"中 的终点 在顶点集合中对应的下标
int v1EndIndex = getEndIndex(ends, v1Index);
// 获取v1Index点在"已有最小生成树"中 的终点 在顶点集合中对应的下标
int v2EndIndex = getEndIndex(ends, v2Index);
if (v1EndIndex != v2EndIndex) { // 如果不构成回路
ends[v1EndIndex] = v2EndIndex; // 设置 下标为v1EndIndex的顶点 在最小生成树中对应的顶点下标是v2EndIndex
res.add(edgeList2.get(i)); // 将该边入选(加入到最小生成树中)
}
}
return res; // 将得到的最小生成树的所有边返回
}
}
/**
* 该类用来表示这个图对象的一条边
* @author 13651
*
*/
public class Edge implements Comparable<Edge>{
String v1; // 边的一点
String v2; // 边的另一点
int weight; // 边的权值
private Edge(String v1, String v2, int weight) {
this.v1 = v1;
this.v2 = v2;
if (weight != 0) { // 当权值不等于0,即表示该边的两个点是连通的
this.weight = weight;
} else {
this.weight = adjacencyMatrix_MAX + 1;
}
}
@Override
public String toString() {
return "Edge [v1=" + v1 + ", v2=" + v2 + ", weight=" + weight + "]";
}
@Override
public int compareTo(Edge o) {
// TODO 表示Edge对象升序排列
return weight - o.weight;
}
}
}
测试代码
package edu.hebeu.bfs_dfs_prim_kruskal;
import java.util.ArrayList;
import edu.hebeu.bfs_dfs_prim_kruskal.MyGraph.Edge;
public class Test {
public static void main(String[] args) {
System.out.println("\n\n\n\n-----------------------测试遍历算法--------------------------------");
testErgodic();
System.out.println("\n\n\n\n-----------------------测试普利姆算法--------------------------------");
testPrim();
System.out.println("\n\n\n\n-----------------------测试克鲁斯卡尔算法--------------------------------");
testKruskal();
}
public static void testPrim() {
String[] vertexs = new String[] { "A", "B", "C", "D", "E", "F", "G" };
// TODO 创建图对象
MyGraph graph = new MyGraph(vertexs.length);
// TODO // 添加节点
for (String vertex : vertexs) {
graph.insertVertex(vertex);
}
// TODO 添加边
graph.insertEdges(0, 1, 5);
graph.insertEdges(0, 2, 7);
graph.insertEdges(0, 6, 2);
graph.insertEdges(1, 3, 9);
graph.insertEdges(1, 6, 3);
graph.insertEdges(2, 4, 8);
graph.insertEdges(4, 5, 5);
graph.insertEdges(4, 6, 4);
graph.insertEdges(5, 3, 4);
graph.insertEdges(5, 6, 6);
// TODO 显示图对应的邻接矩阵
System.out.println("图的邻接矩阵");
graph.showAdjacencyMatrix();
System.out.println("-------------");
System.out.println("所有边");
ArrayList<Edge> edges = graph.getEdges();
for (Edge edge : edges) {
System.out.println(edge);
}
System.out.println("-------------");
// TODO 通过普利姆算法获取图对象选择的边
System.out.println("通过普利姆算法得到的最小生成树的所有边");
ArrayList<Edge> res = graph.prim(0);
for (int i = 0; i < res.size(); i++) {
System.out.println(res.get(i));
}
}
public static void testKruskal() {
String[] vertexs = new String[] { "A", "B", "C", "D", "E", "F", "G" };
// TODO 创建图对象
MyGraph graph = new MyGraph(vertexs.length);
// TODO // 添加节点
for (String vertex : vertexs) {
graph.insertVertex(vertex);
}
// TODO 添加边
graph.insertEdges(0, 1, 12);
graph.insertEdges(0, 6, 14);
graph.insertEdges(0, 5, 16);
graph.insertEdges(1, 2, 10);
graph.insertEdges(1, 5, 7);
graph.insertEdges(2, 3, 3);
graph.insertEdges(2, 4, 5);
graph.insertEdges(2, 5, 6);
graph.insertEdges(3, 4, 4);
graph.insertEdges(4, 5, 2);
graph.insertEdges(4, 6, 8);
graph.insertEdges(5, 6, 9);
// TODO 显示图对应的邻接矩阵
System.out.println("图的邻接矩阵");
graph.showAdjacencyMatrix();
System.out.println("-------------");
System.out.println("所有边");
ArrayList<Edge> edges = graph.getEdges();
for (Edge edge : edges) {
System.out.println(edge);
}
System.out.println("-------------");
System.out.println("通过克鲁斯卡尔获取到的最小生成树的所有边");
ArrayList<Edge> res = graph.kruskal();
for (Edge edge : res) {
System.out.println(edge);
}
/*
通过克鲁斯卡尔获取到的最小生成树的所有边
Edge [v1=E, v2=F, weight=2]
Edge [v1=C, v2=D, weight=3]
Edge [v1=D, v2=E, weight=4]
Edge [v1=B, v2=F, weight=7]
Edge [v1=E, v2=G, weight=8]
Edge [v1=A, v2=B, weight=12]
*/
}
public static void testErgodic() {
// TODO 图对象创建
MyGraph graph = new MyGraph(8); // 创建顶点为8个的无向图对象
// TODO 添加点
String vertexs[] = { "A", "B", "C", "D", "E", "F", "G", "H" }; // 8个顶点
for (String vertex : vertexs) {
graph.insertVertex(vertex); // 将顶点依次添加到图
}
// TODO 添加边 A-B、A-C、B-C、B-D、B-E
graph.insertEdges(0, 1, 1); // A-B
graph.insertEdges(0, 2, 1); // A-C
graph.insertEdges(1, 3, 1); // B-D
graph.insertEdges(1, 4, 1); // B-E
graph.insertEdges(2, 5, 1); // C-F
graph.insertEdges(2, 6, 1); // C-G
graph.insertEdges(5, 6, 1); // F-G
graph.insertEdges(3, 7, 1); // D-H
graph.insertEdges(4, 7, 1); // E-H
// TODO 显示图对应的邻接矩阵
System.out.println("图的邻接矩阵");
graph.showAdjacencyMatrix();
// TODO 测试深度优先遍历
System.out.print("深度优先遍历(DFS):"); graph.dfs();
// TODO 测试广度优先遍历
System.out.print("\n广度优先遍历(BFS):"); graph.bfs();
}
}
结果测试
迪杰斯特拉算法
概述
迪杰斯特拉(Dijkstra)算法是典型最短路径算法,用于计算一个结点到其他结点的最短路径。 它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止。
应用
问题
- 有六个邮差,从G点出发,需要分别把邮件分别送到 A, B, C , D, E, F 六个村庄;
- 各个村庄的距离用边线表示(权) ,比如 A – B 距离 5公里;
- 问:如何计算出G村庄到 其它各个村庄的最短距离?
- 如果从其它点出发到各个点的最短距离又是多少?
(迪杰斯特拉)算法流程
- 从Dis中选择值最小的di并移出Dis集合,同时移出V集合中对应的顶点vi,此时的v到vi即为最短路径;
- 更新Dis集合,更新规则为:比较v到V集合中顶点的距离值,与v通过vi到V集合中顶点的距离值,保留值较小的一个(同时也应该更新顶点的前驱节点为vi,表明是通过vi到达的);
- 重复执行两步骤,直到最短路径顶点为目标顶点即可结束;
代码实现
逻辑代码
package edu.hebeu.dijkstra;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
/**
* 图
* @author 13651
*
*/
public class MyGraph {
/**
* 存放图的顶点的集合
*/
private ArrayList<String> vertexList;
/**
* 存放图的所有直接连通边的集合
*/
private ArrayList<Edge> edgeList;
/**
* 存放图对应的邻接矩阵
*/
private int[][] adjacencyMatrix;
/**
* 表示图的邻接矩阵中没有连通的两个点的权值,值为所有边的权(邻接矩阵的所有元素)之和
*/
private static int NAN;
// private VisitedVertex visitedVertex;
/**
* 创建图对象的构造器
* @param n 图的顶点个数
*/
public MyGraph(int n) {
adjacencyMatrix = new int[n][n]; // 初始化邻接矩阵为n*n
vertexList = new ArrayList<String>(n); // 初始化存放图的顶点的集合
edgeList = new ArrayList<>(); // 初始化存放图的所有直接连通边的集合
// isVisiteds = new boolean[n]; // 初始化存放每个节点访问情况的数组
}
/**
* 调用迪杰斯特拉算法
* @param start 出发顶点对应的下标
* @return 获取到 index对应的顶点距离所有顶点的路径
*/
public Set<Entry<Integer, List<Edge>>> dijkstra(int start) {
Map<Integer, List<Edge>> pathInfos = new HashMap<>();
InnerHandler visitedVertex = dijkstraCreate(start); // 从start下标对应的顶点开始迪杰斯特拉算法
Map<String, Object> res = parseRes(visitedVertex, start); // 解析visitedVertex(通过迪杰斯特拉算法计算得到的VisitedVertex结果),来解析获取路径
@SuppressWarnings("unchecked")
List<List<Edge>> paths = (List<List<Edge>>) res.get("paths"); // 获取到解析的所有的最短路径
@SuppressWarnings("unchecked")
List<Integer> pathLengths = (List<Integer>) res.get("pathLengths"); // 获取到解析的所有的最短路径
int i = 0;
for (List<Edge> path : paths) {
// System.out.println(vertexList.get(start) + "到" + vertexList.get(i) + "的最短路径:" + path + ",总长度:" + pathLengths.get(i));
pathInfos.put(pathLengths.get(i), path); // 将这条最短路径的总长度做为KEY,路径信息做为VALUE
i++;
}
Set<Entry<Integer, List<Edge>>> pathInfosSet = pathInfos.entrySet();
return pathInfosSet;
}
/**
* 迪杰斯特拉算法的实现
* @param index 出发顶点对应的下标
* @return 获取到通过迪杰斯特拉算法计算得到的VisitedVertex结果
*/
private InnerHandler dijkstraCreate(int index) {
// 迪杰斯特拉算法求出起始点到其余所有点的最短路径(VisitedVertex形式封装表示)
InnerHandler visitedVertex = new InnerHandler(index);
toUpdate(visitedVertex, index); // 更新index下标对应的顶点
for (int i = 0; i < vertexList.size(); i++) {
index = visitedVertex.visitedAndUpdate(); // 选择并返回新的访问顶点
toUpdate(visitedVertex, index); // 继续更新
}
// System.out.println("already_arr:" + Arrays.toString(visitedVertex.already_arr));
// System.out.println("pre_visited:" + Arrays.toString(visitedVertex.pre_visited));
// System.out.println("dis:" + Arrays.toString(visitedVertex.dis));
return visitedVertex;
}
/**
* 该方法用来解析通过迪杰斯特拉算法计算得到几个数组来解析获取每条最短路径路径、和最短路径长度等信息
* @return 获取到 index对应的顶点距离所有顶点的路径
*/
private Map<String, Object> parseRes(InnerHandler visitedVertex, int start) {
Map<String, Object> res = new HashMap<>();
// TODO 解析出每天最短路径的总长度
List<Integer> pathLengths = new ArrayList<>();
for (int i = 0; i < visitedVertex.dis.length; i++) {
pathLengths.add(visitedVertex.getDis(i));
}
res.put("pathLengths", pathLengths); // 将解析得到的每条最短路径的长度加入到res
// TODO 解析出每条最短路径
List<List<Edge>> paths = new ArrayList<>(); // 保存通过迪杰斯特拉算法找到的 index顶点到其他顶点的最短路径 和 路径长度
for (int i = 0; i < visitedVertex.pre_visited.length; i++) {
List<Edge> path = new ArrayList<>(); // 保存通过迪杰斯特拉算法找到的 index顶点到 某个顶点的最短路径
if (i != start) { // 当此时的点不是起始点
int j = visitedVertex.pre_visited[i];
int v1 = i; // 边的一个点
int v2 = j; // 边的另一个点
path.add(new Edge(vertexList.get(v1), vertexList.get(v2), adjacencyMatrix[v1][v2])); // 将这条边添加到path,组成一条路径的一段
while (j != start) { // 当得到的点不是起始点
v1 = j;
j = visitedVertex.pre_visited[j]; // 继续找前驱
v2 = j;
path.add(new Edge(vertexList.get(v1), vertexList.get(v2), adjacencyMatrix[v1][v2])); // 将这条边添加到path,组成一条路径的一段
}
// System.out.println(";总长:" + visitedVertex.dis[i]);
} else { // 否则即此时点是起点,那么起点到起点自身是路径长度为0
path.add(new Edge(vertexList.get(i), vertexList.get(i), 0));
// System.out.println(vertexList.get(i) + "-" + vertexList.get(i) + "点,总长:" + visitedVertex.getDis(i) + "");
}
paths.add(path); // 将找到的这条路径path添加到paths
}
res.put("paths",paths); // 将解析得到的每条最短路径加入到res
return res;
}
/**
* 更新index下标顶点 到 其他顶点的距离 和 周围顶点的前驱
* @param visitedVertex
* @param index
*/
private void toUpdate(InnerHandler visitedVertex, int index) {
int len = 0;
for (int i = 0; i < adjacencyMatrix[index].length; i++) {
// 出发顶点到index顶点的距离 + index顶点到i顶点的距离之和
len = visitedVertex.getDis(index) +
(adjacencyMatrix[index][i] == 0 ? (NAN) : adjacencyMatrix[index][i]);
if (!visitedVertex.isVisited(i) && len < visitedVertex.getDis(i)) {
visitedVertex.updatePre(i, index); // 更新i顶点的前驱为index顶点
visitedVertex.updateDis(i, len); // 更新出发顶点 到 i顶点的距离为len
}
}
}
/**
* 存放使用迪杰斯特拉算法处理后的三个数组
* @author 13651
*
*/
public class InnerHandler {
/**
* 记录各个顶点是否访问过,true.表示访问过,false.表示未访问过
*/
private boolean[] already_arr;
/**
* 每个下标对应的值为前一个顶点下标,会动态更新
*/
private int[] pre_visited;
/**
* 记录出发顶点到其他所有顶点的距离,
*/
private int[] dis;
/**
* 构造器
* @param startIndex 出发顶点的下标
*/
public InnerHandler(int startIndex) {
already_arr = new boolean[vertexList.size()];
pre_visited = new int[vertexList.size()];
dis = new int[vertexList.size()];
Arrays.fill(dis, NAN);
dis[startIndex] = 0;
already_arr[startIndex] = true; // 将此时的出发顶点设置为被访问过
}
/**
* 判断指定下标的顶点是否被访问过
* @param index 顶点的下标
* @return
*/
public boolean isVisited(int index) {
return already_arr[index];
}
/**
* 返回出发顶点到index顶点的距离
* @param index
* @return
*/
public int getDis(int index) {
return dis[index];
}
/**
* 更新出发顶点(当前对象对应的顶点) 到 index下标的顶点的距离
* @param index index下标的顶点
* @param data 更新的数据
*/
public void updateDis(int index, int len) {
dis[index] = len;
}
/**
* 更新index顶点 的前驱为pre顶点
* @param index
* @param pre
*/
public void updatePre(int index, int pre) {
pre_visited[index] = pre;
}
/**
* 继续选择并返回新的访问顶点
* @return
*/
public int visitedAndUpdate() {
int min = NAN, index = 0;
for (int i = 0; i < already_arr.length; i++) {
if (!already_arr[i] && dis[i] < min) { // 当i顶没有被访问过,并且出发顶点到i顶点的距离 小于 min
min = dis[i];
index = i;
}
}
already_arr[index] = true; // 更新index顶点为已经访问过的
// System.out.println("选择 -> " + vertexList.get(index) + ", index = " + index + "\n");
return index;
}
}
/**
* 插入顶点的方法
* @param vertex 顶点
*/
public void insertVertex(String vertex) {
vertexList.add(vertex);
}
/** 插入边的方法
* @param v1 连接边的一个顶点的下标
* @param v2 连接边的另一个顶点的下标
* @param weight 边的权重
*/
public void insertEdges(int v1, int v2, int weight) {
// TODO 因为这个图是无向的,所以要如下形式添加边
adjacencyMatrix[v1][v2] = weight;
adjacencyMatrix[v2][v1] = weight;
// 通过此时插入到邻接矩阵的元素创建边对象并加入到edges中
edgeList.add(new Edge(vertexList.get(v1), vertexList.get(v2), adjacencyMatrix[v1][v2]));
// TODO 保存插入边后所有边的权值之和,在之后使用普利姆算法生成最小生成树、迪杰斯特拉算法使用
NAN += weight;
}
/**
* 获取图中的顶点个数
* @return
*/
public int getVertexNum() {
return vertexList.size();
}
/**
* 获取图中边的个数
* @return
*/
public int getEdgesNum() {
return edgeList.size();
}
/**
* 返回下标为i的顶点的数据
* @param i 顶点对应的下标
* @return
*/
public String getVertexValue(int i) {
return vertexList.get(i);
}
/**
* 获取名为data 的顶点在顶点集合中的下标位置
* @param data 顶点的值
* @return data顶点在顶点集合中的下标位置
*/
public int getVertexPostiton(String data) {
return vertexList.indexOf(data);
}
/**
* 获取边(连接顶点v1和v2的边)的权值
* @param v1 边的一个顶点v1的下标
* @param v2 边的另一个顶点v2的下标
* @return
*/
public int getEdgeWeight(int v1, int v2) {
return adjacencyMatrix[v1][v2];
}
/**
* 获取该图对象的全部顶点
* @return
*/
public ArrayList<String> getVertexs() {
return vertexList;
}
/**
* 获取该图对象的全部直接连通边,类似于:{ "A", "B", 12 }、{"B", "F", 18}、...
* @return
*/
public ArrayList<Edge> getEdges() {
return edgeList;
}
/**
* 显示打印这个图对应的邻接矩阵
*/
public void showAdjacencyMatrix() {
for (int[] tmp : adjacencyMatrix) {
for (int i = 0; i < tmp.length; i++) {
System.out.printf("%5d", tmp[i]);
} System.out.println();
}
}
/**
* 该类用来表示这个图对象的一条边
* @author 13651
*
*/
public class Edge implements Comparable<Edge>{
String v1; // 边的一点
String v2; // 边的另一点
int weight; // 边的权值
private Edge(String v1, String v2, int weight) {
this.v1 = v1;
this.v2 = v2;
if (weight != 0) { // 当权值不等于0,即表示该边的两个点是连通的
this.weight = weight;
} else {
this.weight = NAN;
}
}
@Override
public String toString() {
return "Edge [v1=" + v1 + ", v2=" + v2 + ", weight=" + (weight == NAN ? 0 : weight) + "]";
}
@Override
public int compareTo(Edge o) {
// TODO 表示Edge对象升序排列
return weight - o.weight;
}
}
}
测试代码
package edu.hebeu.dijkstra;
import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import edu.hebeu.dijkstra.MyGraph.Edge;
public class Test {
public static void main(String[] args) {
String[] vertexs = new String[] { "A", "B", "C", "D", "E", "F", "G" };
// TODO 创建图对象
MyGraph graph = new MyGraph(vertexs.length);
// TODO // 添加节点
for (String vertex : vertexs) {
graph.insertVertex(vertex);
}
// TODO 添加边
graph.insertEdges(0, 1, 5);
graph.insertEdges(0, 2, 7);
graph.insertEdges(0, 6, 2);
graph.insertEdges(1, 3, 9);
graph.insertEdges(1, 6, 3);
graph.insertEdges(2, 4, 8);
graph.insertEdges(3, 5, 4);
graph.insertEdges(4, 6, 4);
graph.insertEdges(4, 5, 5);
graph.insertEdges(5, 6, 6);
// TODO 显示图对应的邻接矩阵
System.out.println("图的邻接矩阵");
graph.showAdjacencyMatrix();
System.out.println("-------------");
System.out.println("所有边");
ArrayList<Edge> edges = graph.getEdges();
for (Edge edge : edges) {
System.out.println(edge);
}
System.out.println("-------------");
Set<Entry<Integer, List<Edge>>> pathInfos = graph.dijkstra(6);
System.out.println("通过迪杰斯特拉算法得到各个点到" + vertexs[6] + "点的路径如下:");
for (Entry<Integer, List<Edge>> pathInfo : pathInfos) {
System.out.println("最短路径:" + pathInfo.getValue() + ";总长度:" + pathInfo.getKey());
}
}
}
测试
弗洛伊德算法
概述
- 和Dijkstra算法一样,弗洛伊德(Floyd)算法也是一种用于寻找给定的加权图中顶点间最短路径的算法。
- 弗洛伊德算法(Floyd)计算图中各个顶点之间的最短路径;
- 迪杰斯特拉算法用于计算图中某一个顶点到其他顶点的最短路径。
与迪杰斯特拉算法的异同?
弗洛伊德算法、迪杰斯特拉算法:迪杰斯特拉算法通过选定的被访问顶点,求出从出发访问顶点到其他顶点的最短路径;弗洛伊德算法中每一个顶点都是出发访问点,所以需要将每一个顶点看做被访问顶点,求出从每一个顶点到其他顶点的最短路径。
应用
问题
- 对于上述的A, B, C, D, E, F, G点
- 每个点的距离用边线表示(权) ,比如 A – B 距离 5公里;
- 问:如何计算出各村庄到 其它各村庄的最短距离?
代码实现
逻辑代码
package edu.hebeu.floyd;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
* 图
* @author 13651
*
*/
public class MyGraph {
/**
* 存放图的顶点的集合
*/
private ArrayList<String> vertexList;
/**
* 存放图的所有直接连通边的集合
*/
private ArrayList<Edge> edgeList;
/**
* 存放图对应的邻接矩阵
*/
private int[][] adjacencyMatrix;
/**
* 表示图的邻接矩阵中没有连通的两个点的权值,值为所有边的权(邻接矩阵的所有元素)之和
*/
private static int NAN;
/**
* 创建图对象的构造器
* @param n 图的顶点个数
*/
public MyGraph(int n) {
adjacencyMatrix = new int[n][n]; // 初始化邻接矩阵为n*n
vertexList = new ArrayList<String>(n); // 初始化存放图的顶点的集合
edgeList = new ArrayList<>(); // 初始化存放图的所有直接连通边的集合
// isVisiteds = new boolean[n]; // 初始化存放每个节点访问情况的数组
}
public class InnerHandler {
/**
* 保存到达目标顶点的前驱顶点的下标,用来表示某个节点到某个节点的路径时使用
*/
private int[][] pre;
/**
* 保存从各个顶点出发到其他顶点的距离,最后的结果,也是保留在该数组
*/
private int[][] dis;
/**
* 构造器
*/
private InnerHandler() {
dis = adjacencyMatrix; // 初始为邻接矩阵
pre = new int[vertexList.size()][vertexList.size()];
for (int i = 0; i < vertexList.size(); i++) {
Arrays.fill(pre[i], i); // 将一维数组pre[i] 的值初始化为i
}
for (int i = 0; i < vertexList.size(); i++) {
dis[i][i] = 0; // 让所有起点和终点一样的长度设置
}
}
/**
* 通过弗洛伊德算法计算每个点到其他点的最小路径并保存在dis数组中
*/
private void floydCreate() {
// 表示遍历中间顶点数组{ 'A', 'B', 'C', 'D', 'E', 'F', 'G' }
for (int i = 0; i < dis.length; i++) {
// 表示遍历起始顶点数组{ 'A', 'B', 'C', 'D', 'E', 'F', 'G' }
for (int j = 0; j < dis.length; j++) {
// 表示遍历终点顶点数组{ 'A', 'B', 'C', 'D', 'E', 'F', 'G' }
for (int k = 0; k < dis.length; k++) {
if (j == k) { // 如果i==k,即起点和终点相同,此时应该结束本次处理
continue;
}
// 计算从起点i到中点j,再从中点j到终点k的路径长度,需要注意:当dis内的元素为0时,因为上面已经把起点和终点相同的情况排除,所以此时为0表示没有直接的通路,应该设置为NAN(即邻接矩阵所有的元素之和,表示距离无限大)
int len = (dis[j][i] == 0 ? NAN : dis[j][i]) +
(dis[i][k] == 0 ? NAN : dis[i][k]);
if (len < (dis[j][k] == 0 ? NAN : dis[j][k])) { // 如果此时len大小(i->j->k的长度) 小于 原先的i到k的长度
dis[j][k] = len; // 更新长度为len
pre[j][k] = pre[i][k]; // 更新前驱顶点为中间顶点
}
}
}
}
}
public int[][] getPre() {
return pre;
}
public void setPre(int[][] pre) {
this.pre = pre;
}
public int[][] getDis() {
return dis;
}
public void setDis(int[][] dis) {
this.dis = dis;
}
}
/**
* 外部调用的方法,用来调用弗洛伊德算法
*/
public void floyed() {
InnerHandler innerHandler = new InnerHandler();
innerHandler.floydCreate(); // 使用弗洛伊德算法
for (int[] tmp : innerHandler.getPre()) {
System.out.println(Arrays.toString(tmp));
}
Set<Entry<String, Set<Entry<String, List<Edge>>>>> allVertexPaths = parseRes(innerHandler); // 解析使用弗洛伊德算法得到的结果
for (Entry<String, Set<Entry<String, List<Edge>>>> vertexPaths : allVertexPaths) {
System.out.println(vertexPaths.getKey() + "======点到其他点的最小路径如下======");
Set<Entry<String,List<Edge>>> paths = vertexPaths.getValue(); // 获取该点到其他点的所有最短路径
for (Entry<String,List<Edge>> path : paths) {
System.out.println(path.getKey() + "---" + path.getValue());
}
}
}
/**
* 解析通过弗洛伊德算法得到的结果
* @param innerHandler 通过弗洛伊德算法得到的结果存放在该对象中
* @return
*/
private Set<Entry<String, Set<Entry<String, List<Edge>>>>> parseRes(InnerHandler innerHandler) {
int[][] dis = innerHandler.getDis(); // 获取弗洛伊德算法得到的结果 中的dis数组
int[][] pre = innerHandler.getPre(); // 获取弗洛伊德算法得到的结果 中的pre数组
Map<String, Set<Entry<String, List<Edge>>>> allVertexPaths = new HashMap<>(); // 保存每个顶点到其他顶点的最短路径
for (int i = 0; i < dis.length; i++) {
Map<String, List<Edge>> vertexPaths = new HashMap<>(); // 保存i顶点到其他顶点的最短路径
for (int j = 0; j < dis[i].length; j++) {
List<Edge> path = new ArrayList<>(); // 保存i顶点到当前j顶点的路径
int v1 = j;
int v2 = pre[i][j];
if (i == v2) { // 当i == v2(即pre[i][j]),即此时这条路径已经查找结束
path.add(new Edge(vertexList.get(v1), vertexList.get(v2), adjacencyMatrix[v1][v2])); // 将该边做为此条路径的一段
} else { // 否则,这条路径存在不止一个边(没有查找结束)
// if (i != v2) { // 如果这条路径存在不止一个边(没有查找结束)
int tmp = v2; // 先保存初始的v2的值
do {
path.add(new Edge(vertexList.get(v1), vertexList.get(v2), adjacencyMatrix[v1][v2])); // 将该边做为此条路径的一段
v1 = v2;
v2 = pre[i][v2];
} while ((tmp = pre[i][tmp]) != pre[i][tmp]); // 每次令tmp=pre[i][tmp],
path.add(new Edge(vertexList.get(v1), vertexList.get(v2), adjacencyMatrix[v1][v2])); // 将该边做为此条路径的一段
}
// path.add(new Edge(vertexList.get(v1), vertexList.get(v2), adjacencyMatrix[v1][v2])); // 将该边做为此条路径的一段
vertexPaths.put(vertexList.get(j), path); // 将i点到j点 选择的最短路径加入到vertexPaths
}
allVertexPaths.put(vertexList.get(i), vertexPaths.entrySet()); // 将i点到其他点的所有最短路径的集合加入到allVertexPaths
}
return allVertexPaths.entrySet();
}
/**
* 插入顶点的方法
* @param vertex 顶点
*/
public void insertVertex(String vertex) {
vertexList.add(vertex);
}
/** 插入边的方法
* @param v1 连接边的一个顶点的下标
* @param v2 连接边的另一个顶点的下标
* @param weight 边的权重
*/
public void insertEdges(int v1, int v2, int weight) {
// TODO 因为这个图是无向的,所以要如下形式添加边
adjacencyMatrix[v1][v2] = weight;
adjacencyMatrix[v2][v1] = weight;
// 通过此时插入到邻接矩阵的元素创建边对象并加入到edges中
edgeList.add(new Edge(vertexList.get(v1), vertexList.get(v2), adjacencyMatrix[v1][v2]));
// TODO 保存插入边后所有边的权值之和,在之后使用普利姆算法生成最小生成树、迪杰斯特拉算法使用
NAN += weight;
}
/**
* 获取图中的顶点个数
* @return
*/
public int getVertexNum() {
return vertexList.size();
}
/**
* 获取图中边的个数
* @return
*/
public int getEdgesNum() {
return edgeList.size();
}
/**
* 返回下标为i的顶点的数据
* @param i 顶点对应的下标
* @return
*/
public String getVertexValue(int i) {
return vertexList.get(i);
}
/**
* 获取名为data 的顶点在顶点集合中的下标位置
* @param data 顶点的值
* @return data顶点在顶点集合中的下标位置
*/
public int getVertexPostiton(String data) {
return vertexList.indexOf(data);
}
/**
* 获取边(连接顶点v1和v2的边)的权值
* @param v1 边的一个顶点v1的下标
* @param v2 边的另一个顶点v2的下标
* @return
*/
public int getEdgeWeight(int v1, int v2) {
return adjacencyMatrix[v1][v2];
}
/**
* 获取该图对象的全部顶点
* @return
*/
public ArrayList<String> getVertexs() {
return vertexList;
}
/**
* 获取该图对象的全部直接连通边,类似于:{ "A", "B", 12 }、{"B", "F", 18}、...
* @return
*/
public ArrayList<Edge> getEdges() {
return edgeList;
}
/**
* 显示打印这个图对应的邻接矩阵
*/
public void showAdjacencyMatrix() {
for (int[] tmp : adjacencyMatrix) {
for (int i = 0; i < tmp.length; i++) {
System.out.printf("%5d", tmp[i]);
} System.out.println();
}
}
/**
* 该类用来表示这个图对象的一条边
* @author 13651
*
*/
public class Edge implements Comparable<Edge>{
String v1; // 边的一点
String v2; // 边的另一点
int weight; // 边的权值
private Edge(String v1, String v2, int weight) {
this.v1 = v1;
this.v2 = v2;
if (weight != 0) { // 当权值不等于0,即表示该边的两个点是连通的
this.weight = weight;
} else {
this.weight = NAN;
}
}
@Override
public String toString() {
return "Edge [v1=" + v1 + ", v2=" + v2 + ", weight=" + (weight == NAN ? 0 : weight) + "]";
}
@Override
public int compareTo(Edge o) {
// TODO 表示Edge对象升序排列
return weight - o.weight;
}
}
}
测试代码
package edu.hebeu.floyd;
import java.util.ArrayList;
import edu.hebeu.floyd.MyGraph.Edge;
public class Test {
public static void main(String[] args) {
String[] vertexs = new String[] { "A", "B", "C", "D", "E", "F", "G" };
// TODO 创建图对象
MyGraph graph = new MyGraph(vertexs.length);
// TODO // 添加节点
for (String vertex : vertexs) {
graph.insertVertex(vertex);
}
// TODO 添加边
graph.insertEdges(0, 1, 5);
graph.insertEdges(0, 2, 7);
graph.insertEdges(0, 6, 2);
graph.insertEdges(1, 3, 9);
graph.insertEdges(1, 6, 3);
graph.insertEdges(2, 4, 8);
graph.insertEdges(3, 5, 4);
graph.insertEdges(4, 6, 4);
graph.insertEdges(4, 5, 5);
graph.insertEdges(5, 6, 6);
// TODO 显示图对应的邻接矩阵
System.out.println("图的邻接矩阵");
graph.showAdjacencyMatrix();
System.out.println("-------------");
System.out.println("所有边");
ArrayList<Edge> edges = graph.getEdges();
for (Edge edge : edges) {
System.out.println(edge);
}
System.out.println("-------------");
graph.floyed();
}
}
测试
骑士周游算法
概述
- 马踏棋盘算法也被称为骑士周游问题;
- 将马随机放在国际象棋的8×8棋盘Board[0~7][0~7]的某个方格中,马按走棋规则(马走日字)进行移动。要求每个方格只进入一次,走遍棋盘上全部64个方格;
- 马踏棋盘问题(骑士周游问题)实际上是图的深度优先搜索(DFS)的应用。
分析
- 方式一:如果使用回溯(就是深度优先搜索)来解决,假如马儿踏了57个点,当走到了第58个点,坐标(2, 6),发现已经走到尽头,没办法,那就只能回退了,查看其他的路径,就在棋盘上不停的回溯…… ;
- 方式二:使用贪心算法进行优化
- 先获取当前位置 可以走到下一个位置的集合;
- 将上面得到的集合的元素(点) 的下一步可选择点位置 进行非递减排序
- 然后选择第一个元素(下一步可选择点位置最多的元素点)贪心算法的体现!!!,按照方式一进行递归;
代码实现
逻辑代码
package edu.hebeu.knight_tour;
import java.awt.Point;
import java.util.ArrayList;
import java.util.Comparator;
public class ChessBoard {
private int[][] chessBoard; // 棋盘
private int maxRow; // 棋盘的纵向边界(最大行)
private int maxCol; // 棋盘的横向边界(最大列)
// private boolean[][] accords; // 棋盘存储所有点能够满足 实现全部访问的情况(二维数组效率非常低!!)
private boolean[] accords; // 棋盘存储所有点能够满足 实现全部访问的情况(替代上面的二维数组)
private boolean isFinished; // 标记棋盘上的所有点是否都被访问
/**
* 构造器,用来创建棋盘、accords等数组和值
* @param row 棋盘的行
* @param col 棋盘的列
*/
public ChessBoard(int row, int col) {
chessBoard = new int[row][col]; // 初始化棋盘
// accords = new boolean[row][col]; // 初始化存放各点访问情况的棋盘
accords = new boolean[row * col]; // 初始化存放各点访问情况的棋盘
maxRow = row;
maxCol = col;
}
/**
* 外界调用的方法
* @param row
* @param col
* @return
*/
public int[][] knightTour(int row, int col) {
int[][] res = new int[maxRow][maxCol]; // 存放通过骑士周游算法递归方式得到的结果
// knightTourRecursion(row, col, res, 1); // 使用递归回溯的方式进行马踏棋盘
knightGreedy(row, col, res, 1); // 使用贪心算法的方式进行马踏棋盘(优化)
return res;
}
/**
* 进行骑士周游算法(使用贪心算优化的方式)
*
* 步骤:
* 1. 先获取当前位置 可以走到下一个位置的集合;
* 2. 将上面得到的集合的元素(点) 的下一步可选择点位置 进行非递减排序
*
* 非递减排序?
* 9, 7, 6, 4, 3, 1 // 递减排序
* 1, 2, 3, 4, 6, 9 // 递增排序
* 1, 2, 2, 3, 5, 5, 6, 7, 7, 9 // 非递减排序
*
* @return
*/
private int[][] knightGreedy(int row, int col, int[][] res, int step) {
res[row][col] = step - 1;
// accords[row][col] = true; // 标记该位置为能够满足 实现全部访问的情况
accords[row * maxCol + col] = true; // 标记该位置为能够满足 实现全部访问的情况
// TODO 1.获取当前位置点可以走的下一位置点的集合
ArrayList<Point> nexts = next(new Point(col, row));
// TODO 2.将上面得到的集合的元素(点) 的下一步可选择点位置 进行非递减排序
nexts.sort(new Comparator<Point>() {
@Override
public int compare(Point o1, Point o2) {
int size1 = next(o1).size();
int size2 = next(o2).size();
if (size1 < size2) return -1;
else if (size1 == size2) return 0;
else return 1;
}
});
while (!nexts.isEmpty()) { // 如果nexts不为空(即下一个点集合没有访问完)
Point next = nexts.remove(0); // 取出一个可以走的下一个位置点
// if (!accords[next.y][next.x]) { // 如果该位置点 不能够满足 实现全部访问的情况
if (!accords[next.y * maxCol + next.x]) { // 如果该位置点 不能够满足 实现全部访问的情况
knightGreedy(next.y, next.x, res, step + 1); // 进行递归(注意step+1不能改写成 ++step,否则会导致计算的结果错误)
}
}
/**
* 如果是小于X*Y,并且isFinished为true
* step < X * Y 的两种情况
* 1.棋盘到目前位置仍然没有走完
* 2.棋盘此时处于一个回溯的过程
*/
if (step < maxRow * maxCol && !isFinished) {
res[row][col] = 0; // 将该位置置为0,表示不能够满足 实现全部访问的情况
// accords[row][col] = false; // 将该位置设置为能够满足 实现全部访问的情况
accords[row * maxCol + col] = false; // 将该位置设置为能够满足 实现全部访问的情况
} else {
isFinished = true; // 设置为true,表示处理完成
}
return res;
}
/**
* 进行骑士周游算法(使用递归回溯的方式)
* @param row 马所在位置的行,从0开始
* @param col 马所在位置的列,从0开始
* @param res 存放通过骑士周游算法递归方式得到的结果
* @param step 第几步,从1开始(step的值必须是从1开始,因为下面需要借助step的值进行判断)
*/
@Deprecated // 递归方式,所以效率特别低,不推荐使用
private int[][] knightTourRecursion(int row, int col, int[][] res, int step) {
res[row][col] = step - 1;
// accords[row][col] = true; // 标记该位置为能够满足 实现全部访问的情况
accords[row * maxCol + col] = true; // 标记该位置为能够满足 实现全部访问的情况
// 获取当前位置点可以走的下一位置点的集合
ArrayList<Point> nexts = next(new Point(col, row));
while (!nexts.isEmpty()) { // 如果nexts不为空(即下一个点集合没有访问完)
Point next = nexts.remove(0); // 取出一个可以走的下一个位置点
// if (!accords[next.y][next.x]) { // 如果该位置点 不能够满足 实现全部访问的情况
if (!accords[next.y * maxCol + next.x]) { // 如果该位置点 不能够满足 实现全部访问的情况
knightTourRecursion(next.y, next.x, res, step + 1); // 进行递归(注意step+1不能改写成 ++step,否则会导致计算的结果错误)
}
}
/**
* 如果是小于X*Y,并且isFinished为true
* step < X * Y 的两种情况
* 1.棋盘到目前位置仍然没有走完
* 2.棋盘此时处于一个回溯的过程
*/
if (step < maxRow * maxCol && !isFinished) {
res[row][col] = 0; // 将该位置置为0,表示不能够满足 实现全部访问的情况
// accords[row][col] = false; // 将该位置设置为能够满足 实现全部访问的情况
accords[row * maxCol + col] = false; // 将该位置设置为能够满足 实现全部访问的情况
} else {
isFinished = true; // 设置为true,表示处理完成
}
return res;
}
/**
* 通过当前位置cur,利用马走日的方法判断下一个点(最多有8个位置)的位置
* @param cur
* @return
*/
private ArrayList<Point> next(Point cur) {
ArrayList<Point> points = new ArrayList<>();
Point p = new Point();
if((p.x = cur.x - 2) >= 0 && (p.y = cur.y -1) >= 0) {
points.add(new Point(p));
}
//判断马是否可以走6这个位置
if((p.x = cur.x - 1) >=0 && (p.y=cur.y-2)>=0) {
points.add(new Point(p));
}
//判断马是否可以走7这个位置
if ((p.x = cur.x + 1) < maxCol && (p.y = cur.y - 2) >= 0) {
points.add(new Point(p));
}
//判断马是否可以走0这个位置
if ((p.x = cur.x + 2) < maxCol && (p.y = cur.y - 1) >= 0) {
points.add(new Point(p));
}
//判断马是否可以走1这个位置
if ((p.x = cur.x + 2) < maxCol && (p.y = cur.y + 1) < maxRow) {
points.add(new Point(p));
}
//判断马是否可以走2这个位置
if ((p.x = cur.x + 1) < maxCol && (p.y = cur.y + 2) < maxRow) {
points.add(new Point(p));
}
//判断马是否可以走3这个位置
if ((p.x = cur.x - 1) >= 0 && (p.y = cur.y + 2) < maxRow) {
points.add(new Point(p));
}
//判断马是否可以走4这个位置
if ((p.x = cur.x - 2) >= 0 && (p.y = cur.y + 1) < maxRow) {
points.add(new Point(p));
}
return points;
}
public int[][] getChessBoard() {
return chessBoard;
}
public void setChessBoard(int[][] chessBoard) {
this.chessBoard = chessBoard;
}
public int getMaxRow() {
return maxRow;
}
public void setMaxRow(int maxRow) {
this.maxRow = maxRow;
}
public int getMaxCol() {
return maxCol;
}
public void setMaxCol(int maxCol) {
this.maxCol = maxCol;
}
public boolean[] getAccords() {
return accords;
}
public void setAccords(boolean[] accords) {
this.accords = accords;
}
public boolean isFinished() {
return isFinished;
}
public void setFinished(boolean isFinished) {
this.isFinished = isFinished;
}
}
测试代码
package edu.hebeu.knight_tour;
/**
* 测试骑士周游算法(马踏棋盘算法)
* @author 13651
*
*/
public class Test {
public static void main(String[] args) {
int maxRow = 6; // 设置棋盘的最大行为6
int maxCol = 6; // 设置棋盘的最大列为6
ChessBoard board = new ChessBoard(maxRow, maxCol);
long start = System.currentTimeMillis();
int[][] knightTourRes = board.knightTour(4, 0); // 表示马从(4, 0)位置(即第五行第一列位置)开始
System.out.println("总耗时:" + (System.currentTimeMillis() - start) + "ms");
for (int i = 0; i < knightTourRes.length; i++) {
for (int j = 0; j < knightTourRes[i].length; j++) {
System.out.printf("\t" + knightTourRes[i][j]);
} System.out.println();
}
}
}
测试结果