关于dijkstra和spfa以及bellman_ford

 关于为什么dijkstra不能处理带负权的图,但为什么spfa可以,网上一些解释在举例子的时候,用的都是正权图,让我一直不太明白,所以我用一个负权图的例子,我是小白,所以请大家多多包含。

首先借助大佬的图展示一下图论中一些方法所处理的场景,以及时间复杂度

下边是一个例子

      4
  A ——————> B
  |       |
 2|       | -5
  |       |
  v       v
  C <———— D
      1
//  朴素板
int dijkstra()
{
    memset(dist, 0x3f, sizeof(dist));
    dist[1] = 0;
    for(int i = 1; i <= n; i ++)
    {
        int t = -1;
        for(int j = 1; j <= n; j ++)
        {
            if(!st[j] && (t == -1 || dist[t] > dist[j]))
            {
                t = j;
            }
        }
        st[t] = true;
        
        for(int j = h[t]; j != -1; j = ne[j])    // ne 是 j 这条边的下一个边的索引
        {
            int k = e[j];   // j 这条边所指向的顶点
            dist[k] = min(dist[k], dist[t] + w[j]);
        }
  //  h[t] 表示从顶点 t 出发的第一条边的索引。
 // j 作为当前处理的边的索引。
 // ne[j] 表示边 j 在邻接表中的下一条边的索引。如果 ne[j] == -1 则表示这是该顶点的最后一边。
 // 这个循环遍历所有从顶点 t 出发的边。
    }
}

 

假设我们从A点开始使用Dijkstra算法:

  1. 初始化:A的距离设为0,其他点的距离设为无穷大。
  2. 选择A:将A标记为已访问,更新邻接点B和C的距离:
    • B的距离更新为 0+4=4   dist[B] = 4
    • C的距离更新为 0+2=2   dist[A] = 2
  3. 选择C(因为2比4小):将C标记为已访问,C没有出边,不更新任何距离,记住此时由于已经将C标记为以访问,此后C已经确定,不会改动
  4. 选择B:将B标记为已访问。此时B的距离为4:
    • 通过B到达D的距离更新为 4+(−5)=−1
    • D的距离更新为-1。
  5. 选择D:将D标记为已访问。此时D的距离为-1:
    • 通过D到达C的距离更新为 −1+1=0-1 + 1 = 0−1+1=0
    • C的距离为2,因此不会更新C的距离。

所以dijkstra不能处理负权边,是因为她不能回头,他是基于贪心的策略,从已知的最短路的集合S 中选择未被确定的点,来更新未知最短路径的点,而负权边的存在可能使得从其他边到达该点的距离再次变短。        ( dijkstra要求每个点被确定后st[j] = true,dist[j]就是最短距离了,之后就不能再被更新了(一锤子买卖),而如果有负权边的话,那已经确定的点的dist[j]不一定是最短了)

接下来我们看一下spfa算法,实际上spfa是在bellman_ford算法上进行优化,所以我们先来看一下bellman_ford算法

由于我们会遍历所有的边,所以边的存储方式没有什莫限制,所以这里用最简单的结构体

void bellman_ford()
{
    memset(dist, 0x3f, sizeof(dist));
    
    dist[1] = 0;
    
    for(int i = 1; i <= n; i ++)
    {
        memcpy(last, dist, sizeof(dist));
        for(int j = 1; j <= m; j ++)
        {
            int a = res[j].a, b = res[j].b, c = res[j].c;
            dist[b] = min(dist[b], last[a] + c);   // 存在重边
        }
    }
}

具体步骤:
for n 次
   for 所有边 a,b,w (松弛操作)     // a, b, w 是一条从 a 到 b 且长度为 w 的一条边
      dist[b] = min(dist[b],back[a] + w)

注意:last[] 数组是上一次迭代后 dist[] 数组的备份,
由于是每个点同时向外出发,因此需要对 dist[] 数组进行备份,
若不进行备份会因此发生串联效应,影响到下一个点

 该算法有两个需要注意的地方,一是, 外层的 n 次循环是有实际意义的,如果改为循环 k 次,实际是求在经过最多不超过 k 条边的情况下,从 起始点到达某点的最短距离,这也就是为什莫,明明是spfa在时间复杂度等各方面优于bellman_ford的情况下,依旧采用bellman_ford,因为它可以限制边数。 二是, last数组,为了防止串联,那么什么是串联

更新从 1 点到 2 点,距离为dist[2] =  1,如果我们不用last数组备份一下,那么我们在更新 3 点的时候,由于此时 2 点 已经不是无穷大,会通过 2 点更新  dist[3] = 2, 但实际上经过一条边到达 3 点的最短距离是 dist[2] = 3,所以需要备份

那为什莫可以解决负权边呢 ?  因为我们每次更新所有的边,不会出现,不会出现dijkstra中,已经确定的点不会被更新,所以即使出现负权边导致前边的点距离变短,我们也会更新。

那spfa比bellman_ford优化在哪里

具体步骤:
  for n 次
      for 所有边 a,b,w (松弛操作)     // a, b, w 是一条从 a 到 b 且长度为 w 的一条边
         dist[b] = min(dist[b],back[a] + w)

我们仔细观察会发现,我们每次会更新会更新所有的边,这个复杂度就有些高了,

dist[b] = min(dist[b],back[a] + w), 我们发现如果dist[b] 想要被更新变小,只有在back[a]变小的前提下,才有可能,所以我们可以采用宽搜的方式,每次将已经更新的点放入队列中,不断更新,直至队列为空。

int spfa()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    queue<int> q;
    q.push(1);
    st[1] = true;
    
    while(!q.empty())
    {
        int t = q.front();
        q.pop();
        st[t] = false;
        // h[a] 是以 a 为起点的这条边,h[a]存的是边的序号
        for(int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
                
            }
        }
    }
    
    return dist[n];
}

 

好了,就到这里

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值