spfa的魔改方法
spfa的魔改方法
(\(update\) \(on\) \(2019/8/13\))
参考文献:
-
ETO组织成员的题解小合集系列
题外话:
关于这篇博客的题目,我想了几个(光速逃……:
-
关于BellmanFord转生变成spfa的那些事
-
欢迎来到效率至上主义的OI图论
-
RE:从零开始的图论生活
-
能跑网格图,还能卡过菊花图,这样的spfa你喜欢吗
-
某科学的队优spfa
-
我的无优化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\)做如下改进:
-
当\(dis[i]=dis[u]+e[u][i]\)时,我们使\(cnt[i]+=cnt[u]\)
-
当\(dis[i]>dis[u]+e[u][i]\)时,我们使\(cnt[i]=cnt[u]\)
-
当且仅当\(vis[i]==0\)且\(cnt[i]!=0\)时,我们才把v加入队列
-
每次用完队列中的点后注意清空cnt数组,否则会重复记录
-
如果搜到一条边的起点是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求严格次短路
主要思路是:
-
如果x点的最短路能更新y点最短路,那么用x点最短路更新y点最短路,x点次短路更新y点次短路;
-
如果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和欧气)
提供三种方法:
- 边序随机:将读入给你的边随机打乱后进行spfa(可以用random_shuffle函数)
- 队列随机:每个节点入队时,以0.5的概率从队首入队,0.5的概率从队尾入队。
- 队列随机优化版:每 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;
}