题解 UVA1616 【商队抢劫者 Caravan Robbers】

传送门

题意

nn 个区间中各取一个子区间,每个子区间长度相等互不相交,求子区间的最大长度(用分数表示)。

分析

我们设标准答案为 ansans,可以发现,如果 ans1<ansans_1<ans,那么子区间的长度为 ans1ans_1一定是可行的,因为每个子区间长度都变短点也不会相交;如果 ans1>ansans_1>ans,那么长度为 ans1ans_1 一定是不可行的,因为 ansans 为最大长度。

换句话说,所有小于等于 ansans 的答案都是可行的,所有大于 ansans 的答案都是不可行的,也就是说答案具备单调性。这样,我们就可以二分答案,判定当前答案是否可行,若可行,说明 ansans 大于等于它,我们就缩小范围,令 l=mid;否则说明 ansans 小于它,我们也缩小范围,令 r=mid

关于 rr 的初始值,观察题目可得,我们选择的区间 xx 一定是原区间 cic_i 的子区间,说明 x.bx.ax.b-x.a 一定小于等于 ci.bci.ac_i.b-c_i.a,换句话说我们选择的区间的长度一定不会大于任何一个原区间的长度,也不会大于最短的原区间的长度,即 x.bx.amini=1nci.bci.ax.b-x.a≤\min_{i=1}^nc_i.b-c_i.a,我们在输入的过程中取 r=min(ci.bci.a,r)r=\min(c_i.b-c_i.a,r),就能进一步缩小范围。

l=0;r=1e9;//不要忘了给r一个超大值 
for(int i=1;i<=n;i++)
{
	cin>>c[i].a>>c[i].b;
	r=min((long double)(c[i].b-c[i].a),r);//r每次取最短的区间的长度 
}

由于答案不一定是整数,所以我们得使用实数域上的二分,我使用的是固定循环次数的二分方法,这样二分比设精度 epseps 的精准度要高得多。实测至少要循环 5656 次。

for(int i=1;i<=56;i++)//至少循环56次 
{
	long double mid=(l+r)/2;
	if(calc(mid))l=mid;//可行就往右缩小范围 
	else r=mid;//不可行就往左缩小范围 
}

接下来就是考虑如何实现 calccalc 函数,即如何判定了。我们提前按左端点从小到大排序,如果左端点相等就按右端点从小到大排序,这样就方便确定这些区间的位置关系。

bool cmp(asdf x,asdf y)
{
	return x.a<y.a||(x.a==y.a&&x.b<y.b);
	//按 左端点从小到大 或 左端点相等且右端点从小到大 排序 
}

然后我们定义 lastlast 为我们选择的上一个区间的右端点,初始为 00,那么当前可选区间的范围只能是 max(last,ci.a)ci.b\max(last,c_i.a)-c_i.b,为了能给后面留出更多的位置,我们就有一种贪心思路:尽量往左放,这样就为下个区间提供更多的空间。

判定的过程中,如果发现某个区间放不下了,即 max(last,ci.a)+mid>ci.b\max(last,c_i.a)+mid>c_i.b,那就 return 0;,表示不可行;否则最后 return 1;,表示可行。

bool calc(long double x)
{
	long double last=0;//last表示我们选择的上一个区间的右端点 
	for(int i=1;i<=n;i++)
	{
		if(max(last,(long double)(c[i].a))+x>(long double)(c[i].b))
			return 0;//不可行直接返回 
		last=max(last,(long double)(c[i].a))+x;//可行就选择,更新last 
	}
	return 1;
}

二分和判定结束后,我们就得到了小数版的 ansans(即二分后的 ll),麻烦的就是转为分数 pq\dfrac{p}{q}了。如何转化呢?考虑到这些区间被 nn 个商队分,所以分母 qq 应该是 11n+1n+1 的整数,所以我们只需暴力枚举 qq,再求出 pp,判断 pq\dfrac{p}{q}ll 的差是否不超过一个极小值(实测最大 1e101e-10),如果是就可以输出了。

for(q=1;q<=n+1;q++)
{
	p=l*q+0.5;//+0.5是为了四舍五入 
	if (fabs((long double)p/q-l)<1e-10)//差要加上绝对值 
	{
		cout<<p<<"/"<<q<<endl;//分数线不要忘了 
        break;//找到就结束循环 
    }
}

坑点

1.1. 本题对精度要求极高,除了上面说的以外,程序中所有浮点数都必须long double,整数参与浮点数运算时要强制转换。

2.2. 本题有多组数据,不要只输入一组!

代码

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,p,q;
long double l,r;//必须long double 
struct asdf
{
	int a,b;
}c[N];
bool cmp(asdf x,asdf y)
{
	return x.a<y.a||(x.a==y.a&&x.b<y.b);
	//按 左端点从小到大 或 左端点相等且右端点从小到大 排序 
}
bool calc(long double x)
{
	long double last=0;//last表示我们选择的上一个区间的右端点 
	for(int i=1;i<=n;i++)
	{
		if(max(last,(long double)(c[i].a))+x>(long double)(c[i].b))
			return 0;//不可行直接返回 
		last=max(last,(long double)(c[i].a))+x;//可行就选择,更新last 
	}
	return 1;
}
int main()
{
    while(cin>>n)//本题有多组数据!!! 
    {	
        l=0;r=1e9;//不要忘了给r一个超大值 
	    for(int i=1;i<=n;i++)
	    {
	    	cin>>c[i].a>>c[i].b;
	    	r=min((long double)(c[i].b-c[i].a),r);//r每次取最短的区间的长度 
		}
		sort(c+1,c+n+1,cmp);
		for(int i=1;i<=56;i++)//至少循环56次 
		{
			long double mid=(l+r)/2;
			if(calc(mid))l=mid;//可行就往右缩小范围 
			else r=mid;//不可行就往左缩小范围 
		}
		for(q=1;q<=n+1;q++)//分母q在1到n+1之间 
		{
			p=l*q+0.5;//+0.5是为了四舍五入 
			if (fabs((long double)p/q-l)<1e-10)//差要加上绝对值 
			{
				cout<<p<<"/"<<q<<endl;//分数线不要忘了 
	            break;//找到就结束循环 
	        }
		}
	}
	return 0;
}
posted @ 2021-02-25 08:18  luckydrawbox  阅读(20)  评论(0)    收藏  举报  来源