=== ===
这里放传送门
=== ===
题解
这题好!踏!马!难!写!啊。。
可以发现最小覆盖矩形一定有一条边在凸包上,那么如果按顺序枚举凸包上的一条边,可以用旋转卡壳求出和它平行并且也卡在凸包上的另一条边;对于矩形的另外两条边,可以发现随着按顺序的枚举,它们两个也是跟着一起转的。那么从第一个点开始枚举点
p1
——实际上是在枚举
p1
跟它下一个点构成的边A,然后找到跟它平行的卡在凸包上的边B——这个就是旋转卡壳的最简单的应用啦。然后考虑那两个跟它们垂直的直线,以下面这个图为例:
分别在两条直线如何找到跟它们两个垂直并且能够覆盖所有点的直线呢?显然我们应该在某一个范围内枚举点,比如说如果要求C这条直线的位置,我们就应该按照逆时针
p1→p2
的顺序来枚举点。而因为这条直线要求和A垂直,那么它的斜率就确定了。
然后就剩下一个要求就是这个直线和A,B组成的矩形一定要覆盖所有点。那这个怎么办呢?从图上我们可以发现一个很有用的性质就是C和A交点的横坐标在旋转的过程中是单峰的,在恰好覆盖所有点的时候取最值。
那到底是最大值还是最小值呢。。。。。一开始以为C就是要最大值D就是要最小值,实际上当
p1
这个点在下凸壳上的时候确实是C取Max,D取Min,但是当
p1
这个点旋转到上凸壳上的时候判定会发生变化,C变成了取Min,D变成了取Max。所以在求凸包的时候要记录上下凸壳的分界点用来判断。还有一个就是当C和D跟y轴垂直的时候交点的横坐标是不变的,这个时候就要改成判定纵坐标。
代码
#include<cmath>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define inc(x)((x==N)?1:x+1)
using namespace std;
const double eps=1e-10;
const double inf=1e60;
int n,m,k;
double ans;
struct Vector{
double x,y;
Vector (double X=0,double Y=0){x=X;y=Y;}
Vector operator + (const Vector &a){return Vector(x+a.x,y+a.y);}
Vector operator - (const Vector &a){return Vector(x-a.x,y-a.y);}
double operator * (const Vector &a){return x*a.y-y*a.x;}
Vector mul(double t){return Vector(x*t,y*t);}
void read(){scanf("%lf%lf",&x,&y);}
void print(){printf("%.5lf %.5lf\n",x,y);}
}p[50010],c[50010],a1,a2,a3,a4;
struct Line{
Vector P,v;
Line(){P=Vector();v=Vector();}
void get(Vector A,Vector B){P=A;v=B-A;}
};
int comp(Vector a,Vector b){return a.x<b.x||(a.x==b.x&&a.y>b.y);}//注意点的排序
Vector GLI(Line a,Line b){
Vector u=a.P-b.P;
double t=(b.v*u)/(a.v*b.v);
return a.P+a.v.mul(t);
}
void ConvexHull(){
sort(p+1,p+n+1,comp);
for (int i=1;i<=n;i++){
while (m>1&&(c[m]-c[m-1])*(p[i]-c[m-1])<eps) --m;
c[++m]=p[i];
}
k=m;
for (int i=n-1;i>=1;i--){
while (m>k&&(c[m]-c[m-1])*(p[i]-c[m-1])<eps) --m;
c[++m]=p[i];
}
if (n>1) --m;
}
bool better(Vector a,Vector b){
return (b.y-a.y>eps)||(fabs(b.y-a.y)<eps&&b.x-a.x>eps);
}
void Update(Line A,Line B,Vector P,Vector Q){
Line C,D;
Vector t[5];
double ar;
int rec;
C.v=D.v=Vector(-A.v.y,A.v.x);
C.P=P;D.P=Q;
t[1]=GLI(C,A);t[2]=GLI(A,D);
t[3]=GLI(D,B);t[4]=GLI(B,C);
ar=fabs((t[2]-t[1])*(t[3]-t[1]))+fabs((t[3]-t[1])*(t[4]-t[1]));
ar/=2;
if (ans-ar>eps){
ans=ar;rec=1;
for (int i=2;i<=4;i++)
if (better(t[i],t[rec])) rec=i;
a1=t[rec];rec=rec%4+1;
a2=t[rec];rec=rec%4+1;
a3=t[rec];rec=rec%4+1;
a4=t[rec];rec=rec%4+1;
}
}
bool check(Line A,int now,int opt){
Line T1,T2;
Vector ip1,ip2;
T1.P=c[now];T1.v=Vector(-A.v.y,A.v.x);
T2.P=c[now+1];T2.v=T1.v;
ip1=GLI(T1,A);ip2=GLI(T2,A);
if (opt==0) return (ip1.x-ip2.x>eps||(fabs(ip1.x-ip2.x)<=eps&&ip2.y-ip1.y>eps));
else return (ip2.x-ip1.x>eps||(fabs(ip1.x-ip2.x)<eps&&ip1.y-ip2.y>eps));
}
void Rotating_Calipers(int N){
if (N==1){ans=0;a1=a2=a3=a4=c[1];return;}
if (N==2){
ans=0;
if (better(c[2],c[1])) swap(c[1],c[2]);
a1=a2=c[1];a3=a4=c[2];
return;
}
int p1=1,p2=inc(p1),p3,p4,r0=0,r1=1;
Line A,B;
while (p1<=N){
while ((c[p1+1]-c[p1])*(c[p2+1]-c[p2])>eps)
p2=inc(p2);//找到能被当前这条边卡住的点
A.get(c[p1],c[p1+1]);
B.get(c[p2],c[p2]+A.v);
if (p1==1){p3=p2;p4=p1;}//对另外两个点进行初始化
if (p1==k) swap(r0,r1);//注意当旋转到上凸壳以后判断条件要更改
while (p3!=p1&&check(A,p3,r0))
p3=inc(p3);
while (p4!=p2&&check(A,p4,r1))
p4=inc(p4);
Update(A,B,c[p3],c[p4]);
++p1;
}
}
int main()
{
scanf("%d",&n);
for (int i=1;i<=n;i++) p[i].read();
ConvexHull();
ans=inf;
Rotating_Calipers(m);
printf("%.5lf\n",ans);
a1.print();a2.print();a3.print();a4.print();
return 0;
}
偏偏在最后出现的补充说明
旋转卡壳本质上就是利用了凸包上的单调性。
凸包上有很多很多单调性哟