SPFA算法的四种优化(SLF,LLL,SLF+LLL,DFS)
SPFA算法的四种优化(SLF,LLL,SLF+LLL,DFS)
若读者还不太了解SPFA算法,指路一篇博客:https://blog.csdn.net/hzf0701/article/details/107691013
关于SPFA算法,它已经死了,很多出题人都会故意卡SPFA算法,所以我们若进行优化,说不定就能过呢。但u1s1,对于正权图来说,还是老老实实用Dijkstra算法吧,贼香!
进入正题:我们以vector模拟邻接表存储为例。(哪种都一样,只是存储方式不同而已,主要是算法中心思想)
先介绍基本结构信息:
const int maxn=105;//顶点最大数
const int M=1e4;//边最大数
const int inf=0x3f3f3f3f;
int S,E;//起点与终点。
int n,m;//顶点数与边数。
int cnt[maxn];//cnt[i]记录的是i顶点的入队次数。若大于n,则必然存在负环。
typedef struct Edge{
int to;//边的终端节点
int w;//边权值。
Edge(int to,int w):to(to),w(w){}//构造函数
}Edge;
vector<Edge> graph[maxn];//用vector模拟邻接表
int dis[maxn];//存储所有顶点到源点的距离。
bool visited[maxn];//判断是否在队列中。
void init(){
//初始化函数。
memset(cnt,0,sizeof(cnt));
memset(dis,inf,sizeof(dis));
memset(visited,false,sizeof(visited));
}
基础SPFA算法
int spfa(int S){
queue<int> q;
dis[S]=0;
int temp;
q.push(S);
cnt[S]++;
visited[S]=true; //入队即true。
int flag=0; //标志,若为真,则表示存在负环。
while(!q.empty()){
temp=q.front();
q.pop();
visited[temp]=false; //出队则false。
int v,w;
int t=graph[temp].size();//避免多次调用此函数。
//松弛操作
for(int i=0;i<t;i++){
v=graph[temp][i].to;
w=graph[temp][i].w;
if(dis[v]>dis[temp]+w){
dis[v]=dis[temp]+w;//更新最短路径
if(!visited[v]){
//判断是否在队列中
q.push(v);
cnt[v]++;
if(cnt[v]>n){flag=1;return flag;}
visited[v]=true;
}
}
}
}
return flag;
}
SLF优化
SLF优化,即Small Label First策略,使用STL中的双端队列deque容器来实现,较为常用。
这个顾名思义就是在原有的SPFA算法中每次出队进行判断扩展出的点与队头元素进行判断,若小于进队头,否则入队尾。即:对要加入队列的点 u,如果 dist[u] 小于队头元素 v 的 dist[v],将其插入到队头,否则插入到队尾。
注:队列为空时直接插入队尾。
int spfa_slf(int S){
deque<int> q;
dis[S]=0;
int temp;
q.push_back(S);
cnt[S]++;
visited[S]=true; //入队即true。
int flag=0; //标志,若为真,则表示存在负环。
while(!q.empty()){
temp=q.front();
q.pop_front();
visited[temp]=false; //出队则false。
int v,w;
int t=graph[temp].size();//避免多次调用此函数。
//松弛操作
for(int i=0;i<t;i++){
v=graph[temp][i].to;
w=graph[temp][i].w;
if(dis[v]>dis[temp]+w){
dis[v]=dis[temp]+w;//更新最短路径
if(!visited[v]){
//判断是否在队列中
if(!q.empty()&&dis[v]>dis[q.front()]){
//和队头元素进行比较。
q.push_back(v);
}
else q.push_front(v);
cnt[v]++;
if(cnt[v]>n){flag=1;return flag;}
visited[v]=true;
}
}
}
}
return flag;
}
LLL优化
LLL优化即:Large Label Last策略。顾名思义就是大的放后面,这个不是针对要入队的元素,而是针对要出队的元素,我们是这样子来处理的:设队首元素为 temp ,每次松弛时进行判断,队列中所有 dis 值的和为sum,队列元素为num 。
若 dist[ temp] *num>sum ,则将 temp 取出插入到队尾,查找下一元素,直到找到某一个 temp 使得 dis[ temp ]*sum <= x ,则将 temp出队进行松弛操作。
int spfa_lll(int S){
queue<int> q;
int sum,num;//队列中dis总和和顶点数
dis[S]=0;
int temp;
q.push(S);
sum=S,num=1;
cnt[S]++;
visited[S]=true; //入队即true。
int flag=0; //标志,若为真,则表示存在负环。
while(!q.empty()){
temp=q.front();
while(dis[temp]*num>sum){
//LLL优化关键在这。
q.pop();
q.push(temp);
temp=q.front();
}
q.pop();
//更新队列中dis总和和顶点数目。
num--;
sum-=dis[temp];
visited[temp]=false; //出队则false。
int v,w;
int t=graph[temp].size();//避免多次调用此函数。
//松弛操作
for(int i=0;i<t;i++){
v=graph[temp][i].to;
w=graph[temp][i].w;
if(dis[v]>dis[temp]+w){
dis[v]=dis[temp]+w;//更新最短路径
if(!visited[v]){
//判断是否在队列中
q.push(v);
//更新队列中dis总和和顶点数目。
sum+=dis[v];
num++;
cnt[v]++;
if(cnt[v]>n){flag=1;return flag;}
visited[v]=true;
}
}
}
}
return flag;
}
SLF+LLL优化
我们根据前面得知SLF是对入队顶点进行判断,LLL是对出队顶点判断的,两者互不影响,那么不就可以结合了吗?
int spfa_slf_lll(int S){
deque<int> q;
dis[S]=0;
int temp;
q.push_back(S);
int sum=dis[S];int num=1;
cnt[S]++;
visited[S]=true; //入队即true。
int flag=0; //标志,若为真,则表示存在负环。
while(!q.empty()){
temp=q.front();
while(dis[temp]*num>sum){
//lll优化
q.pop_front();
q.push_back(temp);
temp=q.front();
}
q.pop_front();
sum-=dis[temp];
num--;
visited[temp]=false; //出队则false。
int v,w;
int t=graph[temp].size();//避免多次调用此函数。
//松弛操作
for(int i=0;i<t;i++){
v=graph[temp][i].to;
w=graph[temp][i].w;
if(dis[v]>dis[temp]+w){
dis[v]=dis[temp]+w;//更新最短路径
if(!visited[v]){
//判断是否在队列中
if(!q.empty()&&dis[v]>dis[q.front()]){
//和队头元素进行比较。
q.push_back(v);//大于入队尾
}
else q.push_front(v); //小于入队头
sum+=dis[v];
num++;
cnt[v]++;
if(cnt[v]>n){flag=1;return flag;}
visited[v]=true;
}
}
}
}
return flag;
}
DFS优化
我们知道SPFA算法是利用BFS的思想的,由于采用广度优先的思想,每当我们扩展出一个新的节点,总是把它放到队列的末尾,其缺点是中断了迭代的连续性。而实际上如果采用深度优先的思想,我们可以直接从这个新节点继续往下扩展,则我们就可以不断递归进行求解。我们用DFS的思想来代替BFS,这就是DFS优化。可这样优化真的好吗?这种优化常常用于判断正/负环,时间复杂度可以达到O(m)(m是边)。思路是,我们每一次dfs的时候如果走回之前dfs过的点,那就是有环,除了这个dfs的标记,我们还可以打另一个vis数组记录更新过权值的节点,以后就不必重复更新,大大降低复杂度。如果只是要求最短路径,还是使用前三种优化吧。用DFS优化我们要使用一个访问数组来判断一个点是否再次走过,不同于上面的visited。
我们先要进行初始化,注意:绝对不能放在spfa_dfs函数中。
bool visited[maxn];//判断是否访问过。
dis[S]=0;
memset(visited,false,sizeof(visited));
int flag=0;//标志位,判断是否存在负环。
void spfa_dfs(int temp){
int v,w;
int t=graph[temp].size();
for(int i=0;i<t;i++){
v=graph[temp][i].to;
w=graph[temp][i].w;
if(dis[v]>dis[temp]+w){
if(visited[v]){
//判断是否在一条路径上出现了多次。
flag=true;
return;
}
else{
dis[v]=dis[temp]+w;
visited[v]=true;
spfa_dfs(v);//顺着这条路继续扩展。
}
}
}
}
以上为博主自己整理,自己测试都没有问题的。若有任何疑问都可以私信我或者在评论区留言,我都会给予回复,愿能帮助到你。