1. 题目背景与核心需求解析
洛谷P2678跳石头是NOIP2015提高组的经典题目,考察选手对二分答案算法的理解和应用能力。题目场景设定为:在一条长度为L的河道中分布着N块石头(不包括起点和终点),选手需要从起点出发,通过移除最多M块石头,使得所有相邻石头间的最小距离最大化。
1.1 问题建模要点
这道题本质上是一个典型的"最小值最大化"问题,属于二分答案算法的标准应用场景。我们需要找到一个最大的最小跳跃距离d,使得在移除不超过M块石头的情况下,所有相邻石头的间距都不小于d。
关键参数关系:
- 初始石头序列:0(起点), a₁, a₂,..., a_N, L(终点)
- 移除决策:对每个候选距离d,计算需要移除的石头数量
- 约束条件:移除数量 ≤ M
1.2 算法选择依据
为什么二分答案适合这个问题?因为:
- 答案具有单调性:如果d可行,那么所有小于d的值都可行
- 验证函数易实现:对于给定的d,可以在O(N)时间内验证是否可行
- 数据范围支持:L可达10^9,暴力枚举不可行
2. 二分答案实现详解
2.1 算法框架设计
标准二分答案模板包含三个关键部分:
- 确定搜索范围:left=最小可能距离,right=最大可能距离
- 验证函数check(d):判断是否能在移除≤M块石头时满足最小距离≥d
- 二分过程:调整left/right边界,逐步逼近最优解
cpp复制int left = 1, right = L;
while(left <= right){
int mid = (left + right) / 2;
if(check(mid)){
left = mid + 1;
}else{
right = mid - 1;
}
}
return right;
2.2 验证函数实现技巧
check函数的实现直接影响算法效率和正确性。核心思路是贪心遍历石头序列,计算需要移除的石头数量:
cpp复制bool check(int d){
int cnt = 0, last = 0;
for(int i = 1; i <= n + 1; ++i){
if(a[i] - last < d){
cnt++;
}else{
last = a[i];
}
}
return cnt <= m;
}
注意:终点L需要作为最后一个"石头"处理,因此循环终止条件是i <= n+1
2.3 边界条件处理
几个容易出错的边界情况:
- 初始left取值:不能是0,因为题目要求必须跳跃(至少距离1)
- 终止条件:while(left <= right) 保证所有可能被检查
- 返回值:最终right是最后一个满足条件的d值
3. 算法优化与细节调优
3.1 预处理加速
对原始石头序列进行排序预处理(如果题目未保证有序):
cpp复制sort(a + 1, a + 1 + n);
a[0] = 0; a[n+1] = L;
3.2 二分终止优化
当right - left < 1e-6时(浮点二分)或left > right时(整数二分)终止。对于本题,整数二分足够精确。
3.3 防止整数溢出
计算mid时使用:
cpp复制int mid = left + (right - left) / 2;
避免(left + right)可能导致的溢出。
4. 完整AC代码实现
cpp复制#include <iostream>
#include <algorithm>
using namespace std;
const int MAXN = 5e4 + 5;
int L, n, m;
int a[MAXN];
bool check(int d){
int cnt = 0, last = 0;
for(int i = 1; i <= n + 1; ++i){
if(a[i] - last < d){
cnt++;
}else{
last = a[i];
}
if(cnt > m) return false;
}
return true;
}
int main(){
cin >> L >> n >> m;
for(int i = 1; i <= n; ++i) cin >> a[i];
a[0] = 0; a[n+1] = L;
sort(a + 1, a + 1 + n);
int left = 1, right = L;
while(left <= right){
int mid = left + (right - left) / 2;
if(check(mid)){
left = mid + 1;
}else{
right = mid - 1;
}
}
cout << right << endl;
return 0;
}
5. 常见错误与调试技巧
5.1 典型WA原因分析
- 未处理终点L:漏掉a[n+1]=L会导致最后一段距离不被检查
- 初始边界错误:left从0开始会导致答案可能为0,与题意矛盾
- 移除计数错误:在发现cnt>m时应立即返回false,避免无效计算
5.2 测试用例设计策略
设计测试用例时应考虑:
- 极限情况:M=0(不能移除任何石头)和M=N(可以移除所有石头)
- 均匀分布:石头等距排列时验证算法正确性
- 边界值:L很大(1e9)和小数据(n=1)的情况
示例测试用例:
code复制// 样例输入
25 5 2
2
11
14
17
21
// 样例输出
4
5.3 调试输出技巧
在check函数中添加调试输出:
cpp复制cout << "check " << d << ": ";
int cnt = 0, last = 0;
for(int i = 1; i <= n + 1; ++i){
if(a[i] - last < d){
cout << "X "; // 标记被移除的石头
cnt++;
}else{
cout << a[i] << " ";
last = a[i];
}
}
cout << " cnt=" << cnt << endl;
6. 算法复杂度分析
时间复杂度:
- 预处理排序:O(N logN)
- 二分过程:O(logL)
- 每次check:O(N)
- 总复杂度:O(N logL)
空间复杂度:O(N)存储石头位置
7. 同类问题拓展
二分答案法还可解决:
- POJ 3258 River Hopscotch(与本题几乎相同)
- 木材切割问题(将木材切成至少K段,求每段最大长度)
- 分配问题(将工作分配给工人,最小化最大工作量)
关键识别特征:当问题可以表述为"在满足某条件下,求最大/最小的最大/最小值"时,通常可以考虑二分答案。