最短路作为算法入门的基本问题之一,大一开始学的时候也是稀里糊涂的,现在博主在大三上学了算法分析后,对这几种算法有了新的理解与认识,于是总结如下,希望能对自己以后的复习还有刚开始入坑算法的萌新们一点帮助。
首先,Dijkstra算法:
适用范围:
边的权值非负,若图中出现权值为负的边,Dijkstra算法就会失效,求出的最短路径就可能是错的。
具体流程:
一遍dijkstra算法:
找出距离原点的最近的点,然后通过最近的点更新原点与其他点的距离。
需要遍历n-1次。
有点贪心的味道!
其次,spfa算法:
本来打算再写写Bellman-ford算法的,但是我看到了spfa算法(O(ke)),即优化的Bellman-ford算法(O(ve)) v为节点数,经证明 在一般情况下k远小于v,因而不在太过阐述Bellman-ford算法。
想要了解Bellman-ford算法的,我找了一篇不错的博客:
https://blog.csdn.net/niushuai666/article/details/6791765
可以参考。
下面开始讲spfa算法:
适用范围:给定的图存在负权边,这时类似Dijkstra等算法便没有了用武之地,而Bellman-Ford算法的复杂度又过高,SPFA算法便派上用场了。 我们约定有向加权图G不存在负权回路,即最短路径一定存在。当然,我们可以在执行该算法前做一次拓扑排序,以判断是否存在负权回路,但这不是我们讨论的重点。
算法思想:我们用数组d记录每个结点的最短路径估计值,用邻接表来存储图G。我们采取的方法是动态逼近法:设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止
期望的时间复杂度O(ke), 其中k为所有顶点进队的平均次数,可以证明k一般小于等于2。
实现方法:
建立一个队列,初始时队列里只有起始点,再建立一个表格记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)。然后执行松弛操作,用队列里有的点作为起始点去刷新到所有点的最短路,如果刷新成功且被刷新点不在队列中则把该点加入到队列最后。重复执行直到队列为空。(所谓松弛操作就是指通过节点a来更新原点到a的邻接点的长度!)
判断有无负环:
如果某个点进入队列的次数超过N次则存在负环(SPFA无法处理带负环的图)
首先建立起始点a到其余各点的最短路径表格
首先源点a入队,当队列非空时:
1、队首元素(a)出队,对以a为起始点的所有边的终点依次进行松弛操作(此处有b,c,d三个点),此时路径表格状态为:
在松弛时三个点的最短路径估值变小了,而这些点队列中都没有出现,这些点需要入队,此时,队列中新入队了三个结点b,c,d
队首元素b点出队,对以b为起始点的所有边的终点依次进行松弛操作(此处只有e点),此时路径表格状态为:
在最短路径表中,e的最短路径估值也变小了,e在队列中不存在,因此e也要入队,此时队列中的元素为c,d,e
队首元素c点出队,对以c为起始点的所有边的终点依次进行松弛操作(此处有e,f两个点),此时路径表格状态为:
在最短路径表中,e,f的最短路径估值变小了,e在队列中存在,f不存在。因此e不用入队了,f要入队,此时队列中的元素为d,e,f
队首元素d点出队,对以d为起始点的所有边的终点依次进行松弛操作(此处只有g这个点),此时路径表格状态为:
队首元素为e,f,g。
然后e点出对队,e只指向g,然后此时g的最短路径估值没有变小(松弛不成功),没有新结点入队,队列中元素为f,g,表格状态仍然为:
队首元素f点出队,对以f为起始点的所有边的终点依次进行松弛操作(此处有d,e,g三个点),此时路径表格状态为:
在最短路径表中,e,g的最短路径估值又变小,队列中无e点,e入队,队列中存在g这个点,g不用入队,此时队列中元素为g,e
队首元素g点出队,对以g为起始点的所有边的终点依次进行松弛操作(此处只有b点),此时路径表格状态为:
在最短路径表中,b的最短路径估值又变小,队列中无b点,b入队,此时队列中元素为e,b
队首元素e点出队,对以e为起始点的所有边的终点依次进行松弛操作(此处只有g这个点),此时路径表格状态为:
在最短路径表中,g的最短路径估值没变化(松弛不成功),此时队列中元素为b
队首元素b点出队,对以b为起始点的所有边的终点依次进行松弛操作(此处只有e这个点),此时路径表格状态为:
在最短路径表中,e的最短路径估值没变化(松弛不成功),此时队列为空了
最终a到g的最短路径为14。
SPFA算法理论转载博客:
https://blog.csdn.net/maxichu/article/details/45309463
再者,floyd算法:
我在之前的博客写的有,详情请参考:
https://blog.csdn.net/Look_star/article/details/87922786
总结:对于一般点比较少的情况下,可以用floyd算法(O(n^3)),简单粗暴;对于点较多的情况下,可以使用dijkstra算法(O(n^2)),对于点很多,但边不多的情况可以使用spfa算法(O(ke)).
例题展示:
用Dijkstra和spfa算法解决:
算法训练 最短路
问题描述
给定一个n个顶点,m条边的有向图(其中某些边权可能为负,但保证没有负环)。请你计算从1号点到其他点的最短路(顶点从1到n编号)。
输入格式
第一行两个整数n, m。
接下来的m行,每行有三个整数u, v, l,表示u到v有一条长度为l的边。
输出格式
共n-1行,第i行表示1号点到i+1号点的最短路。
样例输入
3 3
1 2 -1
2 3 -1
3 1 2
样例输出
-1
-2
数据规模与约定
对于10%的数据,n = 2,m = 2。
对于30%的数据,n <= 5,m <= 10。
对于100%的数据,1 <= n <= 20000,1 <= m <= 200000,-10000 <= l <= 10000,保证从任意顶点都能到达其他所有顶点。
代码展示:
Spfa算法:算法复杂度O(ke) k:点进入队列数 e边数
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;
public class Main{
//最短路
final static int INF=Integer.MAX_VALUE;//最大值
static int n,m;
static int []u;//始点
static int []v;//终点
static int []l;//长度
static int []d;//始点到i的最短距离
static int []first;//记录前驱 first[i]是指i作为始点的边
static int []next;//next[i]是指邻接u[i]点的第i条边前面的那一条邻接u[i]点边,如果没有为-1即循环终止
static boolean[] vis;//标记状态 是否在队列中 在为true
static Queue<Integer> q=new LinkedList<Integer>();//队列
private static void spfa(int f)//f 原点
{
for(int i=0;i<n;i++)//初始化
d[i]=INF;
d[f]=0;
Arrays.fill(vis, false);
q.add(f);
vis[f]=true;
while(!q.isEmpty())
{
int temp=q.poll();//取出第一个节点并将之赋值给temp
vis[temp]=false;
for(int i=first[temp];i!=-1;i=next[i])//从后往前遍历,领接表 最后加的边第一个遍历
{
//System.out.println(x+" "+ first[x]+" "+i+" "+next[i]);
//System.out.println(v[i]+" "+d[v[i]]+" "+d[x]+" "+l[i]);
if(d[v[i]]>d[temp]+l[i])//松弛操作
{
d[v[i]]=d[temp]+l[i];
if(!vis[v[i]])//没在队列
{
q.add(v[i]);//加到队列
vis[v[i]]=true;//状态置为在队列
}
}
}
}
}
//最短路
public static void main(String[] args) throws IOException {
BufferedReader bfr = new BufferedReader(new InputStreamReader(System.in)); //加快输入,这里如果用scanner会运行超时
String str = bfr.readLine();
String[] s = str.split(" ");
n = Integer.parseInt(s[0]);
m = Integer.parseInt(s[1]);
u=new int [m+1];//这里本来可以用邻接表的,但是为了简便,直接用了几个数组代替
v=new int [m+1];
l=new int [m+1];
first=new int [n+1];//前驱
next=new int[m+1];//后继
d=new int[n+1];//原点到i的最短路径
vis=new boolean[n+1];
Arrays.fill(first, -1);//全部赋值为-1,表示还没有知道的前驱边
for(int i=0;i<m;i++)
{
str = bfr.readLine();
s = str.split(" ");
u[i]=Integer.parseInt(s[0])-1;
v[i]=Integer.parseInt(s[1])-1;
l[i]=Integer.parseInt(s[2]);
next[i]=first[u[i]];//第一条边置-1 后面的依次置前面的边的序号,便于后续从后到前遍历点的邻接边
first[u[i]]=i;//前驱为第i条边
}
spfa(0);
for(int i=1;i<n;i++)
System.out.println(d[i]);
}
}
满分 bingo!
Dijktra算法: 时间复杂度O(n^2)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Scanner;
//只能用一维数组加arrayList 二维数组存储空间肯定炸
class DISTANCE{
ArrayList<Integer> d=new ArrayList<Integer>();
}//两点间的距离
public class Main{
final static int maxn=20000;
final static int INF=0X3f3f3f3f;
static DISTANCE dis[]=new DISTANCE[maxn+5];
static boolean vis[]=new boolean[maxn+5];
static void init(int n)
{
for(int i=0;i<n;i++)
{
dis[i]=new DISTANCE();
for(int j=0;j<n;j++)
{
dis[i].d.add(INF);//初始化两点的距离为无限大
}
}
}
static void Dijkstra(int n)
{
vis[0]=true;
for(int i=0;i<n;i++)//因为有n-1个点,所以要循环n-1次,这里写n次是为了整齐美观
//第n次可以break,当然这只是循环次数,与具体点的坐标不同
{
int p=-1;
int MIN=INF;
for(int j=0;j<n;j++)//找到距原点最近的点
{
if(!vis[j]&&MIN>dis[0].d.get(j))
{
MIN=dis[0].d.get(j);
p=j;
}
}
if(p==-1) break;//没找到
vis[p]=true;//已访问过,就不能在访问了
for(int j=0;j<n;j++)//通过最近的点更新原点到其他点的距离
{
if(dis[0].d.get(j)>dis[0].d.get(p)+dis[p].d.get(j))
{
dis[0].d.set(j,dis[0].d.get(p)+dis[p].d.get(j));
}
}
}
}
public static void main(String[] args) throws IOException {
BufferedReader bfr = new BufferedReader(new InputStreamReader(System.in));
//这里如果用scanner会运行超时
String str = bfr.readLine();
String[] s = str.split(" ");
int n = Integer.parseInt(s[0]);
int m = Integer.parseInt(s[1]);
init(n);
Arrays.fill(vis,false);
for(int i=0;i<m;i++)//从0开始
{
str = bfr.readLine();
s = str.split(" ");
int a=Integer.parseInt(s[0]);
int b=Integer.parseInt(s[1]);
int l=Integer.parseInt(s[2]);
dis[a-1].d.set(b-1,l);
}
Dijkstra(n);
for(int i=1;i<n;i++)
System.out.println(dis[0].d.get(i));
}
}
however,对于足够大n,O(n^2)的时间复杂度已不可忍受,运行超时,60分!但这个题的算法思想是没有问题的。