给定一个n个顶点,m条边的有向图(其中某些边权可能为负,但保证没有负环)。请你计算从1号点到其他点的最短路(顶点从1到n编号)。
第一行两个整数n, m。
接下来的m行,每行有三个整数u, v, l,表示u到v有一条长度为l的边。
1 2 -1
2 3 -1
3 1 2
-2
对于10%的数据,n = 2,m = 2。
对于30%的数据,n <= 5,m <= 10。
对于100%的数据,1 <= n <= 20000,1 <= m <= 200000,-10000 <= l <= 10000,保证从任意顶点都能到达其他所有顶点。
我在网上搜索了最短路算法,发现基本有4种。
1.floyd 算法 2.dijkstra算法 3.bellman算法 4.spfa算法
floyd算法时间复杂度高,dijkstra算法不能计算带负边权的图,故均不考虑。
spfa算法是bellman的一种优化,所以决定从简单的先开始。
bellman实现的代码如下:
#include <iostream>
#include<string.h>
using namespace std;
int dis[20005],u[200005],v[200005],w[200005];//dis[i]代表第一点到第i点距离,u[i],v[i],w[i]分别代表第i条边的起始点,终点和长度。
int main()
{
int m,n;
cin>>n>>m;
for(int i=1;i<=m;i++)
{
cin>>u[i]>>v[i]>>w[i];
}
memset(dis,10001,sizeof(dis));
dis[1]=0;
for(int k=1;k<n;k++)//最极端的情况下,一个点要松弛n-1次
{
for(int i=1;i<=m;i++)//枚举所有的条边
{
if(dis[v[i]]>dis[u[i]]+w[i])
//如果第一点到v[i]点的距离大于第一点到u[i]再加上u[i]到v[i]的距离
dis[v[i]]=dis[u[i]]+w[i];//说明dis[v[i]]的值可以更小,所以更新dis[v[i]]的值,专业的说法就是进行了一次松弛操作
}
}
for(int i=2;i<=n;i++)
cout<<dis[i]<<endl;
}
这段代码很简 单,最关键的地方在两层for循环的那部分。算法的时间复杂度显然是O(mn),最后提交超时。
SPFA(Shortest Path Faster Algorithm)算法是求单源最短路径的一种算法,它是Bellman-ford的队列优化,它是一种十分高效的最短路算法。
SPFA的复杂度大约是O(kE),k是每个点的平均进队次数(一般的,k是一个常数,在稀疏图中小于2)。
但是,SPFA算法稳定性较差,在稠密图中SPFA算法时间复杂度会退化。
实现方法:建立一个队列,初始时队列里只有起始点,在建立一个表格记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)。然后执行松弛操作,用队列里有的点去刷新起始点到所有点的最短路,如果刷新成功且被刷新点不在队列中则把该点加入到队列最后。重复执行直到队列为空。
此外,SPFA算法还可以判断图中是否有负权环,即一个点入队次数超过N。
#include <iostream>
#include<string.h>
#include<queue>
using namespace std;
struct edge
{
int to,val,next;
}e[200100];//e[i].to,e[i].val,e[i].next分别表示第i条边的终点,权值,和另(上)一条于此边相同起点的边的编号
int m,n,head[21000],dis[21000];//head[i]表示当前以i为起点的边的编号,dis[i]表示1号点到i号点的距离
void add(int from,int to,int val,int len)//添加边,参数分别代表此边的起点,终点,权值,还有此边的编号
{
e[len].to=to;
e[len].val=val;
e[len].next=head[from];//此边的起点是from,而head[from]保存上一条起点是from的边的编号
head[from]=len;//添加了这条边后,现在,最新的以from为起点的边编号就是len
}
void spfa()
{
int s;
queue <int> q;
int visit[21000];//visit[i]==0代表第i点不在队列中,visit[i]==1代表已在队列中
for(int i=1;i<=n;i++)
{
dis[i]=99999999;
}//将权值初始化为最大值
memset(visit,0,sizeof(visit));//初始没有点在队列中
dis[1]=0;
q.push(1);
visit[1]=1;//现在把第一个点扔进队列
while(!q.empty())//如果队列不空则重复执行以下操作
{
int t=q.front();//取队列的第一个元素
q.pop();
visit[t]=0;//t现在不在队列里
for(int i=head[t];i!=-1;i=e[i].next)//枚举所有以t为起点的边
{
s=e[i].to;//s为所枚举的边的终点
if(dis[s]>dis[t]+e[i].val)//尝试松弛s点
{
dis[s]=dis[t]+e[i].val;
if(visit[s]==0)
{
q.push(s);
visit[s]=1;
}//如果成功松弛了s点,把s点扔进队列
}
}
}
}
int main ()
{
cin>>n>>m;
int from,val,to;
int len=1;
memset (head,-1,sizeof(head));
for(int i=0;i<m;i++)
{
cin>>from>>to>>val;
add(from,to,val,len);
len++;
}
spfa();
for(int i=2;i<=n;i++)
{
cout<<dis[i]<<endl;
}
}