SPFA详解

引子

定义

SPFA是Shortest Path Faster Algorithm,是Bellman-Ford算法的改进版。和其他最短路算法一样,都是以松弛操作的三角形不等式为基础操作的。

优点

SPFA算法用途广,适应负权,还能判断正环和负环……在差分约束建模中也有重大用处……SPFA是个好东西

SPFA的实现

spfa有两种实现方式,一种是栈实现,一种是队列实现。
在有负环的情况下,栈比队列更快,但是如果没有负环的一般情况下,队列更快。

栈实现

用栈实现的spfa实际上跟dfs很像,实际上也可以说就是dfs。采用邻接表可以很好的实现,时间复杂度O(ne)

队列实现

先把源点进队,然后用源点扩展更新,能迭代的都进队……
就是用队列里的点去迭代其他点,被迭代的点再次进队。
时间复杂度O(ne)

SPFA的应用

spfa在noip中通常用于求最短路/判负环。求最短路,spfa相比Dijkstra、Bellman-Ford等算法有着优秀的时间复杂度和剪枝,还有自己的特色:可以判负环。因为有可以判负环的性质,一般还用在差分约束系统建模后的一些判断工作。

求最短路

单源最短路径模板题
这道题目n数据量10000,m数据量500000,明显floyd超时超空间,Dijkstra也超时。
所以我们用了spfa:

#include
#define maxn 100010
#define maxm 500010
#define maxint 2147483647
using namespace std;
inline int read()
{
    int num=0;
    char c=getchar();
    for(;c<'0'||c>'9';c=getchar());
    for(;c>='0'&&c<='9';c=getchar())num=num*10+c-'0';
    return num;
}
int n,m,s,top,dis[maxn*4];
bool vis[maxn*4];
struct node
{
    int nxt,len,order; 
}a[maxm*4];
int ljhead[maxn*4];
int q[maxn*3],head,tail=0;
void addedge(int x,int y,int length)
{
    top++;
    a[top].len=length;
    a[top].nxt=ljhead[x];
    a[top].order=y;
    ljhead[x]=top;
}//邻接表插入边。
void init()
{
    n=read();
    m=read();
    s=read();
    for(int i=1;i<=m;i++)
    {
        int x,y,length;
        x=read();
        y=read();
        length=read();
        addedge(x,y,length);
    }
}
void SPFA()
{
    for(int i=1;i<=n;i++)
    {
        dis[i]=maxint;
        vis[i]=false;
    }
    dis[s]=0;
    vis[s]=true;
    int v;
    head=0;tail=1;q[1]=s;
    while(tail-head+1>0)
    {
        v=q[++head];
        for(int i=ljhead[v];i;i=a[i].nxt)
        if(dis[v]+a[i].lenif(vis[a[i].order]==false)
            {
                vis[a[i].order]=true;
                tail++;
                q[tail]=a[i].order;
            }
        }
        vis[q[head]]=false;
    }
}
int main()
{
    init();
    SPFA();
    for(int i=1;i<=n;i++)
    printf("%d ",dis[i]);
    return 0;
}

判断负环

负环模板题
这道题目是板子题啊,我们直接上dfs_spfa!

#include
#define maxn 200200
#define INF 2e9
using namespace std;
inline int read()
{
    int num=0;
    bool flag=true;
    char c;
    for(;c>'9'||c<'0';c=getchar())
    if(c=='-')
    flag=false;
    for(;c>='0'&&c<='9';num=num*10+c-48,c=getchar());
    return flag ? num : -num;
}
struct node
{
    int to,val;
};
int dis[maxn],t,n,m;
bool vis[maxn],flag;
vectorG[maxn];

void insert(int x,int y,int w)
{
    G[x].push_back((node){y,w});
}

void init()
{
    n=read();
    m=read();
    for(int i=0;i<=n;i++)
    {
        dis[i]=INF;
        vis[i]=false;
        G[i].clear();
    }
    for(int i=1;i<=m;i++)
    {
        int x=read();
        int y=read();
        int w=read();
        if(w<0)
        {
            insert(x,y,w);
        }
        else
        {
            insert(x,y,w);
            insert(y,x,w);
        }
    }
}

void spfa(int u)
{
    vis[u]=true;
    for(int i=0;iint to=G[u][i].to;//是这条边对面的点
        if(dis[u]+G[u][i].valif(vis[to])
            {
                flag=true;
                return ;
            }
            else
            {
                spfa(to);
            }
        }
    }
    vis[u]=false;
}
int main()
{
    t=read();
    while(t--)
    {
        init();
        flag=false;
        dis[1]=0;
        spfa(1);
        if(flag)
        {
            printf("YE5\n");
        }
        else
        {
            printf("N0\n");
        }
    }
    return 0; 
}

但是可惜啊!tle3个点,O-2优化开了也没用!
我们需要思考下dfs_spfa的优化了!

判断负环的优化

既然我们只需要判断负环,那么就相当于我们需要找到一条权值和为负的回路。
既然我们只需要找到权值和为负的回路,那不妨使距离数组dis初始化为0。
这样处理后,第一次拓展只会拓展到与起点相连边权为负的边。
那么我们就分别枚举所有的点作为起点,如果已经找到一个负环就不再继续枚举。
根据SPFA,我们找到的负环一定包含当前枚举的这个点。
算法期望复杂度O(m)

#include
#define maxn 200200
#define INF 2e9
using namespace std;
inline int read()
{
    int num=0;
    bool flag=true;
    char c;
    for(;c>'9'||c<'0';c=getchar())
    if(c=='-')
    flag=false;
    for(;c>='0'&&c<='9';num=num*10+c-48,c=getchar());
    return flag ? num : -num;
}
struct node
{
    int to,val;
};
int dis[maxn],t,n,m;
bool vis[maxn],flag;
vectorG[maxn];

void insert(int x,int y,int w)
{
    G[x].push_back((node){y,w});
}

void init()
{
    n=read();
    m=read();
    for(int i=0;i<=n;i++)
    {
        dis[i]=0;//这里由于要枚举点,所以要赋值为0;
        vis[i]=false;
        G[i].clear();
    }
    for(int i=1;i<=m;i++)
    {
        int x=read();
        int y=read();
        int w=read();
        if(w<0)
        {
            insert(x,y,w);
        }
        else
        {
            insert(x,y,w);
            insert(y,x,w);
        }
    }
}

void spfa(int u)//spfa基本上没有什么变化
{
    if(flag)return; 
    vis[u]=true;
    for(int i=0;iif(flag)return; 
        int to=G[u][i].to;
        if(dis[u]+G[u][i].valif(vis[to])
            {
                flag=true;
                return ;
            }
            else
            {
                spfa(to);
            }
        }
    }
    vis[u]=false;
}
int main()
{
    t=read();
    while(t--)
    {
        init();
        flag=false;
        dis[1]=0;
        for(int i=1;i<=n;i++)
        {
            spfa(i);//枚举每一个点
            if(flag)break;
        }
        if(flag)
        {
            printf("YE5\n");
        }
        else
        {
            printf("N0\n");
        }
    }
    return 0; 
}

原来的程序运行时间是5112ms,优化后的程序运行时间312ms……

差分约束系统

我们在差分约束题目中会得出一系列代数约束关系,例如xy等,我们这时候可以通过这些约束关系建立有向图,然后利用spfa来判断最值、是否存在解或者有无穷多的解。
详见:差分约束详解by柴犬首相

高阶版敬请期待

GG

你可能感兴趣的:(SPFA详解)