图论学习笔记(4):Bellman-ford算法和SPFA算法

声明:这里简单聊聊我们Bellman-ford算法的思路,我也查了一些资料来进行辅助了解,我们主要掌握SPFA算法的思现,因为我们Bellman-ford算法的时间复杂度是稳定的O(VE)(其中V是顶点个数,E是边的个数),在大多数算法题目里这个时间复杂度已经很大了(打XCPC应该O(n^2)左右几乎都会卡)。而我们的SPFA算法平均情况下的时间复杂度是O(kE)(k是一个小于2的数),所以在大多数情况下SPFA算法都是比较友好的(针对有负权边的情况),不过少数情况出题人可能会造卡SPFA算法的数据,因为我们SPFA算法最差时间复杂度和Bellman-ford是一样的,在O(VE)。所以本章节内容主要学习掌握SPFA算法的思想和具体实现。

一、Bellman-ford算法

为什么要引入一个新的最短路径算法?我们在前面内容中提到过,Dijkstra主要用于实现没有负权边的算法,所以我们需要一个能够解决含有负权边问题的算法,就是Bellman-ford算法了,这里简单介绍一下Bellman-ford算法的思想。在此之前,我们介绍几个概念:

vector v.assign( n , inf ):(介绍的原因是我不会),这个是对vector变长向量里的数据进行分发,可以理解为fill(v,v+n,inf)。就是分配n个inf给v。

负权环(负权回路):负权环就是在图中,存在一个环,这个环所有的边的权值和为负数。(我们可以很容易想到,在求最短路径的时候,如果起点经过一个负权环,再到终点,这种情况下是不存在最短路径的,因为我们起点可以经过无限个负权环,使得它从起点到终点的权值无限小。所以在解决带负权边问题时,如果有负权环,那么这个题目是无解的)。

Bellman-ford算法的思想(AI思路):

初始化:将除源点外的所有顶点的最短距离估计值设为正无穷,源点的最短距离设为 0。

迭代求解:对边集进行 | V|-1 次松弛操作(|V | 为顶点数)。每次松弛操作中,遍历每条边,若通过该边能使目标顶点的最短距离变小,则更新目标顶点的最短距离。

检验负权回路:再进行一次边集遍历,若仍能对边进行松弛操作,说明图中存在负权回路,返回 false;否则返回 true,此时得到的即为各顶点到源点的最短距离

代码一:(AI生成版本)

#include 
#include 
using namespace std;

struct Edge {
    int u, v, w;
    Edge(int a, int b, int c) : u(a), v(b), w(c) {}
};

bool bellmanFord(int n, vector& edges, int s, vector& dist) {
    dist.assign(n, INT_MAX);
    dist[s] = 0;
    for (int i = 0; i < n - 1; ++i) {
        for (const auto& edge : edges) {
            int u = edge.u, v = edge.v, w = edge.w;
            if (dist[u] != INT_MAX && dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
            }
        }
    }

    // 检查负权回路
    for (const auto& edge : edges) {
        int u = edge.u, v = edge.v, w = edge.w;
        if (dist[u] != INT_MAX && dist[u] + w < dist[v]) {
            return false;
        }
    }
    return true;
}

int main() {
    int n = 5;  // 顶点数
    vector edges = {
        Edge(0, 1, -1),
        Edge(0, 2, 4),
        Edge(1, 2, 3),
        Edge(1, 3, 2),
        Edge(1, 4, 2),
        Edge(3, 2, 5),
        Edge(3, 1, 1),
        Edge(4, 3, -3)
    };
    vector dist;
    int source = 0;
    if (bellmanFord(n, edges, source, dist)) {
        for (int i = 0; i < n; ++i) {
            cout << "源点到顶点 " << i << " 的最短距离: " << dist[i] << endl;
        }
    } else {
        cout << "图中存在负权回路" << endl;
    }
    return 0;
}

Bellman-ford算法的思想(个人总结):Bellman-ford算法的实现思路其实是非常简单的。初始时,我们需要构造一个距离数组d[maxn] 和 图结构体 vector g[maxn](用来存一条边的起点和终点和这个边的边权)。初始时我们令距离数组全为inf。令d[s]=0。

接着我们开始进行n-1次(n是顶点数目)松弛操作。每次松弛操作遍历所有的边,如果加入这条边能够使起点到该顶点更近,那么更新距离数组。

进行完n-1次松弛操作后,再进行一个边的遍历,检查距离数组还能不能被更新,如果不能被更新了,说明图中没有负权环。否则,说明图中有负权环,无法求解。

代码二:(纯手敲模板)

#include
using namespace std;
#define endl '\n'
#define int long long
#define pii pair
#define f first
#define s second
#define inf 0x3f3f3f3f

int n,m,s,t;
vector g[1010];//构造图
int d[1010];

bool bellman_ford(int s){
    fill(d,d+n,inf);
    d[s]=0;
    for(int k=1;kd[i]+di){
               d[ti]=d[i]+di; //如果起点从顶点i到ti更近,更新d[ti]
           }
       }
      }
    }

    //检测有没有负权环
    for(int i=0;id[i]+di){
               return false;//说明有负权环
           }
       }
      }
      return true;//没有负权环
}

signed main(){
    cin>>n>>m>>s>>t;
    while(m--){
        int a,b,w;
        cin>>a>>b>>w;
        g[a].push_back({b,w});
        g[b].push_back({a,w});
    }
    bool flag=bellman_ford(s);
    if(flag)//说明有数据
    if(d[t]==inf) cout<<"-1";
    else cout<

需要注意的是,bellman-ford算法如果要统计路径条数会比较麻烦!

二、SPFA算法

同Dijkstra的堆优化算法类似,SPFA是bellman-ford的队列优化算法。,通过减少不必要的冗余计算,来提高算法效率。

SPFA算法思想(AI思路):使用一个队列来维护待处理的顶点。初始时将源点加入队列,然后不断从队列中取出顶点,对其相邻顶点进行松弛操作。如果某个相邻顶点的最短距离因松弛而更新,且该顶点不在队列中,则将其加入队列。重复此过程,直到队列为空。如果一个顶点入队次数超过顶点数,说明图中存在负权回路。

平均情况:在平均情况下,SPFA 算法的时间复杂度为 O (kE),k 是一个较小的常数,一般 k 不超过 2。这是因为它避免了 Bellman-Ford 算法中大量不必要的松弛操作,从而提高了运行效率。

最坏情况:在最坏情况下,例如遇到刻意构造的特殊数据(被称为 “卡 SPFA” 的数据),图中每个顶点都频繁入队和出队,SPFA 算法的时间复杂度会退化为 O (VE) ,与 Bellman-Ford 算法相同。

代码一:AI生成代码模板:

#include 
#include 
#include 
#include 
using namespace std;

struct Edge {
    int to, weight;
    Edge(int t, int w) : to(t), weight(w) {}
};

vector> graph;

bool spfa(int n, int s, vector& dist) {
    vector inQueue(n, false);
    vector count(n, 0);
    dist.assign(n, INT_MAX);
    dist[s] = 0;
    queue q;
    q.push(s);
    inQueue[s] = true;
    count[s]++;

    while (!q.empty()) {
        int u = q.front();
        q.pop();
        inQueue[u] = false;
        for (const auto& edge : graph[u]) {
            int v = edge.to;
            int w = edge.weight;
            if (dist[u] != INT_MAX && dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                if (!inQueue[v]) {
                    q.push(v);
                    inQueue[v] = true;
                    if (++count[v] > n) {
                        return false;
                    }
                }
            }
        }
    }
    return true;
}

int main() {
    int n = 5;  // 顶点数
    graph.resize(n);
    graph[0].push_back(Edge(1, -1));
    graph[0].push_back(Edge(2, 4));
    graph[1].push_back(Edge(2, 3));
    graph[1].push_back(Edge(3, 2));
    graph[1].push_back(Edge(4, 2));
    graph[3].push_back(Edge(2, 5));
    graph[3].push_back(Edge(1, 1));
    graph[4].push_back(Edge(3, -3));

    vector dist;
    int source = 0;
    if (spfa(n, source, dist)) {
        for (int i = 0; i < n; ++i) {
            cout << "源点到顶点 " << i << " 的最短距离: " << dist[i] << endl;
        }
    } else {
        cout << "图中存在负权回路" << endl;
    }
    return 0;
}

SPFA算法思想(个人总结):需要距离数组d[manx]和构造图vector g[1010];此外需要bool数组b[maxn]来记录这个顶点有没有加入队列中,还有一个计数数组num[maxn]用来记录顶点加入队列次数,如果存在一个顶点加入队列的次数大于n次(超过顶点数),说明图中一定有负权环。

SPFA算法初始时,将顶点s加入队列中,并给b[s]=true赋值为真,num[s]=1,表示顶点放入队列,且放入次数为1。然后while(!q.empty){.......},当队列非空时,一直进行循环操作。在一个循环中,将队列中的顶点取出,遍历这个顶点的所有边,如果这个顶点存在一条边,使得起点到另一个顶点的距离更近,更新这条边。 如果这个顶点不在队列中(if( !b[i] )),就把这个顶点加入队列中,令该顶点的布尔数组对应值为真,且该顶点加入队列次数num[i]++。SPFA算法结束条件时,如果num数组中存在有顶点加入队列次数超过n次,说明存在负权环,返回false。否则当队列为空时,得到最短路径,返回true。

代码二:纯手敲代码模板:

#include
using namespace std;
#define endl '\n'
#define int long long
#define pii pair
#define f first
#define s second
#define inf 0x3f3f3f3f


int n,m,s,t;
vector g[1010];//构造图
int d[1010];
bool b[1010];
int num[1010];

bool spfa(int s){
    fill(d,d+n,inf);
    d[s]=0;
    queue q;
    q.push(s);
    num[s]++;
    b[s]=true;
    while(!q.empty()){
        int si=q.front();
        b[si]=false;
        q.pop();
        for(int i=0;id[si]+di){  //ti才是被更新的顶点
                d[ti]=d[si]+di;
                if(!b[ti]){
                    q.push(ti);
                    b[ti]=true;
                    num[ti]++;
                    if(num[ti]>n) return false;//有负权环
                }
            }
        }
    }
    return true;
}


signed main(){
    cin>>n>>m>>s>>t;
    while(m--){
        int a,b,w;
        cin>>a>>b>>w;
        g[a].push_back({b,w});
        g[b].push_back({a,w});
    }
    bool flag=spfa(s);
    if(flag)//说明有数据
    if(d[t]==inf) cout<<"-1";
    else cout<

写在最后:上述个人总结模板,主要根据题目 最短路径 来进行编写。

你可能感兴趣的:(算法,数据库,SPFA,Bellman-ford)