谈谈最短路的几种算法 (Dijkstra,spfa,floyd) 并附上蓝桥杯 最短路的代码实现

最短路作为算法入门的基本问题之一,大一开始学的时候也是稀里糊涂的,现在博主在大三上学了算法分析后,对这几种算法有了新的理解与认识,于是总结如下,希望能对自己以后的复习还有刚开始入坑算法的萌新们一点帮助。

首先,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分!但这个题的算法思想是没有问题的。

猜你喜欢

转载自blog.csdn.net/Look_star/article/details/88528092