P7806 [DCOI2021] A 冰魄吐息 题解
题目中出现了至多和最小等字眼,因此很有可能需要使用二分答案。
怎么进行二分呢?我们对
由于到一个点距离不超过
我们只需要找到这
那么最关键的就是如何求解每个点的
Solution 1 二分套二分
为什么题目要提供点和直线的距离公式呢?实际上,根据之前插图的演示,不难发现直线与点的距离满足一定的单调性。我们可以利用这个性质再一次二分,得到
对于斜率等于
该做法的时间复杂度为
#include<bits/stdc++.h>
using namespace std;
int n,k;
long double l,r=100000,ans; // 将二分区间右端点 r 调成一个较大的值
struct point
{
long double x,y;
}p[25001];
struct interval
{
long double L,R;
bool operator<(const interval &x)const
{
return R<x.R;
}
}a[25001];
bool check_dis(int x,int y,long double k,long double d)
{
long double dis=fabs(y-k*x)/sqrt(k*k+1);
return dis<=d;
}
interval get(point p,long double d)
{
if(!p.x)return (interval){-p.y/d,p.y/d}; // 特判 x 为 0 的特例
interval ans;
long double l=0,r=p.y/p.x; // 第一条切线一定在点 P 的下方
while(l<=r)
{
long double mid=(l+r)*0.5;
if(check_dis(p.x,p.y,mid,d))
{
r=mid-0.00000001; // 第二个二分需要很高的精度,否则无法通过
ans.L=mid;
}
else l=mid+0.00000001;
}
l=p.y/p.x,r=100000; // 第二条切线一定在点 P 的上方
while(l<=r)
{
long double mid=(l+r)*0.5;
if(check_dis(p.x,p.y,mid,d))
{
l=mid+0.00000001;
ans.R=mid;
}
else r=mid-0.00000001;
}
return ans;
}
bool check(long double d)
{
int cnt=0;
for(int i=1;i<=n;++i)
{
if(sqrt(p[i].x*p[i].x+p[i].y*p[i].y)<=d)continue; // 不需要考虑到原点距离已经不超过 d 的点
a[++cnt]=get(p[i],d); // 二分得到斜率区间
}
sort(a+1,a+cnt+1); // 排序并用贪心求解
int tot=0;
long double tmp=0;
for(int i=1;i<=cnt;++i)
{
if(tmp<=a[i].L)
{
tmp=a[i].R;
if(++tot>k)return false; // 一旦需要的直线数量超过 k 就说明不可行
}
}
return true;
}
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;++i)scanf("%Lf%Lf",&p[i].x,&p[i].y);
while(l<=r)
{
long double mid=(l+r)*0.5;
if(check(mid))
{
r=mid-0.0001; // 由于只需要保留两位小数,因此精度可以稍低
ans=mid;
}
else l=mid+0.0001;
}
printf("%.2Lf",ans);
return 0;
}
Solution 2 二分+导角
我们可以通过角和斜率之间的转化求解。
考虑下图的情形(两条直线斜率都非负):
我们可以用反正切函数求出
我们还需考虑下面两种情形:
这种情形下切线斜率一正一负,而这此时直接把负的当左端点、正的当右端点显然是错误的(有一个跨
- 在第一种情形中,符合题意的所有斜率的最小值为其中的正斜率,最大值为
+\infty 。 - 在第二种情形中,最小值为
0 ,最大值为其中的正斜率。
考虑当前的点属于哪一种情形:
- 当切点位于第二象限时(此时
\alpha+\beta \gt 90^\circ ),属于第一种。 - 当切点位于第四象限时(此时
\alpha+\beta \lt 90^\circ ),属于第二种。
那么,为什么二分套二分的方法不需要对此进行考虑呢?这是因为,我们可以在求解
该做法的时间复杂度为
#include<bits/stdc++.h>
using namespace std;
const double rt=acos(-1)*0.5; // 预处理 90° 弧度制下的值,即 0.5π
int n,k;
long double l,r=100000,ans;
struct point
{
long double x,y;
}p[25001];
struct interval
{
long double L,R;
bool operator<(const interval &x)const
{
return R<x.R;
}
}a[25001];
bool check(long double d)
{
int cnt=0;
for(int i=1;i<=n;++i)
{
long double x=p[i].x,y=p[i].y;
if(x*x+y*y<=d*d)continue;
++cnt;
long double alpha,beta;
alpha=atan(y/x); // 求出 OP 与 x 轴的夹角
if(!x)alpha=rt; // 如果 x=0 则与 x 轴的夹角为 90°
beta=asin(d/sqrt(x*x+y*y)); // 求出切线与 OP 连线的夹角
a[cnt].L=tan(alpha-beta),a[cnt].R=tan(alpha+beta);
if(a[cnt].L>a[cnt].R)swap(a[cnt].L,a[cnt].R);
if(a[cnt].L<0)
{
if(alpha+beta>rt)
{
a[cnt].L=a[cnt].R;
a[cnt].R=1e18;
}
else a[cnt].L=0;
}
}
sort(a+1,a+cnt+1);
int tot=0;
long double tmp=0;
for(int i=1;i<=cnt;++i)
{
if(tmp<=a[i].L)
{
tmp=a[i].R;
if(++tot>k)return false;
}
}
return true;
}
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;++i)scanf("%Lf%Lf",&p[i].x,&p[i].y);
while(l<=r)
{
long double mid=(l+r)*0.5;
if(check(mid))
{
r=mid-0.0001;
ans=mid;
}
else l=mid+0.0001;
}
printf("%.2Lf",ans);
return 0;
}
Solution 3 二分+导边
借助之前的示意图:
不妨设
又因为
由于
因此
把
因此我们可以将
再代回
最后求出
特判
此时
作
由于