题解:洛谷 P2678 跳石头

【题目来源】

洛谷:P2678 [NOIP 2015 提高组] 跳石头 - 洛谷(luogu.com.cn)

【题目描述】

一年一度的“跳石头”比赛又要开始了!

这项比赛将在一条笔直的河道中进行,河道中分布着一些巨大岩石。组委会已经选择好了两块岩石作为比赛起点和终点。在起点和终点之间,有 \(N\) 块岩石(不含起点和终点的岩石)。在比赛过程中,选手们将从起点出发,每一步跳向相邻的岩石,直至到达终点。

为了提高比赛,组委会计划移走一些岩石,使得选手们在比赛过程中的最短跳跃距离尽可能长。由于预算限制,组委会至多从起点和终点之间移走 块岩石(不能移走起点和终点的岩石)。

【输入】

第一行包含三个整数 \(L,N,M\),分别表示起点到终点的距离,起点和终点之间的岩石数,以及组委会至多移走的岩石数。保证 \(L\ge 1\)\(N\ge M\ge 0\)

接下来 \(N\) 行,每行一个整数,第 \(i\) 行的整数 \(D_i(0\lt D_i\lt L)\),表示第 \(i\) 块岩石与起点的距离。这些岩石按与起点距离从小到大的顺序给出,且不会有两个岩石出现在同一个位置。

【输出】

一个整数,即最短跳跃距离的最大值。

【输入样例】

25 5 2
2
11
14
17
21

【输出样例】

4

【核心思想】

  1. 问题分析:给定河道长度 \(L\),起点终点间有 \(N\) 块岩石,最多移走 \(M\) 块(不含起点终点)。求移走岩石后,最短跳跃距离的最大值。这是一个整数二分 + 贪心验证问题,与路标设置(P3853)类似但目标相反:本题是"最大化最小值",P3853是"最小化最大值"。

  2. 算法选择

    • 整数二分:在 \([1, L]\) 范围内二分查找最大可行的最短跳跃距离
    • 贪心验证:对于给定的最短距离 \(mid\),贪心移走岩石:若当前岩石到上一个保留岩石的距离 \(< mid\),则移走当前岩石;否则保留。统计移走数量是否 \(\leq M\)
  3. 关键步骤

    • 读入数据\(L, N, M\)\(N\) 个岩石位置,设 \(a[N+1] = L\)(终点)
    • 二分查找\(l = 1, r = L\)\(10^9\)):
      • mid = (l + r + 1) / 2(向上取整,避免死循环)
      • check(mid)
        • cnt = 0, last = 0(上一个保留的岩石索引,起点为0)
        • 遍历 \(i\)\(1\)\(N+1\)
          • a[i] - a[last] < midcnt++(移走当前岩石)
          • 否则:last = i(保留)
        • 返回 cnt <= M
      • check(mid) 为真:l = mid(尝试更大)
      • 否则:r = mid - 1
    • 输出 l
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N \log L)\),二分 \(O(\log L)\),每次验证 \(O(N)\)
    • 空间复杂度:\(O(N)\),存储岩石位置
  5. 整数二分最大化最小值的核心思想

    • 答案单调性:若最短距离 \(mid\) 可行(移走岩石 \(\leq M\)),则所有 \(< mid\) 的值也可行;若不可行,则所有 \(> mid\) 的值也不可行。单调性支撑二分查找最大值
    • 贪心保留策略:验证时,遇到距离不足的岩石就移走,这是最优的——因为移走当前岩石可以"合并"间距,使后续跳跃距离更大,而保留当前岩石会限制后续选择
    • 向上取整防死循环mid = (l + r + 1) / 2 确保当 l + 1 == r 时取 \(r\),配合 l = mid 的更新方式,避免死循环
    • 与路标设置的对比:本题是"移走岩石使间距变大",P3853是"增设路标使间距变小",但都是二分答案 + 贪心验证的框架
    • 适用于"最大化最小间隔"类问题,核心在于二分答案和贪心验证

【解题思路】

【算法标签】

普及 #整数二分

【代码详解】

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

// 全局变量定义
int length, n, m, a[50005], last;  // length: 道路总长度, n: 已有石头数, m: 可移除石头数
                                   // a[]: 存储相邻石头间距, last: 记录上一个石头位置

int main()
{
    // 输入道路总长度、已有石头数和可移除石头数
    cin >> length >> n >> m;

    // 初始化上一个石头位置为0(起点)
    last = 0;

    // 输入每个石头的位置并计算相邻石头间距
    for (int i = 0; i < n; i++) {
        int now;  // 当前石头位置
        cin >> now;
        a[i] = now - last;  // 计算当前石头与前一个石头的间距
        last = now;         // 更新上一个石头位置
    }
    // 计算最后一个石头到终点的间距
    a[n] = length - last;

    // 特殊情况处理:如果可移除石头数等于已有石头数
    if (n == m) {
        cout << length << endl;  // 直接输出道路总长度
        return 0;
    }

    // 二分查找初始化
    int l = 1, r = length / (n - m);  // l: 最小可能间距, r: 最大可能间距

    // 二分查找过程
    while (l <= r) {
        int tmp, k = 0;  // tmp: 临时累计间距, k: 需要移除的石头数
        int mid = l + (r - l) / 2;  // 中间值(当前猜测的最小间距)

        // 检查当前mid是否可行
        for (int i = 0; i <= n; i++) {
            tmp = a[i];  // 初始化为当前间距
            // 如果当前间距小于mid,则合并下一段间距
            while (tmp < mid && i <= n) {
                i++;
                tmp += a[i];  // 合并间距
                k++;          // 增加需要移除的石头数
            }
        }

        // 根据检查结果调整二分范围
        if (k <= m) l = mid + 1;  // 移除石头数不足,可以尝试更大的间距
        else r = mid - 1;         // 移除石头数过多,需要减小间距
    }

    // 输出最终结果(最大可能的最小间距)
    cout << r << endl;
    return 0;
}
// 使用老师的板子再写一遍
#include <bits/stdc++.h>
using namespace std;

int L, n, m, a[50005], ans;

// 检查函数:判断是否可以在移除最多m块石头的情况下
// 使得所有相邻石头间的最短距离不小于x
bool check(int x)
{
    int cnt = 0;      // 记录需要移除的石头数量
    int last = 0;     // 上一个未被移除的石头位置

    for (int i = 1; i <= n + 1; i++)
    {
        // 如果当前石头到上一个未移除石头的距离小于x
        if (a[i] - a[last] < x)
        {
            cnt++;  // 移除当前石头
        }
        else
        {
            last = i;  // 保留当前石头
        }
    }

    return cnt <= m;  // 判断需要移除的石头是否不超过m块
}

// 二分查找函数:寻找最大的最小距离x
int find()
{
    int l = 0, r = 1000000000 + 1;  // 搜索范围

    while (l + 1 < r)  // 开区间二分
    {
        int mid = (l + r) / 2;

        if (check(mid))  // 如果mid可行
        {
            l = mid;  // 尝试更大的值
        }
        else
        {
            r = mid;  // 尝试更小的值
        }
    }

    return l;  // 返回最大的可行值
}

int main()
{
    cin >> L >> n >> m;  // 输入峡谷宽度L,石头数量n,可移除石头数量m

    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];  // 输入每个石头的位置
    }

    a[n + 1] = L;  // 最后一个位置是峡谷终点

    ans = find();  // 查找答案
    cout << ans << endl;

    return 0;
}
// 使用acwing模板二刷
#include <bits/stdc++.h>
using namespace std;

const int N = 50005;
int L, n, m, a[N], ans;

// 检查函数:判断是否可以在移除最多m块石头的情况下,
// 使得所有相邻石头间的最短距离不小于x
bool check(int x)
{
    int cnt = 0;      // 记录需要移除的石头数量
    int last = 0;     // 上一个未移除的石头位置(起点是0)

    for (int i = 1; i <= n + 1; i++)  // 遍历所有石头+终点
    {
        // 如果当前石头到上一个未移除石头的距离小于x
        if (a[i] - a[last] < x)
        {
            cnt++;  // 移除当前石头
        }
        else
        {
            last = i;  // 保留当前石头
        }
    }

    return cnt <= m;  // 判断需要移除的石头是否不超过m块
}

// 二分查找:寻找最大的最小距离x
int find()
{
    int l = 1, r = 1000000000;  // 搜索范围[1, 1e9]

    while (l < r)  // 二分查找
    {
        int mid = l + r + 1 >> 1;  // 向上取整

        if (check(mid))  // 如果mid可行
        {
            l = mid;  // 尝试更大的值
        }
        else
        {
            r = mid - 1;  // 尝试更小的值
        }
    }

    return l;  // 返回最大的可行值
}

int main()
{
    cin >> L >> n >> m;  // 峡谷长度L,石头数量n,最多移除m块

    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];  // 输入石头位置
    }

    a[n + 1] = L;  // 终点位置

    ans = find();  // 计算答案
    cout << ans << endl;

    return 0;
}

【运行结果】

25 5 2
2
11
14
17
21
4
posted @ 2026-08-10 12:28  团爸讲算法  阅读(27)  评论(0)    收藏  举报