【洛谷 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=0,i=1,2-0=2 <4→ 移走,cnt=1i=2,11-0=11≥4→ 保留,last=2i=3,14-11=3 <4→ 移走,cnt=2i=4,17-11=6≥4→ 保留,last=4i=5,21-17=4≥4→ 保留,last=5i=6,25-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;
}
关键细节说明
-
数组下标设计
a[0]隐式为起点 0,不需要存储;a[1]~a[n]是题目给出的中间石头;a[n+1]手动赋值为终点 L,遍历的时候一并判断终点,不用单独处理最后一段距离。
-
二分右边界 1e9
题目 L 最大不会超过 (10^9),设置 1e9 覆盖所有输入范围,不会影响效率。 -
时间复杂度分析
- 二分次数:最多 (log_2(10^9) ≈ 30) 次;
- 单次 check 遍历 (N+1) 个石头,(N≤10^5);
- 总复杂度:(O(30 * N)),(3*10^6) 运算量,完全通过数据范围。
四、二分答案模板总结(最大化最小值通用)
- 确定答案取值区间 ([l, r]);
- 写
check(x)函数,判断 x 是否满足题目约束; - 二分循环:
while(l + 1 < r){
int mid = l + r >> 1;
if(check(mid)) l = mid;
else r = mid;
}
cout << l;
适用题型特征:
- 求 "最小的最大值"/"最大的最小值";
- 答案具备单调性,可快速校验单个值是否合法。
五、易错点避坑
- 忘记把终点 L 存入数组
a[n+1],会漏掉最后一段跳跃距离,答案错误; - check 函数中 last 初始值错设为 1,起点 0 丢失,计算间距全部出错;
- 二分循环写成
l <= r的另一种模板,边界更新逻辑不匹配导致死循环或答案错误; - 数组开太小,N 给 1e5 时数组容量不足,出现越界 RE;
- 搞反 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,与样例输出一致。
浙公网安备 33010602011771号