题目
P2678 [NOIP 2015 提高组] 跳石头
题目背景
NOIP2015 Day2T1
题目描述
一年一度的“跳石头”比赛又要开始了!
这项比赛将在一条笔直的河道中进行,河道中分布着一些巨大岩石。组委会已经选择好了两块岩石作为比赛起点和终点。在起点和终点之间,有 N 块岩石(不含起点和终点的岩石)。在比赛过程中,选手们将从起点出发,每一步跳向相邻的岩石,直至到达终点。
为了提高比赛难度,组委会计划移走一些岩石,使得选手们在比赛过程中的最短跳跃距离尽可能长。由于预算限制,组委会至多从起点和终点之间移走 M 块岩石(不能移走起点和终点的岩石)。
这道题目十分像贪心算法,但实际上需要类似遍历的二分答案尝试,得出最优解
为了验证我们的策略,举一个反例
贪心策略的尝试
贪心策略的步骤是:每次找到当前最小的间隔,移除其中一块石头,合并间隔。重复此过程直到移除了 m 块石头。
步骤 1:初始状态
- 石头位置:
[0, 2, 3, 5, 7, 10] - 间隔:
[2, 1, 2, 2, 3](即2-0=2,3-2=1,5-3=2,7-5=2,10-7=3) - 最小间隔是 1(位置 2 和 3 之间)。
操作 1:移除位置 3 的石头
- 移除后石头位置:
[0, 2, 5, 7, 10] - 新间隔:
[2, 3, 2, 3](因为5-2=3) - 新的最小间隔是 2(例如
5-7=2)。
操作 2:移除位置 5 的石头
- 移除后石头位置:
[0, 2, 7, 10] - 新间隔:
[2, 5, 3] - 最终最短距离是 2。
更优的解法
如果我们选择不同的移除策略:
- 移除位置 2 和 7 的石头:
- 移除后石头位置:
[0, 3, 5, 10] - 间隔:
[3, 2, 5] - 最短距离是 2(似乎和贪心结果相同,但让我们再优化)。
- 移除后石头位置:
或者:
- 移除位置 3 和 7 的石头:
- 移除后石头位置:
[0, 2, 5, 10] - 间隔:
[2, 3, 5] - 最短距离是 2。
- 移除后石头位置:
但如果我们允许更大的间隔:
- 移除位置 2 和 5 的石头:
- 移除后石头位置:
[0, 3, 7, 10] - 间隔:
[3, 4, 3] - 最短距离是 3(明显更优!)
以上的反例证明,贪心解并不能获得最优解
- 移除后石头位置:
那么我们这时候开始选择二分答案近乎遍历的思想
正确方法:二分答案
正确的思路是使用二分法,直接猜测最短距离 d,然后检查是否可以通过移除 ≤m 块石头使得所有间隔都 ≥d。例如:
- 猜测
d = 3,检查是否可行:- 初始位置
[0, 2, 3, 5, 7, 10] - 维护最后一个保留的位置
prev = 0。 2 - prev = 2 < 3→ 必须移除位置 2。3 - prev = 3 ≥ 3→ 保留位置 3,prev = 3。5 - prev = 2 < 3→ 必须移除位置 5。7 - prev = 4 ≥ 3→ 保留位置 7,prev = 7。10 - prev = 3 ≥ 3→ 保留。- 移除了位置 2 和 5(共 2 块),满足条件。最短距离
3是可行的!
- 初始位置
举这个算法的例子,让同学们更能理解此算法的本质
是通过验证是否可以在一定的移除数量内得出相符合答案,依此再进行迭代,让答案逐渐达到最优解。
那么为什么是一定数量内,因为当小于该固定数量就可以满足条件时,再减少的石头,一定会增大距离。
无论具体问题如何变化(如石头问题、牛栏问题、书籍分配问题),只要满足单调性,都可以套用以下模板:
low = 最小可能答案
high = 最大可能答案
while low <= high:
mid = (low + high) / 2
if check(mid) 可行:
尝试更大的答案 (low = mid + 1)
else:
尝试更小的答案 (high = mid - 1)
最终答案 = high (或记录过程中的最优值)
源码如下
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
#define ll long long
bool check(const vector<ll>& stones, ll d, ll m) {
ll prev = 0;
ll count = 0;
for (int i = 1; i < stones.size(); ++i) {
if (stones[i] - prev < d) {
++count;
if (stones[i] == stones.back()) { // 终点无法移除,
break;
}
}
else {
prev = stones[i];
}
}
return count <= m;
}
int main() {
ll l, n, m;
cin >> l >> n >> m;
vector<ll> stones(n + 2);
stones[0] = 0;
for (ll i = 1; i <= n; ++i) {
cin >> stones[i];
}
stones[n + 1] = l;
sort(stones.begin(), stones.end());
ll low = 1, high = l;
ll ans = 0;
while (low <= high) {
ll mid = (low + high) / 2;
if (check(stones, mid, m)) {
ans = mid;
low = mid + 1;
}
else {
high = mid - 1;
}
}
cout << ans << endl;
return 0;
}
感谢观看

1351

被折叠的 条评论
为什么被折叠?



