单源非负边权最短路径
单源非负边权最短路径
关于spfa,他死了”
spfa还没死
关于spfa死了,是指在某些非负边权图中,spfa可以被卡掉。
具体而言,虽然我也不清楚,可以被网格图,菊花图卡掉。原理不明,如何构建卡spfa数据方法不明
但是,spfa还是有用武之地,具体而言:
1.在带负权边的图上跑最短路径,用spfa跑依旧带劲
2.网络流的费用流问题上,spfa依旧有一席之地
3.用法来判负环
同时,spfa的复杂度为O(玄) o(∩_∩)o
spfa还值得抢救
关于spfa的优化如下,按照:原始spfa,spfa+SLF,spfa+LLL,spfa+SLF+LLL的顺序讲解
原始spfa
先来一发最原始的spfa:
//a very naive spfa
#include <cstdio>
#include <algorithm>
#include <iostream>
#include <cstring>
using namespace std;
const int MAXD=1e5+5,MAXE=2e5+5,INF=0x3f3f3f3f;
int n,m,src;
int ecnt,edge[MAXE],head[MAXD],nxt[MAXE],wei[MAXE];
int dis[MAXD],line[(MAXD<<2)+5],l,r; bool vis[MAXD];
int read();
void insert(int,int,int,int);
int adjust(int);
void spfa();
int main(){
freopen("test.in","r",stdin);
n=read(); m=read(); src=read();
for(int i=1;i<=m;++i){
int tmp1=read(),tmp2=read(),tmp3=read();
insert(tmp1,tmp2,tmp3,++ecnt);
}
spfa();
for(int i=1;i<=n;++i) printf("%d ",dis[i]);
return 0;
}
int read(){
char tmp=getchar(); int sum=0; bool flag=false;
while(tmp<'0'||tmp>'9'){
if(tmp=='-') flag=true;
tmp=getchar();
}
while(tmp>='0'&&tmp<='9'){
sum=(sum<<3)+(sum<<1)+tmp-'0';
tmp=getchar();
}
return flag?-sum:sum;
}
void insert(int from,int to,int w,int id){
nxt[id]=head[from]; head[from]=id; edge[id]=to; wei[id]=w;
}
int adjust(int x){
int limit=(MAXD<<2)+5;
while(x>=limit) x-=limit;
while(x<0) x+=limit;
return x;
}
void spfa(){
l=0; r=1; line[r]=src;
for(int i=1;i<=n;++i) dis[i]=INF; dis[src]=0;
while(l<r){
l++; l=adjust(l);
int u=line[l];
vis[u]=false;
for(int i=head[u];i;i=nxt[i]){
int v=edge[i];
if(dis[v]>dis[u]+wei[i]){
dis[v]=dis[u]+wei[i];
if(!vis[v]){
r++; r=adjust(r);
line[r]=v;
}
}
}
}
}
SLF(Small Label First)优化:
原队列(queue)转换为双端队列(deque),对于一个即将入队的u,如果dis[u]小于队首元素的dis dis[line.front()],那么,就将这个u插到队首,否则插到队尾
emm,简单理解理解,可能就是指尽可能用较优的dis去更新,这样的话,更新出来的dis也会更加可能优秀
code:
// the naiva spfa with SLF
#include <cstdio>
#include <algorithm>
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int MAXD=1e5+5,MAXE=2e5+5,INF=0x3f3f3f3f;
int n,m,src;
int ecnt,edge[MAXE],head[MAXD],nxt[MAXE],wei[MAXE];
int dis[MAXD]; bool vis[MAXD];
deque <int> line;
int read();
void insert(int,int,int,int);
int adjust(int);
void spfa();
int main(){
freopen("test.in","r",stdin);
n=read(); m=read(); src=read();
for(int i=1;i<=m;++i){
int tmp1=read(),tmp2=read(),tmp3=read();
insert(tmp1,tmp2,tmp3,++ecnt);
}
spfa();
for(int i=1;i<=n;++i) printf("%d ",dis[i]);
return 0;
}
int read(){
char tmp=getchar(); int sum=0; bool flag=false;
while(tmp<'0'||tmp>'9'){
if(tmp=='-') flag=true;
tmp=getchar();
}
while(tmp>='0'&&tmp<='9'){
sum=(sum<<3)+(sum<<1)+tmp-'0';
tmp=getchar();
}
return flag?-sum:sum;
}
void insert(int from,int to,int w,int id){
nxt[id]=head[from]; head[from]=id; edge[id]=to; wei[id]=w;
}
int adjust(int x){
int limit=(MAXD<<2)+5;
while(x>=limit) x-=limit;
while(x<0) x+=limit;
return x;
}
void spfa(){
line.push_back(src);
for(int i=1;i<=n;++i) dis[i]=INF; dis[src]=0;
while(!line.empty()){
int u=line.front(); line.pop_front();
vis[u]=false;
for(int i=head[u];i;i=nxt[i]){
int v=edge[i];
if(dis[v]>dis[u]+wei[i]){
dis[v]=dis[u]+wei[i];
if(!vis[v]){
vis[v]=true;
if(!line.empty()&&dis[v]<=dis[line.front()]) line.push_front(v);
else line.push_back(v);
}
}
}
}
}
LLL(Large Label Last)优化
它是指,对于每一个即将出队的元素u,比较dis[u]与队列中全部dis的平均值,如果dis[u]相较而言更大,那么就将这个可怜的u重新放在队尾,直至弹出一个u,满足dis[u]小于平均值
它指的也是用较优的解去更新,吧..
code:
// the naiva spfa with LLL
#include <cstdio>
#include <algorithm>
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int MAXD=1e5+5,MAXE=2e5+5,INF=0x3f3f3f3f;
int n,m,src;
int ecnt,edge[MAXE],head[MAXD],nxt[MAXE],wei[MAXE];
int dis[MAXD]; bool vis[MAXD];
deque <int> line; int cnt,sum;
int read();
void insert(int,int,int,int);
int adjust(int);
void spfa();
int main(){
freopen("test.in","r",stdin);
n=read(); m=read(); src=read();
for(int i=1;i<=m;++i){
int tmp1=read(),tmp2=read(),tmp3=read();
insert(tmp1,tmp2,tmp3,++ecnt);
}
spfa();
for(int i=1;i<=n;++i) printf("%d ",dis[i]);
return 0;
}
int read(){
char tmp=getchar(); int sum=0; bool flag=false;
while(tmp<'0'||tmp>'9'){
if(tmp=='-') flag=true;
tmp=getchar();
}
while(tmp>='0'&&tmp<='9'){
sum=(sum<<3)+(sum<<1)+tmp-'0';
tmp=getchar();
}
return flag?-sum:sum;
}
void insert(int from,int to,int w,int id){
nxt[id]=head[from]; head[from]=id; edge[id]=to; wei[id]=w;
}
int adjust(int x){
int limit=(MAXD<<2)+5;
while(x>=limit) x-=limit;
while(x<0) x+=limit;
return x;
}
void spfa(){
for(int i=1;i<=n;++i) dis[i]=INF; dis[src]=0;
line.push_back(src); cnt++; sum+=dis[src];
while(!line.empty()){
int u=line.front(); line.pop_front();
while(dis[u]*cnt>sum){
line.push_back(u);
u=line.front(); line.pop_front();
}
cnt--; sum-=dis[u];
vis[u]=false;
for(int i=head[u];i;i=nxt[i]){
int v=edge[i];
if(dis[v]>dis[u]+wei[i]){
dis[v]=dis[u]+wei[i];
if(!vis[v]){
vis[v]=true;
line.push_front(v);
cnt++; sum+=dis[v];
}
}
}
}
}
SLF+LLL
并没有什么好说的,就是SLF优化与LLL优化同时写。
code:
// the naiva spfa with both SLF and LLL
#include <cstdio>
#include <algorithm>
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int MAXD=1e5+5,MAXE=2e5+5,INF=0x3f3f3f3f;
int n,m,src;
int ecnt,edge[MAXE],head[MAXD],nxt[MAXE],wei[MAXE];
int dis[MAXD]; bool vis[MAXD];
deque <int> line; int cnt,sum;
int read();
void insert(int,int,int,int);
int adjust(int);
void spfa();
int main(){
freopen("test.in","r",stdin);
n=read(); m=read(); src=read();
for(int i=1;i<=m;++i){
int tmp1=read(),tmp2=read(),tmp3=read();
insert(tmp1,tmp2,tmp3,++ecnt);
}
spfa();
for(int i=1;i<=n;++i) printf("%d ",dis[i]);
return 0;
}
int read(){
char tmp=getchar(); int sum=0; bool flag=false;
while(tmp<'0'||tmp>'9'){
if(tmp=='-') flag=true;
tmp=getchar();
}
while(tmp>='0'&&tmp<='9'){
sum=(sum<<3)+(sum<<1)+tmp-'0';
tmp=getchar();
}
return flag?-sum:sum;
}
void insert(int from,int to,int w,int id){
nxt[id]=head[from]; head[from]=id; edge[id]=to; wei[id]=w;
}
int adjust(int x){
int limit=(MAXD<<2)+5;
while(x>=limit) x-=limit;
while(x<0) x+=limit;
return x;
}
void spfa(){
for(int i=1;i<=n;++i) dis[i]=INF; dis[src]=0;
line.push_back(src); cnt++; sum+=dis[src];
while(!line.empty()){
int u=line.front(); line.pop_front();
while(dis[u]*cnt>sum){
line.push_back(u);
u=line.front(); line.pop_front();
}
cnt--; sum-=dis[u];
vis[u]=false;
for(int i=head[u];i;i=nxt[i]){
int v=edge[i];
if(dis[v]>dis[u]+wei[i]){
dis[v]=dis[u]+wei[i];
if(!vis[v]){
vis[v]=true;
if(line.empty()||dis[v]<=dis[line.front()]) line.push_front(v);
else line.push_back(v);
cnt++; sum+=dis[v];
}
}
}
}
}
总结
毕竟spfa的复杂度就是O(玄),加上优化的spfa复杂度就进化为O(玄上加玄)
用P4779 【模板】单源最短路径(标准版)进行了一下测试
naiva spfa: https://www.luogu.org/recordnew/show/17571356 (ac两个点)
spfa slf: https://www.luogu.org/recordnew/show/17571542 (ac两个点)
spfa lll: https://www.luogu.org/recordnew/show/17571619 (ac四个点)
spfa slf+lll: https://www.luogu.org/recordnew/show/17571702 (ac一个点?)
使用的算法 | 结果 |
---|---|
spfa | 2个ac+4个wa |
spfa+SLF | 2个ac+4个tle |
spfa+LLL | 4个ac+2个tle |
spfa+SLF+LLL | 1个ac+5个tle |
emm,wtf??
有可能是这道题就是那道卡spfa的毒瘤题的缘故
spfa这么虚,我们用什么
那就用dijkstra吧
当然,如果用裸的的dijkstra,效果还不如spfa,所以需要为他添加堆优化
接下来是 dijkstra,dijkstra+heap
dijkstra
如果未来的我看到这篇博客时发现连裸的dijkstra都不会写了,趁早AFO吧,别丢yuoi的脸(;´д`)ゞ
code:
// a very naive dijkstra
#include <cstdio>
#include <algorithm>
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int MAXD=1e5+5,MAXE=2e5+5,INF=0x3f3f3f3f;
int n,m,src;
int ecnt,edge[MAXE],head[MAXD],nxt[MAXE],wei[MAXE];
int dis[MAXD]; bool vis[MAXD];
int read();
void insert(int,int,int,int);
int adjust(int);
void dijkstra();
int main(){
freopen("test.in","r",stdin);
n=read(); m=read(); src=read();
for(int i=1;i<=m;++i){
int tmp1=read(),tmp2=read(),tmp3=read();
insert(tmp1,tmp2,tmp3,++ecnt);
}
dijkstra();
for(int i=1;i<=n;++i) printf("%d ",dis[i]);
return 0;
}
int read(){
char tmp=getchar(); int sum=0; bool flag=false;
while(tmp<'0'||tmp>'9'){
if(tmp=='-') flag=true;
tmp=getchar();
}
while(tmp>='0'&&tmp<='9'){
sum=(sum<<3)+(sum<<1)+tmp-'0';
tmp=getchar();
}
return flag?-sum:sum;
}
void insert(int from,int to,int w,int id){
nxt[id]=head[from]; head[from]=id; edge[id]=to; wei[id]=w;
}
int adjust(int x){
int limit=(MAXD<<2)+5;
while(x>=limit) x-=limit;
while(x<0) x+=limit;
return x;
}
void dijkstra(){
for(int i=1;i<=n;++i) dis[i]=INF; dis[src]=0;
int cmp,rec;
for(int i=1;i<=n;++i){
cmp=INF;
for(int j=1;j<=n;++j){
if(!vis[j]&&dis[j]<cmp) cmp=dis[j],rec=j;
}
vis[rec]=true;
for(int j=head[rec];j;j=nxt[j]){
int v=edge[j];
dis[v]=min(dis[v],dis[rec]+wei[j]);
}
}
}
dijkstra+heap
原始dijkstra为什么不优秀?
很明显,他确定一个点时需要遍历所有的点,这就非常不优秀。
那怎么改进呢? 用堆!
首先考虑一下裸的dijkstra,我们可以发现,从一个点最多只需要更新一次。即使这个点被多次更改,我们只关心它被改成最小的那一次。这是一个非常重要的性质,在下面代码中的注释中有体现
然后再想一想,当它通过小根堆抽到了这个点时,一定满足“再也无法通过更小的dis去更新到这个点,即这个点已经被松弛到了最小值”,因此,我们只要大胆的抽出堆顶元素,把它设为确定的点即可。
剩下的与dijkstra一毛一样
code:
// the dijkstra with heap
#include<bits/stdc++.h>
#include <cstdio>
#include <algorithm>
#include <iostream>
#include <cstring>
#include <queue>
#include <vector>
using namespace std;
const int MAXD=1e5+5,MAXE=2e5+5,INF=0x3f3f3f3f;
int n,m,src;
int ecnt,edge[MAXE],head[MAXD],nxt[MAXE],wei[MAXE];
int dis[MAXD]; bool vis[MAXD];
int read();
void insert(int,int,int,int);
int adjust(int);
void dijkstra();
struct node{
int u,d;
bool operator < (const node &x)const{
return d>x.d;
}
};
priority_queue <node> line;
int main(){
freopen("test.in","r",stdin);
n=read(); m=read(); src=read();
for(int i=1;i<=m;++i){
int tmp1=read(),tmp2=read(),tmp3=read();
insert(tmp1,tmp2,tmp3,++ecnt);
}
dijkstra();
for(int i=1;i<=n;++i) printf("%d ",dis[i]);
return 0;
}
int read(){
char tmp=getchar(); int sum=0; bool flag=false;
while(tmp<'0'||tmp>'9'){
if(tmp=='-') flag=true;
tmp=getchar();
}
while(tmp>='0'&&tmp<='9'){
sum=(sum<<3)+(sum<<1)+tmp-'0';
tmp=getchar();
}
return flag?-sum:sum;
}
void insert(int from,int to,int w,int id){
nxt[id]=head[from]; head[from]=id; edge[id]=to; wei[id]=w;
}
int adjust(int x){
int limit=(MAXD<<2)+5;
while(x>=limit) x-=limit;
while(x<0) x+=limit;
return x;
}
void dijkstra(){
node tmp;
tmp.u=src; tmp.d=0;
line.push(tmp);
for(int i=1;i<=n;++i) dis[i]=INF; dis[src]=0;
while(!line.empty()){
node tmp=line.top(); line.pop();
int u=tmp.u,d=tmp.d;
if(d!=dis[u]) continue; //attention!
//这里,为了保证同一个点只会被更新一次
for(int i=head[u];i;i=nxt[i]){
int v=edge[i];
if(dis[v]>dis[u]+wei[i]){
dis[v]=dis[u]+wei[i];
node ins; ins.u=v; ins.d=dis[v];
line.push(ins);
}
}
}
}
总结
同样,还是那道题
dijkstra:https://www.luogu.org/recordnew/show/17578913 (爆零!)
dijkstra+heap:https://www.luogu.org/recordnew/show/17580437 (AC!)
使用算法 | 结果 |
---|---|
dijkstra | 6个tle |
dijkstra | 6个ac |
事实上,在某些测试点上,spfa比dijkstra快了近4倍,但是却被其他某些图卡死
后记
总之,“spfa已经死了”是片面的
合理利用spfa,仍然能起到dijkstra无法匹敌的效果(●ˇ∀ˇ●)