最短路之Bellman-Ford & SPFA

Bellman-Ford

时间复杂度O(V*E),用于求带权图单源最短路(可含负权,可判断是否有回路)

Bellman-Ford和Dijkstra类似,都是以边的松弛操作为基础,回顾一下松弛操作:对于边(k,j),d[j] = min(d[j],d[k]+w(k,j)),即如果加上这条边那么更新起点到j的最短路为二者中最小的那个。但是Dijkstra属于贪心算法,每次都找剩余不确定的点中的最短路,但是在处理含负权边的时候这一方法已不再适用:
在这里插入图片描述
假设上图中1号节点是起点,我们第一次贪心时按Dijkstra来说,是确定(1,3)这条边为1到3的最短路,但是我们发现如果再经过2号节点到达3号节点的路径更短(因为负权的存在),因此我们就不能再贪心,而是全方面的考虑所有边

其实Bellman-Ford还是挺难理解的,建议参考具体的例子,自己画图或者参考下面博客

Bellman-Ford的算法思想是:对于所有的边(u,v,w)来说,每次都对v点进行松弛操作更新最短路。那么经过一个循环就会有的节点最短路被确定而有的节点仍然是估计值,确定值接下来不会再改变,但是估计值接下来会随着其他节点的确定而确定。例如对于部分节点来说,起点可能不含直接到他的边,那么仍然是初始的INF,因此在下面不断对所有边松弛的过程中,这部分节点会因为其他节点最短路的确定而确定。那么我们需要执行多少次呢?因为有n个节点,那么距离起点最远的节点最多有n-1条边,因此考虑最坏的情况要对所有边松弛n-1次才能确定最终的结果,于是便得到了下面的代码

const int maxn=1e5+10; 	//点的最大值
const int maxm=2e5+10; 	//边的最大值
int d[maxn]; 			//起点到每个点的距离
int u[maxm],v[maxm],w[maxm]; //省略了结构体写法

void bellman_ford(int s){ //起点s
    memset(d,0x3f,sizeof(d)); //初始化到每个点都为INF
    d[s]=0;
    for(int i=1;i<=n-1;i++){
        for(int j=1;j<=m;j++){
            d[v[j]]=min(d[v[j]],d[u[j]]+w[j]);
        }
    }
}

除此之外,bellman-ford 算法还可以检测出一个图是否含有负权回路。如果进行n-1轮松弛操作之后仍然存在"if(dis[v[i]] > dis[u[i]] + w[i]) dis[v[i]] = dis[u[i]] + w[i];"的情况,也就是说在进行n-1轮松弛后,仍可以继续成功松弛,那么此图必然存在负权回路。如果一个图没有负权回路,那么最短路径所包含的边最多为n-1条,即进行n-1轮松弛操作后最短路不会再发生变化。如果在第n轮松弛时最短路仍然可以发生变化,则这个图一定有负权回路

bool bellman_ford(int s){  
    memset(d,0x3f,sizeof(d));
    d[s]=0;  
    for(int i=1;i<=n-1;i++)  
        for(int j=1;j<=m;j++)  
            d[v[j]]=min(d[v[j]],d[u[j]]+w[j]);  
    for(int i=1;i<=m;i++)  
        if(d[v[j]]>d[u[j]]+w[j]){  
            return false;
        }  
    return true;  
}  

仔细观察上面代码,我们会发现Bellman-Ford对所有边进行松弛,实际上也相当于对所有边的to(入)节点进行松弛,于是便有了下面的队列优化

SPFA

Shortest Path Faster Algorithm,用来求含负权边的最短路径快速算法,时间复杂度平均O(k*E),k是小常数,但是可能在最坏的情况下为O(V*E)

SPFA实际上是Bellman-Ford的队列优化,我们上面提到了每一次遍历所有边都会使一些节点的最短路确定,但是这些确定的最短路在接下来仍要判断,这便是我们可以优化的地方

用数组d记录每个结点的最短路径估计值,而且用链式前向星来存储图G,因为链式前向星是最优的存储方式。算法思路为:先设置一个队列来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止,具体为:

  • 队首u出队
  • 遍历所有以队首为起点的有向边(u,v),若d[u]+w(u,v)<dis[v],则更新dis[v]
  • 如果点v不在队列中,则v入队
  • 直到队列为空,跳出循环

如果要判断是否含有负环,设置一个记录节点入队次数数组cnt,在while循环里每次入队时更新该节点入队次数,如果次数大于n,那么代表存在负环

const int maxn=1e5+10;
const int maxm=?;
struct node{
  int to,next,w;
};

node edge[maxm];
int head[maxn],d[maxn],cnt[maxn];
bool vis[maxn];
int tot,n;

addEdge(int u,int v,int w){
    tot++;
    edge[from].w=w;
    edge[from].to=v;
    edge[from].next=head[u];
    head[u]=tot;
}

bool SPFA(int s){
    queue<int> q;
    memset(vis,0,sizeof(vis));
    memset(d,0x3f,sizeof(d));
    d[s]=0;
    q.push(s);
    vis[s]=1;
    while(!q.empty()){
        int u=q.top();
        q.pop;
        vis[u]=0;
        for(int i=head[u];i;i=edge[i].next){
            int v=edge[i].to;
            int w=edge[i].w;
            if(d[v]>d[u]+w){
                d[v]=d[u]+w;
                if(!vis[v]){
                    q.push(v);
                    vis[v]=1;
                    if(++cnt[v]>n) return false;
                }
            }
        }
    }
    return true; 
}

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:撸撸猫 设计师:马嘣嘣 返回首页
评论

打赏作者

Happig丶

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值