算法1 Floyd
状态
设立 f[k][i][j] 表示,从 i 到 j ,通过 k 的最短路径长度。
转移
有两种情况
- 不从k 经过,f[k][i][j] = f[k-1][i][j]
- 从k经过,f[k][i][j] = f[k-1][x][k] + f[k-1][k][y]
所以得到转移
f[k][i][j] = min ( f[k-1][i][j] , f[k-1][x][k] + f[k-1][k][y] )
不难发现,转移只会关系到上一个状态的信息,顾可以使用滚动优化。
f[i][j] = min(f[i][j] , f[i][k] + f[k][j])
DP解析
例题1:【模板】Floyd
Floyd 模板
题目描述
给出一张由 n n n 个点 m m m 条边组成的无向图。
求出所有点对 ( i , j ) (i,j) (i,j) 之间的最短路径。
输入格式
第一行为两个整数 n , m n,m n,m,分别代表点的个数和边的条数。
接下来 m m m 行,每行三个整数 u , v , w u,v,w u,v,w,代表 u , v u,v u,v 之间存在一条边权为 w w w 的边。
输出格式
输出 n n n 行每行 n n n 个整数。
第 i i i 行的第 j j j 个整数代表从 i i i 到 j j j 的最短路径。
样例 #1
样例输入 #1
4 4
1 2 1
2 3 1
3 4 1
4 1 1
样例输出 #1
0 1 2 1
1 0 1 2
2 1 0 1
1 2 1 0
提示
对于 100 % 100\% 100% 的数据, n ≤ 100 n \le 100 n≤100, m ≤ 4500 m \le 4500 m≤4500,任意一条边的权值 w w w 是正整数且 1 ⩽ w ⩽ 1000 1 \leqslant w \leqslant 1000 1⩽w⩽1000。
数据中可能存在重边。
问题解决
注意:可能有重边,需要对数据取最小值。
初始化f[i][j] = 0,因为自身到自身的距离是 0,其余赋成最大值。
代码
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
using ll = long long;
const ll N = 105;
ll f[N][N];
ll n,m;
int main()
{
cin >> n >> m;
for(ll i=1;i<=n;i++){
for(ll j=1;j<=n;j++){
if(i==j) f[i][j] = 0;
else f[i][j] = INT_MAX;
}
}
for(ll i=1;i<=m;i++){
ll u,v,w; cin >> u >> v >> w;
f[u][v] = f[v][u] = min(f[u][v],w); // 取最小值
}
for(ll k=1;k<=n;k++){ // 先枚举中间点 k
for(ll i=1;i<=n;i++){ // 枚举起点 i
for(ll j=1;j<=n;j++){// 枚举重点 j
f[i][j] = min(f[i][j],f[i][k]+f[k][j]);
}
}
}
for(ll i=1;i<=n;i++){
for(ll j=1;j<=n;j++){
cout << f[i][j] << ' ';
}
cout << endl;
}
return 0;
}
可达性最短路例题2:【模板】传递闭包
可达性最短路
题目描述
给定一张点数为 n n n 的有向图的邻接矩阵,图中不包含自环,求该有向图的传递闭包。
一张图的邻接矩阵定义为一个 n × n n\times n n×n 的矩阵 A = ( a i j ) n × n A=(a_{ij})_{n\times n} A=(aij)n×n,其中
a i j = { 1 , i 到 j 存在直接连边 0 , i 到 j 没有直接连边 a_{ij}=\left\{ \begin{aligned} 1,i\ 到\ j\ 存在直接连边\\ 0,i\ 到\ j\ 没有直接连边 \\ \end{aligned} \right. aij={1,i 到 j 存在直接连边0,i 到 j 没有直接连边
一张图的传递闭包定义为一个 n × n n\times n n×n 的矩阵 B = ( b i j ) n × n B=(b_{ij})_{n\times n} B=(bij)n×n,其中
b i j = { 1 , i 可以直接或间接到达 j 0 , i 无法直接或间接到达 j b_{ij}=\left\{ \begin{aligned} 1,i\ 可以直接或间接到达\ j\\ 0,i\ 无法直接或间接到达\ j\\ \end{aligned} \right. bij={1,i 可以直接或间接到达 j0,i 无法直接或间接到达 j
输入格式
输入数据共 n + 1 n+1 n+1 行。
第一行一个正整数 n n n。
第 2 2 2 到 n + 1 n+1 n+1 行每行 n n n 个整数,第 i + 1 i+1 i+1 行第 j j j 列的整数为 a i j a_{ij} aij。
输出格式
输出数据共 n n n 行。
第 1 1 1 到 n n n 行每行 n n n 个整数,第 i i i 行第 j j j 列的整数为 b i j b_{ij} bij。
样例 #1
样例输入 #1
4
0 0 0 1
1 0 0 0
0 0 0 1
0 1 0 0
样例输出 #1
1 1 0 1
1 1 0 1
1 1 0 1
1 1 0 1
提示
对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 100 1\le n\le 100 1≤n≤100,保证 a i j ∈ { 0 , 1 } a_{ij}\in\{0,1\} aij∈{0,1} 且 a i i = 0 a_{ii}=0 aii=0。
问题解决
对于中间点 k ,如果 i 可以到 k ,又有 k 可以到 j,那么 一定有 i 可以到 j
f[i][j] |= (f[i][k] && f[k][j])
代码
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
const int N = 1e2+5;
bool f[N][N]; // 默认为不可达
int n;
int main()
{
cin >> n;
for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) cin >> f[i][j];
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
f[i][j] |= (f[i][k] && f[k][j]);
// 转移
}
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
cout << f[i][j] << ' ';
}
cout << endl;
}
return 0;
}
Bellman–Ford 算法
Bellman–Ford 算法是一种基于松弛(relax)操作的最短路算法,可以求出有负权的图的最短路,并可以对最短路不存在的情况进行判断。
在国内 OI 界,你可能听说过的「SPFA」,就是 Bellman–Ford 算法的一种实现。
松弛操作
松弛操作的定义
松弛操作的目的是更新一个节点到另一个节点的最短路径估计值。具体来说,对于图中的每一条边 ( (u, v) ),如果从节点 ( u ) 到节点 ( v ) 的当前最短路径估计值大于从源节点到 ( u ) 的最短路径估计值加上边 ( (u, v) ) 的权重,则更新节点 ( v ) 的最短路径估计值。
松弛操作的具体过程
对于图中的每一条边 (u, v),松弛操作的步骤如下:
- 检查当前路径:查看从源节点到节点 u 的最短路径估计值(记为 dist[u])和边 (u, v) 的权重(记为 weight(u, v))。
- 更新路径:如果从源节点到节点 u 的最短路径加上边 (u, v) 的权重小于当前节点 v 的最短路径估计值(即 dist[v]),那么就更新 dist[v] 的值为 dist[u] + weight(u, v)。
换句话说,如果通过节点 u 到达节点 v 的路径比当前已知的从源节点到节点 v 的路径更短,就更新这个路径的长度。
举个例子
假设我们有一个图,包含以下边和权重:
- 边 (A, B),权重为 1
- 边 (A, C),权重为 4
- 边 (B, C),权重为 2
- 边 (B, D),权重为 5
- 边 (C, D),权重为 1
初始时,我们从节点 A 开始,节点的最短路径估计值如下:
- dist[A] = 0 (源节点到自身的距离为 0)
- dist[B] = ∞
- dist[C] = ∞
- dist[D] = ∞
进行松弛操作
- 对于边 (A, B):
- 计算:dist[B] = min(∞, 0 + 1) = 1
- 对于边 (A, C):
- 计算:dist[C] = min(∞, 0 + 4) = 4
- 对于边 (B, C):
- 计算:dist[C] = min(4, 1 + 2) = 3(更新 C 的值)
- 对于边 (B, D):
- 计算:dist[D] = min(∞, 1 + 5) = 6
- 对于边 (C, D):
- 计算:dist[D] = min(6, 3 + 1) = 4(更新 D 的值)
最终结果
经过这些松弛操作后,各节点的最短路径估计值为:
- dist[A] = 0
- dist[B] = 1
- dist[C] = 3
- dist[D] = 4
通过松弛操作,Bellman-Ford 算法能够逐步更新最短路径的估计值,直到找到从源节点到所有其他节点的最短路径。
Bellman–Ford的缺陷
-
时间复杂度:Bellman-Ford 算法的时间复杂度为 O(VE),其中 V 是图中顶点的数量,E 是边的数量。对于大规模图,这个复杂度可能导致性能问题,特别是与 Dijkstra 算法相比,后者在非负权图中通常更高效(使用优先队列时时间复杂度为 O((V + E) log V))。
-
负权环:Bellman-Ford 算法能够检测负权环,但如果图中存在负权环,则无法找到有效的最短路径。在这种情况下,算法会继续更新路径估计值,导致结果不可靠。因此,使用 Bellman-Ford 算法时,需要在最后一轮松弛操作后检查是否还有更新,如果有,则说明图中存在负权环。
-
适用场景有限:虽然 Bellman-Ford 算法能够处理负权边,但在实际应用中,许多场景中图的边权是非负的。在这种情况下,使用 Dijkstra 算法会更为高效。
-
实现复杂性:相较于某些其他算法,Bellman-Ford 的实现可能稍显复杂,尤其是在需要处理负权环检测时,需要额外的逻辑来确保结果的正确性。
所以可以使用队列优化
这就是SPFA!
SPFA 算法
算法流程
- 定义一个队列,将起点入队,定义一个
d
i
s
dis
dis 数组,
d
i
s
[
i
]
dis[i]
dis[i] 记录从起点到第
i
i
i 个点的最短距离
定义数组 c n t cnt cnt , c n t [ i ] cnt[i] cnt[i] 表示 从起点到 i i i 最短路经过的边数。 - 从队首取出一个点 u u u,遍历与这个点相邻的点 v v v,也同样取出 u u u 到 v v v 之间的边权 w w w
- 如果 从 u u u 点 间接 到达 v v v 点 的最短距离 小于直接到达 v v v 的距离 ,那么更新 d i s dis dis 和 c n t cnt cnt (即为进行松弛操作)
- 重复上述步骤直达队列为空
如何判断 负环
当存在负环时,会发现所谓的最短路径长度为 负无穷,会一直在 1 2 3 这几个点中循环,入下图:
在最坏的情况下,访问到最后一个节点,不走重复的路,所经过的边个数为
n
−
1
n-1
n−1,既是一条链
所以当访问边数大于
n
−
1
n-1
n−1 就说明存在 负环,因为走了重复的路
例题:【模板】负环
SPFA求负环模板
题目描述
给定一个 n n n 个点的有向图,请求出图中是否存在从顶点 1 1 1 出发能到达的负环。
负环的定义是:一条边权之和为负数的回路。
输入格式
本题单测试点有多组测试数据。
输入的第一行是一个整数 T T T,表示测试数据的组数。对于每组数据的格式如下:
第一行有两个整数,分别表示图的点数 n n n 和接下来给出边信息的条数 m m m。
接下来 m m m 行,每行三个整数 u , v , w u, v, w u,v,w。
- 若 w ≥ 0 w \geq 0 w≥0,则表示存在一条从 u u u 至 v v v 边权为 w w w 的边,还存在一条从 v v v 至 u u u 边权为 w w w 的边。
- 若 w < 0 w < 0 w<0,则只表示存在一条从 u u u 至 v v v 边权为 w w w 的边。
输出格式
对于每组数据,输出一行一个字符串,若所求负环存在,则输出 YES
,否则输出 NO
。
样例 #1
样例输入 #1
2
3 4
1 2 2
1 3 4
2 3 1
3 1 -3
3 3
1 2 3
2 3 4
3 1 -8
样例输出 #1
NO
YES
提示
数据规模与约定
对于全部的测试点,保证:
- 1 ≤ n ≤ 2 × 1 0 3 1 \leq n \leq 2 \times 10^3 1≤n≤2×103, 1 ≤ m ≤ 3 × 1 0 3 1 \leq m \leq 3 \times 10^3 1≤m≤3×103。
- 1 ≤ u , v ≤ n 1 \leq u, v \leq n 1≤u,v≤n, − 1 0 4 ≤ w ≤ 1 0 4 -10^4 \leq w \leq 10^4 −104≤w≤104。
- 1 ≤ T ≤ 10 1 \leq T \leq 10 1≤T≤10。
提示
请注意, m m m 不是图的边数。
代码
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
using ll = long long;
const int N = 2e3+5;
int n,m;
vector<pair<int,int>> ed[N];
int dis[N],cnt[N];
bool vis[N];
bool spfa(int last)
{
queue<int> q;
dis[1] = 0,vis[1] = true;
q.push(1);
while(!q.empty()){
int u = q.front(); q.pop();
vis[u] = false;
for(auto x:ed[u]){
int v = x.first,w = x.second;
if(dis[v] > dis[u] + w){
dis[v] = dis[u] + w; // 更新答案
cnt[v] = cnt[u] + 1; // 记录边数
if(cnt[v] >= last) return true; // 如果访问的边数大于等于 last
if(!vis[v]) q.push(v),vis[v] = true; // 如果已在队列中将不再入队
}
}
}
return false;
}
void clear() // 多组数据 需要清空
{
for(int i=1;i<=N;i++) ed[i].clear();
memset(dis,0x3f,sizeof(dis));
memset(vis,false,sizeof(vis));
memset(cnt,0,sizeof(cnt));
}
int main()
{
int t; cin >> t;
while(t--){
clear();
cin >> n >> m;
for(int i=1;i<=m;i++){
int u,v,w; cin >> u >> v >> w;
ed[u].push_back(make_pair(v,w));
if(w>=0) ed[v].push_back(make_pair(u,w)); // 题目要求
}
if(spfa(n)) cout << "YES" << endl;
else cout << "NO" << endl;
}
return 0;
}
【模板】差分约束
差分约束模板
题目描述
给出一组包含 m m m 个不等式,有 n n n 个未知数的形如:
{ x c 1 − x c 1 ′ ≤ y 1 x c 2 − x c 2 ′ ≤ y 2 ⋯ x c m − x c m ′ ≤ y m \begin{cases} x_{c_1}-x_{c'_1}\leq y_1 \\x_{c_2}-x_{c'_2} \leq y_2 \\ \cdots\\ x_{c_m} - x_{c'_m}\leq y_m\end{cases} ⎩ ⎨ ⎧xc1−xc1′≤y1xc2−xc2′≤y2⋯xcm−xcm′≤ym
的不等式组,求任意一组满足这个不等式组的解。
输入格式
第一行为两个正整数 n , m n,m n,m,代表未知数的数量和不等式的数量。
接下来 m m m 行,每行包含三个整数 c , c ′ , y c,c',y c,c′,y,代表一个不等式 x c − x c ′ ≤ y x_c-x_{c'}\leq y xc−xc′≤y。
输出格式
一行,
n
n
n 个数,表示
x
1
,
x
2
⋯
x
n
x_1 , x_2 \cdots x_n
x1,x2⋯xn 的一组可行解,如果有多组解,请输出任意一组,无解请输出 NO
。
样例 #1
样例输入 #1
3 3
1 2 3
2 3 -2
1 3 1
样例输出 #1
5 3 5
提示
样例解释
{ x 1 − x 2 ≤ 3 x 2 − x 3 ≤ − 2 x 1 − x 3 ≤ 1 \begin{cases}x_1-x_2\leq 3 \\ x_2 - x_3 \leq -2 \\ x_1 - x_3 \leq 1 \end{cases} ⎩ ⎨ ⎧x1−x2≤3x2−x3≤−2x1−x3≤1
一种可行的方法是 x 1 = 5 , x 2 = 3 , x 3 = 5 x_1 = 5, x_2 = 3, x_3 = 5 x1=5,x2=3,x3=5。
{ 5 − 3 = 2 ≤ 3 3 − 5 = − 2 ≤ − 2 5 − 5 = 0 ≤ 1 \begin{cases}5-3 = 2\leq 3 \\ 3 - 5 = -2 \leq -2 \\ 5 - 5 = 0\leq 1 \end{cases} ⎩ ⎨ ⎧5−3=2≤33−5=−2≤−25−5=0≤1
数据范围
对于 100 % 100\% 100% 的数据, 1 ≤ n , m ≤ 5 × 1 0 3 1\leq n,m \leq 5\times 10^3 1≤n,m≤5×103, − 1 0 4 ≤ y ≤ 1 0 4 -10^4\leq y\leq 10^4 −104≤y≤104, 1 ≤ c , c ′ ≤ n 1\leq c,c'\leq n 1≤c,c′≤n, c ≠ c ′ c \neq c' c=c′。
评分策略
你的答案符合该不等式组即可得分,请确保你的答案中的数据在 int
范围内。
如果并没有答案,而你的程序给出了答案,SPJ 会给出 There is no answer, but you gave it
,结果为 WA;
如果并没有答案,而你的程序输出了 NO
,SPJ 会给出 No answer
,结果为 AC;
如果存在答案,而你的答案错误,SPJ 会给出 Wrong answer
,结果为 WA;
如果存在答案,且你的答案正确,SPJ 会给出 The answer is correct
,结果
为 AC。
问题解决
对不等式的变形,会发现,这就相当于松弛操作的相反操作,顾可以根据给定 X 构图,跑最长路,并且在存入边权的时候,要存 − w -w −w
代码
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
using ll = long long;
const int N = 5e3+5;
vector<pair<int,int>> ed[N];
int cnt[N],dis[N];
bool vis[N];
queue<int> q;
bool spfa(int s)
{
memset(dis,-1,sizeof(dis));
dis[s] = 0,vis[s] = true;
q.push(s);
while(!q.empty()){
int u = q.front();
q.pop();vis[u] = false;
for(auto x:ed[u]){
int v = x.first,w = x.second;
if(dis[v] < dis[u] + w){
dis[v] = dis[u] + w;
cnt[v] = cnt[u] + 1;
if(cnt[v] > s) return false;
if(!vis[v]) q.push(v),vis[v] = true;
}
}
}
return true;
}
int main()
{
int n,m; cin >> n >> m;
for(int i=1;i<=m;i++){
int u,v,w; cin >> u >> v >> w;
ed[u].push_back({v,-w});
}
for(int i=1;i<=n;i++) ed[n+1].push_back({i,0});
if(!spfa(n+1)) cout << "NO";
else{
for(int i=1;i<=n;i++){
cout << dis[i] << ' ';
}
}
return 0;
}
SPFA的弊端
不难发现,当在毒瘤数据的测试下会把SPFA卡在O(n*m) 所以如果没有负边权,就用Dijkstra
Dijkstra 算法
是一种求解 非负权图 上单源最短路径的算法。
算法引入
如果已知下方非负权图,问可以确定从 1号点 到 哪个点 的最短路
答案是3号点,
d
i
s
[
3
]
=
1
dis[3] = 1
dis[3]=1
因为 3 是与 1 连边中边权最小的
证明
假设有一个点
x
x
x,与
1
1
1 号点的距离为 x 然后与3号点有连边,如下图
假设通过x号点再到3号点是最短的,那么就有
x
+
y
<
1
x + y < 1
x+y<1 将 y 假设成 0
所以
x
<
1
x < 1
x<1 因为 1 是与 x 相连边中的最小的,顾条件矛盾,不存在 x号点 再到 3号点 是比直接到3号点短的
算法核心
选择与 当前点 相邻 且 边权最小 的点拓展。所以 可以使用 优先队列 维护 到起点距离最小的。
有红点和蓝点,红点表示已经确定最短路的点,而蓝点没有
算法流程
初始化
dis[i] 表示 起始点到 i 号点的最短距离
定义 vis数组,vis[i] = 1表示 i 号节点是红点
最初所有点均为蓝点,vis[i] = 0
dis[1] = 0
dis[i] = +∞
流程
- 首先,将起点入队
- 如果队列不为空则执行一下操作
- 取出队首元素 u u u,将队首元素 u u u变为红点后,将 u u u 弹出。
- 遍历 u u u 的所有与之相连的 点 v v v ,如果 v v v 可以进行松弛操作,那么进行松弛操作,并将 v v v 入队。
Dijkstra例题:【模板】单源最短路径(标准版)
单源最短路径
题目描述
给定一个 n n n 个点, m m m 条有向边的带非负权图,请你计算从 s s s 出发,到每个点的距离。
数据保证你能从 s s s 出发到任意点。
输入格式
第一行为三个正整数
n
,
m
,
s
n, m, s
n,m,s。
第二行起
m
m
m 行,每行三个非负整数
u
i
,
v
i
,
w
i
u_i, v_i, w_i
ui,vi,wi,表示从
u
i
u_i
ui 到
v
i
v_i
vi 有一条权值为
w
i
w_i
wi 的有向边。
输出格式
输出一行 n n n 个空格分隔的非负整数,表示 s s s 到每个点的距离。
样例 #1
样例输入 #1
4 6 1
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4
样例输出 #1
0 2 4 3
提示
样例解释请参考 数据随机的模板题。
1 ≤ n ≤ 1 0 5 1 \leq n \leq 10^5 1≤n≤105;
1 ≤ m ≤ 2 × 1 0 5 1 \leq m \leq 2\times 10^5 1≤m≤2×105;
s = 1 s = 1 s=1;
1 ≤ u i , v i ≤ n 1 \leq u_i, v_i\leq n 1≤ui,vi≤n;
0 ≤ w i ≤ 1 0 9 0 \leq w_i \leq 10 ^ 9 0≤wi≤109,
0 ≤ ∑ w i ≤ 1 0 9 0 \leq \sum w_i \leq 10 ^ 9 0≤∑wi≤109。
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
vector<pair<int,int>> ed[N];
int n,m,s;
int dis[N],vis[N];
void dijk(int s)
{
memset(dis,0x3f,sizeof(dis));
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>> q;
q.push({0,s});
dis[s] = 0;
while(!q.empty()){
int u = q.top().second;
q.pop();
if(vis[u]) continue;
vis[u] = 1; // 将队首的元素设置为红点
for(auto x:ed[u]){
int v = x.first,w = x.second;
if(dis[v] > dis[u] + w){
dis[v] = dis[u] + w;
q.push({dis[v],v});
}
}
}
}
int main()
{
cin >> n >> m >> s;
for(int i=1;i<=m;i++){
int u,v,w; cin >> u >> v >> w;
ed[u].push_back({v,w});
}
dijk(s);
for(int i=1;i<=n;i++){
cout << dis[i] << ' ';
}
return 0;
}
稀世珍宝
题目描述:
给定一个无向图,有
n
n
n 个点,编号为
1
−
n
1-n
1−n,
m
m
m 条边,接下来,
m
m
m 行数据,每行一个三元组
u
u
u
v
v
v
w
w
w 代表从
u
u
u 到
v
v
v 有一条边权为
w
w
w 的连边 ,给定 起点
s
s
s 终点
t
t
t , 问
1
−
n
1-n
1−n 中的点是否在
s
−
t
s-t
s−t的最短路上出现
输出n行,第i行 Yes 或者 No 表示,第i号点是否在路上出现
1
≤
n
≤
1
0
6
1 \leq n \leq 10^6
1≤n≤106;
洛谷上没有原题
解析
对于第 x x x号点( 1 − n 1-n 1−n)来说,不难发现 如果 x到s的最短路长度 + x到t的最短路长度 = s到t的最短路长度 ,就说明x在 s − t s-t s−t的最短路上。可以是如果对每个点跑Dijkstra,时间复杂度为O(nmlogm),肯定会爆,有一个优化。
优化
- x到s的最短路长度 = s到x的最短路长度
- x到t的最短路长度 = t到x的最短路长度
- 所以 只需要从头s和尾t各跑一次Dijkstra就可以了