最短路算法基本可以分为以下两个步骤:
①初始化(任意两边的距离)
②松弛操作
在图论中,最关键的是如何建图。
在最短路算法中,首先要处理数据,在这个时候,要考虑该用那种方式建图。
比较常见的建图方式:邻接链表、邻接矩阵、前向星、链式前向星、十字链表。
对于这五种建图方式,在这里不做详细讨论,只是大概介绍一下优点和缺点。
邻接链表:适合点多的图
邻接矩阵:适合边多的图
链式前向星:适合不带重边的图。除此之外,无论点多还是边多,链式前向星都能表现出很完美的效率。
前向星和十字链表个人用的很少,不做描述。
①Floyd最短路算法
Floyd最短路算法的代码很短,5行就能搞定,但是思想却非常值得学习。
采用动态规划思想:
dp[k][i][j]表示从i到j之间可以经过1~k节点的最短路径。
状态转移方程:dp[k][i][j]=min{dp[k-1][i][j],dp[k-1][i][k]+dp[k-1][k][j]};
对于dp[k][i][j],可以从dp[k-1][i][j]不经过k结点,或者从dp[k-1][i][j]经过k节点,即dp[k-1][i][k]+dp[k-1][k][j];
因为dp[k]只和dp[k-1]有关,所以可以省略dp最外层的一维空间。
(在初始化操作中,需要将dp初始化为无穷大,但是在松弛操作中,又需要避免数据溢出,所以需要选择一个合理的“无穷大”,0x3f3f3f3f是1061109567)
int dp[maxn][maxn];
void Floyd(){
memset(dp,0x3f3f3f3f,sizeof(dp));
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]);
}
}
}
}
下面介绍一下如何打印路径。
用path[i][j]表示j节点的前驱。在更新dp[i][j]最短路径的同时记录更新path[i][j];
(推荐题:hdu1385)
void print(int a,int b){
printf("Path: %d",a);
int next=path[a][b];
while(next!=b){
printf("-->%d",next);
next=path[next][b];
}
if(a!=b)
printf("-->%d",b);
putchar(10);
}
②Dijsltra最短路算法
Dijsktra算法是求单源路径最短路问题。
在n^2的效率下计算出源点src到任一点的最短路径,而Floyd算法是在n^3的效率下计算出任意两点的最短距离。
另外一点,在最短路中也需要注意考虑重边的情况,养成好习惯。
下面先给出Dijsktra算法的详细代码并标注解释:
const int INF=0x3f3f3f3f;
const int maxn=1010;
int map[maxn][maxn];//map[i][j]表示从i到j的距离
int dis[maxn];//表示从源点到i点的最短距离
bool vis[maxn];//记录该点是否已经访问过
int Dijsktra(int src,int des){
memset(dis,INF,sizeof(dis));//初始化dis数组
dis[src]=0;//源点到本身的距离为0
vis[src]=true;//标记源点
for(int i=1;i<=n-1;i++){//只需要更新n-1次
int pos,min=-INF;
for(int j=1;j<=n;j++){
if(!vis[j]&&dis[j]<min)
min=dis[pos=j];
}
if(min==INF) return -1;//如果不存在,返回-1
vis[pos]=1;
for(int j=1;j<=n;j++){//更新dis数组
if(!vis[j]&&dis[j]>dis[pos]+map[pos][j])
dis[j]=dis[pos]+map[pos][j];
}
}
return dis[des];//返回源点到目的点的最短距离
}
应该可以注意到,通俗一点来讲,Dijsktra就是彼此找出一个距离src最近的点pos,以这个点作为中介点更新src到其他所有点的最短距离。
在更新的过程中,已访问的节点做一个标记,这样可以提高效率,源点初始化后不需要再访问,所以更新n-1次即可。
但是在Dijsktra的优点在于,在查找中介点的过程中,需要遍历所有点,效率为o(n),但是如果用二叉堆来优化的话,效率只需要o(logn)。
这里,我们用priority_queue来实现。
如果不用优先队列的话,Dijsktra的效率为O(2|E|+|v|^2),用优先队列查找里源点最近的点时效率为O(log|V|),整体效率为O(2|E|+|v|log|V|)
具体代码如下(Dijsktra+优先队列这种方式也是有必要掌握的。):
struct edge{
int to,cost;
edge(){}
edge(int to,int cost):to(to),cost(cost){}
};
typedef pair<int,int> P;
vector<edge> G[maxn];
int dis[maxn][maxn];//dis[i][j]表示i->j的最短距离
int a,b,c;
void Dijsktra(int s){
priority_queue<P, vector<P>, greater<P> > q;//优先队列优化,维护最短路径
memset(dis,INF,sizeof(dis));
dis[s][s]=0;;
q.push(P(0,s));
while(!q.empty()){
P p=q.top();q.pop();
int v=p.second;
if(dis[s][v]<p.first) continue;
for(int i=0;i<G[v].size();i++){//更新最短路径
edge e=G[v][i];
if(dis[s][e.to]>dis[s][v]+e.cost){
dis[s][e.to]=dis[s][v]+e.cost;
q.push(P(dis[s][e.to],e.to));
}
}
}
}
③Bellman-Ford算法:
Bellman-Ford算法可以解决帶负环的问题。这也是它相对于上面两个算法最大的优势所在。
对每一条边e[x],如果dis[edge[x].u]>dis[edge[x].v]+edge[x].w,则edge[x].u=edge[x].v+edge[x].w;该操作至多只需要进行n-1次
为了判断图中是否存在负环,即权值之和<0的环路,对于每一条边e[x],如果存在dis[e[x].u]>dis[edge[x].v]+edge[x].w,则图中存在负环,无法求出单源最短路径。
(推荐提:POJ 1860)
const int INF=0x3f3f3f3f;
const int maxn=1010;
int dis[maxn];
int e;
void init(){
memset(dis,INF,sizeof(dis));
e=0;
}
struct node{
int u;
int v;
int w;
}edge[maxn];
void addEdge(int u,int v,int w){
edge[e].u=u,edge[e].v=v,edge[e].w=w;
e++;
}
void relax(int x){//松弛操作
if(dis[edge[x].u]>dis[edge[x].v]+edge[x].w){
edge[x].u=edge[x].v+edge[x].w;
}
}
bool Bellman_Ford(int src){
dis[src]=0;
for(int i=1;i<=n;i++){
for(int j=0;j<e;j++){
relax(j);//对每一条变进行松弛操作
}
}
for(int i=0;i<e;i++){
if(dis[edge[i].u]>dis[edge[i].v]+edge[i].w){
return false;//有回路
}
}
return true;//无回路
}
④SPFA最短路算法
SPFA其实是Bellman-Ford算法的队列优化。
先取队首元素u,并将其出队,取消标记,将于点u直接相连的所有点进行松弛操作,如果能进行松弛,那么就更新dis数组。
更新结束后,判断该点是否在队列中,如果不在,那么将点入列,然后进行标记。
判断有无负环:如果某个点进入队列的次数超过n次,则存在负环。
下面给出用STL队列实现的算法:
(推荐题:POJ 3259)
const int INF=0x3f3f3f3f;
const int maxn=1010;
int dis[maxn],head[maxn],inQueue[maxn];
bool vis[maxn];
int e;
void init(){
memset(dis,INF,sizeof(dis));
memset(head,-1,sizeof(head));
memset(vis,0,sizeof(vis));
memset(inQueue,0,sizeof(inQueue));
e=0;
}
struct node{//链式前向星建图
int v;
int w;
int next;
}edge[maxn];
void addEdge(int u,int v,int w){
edge[e].v=v,edge[e].w=w,edge[e].next=head[u],head[u]=e;
e++;
}
bool Spfa(int src){
dis[src]=0;
vis[src]=1;
queue<int>Q;
Q.push(src);//源点放入队列
while(!Q.empty()){
int s=Q.front();
Q.pop();
vis[s]=0;//出队时取消标记
inQueue[s]++;
if(inQueue[s]>n) return false;//如果一个点入队n次,表明存在负环
for(int i=head[s];i!=-1;i=edge[s].next){
if(dis[edge[i].v]>dis[s]+edge[i].w){//松弛操作
dis[edge[i].v]=dis[s]+edge[i].w;
if(!vis[edge[i].v]){
vis[edge[i].v]=1;//入队时进行标记
Q.push(edge[i].v);
}
}
}
}
return true;
}
如果想要快速判断是否存在负环,Dfs深搜的效率会明显较高。
在无负环的情况下,选择Dijsktra最短路算法效率会比较高,SPFA算法的时间不稳定,Bellman-Ford和Floyd算法的效率都比较高。
在有负环的情况下,选择SPFA算法会比较合理一些。