AcWing 851 spfa求最短路

题解

以此题为例介绍一下图论中的最短路算法 \(Bellman\)-\(Ford\) 算法。算法的步骤和正确性证明参考文章最短路径(Bellman-Ford算法)

松弛函数

对边集合 \(E\) 中任意边,\(w(u,v)\) 表示顶点 \(u\) 到顶点 \(v\) 的边的权值,用 \(d[v]\) 表示当前从起点 \(s\) 出发到顶点 \(v\) 的最短距离。

若存在边 \(e\),权值为 \(w(u,v)\),使得:

\[d[v] > d[u] + w(u,v)
\]

则更新 \(d[v]\) 值

\[d[v] = d[u] + w(u,v)
\]

松弛函数作用:判断经过某个顶点,或者某条边,是否可以缩短起点到终点的当前最短距离(路径权值)。

松弛函数迭代

以对边集合 \(E\) 中每条边执行一次迭代函数作为一次迭代(称为松弛函数迭代),我们可以判断,只需经过有限次迭代,即可确保计算出起点到每个顶点的最短距离。

以下图作为示例示例演示松弛函数迭代过程。

为了凸显出过程的特点,我们直接在最坏情况下分析。

用 \(d[v]\) 表示起点 \(s\) 到顶点 \(v\) 的当前最短距离,以 \(\delta(u,v)\) 表示顶点 \(u\) 到顶点 \(v\) 的全局最短距离,集合 \(S\) 表示当前已找到全局最短距离的顶点集

迭代前,分别设置为

\[d[v] = \infty, \forall v \in V \quad
S = \{ s \}
\]

第一次迭代

  • 对边 \(w(c,d)\) 执行松弛函数,则 \(d[d]=\infty\)
  • 对边 \(w(b,c)\) 执行松弛函数,则 \(d[c]=\infty\)
  • 对边 \(w(a,b)\) 执行松弛函数,则 \(d[b]=d[a]+w(a,b)=1\)
  • 对边 \(w(a,c)\) 执行松弛函数,则 \(d[c]=d[a]+w(a,c)=6\)
  • 对边 \(w(a,d)\) 执行松弛函数,则 \(d[d]=d[a]+w(a,d)=10\)

第一轮迭代,有三条边起到了松弛效果,直观的可以看出 \(d[b] = \delta(a,b)\),此时有一个顶点获得了全局最短距离,\(S = \{ a, b \}\)。

第二次迭代

  • 对边 \(w(c,d)\) 执行松弛函数,则 \(d[d]=10\)
  • 对边 \(w(b,c)\) 执行松弛函数,则 \(d[c]=d[b] + w(b,c)=3\)
  • 对边 \(w(a,b)\) 执行松弛函数,则 \(d[b]=1\)
  • 对边 \(w(a,c)\) 执行松弛函数,则 \(d[c]=3\)
  • 对边 \(w(a,d)\) 执行松弛函数,则 \(d[d]=10\)

第一轮迭代,有三条边起到了松弛效果,直观的可以看出 \(d[c] = \delta(a,c)\),此时有一个顶点获得了全局最短距离,\(S = \{ a, b, c \}\)。

第三次迭代

  • 对边 \(w(c,d)\) 执行松弛函数,则 \(d[d]=d[c] + w(c,d)=8\)
  • 对边 \(w(b,c)\) 执行松弛函数,则 \(d[c]=3\)
  • 对边 \(w(a,b)\) 执行松弛函数,则 \(d[b]=1\)
  • 对边 \(w(a,c)\) 执行松弛函数,则 \(d[c]=3\)
  • 对边 \(w(a,d)\) 执行松弛函数,则 \(d[d]=8\)

第一轮迭代,有三条边起到了松弛效果,直观的可以看出 \(d[d] = \delta(a,d)\),此时有一个顶点获得了全局最短距离,\(S = \{ a, b, c, d \}\)。

迭代次数(算法正确性)

对于顶点 \(v\),若此时有 \(d[v] = \delta(s, v)\),即顶点 \(v\) 的全局最短距离已被确定,则称顶点 \(v\) 为已确定顶点。若 \(v \notin S\),则需要将顶点 \(v\) 加入到集合 \(S\) 中。

有下面定理:

若一个图中存在未确认顶点,则对边集合的一次松弛迭代后,会增加至少一个已确认顶点。具体地说,至少增加的已确认顶点为 \(\arg\min_{j \in V - S} d[j]\)

定理证明

  • \(V\) 为图的顶点集;

  • \(S\) 为已找到全局最短距离的顶点集;

  • \(T=V-S\) 为未找到全局最短距离的顶点集。

初始情形

初始情况下,只有顶点 \(s\) 属于已确定顶点(\(s \in S\)), 此时有两种情况:起点 \(s\) 不存在相邻顶点;起点 \(s\) 存在相邻顶点。对于第一种情况,此时已找到全局最短距离,算法结束。对于第二种情况,我们证明,在对边集合进行一次松弛迭代后,必定会增加至少一个已确认顶点。

对于第二种情形,初始

\[d[s] = 0, \quad d[v]=\infty, \forall v \in V-S
\]

第一轮松弛迭代后,令 \(k = \arg\min_{j \in V - S} d[j]\),则 \(k\) 是起点 \(s\) 的相邻顶点(所有非相邻顶点在第一轮迭代后当前最短距离都仍为 \(\infty\))。此时 \(d[k] = w(s,k)\),我们说 \(k\) 此时达到了全局最短距离,否则若 \(d[k]\) 没有达到全局最短距离,存在一条路径更短(下式),其中 \(r\) 为相邻顶点。

\[p’ = \langle s, r, \cdots, k \rangle
\]

相比较而言,当前最短距离 \(d[k]\) 对应的路径为

\[p = \langle s, k \rangle
\]
  • 若 \(r = k\),则 \(p’ = \langle s, k, \cdots, k \rangle\),路径 \(p'\) 的权值小于路径 \(p\) 的权值,说明路径 \(\langle k, \cdots, k \rangle\) 权值为负,即图中存在负权回路,矛盾。如下图所示

  • 若 \(r \neq k\),对于路径 \(p’\) 中的 \(\langle s, r \rangle\) 部分,由于对于任意相邻顶点 \(v\),存在 \(d[k] \leqslant d[v]\),所以有 \(d[k] \leqslant d[r]\),导出矛盾。
一般情形

已确认顶点集合为 \(S\),进行一次松弛迭代后,令 \(k = \arg\min_{j \in V - S} d[j]\),则 \(k\) 是集合 \(S\) 中某个点的相邻顶点(所有非相邻顶点此时当前最短距离都仍为 \(\infty\),更详细的证明用归纳法)。

我们说 \(k\) 此时达到了全局最短距离,否则若 \(d[k]\) 没有达到全局最短距离,存在一条路径更短(下式),其中 \(t \in T\) 。

\[p’ = \langle s, \cdots, t, k\rangle
\]

相比较而言,当前最短距离 \(d[k]\) 对应的路径为下式,其中 \(r \in S\),\(p_1\) 为 \(S\) 到 \(r\) 的最短路径,归纳法易知 \(p_1\) 路径经过的所有点 \(v\),有 \(v \in S\)。

\[p = \langle s, \cdots, r, k \rangle = \langle p_1, k \rangle
\]

对于 \(t\),有两种情况:\(t\) 是集合 \(S\) 中某个点的相邻顶点,由于 \(d[t] \leqslant d[k]\),故 \(p'\) 的路径权值必定比 \(p\) 的路径权值要大,矛盾;\(t\) 不是集合 \(S\) 中某个点的相邻顶点,此时 \(d[t] = \infty\),矛盾。

故当前最短距离 \(d[k]\) 达到了全局最短距离。

证毕。

程序设计

为方便展示,用 Python 代码实现松弛函数:

# distance 列表存储从起点到当前顶点的路径权值
# parent 列表存储到当前顶点的前驱顶点下标值。
# 初始 distance 列表和parent 列表元素皆为 None,表示路径权值无穷大
def releax(edge, distance, parent):
if distance[edge.begin - 1] == None:
pass
elif distance[edge.end - 1] == None or distance[edge.end - 1] > distance[edge.begin - 1] + edge.weight:
distance[edge.end - 1] = distance[edge.begin - 1] + edge.weight
parent[edge.end - 1] = edge.begin - 1
return True
return False

利用松弛函数实现 \(Bellman-Ford\) 算法。

# times 变量用于记录迭代的执行次数
# edges 列表是存储边的集合
# getEdgesFromAdjacencyList 函数用于从邻接矩阵中转换出边的集合
def bellman_ford(graph, start, end):
distance, parent = [None for i in range(graph.number)], [None for i in range(graph.number)]
times, distance[start - 1], edges = 1, 0, getEdgesFromAdjacencyList(graph)
while times < graph.number:
for i in range(len(edges)):
releax(edges[i], distance, parent)
times += 1
for i in range(len(edges)):
if releax(edges[i], distance, parent):
return False
return True

算法优化

对于 \(Bellman-Ford\) 算法的一种常见实现是使用队列优化。在中国的 IO 届也常称该算法为 \(SPFA\) 算法。

在 \(Bellman-Ford\) 算法介绍过程中我们会发现,有些边的松弛操作没有必要,我们可以证明如下结论:

一条边执行了松弛操作,当且仅当这条边的起点的当前最短距离之前被更新了。

在迭代过程中,每次取出队列的首节点,遍历该节点的所有出边,判断每条出边是否执行了松弛操作,若该边执行了松弛,则说明边对应的终点的当前最短距离 \(dist\) 被更新了,就将该点压入到队列 \(queue\) 中。

若存在负权回路,则该迭代过程会是一个死循环,但我们可以通过负权回路的特点来判断负权回路从而终止循环。负权回路的判断见另一篇文章。

算法正确性证明

在一轮取节点——遍历出边结束后,我们会发现此时图的所有边只有两种:

  • 松弛一定无效的边

  • 松弛可能有效的边

当前队列若为:

\[queue = \{ a_1, a_2,\cdots,a_n \}
\]

对图中任意一条边,它一定有起点终点,起点若在队列内,说明该边为松弛可能有效的边;起点若不在队列内,说明该边为松弛一定无效的边

因此,当退出循环时,退出的判定条件为队列为空,说明了此时所有边都一定是松弛一定无效的边,这是我们说所有点的 \(dist\) 都达到的最短路径。否则存在至少一点没有达到最短路径,即对于该点存在一条更短的路径,而更短路径的存在说明松弛可能有效的边也一定存在,矛盾。

符号说明一下,设原路径为

\[p_1 = \langle a, t_1, \cdots, t_n, b \rangle
\]

更短路径为

\[p_2 = \langle a, k_1, \cdots, k_m, b \rangle
\]

则此时必有

\[dist[k_m] + graph[k_m][b] < dist[b]
\]

即说明了松弛有效的边的存在性。

我们还需证明当无负权回路时,循环一定终止。反证法,若循环为死循环,则我们断言存在一点 \(c \in V\),它在队列 \(queue\) 中出现了无穷多次,则 \(c\) 被松弛了无穷多次。由于以 \(c\) 为终点的边只有有限条,而松弛了 \(c\) 点的边有无限条,因此负权回路必定存在,矛盾,循环的终止性成立。

综上,\(SPFA\) 算法正确。

队列优化算法的程序设计

实现代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<vector>
#include<queue>
#define PII pair<int, int>
#define INF 0x3f3f3f3f
using namespace std; const int N = 100010;
int n, m, q;
vector<PII> E[N]; // 用邻接表存储边
int dist[N]; // 第i号点距离源点的当前最短距离
bool vis[N]; // vis数组存的是当前结点是否在队列中 int spfa()
{
// 初始化
memset(dist, 0x3f, sizeof(dist));
dist[1] = 0;
queue<int> q;
q.push(1);
vis[1] = true;
// 队列优化的 Bellman-Ford 算法
while (!q.empty()) {
// 弹出队列首节点
int t = q.front();
q.pop();
// 首节点弹出,vis更新
vis[t] = false;
// 遍历首节点的所有出边
for (int i = 0, len = E[t].size(); i < len; ++i) {
int j = E[t][i].first, k = E[t][i].second;
if (dist[j] > dist[t] + k) {
dist[j] = dist[t] + k;
if (!vis[j]) {
vis[j] = true;
q.push(j);
}
}
}
}
return dist[n];
} int main()
{
cin >> n >> m;
int u, v, w;
for (int i = 0; i < m; ++i) {
cin >> u >> v >> w;
E[u].push_back({ v, w });
}
int tmp = spfa();
if (tmp >= INF) cout << "impossible" << endl;
else cout << tmp << endl; return 0;
}

最短路算法对比

算法 \(Floyd\) \(Dijkstra\) \(Bellman\)-\(Ford\)
空间复杂度 \(O\)\((V^2)\) \(O\)\((E)\) \(O\)\((E)\)
时间复杂度 \(O\)\((V^3)\) 看具体实现 \(O\)\((VE)\)
负权边时是否可以处理 可以 不能 可以
判断是否存在负权回路 不能 不能 可以

其中 \(V\) 表示图的顶点数,\(E\) 表示图的边数。

ACM - 最短路 - AcWing 851 spfa求最短路的更多相关文章

  1. acwing 851. spfa求最短路 模板

    地址 https://www.acwing.com/problem/content/description/853/ 给定一个n个点m条边的有向图,图中可能存在重边和自环, 边权可能为负数. 请你求出 ...

  2. AcWing 851. spfa求最短路 边权可能为负数。 链表 队列

    #include <cstring> #include <iostream> #include <algorithm> #include <queue> ...

  3. ACM - 最短路 - AcWing 849 Dijkstra求最短路 I

    AcWing 849 Dijkstra求最短路 I 题解 以此题为例介绍一下图论中的最短路算法.先让我们考虑以下问题: 给定一个 \(n\) 个点 \(m\) 条边的有向图(无向图),图中可能存在重边 ...

  4. 851. spfa求最短路(spfa算法模板)

    给定一个n个点m条边的有向图,图中可能存在重边和自环, 边权可能为负数. 请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出impossible. 数据保证不存在负权回路. 输入格式 ...

  5. 851. spfa求最短路

    给定一个n个点m条边的有向图,图中可能存在重边和自环, 边权可能为负数. 请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出impossible. 数据保证不存在负权回路. 输入格式 ...

  6. 基于bellman-ford算法使用队列优化的spfa求最短路O(m),最坏O(n*m)

    acwing851-spfa求最短路 #include<iostream> #include<cstring> #include<algorithm> #inclu ...

  7. spfa求次短路

    思路:先算出每个点到1的最短路d1[i],记录下路径,然后枚举最短路上的边 删掉之后再求一遍最短路,那么这时的最短路就可能是答案. 但是这个做法是错误的,可以被卡掉. 比如根据下面的例题生成的一个数据 ...

  8. BZOJ 1726 [Usaco2006 Nov]Roadblocks第二短路:双向spfa【次短路】

    题目链接:http://www.lydsy.com/JudgeOnline/problem.php?id=1726 题意: 给你一个无向图,求次短路. 题解: 两种方法. 方法一: 一遍spfa,在s ...

  9. acwing 849 Dijkstra求最短路 I 模板

    地址 https://www.acwing.com/problem/content/description/851/ 给定一个n个点m条边的有向图,图中可能存在重边和自环,所有边权均为正值. 请你求出 ...

随机推荐

  1. 【windows 操作系统】异步

    转载:https://cloud.tencent.com/developer/article/1744660 二.异步与多线程 1)基本概念 1. 并发:在操作系统中,是指一个时间段中有几个程序都处于 ...

  2. 文件上传漏洞之js验证

    0x00 前言 只有前端验证=没有验证 0x01 剔除JS 打开burpsuite,进入Proxy的Options,把Remove all JavaScript选上. 设置浏览器代理直接上传PHP木马 ...

  3. MongoDB数据库的下载安装及配置方法

    MongoDB安装与配置步骤 MongoDB数据库之安装篇 # 1 下载MongoDB数据库 1.打开浏览器,登录"https://www.mongodb.com/try/download/ ...

  4. RGBA()函数详解

    RGBA()函数详解 RGBA()函数用于设定颜色和颜色的透明度:

  5. JZ-012-数值的整数次方

    数值的整数次方 题目描述 给定一个double类型的浮点数base和int类型的整数exponent.求base的exponent次方. 保证base和exponent不同时为0. 题目链接: 数值的 ...

  6. Linux下安装mysql你又踩过多少坑【宇宙最全教程】

    一.检查以前是否安装过MySql 因为cnetos7一般默认安装mariadb,所以要检查mysql或者mariadb是否安装 rpm -pa | grep -i mysql rpm -pa | gr ...

  7. 矩池云 | 使用LightGBM来预测分子属性

    今天给大家介绍提升方法(Boosting), 提升算法是一种可以用来减小监督式学习中偏差的机器学习算法. 面对的问题是迈可·肯斯(Michael Kearns)提出的:一组"弱学习者&quo ...

  8. 【SQL登录问题】

    essay from:http://www.jb51.net/article/59352.htm 在与 SQL Server 建立连接时出现与网络相关的或特定于实例的错误.未找到或无法访问服务器 今早 ...

  9. CF1385G口胡

    只能说很神秘??? 首先观察题面,假设给出的第一个序列为 \(a\),第二个序列为 \(b\).对于 \((a_i,b_i)\) 我们连一条边. 得到的是一个 \(n\) 个点 \(n\) 条边的不一 ...

  10. LGP5386题解

    写在前面的废话 自己写了两天,调了半天,然后jzp来帮忙调了一个小时,终于过了 过的时候耳机里放着桐姥爷的bgm,就差哭出来了 题解 首先这题没有部分分差评( 值域不变 我们可以注意到,如果一个区间全 ...