把每个可选落点产生的后退位置视为 BFS 转移,并用并查集删除已扫描落点,使所有区间转移近线性完成。
OJ: shumeng
题目 ID: CSP202412D
难度:普及+/提高-
标签:BFS并查集区间遍历最短路
日期: 2026-07-31 16:21
形式化题目
站在格子
求从格子
思路
把每个状态
朴素做法:直接枚举落点
先看直接 BFS:对每个出队状态枚举区间内所有落点,做转移。
cpp
/**
* 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-07-31 16:21
* update_at: 2026-08-17 22:39
*/
// brute.cpp:小数据暴力解,BFS 中直接枚举当前状态能跳到的所有落点,最坏 O(n^2)。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> a(n + 1);
vector<long long> k(n + 1);
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++) cin >> k[i];
vector<int> distance_to(n + 1, -1);
queue<int> states;
distance_to[1] = 0;
states.push(1);
while (!states.empty()) {
int current = states.front();
states.pop();
// 枚举区间内每个落点 j,落到 j 后退回 j-a[j]
long long right = min((long long)n, current + k[current]);
for (int landing = current + 1; landing <= right; landing++) {
int next_state = landing - a[landing];
if (distance_to[next_state] == -1) {
distance_to[next_state] = distance_to[current] + 1;
states.push(next_state);
}
}
}
cout << distance_to[n] << '\n';
return 0;
}同一落点
关键观察:落点只需处理一次
BFS 按距离从小到大出队。对于某个落点
用并查集跳过已处理的落点
维护一个并查集表示“下一个还未处理的落点”:
find(x)返回不小于的最小未删除下标; - 处理完落点
后删除它,令它指向 find(j+1); - 处理状态
时,从 find(i+1)开始逐个处理,直到超过。
处理落点
样例推演
下表记录样例一的 BFS 过程,落点只在第一次被扫描时出现:
| 出队状态 | 跳数 | 本次处理的落点及转移 | 新入队状态 |
|---|---|---|---|
| 1 | 0 | 2 -> 1,3 -> 2 |
2 |
| 2 | 1 | 4 -> 3,5 -> 4,6 -> 3 |
3, 4 |
| 3 | 2 | 7 -> 6,8 -> 8 |
6, 8 |
| 4 | 2 | 无(落点已删除) | 无 |
| 6 | 3 | 9 -> 6,10 -> 10 |
10 |
状态 10 首次以距离 8 后续即使再覆盖部分落点,也不会改变已经得到的答案。
代码
cpp
/**
* 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-07-31 16:21
* update_at: 2026-08-17 22:39
*/
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
int n;
int a[MAXN];
long long k[MAXN];
int distance_to[MAXN];
int next_unused[MAXN];
int find_next(int x) {
int root = x;
while (next_unused[root] != root) root = next_unused[root];
while (next_unused[x] != x) {
int next = next_unused[x];
next_unused[x] = root;
x = next;
}
return root;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++) cin >> k[i];
for (int i = 1; i <= n; i++) distance_to[i] = -1;
for (int i = 2; i <= n + 1; i++) next_unused[i] = i;
queue<int> states;
distance_to[1] = 0;
states.push(1);
while (!states.empty()) {
int current = states.front();
states.pop();
long long right = min((long long)n, current + k[current]);
int landing = find_next(current + 1);
while (landing <= right) {
// 落点 landing 只保留第一次被某个最短路状态扫描的机会。
next_unused[landing] = find_next(landing + 1);
int next_state = landing - a[landing];
if (distance_to[next_state] == -1) {
distance_to[next_state] = distance_to[current] + 1;
states.push(next_state);
}
landing = find_next(landing);
}
}
cout << distance_to[n] << '\n';
return 0;
}复杂度
- 时间:每个落点只被删除一次,并查集
find均摊近常数,总复杂度。 - 空间:
。
总结
区间转移题的难点不是 BFS 本身,而是避免重复扫描区间内的落点。BFS 提供“第一次覆盖落点时距离最优”的顺序保证,并查集把所有已处理落点跳过,从而把大量区间枚举压缩成一次近线性的删除过程。
图示解析
这张流程图概括每个 BFS 状态的处理方式:
text
出队状态 i
|- 确定落点区间 [i+1, min(n, i+k_i)]
`- 并查集找到区间内尚未处理的 j
|- 删除 j,转移到 j-a[j]
`- 未访问状态入队落点