【SPFA】

【SPFA】

“关于spfa,它死了。”
“卡spfa是作为一个合格的出题人的基本操作。”
模版题
https://www.acwing.com/file_system/file/content/whole/index/content/4379/

思路

对Bellman-Ford的优化:

Bellman_ford算法会遍历所有的边,但是有很多的边遍历了其实没有什么意义
我们只用遍历那些到源点距离变小的点所连接的边即可
只有当一个点的前驱结点更新了,该节点才会得到更新
->创建一个队列:每一次加入距离被更新的结点

queue <- 1
while queue不空
	(1)t <- q.front
	   q.pop();
	(2)更新t的所有出边 t - w -> b
	   queue <- b

代码模版(求最短路)

#include<bits/stdc++.h>
using namespace std;
const int N=150010;
int n,m;
int h[N],ne[N],e[N],idx,val[N];
int dist[N];
bool st[N];
void add(int x,int y,int c){
	e[idx]=y;
	ne[idx]=h[x];
	h[x]=idx;
	val[idx++]=c;
}
int spfa(){
	memset(dist,0x3f,sizeof dist);
	dist[1]=0;
	queue<int> q;
	q.push(1);
	st[1]=true;
	while(q.size()){
		//取队头 
		int t=q.front();
		q.pop();
		st[t]=false;//从队列中取出来之后该节点st被标记为false,代表之后该节点如果发生更新可再次入队
		for(int i=h[t];i!=-1;i=ne[i]){
			int j=e[i];
			if(dist[j]>dist[t]+val[i]){//val值是跟着邻接表的序列存的 
				dist[j]=dist[t]+val[i];
				if(!st[j]){////当前已经加入队列的结点,无需再次加入队列,即便发生了更新也只用更新数值即可,重复添加降低效率
					q.push(j);
					st[j]=true;
				}
			}
		}
	}
	return dist[n];
}
int main(){
	scanf("%d%d",&n,&m);
	memset(h,-1,sizeof h); 
	while(m--){
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		add(x,y,z);
	}
	int ans=spfa();
	if(ans==0x3f3f3f3f) printf("impossible");
	else printf("%d",ans);
	return 0;
}

需要注意的点

(1)Dijkstra算法中的st数组:当前**确定**了到源点距离最小的点
   一旦确定了最小那么就不可逆了(不可标记为true后改变为false)
  
  SPFA算法中的st数组仅仅只是表示的当前发生过更新的点,且spfa中的st数组可逆
  (可以在标记为true之后又标记为false)
  
  BFS中的st数组记录的是当前已经被遍历过的点

(2)Bellman_ford算法里最后return-1的判断条件:dist[n]>0x3f3f3f3f/2
  而spfa算法写的是dist[n]==0x3f3f3f3f
  
  ※原因:Bellman_ford算法会遍历所有的边,因此不管是不是和源点连通的边它都会得到更新
  
  SPFA算法相当于采用了BFS,因此遍历到的结点都是与源点连通的
  因此如果你要求的n和源点不连通,它不会得到更新,还是保持的0x3f3f3f3f
  
(3)Bellman_ford算法可以存在负权回路,是因为其循环的次数是有限制的因此最终不会发生死循环
  
  但SPFA算法不可以,由于用了队列来存储,只要发生了更新就会不断的入队,
  因此假如有负权回路会死循环

SPFA求负环

https://www.acwing.com/file_system/file/content/whole/index/content/4380/

思路

dist[x] 最短距离
cnt[x] 边数

dist[x]=dist[t]+w[i]
cnt[x]=cnt[t]+1

当cnt[x]>=n->已经经过n+1个点->一定存在负环

代码

#include<bits/stdc++.h>
using namespace std;
const int N=150010;
int n,m;
int h[N],ne[N],e[N],idx,val[N];
int dist[N],cnt[N];
bool st[N];
void add(int x,int y,int c){
	e[idx]=y;
	ne[idx]=h[x];
	h[x]=idx;
	val[idx++]=c;
}
bool spfa(){
	queue<int> q;
	//负环不一定和1相连->把所有点都放到队列里
	for(int i=1;i<=n;i++){
		st[i]=true;
		q.push(i);
	}
	while(q.size()){
		//取队头 
		int t=q.front();
		q.pop();
		st[t]=false;//从队列中取出来之后该节点st被标记为false,代表之后该节点如果发生更新可再次入队
		for(int i=h[t];i!=-1;i=ne[i]){
			int j=e[i];
			if(dist[j]>dist[t]+val[i]){//val值是跟着邻接表的序列存的 
				dist[j]=dist[t]+val[i];
				cnt[j]=cnt[t]+1;
				if(cnt[j]>=n) return true;
				if(!st[j]){//当前已经加入队列的结点,无需再次加入队列,即便发生了更新也只用更新数值即可,重复添加降低效率
					q.push(j);
					st[j]=true;
				}
			}
		}
	}
	return false;
}
int main(){
	scanf("%d%d",&n,&m);
	memset(h,-1,sizeof h); 
	while(m--){
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		add(x,y,z);
	}
	if(spfa()) printf("Yes");
	else printf("No");
	return 0;
}

由于SPFA经常被卡 所以产生了如下优化
例题 https://www.acwing.com/problem/content/description/344/

SLF(Small Label First)优化:小的放前面

思路

※每次出队进行判断扩展出的点 与 队头元素进行判断:若小于进队头,否则入队尾
即:对要加入队列的点 u,
如果 dist[u] 小于队头元素 v 的 dist[v],
将其插入到队头,否则插入到队尾
※队列为空时直接插入队尾

代码

#include<bits/stdc++.h>
using namespace std;
const int N=150010;
int n,m;
int h[N],ne[N],e[N],idx,val[N];
int dist[N];
bool st[N];
void add(int x,int y,int c){
	e[idx]=y;
	ne[idx]=h[x];
	h[x]=idx;
	val[idx++]=c;
}
int spfa(){
	memset(dist,0x3f,sizeof dist);
	dist[1]=0;
	deque<int> q;//运用双端队列 
	q.push_back(1);//队列元素为空->直接进队尾 
	st[1]=true;
	while(q.size()){
		int t=q.front();
		q.pop_front();
		st[t]=false;
		for(int i=h[t];i!=-1;i=ne[i]){
			int j=e[i];
			if(dist[j]>dist[t]+val[i]){
				dist[j]=dist[t]+val[i];
				if(!st[j]){
					//一定要判断队列空不空! 
					if(!q.empty() && dist[j]>dist[q.front()]){//和队头元素进行比较
						q.push_back(j);//大于加队尾 
					}
					else q.push_front(j);//小于加队头 
					st[j]=true;
				}
			}
		}
	}
	return dist[n];
}
int main(){
	scanf("%d%d",&n,&m);
	memset(h,-1,sizeof h); 
	while(m--){
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		add(x,y,z);
	}
	int ans=spfa();
	if(ans==0x3f3f3f3f) printf("impossible");
	else printf("%d",ans);
	return 0;
}

好像快一点点也(挠头
image
image

LLL(Large Label Last)优化:大的放后面

貌似是反向优化所以存疑。

思路

针对出队的元素:
设队首元素为 temp ,每次松弛时进行判断:
队列中所有dis值的和为sum,队列元素为num
(1)若 dist[temp]*num>sum,则将 temp 取出,插入到队尾
(2)查找下一元素,直到找到某一个 temp,使得dis[temp]*sum<=x,则将temp出队进行松弛操作

代码

#include<bits/stdc++.h>
using namespace std;
const int N=150010;
int n,m;
int h[N],ne[N],e[N],idx,val[N];
int dist[N];
bool st[N];
void add(int x,int y,int c){
	e[idx]=y;
	ne[idx]=h[x];
	h[x]=idx;
	val[idx++]=c;
}
int spfa(){
	memset(dist,0x3f,sizeof dist);
	dist[1]=0;
	queue<int> q;
	q.push(1);
	int sum=dist[1],num=1;//队列中dist总和 和 顶点数 
	st[1]=true;
	while(q.size()){ 
		int t=q.front();
		//LLL优化关键:大的放后面
		while(dist[t]*num>sum){
			q.pop();
			q.push(t);
			t=q.front();
		}
		q.pop();
		//弹出后更新 
		num--;
		sum-=dist[t];
		st[t]=false;
		for(int i=h[t];i!=-1;i=ne[i]){
			int j=e[i];
			if(dist[j]>dist[t]+val[i]){
				dist[j]=dist[t]+val[i];
				if(!st[j]){
					q.push(j);
					//记得更新 
					sum+=dist[j];
					num++;
					st[j]=true;
				}
			}
		}
	}
	return dist[n];
}
int main(){
	scanf("%d%d",&n,&m);
	memset(h,-1,sizeof h); 
	while(m--){
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		add(x,y,z);
	}
	int ans=spfa();
	if(ans==0x3f3f3f3f) printf("impossible");
	else printf("%d",ans);
	return 0;
}

看起来并没有快多少(沉思
image

SLF+LLL优化

貌似反向优化存疑。

代码

#include<bits/stdc++.h>
using namespace std;
const int N=150010;
int n,m;
int h[N],ne[N],e[N],idx,val[N];
int dist[N];
bool st[N];
void add(int x,int y,int c){
	e[idx]=y;
	ne[idx]=h[x];
	h[x]=idx;
	val[idx++]=c;
}
int spfa(){
	memset(dist,0x3f,sizeof dist);
	dist[1]=0;
	deque<int> q;
	q.push_back(1);
	int sum=dist[1],num=1;//队列中dist总和 和 顶点数 
	st[1]=true;
	while(q.size()){ 
		int t=q.front();
		//LLL优化关键:大的放后面
		while(dist[t]*num>sum){
			q.pop_front();
			q.push_back(t);
			t=q.front();
		}
		q.pop_front();
		//弹出后更新 
		num--;
		sum-=dist[t];
		st[t]=false;
		for(int i=h[t];i!=-1;i=ne[i]){
			int j=e[i];
			if(dist[j]>dist[t]+val[i]){
				dist[j]=dist[t]+val[i];
				if(!st[j]){
					if(!q.empty() && dist[j]>dist[q.front()]) q.push_back(j);
					else q.push_front(j);
					//记得更新 
					sum+=dist[j];
					num++;
					st[j]=true;
				}
			}
		}
	}
	return dist[n];
}
int main(){
	scanf("%d%d",&n,&m);
	memset(h,-1,sizeof h); 
	while(m--){
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		add(x,y,z);
	}
	int ans=spfa();
	if(ans==0x3f3f3f3f) printf("impossible");
	else printf("%d",ans);
	return 0;
}

DFS优化:常用于判环

posted @ 2025-01-09 10:53  White_ink  阅读(32)  评论(0)    收藏  举报