SPFA求单源最短路径

1 篇文章 0 订阅

最短路径的算法有很多,各有优劣。
比如Dijkstra(及其堆(STL-priority_queue)优化),但是无法处理负环的情况;
比如O(n^3)的Floyd算法;比如Bellman-Ford算法,可以处理负环的情况。

SPFA算法就是基于Bellman-Ford算法的改进。
SPFA,全称为Shortest Path Faster Algorithm,也被很多Oler笑称为Super Fast Algorithm.
无可否认的是,SPFA的效率的确很高。


逻辑与思路

SPFA的核心代码很短,只有三十行(但是还有各种初始化)。
乍一看就是一个广度优先搜索。下文的代码是一个指针操作的,进行一定优化的,使用一个不太常见的方法存边写的spfa函数。

初始化

1.将所有点的距离设为INF(memset(0x3f)),及无穷大,将到源点的距离设为(dis[st] = 0);
2.将源点压入队列q;(q.push_back(st));

循环执行直到队列为空

取出队首节点cur,对所有与cur相连的节点进行以下操作:
如果源点到cur的距离与cur到该节点距离的和小于源点到该节点的距离,则更新源点对该节点的距离,并将该节点压入队列;
也就是: if(dis[cur] + edge[cur, i] < dis[i]) update(dis[i]); q.push_back(i);

最终得到的各点距离就是最短路径(如果不连通,则距离为初始值INF)。


原理:

从直觉层面上想,这不难理解。
如果该节点的最短路径被更新(也就是变小),则说明通过该节点到其他节点的路径长度便有可能因此变小。
于是压入队列,等待下一次操作。

优化

用一个双端队列维护。
如果得到新的距离dis[ne]小于位于队首的距离dis[q.front()],则将该节点压入队首,反之则压入队尾。实测效率的确更高。

另一个优化是队列节点进出现一次。
用一个数组wh[I]表示节点i是否在队列中,如果在则只更新距离不压入队列(因为队列里有)。

图的存储方式

大致有三种方式存储。
一是邻接矩阵,不推荐,除了好写一点,又费空间又费时间。
二是前向心,存每条弧的next指向与之节点相连的另一节点。不常写,不做评价。
三是将所有弧存入一个vector(或者数组),记录所有节点与之相连弧的编号。下文的代码实现便是基于这种存储方式。

具体存储方式:
1.读入每一条弧的信息,将它们存入vector中。
2.每次存储时,将该弧的标号(或者指针)存到另一个vector中
例如读入一条弧: edge a -> b, weight(a, b) = c.
存边的结构体存储每条弧的始点,终点与权重(最短路径则不需要存权重)

struct Edge{
    int st, en, weight;
    Edge(){}
    Edge(int s, int e, int w):
        st(s), en(e), weight(w){}
};

那么按照以下操作。(如果是有向图则不需要存回边)

read(a); read(b); read(c);
edge.push_back(Edge(a, b, c));
edge.push_back(Edge(b, a, c));
arc[a].push_back(edge.size()-2);
arc[b].push_back(edge.size()-1);

当然也可选择只存一次边,但是如果是存储网络流,则必须这么存。

那么遍历所有与节点x相连的弧便是这样的。

for(int i = 0, i_end_ = arc[x].size(); i < i_end_; ++i)
{
    int j = arc[x][i];
    Edge& e = edge[j];
    ne = e.en;//e.en就是弧的终点
}

SPFA代码实现

void Spfa()
{
    int q[maxn];
    int *s = q, *t = s + 1, *en = q + n + 1;
    int cur, ne, cur_dis;
    bool wh[maxn] = {0};
    Edge *j;
    memset(dis, 0x3f, sizeof dis);
    dis[st] = 0;
    *s = st;
    while(s != t)
    {
        cur = *s++;
        s = s == en ? q : s;
        wh[cur] = 0;
        REP(i, 0, arc[cur].size())//for(int i = 0; i < arc[cur].size(); ++i)
        {
            j = arc[cur][i];
            ne = j -> en; 
            cur_dis = dis[cur] + j -> weight; 
            if(cur_dis < dis[ne])    
            {
                dis[ne] = cur_dis;
                if(wh[ne])  continue;
                wh[ne] = 1;
                if(dis[ne] < dis[*s])
                {
                    s = s == q ? en - 1 : s - 1;
                    *s = ne;
                }
                else *t++ = ne;
                t = t == en ? q : t;
            }
        }
    }
    return ;
}

以上代码为了提高整体效率,牺牲了一定可读性,基本使用指针操作
但是效率的提高是非常可观的。从800+ms -> 400+ms。

可以在洛谷上交一下板子题。
luogu P3371 【模板】单源最短路径


完整代码实现

/*
About:
From: luogu 3371
Auther: kongse_qi
Date:2017/05/24
*/

#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <vector>
#include <ctype.h>

namespace IO{
    static int in;
    static char *X, *Buffer;
    static char c;
    void Get_All()
    {
        fseek(stdin, 0, SEEK_END);
        long long file_lenth = ftell(stdin);
        rewind(stdin);
        X = Buffer = (char*)malloc(file_lenth);
        fread(Buffer, 1, file_lenth, stdin);
        return ; 
    }

    void Get_Int()
    {
        in = 0;
        while(!isdigit(*X))    ++X;
        while(isdigit(*X))
        {
            in = in * 10 + *(X++) - '0';
        }
        return ;
    }
}//读入优化,注意必须是文件读入

using namespace IO;
using namespace std;

#define read(x) Get_Int(), x = in;
#define REP(i, a, b) for (int i = (a), i##_end_ = b; i < i##_end_; ++i)
#define min(a, b) a > b ? b : a

const int maxn = 10005;
const int INF = 2147483647;
const int maxm = 500005;

struct Edge
{
    int st, en, weight;
    Edge(){}
    Edge(int f, int t, int w):
        st(f), en(t), weight(w){}
};

int n, m, st;
int dis[maxn]; 
Edge edge[maxm], *cur = edge;
vector<Edge*> arc[maxn];
int q[maxn]; 

void Add_Edge(int& st, int& en, int& weight)
{
    *cur = Edge(st, en, weight);
    arc[st].push_back(cur++);
    return ;
} 

void Read()
{
    int a, b, c;
    read(n); read(m); read(st);
    REP(i, 0, m)
    {
        read(a); read(b); read(c);
        if(a != b) 
        {
            Add_Edge(a, b, c);
        }
    }
    return ;
}

void Spfa()
{
    int *s = q, *t = s + 1, *en = q + n + 1;
    int cur, ne, cur_dis;
    bool wh[maxn] = {0};
    Edge *j;
    memset(dis, 0x3f, sizeof dis);
    dis[st] = 0;
    *s = st;
    while(s != t)
    {
        cur = *s++;
        s = s == en ? q : s;
        wh[cur] = 0;
        REP(i, 0, arc[cur].size())
        {
            j = arc[cur][i];
            ne = j -> en; 
            cur_dis = dis[cur] + j -> weight; 
            if(cur_dis < dis[ne])    
            {
                dis[ne] = cur_dis;
                if(wh[ne])  continue;
                wh[ne] = 1;
                if(dis[ne] < dis[*s])
                {
                    s = s == q ? en - 1 : s - 1;
                    *s = ne;
                }
                else *t++ = ne;
                t = t == en ? q : t;
            }
        }
    }
    return ;
}

void Print()
{
    int *p = dis + 1;
    REP(i, 1, n + 1)
    {
        *p = *p == 0x3f3f3f3f ? INF : *p; 
        printf("%d ", *p++);
    }
    return ;
}

int main()
{
    freopen("test.in", "r", stdin);
    Get_All(); 
    Read();
    Spfa();
    Print();
    return 0;
}

至此结束。
箜瑟_qi 10:07 2017.05.25

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值