题目背景
在 Nescafe27 和 28 中,讲述了一支探险队前往 Nescafe 之塔探险的故事……
当两位探险队员以最快的时间把礼物放到每个木箱里之后,精灵们变身为一缕缕金带似的光,簇簇光芒使探险队员们睁不开眼睛。待一切平静下来之后,探险队员来到了一座宫殿中,玉制的石椅上坐着两个人。「你们就是……Nescafe 之塔护法中的两位?」
「是的,我们就是神刀护法 xlk 和飞箭护法 riatre……你们来这里做什么?」
「我们是前来拜访圣主和四位护法的……」
「如果你们想见圣主和其它两位护法,你们必须穿过前方的七色彩虹。请随我来吧……」
题目描述
探险队员们跟随两位护法来到了七色虹前。七色虹,就是平面直角坐标系中赤、橙、黄、绿、青、蓝、紫七个半圆。第 (i) 座半圆形彩虹的圆心是 ((x_i,0)),半径是 (r_i),半圆上所有点的纵坐标均为非负数。探险队员可以看做一条竖直的,长度等于身高的线段。线段的底端纵坐标为 (0),最高的一位探险队员的身高为 (h)。
现在探险队员们要从 ((0,0)) 穿越七色虹到达 ((x_0,0))。穿越七色虹的过程中,探险队员的整个身体必须始终在至少一个半圆形彩虹的内部。由于彩虹的半径 (r_i) 可能太小了,不足以满足这个条件,因此两位护法决定帮助他们把所有彩虹的半径都增大一个非负实数 (r)。探险队员们想知道,(r) 最小是多少呢?
输入格式
第一行两个实数 (h)、(x_0),表示身高和目的地横坐标。
接下来七行每行两个实数 (x_i)、(r_i),表示七座半圆形彩虹的圆心和半径。
输出格式
输出最小的 (r),四舍五入保留 (2) 位小数。
数据范围
评测时间限制 (500 extrm{ms}),空间限制 (512 extrm{MiB})。
对于 (100\%) 的数据,(0le x_i,x_0le 10^4),(0<h<100),(1le ile 7)。
分析
我们只有彻底抓住这道题的所有性质,才能比较好地找出适合的算法。
(100 exttt{pts})
根据我们的思考,我们发现以下几个结论:
- 对于给定的 (r),我们可以在 (mathcal{O}(1)) 的时间内判断是否合法。
- 答案是单调的,也就是说如果 (r^prime) 是合法的,那么 (forall r^{primeprime}>r^prime) 都是合法的。
根据这几点性质,我们考虑二分答案。复杂度为 (mathcal{O}(log x_0)),常数大约为 (7 imes log_2 100=40),可以通过本题。
Code
// @author 5ab
#include <algorithm>
#include <cstdio>
#include <cmath>
using namespace std;
const double EPS = 1e-4; // 防止缺精度多取几位。
const int max_n = 7; // 颜色数
/*
判断是否合法的标志:区间合并,判断 [0, x0] 是否在一个连续区间内。
复杂度是 O(nlogn),其中 n 是颜色数。这个复杂度实际上是排序的复杂度。
注意不能开始时排序,因为随着半径的变化,顺序也是有可能变化的。
对于半径为 r,身高为 h,圆心位于 xi 的园的区间为 [xi-sqrt(r^2-h^2), xi+sqrt(r^2-h^2)],可以由勾股定理得到。
*/
struct interval
{
double lp, rp;
bool operator<(const interval& it)
{
if (rp != it.rp)
return rp < it.rp;
else
return lp < it.lp;
}
};
/*
变量解释:
it[i]:第 i 个圆所对应的区间;
rad[i]:第 i 个圆的半径;
pos[i]:第 i 个圆的位置;
hei:最高身高;
ed:终点坐标。
*/
interval it[max_n];
double rad[max_n], pos[max_n], hei, ed;
bool check(double added)
{
double tmp;
for (int i = 0; i < max_n; i++)
{
tmp = sqrt((rad[i] + added) * (rad[i] + added) - hei * hei);
it[i].lp = pos[i] - tmp;
it[i].rp = pos[i] + tmp;
}
sort(it, it + max_n); // 统计区间并排序
for (int i = max_n - 1; i > 0; i--) // 合并区间
{
if (it[i-1].rp < it[i].lp) // 无法合并了
{
if (it[i].lp <= 0 && it[i].rp >= ed) // 判断是否包含
return true; // 是的就退出
continue; // 否则继续
}
if (it[i-1].lp > it[i].lp)
it[i-1].lp = it[i].lp;
it[i-1].rp = it[i].rp; // 合并区间,取较大的左端点
}
if (it[0].lp <= 0 && it[0].rp >= ed)
return true; // 最后一个特判
else
return false; // 没有被完全包含的区间,r 还不够大
}
int main()
{
double lb = 0, ub = 10010, mid, ans = -1;
scanf("%lf%lf", &hei, &ed);
for (int i = 0; i < max_n; i++)
scanf("%lf%lf", pos + i, rad + i); // 输入
while (ub - lb > EPS) // 二分,不解释
{
mid = (lb + ub) / 2;
if (check(mid))
ans = mid, ub = mid; // 试图寻找更小的答案
else
lb = mid + EPS; // 一定要加这个 EPS,否则会死循环
}
printf("%.2lf
", ans);
return 0;
}
后记
这道题是一道很好的二分答案练手题,如果要练习二分答案可以做一下。
虽然这道题有计算几何的味道,但仔细想想还是一道二分答案题。
有时,我们不要被其包装所误导,真正重要的是其内在法则。