算法应用
- 指定一个起点,得到该起点到图的其他所有节点的最短路径
核心思想
- Dijkstra算法是一种动态规划算法,核心思想是找出指定起点到某个节点的最短路径,就要先找出到达该节点的前一个节点的最短路径
- 执行过程要记录指定起点到其余节点最短路径的路径权值以及当前最短路径终点的前驱节点,并可能随时更新
算法思路
- 从指定起点开始,找出所有邻接节点,更新起点到邻接节点路径权值和记录的前驱节点,从中选出路径权值最小的一个节点,作为下一轮的起点
比如起点是B,B的所有邻接情况有,B-7-A,B-1-C,可以看出B到C是最短的,这里就先选出C为下一轮的起点 - 从次轮起点开始,重复第一轮的操作
- 每一轮更新记录的路径权值,是把 "记录得原始起点到该目标节点的路径总权值" 与 "记录中原始起点到本轮起点的路径权值 + 本轮起点到邻接节点的权值" 比较,如果后者比较大,说明之前记录的路径不是最优选择
接着上述例子,B-7-A是原先记录的B到A的最短路径,假如第二轮起点C,找到路径B-1-C-3-A,可以看到B到A总权值只有4,则把记录的B到达A的最短路径权值从7修改为4,并把A的前驱从B改成C - 更新了权值的同时要记得更新路径终点的前驱节点
- 每一轮都将此轮的起点设置为已访问,并且寻找邻接节点时也要跳过那些已访问的
- 所有节点都"已访问"时结束
注意
- 一个节点一旦被指定为下一轮的起点,也就是"已访问",则该节点的最短路径以及前驱节点已经找到
- 每一轮选出的下一轮的起点, 不可能再找出另外的路径使得起点到达选出的点的路径权值更小,比如从A到D权值3, 假定D节点就是A到所有节点中路径最短的,假如A能通过另外的节点到达D并且路径更短,比如A-1-E-1-D权值为2, 则这一轮取出的节点将是E而不是D
算法实现
-
用这个例子
完整代码
- 小伙伴们可以先复制完全代码粘贴到IDE上,然后再看下面的完整步骤
enum Status { // 节点对象的状态
// 未被发现, 已被遍历
UNDISCOVERD, VISITED
}
public class Graph {
private int N; // N个节点
public int[][] matrix; // 邻接矩阵
private Status[] statuses; // 保存每个节点的状态
private T[] datas; // 保存每个节点的数据
public Graph(int N) {
this.N = N;
matrix = new int[N][N];
statuses = new Status[N];
datas = (T[]) new Object[N]; // 泛型数组实例化
initStatuses();
}
/**
* 用传进来的矩阵初始化图的邻接矩阵
*
* @param matrix 传进来用于初始化邻接矩阵的矩阵
* @return void
*/
public void setMatrix(int[][] matrix) {
this.matrix = matrix;
}
/**
* 使图变成无向图(把邻接矩阵镜像化)
*
* @return void
*/
public void makeUndirected() {
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
if (matrix[i][j] > 0 && matrix[i][j] != matrix[j][i]) {
matrix[j][i] = matrix[i][j];
}
}
}
}
public void setDatas(T[] datas) {
this.datas = datas;
}
/**
* 初始化状态数组
*
* @return void
*/
public void initStatuses() {
for (int i = 0; i < N; i++) {
statuses[i] = Status.UNDISCOVERD;
}
}
/**
* 邻接矩阵保存的信息是从一个节点指向另一个节点的信息
*
* @param from 从这个节点
* @param to 指向这个节点
* @param weight 路径权重
* @return void
*/
public void setMatrix(int from, int to, int weight) {
matrix[from][to] = weight;
}
/**
* 最短路径-迪杰斯特拉算法(找出某个点到其他所有点的最短路径)
*
* @param index 指定某个点
* @return void
*/
public void DijkstraPath(int index) {
// 每一轮选出的路径权值最小的节点, 则不可能再找出另外的路径权值更小
// 比如从A到D是2, 则这一轮取出D节点, 假如有A能通过另外的节点到达D并且更短,
// 比如A-1-E-1-D, 则上一轮取出的节点将是E而不是D
// 数组存放该点到各个点的路径权值
int[] weights = new int[N];
// 将每个默认权值设置为整型最大值
for (int i = 0; i < N; i++) {
weights[i] = Integer.MAX_VALUE;
}
// 数组记录指定节点到每个节点的最短路径中, 终点节点的前驱节点
// 动态规划: 找到到达某个节点的最短路径, 先找到到达他的上一个节点的最短路径
int[] prevs = new int[N];
prevs[index] = -1; // 负数表示该点没有前驱
// 循环所用的辅助索引
int from = index;
// 只要不是全部被遍历
while (!isAllVisited()) {
// 将这个节点设置为已访问
statuses[from] = Status.VISITED;
// 查看邻接矩阵中与指定节点邻接的节点
for (int i = 0; i < N; i++) {
// 可能的新路径权值: 从最开始的指定起点到本轮起点到该节点的路径权值总和
int newWeight;
if (weights[from] == Integer.MAX_VALUE) {
newWeight = matrix[from][i];
} else {
newWeight = weights[from] + matrix[from][i];
}
// 如果节点未访问, 且是邻接节点
if (statuses[i] == Status.UNDISCOVERD && matrix[from][i] > 0
// 并且如果小于weights中记录的该节点原来的路径权值
&& newWeight < weights[i]) {
// 则更新该节点的最小路径值, 更新该节点的前驱为本轮起点
weights[i] = newWeight;
prevs[i] = from;
}
}
// 下轮起点from设置为: weights数组中数值最小的并且未访问的节点
from = indexOfMin(weights);
}
// 输出结果
System.out.println("指定起点为:" + datas[index]);
for (int i = 0; i < N; i++) {
if (i != index) { // 除去最开始指定的起点
List nodesInPath = allPrevs(prevs, i);
System.out.print("起点" + datas[index] + "到" + datas[i] + "点的最短路径是: " + datas[index]);
for (int j :nodesInPath) {
System.out.print("-" + matrix[prevs[j]][j] + "-" + datas[j]);
}
System.out.println("-" + matrix[prevs[i]][i] + "-" + datas[i] + ", 路径权值总和为: " + weights[i]);
}
}
}
/**
* 指定节点, 按路径顺序返回该节点的所有前驱节点
*
* @param prevs 记录前驱节点的数组
* @param index 指定节点
* @return java.util.List
*/
private List allPrevs(int[] prevs, int index) {
// 记录指定节点到达指定起点的最短路径沿途的节点
Stack prevStack = new Stack<>();
int prev = prevs[index];
// 前面设置的算法最开始指定的起点的前驱索引为-1在这里起作用
// 只要前驱的前驱索引不为最开始指定的起点
while (prevs[prev] != -1) {
// 把前驱索引加入栈
prevStack.add(prev);
// 下次循环要检查此次循环前驱节点的前驱节点, 所以更新变量
prev = prevs[prev];
}
// 方便遍历, 倒序输出
List result = new ArrayList<>();
while (!prevStack.isEmpty()) {
result.add(prevStack.pop());
}
return result;
}
/**
* 检查是否全部被遍历(只要有一个是未被遍历返回false)
*
* @return boolean
*/
private boolean isAllVisited() {
for (Status status : statuses) {
if (status == Status.UNDISCOVERD) {
return false;
}
}
return true;
}
/**
* 找到数组中最小的值的索引
*
* @return int
*/
private int indexOfMin(int[] nums) {
List remain = new ArrayList<>();
for (int i = 0; i < N; i++) {
if (statuses[i] == Status.UNDISCOVERD) {
remain.add(i);
}
}
if (remain.size() == 0) {
return 0; // 这里返回什么都行, 因为所有节点会在下一循环全部设置为已访问, 从而循环内无任何操作
}
int minIndex = remain.get(0);
for (int j : remain) {
if (nums[j] < nums[minIndex]) {
minIndex = j;
}
}
return minIndex;
}
public static void main(String[] args) {
Graph graph = new Graph<>(7);
graph.setDatas(new String[]{"A", "B", "C", "D", "E", "F", "G"});
int[][] matrix = {
{0, 7, 3, 2, 2, 0, 0},
{0, 0, 1, 0, 0, 0, 0},
{0, 0, 0, 0, 4, 3, 0},
{0, 0, 0, 0, 1, 10, 2},
{0, 0, 0, 0, 0, 4, 2},
{0, 0, 0, 0, 0, 0, 7},
{0, 0, 0, 0, 0, 0, 0}};
graph.setMatrix(matrix);
graph.makeUndirected();
for (int i = 0; i < 7; i++) {
graph.initStatuses();
graph.DijkstraPath(i);
}
}
}
图的实现
- 此实现方法没有节点类
- 采用邻接矩阵,并用顶点索引代表顶点
- 邻接矩阵
int[][] matrix
-
matrix[i][j]
表示从索引i
的节点指向索引j
的节点的权值 - 权值为0表示两点不连接或者自身与自身不连接
-
- 使用枚举来定义节点的状态
enum Status { UNDISCOVERD, VISITED }
- 枚举数组
Status[] statuses
记录每个节点的状态
enum Status { // 节点对象的状态
// 未被发现, 已被遍历
UNDISCOVERD, VISITED
}
public class Graph {
private int N; // N个节点
public int[][] matrix; // 邻接矩阵
private Status[] statuses; // 保存每个节点的状态
private T[] datas; // 保存每个节点的数据
}
具体过程
- 算法主体方法
void DijkstraPath(int index)
,index
就是指定原始起点 -
int[] weights
存放指定起点到各个点的路径权值(初始值设定为整型最大值,动态更新) -
int[] prevs
记录指定起点到每个节点的最短路径中, 终点节点的前驱节点(将起点的前驱索引设置为负数,表示没有前驱) -
int from
每一轮指定的起点索引(循环的辅助索引),初始化为原始起点索引
public void DijkstraPath(int index) {
// 数组存放该点到各个点的路径权值
int[] weights = new int[N];
// 将每个默认权值设置为整型最大值
for (int i = 0; i < N; i++) {
weights[i] = Integer.MAX_VALUE;
}
// 数组记录指定节点到每个节点的最短路径中, 终点节点的前驱节点
// 动态规划: 找到到达某个节点的最短路径, 先找到到达他的上一个节点的最短路径
int[] prevs = new int[N];
prevs[index] = -1; // 负数表示该点没有前驱
// 循环所用的辅助索引
int from = index;
- 补充三个方法
-
boolean isAllVisited()
判断是否所有节点都是"已访问",检查所有节点的状态,只要有一个是未被访问UNDISCOVERD
,就返回false
-
int indexOfMin(int[] nums)
找出给定数组中最小值的索引,过滤掉已访问的节点,把未访问的节点索引加入集合List
,remain remain.add(i)
用于每轮循环结束前,找出未被访问且在weights
中记录的路径权值最小的节点的索引 -
List
从记录所有节点的前驱索引的数组allPrevs(int[] prevs, int index) prevs
找出原始节点到指定索引index
的节点的最短路径上除了原始起点和终点外的所有中间节点,并记录在栈Stack
中,最后依次弹出存到prevStack List
中,用于最后输出查看结果result
用prev
作为辅助变量,逐层往上寻找前驱,只要当前节点前驱索引不是-1while (prevs[prev] != -1)
也就是当前节点不是原始起点(上面已经把原始起点的前驱索引设置为-1),就把当前节点加入栈prevStack.add(prev)
,最后只要栈不空!prevStack.isEmpty()
,就弹出栈顶加入集合result.add(prevStack.pop())
-
/**
* 检查是否全部被遍历(只要有一个是未被遍历返回false)
*
* @return boolean
*/
private boolean isAllVisited() {
for (Status status : statuses) {
if (status == Status.UNDISCOVERD) {
return false;
}
}
return true;
}
/**
* 找到数组中最小的值的索引
*
* @return int
*/
private int indexOfMin(int[] nums) {
// 记录剩余的未访问的节点
List remain = new ArrayList<>();
for (int i = 0; i < N; i++) {
if (statuses[i] == Status.UNDISCOVERD) {
remain.add(i);
}
}
if (remain.size() == 0) {
return 0; // 这里返回什么都行, 因为所有节点会在下一循环全部设置为已访问, 从而循环内无任何操作
}
int minIndex = remain.get(0);
for (int j : remain) {
if (nums[j] < nums[minIndex]) {
minIndex = j;
}
}
return minIndex;
}
/**
* 指定节点, 按路径顺序返回该节点的所有前驱节点
*
* @param prevs 记录前驱节点的数组
* @param index 指定节点
* @return java.util.List
*/
private List allPrevs(int[] prevs, int index) {
// 记录指定节点到达指定起点的最短路径沿途的节点
Stack prevStack = new Stack<>();
int prev = prevs[index];
// 前面设置的算法最开始指定的起点的前驱索引为-1在这里起作用
// 只要前驱的前驱索引不为最开始指定的起点
while (prevs[prev] != -1) {
// 把前驱索引加入栈
prevStack.add(prev);
// 下次循环要检查此次循环前驱节点的前驱节点, 所以更新变量
prev = prevs[prev];
}
// 方便遍历, 倒序输出
List result = new ArrayList<>();
while (!prevStack.isEmpty()) {
result.add(prevStack.pop());
}
return result;
}
- 只要不是所有节点都是"已访问"
while (!isAllVisited())
,则循环执行- 将每一轮的起点设置为"已访问"
VISITED
- 查看邻接矩阵中与指定节点邻接的节点
-
int newWeight
表示可能的新路径权值: 从最开始的指定起点到本轮起点到该节点的路径权值总和
由于最开始weights
数组所有值初始化为整型最大值,所以判断if (weights[from] == Integer.MAX_VALUE)
该本轮起点在weights
中记录的权值是不是整型最大值,是就说明这个节点第一次被查找(这种情况只在第一轮循环中会出现),newWeight = matrix[from][i]
则可能的新权值直接设置为起点到该邻接节点的边权值,否则设置为newWeight = weights[from] + matrix[from][i]
"原始起点到本轮起点的路径权值 + 本轮起点到该邻接节点的边权值", -
if (statuses[i] == Status.UNDISCOVERD && matrix[from][i] > 0 && newWeight < weights[i])
如果该节点未被访问,且是邻接节点,并且如果newWeight
小于weights
中记录的原始起点到该节点的路径权值,则更新该节点的最小路径值, 更新该节点的前驱为本轮起点weights[i] = newWeight
,prevs[i] = from
-
from = indexOfMin(weights)
设定下一轮的起点为未被访问且是weights
数组中记录的路径权值中最小的一个节点
-
- 将每一轮的起点设置为"已访问"
// 循环所用的辅助索引
int from = index;
// 只要不是全部被遍历
while (!isAllVisited()) {
// 将这个节点设置为已访问
statuses[from] = Status.VISITED;
// 查看邻接矩阵中与指定节点邻接的节点
for (int i = 0; i < N; i++) {
// 可能的新路径权值: 从最开始的指定起点到本轮起点到该节点的路径权值总和
int newWeight;
if (weights[from] == Integer.MAX_VALUE) {
newWeight = matrix[from][i];
} else {
newWeight = weights[from] + matrix[from][i];
}
// 如果节点未访问, 且是邻接节点
if (statuses[i] == Status.UNDISCOVERD && matrix[from][i] > 0
// 并且如果小于weights中记录的该节点原来的路径权值
&& newWeight < weights[i]) {
// 则更新该节点的最小路径值, 更新该节点的前驱为本轮起点
weights[i] = newWeight;
prevs[i] = from;
}
}
// 下轮起点from设置为: weights数组中数值最小的并且未访问的节点
from = indexOfMin(weights);
}
- 整个
while
循环结束之后,就可以检查输出结果-
datas[index]
是输出起点保存的数据,datas
数组是图类的成员变量,保存每个节点储存的数据,比如我用的是每个节点保存一个字母,方便观察 - 遍历每个节点,得到每个节点与起点的路径上的所有中间节点
List
,再遍历nodesInPath = allPrevs(prevs, i) nodesInPath
依次输出边的权值matrix[prevs[j]][j]
(这里prevs[j]
和j
可以倒过来,因为用的是无向图),输出节点数据datas[j]
最后在跟上总路径权值weights[i]
-
System.out.println("指定起点为:" + datas[index]);
for (int i = 0; i < N; i++) {
if (i != index) { // 除去最开始指定的起点
List nodesInPath = allPrevs(prevs, i);
System.out.print("起点" + datas[index] + "到" + datas[i] + "点的最短路径是: " + datas[index]);
for (int j :nodesInPath) {
System.out.print("-" + matrix[prevs[j]][j] + "-" + datas[j]);
}
System.out.println("-" + matrix[prevs[i]][i] + "-" + datas[i] + ", 路径权值总和为: " + weights[i]);
}
}
测试
-
graph
7个节点一次保存7个字母"ABCDEFG" - 邻接矩阵
int[][] matrix
所表示的图请看下方图片 -
graph.makeUndirected()
是把图变成无向图,也就是使得邻接矩阵沿左对角线对称 - 查看分别以每个节点为起点时,到其他节点的最短路径的情况,
graph.initStatuses()
是把所有节点的状态设置为未访问,graph.DijkstraPath(i)
就是算法主体的方法
public static void main(String[] args) {
Graph graph = new Graph<>(7);
graph.setDatas(new String[]{"A", "B", "C", "D", "E", "F", "G"});
int[][] matrix = {
{0, 7, 3, 2, 2, 0, 0},
{0, 0, 1, 0, 0, 0, 0},
{0, 0, 0, 0, 4, 3, 0},
{0, 0, 0, 0, 1, 10, 2},
{0, 0, 0, 0, 0, 4, 2},
{0, 0, 0, 0, 0, 0, 7},
{0, 0, 0, 0, 0, 0, 0}};
graph.setMatrix(matrix);
graph.makeUndirected();
for (int i = 0; i < 7; i++) {
graph.initStatuses();
graph.DijkstraPath(i);
}
}
- 输出结果
指定起点为:A
起点A到B点的最短路径是: A-3-C-1-B, 路径权值总和为: 4
起点A到C点的最短路径是: A-3-C, 路径权值总和为: 3
起点A到D点的最短路径是: A-2-D, 路径权值总和为: 2
起点A到E点的最短路径是: A-2-E, 路径权值总和为: 2
起点A到F点的最短路径是: A-2-E-4-F, 路径权值总和为: 6
起点A到G点的最短路径是: A-2-D-2-G, 路径权值总和为: 4
指定起点为:B
起点B到A点的最短路径是: B-1-C-3-A, 路径权值总和为: 4
起点B到C点的最短路径是: B-1-C, 路径权值总和为: 1
起点B到D点的最短路径是: B-1-C-3-A-2-D, 路径权值总和为: 6
起点B到E点的最短路径是: B-1-C-4-E, 路径权值总和为: 5
起点B到F点的最短路径是: B-1-C-3-F, 路径权值总和为: 4
起点B到G点的最短路径是: B-1-C-4-E-2-G, 路径权值总和为: 7
指定起点为:C
起点C到A点的最短路径是: C-3-A, 路径权值总和为: 3
起点C到B点的最短路径是: C-1-B, 路径权值总和为: 1
起点C到D点的最短路径是: C-3-A-2-D, 路径权值总和为: 5
起点C到E点的最短路径是: C-4-E, 路径权值总和为: 4
起点C到F点的最短路径是: C-3-F, 路径权值总和为: 3
起点C到G点的最短路径是: C-4-E-2-G, 路径权值总和为: 6
指定起点为:D
起点D到A点的最短路径是: D-2-A, 路径权值总和为: 2
起点D到B点的最短路径是: D-1-E-4-C-1-B, 路径权值总和为: 6
起点D到C点的最短路径是: D-1-E-4-C, 路径权值总和为: 5
起点D到E点的最短路径是: D-1-E, 路径权值总和为: 1
起点D到F点的最短路径是: D-1-E-4-F, 路径权值总和为: 5
起点D到G点的最短路径是: D-2-G, 路径权值总和为: 2
指定起点为:E
起点E到A点的最短路径是: E-2-A, 路径权值总和为: 2
起点E到B点的最短路径是: E-4-C-1-B, 路径权值总和为: 5
起点E到C点的最短路径是: E-4-C, 路径权值总和为: 4
起点E到D点的最短路径是: E-1-D, 路径权值总和为: 1
起点E到F点的最短路径是: E-4-F, 路径权值总和为: 4
起点E到G点的最短路径是: E-2-G, 路径权值总和为: 2
指定起点为:F
起点F到A点的最短路径是: F-3-C-3-A, 路径权值总和为: 6
起点F到B点的最短路径是: F-3-C-1-B, 路径权值总和为: 4
起点F到C点的最短路径是: F-3-C, 路径权值总和为: 3
起点F到D点的最短路径是: F-4-E-1-D, 路径权值总和为: 5
起点F到E点的最短路径是: F-4-E, 路径权值总和为: 4
起点F到G点的最短路径是: F-4-E-2-G, 路径权值总和为: 6
指定起点为:G
起点G到A点的最短路径是: G-2-D-2-A, 路径权值总和为: 4
起点G到B点的最短路径是: G-2-E-4-C-1-B, 路径权值总和为: 7
起点G到C点的最短路径是: G-2-E-4-C, 路径权值总和为: 6
起点G到D点的最短路径是: G-2-D, 路径权值总和为: 2
起点G到E点的最短路径是: G-2-E, 路径权值总和为: 2
起点G到F点的最短路径是: G-2-E-4-F, 路径权值总和为: 6
希望我写明白了
如果你看完之后,知道如何实现这个算法,我会很开心
觉得我写的还行的话,麻烦给老弟点个赞
这里再不要脸的推一下自己写的另外的算法详解
最小生成树-Prim算法(Java实现)
最小生成树-Kruskal算法(Java实现)
谢谢~