【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;
}
好像快一点点也(挠头
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;
}
看起来并没有快多少(沉思
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;
}