题目背景
在 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\ \textrm{ms}\),空间限制 \(512\ \textrm{MiB}\)。
对于 \(100\%\) 的数据,\(0\le x_i,x_0\le 10^4\),\(0<h<100\),\(1\le i\le 7\)。
分析
我们只有彻底抓住这道题的所有性质,才能比较好地找出适合的算法。
\(100\ \texttt{pts}\)
根据我们的思考,我们发现以下几个结论:
- 对于给定的 \(r\),我们可以在 \(\mathcal{O}(1)\) 的时间内判断是否合法。
- 答案是单调的,也就是说如果 \(r^\prime\) 是合法的,那么 \(\forall r^{\prime\prime}>r^\prime\) 都是合法的。
根据这几点性质,我们考虑二分答案。复杂度为 \(\mathcal{O}(\log x_0)\),常数大约为 \(7\times \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\n", ans);
return 0;
}
后记
这道题是一道很好的二分答案练手题,如果要练习二分答案可以做一下。
虽然这道题有计算几何的味道,但仔细想想还是一道二分答案题。
有时,我们不要被其包装所误导,真正重要的是其内在法则。
来源:oschina
链接:https://my.oschina.net/u/4289967/blog/3275073