【洛谷 P2678】 [NOIP2015 提高组] 跳石头 超详细题解

一、题目分析

1. 题意简述

河道起点距离终点总长 L,起点终点中间有 N 块石头,最多移走 M 块石头(起点终点不能动)。移走石头后,选手跳跃时相邻两块石头的最小跳跃距离要尽可能大,求这个最大的最小距离。

样例输入:

25 5 2
2
11
14
17
21

石头位置数组: [0, 2, 11, 14, 17, 21, 25](0 是起点,25 是终点)

最多移走 2 块石头,最终答案是 4。

2. 题型核心:最大化最小值

这类 "最小的最大值 / 最大的最小值" 是二分答案经典题型:

  • 直接枚举所有方案复杂度太高,暴力无法通过大数据;
  • 答案存在明确的取值区间,且具有单调性:
    • 如果距离 x 是可行解(能移走 ≤ M 块石头满足所有跳跃 ≥ x),那么所有小于 x 的距离一定也可行;
    • 如果距离 x 不可行,所有大于 x 的距离一定不可行;
  • 单调性满足二分的使用条件,二分枚举答案,再写 check 函数验证可行性。

二、二分答案思路拆解

1. 二分区间确定

  • 左边界 l:最小跳跃距离下限为 0;
  • 右边界 r:最大跳跃距离上限为河道总长 L(极端情况只剩起点终点)。

2. 二分循环写法(开区间模板:(l+1<r))

while(l + 1 < r){
    int mid = l + r >> 1; // 等价于 (l + r) / 2,向下取整
    if(check(mid)) 
        l = mid; // mid可行,尝试更大的答案
    else 
        r = mid; // mid不可行,缩小上界
}
return l; // 循环结束l就是最大合法答案

循环终止条件: l 和 r 相邻,此时 l 是最后一个合法值,也就是我们要的最优解。

3. check 函数核心逻辑

函数传入一个假设的最短跳跃距离 q,判断:要让所有相邻石头间距 ≥ q,最少需要移走多少块石头。

变量定义:

  • last:上一块保留的石头下标,初始是起点 0;
  • cnt:需要移走的石头数量,初始 0;

遍历每一块石头(包含终点):

  • 如果当前石头和上一块保留石头的距离 (< q):这块石头必须移走,cnt++
  • 如果距离 (≥ q):这块石头保留,更新 last 为当前下标;

判定: 若需要移走的石头总数 (cnt ≤ M),说明 q 这个距离可以做到,返回 true;否则返回 false

样例验证(q=4):
数组:0, 2, 11, 14, 17, 21, 25

  • last=0i=12-0=2 <4 → 移走,cnt=1
  • i=211-0=11≥4 → 保留,last=2
  • i=314-11=3 <4 → 移走,cnt=2
  • i=417-11=6≥4 → 保留,last=4
  • i=521-17=4≥4 → 保留,last=5
  • i=625-21=4≥4 → 保留

总移走 cnt=2 ≤ M=2,q=4 合法。

如果尝试 q=5:最少需要移走 3 块石头,超过 M=2,不合法。因此 4 是最大合法值。

三、完整代码逐行讲解

#include <bits/stdc++.h>
using namespace std;

// 数组最大容量,题目N≤1e5,开1e5足够
const int N = 1e5;

// L:河道总长,n:石头数,m:最多移走数量,a存储石头位置
int n, m, L, a[N];

// 校验函数:假设最短跳跃距离为q,是否可行
bool check(int q){
    int cnt = 0;   // 需要移走的石头数量
    int last = 0;   // 上一块保留石头的下标(起点a[0]=0)
    
    // 遍历1~n+1,a[n+1]是终点L
    for(int i = 1; i <= n + 1; i++){
        // 间距不足q,移走当前石头
        if(a[i] - a[last] < q)
            cnt++;
        // 间距足够,保留当前石头,更新last
        else
            last = i;
    }
    
    // 需要移走的石头不超过上限m,则可行
    return cnt <= m;
}

// 二分主函数,寻找最大合法距离
int find(){
    int l = 0, r = 1e9; // 二分左右边界
    
    while(l + 1 < r){
        int mid = l + r >> 1; // 二分中点
        
        if(check(mid))
            l = mid; // mid可行,答案至少是mid,向右找更大值
        else
            r = mid; // mid不可行,答案一定小于mid,向左缩小范围
    }
    
    return l; // 循环结束l为最优解
}

int main(){
    // 输入总长L、石头数n、最多移走m
    cin >> L >> n >> m;
    
    // 读入n块石头的位置,存入a[1]~a[n]
    for(int i = 1; i <= n; i++){
        cin >> a[i];
    }
    
    // a[n+1]存放终点位置L
    a[n + 1] = L;
    
    // 二分求解并输出答案
    cout << find();
    
    return 0;
}

关键细节说明

  1. 数组下标设计

    • a[0] 隐式为起点 0,不需要存储;
    • a[1]~a[n] 是题目给出的中间石头;
    • a[n+1] 手动赋值为终点 L,遍历的时候一并判断终点,不用单独处理最后一段距离。
  2. 二分右边界 1e9
    题目 L 最大不会超过 (10^9),设置 1e9 覆盖所有输入范围,不会影响效率。

  3. 时间复杂度分析

    • 二分次数:最多 (log_2(10^9) ≈ 30) 次;
    • 单次 check 遍历 (N+1) 个石头,(N≤10^5);
    • 总复杂度:(O(30 * N)),(3*10^6) 运算量,完全通过数据范围。

四、二分答案模板总结(最大化最小值通用)

  1. 确定答案取值区间 ([l, r]);
  2. check(x) 函数,判断 x 是否满足题目约束;
  3. 二分循环:
while(l + 1 < r){
    int mid = l + r >> 1;
    if(check(mid)) l = mid;
    else r = mid;
}
cout << l;

适用题型特征:

  • 求 "最小的最大值"/"最大的最小值";
  • 答案具备单调性,可快速校验单个值是否合法。

五、易错点避坑

  1. 忘记把终点 L 存入数组 a[n+1],会漏掉最后一段跳跃距离,答案错误;
  2. check 函数中 last 初始值错设为 1,起点 0 丢失,计算间距全部出错;
  3. 二分循环写成 l <= r 的另一种模板,边界更新逻辑不匹配导致死循环或答案错误;
  4. 数组开太小,N 给 1e5 时数组容量不足,出现越界 RE;
  5. 搞反 check 逻辑:移走石头数量判断写成 cnt >= m,返回值颠倒。

六、样例模拟完整流程

输入: (L=25, n=5, m=2),石头位置 [2, 11, 14, 17, 21]

数组填充后: a[1]=2, a[2]=11, a[3]=14, a[4]=17, a[5]=21, a[6]=25

二分过程简要:

  • (l=0, r=1e9),mid=5e8,check 直接不合法,r 缩小;
  • 不断二分收敛到区间 (l=4, r=5),此时 (l+1<r) 成立,mid=4
  • check(4) 返回 true,更新 (l=4);
  • 现在 (l+1=r)(4+1=5),循环结束,返回 l=4,与样例输出一致。

posted on 2026-06-15 23:18  5iCode  阅读(0)  评论(0)    收藏  举报

导航