目录
SPFA
SPFA 算法通常用于求含负权边的单源最短路径,以及判负权环。在正权图上应使用效率更高的Dijkstra算法。若给定的图存在负权边,类似Dijkstra等算法便没有了用武之地
原理
我们约定加权有向图G不存在负权回路,即最短路径一定存在。用数组d记录每个结点的最短路径估计值,而且用邻接表(或者前向星存图)来存储图G。我们采取的方法是动态逼近法:设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。
代码实现:
//------邻接表------------(可为前向星存图)
struct edge
{
int to;//终点
int w;//权值
edge *next;//指向下一条边的指针
};
edge *p[maxn];//每条边的头结点.初始化NULL
//-------------------------
int n,m,s;
int vis[maxn];//是否在队列中
int dis[maxn];
void SPFA(int s)
{
const int INF = 0x3f3f3f3f;
for(int i=0;i<=n;i++)
{
vis[i]=0;
dis[i]=INF;
}//初始化
dis[s]=0;
vis[s]=1;
queue<int>q;//创建队列
q.push(s);
while(!q.empty())
{
int from = q.front();
q.pop();
vis[from]=0;
edge *t=p[from];
while(t!=NULL)
{
int to=t->to;//终点
int w=t->w;
if(w+dis[from]<dis[to])
{
dis[to]=w+dis[from];//更新
if(!vis[to]){
vis[to]=1;
q.push(to);//入队
}
}
t=t->next;
}
}
for(int i=1;i<=n;i++)
cout<<dis[i]<<' ';//最短路
}
优化
同Dijkstra一样,SPFA一样可以采用堆优化的方式,虽然看起来堆优化后的SPFA真是越来越像 djikstra 了 QWQ~~
就不贴代码了
判断负环
如果图中存在负环,那么在求最短路径时队列永远不会空,每次都会更新那个负环上的边。
方法一
在松弛过程中记录起点到每个点的当前最短路径上的边的条数t,如果不存在负环,则t<=n-1,所以如果t>=n,则一定存在负环
代码
int cnt=1;
int n,m,s;
int vis[maxn];//是否在队列中
int dis[maxn];
//-------前向星存图-------
struct edge
{
int next;
int to;
int w;
}p[maxm];
int h[maxn];
void add(int a,int b,int w)
{
p[cnt].next=h[a];
p[cnt].to=b;
p[cnt].w=w;
h[a]=cnt;
cnt++;
}
//----------------------
bool SPFA(int s)
{
int t[maxn]={0};
for(int i=0;i<=n;i++)
{
vis[i]=0;
dis[i]=INF;
}
dis[s]=0;
vis[s]=1;
queue<int>q;
q.push(s);
while(!q.empty())
{
int from = q.front();
q.pop();
vis[from]=0;
for(int i=h[from];i;i=p[i].next)
{
int to=p[i].to;
int w=p[i].w;
if(dis[to]>dis[from]+w)
{
//---------------------关键------------------------
t[to]=t[from]+1;
if(t[to]==n)
return false;//存在负环
//-------------------------------------------------
dis[to]=dis[from]+w;
if(!vis[to])
{
vis[to]=1;
q.push(to);//入队
}
}
}
}
return true;//不存在负环
}
方法二
如果图中不存在负环,那么在SPFA过程中,每个点进入队列的次数ans<=n的,反之,必定存在负环
但是:这种方法比第一种方法慢,方法一只需绕环一次就能够判断,而这个方法需要绕n次环,不推荐使用
代码
int cnt=1;
int n,m,s;
int vis[maxn];//是否在队列中
int dis[maxn];
//-------前向星存图-------
struct edge
{
int next;
int to;
int w;
}p[maxm];
int h[maxn];
void add(int a,int b,int w)
{
p[cnt].next=h[a];
p[cnt].to=b;
p[cnt].w=w;
h[a]=cnt;
cnt++;
}
//----------------------
bool SPFA(int s)
{
int t[maxn]={0};
for(int i=0;i<=n;i++)
{
vis[i]=0;
dis[i]=INF;
}
dis[s]=0;
vis[s]=1;
queue<int>q;
q.push(s);
while(!q.empty())
{
int from = q.front();
q.pop();
vis[from]=0;
for(int i=h[from];i;i=p[i].next)
{
int to=p[i].to;
int w=p[i].w;
if(dis[to]>dis[from]+w)
{
dis[to]=dis[from]+w;
if(!vis[to])
{
//----------关键代码------
t[to]++;
if(t[to]>n)
return false;//存在负环
//---------------------
vis[to]=1;
q.push(to);//入队
}
}
}
}
return true;
}
方法三
用dfs来判断,在下面说
SPFA与BFS
当一张图从加权图变为无权图,显然SPFA算法就成为了一个BFS操作,只是除去了松弛操作。而在加权图中,因为每条边的权重并不相同,所以在BFS过程中,第一次到达的点的路径并不一定是最段路径,只得进行松弛操作,直到没有路径可以松弛
SPFA与DFS
显然上边的SPFA 是基于BFS的。
而我们知道,每一个BFS基本都可以变为DFS。相当于是把"先进先出"的队列换成了"先进后出"的栈
也就是说,每次都以刚刚松弛过的点来松弛其他的点。
在判断负环方面:如果能够松弛点 x 并且 x 还在栈中,那图中就有负环。一般来说的话,若存在负环,那么 dfs 会比 bfs 快
但是如果不存在负环,dfs 可能会严重影响求最短路的效率,要谨慎使用
代码
bool flag;
void SPFA(int s)
{
vis[s]=1;
for(int i=h[s];i;i=p[i].next)
{
int w=p[i].w;
int to=p[i].to;
if(w+dis[s]<dis[to])
{
dis[to]=w+dis[s];
if(vis[to])
{
flag=false;//存在负环
return;
}
SPFA(to);
}
}
vis[s]=0;
}
//flag初始化true dis[i]=INF dis[start]=0;