单源非负边权最短路径

单源非负边权最短路径

关于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无法匹敌的效果(●ˇ∀ˇ●)

猜你喜欢

转载自www.cnblogs.com/ticmis/p/13210988.html
今日推荐