最短路分为单源最短路和多源最短路。
在这里先写出两种能够解决单源最短路的算法。
值得一提的是 二叉堆优化基于贪心的Dijkstra算法 和 优先队列优化基于BFS的SPFA算法 两种算法的思想殊途同归,两者的代码实现有大多相同之处,都能解决非负权单源最短路的问题,Dijkstra算法较稳定可作为首选,但如果图中存在长度为负数的边,便只有SPFA算法能够正常工作。
1.Dijkstra 算法
算法思想:
1.初始化距离d[s]=0,其余节点的d值为正无穷大;
2.找出一个未被标记且d[u]最小的节点,然后标记u;
3.扫描跟u相连的所有边v , 若d[v]>d[u]+w,则松弛更新距离 d[v]=d[u]+w;
4.重复1-3的步骤,直到所有点被标记。
朴素版本 时间复杂度O(n^2):
#include<iostream>
#include<cstdio>
#include<cstring>
const int manx=1e5+5;
const int inf=2147483647;
int a[manx][manx],d[manx];
bool vis[manx];
int n,m,s,e;
void dijkstra()
{
for(int i=1;i<=n;i++) d[i]=inf,vis[i]=0,a[i][i]=0; //初始化各数组
d[s]=0; //s作为起点
for(int i=1;i<n;i++) //重复n-1次
{
int x=0;
for(int j=1;j<=n;j++)
if( !vis[j] && (x==0||d[j]<d[x]) ) //寻找未访问过且离起点最近的点
x=j;
vis[x]=1;
for(int y=1;y<=n;y++) //第一轮d[y]中由起点可到达的点得到更新
d[y]=min( d[y ], d[x]+a[x][y])
}
}
int main()
{
cin>>n>>m;
memset(a,0x3f, sizeof(a));
for(int i=1 ; i<=m;i++)
{
int u, v, w;
scanf("%d%d%d",&u,&v,&w);
a[u][v]=min(a[u][v],w);
}
s=1;
dijkstra();
for(int i=1;i<=n;i++)
printf("%d ",d[i]);
return 0;
}
朴素算法的瓶颈在于第一步的寻找全局的最小值,可以用二叉堆对d 数组进行维护, 用O(log n)的时间分别进行获得最小值和对一条边的扩展和更新操作,最终得到优化。
优先队列(堆)优化 时间复杂度 (mlogn) :
#include<iostream>
#include<cstdio>
#include<queue>
using namespace std;
const int inf=2147483647;
const int manx=1e4+5; //与n相对,对应顶点的个数
const int mamx=5e5+5; //与m相对,对应边的个数
priority_queue< pair<int,int> >q;
struct node{
int next,v,w;
}edge[mamx]; //边去mamx,其余取manx
bool vis[manx]; //这里的标记数组与spfa的vis数组含义不同,这里标记是否入过队列
int head[manx],d[manx];
int k=0;
int n,m,s,e; //s作为起点,e作为终点
void add(int u,int v, int w) //链式前向星存图
{
edge[++k].next=head[u];
edge[k].v=v;
edge[k].w=w;
head[u]=k;
}
void dijkstra()
{
for(int i=1;i<=n;i++) //初始化vis d 数组
d[i]=inf,vis[i]=0;
d[s]=0; //s作为起点
q.push(make_pair(0,s));
while(q.size()){
int x=q.top().second; //取出队头
q.pop();
if(vis[x]) continue; //如果点x访问过,跳过,访问下一个队头
vis[x]=1; //访问x做标记
for(int i=head[x];i;i=edge[i].next){
int v=edge[i].v,w=edge[i].w;
if(d[v]>d[x]+w){ //松弛操作,更新距离
d[v]=d[x]+w;
q.push(make_pair(-d[v],v)); //把更新的距离和点入队,这里距离取负变成小根堆
}
}
}
}
int main()
{
cin>>n>>m>>s;
for(int i=1;i<=m;i++){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);
//无向图 add(v,u,w);
}
s=1;
dijkstra();
for(int i=1;i<=n;i++)
printf("%d ",d[i]);
return 0;
}
2.spfa算法
算法思想:
1.建立一个队列,队列初始只有起点;
2.取出队头点,扫描与它相连的所有边,如果出现dis[v] > dis[u]+w, 则进行更新操作,dis[v] = dis[u] +w;
3.重复1-2步骤,直到队列为空。
在某些特殊构造的图上,该算法会对不需要扩展的节点进行扫描,将原本运行效率O(Km) 中的K极大化,验证了其不稳定性,因此本文开头讲了一句非负权图的单源最短路最好用Dijkstra算法。
PFA的不稳定性也让我知道了他的可优化性,SLF优化 / LLL优化 / DFS优化 留给以后补上。
优先队列版本:
#include<iostream>
#include<cstdio>
#include<queue>
const long long int inf=214748647;
const int manx=1e5+5; //与n相对,对应顶点的个数
const int mamx=5e5+5; //与m相对,对应边的个数
using namespace std;
int n,m,s,e,k=0;
int dis[manx],head[manx];
bool vis[manx]; //这里的标记是判断点是否在队列中
struct Edge{
int next,to,dis;
}edge[mamx];
void add(int from,int to, int dis) //链式前向星
{
edge[++k].next=head[from];
edge[k].to=to;
edge[k].dis=dis;
head[from]=k;
}
void spfa()
{
queue<int >q; //建立队列
for(int i=1;i<=n;i++) //初始化dis vis数组
dis[i]=inf,vis[i]=0;
q.push(s);//s作为起点
dis[s]=0;//此处开始与dij不同
vis[s]=1;
while(!q.empty())
{
int u=q.front();
q.pop();
vis[u]=0; //出队列的时候改标记
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].to,w=edge[i].dis;
if(dis[v]>dis[u]+w) //如果存在边使得两顶点距离更小进行更新
{
dis[v]=dis[u]+w;
if(vis[v]==0) //如果不在队列中就入队
{
vis[v]=1;
q.push(v);
}
}
}
}
}
int main()
{
cin>>n>>m>>s;
for(int i=1;i<=m;i++){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);
//无向图 add(v,u,w);
}
spfa();
for(int i=1;i<=n;i++)
cout<<dis[i]<<" ";
return 0;
}
多源最短路问题便可以用Floyd算法来完成求解。
Folyd算法 时间复杂度O(n^3) :
算法思想 (动态规划) :
d[i][j] = min ( d[i][j] , d[i][k] + d[k][j] ) // 从i顶点到j顶点只经过前k个点的最短路程
在由i 到 j 的最短路径可由 两个状态转移:
1.由i 到 j 的路径(只经过k-1个点)
2.由i 到k , 再由k 到j 的路径(经过k个点)
这里的k指的是能够作为中转点(即由i到j的途中经过的点) 的 顶点数。
memset( d , 0x3f , sizeof(d) ); //初始化距离
for(int i=1 ;i<=n ;i++) d[i][i]= 0; // 同一点距离为0
for(int i=1 ;i<=m ;i++){
int u, v, w;
cin>>u>> v>> w;
a[u][v]=min( a[u][v] , w);
}
for(int k=1 ;k<=n ;k++) //注意k为阶段必须放在最外层
for(int i=1 ;i<=n ;i++)
for(int j=1 ;j<=n ;j++)
d[i][j] = min( d[i][j] , d[i][k] +d[k][j]);
还有,Floyd也不能解决带负权图的问题。