跳房子

把每个可选落点产生的后退位置视为 BFS 转移,并用并查集删除已扫描落点,使所有区间转移近线性完成。

OJ: shumeng

题目 ID: CSP202412D

难度:普及+/提高-

标签:BFS并查集区间遍历最短路

日期: 2026-07-31 16:21

形式化题目

站在格子 ii 时,可以跳到区间 [i+1,min(n,i+ki)][i+1, \min(n, i+k_i)] 中的任意落点 jj;落到 jj 后立即后退 aja_j 格,下一次实际站在 jajj - a_j

求从格子 11 跳到格子 nn 的最少跳跃次数,不可达输出 1-1

思路

把每个状态 ii 到落点 jj 的跳跃看成转移 ijaji \to j - a_j,问题就是一个 BFS 最短路。难点在于避免重复扫描同一段区间内的落点。

朴素做法:直接枚举落点

先看直接 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;
}

同一落点 jj 可能被许多状态的跳跃区间重复覆盖,最坏 O(n2)O(n^2)

关键观察:落点只需处理一次

BFS 按距离从小到大出队。对于某个落点 jj,第一次有状态的跳跃区间覆盖它时,已经用到了能到达该状态的最小跳数;之后从更晚出队的状态使用同一个 jj 不可能得到更短路径。因此每个落点只处理一次即可。

用并查集跳过已处理的落点

维护一个并查集表示“下一个还未处理的落点”:

  • find(x) 返回不小于 xx 的最小未删除下标;
  • 处理完落点 jj 后删除它,令它指向 find(j+1)
  • 处理状态 ii 时,从 find(i+1) 开始逐个处理,直到超过 min(n,i+ki)\min(n, i + k_i)

处理落点 jj 时令 v=jajv = j - a_j;若 vv 未访问则入队并记录距离。注意删除的是落点下标而不是状态下标,所以多个落点后退到同一个状态也不会出错。

样例推演

下表记录样例一的 BFS 过程,落点只在第一次被扫描时出现:

出队状态 跳数 本次处理的落点及转移 新入队状态
1 0 2 -> 13 -> 2 2
2 1 4 -> 35 -> 46 -> 3 3, 4
3 2 7 -> 68 -> 8 6, 8
4 2 无(落点已删除)
6 3 9 -> 610 -> 10 10

状态 10 首次以距离 44 入队,答案为 44。状态 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 均摊近常数,总复杂度 O(nα(n))O(n \alpha(n))
  • 空间:O(n)O(n)

总结

区间转移题的难点不是 BFS 本身,而是避免重复扫描区间内的落点。BFS 提供“第一次覆盖落点时距离最优”的顺序保证,并查集把所有已处理落点跳过,从而把大量区间枚举压缩成一次近线性的删除过程。

图示解析

这张流程图概括每个 BFS 状态的处理方式:

text
出队状态 i
|- 确定落点区间 [i+1, min(n, i+k_i)]
   `- 并查集找到区间内尚未处理的 j
      |- 删除 j,转移到 j-a[j]
      `- 未访问状态入队

落点 jj 被处理后不会再次参与后续区间扫描。因为 BFS 出队距离单调不减,后面状态通过同一落点得到的路径不可能更短。