求最短路的基本算法有三种 Dijkstra算法,Floyd算法, Spfa算法。
其中Djs和Spfa都是单源最短路算法一般用于求从单一源点出发到其他点的最短距离,Floyd是多源最短路算法,可以求出任意两点的最短距离。
其次这三种算法的基本原理都是通过中转点达到缩短两点距离的目的。例如:1->2 距离为5、1->3距离为1、3->2距离为2.那么1->2的最短距离就可以通过中转点3来缩短,1->3->2 距离为3,比直接1->2的5要小。
Dijkstra算法(堆优化)
朴素的Djs算法是根据每次找到距离源点最近的城市(这里用城市来描述点),将其当做中转点来缩短其他城市的距离(此后所有距离都指距离源点的距离),把所有能缩短距离的城市都放入队列中,然后将这个中转城市标记。再一次找到未标记的距离源点最近的城市进行如上操作。因为每次都需要一个循环来找到未标记的距离源点最近的中转城市,所以时间复杂度大致是O(n^2)的。稍微大一点的图的我们就跑不动了。是不是可以优化呢,答案是肯定的。我们把优化的方向放到每次找中转点都需要O(n)的时间这一过程上。
于是我们就用到了优先队列(堆实现)维护的数据是城市编号与距离源点的距离,每次将缩短距离的城市入队时,优先队列会自动排序将距离源点最近的点放到队首,我们之间取用就行了。时间复杂度O(n*logn)
两道模板题,其中弱化版也许大概可能应该 可以用朴素算法AC我也没试过
P3371 【模板】单源最短路径(弱化版)
P4779 【模板】单源最短路径(标准版)
下面是AC代码
#include<queue>
#include<vector>
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
const int N=1e5+3;
const int M=5e5+3;
const int INF=0x7FFFFFFF;
int n,m,s,tot;
int dis[N],vis[N],head[N];
struct Edge
{
int to,dis,next;
}edge[M];
struct node
{
int u,L;
};
bool operator<(node a,node b)//重载比较运算符
{
return a.L>b.L;//大于号是小顶堆,小的在队首,小于号才是大顶堆
}
void add(int from,int to,int w)//链式前向星
{
edge[++tot].to = to;
edge[tot].dis = w;
edge[tot].next = head[from];
head[from] = tot;
}
priority_queue<node>q;//优先队列 堆优化
void djs()
{
q.push({s,0});
dis[s]=0;
while(!q.empty())
{
node t = q.top();
int u = t.u;
q.pop();
if(vis[u])continue;//若已经被标记过了,直接跳过取下一个元素
vis[u]=1;//标记
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].to;
if(vis[v])continue;
if(dis[v] > dis[u]+edge[i].dis)//如果能缩短距离就入队
{
dis[v] = dis[u]+edge[i].dis;
q.push({v,dis[v]});
}
}
}
for(int i=1;i<=n;i++)//输出距离
printf("%d ",dis[i]);
}
int main()
{
int i,u,v,w;
scanf("%d%d%d",&n,&m,&s);
for(i=1;i<=n;i++) dis[i]=INF;//将所有距离初始化为无穷大
for(i=0;i<m;i++)
{
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);
}
djs();
return 0;
}
注意!!!Dijkstra算法不能求有负数边权的最短路,因为Djs每次都是寻找距离源点最近的点中转并标记,所有点仅仅有一次或零次机会作为中转点。都是正数边的话这样缩短的一定是最优解,但有负权的话可能需要多次入队将其作为中转点才能找到最优解。
SPFA算法
Spfa算法与Djs最大的区别在于,是将任意点作为中转站来进行松弛操作的,并不是距离源点最近的点。并且所有点都有可能有多次作为中转的点的机会,所以SPFA是可以求负权边的。并且SPFA可以判断有负环的最短路的这样的图是不存在最短路径的,每绕着环跑一次都会缩小。
模板题:试题 算法提高 道路和航路记得加优化哦,不然只有90分
朴素SPFA:
#include<queue>
#include<vector>
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
const int N=1e5+3;
const int M=5e5+3;
const int INF=0x7FFFFFFF;
int n,m,s,tot;
int dis[N],vis[N],head[N];
struct Edge
{
int to,dis,next;
}edge[M];
void add(int from,int to,int w)
{
edge[++tot].to = to;
edge[tot].dis = w;
edge[tot].next = head[from];
head[from] = tot;
}
void djs()
{
queue<int>q;
q.push(s);
dis[s]=0;
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;
if(dis[v]>dis[u]+edge[i].dis)
{
dis[v] = dis[u]+edge[i].dis;
if(!vis[v])
{
vis[v]=1;
q.push(v);
}
}
}
}
}
int main()
{
int i,u,v,w,p;
scanf("%d%d%d%d",&n,&m,&p,&s);
for(i=1;i<=n;i++) dis[i]=INF;
for(i=0;i<m;i++)
{
scanf("%d%d%d",&u,&v,&w);
add(u,v,w), add(v,u,w);
}
for(i=0;i<p;i++)
{
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);
}
djs();
for(i=1;i<=n;i++)
{
if(dis[i]==INF)printf("NO PATH\n");
else printf("%d\n",dis[i]);
}
return 0;
}
不经过优化的spfa较慢,自然就会有优化的方法,以下两种方法学自博客SPFA的两种优化方法——SLF和LLL
(一)SLF(Small Label First) 优化
优化思路:将原队列改成双端队列,对要加入队列的点 p,如果 dist[p] 小于队头元素 u 的 dist[u],将其插入到队头,否则插入到队尾。
#include<queue>
#include<vector>
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
const int N=1e6+3;
const int M=5e6+3;
const int INF=0x7FFFFFFF;
int n,m,s,tot;
int dis[N],vis[N],head[N];
struct Edge
{
int to,dis,next;
}edge[M];
inline int read()
{
int x = 0, y = 1;char c = getchar();
while (c < '0' || c>'9') { if (c == '-') y = -1;c = getchar(); }
while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
return x * y;
}
void add(int from,int to,int w)
{
edge[++tot].to = to;
edge[tot].dis = w;
edge[tot].next = head[from];
head[from] = tot;
}
void djs()
{
deque<int>q;
q.push_back(s);
dis[s]=0;
while(!q.empty())
{
int u=q.front();
q.pop_front();
vis[u] = 0;
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].to;
if(dis[v]>dis[u]+edge[i].dis)
{
dis[v] = dis[u]+edge[i].dis;
if(!vis[v])
{
vis[v]=1;
if(!q.empty()&&dis[v]<=dis[q.front()]) q.push_front(v);
else q.push_back(v);
}
}
}
}
}
int main()
{
int i,u,v,w,p;
scanf("%d%d%d%d",&n,&m,&p,&s);
for(i=1;i<=n;i++) dis[i]=INF;
for(i=0;i<m;i++)
{
u=read(), v=read(), w=read();
add(u,v,w), add(v,u,w);
}
for(i=0;i<p;i++)
{
u=read(), v=read(), w=read();
add(u,v,w);
}
djs();
for(i=1;i<=n;i++)
{
if(dis[i]==INF)printf("NO PATH\n");
else printf("%d\n",dis[i]);
}
return 0;
}
(二)LLL(Large Label Last) 优化
优化思路:对每个要出队的队头元素 u,比较 dis[u] 和队列中点的 dis 的平均值,如果 dis[u] 更大,将其弹出放到队尾,然后取队首元素进行相同操作,直到队头元素的 dis 小于等于平均值。
由于优化效果不明显甚至可能更慢故不贴出代码。
可以看看这篇博客SPFA的几种优化以及Hack的方法
最后关于SPFA,他死了,大哭 嗯,能用Djs就不要用SPFA.
Floyd算法
多源最短路算法,核心思想就是将所有点都当做中转站来进行松弛操作,核心代码很简短,三重循环,时间复杂度O(n^3).具体内容可以在这篇博客学习:Floyd-傻子也能看懂的弗洛伊德算法
for(k=1;k<=n;k++)//中转的点
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
if(e[i][j]>e[i][k]+e[k][j])
e[i][j]=e[i][k]+e[k][j];//邻接矩阵存图
推荐一道题,能帮助理解FloydP1119 灾后重建
AC代码:
#include<queue>
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
const int N = 203;
const int inf = 0x3f3f3f3f;
int time[N],dis[N][N],n;
bool vis[N];
void Floyd(int k)//将时间更新的点作为中转更新最短路
{
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
dis[i][j] = min(dis[i][j],dis[i][k]+dis[k][j]);
}
int main()
{
int m,i,u,v,w,q,ti;
scanf("%d%d",&n,&m);
memset(dis,inf,sizeof(dis));
for(i=0;i<n;i++)
{
scanf("%d",&time[i]);
dis[i][i] = 0;
}
for(i=0;i<m;i++)
{
scanf("%d%d%d",&u,&v,&w);
dis[u][v] = dis[v][u] = w;
}
scanf("%d",&q);
int now=0;
for(i=0;i<q;i++)
{
scanf("%d%d%d",&u,&v,&ti);
if(time[u]>ti||time[v]>ti)
{
printf("-1\n");
continue;
}
while(time[now]<=ti&&now<n)//将询问时间点之前的所有未作过中转站的点跑一遍Floyd
{
Floyd(now);
now++;
}
if(dis[u][v]!=inf) printf("%d\n",dis[u][v]);
else printf("-1\n");
}
return 0;
}
优化呢,我还没学也不知道有没有,先鸽了 ,下次再更新。