【题解】穿越七色虹

题目背景

在 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}\)

根据我们的思考,我们发现以下几个结论:

  1. 对于给定的 \(r\),我们可以在 \(\mathcal{O}(1)\) 的时间内判断是否合法。
  2. 答案是单调的,也就是说如果 \(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;
}

后记

这道题是一道很好的二分答案练手题,如果要练习二分答案可以做一下。

虽然这道题有计算几何的味道,但仔细想想还是一道二分答案题。

有时,我们不要被其包装所误导,真正重要的是其内在法则。

posted @ 2020-04-18 15:52  5ab  阅读(289)  评论(0编辑  收藏  举报