试题 算法提高 两条直线
资源限制
时间限制:1.0s 内存限制:256.0MB
问题描述
给定平面上n个点。
求两条直线,这两条直线互相垂直,而且它们与x轴的夹角为45度,并且n个点中离这两条直线的曼哈顿距离的最大值最小。
两点之间的曼哈顿距离定义为横坐标的差的绝对值与纵坐标的差的绝对值之和,一个点到两条直线的曼哈顿距离是指该点到两条直线上的所有点的曼哈顿距离中的最小值。
输入格式
第一行包含一个数n。
接下来n行,每行包含两个整数,表示n个点的坐标(横纵坐标的绝对值小于10^9)。
输出格式
输出一个值,表示最小的最大曼哈顿距离的值,保留一位小数。
样例输入
4
1 0
0 1
2 1
1 2
样例输出
1.0
数据规模与约定
对于30%的数据,n<=100。
对于另外30%的数据,坐标范的绝对值小于100。
对于100%的数据,n<=105。
解题思路
这题挺难理解的,刚上手不大有思路,编者也是借鉴了https://blog.dotcpp.com/a/3702
首先明确题意,点到直线的距离为点到两条直线距离中的较小者,然后我们要找 所有点到直线的距离的最大值,并且要这个最大值越小越好。(编程语文理解也要好==,不知道读者有没有被我绕晕)
其次,要明确曼哈顿距离的概念,曼哈顿距离又称街区距离,因为两点间的不可直接到达,只能通过垂直,水平方向的道路到达,称为街区距离很形象。如下图:
如果只有一条直线,那么不难得出结论:让直线卡在最远的两个点的正中间,这样点到直线的最大距离最小,如下图所示。但是本题中有两条直线,点到哪个直线近,距离就为哪一个,这样要考虑的情况很多,两条直线分下移上移讨论(编者认为要分成四个象限讨论)。
如果只有一条直线,那么上图直线的位置,为最优距离,为a,b两点距离的一半。
这时候,我们要一个一个试出答案,也就是枚举,因为精度要求为0.1,如果以步长为0.1枚举,最差要试10^ 10次,肯定是不可取的,较好的做法是采用二分法的办法:第一次取总区间长度的一半,如果满足条件,那么就要减少距离,就是在左区间继续枚举,如果不满足,就把距离加大,也就是在右区间枚举,直到 右区间端点-左区间端点的值小于0.1,达到了精度要求 。
那么如何判断一次枚举满不满足要求呢,难道要把n个点的两个距离求出来么,这样肯定是不行的。那么注意,这两条直线是垂直的,而且与坐标轴夹角为45^ ,这样我们可以把原来的坐标轴逆时针旋转45^ ,这样点到两条直线的距离就可以转换为x,y坐标,原来的(0,1) 为新坐标系的(1,1) 如下图:
转换公式为:
d[i].x=x+y;
d[i].y=y-x;
这样,点到直线的距离,就变成了点的x y坐标的绝对值,变换坐标系以后,还得继续提高算法的效率:
假设当前最小距离为m,先对横坐标x排序(新的坐标系),然后我们忽略x坐标在2* m内的点,因为不论y的距离为多少,题目要求是两者的小的,所以已经满足了要求(注意是2* m,还记得只考虑一条直线时我们得出的结论么,把直线卡在两个最远的点的中间,那么最大距离就为m了)。那么就只要考虑在区间2* m两端外面的点,此时再考虑y距离,若y两端的距离的差值小于2* m,同样的我们把另一条直线插在这两点中间,那么也可以使他们的y坐标满足要求,(这里,两条直线是可以随意移动的,互不干扰,你可以想象为x y两个坐标轴的移动,如果x方向,最远的两点相距2 * m,那么就把y轴放到两点的正中间,这样两点到y轴的水平距离就都为m了,y方向同理)。
代码如下,有详细地注释
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cmath>
using namespace std;
const int N=100000;
struct P{int x,y;}; //构造一个点地结构体
P d[N+5];
struct F{int max,min;}; //用来存储y方向的最大最小值
F fl[N+5],fr[N+5];
inline double Max(double a,double b){return a>b?a:b;} //返回最大值
inline double Min(double a,double b){return a>b?b:a;} //返回最小值
bool cmp(P a,P b){ //用于sort函数,按x的升序和y的升序排列
if(a.x==b.x)return a.y<b.y;
return a.x<b.x;
}
bool check(double m,int n){
m*=2; //区间长度为m的两倍
int i,j=0;
for(i=0;i<n;i++){ //注意:从每一个点出发(x的升序),画一个长度为2m的区间,只要有一种满足,那么就满足了
while(j<n&&d[j].x-d[i].x<=m)j++; //找出最大的xj使xj-xi<=m,所以在xi到xj的这段区间的范围
//是垂直线的范围之后找出[1,i-1]以及[j+1,n-1]范围内的最大的y和最小的y值,如果最大的y值减去最小的y值<=m,
//则说明这是水平线的范围,则进行下一轮二分找出中点,找出更优解。
double MAX=-1e10;
double MIN=1e10;
if(j!=n){ //分别在区间两端求解
MAX=Max(MAX,fr[j].max);
MIN=Min(MIN,fr[j].min);
}
if(i-1>=0){
MAX=Max(MAX,fl[i-1].max);
MIN=Min(MIN,fl[i-1].min);
}
if(MAX-MIN<=m) //如果其y方向也满足距离小于m,则满足条件
return true;
}
return false;
}
void init(int n){
int i;
fl[0].min=fl[0].max=d[0].y;
for(i=1;i<n;i++){
fl[i].max=Max(fl[i-1].max,d[i].y); //fl[i]代表着前i个y坐标里面的最大值,
fl[i].min=Min(fl[i-1].min,d[i].y); //fl[i]代表着前i个y坐标里面的最小值。
}
fr[n-1].min=fr[n-1].max=d[n-1].y;
for(i=n-2;i>=0;i--){
fr[i].max=Max(fr[i+1].max,d[i].y); //fr[i]代表着后(n-i)个y坐标里面的最大值
fr[i].min=Min(fr[i+1].min,d[i].y); //fr[i]代表着后(n-i)个y坐标里面的最小值
}
}
int main(){
int i,n;
cin>>n;
for(i=0;i<n;i++){
int x,y;
scanf("%d%d",&x,&y);
d[i].x=x+y;
d[i].y=y-x; //将坐标轴逆时针旋转45度
}
sort(d,d+n,cmp); //按x坐标从小到大的顺序排序
init(n); //初始化
//二分法求出答案
double l=0.0;
double r=1000000000;
while(r-l>=0.01){ //达到精度则退出迭代
double m=(l+r)/2; //m为最小的距离
if(check(m,n)) //如果这个距离满足,则缩小距离
r=m;
else l=m;
}
printf("%.1f\n",r);
return 0;
}