GESP2026年3月认证C++八级( 第三部分编程题(2、子图最短路)精讲



第一部分 题目分析

1、看懂题目

(1)有一张无向带权图。

例如:

复制代码
1 --2-- 2 --3-- 3 --4-- 4
 \      |
   5    1
    \   |

        5

(2)图中一共有

复制代码
n 个点
m 条边

(3)现在不是只求一次最短路。

而是要求:

对于所有区间

复制代码
[l,r]

都建立一个子图。


(4)例如

复制代码
l=2
r=5

那么保留

复制代码
2
3
4
5

其它点全部删掉。


(5)然后要求:

这个子图里面

所有点对最短路之和。


(6)最后

所有区间全部加起来。


2、举个例子

(1)假设

复制代码
1
2
3

三点。


(2)边:

复制代码
1-2=1

2-3=2

(3)那么区间有:

复制代码
[1,1]

[1,2]

[1,3]

[2,2]

[2,3]

[3,3]

(4)每一个区间都要求

复制代码
所有点之间最短路

是不是非常多?


(5)如果暴力:

复制代码
O(n²)

每次跑 Floyd

O(n³)

总复杂度

O(n⁵)

显然不可能。需要更高效的做法。


第二部分 数据范围告诉我们什么?

1、题目里最关键的是:

复制代码
n≤100

为什么?

100 非常小。

说明:

可以允许

复制代码
100³

甚至

复制代码
100⁴

但是

复制代码
100⁵

就炸了。


2、所以我们要努力做到

复制代码
O(n⁴)

第三部分 最容易想到的方法

1、每个区间:

复制代码
跑一次 Floyd

区间数量:

复制代码

每次:

复制代码

总共:

复制代码
n⁵

2、这是第一种思路。

直接淘汰。


第四部分 Floyd到底干了什么?

1、有的同学不会观察 Floyd。

它实际上就是:

复制代码
一个一个加入中转点。

2、例如:

开始:

复制代码
没有中转点

然后

加入

复制代码
1

再加入

复制代码
2

再加入

复制代码
3

......


3、经典代码:

复制代码
for(k)

    for(i)

        for(j)

            dist=min(...)

其实就是:

当前允许经过

复制代码
1~k

这些点。


4、所以:

Floyd最大的特点:

它是

动态增加中转点。

这就是本题突破口。


第五部分 本题也是不断增加点

1、观察区间。

(1)固定左端点:

复制代码
l=3

(2)那么:

复制代码
[3,3]

↓

[3,4]

↓

[3,5]

↓

[3,6]

是不是每次:

只增加一个点?


(3)增加的是:

复制代码
r

例如:

复制代码
[3,5]

↓

[3,6]

只增加

复制代码
6

(4)是不是跟

复制代码
Floyd加入中转点

特别像?


第六部分 如何利用?

1、对于固定

复制代码
l

先复制一份最初距离:

复制代码
g=f;

2、为什么?

因为:

复制代码
f

永远保存原图。

复制代码
g

不断更新。

所以:

每换一个

复制代码
l

重新开始。


3、随后:

复制代码
r=l

r=l+1

r=l+2

一直扩大。

每扩大一次:

复制代码
新增一个点 r

那么:

只需要

复制代码
让 r 做一次 Floyd 中转点

即可。


4、代码就是:

复制代码
for(i)

    for(j)

        g[i][j]=

min(

g[i][j],

g[i][r]+g[r][j]

);

是不是跟 Floyd 一模一样?


5、区别只是:

普通 Floyd

复制代码
k=1

2

3

...

n

这里

复制代码
k=r

每次只增加一个。


第七部分 为什么这样就是对子图?

1、例如:

复制代码
l=4

(1)第一次:

复制代码
r=4

允许中转:

复制代码
4

(2)第二次:

复制代码
r=5

允许:

复制代码
4

5

(3)第三次:

复制代码
r=6

允许:

复制代码
4

5

6

2、是不是:

永远都是:

复制代码
[l,r]

里面的点?


3、没错。

所以:

求出来就是

复制代码
子图

[l,r]

最短路

第八部分 为什么不用前面的点?

1、因为:

复制代码
1

2

3

已经删掉了。

它们不存在。

自然不能作为中转点。


2、所以:

只加入

复制代码
r

即可。


第九部分 为什么复制 g=f?

1、看看参考代码:

复制代码
for(i)

    for(j)

        g[i][j]=f[i][j];

为什么?


2、假设:

复制代码
l=1

更新结束以后:

复制代码
g

已经包含很多信息。

下一次:

复制代码
l=2

如果继续用:

复制代码
g

里面还有:

复制代码
点1

参与过中转。

那就错了。


2、所以:

必须:

复制代码
重新复制。

第十部分 为什么只统计 i~r?

1、代码:

复制代码
for(i=l;i<=r;i++)

    for(j=i;j<=r;j++)

为什么?


2、因为:

题目只要求:

复制代码
区间里面。

而不是:

整个图。


3、另外:

由于

复制代码
dist(i,j)

=

dist(j,i)

无向图。


4、所以:

统计:

复制代码
i≤j

即可。

避免重复。


第十一部分 参考程序

cpp 复制代码
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 105;
const int mod = 1e9;
int n, m;
int f[N][N];
int g[N][N];
int ans;
int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++)
            f[i][j] = mod;
        f[i][i] = 0;
    }
    for (int i = 1; i <= m; i++) {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        f[u][v] = min(f[u][v], w);
        f[v][u] = min(f[v][u], w);
    }
    for (int l = 1; l <= n; l++) {
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++)
                g[i][j] = f[i][j];
        for (int r = l; r <= n; r++) {
            for (int i = 1; i <= n; i++)
                for (int j = 1; j <= n; j++)
                    g[i][j] = min(g[i][j], g[i][r] + g[r][j]);
            for (int i = l; i <= r; i++)
                for (int j = i; j <= r; j++)
                    ans = (ans + g[i][j]) % mod;
        }
    }
    printf("%d\n", ans);
    return 0;
}

1、初始化:

复制代码
f[i][j]=INF;

f[i][i]=0;

表示:

复制代码
没有边

无限大

自己到自己

0

2、然后:

读边:

复制代码
f[u][v]=min(...)

f[v][u]=min(...)

为什么

复制代码
min

因为:

有重边。

保留最短。


3、接着:

复制代码
for(l=1;l<=n;l++)

固定左端点。

复制:

复制代码
g=f;

然后:

复制代码
for(r=l;r<=n;r++)

不断扩大区间。

新增一个点:

复制代码
r

于是:

复制代码
for(i)

for(j)

g[i][j]=

min(

g[i][j],

g[i][r]+g[r][j]

);

4、最后:

复制代码
for(i=l;i<=r;i++)

for(j=i;j<=r;j++)

ans+=g[i][j];

统计答案。


5、整个算法时间复杂度:

复制代码
外层:

l

O(n)

×

r

O(n)

×

Floyd一次更新

O(n²)

=

O(n⁴)

当 (n=100) 时,大约是 (10^8) 级别操作,在题目的限制下是可以接受的。


第十二部分 本题考察什么?

1、这题真正考察的不是 Floyd 模板,而是:

能否发现 Floyd 的本质是"逐步加入中转点",再把这种思想迁移到区间子图中。


2、很多同学都会背:

复制代码
for(k)
    for(i)
        for(j)

3、真正优秀的同学会想到:

  • 加入一个点,就做一次松弛;

  • 区间右端点每次增加一个点,正好可以作为新的中转点;

  • 固定左端点、逐步扩大右端点,就能把总复杂度从 O(n⁵) 优化到 O(n⁴)


本节课总结

这一题最值得记住的不是代码,而是下面四句话:

① Floyd 的核心不是三层循环,而是"不断加入新的中转点"。

② 固定左端点,右端点每次增加一个点,就相当于新增一个中转点。

③ 每增加一个点,只需要执行一次 Floyd 的松弛更新,而不用重新计算全部最短路。

④ 学算法不要只记模板,要理解算法背后的思想,才能在新题目中灵活迁移。


相关推荐
十五年专注C++开发2 小时前
C++设计一个函数来求容器元素绝对值之和的平均值
c++·模板·accumulate
SilentSlot2 小时前
【C/C++】手写 DPDK 协议栈(十一):基于 PPS、SYN 比例和源 IP 熵的 DDoS 检测
c语言·c++·tcp/ip
王老师青少年编程3 小时前
csp信奥赛C++高频考点专项训练:【排序算法】案例9:成绩排序
c++·排序算法·csp·高频考点·信奥赛·成绩排序
程序喵大人4 小时前
【C++进阶】STL容器与迭代器 - 05 map 和 set 为什么按键保持有序
开发语言·c++·容器·迭代器·stl
库克克5 小时前
【C++】 unordered_map 与unordered_set
开发语言·c++
李迟6 小时前
一种轻量级C++ CSV文件读写库的实现方案
开发语言·c++
学无止境_永不停歇6 小时前
9. 缓冲区
linux·服务器·c++
古道青阳6 小时前
Duilib 技术全景剖析
c++·windows
Rabitebla7 小时前
C++ 内存管理全面复习:从内存分布到 operator new/delete
java·c语言·开发语言·c++·算法·leetcode