图论 —— 最短路 —— Bellman-Ford 算法与 SPFA

【概述】

Bellman-Ford算法适用于计算单源最短路径,即:只能计算起点只有一个的情况。

其最大特点是可以处理存在负边权的情况,但无法处理存在负权回路的情况。

其时间复杂度为:O(V*E),其中,V 是顶点数,E 是边数。

【算法分析】

Bellman Ford 算法与 Dijkstra 算法的思想相同,只不过 Dijkstra 是每次确定一个最短距离点,并用这个点去更新与之相连的其他边,而 Ford 算法是每次更新所有的边,从而确定一个点的最短距离

起始时,认为起点是白点(dis[1]=0),每一次都枚举所有的边,必然会有一些边,连接着白点和蓝点。因此每次都能用所有的白点去修改所有的蓝点,每次循环也必然会有至少一个蓝点变成白点。

以下图为例

令起点为白点,即:dis[1]=0,dis[2、3、4、5]=∞

遍历所有边,将与白点相连的蓝点变为白点,即:dis[1]=0,dis[2]=2,dis[3]=1,dis[4]=2,dis[5] =∞

继续向下遍历,修改蓝点为白点,即:dis[1]=0,dis[2]=2,dis[3]=1,dis[4]=2,dis[5] =4

【算法核心】

设起点为 s,dis[v] 表示从 s 到 v 的最短路径,u[i] 和 v[i] 分别表示第 i 条边的起点和终点,w[j] 是连接 u、v 的边 j 的长度。

初始化:

dis[s]=0,dis[v]=0x3f3f3f3f(v≠s),即:初始化为一极大值

算法主体:

void Bellman_Ford()
{
    for(int i=0;i<n;i++) 
        dis[i]=INF;
    dis[0]=0;

    for(int i=1;i<=n-1;i++)//枚举除终点外的所有点
        for(int j=1;j<=m;j++)//枚举所有边
        {
            int x=u[j];//边j的起点
            int y=v[j];//边j的终点
            if(dis[x]<INF)//松弛
                dis[y]=min(dis[y],dis[x]+w[j]);
        }
}

算法结束:dis[v] 即为 s 到 v 最短距离

【SPFA】

SPFA 实质就是 Ford 算法加了判断负环的队列实现版本。

其利用队列以进行 Ford 算法的过程,初始时将起点加入队列,每次从队列中取出一个元素,并对所有与它相邻的点进行修改,若相邻的点修改成功,则将其入队,直到队列为空时算法结束。

SPFA 时间复杂度可达:O(k*E),其中,E 是边数,k 是常数,平均值是 2。

int dis[N];
bool vis[N];
void SPFA(int S) {
    memset(vis, false, sizeof(vis));
    memset(dis, INF, sizeof(dis));
    dis[S] = 0;

    queue<int> Q;
    Q.push(S);

    while (!Q.empty()) {
        int x = Q.front();
        Q.pop();
        vis[x] = false;
        for (int i = head[x]; i != -1; i = edge[i].next) {
            int to = edge[i].to;
            if (dis[to] > dis[x] + edge[i].dis) {
                dis[to] = dis[x] + edge[i].dis;
                if (!vis[to]) {
                    vis[to] = true;
                    Q.push(to);
                }
            }
        }
    }
}

【SPFA 的 SLF 优化】

在 SPFA 的基础上,可以进一步的进行优化,即 SLF 优化

SLF 优化就是 small label first 优化,由于先扩展最小的点可以尽量使程序尽早结束,因此当加入一个新点 u 的时候,如果此时的 dis[u] 比队首的 dis[q.front()] 小的话,就将点 v 加入队首,否则加入队尾,利用 STL 中的双端队列 deque 即可  

struct Edge{
    int to,dis;
};
vector<Edge> edge[N];
bool vis[N];
int dis[N];
void SPFA(int s) {
    memset(dis, INF, sizeof(dis));
    memset(vis, false, sizeof(vis));
    vis[s] = true;
    dis[s] = 0;
    
    deque<int> Q;
    Q.push_back(s);
    while (!Q.empty()) {
        int x = Q.front();
        Q.pop_front();
        vis[x] = 0;
        for (int i = 0; i < edge[x].size(); i++) {
            int y = edge[x][i].to;
            if (dis[y] > dis[x] + edge[x][i].to) {
                dis[y] = dis[x] + edge[x][i].to;
                if (!vis[y]) {
                    vis[y] = true;
                    if (!Q.empty() && dis[y] < dis[Q.front()])//加入队首
                        Q.push_front(y);
                    else//加入队尾
                        Q.push_back(y);
                }
            }
        }
    }
}

【模版】

1.标准版

struct Edge {
    int to, next;
    int dis;
} edge[N];
int head[N], tot;
bool vis[N];
int dis[N];
void addEdge(int x, int y, int dis) {
    edge[++tot].to = y;
    edge[tot].dis = dis;
    edge[tot].next = head[x];
    head[x] = tot;
}
void SPFA(int S) {
    memset(vis, false, sizeof(vis));
    memset(dis, INF, sizeof(dis));
    dis[S] = 0;

    queue<int> Q;
    Q.push(S);

    while (!Q.empty()) {
        int x = Q.front();
        Q.pop();
        vis[x] = false;
        for (int i = head[x]; i != -1; i = edge[i].next) {
            int to = edge[i].to;
            if (dis[to] > dis[x] + edge[i].dis) {
                dis[to] = dis[x] + edge[i].dis;
                if (!vis[to]) {
                    vis[to] = true;
                    Q.push(to);
                }
            }
        }
    }
}
int main(){
    int n,m;
    while(scanf("%d%d",&n,&m)!=EOF){
        memset(head,-1,sizeof(head));
        for(int i=1;i<=m;i++){
            int x,y,dis;
            scanf("%d%d%d",&x,&y,&dis);
            //无向图添边两次
            addEdge(x,y,dis);
            addEdge(y,x,dis);
        }
        int S;
        scanf("%d",&S);
        SPFA(S);
        for(int i=1;i<=n;i++)
            printf("%d\n",dis[i]);
    }    
    return 0;
}

2.带负环判断的SPFA

struct Edge {
    int from, to;
    int dis;
    Edge() {}
    Edge(int from, int to, int dis) : from(from), to(to), dis(dis) {}
};
struct SPFA {
    int n, m;
    Edge edges[N]; //所有的边信息
    int head[N];   //每个节点邻接表的头
    int next[N];   //每个点的下一条边
    int pre[N];    //最短路中的上一条弧
    bool vis[N];
    int dis[N];
    int cnt[N]; //进队次数

    void init(int n) {
        this->n = n;
        this->m = 0;
        memset(head, -1, sizeof(head));
    }

    void AddEdge(int from, int to, int dist) {
        edges[m] = Edge(from, to, dist);
        next[m] = head[from];
        head[from] = m++;
    }

    bool negativeCycle(int s) { //判负环
        memset(vis, false, sizeof(vis));
        memset(cnt, 0, sizeof(cnt));
        memset(dis, INF, sizeof(dis));
        dis[s] = 0;

        queue<int> Q;
        Q.push(s);

        while (!Q.empty()) {
            int x = Q.front();
            Q.pop();
            vis[x] = false;
            for (int i = head[x]; i != -1; i = next[i]) {
                Edge &e = edges[i];
                if (dis[e.to] > dis[x] + e.dis) {
                    dis[e.to] = dis[x] + e.dis;
                    pre[e.to] = i;
                    if (!vis[e.to]) {
                        vis[e.to] = true;
                        Q.push(e.to);
                        if (++cnt[e.to] > n)
                            return true;
                    }
                }
            }
        }
        return false;
    }
} spfa;
int main() {
    int n, m;
    while (scanf("%d%d", &n, &m) != EOF) {
        spfa.init(n);
        int S;
        scanf("%d", &S);
        for (int i = 1; i <= m; i++) {
            int x, y, dis;
            scanf("%d%d%d", &x, &y, &dis);
            //无向边添边两次
            spfa.AddEdge(x, y, dis);
            spfa.AddEdge(y, x, dis);
        }
        spfa.negativeCycle(S);
        for (int i = 1; i <= n; i++)
            printf("%d\n", spfa.dis[i]);
    }
    return 0;
}

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值