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; }
来源:https://www.cnblogs.com/xsx-blog/p/11344654.html