惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Jina AI
Jina AI
V
Vulnerabilities – Threatpost
Security Latest
Security Latest
AI
AI
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
量子位
H
Help Net Security
Attack and Defense Labs
Attack and Defense Labs
The GitHub Blog
The GitHub Blog
L
LINUX DO - 最新话题
A
Arctic Wolf
博客园_首页
S
Securelist
S
Secure Thoughts
Google DeepMind News
Google DeepMind News
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
T
Tailwind CSS Blog
Apple Machine Learning Research
Apple Machine Learning Research
酷 壳 – CoolShell
酷 壳 – CoolShell
Stack Overflow Blog
Stack Overflow Blog
N
Netflix TechBlog - Medium
Cyberwarzone
Cyberwarzone
小众软件
小众软件
T
Threatpost
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Blog — PlanetScale
Blog — PlanetScale
N
News and Events Feed by Topic
NISL@THU
NISL@THU
Forbes - Security
Forbes - Security
博客园 - 聂微东
F
Fortinet All Blogs
Simon Willison's Weblog
Simon Willison's Weblog
H
Heimdal Security Blog
罗磊的独立博客
S
Security @ Cisco Blogs
B
Blog
T
Troy Hunt's Blog
Engineering at Meta
Engineering at Meta
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
The Hacker News
The Hacker News
The Last Watchdog
The Last Watchdog
Hacker News - Newest:
Hacker News - Newest: "LLM"
I
Intezer
T
Threat Research - Cisco Blogs
C
Cybersecurity and Infrastructure Security Agency CISA
The Cloudflare Blog
S
Schneier on Security
月光博客
月光博客
L
LINUX DO - 热门话题
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org

追梦人物的博客

0x05:Merkle Tree & Patricia Trie 0x04:ECDSA - 以太坊设计与实现 LeetCode 105 从前序与中序遍历构造二叉树 迭代算法原理 + 完整证明 0x03:Address - 以太坊设计与实现 0x02:Secp256k1 - 以太坊设计与实现 0x00:专栏开篇 - 以太坊设计与实现 0x01:RLP 编码 - 以太坊设计与实现 迪杰斯特拉(Dijkstra)最短路径算法原理、实现与证明 - 追梦人物的博客 实战:CEX-DEX 稳定币套利监控程序开发 - 追梦人物的博客 CEX-DEX 稳定币套利模型 - 追梦人物的博客 Uniswap 手续费和协议费机制剖析 - 追梦人物的博客 Uniswap 流动性机制及相关数学原理分析 - 追梦人物的博客 uv 替代 pyenv + pipx + poetry 环境管理实践 Rust 项目从创建到发布 - 追梦人物的博客 VS Code 调试 Python - 追梦人物的博客 比特币跨市场套利的数学模型 - 追梦人物的博客 ERC-20 相关知识点总结 - 追梦人物的博客 Django 老项目如何从 SQLite 迁到 PostgreSQL 自动生成接口文档 - HelloDjango - django REST framework 教程 单元测试 - HelloDjango - django REST framework 教程 限制接口访问频率 - HelloDjango - django REST framework 教程 API 版本管理 - HelloDjango - django REST framework 教程 如何在 Windows 下搭建高效的 django 开发环境 拓展Python Markdown - 追梦人物的博客 加缓存为接口提速 - HelloDjango - django REST framework 教程 基于 drf-haystack 实现文章搜索接口 - HelloDjango 评论接口 - HelloDjango - django REST framework 教程 实现分类、标签、归档日期接口 - HelloDjango - django REST framework 教程 在接口返回Markdown解析后的内容 - HelloDjango - django REST framework 教程 文章详情接口 - HelloDjango - django REST framework 教程 分页 - HelloDjango - django REST framework 教程 使用视图集简化代码 - HelloDjango - django REST framework 教程 用类视图实现首页 API - HelloDjango - django REST framework 教程 实现博客首页文章列表 API - HelloDjango - django REST framework 教程 初始化 RESTful API 风格的博客系统 - HelloDjango django-rest-framework 是什么鬼? - HelloDjango - django REST framework 教程 结束 or 开始? - HelloDjango Coverage.py 统计测试覆盖率 - HelloDjango - Django博客教程(第二版) 单元测试:测试评论应用 - HelloDjango - Django博客教程(第二版) 单元测试:测试 blog 应用 - HelloDjango Django Haystack 全文检索与关键词高亮 - HelloDjango Django 博客实现简单的全文搜索 - HelloDjango - Django博客教程(第二版) 开启 Django 博客的 RSS 功能 - HelloDjango 统计各个分类和标签下的文章数 - HelloDjango - Django博客教程(第二版) 稳定易用的 Django 分页库,完善分页功能 - HelloDjango 通过 Django Pagination 实现简单分页 - HelloDjango 在脚本中使用 ORM:Faker 批量生成测试数据 - HelloDjango Django 官方推荐的姿势:类视图 - HelloDjango - Django博客教程(第二版) Django 使用 union 合并不同模型(Model) 的查询集(QuerySet) 开发博客文章阅读量统计功能 - HelloDjango - Django博客教程(第二版) 使用 Docker 让部署 Django 项目更加轻松 - HelloDjango 使用 Certbot 向 Let's Encrypt 免费申请 HTTPS 证书 - HelloDjango 使用 Fabric 自动化部署 - HelloDjango 博客代码开源啦 - 追梦人物的博客 (赠书)推荐一本django书籍:Django企业开发实战 - 追梦人物的博客 Nginx+Gunicorn+Supervisor 部署 Django 博客应用 - HelloDjango 优化博客功能细节,提升使用体验 - HelloDjango - Django博客教程(第二版) 交流的桥梁:评论功能 - HelloDjango - Django博客教程(第二版) 分类、归档和标签页 - HelloDjango - Django博客教程(第二版) 页面侧边栏:使用自定义模板标签 - HelloDjango - Django博客教程(第二版) 自动生成文章摘要 - HelloDjango - Django博客教程(第二版) Markdown 文章自动生成目录,提升阅读体验 - HelloDjango - Django博客教程(第二版) 让博客支持 Markdown 语法和代码高亮 - HelloDjango 开发博客文章详情页 - HelloDjango - Django博客教程(第二版) 创作后台开启,请开始你的表演 - HelloDjango - Django博客教程(第二版) 博客从“裸奔”到“有皮肤” - HelloDjango - Django博客教程(第二版) Django 的接客之道 - HelloDjango - Django博客教程(第二版) Django 迁移、操作数据库 - HelloDjango - Django博客教程(第二版) 创建 Django 博客的数据库模型 - HelloDjango 开始进入 django 开发之旅 - HelloDjango "空空如也"的博客应用 - HelloDjango - Django博客教程(第二版) 一种自顶而下的Python装饰器设计方法 - 追梦人物的博客 Python提取支付宝和微信支付二维码 - 追梦人物的博客 2018 - 我的学生生涯最后一年回顾 - 追梦人物的博客 批量清除todo练习参考答案 - Vue 2.x Todo 教程练习参考答案 筛选练习参考答案 - Vue 2.x Todo 教程练习参考答案 删除todo练习参考答案 - Vue 2.x Todo 教程练习参考答案 编辑todo练习参考答案 - Vue 2.x Todo 教程练习参考答案 添加todo练习参考答案 - Vue 2.x Todo 教程练习参考答案 标为完成练习参考答案 - Vue 2.x Todo 教程练习参考答案 入门仪式_Hello_Vue练习参考答案 - Vue 2.x Todo 教程练习参考答案 组件化todo应用 - Vue 2.x Todo 教程 批量清除todo - Vue 2.x Todo 教程 本地存储 - Vue 2.x Todo 教程 筛选 - Vue 2.x Todo 教程 还剩多少todo未完成 - Vue 2.x Todo 教程 自定义指令实现自动聚焦 - Vue 2.x Todo 教程 全部标为完成 - Vue 2.x Todo 教程 删除todo - Vue 2.x Todo 教程 编辑todo - Vue 2.x Todo 教程 添加todo - Vue 2.x Todo 教程 标为完成 - Vue 2.x Todo 教程 UI - Vue 2.x Todo 教程 显示todo列表 - Vue 2.x Todo 教程 入门仪式:Hello Vue - Vue 2.x Todo 教程 在学习django-rest-framework时收集的学习资料推荐 - 追梦人物的博客 区块链理论与应用研究小组成员招募书 - 追梦人物的博客 Python界网红,豆瓣工程师董伟明加了我的QQ后 - 追梦人物的博客 招募Django学习小组项目组核心成员 - 追梦人物的博客
Bellman-Ford 算法原理及其在 DeFi 套利中的应用 - 追梦人物的博客
2025-12-31 · via 追梦人物的博客

研究 DeFi 套利时,Bellman-Ford 算法是经典工具之一。这篇文章将详细讲解该算法的原理并通过证明其正确性加深对算法的理解,同时说明 DeFi 中的环形套利策略等价于在代币汇率图中寻找负权环。基于这一等价关系,便可应用 Bellman-Ford 算法高效定位套利机会。

问题定义

给定一个带权有向图 \(G = (V, E)\),其中:

  • \(V\) 是图中所有节点的集合
  • \(E\) 是图中所有边的集合
  • 对于任意边 \((u, v) \in E\),其权重为 \(w(u, v)\) (可正可负)

指定一个源点 \(s \in V\)

  1. 求源点 \(s\) 到图中所有其他节点 \(v \in V\) 的最短路径长度(若存在)
  2. 若图中存在"从源点可达的负权环",则精准检测该环(此时环上节点无最短路径)

所谓负权环,指环 \(C = v_1 \to v_2 \to \dots \to v_k \to v_1\),满足 \(\sum_{i=1}^k w(v_i, v_{i+1}) < 0\)(其中 \(v_{k+1} = v_1\))。若源点能到达这个环,则可以无限绕环来不断减小路径长度,此时最短路径为 \(-\infty\)

算法思想

与 Dijkstra 算法通过"优先选择当前已知距离起点最近的顶点"这一局部最优决策来求全局最短路径的贪心策略不同,Bellman-Ford 算法通过“开天眼”的方式总是选择全局最优路径。Bellman-Ford 算法在每一轮迭代中遍历所有的边,考察以该边为最后一节的路径是否比节点已记录的路径更短,如果更短就更新距离记录(这叫"松弛操作")。

算法神奇之处在于,如果节点的真实最短路径最多包含 \(k\) 条边,那么第 \(k\) 轮迭代得到的值就是节点的真实最短路径。这是算法正确性的关键,后续我们会证明这一点。

算法流程

基于上面的思想,我们定义一些数据结构来记录状态:

  • 距离数组 \(dist\)\(dist[v]\) 表示算法运行过程中,当前已知的从源点 \(s\) 到节点 \(v\) 的最短路径长度。初始时 \(dist[s] = 0\),其余节点的 \(dist[v] = +\infty\)(表示初始状态下不可达)
  • 边列表:存储所有边 \((u, v, w)\),其中 \(u\) 是起点,\(v\) 是终点,\(w\) 是权重

为了提高算法效率,通常采用边列表存储图结构(每个节点维护出边列表)。

步骤 1:初始化

  • 初始化距离数组 \(dist\)\(dist[s] = 0\),其余 \(dist[v] = +\infty\)
  • (可选)初始化前驱数组 \(prev\)\(prev[v] = -1\),用于后续重构最短路径路径

步骤 2:\(V-1\) 轮松弛迭代

循环 \(V-1\) 次,每轮遍历所有边:

  1. 遍历所有边:对于每条边 \((u, v)\),若 \(dist[u] \neq +\infty\)\(u\) 已可达)且 \(dist[v] > dist[u] + w(u, v)\)(通过 \(u\)\(v\) 更短),则执行松弛操作:更新 \(dist[v] = dist[u] + w(u, v)\)
  2. 提前终止优化:若某轮松弛没有发生任何更新,说明所有最短路径已找到,可以提前终止(后续轮次也不会有更新)

步骤 3:负权环检测

  • 遍历所有边 \((u, v)\) 一次:
  • \(dist[u] \neq +\infty\)\(dist[v] > dist[u] + w(u, v)\),说明存在"从源点可达的负权环"
  • 若未找到这样的边,则 \(dist\) 数组即为源点到所有节点的最短路径长度

算法示例

为了让流程更直观,我们用一个具体例子来演示算法执行过程。

考虑一个有向图 \(G = (V, E)\),其中:
- 节点集合 \(V = \{s, A, B, C, D\}\)(共 \(V=5\) 个节点,需执行 \(5-1=4\) 轮松弛)
- 边及权重:
- \(s \to A\)(权重 6)
- \(s \to C\)(权重 7)
- \(A \to B\)(权重 5)
- \(A \to C\)(权重 8)
- \(A \to D\)(权重 -4)
- \(B \to A\)(权重 -2)
- \(C \to B\)(权重 -3)
- \(C \to D\)(权重 9)
- \(D \to s\)(权重 2)
- \(D \to B\)(权重 7)

graph LR s((s)) -->|6| A((A)) s((s)) -->|7| C((C)) A((A)) -->|5| B((B)) A((A)) -->|8| C((C)) A((A)) -->|-4| D((D)) B((B)) -->|-2| A((A)) C((C)) -->|-3| B((B)) C((C)) -->|9| D((D)) D((D)) -->|2| s((s)) D((D)) -->|7| B((B)) style s fill:#f9f,stroke:#333,stroke-width:2px style A fill:#9ff,stroke:#333 style B fill:#9ff,stroke:#333 style C fill:#9ff,stroke:#333 style D fill:#9ff,stroke:#333

我们要求从源点 \(s\) 到所有其他节点的最短路径。下面是算法执行的完整过程:

初始化

\(dist = [0, +\infty, +\infty, +\infty, +\infty]\)(对应节点:\(s, A, B, C, D\)

第 1 轮松弛

操作 \(dist\) 更新后
\(s→A(6)\) \(dist[A] = 0+6=6\) \([0, 6, +\infty, +\infty, +\infty]\)
\(s→C(7)\) \(dist[C] = 0+7=7\) \([0, 6, +\infty, 7, +\infty]\)
\(A→B(5)\) \(dist[B] = 6+5=11\) \([0, 6, 11, 7, +\infty]\)
\(A→C(8)\) \(dist[C] = 6+8=14 >7\)(不更新) \([0, 6, 11, 7, +\infty]\)
\(A→D(-4)\) \(dist[D] =6+(-4)=2\) \([0,6,11,7,2]\)
\(B→A(-2)\) \(dist[A] =11+(-2)=9 >6\)(不更新) \([0,6,11,7,2]\)
\(C→B(-3)\) \(dist[B] =7+(-3)=4 <11\)(更新) \([0,6,4,7,2]\)
\(C→D(9)\) \(dist[D] =7+9=16 >2\)(不更新) \([0,6,4,7,2]\)
\(D→s(2)\) \(dist[s] =2+2=4 >0\)(不更新) \([0,6,4,7,2]\)
\(D→B(7)\) \(dist[B] =2+7=9 >4\)(不更新) \([0,6,4,7,2]\)

第 1 轮后:\(dist = [0, 6, 4, 7, 2]\)

第 2 轮松弛

操作 \(dist\) 更新后
\(s→A(6)\) \(dist[A] =0+6=6\)(不变) \([0,6,4,7,2]\)
\(s→C(7)\) \(dist[C] =0+7=7\)(不变) \([0,6,4,7,2]\)
\(A→B(5)\) \(dist[B] =6+5=11 >4\)(不更新) \([0,6,4,7,2]\)
\(A→C(8)\) \(dist[C] =6+8=14 >7\)(不更新) \([0,6,4,7,2]\)
\(A→D(-4)\) \(dist[D] =6+(-4)=2\)(不变) \([0,6,4,7,2]\)
\(B→A(-2)\) \(dist[A] =4+(-2)=2 <6\)(更新) \([0,2,4,7,2]\)
\(C→B(-3)\) \(dist[B] =7+(-3)=4\)(不变) \([0,2,4,7,2]\)
\(C→D(9)\) \(dist[D] =7+9=16 >2\)(不更新) \([0,2,4,7,2]\)
\(D→s(2)\) \(dist[s] =2+2=4 >0\)(不更新) \([0,2,4,7,2]\)
\(D→B(7)\) \(dist[B] =2+7=9 >4\)(不更新) \([0,2,4,7,2]\)

第 2 轮后:\(dist = [0, 2, 4, 7, 2]\)

第 3 轮松弛

操作 \(dist\) 更新后
\(s→A(6)\) \(dist[A] =0+6=6 >2\)(不更新) \([0,2,4,7,2]\)
\(s→C(7)\) \(dist[C] =0+7=7\)(不变) \([0,2,4,7,2]\)
\(A→B(5)\) \(dist[B] =2+5=7 >4\)(不更新) \([0,2,4,7,2]\)
\(A→C(8)\) \(dist[C] =2+8=10 >7\)(不更新) \([0,2,4,7,2]\)
\(A→D(-4)\) \(dist[D] =2+(-4)=-2 <2\)(更新) \([0,2,4,7,-2]\)
\(B→A(-2)\) \(dist[A] =4+(-2)=2\)(不变) \([0,2,4,7,-2]\)
\(C→B(-3)\) \(dist[B] =7+(-3)=4\)(不变) \([0,2,4,7,-2]\)
\(C→D(9)\) \(dist[D] =7+9=16 >-2\)(不更新) \([0,2,4,7,-2]\)
\(D→s(2)\) \(dist[s] =-2+2=0\)(不变) \([0,2,4,7,-2]\)
\(D→B(7)\) \(dist[B] =-2+7=5 >4\)(不更新) \([0,2,4,7,-2]\)

第 3 轮后:\(dist = [0, 2, 4, 7, -2]\)

第 4 轮松弛

操作 \(dist\) 更新后
\(s→A(6)\) \(dist[A] =0+6=6 >2\)(不更新) \([0,2,4,7,-2]\)
\(s→C(7)\) \(dist[C] =0+7=7\)(不变) \([0,2,4,7,-2]\)
\(A→B(5)\) \(dist[B] =2+5=7 >4\)(不更新) \([0,2,4,7,-2]\)
\(A→C(8)\) \(dist[C] =2+8=10 >7\)(不更新) \([0,2,4,7,-2]\)
\(A→D(-4)\) \(dist[D] =2+(-4)=-2\)(不变) \([0,2,4,7,-2]\)
\(B→A(-2)\) \(dist[A] =4+(-2)=2\)(不变) \([0,2,4,7,-2]\)
\(C→B(-3)\) \(dist[B] =7+(-3)=4\)(不变) \([0,2,4,7,-2]\)
\(C→D(9)\) \(dist[D] =7+9=16 >-2\)(不更新) \([0,2,4,7,-2]\)
\(D→s(2)\) \(dist[s] =-2+2=0\)(不变) \([0,2,4,7,-2]\)
\(D→B(7)\) \(dist[B] =-2+7=5 >4\)(不更新) \([0,2,4,7,-2]\)

第 4 轮后:\(dist\) 无变化(已完成 \(V-1\) 轮松弛)

遍历所有边,对每一条边执行松弛判断,均无法更新距离数组 \(dist\),说明图中不存在从源点 \(s\) 可达的负权环

最终结果:\(dist = [0, 2, 4, 7, -2]\)\(s→A=2\)\(s→B=4\)\(s→C=7\)\(s→D=-2\))。

可以验证一下:

  • \(s \to A\):最优路径 \(s \to C \to B \to A\),长度 \(7+(-3)+(-2)=2\)
  • \(s \to B\):最优路径 \(s \to C \to B\),长度 \(7+(-3)=4\)
  • \(s \to C\):直接路径,长度 7 ✓
  • \(s \to D\):最优路径 \(s \to C \to B \to A \to D\),长度 \(7+(-3)+(-2)+(-4)=-2\)

示例算法中,外层循环执行 \(V-1\) 次,内层遍历所有边,每次 \(O(E)\),因此总的时间复杂度\(O((V-1) \cdot E) = O(VE)\)

算法证明

理解 Bellman-Ford 算法的关键,在于能够证明其正确性。需证明两点:

  1. \(V-1\) 轮松弛后,可达节点的 \(dist[v] = \delta(v)\)\(\delta(v)\) 为真实最短路径长度)
  2. \(V\) 轮松弛仍能更新则存在负权环。

先定义几个符号:

  • \(\delta(u)\):源点 \(s\) 到节点 \(u\)真实最短路径长度(客观值,与算法无关)。若 \(u\) 不可达则 \(\delta(u) = +\infty\),若存在可达负权环则 \(\delta(u) = -\infty\)
  • \(dist[u]\):算法维护的当前距离(迭代过程中的近似值)
  • \(dist_i[u]\):第 \(i\) 轮松弛后 \(dist[u]\) 的值

根据定义,显然有 \(dist[u] \geq \delta(u)\)。我们的目标是证明当 \(u\) 可达且无负权环时,\(dist_{V-1}[u] = \delta(u)\)

引理 1(三角不等式)

对于任意边 \((u, v) \in E\),有:

\[\delta(v) \leq \delta(u) + w(u, v) \tag{1}\]

证明:源点到 \(v\) 的最短路径有两种可能,要么经过 \(u\),要么不经过。

  • 若经过 \(u\),则 \(\delta(v) = \delta(u) + w(u, v)\)
  • 若不经过,则 \(\delta(v) < \delta(u) + w(u, v)\)(否则经过 \(u\) 的路径成了最短路径,矛盾)

因此总有 \(\delta(v) \leq \delta(u) + w(u, v)\)

引理 2(路径长度上界性质)

在第 \(i\) 轮松弛后,对于所有节点 \(v\)\(dist_i[v]\) 的值等于从源点 \(s\)\(v\) 最多包含 \(i\) 条边的最短路径长度。

证明:用数学归纳法。\(i=0\) 时,\(dist_0[s] = 0\)(0 条边的路径),其余 \(dist_0[v] = +\infty\)(不存在 0 条边的路径),成立。假设第 \(k\) 轮后,\(dist_k[v]\) 等于从 \(s\)\(v\) 的最多包含 \(k\) 条边的最短路径长度。现在分析第 \(k+1\) 轮松弛:

对于任意节点 \(v\),从 \(s\)\(v\) 的最多包含 \(k+1\) 条边的最短路径,可以分解为:从 \(s\) 到某节点 \(u\) 的最多包含 \(k\) 条边的最短路径,再加上边 \((u, v)\)。因此:

\[\delta(v) = \min\{ dist_k[u] + w(u, v) \mid (u, v) \in E \}\]

\(k+1\) 轮松弛会遍历所有边 \((u, v)\),并执行 \(dist_{k+1}[v] = \min(dist_k[v], dist_k[u] + w(u, v))\),这正是上述最小值的计算过程。因此 \(dist_{k+1}[v]\) 等于从 \(s\)\(v\) 的最多包含 \(k+1\) 条边的最短路径长度。

定理(\(V-1\) 轮松弛的正确性)

定理:若节点 \(v\) 可达且图中不存在从源点可达的负权环,则 \(dist_{V-1}[v] = \delta(v)\)

证明:由引理 2,\(dist_{V-1}[v]\) 等于从 \(s\)\(v\) 的最多包含 \(V-1\) 条边的最短路径长度。由于图中无负权环,\(v\) 的最短路径必然是简单路径(不含重复节点),因此最多包含 \(V-1\) 条边(\(V\) 个节点)。所以 \(dist_{V-1}[v] = \delta(v)\)

定理(负权环检测的正确性)

定理:若 \(V-1\) 轮松弛后,存在边 \((u, v)\) 满足 \(dist[v] > dist[u] + w(u, v)\),则图中存在从源点可达的负权环。

证明:用反证法。假设图中不存在可达负权环,由前述定理,所有可达节点的 \(dist_{V-1}[v] = \delta(v)\)。由引理 1(三角不等式),\(\delta(v) \leq \delta(u) + w(u, v)\),即 \(dist_{V-1}[v] \leq dist_{V-1}[u] + w(u, v)\),与"仍能松弛"的条件 \(dist[v] > dist[u] + w(u, v)\) 矛盾,因此假设不成立,存在可达负权环。

算法实现

下面我们用 Go 语言实现 Bellman-Ford 算法,采用边列表存储图结构。

package main

import (
    "fmt"
    "math"
)

// Edge 定义边结构:起点 u→终点 v,权重 w(支持正负权)
type Edge struct {
    u      int // 起点
    v      int // 终点
    weight int // 权重
}

// Graph 定义图结构:存储节点数和边列表(邻接表法)
type Graph struct {
    vertexCount int    // 节点总数V
    edges       []Edge // 边列表E
}

// NewGraph 初始化图(n 为节点数,节点编号从 0 开始)
func NewGraph(n int) *Graph {
    return &Graph{
        vertexCount: n,
        edges:       make([]Edge, 0),
    }
}

// AddEdge 向图中添加有向边 u→v(权重 w),带节点越界校验
func (g *Graph) AddEdge(u, v, w int) error {
    if u < 0 || v < 0 || u >= g.vertexCount || v >= g.vertexCount {
        return fmt.Errorf("节点编号越界(合法范围 0<=u,v<%d)", g.vertexCount)
    }
    g.edges = append(g.edges, Edge{u: u, v: v, weight: w})
    return nil
}

// BellmanFord 求解单源最短路径,核心能力:支持负权边 + 检测可达负权环 + 输出负权环路径
// 返回:距离数组、负权环路径数组(无环则为空)、是否存在负权环、错误信息
func (g *Graph) BellmanFord(s int) ([]int, []int, bool, error) {
    n := g.vertexCount
    if s < 0 || s >= n {
        return nil, nil, false, fmt.Errorf("源点编号越界(合法范围0<=s<%d)", n)
    }

    // 1. 初始化距离数组:源点到自身为 0,其余为 +∞
    dist := make([]int, n)
    for i := range dist {
        dist[i] = math.MaxInt32
    }
    dist[s] = 0

    // 前驱节点数组,记录每个节点的最短路径前驱,用于回溯路径/提取负权环
    // prev[v] = u 表示最短路径中 u → v,初始值 -1 表示无前置节点
    prev := make([]int, n)
    for i := range prev {
        prev[i] = -1
    }

    // 2. V-1 轮松弛迭代:松弛所有边,找到所有最短路径(无负权环时最多松弛 V-1 次)
    for i := 0; i < n-1; i++ {
        updated := false // 优化:本轮无更新则提前退出,提升效率
        for _, edge := range g.edges {
            u, v, w := edge.u, edge.v, edge.weight
            if dist[u] == math.MaxInt32 { // 起点不可达,跳过松弛
                continue
            }
            // 松弛操作:更新更短路径
            if dist[v] > dist[u]+w {
                dist[v] = dist[u] + w
                prev[v] = u // 同步更新前驱节点,记录路径走向
                updated = true
            }
        }
        if !updated {
            break
        }
    }

    // 3. 负权环检测:第 V 轮仍能松弛 → 存在从源点可达的负权环
    hasNegativeCycle := false
    cycleStartNode := -1 // 记录负权环上的任意一个节点(松弛成功的节点)
    for _, edge := range g.edges {
        u, v, w := edge.u, edge.v, edge.weight
        if dist[u] == math.MaxInt32 {
            continue
        }
        // 仍可松弛,说明存在负权环,记录环上节点 v
        if dist[v] > dist[u]+w {
            hasNegativeCycle = true
            cycleStartNode = v
            break
        }
    }

    // 负权环路径提取核心逻辑
    negativeCycle := make([]int, 0) // 存储负权环的节点路径(有向)
    if hasNegativeCycle {
        // 步骤 1:从环上节点出发,回溯 n 步前驱 → 必然落到负权环的某个节点(环入口)
        // 原理:图中最多 n 个节点,回溯 n 步后一定会进入环内,避免前驱链过长导致找不到环
        entry := cycleStartNode
        for i := 0; i < n; i++ {
            entry = prev[entry]
        }

        // 步骤 2:从环入口出发,再次回溯前驱,收集所有环节点,直到回到入口
        current := entry
        negativeCycle = append(negativeCycle, current)
        for {
            current = prev[current]
            negativeCycle = append(negativeCycle, current)
            if current == entry { // 回到入口,环收集完成
                break
            }
        }
    }

    return dist, negativeCycle, hasNegativeCycle, nil
}

// 格式化输出距离结果:将极大值转为 ∞,提升可读性
func formatDist(dist []int) []string {
    res := make([]string, len(dist))
    for i, d := range dist {
        if d == math.MaxInt32 {
            res[i] = "∞"
        } else {
            res[i] = fmt.Sprintf("%d", d)
        }
    }
    return res
}

// 新增:格式化负权环路径,转为 "节点→节点→节点" 的有向格式,提升可读性
func formatCyclePath(cycle []int) string {
    if len(cycle) == 0 {
        return "无"
    }
    res := ""
    for i := len(cycle) - 1; i >= 0; i-- {
        res += fmt.Sprintf("%d", cycle[i])
        if i > 0 {
            res += "→"
        }
    }
    return res
}

func main() {
    fmt.Println("===== Case 1:无负权环 =====")
    // 节点数:5 个(编号 0-4),源点设为 0,含正权/负权边
    g1 := NewGraph(5)

    _ = g1.AddEdge(0, 1, 6)
    _ = g1.AddEdge(0, 3, 7)
    _ = g1.AddEdge(1, 2, 5)
    _ = g1.AddEdge(1, 3, 8)
    _ = g1.AddEdge(1, 4, -4)
    _ = g1.AddEdge(2, 1, -2)
    _ = g1.AddEdge(3, 2, -3)
    _ = g1.AddEdge(3, 4, 9)
    _ = g1.AddEdge(4, 0, 2)
    _ = g1.AddEdge(4, 2, 7)

    // 求解源点 0 到所有节点的最短路径(适配新增的负权环路径返回值)
    dist1, cyclePath1, hasCycle1, err1 := g1.BellmanFord(0)
    if err1 != nil {
        fmt.Printf("算法执行失败:%v\n", err1)
        return
    }
    // 格式化输出结果
    fmt.Printf("源点 0 → 各节点最短路径:%v\n", formatDist(dist1))
    fmt.Printf("图中是否存在【源点可达的负权环】:%t\n", hasCycle1)
    // 新增:输出负权环路径(无环则显示「无」)
    fmt.Printf("负权环具体路径:%s\n\n", formatCyclePath(cyclePath1))

    fmt.Println("===== Case 2:带负权环 =====")
    // 节点数:4 个(编号 0-3),包含明确的负权环 1→2→1(总权重=-1)
    g2 := NewGraph(4)
    _ = g2.AddEdge(0, 1, 1)
    _ = g2.AddEdge(1, 2, -3)
    _ = g2.AddEdge(2, 1, 1)
    _ = g2.AddEdge(1, 3, 2)
    _ = g2.AddEdge(2, 3, 1)

    // 求解源点 0 到所有节点的最短路径(适配新增的负权环路径返回值)
    dist2, cyclePath2, hasCycle2, err2 := g2.BellmanFord(0)
    if err2 != nil {
        fmt.Printf("算法执行失败:%v\n", err2)
        return
    }
    // 格式化输出结果
    fmt.Printf("源点 0 → 各节点最短路径:%v\n", formatDist(dist2))
    fmt.Printf("图中是否存在【源点可达的负权环】:%t\n", hasCycle2)
    // 新增:输出负权环路径
    fmt.Printf("负权环具体路径:%s\n", formatCyclePath(cyclePath2))
}

负权环的应用:环形套利

金融市场中,不同资产可相互兑换,因汇率差异存在一种经典套利策略:环形套利。我们定义 \(A \to B\) 为“将资产 \(A\) 兑换为资产 \(B\)”:若从资产 \(A\) 出发,经过 \(A \to B \to C \to \dots \to A\) 的环形交易后,最终持有的 \(A\) 资产数量高于初始数量,即可通过该路径实现套利。

以 DeFi 交易为例,我们可将 DEX(去中心化交易所)中的代币作为图的顶点,代币间的汇率(准确来说是汇率的自然对数的负值)作为边的权重,构建代币汇率图。

假设存在代币兑换环路 \(t_1 \to t_2 \to \dots \to t_n\),对应汇率依次为 \(r_1, r_2, \dots, r_n\)。若初始本金为 1,绕环兑换后的最终本金为:

\[ P = 1 \times r_1 \times r_2 \times \dots \times r_n \tag1\]

\(P > 1\) 时,说明存在套利机会。

对公式 (1) 两边取自然对数,可得:

\[ \ln(P) = \ln(r_1) + \ln(r_2) + \dots + \ln(r_n) \]

\(P > 1\),则 \(\ln(P) > 0\);对等式两边取负号后:

\[ (-\ln(r_1)) + (-\ln(r_2)) - \dots + (-\ln(r_n))< 0 \]

若我们将代币汇率图中边的权重定义为汇率的自然对数的负值(即 \(w = -\ln(r_i)\)),则上述不等式可表示为环形路径上所有边的权重之和:

\[ w(t_1 \to t_2) + w(t_2 \to t_3) + \dots + w(t_n \to t_1) < 0 \]

这意味着,存在套利机会的环形路径,对应图中的一个负权环

反之亦然:图中存在负权环,就等价于对应代币兑换路径存在套利机会。

在实际开发中,首先会构建初始的代币汇率图;之后根据交易事件实时更新代币间的汇率(即图中边的权重),这一步通常会采用线下模拟计算的方式,主要原因是链上直接查询的速度可能较慢。完成汇率图更新后,运行 Bellman-Ford 算法检测图中是否存在负权环。

我们会在后续文章中展示具体的套利程序开发过程,如果感兴趣,请关注我的博客更新 👀。

-- EOF --