Dijkstra 算法
应用场景-最短路径问题
有 7 个村庄(A, B, C, D, E, F, G) ,现在有六个邮差,从 G 点出发,需要分别把邮件分别送到A, B, C , D, E, F 六个村庄
各个村庄的距离用边线表示(权) ,比如 A – B 距离 5 公里
问:如何计算出 G 村庄到 其它各个村庄的最短距离?
如果从其它点出发到各个点的最短距离又是多少?
迪杰斯特拉(Dijkstra)算法介绍
迪杰斯特拉(Dijkstra)算法是典型最短路径算法,**用于计算一个结点到其他结点的最短路径。**它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止。
迪杰斯特拉(Dijkstra)算法过程
-
设置出发顶点为 v,顶点集合 V{v1,v2,vi…},v 到 V 中各顶点的距离构成距离集合 Dis,Dis{d1,d2,di…},Dis集合记录着 v 到图中各顶点的距离(到自身可以看作 0,v 到 vi 距离对应为 di)
-
从 Dis 中选择值最小的 di 并移出 Dis 集合,同时移出 V 集合中对应的顶点 vi,此时的 v 到 vi 即为最短路径
-
更新 Dis 集合,更新规则为:比较 v 到 V 集合中顶点的距离值,与 v 通过 vi 到 V 集合中顶点的距离值,保留值较小的一个(同时也应该更新顶点的前驱节点为 vi,表明是通过 vi 到达的)
-
递归改过程直到所有节点都被访问过。
代码实现
package Algorithm.Dijkstra;
import java.sql.Struct;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Stack;
/**
* @author lixiangxiang
* @description
* @date 2021/8/21 16:07
*/
public class Dijkstra {
public static void main(String[] args) {
char[] vertices = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
//邻接矩阵
int[][] matrix = new int[vertices.length][vertices.length];
//创建 Graph对象
matrix[0] = new int[]{-1, 5, 7, -1, -1, -1, 2};
matrix[1] = new int[]{5, -1, -1, 9, -1, -1, 3};
matrix[2] = new int[]{7, -1, -1, -1, 8, -1, -1};
matrix[3] = new int[]{-1, 9, -1, -1, -1, 4, -1};
matrix[4] = new int[]{-1, -1, 8, -1, -1, 5, 4};
matrix[5] = new int[]{-1, -1, -1, 4, 5, -1, 6};
matrix[6] = new int[]{2, 3, -1, -1, 4, 6, -1};
int index = 4;
Graph graph = new Graph(vertices,matrix,index);
graph.dsj(index);
graph.print();
}
}
class Graph {
//顶点
char[] vertices;
// 记录起点到其他顶点的距离
int[] dis;
// 记录已经访问过的节点
private boolean[] visited;
// 每个下标对应的值为前一个顶点下标, 会动态更新
int[] pre_visited;
// 邻接矩阵储存表
private int[][] matrix;
// 开始顶点
private int index;
public Graph(char[] vertices, int[][] matrix,int index) {
this.vertices = vertices;
this.matrix = matrix;
this.index = index;
visited = new boolean[vertices.length];
pre_visited = new int[vertices.length];
dis = new int[vertices.length];
this.visited[index] = true;
Arrays.fill(dis,Integer.MAX_VALUE);
dis[index] = 0;
}
/**
* 更新其他顶点到index顶点的距离 以及 设置其他顶点前一顶点为 index
*
* @param index
*/
public void dsj(int index) {
int len = 0;
// 如果已经没有未访问过的退出递归
if (index == -1) {
return;
}
// 找到未访问过的最小下标
for (int i = 0; i < matrix[index].length; i++) {
if (matrix[index][i] != -1) {
// 计算 index 距离 i的距离,是一个逐渐累加的过程。顶点到index顶点的距离+index到j顶点的距离之和
len = dis[index] + matrix[index][i];
// 如果当前节点未访问过 则拿当前的这条路径的距离 与数组中index 到 i的距离相比较,取最小值
if (!visited[i] && len < dis[i]) {
dis[i] = len;
//设置i的前趋节点为index
pre_visited[i] = index;
}
}
}
// 设置当前节点已经访问过
visited[index] = true;
int min = getMinUnVisited();
dsj(min);
}
/**
* 获取dis中最小一个被访问过的节点
*/
public int getMinUnVisited() {
int min = Integer.MAX_VALUE;
int index = -1;
for(int i = 0; i < visited.length; i++) {
if (!visited[i] && min > dis[i]) {
min = dis[i];
index = i;
}
}
return index;
}
public void print() {
for (int i = 0; i < dis.length; i++) {
if (i != index) {
System.out.println(vertices[index] +"到"+vertices[i]+"的最短距离为"+ dis[i]);
System.out.print("路线为:");
int pre = i;
Stack<Integer> stack = new Stack<>();
while (pre != index) {
pre = pre_visited[pre];
stack.push(pre);
}
while (!stack.isEmpty()) {
System.out.print(vertices[stack.pop()]+"->");
}
System.out.print(vertices[i]);
System.out.println();
}
}
}
}
弗洛伊德算法
弗洛伊德算法介绍
-
弗洛伊德算法(Floyd)计算图中各个顶点之间的最短路径
-
迪杰斯特拉算法用于计算图中某一个顶点到其他顶点的最短路径。
-
弗洛伊德算法 与 迪杰斯特拉算法区别:
迪杰斯特拉算法通过选定的被访问顶点,求出从出发访问顶点到其他顶点的最短路径;
弗洛伊德算法中每一个顶点都是出发访问点,所以需要将每一个顶点看做被访问顶点,求出从每一个顶点到其他顶点的最短路径
弗洛伊德(Floyd)算法图解分析
-
设置顶点 vi 到顶点 vk 的最短路径已知为 Lik,顶点 vk 到 vj 的最短路径已知为 Lkj,顶点 vi 到 vj 的路径为 Lij, 则 vi 到 vj 的最短路径为:min((Lik+Lkj),Lij),vk 的取值为图中所有顶点,则可获得 vi 到 vj 的最短路径
-
至于 vi 到 vk 的最短路径 Lik 或者 vk 到 vj 的最短路径 Lkj,是以同样的方式获得
代码思路:使用三层for循环对初始距离表 进行遍历,第一层为中间节点的下标,第二层为起始节点的下标,第三层为终点节点的下标。
初始的距离表和前趋节点表
第一次遍历时,以A作为中间节点,获取所有以A为中间顶点到达终点的情况,更新距离表和前趋关系表。
如 BAC(12)、CAG(9)
以 A 顶点作为中间顶点是,B->A->C 的距离由 N->12,同理 C 到 B;C->A->G 的距离由 N->9,更新后的表:
当以A为中间顶点的路径遍历完之后再以其他顶点为中间节点继续遍历,计算结束。
代码实现
package Algorithm.Floyd;
import java.util.Arrays;
import java.util.Stack;
/**
* @author lixiangxiang
* @description Floyd算法
* @date 2021/8/24 11:47
*/
public class FloydAlgorithm {
public static void main(String[] args) {
char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
int[][] matrix = new int[vertex.length][vertex.length];
final int N = 65535;
matrix[0] = new int[] { 0, 5, 7, N, N, N, 2 };
matrix[1] = new int[] { 5, 0, N, 9, N, N, 3 };
matrix[2] = new int[] { 7, N, 0, N, 8, N, N };
matrix[3] = new int[] { N, 9, N, 0, N, 4, N };
matrix[4] = new int[] { N, N, 8, N, 0, 5, 4 };
matrix[5] = new int[] { N, N, N, 4, 5, 0, 6 };
matrix[6] = new int[] { 2, 3, N, N, 4, 6, 0 };
//创建 Graph 对象
Graph graph = new Graph(vertex, matrix);
graph.floyd();
System.out.println(Arrays.deepToString(graph.dis));
System.out.println(Arrays.deepToString(graph.pre));
graph.print();
}
}
class Graph {
//顶点
char[] vertices;
// 记录前趋顶点
int[][] pre;
// 记录距离
int[][] dis;
public Graph(char[] vertices, int[][] matrix) {
this.vertices = vertices;
this.dis = matrix;
this.pre = new int[vertices.length][vertices.length];
// 对pre数组初始化, 注意存放的是前驱顶点的下标
for (int i = 0; i < vertices.length; i++) {
Arrays.fill(pre[i], i);
}
}
/**
* 更新其他顶点到index顶点的距离 以及 设置其他顶点前一顶点为 index
*
* @param
*/
public void floyd() {
// 记录距离
int len = 0;
for (int k = 0; k < dis.length; k++) {
for (int i = 0; i < dis.length; i++) {
for (int j = 0; j < dis.length; j++) {
// 求出从i 顶点出发,经过 k中间顶点,到达 j 顶点的距离
len = dis[i][k] + dis[k][j];
// 如果比之前记录的距离小则更新。
if (len < dis[i][j]) {
// 更新距离
dis[i][j] = len;
// 更新前驱节点
pre[i][j] = pre[k][j];
}
}
}
}
}
public void print() {
for (int index = 0; index < dis.length; index++) {
System.out.println("以"+vertices[index]+"为起点");
for (int i = 0; i < dis[index].length; i++) {
if (i != index) {
System.out.println(vertices[index] +"到"+vertices[i]+"的最短距离为"+ dis[index][i]);
System.out.print("路线为:");
int start = i;
Stack<Integer> stack = new Stack<>();
while (start != index) {
start = pre[index][start];
stack.push(start);
}
while (!stack.isEmpty()) {
System.out.print(vertices[stack.pop()]+"->");
}
System.out.print(vertices[i]);
System.out.println();
}
}
System.out.println("=====================================");
}
}
}
马踏棋盘算法
马踏棋盘算法介绍
-
马踏棋盘算法也被称为骑士周游问题
-
将马随机放在国际象棋的 8×8 棋盘 Board[0~7][0~7]的某个方格中,马按走棋规则(马走日字)进行移动。要求每个方格只进入一次,走遍棋盘上全部 64 个方格
马踏棋盘算法实现
- 马踏棋盘问题(骑士周游问题)实际上是图的深度优先搜索(DFS)的应用。
- 使用回溯来解决,马每走一步就去寻找下面能走的路径,一直往下走,当无路可走时判断是否走满棋盘,如果没有说明路径不合适,进行回溯,重新选择一条路继续走。
回溯法代码实现
package Algorithm.horse;
import java.awt.*;
import java.util.ArrayList;
import java.util.Comparator;
/**
* @author lixiangxiang
* @description 马踏棋盘算法
* @date 2021/8/24 15:18
*/
public class Horse {
private static int X; // 棋盘的列数
private static int Y; // 棋盘的行数
//创建一个数组,标记棋盘的各个位置是否被访问过
private static boolean[] visited;
//标记是否棋盘的所有位置都被访问
private static boolean finished;
public static void main(String[] args) {
Horse.X = 6;
Horse.Y = 6;
Horse.visited = new boolean[X * Y];
// 创建棋盘
int[][] chessboard = new int[X][Y];
long start = System.currentTimeMillis();
traversalChessboard(chessboard,0,5,1);
long end = System.currentTimeMillis();
System.out.println("共耗时"+(end - start) +"ms");
//输出棋盘的最后情况
for(int[] rows : chessboard) {
for(int step: rows) {
System.out.print(step + "\t");
}
System.out.println();
}
}
/**
* description:
*
* @param chessboard 棋盘
* @param row 当前行
* @param column 当前列
* @param step 步数
* @return void
* @author: lixiangxiang
* @date 2021/8/24 15:43
*/
public static void traversalChessboard(int[][] chessboard, int row, int column, int step) {
// 记录步数
chessboard[row][column] = step;
// 标记该位置已经被访问
visited[row * X + column] = true;
// 根据当前节点的下下一步能走的个数对nexts排序(贪心算法)
nexts.sort(Comparator.comparingInt(o -> next(o).size()));
while (!nexts.isEmpty()) {
// 取出下一个走的位置
Point point = nexts.remove(0);
// 如果这个位置还未走过
if (!visited[point.y * X + point.x]) {
// 对该位置进行递归
traversalChessboard(chessboard, point.y, point.x, step + 1);
}
}
// 判断运算是否完成,如果未完成则进行回溯
if (step < X * Y && !finished) {
// 未完成将棋盘所有走过的位置 置0
chessboard[row][column] = 0;
// 将已经访问过的节点
visited[row * X + column] = false;
step = 1;
} else {
finished = true;
}
}
/**
* description: 获取下一步可走的位置
*
* @param curPoint 当前点的坐标
* @return java.util.ArrayList<java.awt.Point>
* @author: lixiangxiang
* @date 2021/8/24 15:42
*/
public static ArrayList<Point> next(Point curPoint) {
//创建一个ArrayList
ArrayList<Point> ps = new ArrayList<>();
// 创建坐标
Point p1 = new Point();
//判断马儿是否可以走5这个位置
if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y - 1) >= 0) {
ps.add(new Point(p1));
}
//判断马儿是否可以走6这个位置
if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y - 2) >= 0) {
ps.add(new Point(p1));
}
//判断马儿是否可以走7这个位置
if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y - 2) >= 0) {
ps.add(new Point(p1));
}
//判断马儿是否可以走0这个位置
if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y - 1) >= 0) {
ps.add(new Point(p1));
}
//判断马儿是否可以走1这个位置
if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y + 1) < Y) {
ps.add(new Point(p1));
}
//判断马儿是否可以走2这个位置
if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y + 2) < Y) {
ps.add(new Point(p1));
}
//判断马儿是否可以走3这个位置
if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y + 2) < Y) {
ps.add(new Point(p1));
}
//判断马儿是否可以走4这个位置
if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y + 1) < Y) {
ps.add(new Point(p1));
}
return ps;
}
}
使用贪心算法对算法进行优化
优化方法很简单,我们只需要在选择马下步走的位置时,选择其下下部可走位置最少的即可,即根据下下步可走位置来对下步可走位置进行排序,每步都选择下下步可走路径最少的位置走。这样可大大减少回溯次数。
public static void traversalChessboard(int[][] chessboard, int row, int column, int step) {
// 记录步数
chessboard[row][column] = step;
// 标记该位置已经被访问
visited[row * X + column] = true;
// 获取当前位置可以走的下一个位置的集合
ArrayList<Point> nexts = next(new Point(column, row));
// 根据当前节点的下下一步能走的个数对nexts排序(贪心算法)
nexts.sort(Comparator.comparingInt(o -> next(o).size()));
while (!nexts.isEmpty()) {
// 取出下一个走的位置
Point point = nexts.remove(0);
// 如果这个位置还未走过
if (!visited[point.y * X + point.x]) {
// 对该位置进行递归
traversalChessboard(chessboard, point.y, point.x, step + 1);
}
}
// 判断运算是否完成,如果未完成则进行回溯
if (step < X * Y && !finished) {
// 未完成将棋盘所有走过的位置 置0
chessboard[row][column] = 0;
// 将已经访问过的节点
visited[row * X + column] = false;
step = 1;
} else {
finished = true;
}
}
我们通过测试运算速度可以发现时间消耗大大减少
未加贪心:
使用贪心算法之后