本篇文章涉及以下内容
🧡Dijkstra算法
💛Bellman-ford算法
💚Spfa算法
💙Floyd算法
单源最短路
单源最短路就是求从某一源点到目标点的最短路径问题
Dijkstra算法
适用场景
无 负 权 路 \textcolor{red}{无负权路} 无负权路的单源最短路问题
原理
迪杰斯特拉算法是解决单源最短路的最简单的算法,运用了 贪 心 \textcolor{red}{贪心} 贪心的思想,在每次遍历中选取当前距离源点最近的且没有被遍历过的点进行松弛操作。每一次遍历都可以找到源点到某一个点的最短路,直到找到源点到所有点的最短路。
步骤
- 设一个集合,保存已经找到最短路的顶点,将起点加入其中
- 将起点与所有点的权值存入dis数组,对于不能直接到达的点,权值为无穷大
- 寻找dis数组中值 最 小 且 未 被 选 为 过 起 点 \textcolor{blue}{最小且未被选为过起点} 最小且未被选为过起点的点作为新起点,如果通过该点源点到其他点的权值小于dis数组中对应的值,则更新dis数组。
- 重复上述操作,直到所有点都作为顶点或者找到源点到目标点的最短路为止(即经过某次遍历后,dis数组中未被作为顶点的权值最小的值正好是目标点)
实现
问题:设计算法求出源点0到目标点5的最短路
public class Dijkstra {
public static void main(String[] args){
//目标顶点下标为5
int target=5;
//使用邻接矩阵来建图
int[][] graph={
{0,1,12,Integer.MAX_VALUE,Integer.MAX_VALUE,Integer.MAX_VALUE},
{Integer.MAX_VALUE,0,9,3,Integer.MAX_VALUE,Integer.MAX_VALUE},
{Integer.MAX_VALUE,Integer.MAX_VALUE,0,Integer.MAX_VALUE,5,Integer.MAX_VALUE,},
{Integer.MAX_VALUE,Integer.MAX_VALUE,4,0,13,15},
{Integer.MAX_VALUE,Integer.MAX_VALUE,Integer.MAX_VALUE,Integer.MAX_VALUE,0,4},
{Integer.MAX_VALUE,Integer.MAX_VALUE,Integer.MAX_VALUE,Integer.MAX_VALUE,Integer.MAX_VALUE,0}
};
int vertices_num= graph.length;
//初始化dis数组
int[] dis=graph[0];
//初始化标记数组,代表各个点是否已被当作中间点进行过遍历
Boolean[] flag=new Boolean[vertices_num];
Arrays.fill(flag,Boolean.TRUE);
flag[0]=Boolean.FALSE;
//因为一共有vertices_num个顶点,每次遍历都会得到源点到一个顶点的最短路径,因此最多循环vertices_num-1次
for(int i=1;i<vertices_num;i++){
int temp=Integer.MAX_VALUE,t=0;
//寻找dis数组中未被当作中间顶点的最小值
for(int j=0;j<vertices_num;j++){
if(flag[j] && dis[j]<temp){
temp=dis[j];
t=j;
}
}
//如果t正是目标顶点,则代表源点到终点的最短路已被求出,可以直接跳出循环
if(t==target)
break;
flag[t]=Boolean.FALSE;
for(int j=0;j<vertices_num;j++){
//进行松弛操作,其中对graph[t][j]进行if判断是为了防止整型溢出
if(graph[t][j]!=Integer.MAX_VALUE && graph[t][j]+dis[t]<dis[j])
dis[j]=graph[t][j]+dis[t];
}
}
System.out.println(dis[target]);
}
}
优化
经过观察,我们发现上述代码有两处可以优化的地方,
- 可以采用优先队列来寻找dis数组中未访问过的最小值,此过程的时间复杂度将从O(N)优化为O(logn)
- 在进行松弛操作时,我们不必要对所有边进行松弛操作,只需要对中间点的邻边进行松弛操作即可。因此我们可以使用邻接表来代替邻接矩阵进行建图。
public class Dijkstra_pro {
Map<Integer,int[][]> graph=new HashMap<>(){{
put(0,new int[][]{{1,1},{2,12}});
put(1,new int[][]{{2,9},{3,3}});
put(2,new int[][]{{4,5}});
put(3,new int[][]{{2,4},{4,13},{5,15}});
put(4,new int[][]{{5,4}});
put(5,new int[][]{});
}};
//创建顶点对象并继承Comparable接口,方便进行优先队列操作
static class Node implements Comparable<Node> {
public int index;
public int dis;
public Node(int index, int dis) {
this.index = index;
this.dis = dis;
}
@Override
public int compareTo(Node o) {
return this.dis - o.dis;
}
}
public void Dijkstra(){
Queue<Node> queue=new PriorityQueue<>();
//目标顶点下标为5
int target=5;
//使用邻接表进行建表
int vertices_num= graph.size();
//初始化dis数组
int[] dis=new int[vertices_num];
Arrays.fill(dis,Integer.MAX_VALUE);
dis[0]=0;
queue.add(new Node(0,0));
while(!queue.isEmpty()){
int t=queue.poll().index;
if(t==target)
break;
for(int[] j:graph.get(t))
//对邻边进行松弛操作,如果得到更小值,就将其加入优先队列
if(j[1]+dis[t]<dis[j[0]]){
dis[j[0]]=j[1]+dis[t];
queue.add(new Node(j[0],dis[j[0]]));
}
}
System.out.println(dis[target]);
}
public static void main(String[] args){
new Dijkstra_pro().Dijkstra();
}
}
优缺点
✨优点
- 简单易理解
- 时间复杂度优秀
💥缺点
- 由于Dijkstra算法使用的是贪心策略,所以它只能适用于 无 负 权 边 \textcolor{red}{无负权边} 无负权边的场景。
Bellman-ford算法
原理
Bellman-ford算法对边进行松弛操作,直到找到源点到所有点的最短路或者发现图中存在负环为止。
它的本质实际上是在第i次迭代中找到源点分别经过[0,i]个点到达各个顶点的最小权值,如果分别经过[0,i]个顶点都无法到达某个顶点,则令dis数组对应值为无穷大
步骤
- 将除起点外所有的顶点到源点的距离初始为无穷大
- 遍历每一条边,对边的两个顶点进行松弛操作,直到不能再松弛为止
- 因为图中有vertices_num个顶点,因此最多有经过vertices_num-1个顶点的最短路,如果循环达到了vertices_num次还能继续进行松弛,则说明图中存在负环。此时最短路就没有意义了。
实现
注意:以下的Bellman-ford算法和SPFA算法代码都是在默认不存在负环的情况下编写的。如果要判断是否存在负环只需要判断循环次数即可,读者可自行思考实现。
我们还是以这个图为例
public class BellmanFord {
//定义边对象
public static class Edge{
int x,y,dis;
public Edge(int x,int y, int dis){
this.x=x;
this.y=y;
this.dis=dis;
}
}
//创建图结构
public Edge[] edges={
new Edge(0,1,1),
new Edge(0,2,12),
new Edge(1,2,9),
new Edge(1,3,3),
new Edge(2,4,5),
new Edge(3,2,4),
new Edge(3,4,13),
new Edge(3,5,15),
new Edge(4,5,4)
};
public void Bellman_Ford(){
//顶点的数目
int n=6;
//初始化dis数组
int[] dis=new int[n];
Arrays.fill(dis,Integer.MAX_VALUE);
dis[0]=0;
for(int i=1;i<n;i++){
//对每条边的顶点进行松弛操作
for(Edge edge:edges){
if (dis[edge.x]!=Integer.MAX_VALUE && dis[edge.y]>dis[edge.x]+edge.dis){
dis[edge.y]=dis[edge.x]+edge.dis;
}
}
}
for (int i:dis)
System.out.println(i);
}
public static void main(String[] args){
new BellmanFord().Bellman_Ford();
}
}
队列优化–SPFA算法
在每次遍历所有边进行松弛操作的时候,我们很容易理解只有松弛过的点才有可能影响下一次松弛操作的结果,因此我们可以使用队列来进行优化。优化后的算法又被成为SPFA算法。
import java.util.*;
public class SPFA {
//使用邻接表进行建图
Map<Integer,int[][]> graph=new HashMap<>(){{
put(0,new int[][]{{1,1},{2,12}});
put(1,new int[][]{{2,9},{3,3}});
put(2,new int[][]{{4,5}});
put(3,new int[][]{{2,4},{4,13},{5,15}});
put(4,new int[][]{{5,4}});
put(5,new int[][]{});
}};
public void spfa(){
Deque<Integer> deque=new ArrayDeque<>();
int vertices_num= graph.size();
//初始化dis数组
int[] dis=new int[vertices_num];
Arrays.fill(dis,Integer.MAX_VALUE);
dis[0]=0;
deque.addLast(0);
while (!deque.isEmpty()){
int vertice=deque.pollFirst();
for(int[] edge:graph.get(vertice)){
if(dis[edge[0]]>dis[vertice]+edge[1]){
//当前点进行松弛操作,证明其可能影响其他点,加入队列进行下一次松弛
dis[edge[0]]=dis[vertice]+edge[1];
deque.addLast(edge[0]);
}
}
}
}
public static void main(String[] args){
new SPFA().spfa();
}
}
优缺点
✨优点
- 可以解决存在 负 权 边 \textcolor{blue}{负权边} 负权边的单源最短路问题,可以判断图中是否存在 负 环 \textcolor{blue}{负环} 负环
💥缺点
- 时间复杂度较高,适合应用于边比较少的场景。
多源最短路
多源最短路求解的是任意两个顶点之间的最短路问题。
Floyd算法
原理
Floyd本质上是使用了 动 态 规 划 \textcolor{purple}{动态规划} 动态规划思想的算法。
从点i到点j有两种情况,由i直接到j或者从i经过一系列点到达j
因此我们可以得到状态转移方程
d p [ k ] [ i ] [ j ] = m i n ( d p [ k − 1 ] [ i ] [ j ] , d p [ k − 1 ] [ i ] [ k ] + d p [ k − 1 ] [ k ] [ j ] ) dp[k][i][j]=min(dp[k-1][i][j],dp[k-1][i][k]+dp[k-1][k][j]) dp[k][i][j]=min(dp[k−1][i][j],dp[k−1][i][k]+dp[k−1][k][j])
其中 d p [ k ] dp[k] dp[k]的含义是通过1~k的某些点i到j的最小距离。
由于每一层状态只与上一层的值有关,因此只需要 O ( N 2 ) O(N^2) O(N2)的空间复杂度即可,其中N是顶点的数量。
状态的初始情况即是图的邻接矩阵。
实现
依旧以此图为例
public class Floyd {
public static void main(String[] args){
//采用邻接矩阵
int[][] graph={
{0,1,12,Integer.MAX_VALUE,Integer.MAX_VALUE,Integer.MAX_VALUE},
{Integer.MAX_VALUE,0,9,3,Integer.MAX_VALUE,Integer.MAX_VALUE},
{Integer.MAX_VALUE,Integer.MAX_VALUE,0,Integer.MAX_VALUE,5,Integer.MAX_VALUE,},
{Integer.MAX_VALUE,Integer.MAX_VALUE,4,0,13,15},
{Integer.MAX_VALUE,Integer.MAX_VALUE,Integer.MAX_VALUE,Integer.MAX_VALUE,0,4},
{Integer.MAX_VALUE,Integer.MAX_VALUE,Integer.MAX_VALUE,Integer.MAX_VALUE,Integer.MAX_VALUE,0}
};
int n= graph.length;
for(int k=0;k<n;k++){
for(int i=0;i<n;i++){
for(int j=0;j<n;j++)
//防止整形溢出
if(graph[i][k]!=Integer.MAX_VALUE && graph[k][j]!=Integer.MAX_VALUE && graph[i][j]>graph[i][k]+graph[k][j])
graph[i][j]=graph[i][k]+graph[k][j];
}
}
//输出结果
for(int []a:graph){
for (int b:a)
System.out.print(b+" ");
System.out.println();
}
}
}
优缺点
✨优点
- 代码十分简单
- 能求出任意两点间的最短路
- 适用于 负 权 边 \textcolor{blue}{负权边} 负权边的场景。
💥缺点
- 时间复杂度过高,高达 O ( N 3 ) O(N^3) O(N3)