德育未来集训笔记-Day 2-SPFA

SPFA 最短路径快速算法

算法简介

SPFA(Shortest Path Faster Algorithm)算法是Bellman-Ford 算法的优化版本,主要用于求解单源最短路径问题,同时具备检测图中负权环的能力。
主要特点是通过队列记录需要进行松弛操作的顶点,避免了 Bellman-Ford 算法中对所有顶点的无意义遍历,仅对可能更新最短路径的顶点进行松弛,大幅提升了平均情况下的运行效率。
SPFA 算法能处理带权有向图和无向图,支持负权边(这是它相比 Dijkstra 算法的核心优势之一),但在最坏情况下时间复杂度与 Bellman-Ford 算法一致,对于存在负权环的图,能准确检测并告知无法求解最短路径(因为负权环可以无限降低路径长度)。

基本思想

其基本思想是基于“松弛操作”“队列优化”,核心思路如下:

  1. 以源点为起点,初始化源点到所有其他顶点的最短路径长度(源点到自身为 0,其余为无穷大 \(∞\))。
  2. 利用队列来维护待进行松弛操作的顶点,初始时将源点入队,并标记该顶点已在队列中(避免重复入队)。
  3. 不断从队列中取出队首顶点 \(u\),对 \(u\) 的所有邻接顶点 \(v\) 执行松弛操作:判断“源点→u→v”的路径长度是否比当前已知的“源点→v”路径长度更短。
  4. 若松弛操作成功(即更新了 \(v\) 的最短路径长度),且 \(v\) 不在队列中,则将 \(v\) 入队,并标记其已在队列中;同时记录 \(v\) 的入队次数(用于检测负权环)。
  5. 重复步骤 3-4,直到队列为空(说明所有可达顶点的最短路径已确定,无负权环)或某个顶点的入队次数超过顶点总数 \(n\)(说明图中存在负权环,无法求解最短路径)。

简单来说,SPFA 算法的本质是“只对有可能更新最短路径的顶点进行松弛操作,通过队列减少无效计算,同时通过入队次数检测负权环”。

操作步骤

步骤 1:初始化基础数据结构

定义并初始化以下核心数据结构(假设顶点编号从 1 开始,\(n\) 为顶点总数,\(st\) 为源点):

  1. 邻接表 \(adj[n+1]\):用于存储图的边信息,相比邻接矩阵更节省空间,适合稀疏图。每个顶点 \(u\) 对应一个列表,存储其所有邻接边(包含邻接顶点 \(v\) 和边权 \(w\))。
  2. 距离数组 \(dist[n+1]\)\(dist[v]\) 表示源点 \(st\) 到顶点 \(v\) 的当前最短路径长度。
    • 初始化规则:\(dist[st] = 0\)(源点到自身距离为 0);其余顶点 \(dist[v] = ∞\)(初始无可达路径)。
  3. 入队标记数组 \(in_queue[n+1]\):用于标记顶点是否已在队列中,避免重复入队,提升效率。初始化所有元素为 false
  4. 入队次数数组 \(cnt[n+1]\):用于记录每个顶点的入队次数,用于检测负权环。初始化所有元素为 0。
  5. 队列 \(q\):用于维护待进行松弛操作的顶点,通常使用普通队列(也可使用双端队列实现 SLF 优化,进一步提升效率)。
  6. 可选前驱数组 \(prev[n+1]\):用于回溯源点到目标顶点的具体最短路径,初始时所有顶点前驱为 -1。

步骤 2:初始化队列与核心循环(队列优化的松弛操作)

  1. 队列初始化:将源点 \(st\) 入队,设置 \(in_queue[st] = true\)(标记已入队),\(cnt[st] = 1\)(源点入队次数为 1)。
  2. 核心循环:当队列不为空时,重复执行以下操作:
    • 取出队首顶点:弹出队列头部的顶点 \(u\),并将 \(in_queue[u] = false\)(取消入队标记,允许后续再次入队)。
    • 遍历邻接顶点:遍历 \(u\) 的所有邻接边 \((u, v, w)\)\(v\) 为邻接顶点,\(w\)\(u\)\(v\) 的边权)。
    • 执行松弛操作:判断是否满足 \(dist[u] + w < dist[v]\),且 \(dist[u] ≠ ∞\)(避免不可达路径的无效更新)。
    • 若满足条件:更新 \(dist[v] = dist[u] + w\),同时更新前驱数组 \(prev[v] = u\)(可选,路径回溯)。
    • \(v\) 未在队列中(\(in_queue[v] = false\)):将 \(v\) 入队,设置 \(in_queue[v] = true\)\(cnt[v] += 1\)(入队次数加 1)。
    • 负权环检测:判断 \(cnt[v] > n\)(顶点入队次数超过顶点总数),若满足则直接返回“存在负权环,无法求解最短路径”,终止算法。
  3. 循环结束:当队列为空且未检测到负权环时,说明所有可达顶点的最短路径已求解完成。

步骤 3:输出结果

  1. 最短路径长度:距离数组 \(dist\) 中存储了源点 \(st\) 到所有顶点的最短路径长度。
    • \(dist[v] = ∞\),表示源点 \(st\) 无法到达顶点 \(v\)
    • 否则,\(dist[v]\) 即为源点 \(st\) 到顶点 \(v\) 的最短路径长度。
  2. 具体路径回溯:若需要获取源点 \(st\) 到目标顶点 \(t\) 的具体路径,可通过前驱数组 \(prev\)\(t\) 倒推回 \(st\),再将路径反转即可得到正序路径。
  3. 负权环说明:若算法过程中检测到负权环,则输出提示信息,说明无法求解最短路径(因为绕负权环循环一周,路径长度会更短,不存在最优解)。

例1

【题目描述】

输入一个图,求出指定源点到所有其他顶点的最短路径长度,若图中存在负权环则输出提示信息。

【输入】

第一行两个整数 \(N\)\(M\)\(st\),分别表示顶点个数、边的条数、源点编号;
接下来 \(M\) 行,每行三个整数 \(u\)\(v\)\(w\),表示存在一条从顶点 \(u\) 到顶点 \(v\) 的有向边,边权为 \(w\)\(w\) 可以为负数)。

【输出】

若图中存在负权环,输出 Exist Negative Cycle
否则,输出共 \(N\) 行,每行格式为 st:v:dist,表示源点 \(st\) 到顶点 \(v\) 的最短路径长度;
若源点无法到达某顶点,输出 st:v:NaN

【输入样例1】(无负权环)

7 10 1
1 2 3
1 3 4
2 3 1
2 4 2
2 6 3
3 4 3
4 5 7
5 7 7
6 7 12
4 7 10

【输出样例1】

1:1:0
1:2:3
1:3:4
1:4:5
1:5:12
1:6:6
1:7:13

【输入样例2】(有负权环)

3 3 1
1 2 2
2 3 -1
3 2 -2

【输出样例2】

Exist Negative Cycle

【提示】

  1. \(1\le N\le 2.5\times10^2\)\(1\le M\le 10^3\),边权 \(w\) 范围为 \([-10^3, 10^3]\)
  2. 顶点编号从 1 开始,数字不超过 C/C++ 的 int 范围。

答案

#include <bits/stdc++.h>
using namespace std;

const int N = 250;       // 最大顶点数
const int M = 1000;      // 最大边数
const int INF = 0x3f3f3f3f; // 表示无穷大(避免溢出)

// 邻接表存储:每个顶点对应一个vector,存储{邻接顶点, 边权}
vector<pair<int, int>> adj[N];
int dist[N];             // 源点到各顶点的最短路径长度
bool in_queue[N];        // 标记顶点是否在队列中
int cnt[N];              // 顶点入队次数(检测负权环)
int n, m, st;            // 顶点数、边数、源点

// SPFA算法核心函数,返回true表示存在负权环,false表示无负权环
bool spfa()
{
    // 1. 初始化数据结构
    memset(dist, 0x3f, sizeof(dist));
    memset(in_queue, false, sizeof(in_queue));
    memset(cnt, 0, sizeof(cnt));
    queue<int> q;
    
    // 2. 源点初始化入队
    dist[st] = 0;
    q.push(st);
    in_queue[st] = true;
    cnt[st] = 1;
    
    // 3. 核心循环:队列优化的松弛操作
    while (!q.empty()) 
	{
        // 取出队首顶点u
        int u = q.front();
        q.pop();
        in_queue[u] = false; // 取消入队标记,允许后续再次入队
        
        // 遍历u的所有邻接顶点
        for (auto &edge : adj[u]) 
		{
            int v = edge.first;  // 邻接顶点
            int w = edge.second; // 边权
            
            // 执行松弛操作
            if (dist[u] != INF && dist[u] + w < dist[v])
			{
                dist[v] = dist[u] + w;
                
                // 若v不在队列中,入队并更新标记和入队次数
                if (!in_queue[v]) 
				{
                    q.push(v);
                    in_queue[v] = true;
                    cnt[v]++;
                    
                    // 检测负权环:入队次数超过顶点总数
                    if (cnt[v] > n) 
					{
                        return true;
                    }
                }
            }
        }
    }
    
    // 队列为空,无负权环
    return false;
}

int main()
{
    // 1. 输入顶点数、边数、源点
    cin >> n >> m >> st;
    
    // 2. 输入边的信息,构建邻接表
    for (int i = 0; i < m; i++)
	{
        int u, v, w;
        cin >> u >> v >> w;
        adj[u].emplace_back(v, w); // 有向边,仅添加u到v的边
    }
    
    // 3. 执行SPFA算法,检测负权环并求解最短路径
    bool has_negative_cycle = spfa();
    
    // 4. 输出结果
    if (has_negative_cycle) 
	{
        cout << "Exist Negative Cycle" << endl;
    }
	else
	{
        for (int v = 1; v <= n; v++) 
		{
            cout << st << ":" << v << ":";
            if (dist[v] == INF)
			{
                cout << "NaN" << endl;
            } 
			else 
			{
                cout << dist[v] << endl;
            }
        }
    }
    
    return 0;
}
posted @ 2026-01-11 14:23  jtbg  阅读(1)  评论(0)    收藏  举报