浅谈SPFA(没有特别的探讨,只是对某天晚上的思考做个记录)

参考文献

https://www.cnblogs.com/jason2003/p/7224580.html
一下代码均抄自此大佬的博客。

最短路推论

对于一个最短路,最多经过\(n-1\)条边,如果超过这个,则必然重复经过一个点,则必然存在负环,证毕。

Bellman-ford 算法

通过上面,我们不难知道,我们可以用dp思想,设\(f[i]\)表示\(1\)\(i\)在最多经过\(j\)条边的最短路径,那么,我们只需要在添上一条边就是\(j+1\)条边了。

#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <cstdlib>
using namespace std;
int dis[10010];
int origin[10010],destination[10010],value[10010];//刚刚说过的三个数组
int n,m;
void Bellman_ford(int a)
{
    memset(dis,88,sizeof(dis));//赋初始值
    dis[a]=0;
    for(int i=1;i<=n-1;i++)//更新n-1次
        for(int j=1;j<=m;j++)//更新每一条边
            dis[destination[j]]=min(dis[destination[j]],dis[origin[j]]+value[j]);//判断是否更新
 } 
int main()
{
    cin>>n>>m;
    for(int i=1;i<=m;i++)
        cin>>origin[i]>>destination[i]>>value[i];
    Bellman_ford(1);
    for(int i=1;i<=n;i++)
        cout<<dis[i]<<" "; 
}

不难看出,时间复杂度是:\(O(nm)\)

SPFA

不难发现,如果\(f[i]\)在第\(j\)层没有被更新,那么他在第\(j+1\)层不会去更新人,因此可以用队列储存被更新的,一般思路就是对于队列中的这个点而言,如果他可以更新\(x\),那么就把\(x\)扔进队列里面,这样子的话,不难发现时间复杂度是\(O(nm)\)的,因为只是把没可能更新的给剔除了罢了。

但是我们发现如果\(x\)正在队列里面,我们就可以不扔进去,为什么?看这么一个队列:
在这里插入图片描述
假设\(i,x,j\)\(dis\)的层数为\(k\),并且\(i,j\)都可以更新\(x\)并产生\(k+1\)层的\(x\)的话,那么在\(x\)更新完后,\(j\) 依旧会产生\(k+1\)层的\(x\),但关键就是有时候不会存在\(j\),所以我们不会产生\(k+1\)层的\(x\),况且就是存在了,第\(k\)层的\(x\)\(dis\)竟然是\(k+1\)的(因为dis是一维数组,没有储存层数信息)!!!会不会产生影响呢?

首先,层数增大最多导致\(dis\)的减少,减少了,何乐而不为呢?

在这里插入图片描述
我们再看有没有可能经过\(x\)和虚\(x\)之间的点更新过导致虚\(x\)可以引发更多的更新?

由于dis经过更新只会不断的变小的,所以\(x\)原本更新不了的,现在也更新不了。(当然如果\(x\)到虚\(x\)之间有数字可以更新\(x\)的话就当我没说,不过这种情况会产生新的\(x\)放到队尾。)

如果你看了上面还是对这个优化绝对是正优化的证明感觉还不是很严谨,我给你简化一下过程,对于一个未优化过的队列,两个\(x\)之间不存在一个点的\(dis\)可以更新\(x\),那么后面的\(x\)的作用可以被前面的代替,且第\(k\)\(x\)\(dis\)的更新作用可以被第\(k+1\)层的\(x\)所代替,所以可以直接把后面的\(x\)删掉并且用前面的\(x\)代替后面的\(x\)(这也是为什么\(dis\)只用一维的原因)。

#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <queue>
using namespace std;
int dis[10010];
int book[10010];
int origin[10010],destination[10010],value[10010];
int n,m;
int total;
int next[10010],head[10010];
void adl(int a,int b,int c)//邻接表
{
   total++;
   origin[total]=a;
   destination[total]=b;
   value[total]=c;
   next[total]=head[a];
   head[a]=total;
}
void Bellman_ford(int a)
{
    memset(book,0,sizeof(book));//book[i]表示i号点是否在队列里
    memset(dis,88,sizeof(dis));
    queue <int> q;
    q.push(a);
    book[a]=1;
    dis[a]=0;
    while(!q.empty())//当队列不为空时更新
    {
        for(int e=head[q.front()];e;e=next[e])//枚举队首点相邻的每一个点
        {
            if(dis[destination[e]]>dis[origin[e]]+value[e])
            {
                dis[destination[e]]=dis[origin[e]]+value[e];
                if(book[destination[e]]==0)
                {
                    q.push(destination[e]);//将更新的这一个点入队
                    book[destination[e]]=1;
                }
            }
        }
        q.pop();//弹出队首元素
    }
 } 
int main()
{
    cin>>n>>m;
    for(int i=1;i<=m;i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        adl(a,b,c);
   } 
    Bellman_ford(1);
    for(int i=1;i<=n;i++)
        cout<<dis[i]<<" "; 
}

错误的证明方法

当然,时间复杂度还是不变,有人说可以利用一条边最多引起一次全图范围的更新来证明(我机房的一个大佬提出的),但是这个是否成立呢?我们称一条边所引发的全图范围的更新为涟漪(像水波一样)看这个图:

在这里插入图片描述

虽然打上了边的方向,但是个人觉得无向图也可以用这个图。

我们只需要合理调控边的边权,就可以让1号点旁边的绿边从1号点出发引发一次涟漪,但是,正是由于走了绿边,所以这个涟漪的更新速度会比\(1\)\(3\)号点的\(dis\)向外更新的速度慢一。所以涟漪和\(1\)\(3\)号点的\(dis\)向外更新均会在\(2\)号点的绿边引发一次涟漪,所以\(2\)号点周围的绿边竟然引发了两次涟漪!!!!

往后不断重复,一条边甚至可以产生更多的涟漪!!!

当然如果我有问题欢迎指出

所以应该是不能用这个方法证明的。

卡法

现在给出一种比较神奇的卡法(点多的随机稠密图好像也可以卡)。

在这里插入图片描述
调一下边的权值,争取上面小,上面大,上面的不断更新下面的,应该是可以卡到\(\frac{nm}{4}\)的(当然我没细算,我又不会时间复杂度分析)。

至此,我终于记录下了我某个平常晚上的思考。

posted @ 2020-09-13 22:00  敌敌畏58  阅读(128)  评论(0编辑  收藏  举报