前置芝士:凸包,旋转卡壳,向量的基本运算
题意
给出平面上的一堆点,找出一个能够覆盖所有点的面积最小的矩形,输出面积及四个顶点的坐标。
分析
一眼计算几何好毒瘤题。(废话)
经过一番涂涂画画之后,我们可以非常感性地得出一个结论:最后这个矩形一定有一边和这些点的凸包上的一条边重合。
也就是说,最后的矩形应该是长成下面这样子的。
那么我们很显然需要先求出凸包,然后枚举凸包上的一条边来和矩形底边重叠。
接着观察这张图,我们可以发现还需要三个点来确定这个矩形:也就是最上方的点 E E E(即底边的对踵点)、最左边和最右边的点 F , C F,C F,C。只要找到这三个点,就可以确定这一个矩形。
其中最容易求的应该是最上面的那个点,其实就是旋转卡壳的模板,用叉积判断一下点到直线的距离即可。
然后是左右两个点。这两个点大体框架和旋转卡壳差不多,也是维护单调队列,逆时针遍历凸包上的点,唯一有区别的就是判断条件了。
仔细观察上面的图,当逆时针遍历时,遍历到最右点之前,所有凸包上的边与底边(如 A B ⃗ \vec{AB} AB和 B C ⃗ \vec{BC} BC)的点积始终为正;但是过了最右点之后(如 C D ⃗ \vec{CD} CD),点积变为负;接着直到过了最左点,才又变为正。
所以问题就很简单了,我们套用旋转卡壳的板子,把中间用叉积判断的部分改成点积判断,就可以找出最左点和最右点。
找出三个点之后,我们考虑怎么求矩形的面积和顶点坐标。由于矩形的高和底边垂直,那么我们可以先求出底边的垂线。那么就可以结合左右点求出矩形的左右两边所在的直线,再结合上下两点所确定的高,就可以很方便地求出矩形的面积和顶点。(具体可以结合代码理解)
代码
先上板子题:P3187 最小矩形覆盖
#include <bits/stdc++.h>
#define MAX 100005
#define eps 1e-8
using namespace std;
struct vec{
double x, y;
friend bool operator <(vec a, vec b){
//真·小于
if(fabs(a.x-b.x) > eps) return a.x < b.x;
return a.y < b.y;
}
friend bool operator <= (vec a, vec b){
//用来逆时针输出
if(fabs(a.y-b.y) > eps) return a.y < b.y;
return a.x < b.x;
}
friend vec operator +(vec a, vec b){
return (vec){
a.x+b.x, a.y+b.y};
}
friend vec operator -(vec a, vec b){
return (vec){
a.x-b.x, a.y-b.y};
}
friend vec operator ^(vec a, double b){
//数乘
return (vec){
a.x*b, a.y*b};
}
friend double operator %(vec a, vec b){
//点积
return a.x*b.x+a.y*b.y;
}
friend double operator *(vec a, vec b){
//叉积
return a.x*b.y-a.y*b.x;
}
}a[MAX], h[MAX];
double dis(vec a, vec b){
//两点之间的距离
return sqrt((a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y));
}
double dis(vec a, vec b, vec c){
//c到直线ab的距离
return fabs((c-a)*(c-b)/dis(a, b));
}
vec inter(vec a, vec b, vec c, vec d){
//直线ab和cd的交点
double s = (c-a)*(d-a);
double S = s+(d-b)*(c-b);
return a+((b-a)^(s/S));
}
int n;
int st[MAX], top, vis[MAX];
vec pt[5];
int main()
{
cin >> n;
for(int i = 1; i <= n; ++i){
scanf("%lf%lf", &a[i].x, &a[i].y);
}
sort(a+1, a+n+1);
st[++top] = 1;
for(int i = 2; i <= n; ++i){
//求凸包
while(top > 1 && (a[st[top]]-a[st[top-1]]) * (a[i]-a[st[top]]) <= 0){
vis[st[top]] = 0;
top--;
}
vis[i] = 1;
st[++top] = i;
}
int tmp = top;
for(int i = n-1; i >= 1; --i){
if(vis[i]) continue;
while(top > tmp && (a[st[top]]-a[st[top-1]]) * (a[i]-a[st[top]]) <= 0){
vis[st[top]] = 0;
top--;
}
vis[i] = 1;
st[++top] = i;
}
for(int i = 1; i <= top; ++i){
h[i] = a[st[i]];
}
top--;
int l, r = 1, t = 1;
while(dis(h[1], h[2], h[t]) <= dis(h[1], h[2], h[t+1])){
t = t%top+1;
}
l = t; //确定最左点的初始位置,因为最左点必须在最右点的后面,否则初始值为1就会找到错误的点积大于0的点
double ans = 1e18;
for(int i = 1; i <= top; ++i){
//找出三个关键点,套用旋转卡壳板子
while((h[r+1]-h[r]) % (h[i+1]-h[i]) >= 0)
r = r%top+1;
while(dis(h[i], h[i+1], h[t]) <= dis(h[i], h[i+1], h[t+1]))
t = t%top+1;
while((h[l+1]-h[l]) % (h[i+1]-h[i]) <= 0)
l = l%top+1;
vec cx = (vec){
-(h[i+1]-h[i]).y, (h[i+1]-h[i]).x}; //垂线
double area = dis(h[i], h[i+1], h[t])*dis(h[r], h[r]+cx, h[l]); //求面积
if(area < ans){
ans = area;
//利用直线间的交点求出顶点坐标
pt[1] = inter(h[i], h[i+1], h[r], h[r]+cx);
pt[2] = inter(h[r], h[r]+cx, h[t], h[t]+(h[i+1]-h[i]));
pt[3] = inter(h[t], h[t]+(h[i+1]-h[i]), h[l], h[l]+cx);
pt[4] = inter(h[l], h[l]+cx, h[i], h[i+1]);
}
}
for(int i = 1; i <= 4; ++i){
//防止出现-0.00000
if(fabs(pt[i].x) < eps) pt[i].x = 0.0;
if(fabs(pt[i].y) < eps) pt[i].y = 0.0;
}
int mn = 1;
for(int i = 2; i <= 4; ++i){
//保证逆时针输出
if(pt[i] <= pt[mn]) mn = i;
}
printf("%.5lf\n", ans);
for(int i = mn; i <= 4; ++i)
printf("%.5lf %.5lf\n", pt[i].x, pt[i].y);
for(int i = 1; i < mn; ++i)
printf("%.5lf %.5lf\n", pt[i].x, pt[i].y);
return 0;
}