Bellman-Ford算法
Dijkstra算法要求图中不可有负权边,如果有负权边呢?对于存在负权边的图,我们采用Bellman-Ford算法来求解。
Bellman-Ford算法基于迭代思想。假设图有n个顶点,m条边,源点为1,基本步骤如下:
- 初始化dis数组
- dis[1]初始化为0
- 其他初始化为 + ∞ +\infty +∞
- 反复对m条边进行松弛操作,总共进行n-1次。
- 判断是否还会进行松弛操作(dis数组是否还会变化),如果仍然可以进行松弛,表示存在负权回路,无最短路径。否则dis数组中的值即为从源点出发的最短路径。
源点S确定后,dis[S]的距离确定,经过一轮边松弛操作,至少可以确定一个点的最短路径。反复进行迭代松弛操作,进行n-1次边松弛操作后,可以确定除去源点后全部n-1个顶点的最短路径。如果第n次松弛操作仍然可以进行,则表示存在负权回路,无最短路径。
代码
for(int i=1;i<n;i++)//最多n-1次
{
for(int j=1;j<=m;j++)//m条边
dis[e[j].to]=min(dis[e[j].to],dis[e[j]].from+e[j].w);
}
SPFA
SPFA即队列优化的bellman-ford算法,国内称为SPFA算法。
bellman-ford算法进行边松弛操作时,上一轮松弛中没有发生变化的顶点出发的边也会进行松弛,存在大量的冗余操作。SPFA算法对此情况进行了优化,只对发生变化的顶点出发的边进行松弛,如果所有顶点的距离都不再改变,算法结束。
基本操作:
- 初始化dis[1]为0,其他为+ ∞ \infty ∞.
- 源点1入队列
- 从队列头中取出顶点,对从该点出发的边进行松弛操作,如果顶底的dis估值发生变化且不在队列中,将该点加入队列。
- 反复循环,直到队列为空为止。
代码
//SPFA
while(!q.empty())
{
int t=q.front();q.pop();book[t]=0;
for (int j=head[t];j!=-1;j=edge[j].next)
{
if (dis[edge[j].to]>dis[edge[j].from]+edge[j].w)
{
dis[edge[j].to]=dis[edge[j].from]+edge[j].w;
if (!book[edge[j].to]) {q.push(edge[j].to);book[edge[j].to]=1;}
}
}
}
如果元素已经在队列中,只更新路径,无需再次入队。 一个点可能出队、入队多次,但不会同时在队列里出现多次
检测负权环
SPFA中,可以通过增加计数数组,统计每一个点被松弛的次数,判断是否存在负权环。
if (dis[edge[j].to]>dis[edge[j].from]+edge[j].w)
{
dis[edge[j].to]=dis[edge[j].from]+edge[j].w;
cnt[edge[j].to]++;
if(cnt[edge[j].to]>=n)
{
cout<<"负权边";//该边的更新出现n次,表示出现负环,且该边的w为负值。
return;
}
if (!book[edge[j].to]) {q.push(edge[j].to);book[edge[j].to]=1;}
}
输出路径
while(!q.empty())
{
int t=q.front();q.pop();book[t]=0;
for (int j=head[t];j!=-1;j=edge[j].next)
{
int v=edge[j].to, w=edge[j].w;
if (dis[v]>dis[t]+w)
{
pre[v]=t;
dis[v]=dis[t]+w;
if (!book[v]) {q.push(v);book[v]=1;}
}
}
}
使用pre数组记录节点的前向节点即可输出路径