①初始化(任意两边的距离)
②松弛操作
在图论中,最关键的是如何建图。
在最短路算法中,首先要处理数据,在这个时候,要考虑该用那种方式建图。
比较常见的建图方式:邻接链表、邻接矩阵、前向星、链式前向星、十字链表。
对于这五种建图方式,在这里不做详细讨论,只是大概介绍一下优点和缺点。
邻接链表:适合点多的图
邻接矩阵:适合边多的图
链式前向星:适合不带重边的图。除此之外,无论点多还是边多,链式前向星都能表现出很完美的效率。
前向星和十字链表个人用的很少,不做描述。
①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); }
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];//返回源点到目的点的最短距离 }
在更新的过程中,已访问的节点做一个标记,这样可以提高效率,源点初始化后不需要再访问,所以更新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算法会比较合理一些。