引言
刷题的同学一定见过这类"套路题":给你一张图,你手里还有几张"免费通行券",用掉一张就能让某一条路不花代价地走过去,问从起点到终点最少要多少代价。
这种"还能免费用 k 次"的限制,正是 分层图最短路(Layered Graph Shortest Path) 的典型场景。它把"已经用过几张券"变成一个状态维度,套在普通最短路外面,是蓝桥杯、全国青少年信息素养大赛算法赛项里反复出现的高频模型。本文用一道原创题带你吃透它,并给出 C++ / Python 双版参考解。
赛事锚点:第十八届蓝桥杯软件赛报名已开启(2026-10-08 10:00---12-18 14:00,省赛 2027-04-10,C/C++·Python 程序设计 9:00--13:00);中国电子学会 2026 年 12 月软件编程(C/C++ 一级至十级、Python 一至六级)考级报名 9/23--12/2、统考 12-12;全国青少年信息素养大赛 2026---2027 赛季算法应用主题赛同样覆盖图论最短路。

题目 / 项目目标
【校园科技节摆渡车】
校园里有 n 个站点、m 条双向道路,每条道路有一个通行时间 w。你手上有 k 张"瞬移券",使用一张可以让任意一条 道路的通行时间变为 0(相当于免费走一次)。每张券只能用一次,且全局共享这 k 张。
求从 1 号站点到 n 号站点的最短通行时间 ;如果怎么走都到不了,输出 -1。
输入格式
-
第一行
n m k(站点数、道路数、瞬移券数量) -
接下来
m行,每行u v w(站点 u、v 之间有一条双向路,通行时间 w)
输出格式
- 一个整数:最短通行时间;不可达输出
-1
样例输入
4 5 1
1 2 2
2 3 3
3 4 2
1 3 10
2 4 10
样例输出
2
样例说明 :用 1 张券免掉道路 1--3 的代价(原 10 → 0),走 1→(免费)3→4 = 0 + 2 = 2,比不用券的 1→2→3→4 = 7 更优。
核心考点
- 最短路算法(Dijkstra):边权非负,用堆优化 Dijkstra 即可。
- 状态维度建模(分层图的灵魂) :把"已用券数"变成额外一维。原图复制
k+1份,第i层表示"已经用了i张券"。 - 层内边 vs 跨层边:
- 层内边:在某一层里照原图正常连,边权不变;
- 跨层边:从第
i层走到第i+1层时,把这条边"免费"连过去,边权为0。 - 终点取各层最小值 :不一定要用满
k张券,所以终点是k+1层里 n 号点的最小值。
解法 / 拆解
第一步:把图"竖着"复制 k+1 层
设原图有 n 个点。我们构造一个新图,点编号写作 u + layer * n,其中:
-
u是原站点编号(0-based,方便计算); -
layer是"已用券数",取值0, 1, ..., k,共k+1层。
新图总点数 = (k+1) * n。
第二步:连两种边
对原图的每条边 (u, v, w):
- 层内边 (每一层都连,保持正常通行):
第 layer 层:u+layer*n ←→ v+layer*n 权值 w - 跨层边 (用掉一张券,条件 layer < k):
u+layer*n → v+(layer+1)*n 权值 0 v+layer*n → u+(layer+1)*n 权值 0
跨层边权是0,代表"这条边被免单";因为原图无向,免费边也要双向连。
第三步:跑一次 Dijkstra,终点取最小值
- 起点:第 0 层的 1 号点,即节点
0(0-based 的 1 号)。 - 终点候选:第
layer层的 n 号点,即n-1 + layer*n,对layer = 0..k取dist最小。 - 边权非负,直接堆优化 Dijkstra,时间复杂度
O((k·n + k·m) log(k·n))。
等价写法:distuc 状态数组(更省事)
很多同学觉得"复制 k+1 层"占空间,其实本质就是二维状态 dist[u][c] = 到达站点 u、已用 c 张券的最短路。松弛时:
-
普通走:
dist[v][c] = min(dist[v][c], dist[u][c] + w) -
免费走:
dist[v][c+1] = min(dist[v][c+1], dist[u][c] + 0)(需c < k)
下面两份代码都采用"节点展开"的写法,最直观、最不容易写错。
C++ 参考代码
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<ll, int> pli;
const ll INF = 1e18;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m, k;
cin >> n >> m >> k;
// 新图:共 (k+1) 层,每层 n 个点
vector<vector<pair<int, ll>>> g((k + 1) * n);
for (int i = 0; i < m; i++) {
int u, v; ll w;
cin >> u >> v >> w;
u--; v--; // 转成 0-based
// 1) 层内边:每一层都照原图连
for (int layer = 0; layer <= k; layer++) {
int a = u + layer * n, b = v + layer * n;
g[a].push_back({b, w});
g[b].push_back({a, w});
}
// 2) 跨层边:用掉一张券,把这条边免单(双向)
for (int layer = 0; layer < k; layer++) {
g[u + layer * n].push_back({v + (layer + 1) * n, 0});
g[v + layer * n].push_back({u + (layer + 1) * n, 0});
}
}
// 3) Dijkstra,起点 = 第 0 层的 1 号点
vector<ll> dist((k + 1) * n, INF);
priority_queue<pli, vector<pli>, greater<pli>> pq;
dist[0] = 0;
pq.push({0, 0});
while (!pq.empty()) {
auto [d, u] = pq.top(); pq.pop();
if (d > dist[u]) continue; // 过期堆元素
for (auto [v, w] : g[u]) {
if (dist[v] > d + w) {
dist[v] = d + w;
pq.push({dist[v], v});
}
}
}
// 4) 终点:取所有层 n 号点的最小值
ll ans = INF;
for (int layer = 0; layer <= k; layer++)
ans = min(ans, dist[n - 1 + layer * n]);
cout << (ans == INF ? -1 : ans) << "\n";
return 0;
}
Python 参考代码
python
import heapq
def solve():
n, m, k = map(int, input().split())
g = [[] for _ in range((k + 1) * n)]
for _ in range(m):
u, v, w = input().split()
u, v, w = int(u) - 1, int(v) - 1, int(w)
# 1) 层内边
for layer in range(k + 1):
a = u + layer * n
b = v + layer * n
g[a].append((b, w))
g[b].append((a, w))
# 2) 跨层边(免费,双向)
for layer in range(k):
g[u + layer * n].append((v + (layer + 1) * n, 0))
g[v + layer * n].append((u + (layer + 1) * n, 0))
# 3) Dijkstra
INF = 10 ** 18
dist = [INF] * ((k + 1) * n)
dist[0] = 0
pq = [(0, 0)]
while pq:
d, u = heapq.heappop(pq)
if d > dist[u]:
continue
for v, w in g[u]:
if dist[v] > d + w:
dist[v] = d + w
heapq.heappush(pq, (dist[v], v))
# 4) 终点取各层最小值
ans = min(dist[n - 1 + layer * n] for layer in range(k + 1))
print(-1 if ans == INF else ans)
if __name__ == "__main__":
solve()
时间与空间复杂度
- 时间复杂度 :新图点数
O(k·n),边数O(k·m)(层内)+O(k·m)(跨层)=O(k·m)。Dijkstra 用优先队列,整体O((k·n + k·m) log(k·n))。 - 空间复杂度 :邻接表
O(k·m),距离数组O(k·n)。 - 当
k较小(如 ≤ 20)、n, m在10^5级别时,本写法完全够用。若k很大(比如k = n),需考虑dist[u][c]状态 DP + 更精细的剪枝,或改用其他建模。
易错点(七个)
- 层数开错 :必须建
k+1层(0 到 k),只建k层会丢失"恰好用满 k 张券"的状态。 - 跨层边漏反向 :原图是无向边,免费跨层边也要双向连(从 u 到 v 的下一层、从 v 到 u 的下一层),只连单向会 WA。
- 跨层边权填错 :免费 = 权值
0,不是原权w,更不是-w或w取反。 - 终点只取一层 :要用满 k 张券才最优?不一定!必须取所有
k+1层中 n 号点的dist最小值。 - 下标与越界 :节点编号是
u + layer*n,数组大小要开(k+1)*n;u务必先转成 0-based,别混着 1-based 用。 - 溢出与不可达 :代价累加用
long long(C++)/ 大整数(Python);终点若全是INF要输出-1,不能默认输出 0。 - 券是全局共享的 k 次:不是"每条边都能免费一次"!券数量固定为 k,跨层走一次消耗一张,这是分层图的核心设定,理解错模型就全错。
进阶
- 瞬移任意两点("坐飞机"模型) :若券的效果是"瞬间移动到任意站点",那么跨层时不是对某条边免单,而是在第
i层任意两点间连权 0 的边(注意这样边数会爆炸,通常需要结合"最短路预处理 + 仅从当前点连向全局最优点"等技巧)。 - 反转边方向 k 次:把"免费"换成"把一条有向边反向且不花代价",跨层边改成翻转方向即可,建模完全同构。
- 与 0-1 BFS 的关系 :当边权只有
0和1时,可用双端队列的 0-1 BFS 替代 Dijkstra,降到O(V+E);分层图是更通用的框架。 - 与"次短路"的区别:次短路求的是"第二短",分层图求的是"带 k 次特殊操作的最短",两者建模不同,别混。
- 状态压缩优化 :当
k很大但"最优解几乎不会用满"时,可只保留dist[u]与其"用券数最少的那个状态"(即每个点只保留当前最优的用券数),能省不少空间。
小结与互动
分层图最短路的本质,就是给普通最短路加一个"操作次数"状态维 ,把"能不能免单"变成图上的一层一层。记住三句话:层内照常走、跨层权为 0、终点取各层最小,这类题基本就稳了。
这道题你在蓝桥杯/信息素养大赛里遇到过哪些"免费通行"变形?欢迎在评论区留言你见过的原题,咱们一起拆解建模思路。觉得有用就点个赞 + 收藏,关注我,每天一道信奥真题拆解。
📚 免费少儿编程资料(夸克网盘领取)
以下资料来自夸克网盘分享,点击链接可直接保存;若需在 App 内打开,也可复制下方明文链接:
- 全国青少年信息素养大赛复赛集训题目Python&C++.docx
https://pan.quark.cn/s/93995d3cb150 - 2024信息素养-智能算法应用挑战赛-复赛初中组题目7月7日.pdf
https://pan.quark.cn/s/da97b5dbf75d - Python背记手册.pdf
https://pan.quark.cn/s/7568ae9ca92b - Python课程
https://pan.quark.cn/s/a94bf02d00c6 - 2024信息素养大赛图形化复赛集训题答案3-9
https://pan.quark.cn/s/6ccab7ec3cbc - 2025年03月份电子学会考级真题
https://pan.quark.cn/s/4403c4228912 - 2025全国青少年信息素养大赛赛项说明
https://pan.quark.cn/s/d9d0df4a9f29 - 青少儿信息素养大赛编程资料
https://pan.quark.cn/s/4ab6bd83be8a
资料持续更新,关注获取最新分享。