最短路QWQ

算法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 n100 m ≤ 4500 m \le 4500 m4500,任意一条边的权值 w w w 是正整数且 1 ⩽ w ⩽ 1000 1 \leqslant w \leqslant 1000 1w1000

数据中可能存在重边。

问题解决

注意:可能有重边,需要对数据取最小值。
初始化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 1n100,保证 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),松弛操作的步骤如下:

  1. 检查当前路径:查看从源节点到节点 u 的最短路径估计值(记为 dist[u])和边 (u, v) 的权重(记为 weight(u, v))。
  2. 更新路径:如果从源节点到节点 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] = ∞
进行松弛操作
  1. 对于边 (A, B):
    • 计算:dist[B] = min(∞, 0 + 1) = 1
  2. 对于边 (A, C):
    • 计算:dist[C] = min(∞, 0 + 4) = 4
  3. 对于边 (B, C):
    • 计算:dist[C] = min(4, 1 + 2) = 3(更新 C 的值)
  4. 对于边 (B, D):
    • 计算:dist[D] = min(∞, 1 + 5) = 6
  5. 对于边 (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的缺陷

  1. 时间复杂度:Bellman-Ford 算法的时间复杂度为 O(VE),其中 V 是图中顶点的数量,E 是边的数量。对于大规模图,这个复杂度可能导致性能问题,特别是与 Dijkstra 算法相比,后者在非负权图中通常更高效(使用优先队列时时间复杂度为 O((V + E) log V))。

  2. 负权环:Bellman-Ford 算法能够检测负权环,但如果图中存在负权环,则无法找到有效的最短路径。在这种情况下,算法会继续更新路径估计值,导致结果不可靠。因此,使用 Bellman-Ford 算法时,需要在最后一轮松弛操作后检查是否还有更新,如果有,则说明图中存在负权环。

  3. 适用场景有限:虽然 Bellman-Ford 算法能够处理负权边,但在实际应用中,许多场景中图的边权是非负的。在这种情况下,使用 Dijkstra 算法会更为高效。

  4. 实现复杂性:相较于某些其他算法,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 n1,既是一条链
在这里插入图片描述
所以当访问边数大于 n − 1 n-1 n1 就说明存在 负环,因为走了重复的路

例题:【模板】负环

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 w0,则表示存在一条从 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 1n2×103 1 ≤ m ≤ 3 × 1 0 3 1 \leq m \leq 3 \times 10^3 1m3×103
  • 1 ≤ u , v ≤ n 1 \leq u, v \leq n 1u,vn − 1 0 4 ≤ w ≤ 1 0 4 -10^4 \leq w \leq 10^4 104w104
  • 1 ≤ T ≤ 10 1 \leq T \leq 10 1T10
提示

请注意, 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} xc1xc1y1xc2xc2y2xcmxcmym

的不等式组,求任意一组满足这个不等式组的解。

输入格式

第一行为两个正整数 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 xcxcy

输出格式

一行, n n n 个数,表示 x 1 , x 2 ⋯ x n x_1 , x_2 \cdots x_n x1,x2xn 的一组可行解,如果有多组解,请输出任意一组,无解请输出 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} x1x23x2x32x1x31

一种可行的方法是 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} 53=2335=2255=01

数据范围

对于 100 % 100\% 100% 的数据, 1 ≤ n , m ≤ 5 × 1 0 3 1\leq n,m \leq 5\times 10^3 1n,m5×103 − 1 0 4 ≤ y ≤ 1 0 4 -10^4\leq y\leq 10^4 104y104 1 ≤ c , c ′ ≤ n 1\leq c,c'\leq n 1c,cn 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 1n105

1 ≤ m ≤ 2 × 1 0 5 1 \leq m \leq 2\times 10^5 1m2×105

s = 1 s = 1 s=1

1 ≤ u i , v i ≤ n 1 \leq u_i, v_i\leq n 1ui,vin

0 ≤ w i ≤ 1 0 9 0 \leq w_i \leq 10 ^ 9 0wi109,

0 ≤ ∑ w i ≤ 1 0 9 0 \leq \sum w_i \leq 10 ^ 9 0wi109

代码

#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 1n, 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 1n 中的点是否在 s − t s-t st的最短路上出现
输出n行,第i行 Yes 或者 No 表示,第i号点是否在路上出现
1 ≤ n ≤ 1 0 6 1 \leq n \leq 10^6 1n106

洛谷上没有原题

解析

对于第 x x x号点( 1 − n 1-n 1n)来说,不难发现 如果 x到s的最短路长度 + x到t的最短路长度 = s到t的最短路长度 ,就说明x在 s − t s-t st的最短路上。可以是如果对每个点跑Dijkstra,时间复杂度为O(nmlogm),肯定会爆,有一个优化。

优化

  • x到s的最短路长度 = s到x的最短路长度
  • x到t的最短路长度 = t到x的最短路长度
    - 所以 只需要从头s和尾t各跑一次Dijkstra就可以了
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值