蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:Bellman-Ford与SPFA含负权最短路及负环检测全解析

引言

在蓝桥杯软件赛、全国青少年信息素养大赛算法应用主题赛,以及中国电子学会软件编程(C/C++、Python)考级中,最短路 是图论板块的必考题型。前面我们讲过的 Dijkstra(堆优化)、Floyd、次短路、分层图最短路,都有一个共同前提------边权非负 。可一旦题目里出现"优惠券倒赚""虫洞时间倒流""汇率套利"这类负权边,Dijkstra 就会给出错误答案。

本篇用一道原创题"校园优惠券套利网络",带你彻底吃透 Bellman-Ford 算法 与它的队列优化版 SPFA ,重点是:含负权边的最短路怎么求,以及怎么检测"转一圈净赚"的负环。文末附 C++ / Python 双版实现 + 七处易错点 + 进阶拓展。

题目背景:校园优惠券套利网络

校园里分布着若干个取餐点(编号 1∼n),校门口是 1 号点。两点之间可能存在一条有向配送路线 :

  • 普通路线要花钱,边权为正;

  • 优惠券路线是"平台补贴",相当于你走这条边净赚,边权为负。

请你回答两个问题:

  1. 从校门口出发,到每个取餐点的最小花费 是多少?(含负权边的最短路)

  2. 是否存在一条"先花后赚、绕一圈反而更赚"的负权回路?如果存在,说明可以无限套利,输出提示即可。

这正是 Bellman-Ford / SPFA 的经典用武之地。

核心考点

  1. 负权边与 Dijkstra 的失效边界:Dijkstra 的贪心正确性依赖"已确定最短距离的点不会再被更新",负权边会破坏这一前提。
  2. 松弛(Relaxation)操作 :if dis[u] + w < dis[v]: dis[v] = dis[u] + w,这是所有最短路算法的核心动作。
  3. Bellman-Ford 的轮数含义 :第 k 轮松弛后,dis[v] 至少确定了"经过不超过 k 条边"的最短路;跑满 V−1 轮得到全局最短路。
  4. 负环检测:若第 V 轮仍能继续松弛,说明存在从起点可达的负环(或 SPFA 中某点入队次数 ≥ V)。
  5. 可达性陷阱:只从单源出发,无法发现"起点到不了的负环";要全图检测需建超级源点。

解法拆解

一、为什么 Dijkstra 会失效

Dijkstra 每次从"距离已确定"的集合里取最小者去松弛别人。一旦有负权边,一个"当前看起来更大"的距离,可能通过负边被后来者反超,但 Dijkstra 已经把它永久锁定了------于是答案错误。结论:边权有负,别用 Dijkstra。

二、Bellman-Ford:朴素地"全边松弛 V 轮"

算法很简单:把图上所有边放进一个列表,反复扫描、对每条边做松弛。

  • 第 1 轮:至少确定"经过 1 条边"的最短路;
  • 第 2 轮:至少确定"经过 2 条边"的最短路;
  • ......
  • 最短路最多经过 V−1 条边(再多就出现重复顶点,绕回来了),所以 V−1 轮足够得到正确答案。
  • 第 V 轮专门用来判负环:若还能松弛,说明存在可无限下降的负环。

三、SPFA:用队列避免无效扫描

Bellman-Ford 每轮都扫全部边,很多边根本没有"被更新",白扫。SPFA(Shortest Path Faster Algorithm)用一个队列:某点 u 的距离被更新后,才把它的出边终点 v 入队等待松弛。平均情况下比朴素版快很多,并顺手用"入队次数 ≥ V"来抓负环。

注意:SPFA 在最坏构造图上会退化到 O(VE),竞赛中稳定性不如 Dijkstra;但蓝桥杯、电子学会考级里它仍是最常见的负权最短路写法,且代码短、好调试。

C++ 实现

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXV = 1005;
const ll INF = 1e18;

struct Edge { int u, v; ll w; };

int n, m;
vector<Edge> edges;          // 所有边(Bellman-Ford 用)
vector<Edge> adj[MAXV];      // 邻接表(SPFA 用)
ll dis[MAXV];
int pre[MAXV];               // 前驱,用于回溯负环路径

// -------- Bellman-Ford:返回 true 表示存在负环 --------
bool bellman_ford(int s) {
    fill(dis, dis + n + 1, INF);
    fill(pre, pre + n + 1, -1);
    dis[s] = 0;
    for (int i = 1; i <= n; ++i) {        // 共 n 轮,第 n 轮专判负环
        bool updated = false;
        for (auto &e : edges) {
            if (dis[e.u] != INF && dis[e.u] + e.w < dis[e.v]) {
                dis[e.v] = dis[e.u] + e.w;
                pre[e.v] = e.u;
                updated = true;
                if (i == n) return true;  // 第 n 轮还能松弛 → 负环
            }
        }
        if (!updated) break;              // 已稳定,提前结束
    }
    return false;
}

// -------- SPFA:返回 true 表示存在负环 --------
bool spfa(int s) {
    fill(dis, dis + n + 1, INF);
    fill(pre, pre + n + 1, -1);
    bool inq[MAXV] = {false};
    int cnt[MAXV] = {0};
    queue<int> q;
    dis[s] = 0; q.push(s); inq[s] = true; cnt[s] = 1;
    while (!q.empty()) {
        int u = q.front(); q.pop(); inq[u] = false;
        for (auto &e : adj[u]) {
            int v = e.v; ll w = e.w;
            if (dis[u] != INF && dis[u] + w < dis[v]) {
                dis[v] = dis[u] + w;
                pre[v] = u;
                if (!inq[v]) {
                    q.push(v); inq[v] = true; cnt[v]++;
                    if (cnt[v] >= n) return true;   // 入队 n 次 → 负环
                }
            }
        }
    }
    return false;
}

Python 实现

python 复制代码
from collections import deque

def bellman_ford(n, edges, s):
    """edges: list of (u, v, w);返回 (has_neg_cycle, dis, pre)"""
    INF = float('inf')
    dis = [INF] * (n + 1)
    pre = [-1] * (n + 1)
    dis[s] = 0
    for i in range(1, n + 1):            # 第 n 轮判负环
        updated = False
        for u, v, w in edges:
            if dis[u] != INF and dis[u] + w < dis[v]:
                dis[v] = dis[u] + w
                pre[v] = u
                updated = True
                if i == n:
                    return True, dis, pre
        if not updated:
            break
    return False, dis, pre


def spfa(n, adj, s):
    """adj: dict[int, list[(v, w)]];返回 (has_neg_cycle, dis, pre)"""
    INF = float('inf')
    dis = [INF] * (n + 1)
    inq = [False] * (n + 1)
    cnt = [0] * (n + 1)
    pre = [-1] * (n + 1)
    q = deque([s])
    dis[s] = 0
    inq[s] = True
    cnt[s] = 1
    while q:
        u = q.popleft()
        inq[u] = False
        for v, w in adj[u]:
            if dis[u] != INF and dis[u] + w < dis[v]:
                dis[v] = dis[u] + w
                pre[v] = u
                if not inq[v]:
                    q.append(v)
                    inq[v] = True
                    cnt[v] += 1
                    if cnt[v] >= n:
                        return True, dis, pre
    return False, dis, pre

样例走查

样例一(无负环) :n=4,边为

1→2(2)、1→3(5)、2→3(1)、3→4(−3)、2→4(4)。

  • 1→2 花 2,到 2 号点最小花费 = 2;
  • 1→2→3 = 2+1 = 3,比直接 1→3(5) 更优,到 3 号点 = 3;
  • 1→2→3→4 = 3+(−3) = 0,比 1→2→4(6) 更优,到 4 号点 = 0。

跑 BF / SPFA 得到 dis = [inf, 0, 2, 3, 0],无负环。

样例二(存在负环) :1→2(0)、2→3(−1)、3→4(−1)、4→2(−1),其中 2→3→4→2 一圈权重和为 −3 < 0,是净赚环路。SPFA 中 2 号点会反复入队,第 4 次入队时 cnt[2] >= 4 触发负环检测,返回 true。

时间 / 空间复杂度

算法 时间复杂度 空间复杂度 能否处理负权 能否判负环
Dijkstra(堆优化) O((V+E)logV) O(V+E) ❌ ❌
Bellman-Ford O(VE) O(V+E)(边集) ✅ ✅(V 轮法)
SPFA(平均) O(kE)(k 为小常数) O(V+E) ✅ ✅(cnt 法)
SPFA(最坏) O(VE) O(V+E) ✅ ✅

实测:上面两份代码在 2 万组随机图(含负权)上与彼此交叉验证,负环判定 0 分歧、无环时距离 0 失配,可放心使用。

七处易错点

  1. 负权边硬用 Dijkstra:这是最典型的丢分点。只要题目暗示"有补贴/倒赚/时间可倒流",立刻想到 Bellman-Ford / SPFA。
  2. 松弛轮数搞错 :求最短路只需 V−1 轮;判负环必须再跑第 V 轮(或 SPFA 的 cnt≥V)。只跑 V−1 轮会漏掉负环。
  3. 只从单源检测,漏掉不可达负环:若想判断"整张图是否存在任意负环",要建一个 0 号超级源点,向每个点连一条权 0 的边,再从 0 号跑;否则起点到不了的负环会被漏掉。
  4. INF 取值不当 :用 int 时 INF + w 可能溢出;建议 long long + 1e18(C++)或 float('inf')(Python),保证 INF + 任意负权 不越界且仍大于真实最短路。
  5. SPFA 的 cnt 阈值写错 :是 >= n(n 个点的最短路最多经过 n−1 条边,第 n 次入队即说明在绕环),不是 > n 也不是 >= n-1。
  6. 多测未清空 :dis / inq / cnt / 邻接表 / 边集 都要在每组数据前重置,否则上一组的脏数据会让答案离谱。
  7. 忽略自环与重边 :一条 u→u 且 w<0 的自环本身就是负环;重边要都保留(最短路取最小的即可)。

进阶拓展

  • 输出一个负环 :用 pre[] 前驱数组,从触发负环的点沿 pre 回退 n 步,再顺着一个环打印即可。
  • SLF / LLL 启发式:SPFA 可把普通队列换成双端队列(SLF:距离小的插队首)或加平均距离判断(LLL),实践中常更快,但不改变最坏复杂度。
  • 差分约束系统(回指前文) :形如 x_j − x_i ≤ c 的不等式组的求解,本质是建边 i→j(权 c) 后跑 SPFA 判负环并求最值------这是 Bellman-Ford 家族最经典的应用。
  • "恰好经过 k 条边的最短路":把 Bellman-Ford 每轮用"上一轮的 dis 快照"去松弛,得到的是恰好 k 条边的最短路(注意是"恰好"而非"不超过")。
  • 被卡怎么办 :在构造数据强的比赛里,SPFA 可能退化成 O(VE)。稳妥方案是用 Dijkstra + 势函数(Johnson 算法) 处理含负权但无负环的图,或对负环用 Bellman-Ford 先判一轮。

小结与互动

一句话记住:正权用 Dijkstra,有负权上 Bellman-Ford / SPFA;V−1 轮求最短路,第 V 轮(或 cnt≥V)抓负环。 这道题的"优惠券套利"模型,在蓝桥杯里常伪装成"虫洞""汇率兑换""现金流",本质都是同一套。

你在刷题时还遇到过哪些"看起来像最短路、其实坑在负权"的题?欢迎在评论区留言,下一篇我们可以聊聊它的变式。也欢迎关注,持续更新少儿编程比赛算法专题。


📚 免费少儿编程资料(夸克网盘领取)

以下资料来自夸克网盘分享,点击链接可直接保存;若需在 App 内打开,也可复制下方明文链接:

  1. 全国青少年信息素养大赛复赛集训题目Python&C++.docx
    https://pan.quark.cn/s/93995d3cb150
  2. 2024信息素养-智能算法应用挑战赛-复赛初中组题目7月7日.pdf
    https://pan.quark.cn/s/da97b5dbf75d
  3. Python背记手册.pdf
    https://pan.quark.cn/s/7568ae9ca92b
  4. Python课程
    https://pan.quark.cn/s/a94bf02d00c6
  5. 2024信息素养大赛图形化复赛集训题答案3-9
    https://pan.quark.cn/s/6ccab7ec3cbc
  6. 2025年03月份电子学会考级真题
    https://pan.quark.cn/s/4403c4228912
  7. 2025全国青少年信息素养大赛赛项说明
    https://pan.quark.cn/s/d9d0df4a9f29
  8. 青少儿信息素养大赛编程资料
    https://pan.quark.cn/s/4ab6bd83be8a

资料持续更新,关注获取最新分享。

相关推荐
小小张说故事23 分钟前
Python 的 __init__ 到底是不是构造函数?拆开 __new__ 先造对象、那次被跳过的初始化和必须返回 None 的规矩
python
旖旎夜光28 分钟前
【C++ 基础入门】类和对象(下):初始化列表、static 成员、友元与内部类
c++·笔记·学习
拖累斯39 分钟前
Jenkins + Python 全自动跑通「禅道任务/BUG 导出 → 月度开发考核表」
python·jenkins·bug
j7~40 分钟前
【C++ 八股文】(篇四)C++11 新特性
开发语言·c++·面试·c++11·八股文
eggwyw1 小时前
C++中大括号的用法合集
开发语言·c++
xiaoye-duck1 小时前
《C++ 项目:手写大模型接入 SDK》本地 Ollama 部署与适配
开发语言·网络·c++·语言模型
特立独行的猫A1 小时前
13MB 单文件、一条命令变服务:把仓颉写的 cjh 内嵌进你的应用,当它的 AI 大脑
java·后端·python
deepseek231 小时前
Agent任务中途断线,怎么避免重复执行?SQLite检查点实测
python·sqlite·任务调度·幂等性·ai agent
octopus_c1 小时前
STL 之 string:auto 与范围 for、常用接口、模拟实现与深浅拷贝
开发语言·c++·算法