spfa的魔改方法

北慕城南 提交于 2020-03-11 07:40:21

spfa的魔改方法

\(update\) \(on\) \(2019/8/13\)

参考文献:

  1. spfa的玄学优化和Hack方法

  2. 学图论,你真的了解最短路吗?

  3. ETO组织成员的题解小合集系列

题外话:

关于这篇博客的题目,我想了几个(光速逃……:

  1. 关于BellmanFord转生变成spfa的那些事

  2. 欢迎来到效率至上主义的OI图论

  3. RE:从零开始的图论生活

  4. 能跑网格图,还能卡过菊花图,这样的spfa你喜欢吗

  5. 某科学的队优spfa

  6. 我的无优化spfa果然有问题

进入正题:

先来说一下\(spfa\)吧,\(spfa\)的全称是\(Shortest\) \(Path\) \(Fastest\) \(Algorithm\),其实就是\(Bellman\)-\(Ford\)算法加上一个队列优化。

其时间复杂度并不稳定,最快可以达到\(O(1)\),最慢会被卡成\(Bellman\)-\(Ford\)\(O(nm)\)

今天就来总结一下学到的\(spfa\)的各种魔改:

前置:读入优化

由于毒瘤出题人很有可能会卡时间,所以我们需要读优(输出优化不一定)

标准代码:(随用随复制)

inline int read()
{
    int fu=1,x=0;char o=getchar();
    while(o<'0'||o>'9'){if(o=='-')fu=-1;o=getchar();}
    while(o>='0'&&o<='9'){x=(x<<1)+(x<<3)+(o^48);o=getchar();}
    return x*fu;
}

一、spfa记录路径

由于我这个人特别喜欢前向星存图,因此只写前向星的记录方式啦

我们用一个\(pre\)数组记录节点的前驱。当松弛成功时,我们记录\(pre[v]=u\),最后输出(或调用)时只需一个dfs从终点开始倒序输出就好了。

其实这个方法在dijkstra上也能用……

先来贴标程:

#include<queue>
#include<cstdio>
#include<iostream>
using namespace std;
struct Edge
{
    int dis,nst,to;
}edge[200010];
int n,m,head[100010],dis[100010],vis[100010],cnt,pre[100010];
void add(int a,int b,int c)
{
    edge[++cnt].nst=head[a];
    edge[cnt].to=b;
    edge[cnt].dis=c;
    head[a]=cnt;
}
void print(int x)//找路函数
{
    if (pre[x]==-1)//找到了起始点
    {
    printf("%d",x);//输出
    return ;
    }
    print(pre[x]);
    printf("-->%d",x);//输出下一个点
    return;
}
void spfa()//标准的spfa
{
    queue<int>q;
    for(int i=1;i<=n;i++)
    {
        dis[i]=0x7fffffff;
        vis[i]=0;
        pre[i]=-1;
    }
    q.push(1);
    vis[1]=1;
    dis[1]=0;
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        vis[u]=0;
        for(int i=head[u];i;i=edge[i].nst)
        {
            int v=edge[i].to;
            pre[v]=u;记录v的前驱为u
            if(dis[v]>dis[u]+edge[i].dis)
            {
                dis[v]=dis[u]+edge[i].dis;
                if(!vis[v])q.push(v),vis[v]=1;
            }
        }
    }
}
int main()
{
    int a,b,c;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
    }
    spfa();
    print(n);//找从1到n的最短路径
    printf("\n%d",dis[n]);//并输出距离
    return 0;
}

二、spfa记录最短路数量

我们用cnt数组记录从起点到每个点i的最短路数量。为了实现计数,我们只需对\(spfa\)做如下改进:

  1. \(dis[i]=dis[u]+e[u][i]\)时,我们使\(cnt[i]+=cnt[u]\)

  2. \(dis[i]>dis[u]+e[u][i]\)时,我们使\(cnt[i]=cnt[u]\)

  3. 当且仅当\(vis[i]==0\)\(cnt[i]!=0\)时,我们才把v加入队列

  4. 每次用完队列中的点后注意清空cnt数组,否则会重复记录

  5. 如果搜到一条边的起点是n,就跳过它接着搜,否则最后cnt[n]会被直接清零

请结合代码自行理解:

void spfa()
{
    queue <int> q;
    for(int i=1;i<=n;i=-~i)//这里的i=-~i就相当于i++ 
    {
        dis[i]=1e9;
        vis[i]=0;
    }
    dis[1]=0;vis[1]=1;cnt[1]=1;
    q.push(1);
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        vis[u]=0;
        if(u==n)continue;
        for(int i=head[u];i;i=edge[i].nst)
        {
            int v=edge[i].to,w=edge[i].dis;
            if(dis[v]==dis[u]+w)
            cnt[v]+=cnt[u];
            if(dis[v]>dis[u]+w)
            {
                dis[v]=dis[u]+w;
                cnt[v]=cnt[u];
            }
            if(!vis[v]&&cnt[v])
            {
                vis[v]=1;
                q.push(v);
            }
        }
        cnt[u]=0;
    }
}

三、spfa判负环

能处理负权边是\(Bellman\)-\(Ford\)算法的最大优势。作为它的优化,\(spfa\)也继承了判负环的功能。我们只需要一个\(cnt\)数组记录每个点的入队和出队次数,如果这个次数大于n,那么一定有负环,可以直接break掉

板子代码:

#include<cstdio>
#include<iostream>
#include<queue>
#include<algorithm>
using namespace std;
struct Edge
{
    int dis,nst,to;
}edge[10010];
int n,m,head[5000],dis[5000],vis[5000],cnt,num[5000];
void add(int a,int b,int c)
{
    edge[++cnt].nst=head[a];
    edge[cnt].to=b;
    edge[cnt].dis=c;
    head[a]=cnt;
}
int read()
{
    int a=0,b=0;char o;
    while((o=getchar())!='\n'&&o!=' ')
    {
        if(o=='-')b=1;
        else if(o>'9'||o<'0')continue;
        else {a=a*10;a+=o-'0';}
    }
    if(b==0)return a;
    else return -a;
}
void spfa(int s)
{
    queue <int> q;
    for(int i=1;i<=n;i++)
    {
        dis[i]=0x7fffffff;
        vis[i]=0;
    }
    dis[s]=0;
    vis[s]=1;
    q.push(s);
    while(!q.empty())
    {
        int u=q.front();
        q.pop();vis[u]=0;
        for(int i=head[u];i;i=edge[i].nst)
        {
            int v=edge[i].to;
            if(edge[i].dis+dis[u]<dis[v])
            {
                dis[v]=dis[u]+edge[i].dis;
                num[v]++;
                if(num[v]>n){printf("YE5\n");return;}
                if(!vis[v])
                {
                    q.push(v);
                    vis[v]=1;
                }
            }
        }
    }
    printf("N0\n");
}
int main()
{
    int t;
    t=read();
    for(int i=1;i<=t;i++)
    {
        n=read();m=read();
        for(int i=1;i<=m;i++)
        {
            int a,b,c;
            a=read();b=read();c=read();
            add(a,b,c);
            if(c>=0)add(b,a,c);
        }
        spfa(1);
        for(int i=0;i<=n;i++)
        head[i]=0,num[i]=0;
        cnt=0;
    }
    return 0;
}

四、spfa找最长路

在学习差分约束算法时,我们总会遇到求最长路的情况。由于\(dijkstra\)的贪心本质,用它求最长并不是最好的选择,所以我们还要用\(spfa\)

不过话虽这么说,如果你仅仅是判断最长路进行松弛,如果出题人脑子一抽会让你过,正常的出题人都会构造数据卡你,让你原地打转\(T\)到飞起

我们知道,\(spfa\)可以处理负权边,所以我们可以把边权都取相反数,然后求最短路,最后输出最短路的相反数即可

以下是代码(还没写,找时间补上


五、spfa找次短路

spfa求严格次短路

主要思路是:

  1. 如果x点的最短路能更新y点最短路,那么用x点最短路更新y点最短路,x点次短路更新y点次短路;

  2. 如果x点的最短路不能更新y点最短路,那么就用x点的最短路更新y点次短路;

核心代码:

//dis是最短路数组,dist是次短路数组
void spfa()
{
    queue <int> q;
    for(int i=1;i<=n;i++)
    {
        dis[i]=1e9;
        dist[i]=1e9;
        vis[i]=0;
    }
    dis[1]=0;
    vis[1]=1;
    q.push(1);
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        vis[u]=0;
        for(int i=head[u];i;i=edge[i].nst)
        {
            int v=edge[i].to,w=edge[i].dis;
            if(ju[v])continue;
            if(dis[v]>=dis[u]+w)
            {
                dist[v]=min(dist[v],dist[u]+w);
                dis[v]=dis[u]+w;
                if(!vis[v])
                {
                    q.push(v);
                    vis[v]=1;
                }
            }
            else if(dist[v]>dis[u]+w)
            {
                dist[v]=dis[u]+w;
                if(!vis[v])
                {
                    q.push(v);
                    vis[v]=1;
                }
            }
        }
    }
}

spfa求非严格次短路

这个要配合\(spfa\)记录路径实现。大体思路就是:先跑一边\(spfa\)找到原图最短路,然后遍历删除该最短路的每一条边,找到对应的每个图的最短路,取其中的最大值输出即可

核心代码:

int spfa()
{
    queue <int> q;
    for(int i=1;i<=n;i++)
    {
        dis[i]=0x3f3f3f3f;
        vis[i]=0;
    }
    dis[1]=0;
    vis[1]=1;
    q.push(1);
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        vis[u]=0;
        for(int i=head[u];i;i=edge[i].nst)
        {
            int v=edge[i].to;
            if(!a[u][v]&&dis[v]>dis[u]+edge[i].dis)
            {
                dis[v]=dis[u]+edge[i].dis;
                if(!flag)pre[v]=u;//只在第一遍spfa时记录路径
                if(!vis[v])
                {
                    q.push(v);
                    vis[v]=1;
                }
            }
        }
    }
    return dis[n];
}
void dfs(int u)
{
    if(pre[u]==0)return;
    a[u][pre[u]]=1;
    a[pre[u]][u]=1;
    rec[++num]=spfa();
    a[u][pre[u]]=0;
    a[pre[u]][u]=0;
    dfs(pre[u]);
}

题外话:k短路

由于求k短路要用A*算法,和\(spfa\)没有啥关系,所以本篇博客里不谈。如果感兴趣的话可以移步题解小合集——第六弹第三题

六、针对spfa的优化与Hack

此部分参考:队列优化spfa的玄学方法如何看待 SPFA 算法已死这种说法?

前置说明:所有spfa的优化都是使spfa的队列尽可能接近优先队列,而维护优先队列要log的复杂度,所以低于该复杂度的一定能被Hack。说人话就是,不论你用什么玄学方法优化spfa,只要出题人想卡你,spfa都能被卡。所以非负权图尽量别写spfa……

不过,毕竟是spfa专题,这几个优化我也说一下吧

1、SLF优化

SLF优化就是用双端队列优化\(Bellman\)-\(Ford\),每次将入队结点的dis和队首的dis比较,如果更大则插入至队尾,否则插入至队首。

if(q.size()&&dis[q.front()]<dis[v])q.push_back(v);
else q.push_front(v);

Hack:使用链套菊花的方法,在链上用几个并列在一起的小边权边就能欺骗算法多次进入菊花

2、LLL优化

对每个要出队的元素u,比较dis[u]和队列中dis的平均值,如果dis[u]更大,那么将它弹出放到队尾,取队首元素在进行重复判断,直至存在dis[x]小于平均值

while (dis[u]*cnt > sum) 
{
     q.pop();
     q.push(u);
     u = q.front();
}

Hack:向 1 连接一条权值巨大的边,这样 LLL 就失效了

3、macf优化

在第 \([l,r]\) 次访问一个结点时,将其放入队首,否则放入队尾。通常取\(l=2,r=\sqrt{v}\)

mcfx优化的原理是:如过某个节点出发的大多数边都只能更新一个次解(说白了就是这个点如果是出题人用来故意让你经过多次的节点,并且每次更新会导致一次特别长的迭代,类似菊花图的根),那么它在队列中的优先级就会降低,就像你知道出题人用这个点来卡你,你竟然还把它最先拿来最先更新,肯定是不够好的

Hack:能有效通过网格图,但是会被菊花图卡到飞起

4、SLF+容错优化

我们定义容错值\(val\),当满足 \(dis[now] > dis[q.front()] + val\)时从队尾插入,否则从队首插入。

\(w\)为边权之和,\(val\)一般为\(\sqrt{w}\)

容错SLF可以让你的程序不陷入局部最优解,与模拟退火类似

Hack:如果边权之和很小的话似乎没有什么很好的办法,所以卡法是卡 SLF 的做法,并开大边权,总和最好超过\(10^{12}\)

5.SLF+swap优化

每当队列改变时,如果队首距离大于队尾,则交换首尾。

我也没有搞懂为什么这个优化能比普通的SLF快辣么多

Hack: 与卡 SLF 类似,外挂诱导节点即可

6.随机化优化(需要rp和欧气

提供三种方法:

  1. 边序随机:将读入给你的边随机打乱后进行spfa(可以用random_shuffle函数)
  2. 队列随机:每个节点入队时,以0.5的概率从队首入队,0.5的概率从队尾入队。
  3. 队列随机优化版:每 x 次入队后,将队列元素随机打乱。

像我这种非酋还是算了吧

七、spfa求最小费用最大流

最小费用最大流算法EK

EK算法,即spfa+增广路。

我们以费用为边权建图,然后跑\(spfa\)。在spfa中,如果松弛成功,那我们往下一个结点尽可能多地流水,并且把流水的路径记录下来(\(pre[v]=u\))。

跑完SPFA后,顺着之前记录的路径从汇点溯回到源点,同时寻找增广路

最小的总费用就是 (当前路径所流总量) * (s到t的最短路径) 的和

如果看不懂可以移步网络流(六)最小费用最大流问题(毕竟我的语文太烂了……)

以下是代码:

#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
#define inf 0x3f3f3f3f
using namespace std;
int cnt=1,n,m,s,t,mcost,mflow;
int head[5005],pre[5005];
int dis[5005];//dis为从S到这个点的最小总费用
int flow[5005];//flow为从S到这个点的最大可更新流量
bool vis[5005];//vis为这个点有没有被标记过(废话)
struct Edge
{
    int to,nst,dis,flow;//dis是费用,flow是流量
}edge[100005];
void add(int a,int b,int c,int d)
{
    edge[++cnt].nst=head[a];
    edge[cnt].to=b;
    edge[cnt].flow=c;//输入的时候仔细看一看
    edge[cnt].dis=d;//要是把费用和流量搞反了就凉了
    head[a]=cnt;
}
bool spfa()
{
    queue <int> q;
    memset(vis,0,sizeof(vis));
    memset(dis,inf,sizeof(dis));
    dis[s]=0;
    vis[s]=1;
    flow[s]=inf;
    q.push(s);
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        vis[u]=0;
        for(int i=head[u];i;i=edge[i].nst)
        {
            int v=edge[i].to;
            if(edge[i].flow&&dis[u]+edge[i].dis<dis[v])//如果边还有流量就尝试更新
            {
                dis[v]=edge[i].dis+dis[u];//更新最短路径
                flow[v]=min(flow[u],edge[i].flow);//到达v点的水量取决于边剩余的容量和u点的水量
                pre[v]=i;//记录路径
                if(!vis[v])
                {
                    vis[v]=1;
                    q.push(v);
                }
            }
        }
    }
    return dis[t]!=inf;//如果还能跑到终点,就说明还不是最大流,还要继续跑spfa
}
void update()
{
    int x=t;
    while(x!=s)
    {
        int i=pre[x];
        edge[i].flow-=flow[t];//正向边加上流量
        edge[i^1].flow+=flow[t];//反向边减去流量
        x=edge[i^1].to;//沿着记录下的路径寻找增广路
    }
    mflow+=flow[t];//累计流量
    mcost+=flow[t]*dis[t];//累计费用
}
void EK(int s,int t)
{
    while(spfa())//当还有多余流量时
    update();
}
int main()
{
    scanf("%d%d%d%d",&n,&m,&s,&t);
    int a,b,c,d;
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d%d%d",&a,&b,&c,&d);
        add(a,b,c,d);
        add(b,a,0,-d);//依旧不要忘记反向建边
    }
    EK(s,t);
    printf("%d %d\n",mflow,mcost);
    return 0;
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!