通过倍增预处理图上任意两点间是否存在长度为 2^k 的路径,转化为边权为 1 的最短路问题并用 Floyd 求解。
OJ: luogu
题目 ID: P1613
难度:普及+/提高-
标签:图论倍增最短路Floyd
创建: 2026-10-23 23:04
更新: 2026-10-23 23:24
形式化题目
给定一个包含
暴力解法
思路
暴力的直观思路是不做倍增假设,而是直接显式枚举真实路径长度并递推路径可达性:
设布尔数组
递推完成后,检查所有满足
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-23 23:13
* update_at: 2026-10-24 00:00
*/
// brute.cpp:小数据真实朴素暴力解
// 说明:为什么只适合小数据:需要显式枚举路径长度,长度上界一小才能跑。
// 本解法不依赖倍增性质,而是通过显式路径长度递推(reach[len][i][j] 表示从 i 到 j 是否存在长度恰为 len 的路径)。
// 在长度达到 LIMIT(这里取 64,覆盖 2^0, 2^1, ..., 2^6)后,检查所有 2^k 长度的点对,标记为 1 秒可达边。
// 最后在这些 1 秒边上做 BFS 求出 1 到 n 的最少秒数。
// 如果路径长度很大(如最优解需 2^31-1 步),显式枚举长度会直接 MLE/TLE,因此仅用于小规模数据对拍。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 65;
const int LIMIT = 64; // 显式枚举路径长度的上界
const int INF = 0x3f3f3f3f;
int n, m;
vector<int> adj[MAXN];
// reach[len][i][j]:从 i 到 j 是否存在一条长度恰好为 len 的有向路径
bool reach[LIMIT + 1][MAXN][MAXN];
// one_sec[i][j]:从 i 到 j 是否能用 1 秒(即恰好 2^k 步)直达
bool one_sec[MAXN][MAXN];
// dist[i]:从 1 到 i 的最少秒数
int dist_step[MAXN];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> n >> m)) return 0;
for (int i = 1; i <= m; ++i) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
reach[1][u][v] = true;
}
// 显式逐层递推路径长度:len 从 2 到 LIMIT
for (int len = 2; len <= LIMIT; ++len) {
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
if (!reach[len - 1][i][j]) continue;
for (int nxt : adj[j]) {
reach[len][i][nxt] = true;
}
}
}
}
// 检查所有恰好为 2^k (<= LIMIT) 的长度,构建 1 秒可达图
for (int p2 = 1; p2 <= LIMIT; p2 <<= 1) {
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
if (reach[p2][i][j]) {
one_sec[i][j] = true;
}
}
}
}
// 在 1 秒可达边上进行 BFS,求 1 到 n 的最少秒数
memset(dist_step, 0x3f, sizeof(dist_step));
queue<int> q;
dist_step[1] = 0;
q.push(1);
while (!q.empty()) {
int u = q.front();
q.pop();
if (u == n) break;
for (int v = 1; v <= n; ++v) {
if (one_sec[u][v] && dist_step[v] == INF) {
dist_step[v] = dist_step[u] + 1;
q.push(v);
}
}
}
cout << dist_step[n] << "\n";
return 0;
}复杂度
- 时间复杂度:显式枚举长度上限为
,每次递推遍历所有点对和出边,耗时 ;在 1 秒边上运行 BFS 耗时 。总时间复杂度为 。当 较小时为多项式级别,但由于题目最优解路径长度可达 ,要想覆盖更大距离, 必须取到指数级别(如 ),导致时间完全不可行。 - 空间复杂度:需要存储状态数组
,空间复杂度为 ,当 稍大时便会立即内存超限。
瓶颈
题目的最优解路径长度可以达到
正解
思路
本题的关键在于将问题解耦为两个独立阶段:倍增预处理与重构图上的最短路。
-
倍增预处理直达状态: 观察跑路器的特性:如果点
到点 存在一条长度恰好为 的路径,那么无论 多大,小 A 都可以用 秒直达。 长度为 的路径可以等分成两段长度为 的路径。 定义状态: (布尔型)表示从 到 是否存在一条长度恰好为 的路径。 - 基础状态:对于输入中的每条有向边
,置 。 - 状态转移:从小到大枚举
( ),并枚举中间转折点 以及起点 、终点 : - 一旦求得
,说明可以在 秒内从 到达 ,我们在重构图上将有向边 的耗时标为 秒。
- 基础状态:对于输入中的每条有向边
-
最短路求解最少耗时: 经过倍增处理后,所有只需
秒就能互达的点对之间都有了权值为 的边。 此时问题转化为求新图上从点 到点 的最短路。由于节点数极小( ),直接使用经典的 Floyd 算法即可在 时间内求出全源最短路,最终答案即为 。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-23 23:13
* update_at: 2026-10-23 23:13
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 65;
const int MAXK = 65;
const int INF = 0x3f3f3f3f;
ll n, m;
// f[k][i][j] 表示从点 i 到点 j 是否存在一条长度恰好为 2^k 的路径
bool f[MAXK][MAXN][MAXN];
// dis[i][j] 表示从 i 到 j 的最少秒数
int dis[MAXN][MAXN];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> n >> m)) return 0;
memset(dis, 0x3f, sizeof(dis));
for (int i = 1; i <= n; ++i) {
dis[i][i] = 0;
}
for (int i = 1; i <= m; ++i) {
int u, v;
cin >> u >> v;
f[0][u][v] = true;
dis[u][v] = 1; // 距离为 2^0 = 1 千米,耗时 1 秒
}
// 倍增:f[k][i][j] = exists p, f[k-1][i][p] && f[k-1][p][j]
// 最多 64 次方足以覆盖 2^64(远大于 2^31-1 和 n <= 50 时的实际需要)
for (int k = 1; k <= 64; ++k) {
for (int p = 1; p <= n; ++p) {
for (int i = 1; i <= n; ++i) {
if (!f[k - 1][i][p]) continue;
for (int j = 1; j <= n; ++j) {
if (f[k - 1][p][j]) {
f[k][i][j] = true;
dis[i][j] = 1; // 存在一条长为 2^k 的路径,耗时 1 秒
}
}
}
}
}
// 在新图上跑 Floyd 求 1 到 n 的最短路
for (int k = 1; k <= n; ++k) {
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
if (dis[i][k] + dis[k][j] < dis[i][j]) {
dis[i][j] = dis[i][k] + dis[k][j];
}
}
}
}
cout << dis[1][n] << "\n";
return 0;
}复杂度
- 时间复杂度:
- 倍增预处理阶段:
枚举至常数 ,三重循环遍历 ,耗时为 。当 时,运算次数约为 ,可以在数毫秒内完成。 - Floyd 最短路阶段:三重循环耗时为
。 - 总时间复杂度为
,远优于时限要求。
- 倍增预处理阶段:
- 空间复杂度:
- 布尔数组
占用内存不到 ,距离数组 极小,空间复杂度为 。
- 布尔数组
总结
本题是倍增与图论最短路的经典结合。核心启发在于:
- 幂次步长与倍增:面对以
为原子步长的问题,利用 的性质,借用 Floyd 的松弛结构直接做倍增传递闭包。 - 两阶段解耦:将看似复杂的连续跳转化归为两个标准问题——先用倍增找出所有“耗时为 1”的有向边建立新图,再在新图上求无权单源最短路。