最小正方形覆盖
起因是做2024上海市赛的C题,题目是这样的:

这题的做法就是n^3枚举圆的情况(三点确定圆),剩余的点用一个最小正方形覆盖住。(还有一些corner cases,比如圆只覆盖1~2个点。注意不需要考虑0,因为覆盖一个点面积为0,一定优于0个点)
于是关键就来到了子问题:最小正方形覆盖。
这个题目,目前互联网上所有题解都认为是三分,只发现一个人用的退火,不知道他是否发现了三分的错误

也就是认为旋转正方形过程中,框住这些点的最小面积,是一个凸函数,但是这根本不正确。
这里给出一组hack数据,
19
-9599 -664
-230 -7409
-7676 -721
-7510 8825
-5706 7606
8025 -1377
-4980 8470
1910 6394
7237 6414
4045 3941
4769 5083
5100 8678
7678 -8787
-872 5143
-4544 2787
-8348 7831
-5726 8006
23 4415
1793 -4819
发现这个函数长这样

所以压根不能三分。退火倒是能一定程度解决上面这组数据卡进局部最优的问题,但严格正确的方法是什么?
上海市赛此题的题解给了我们答案:取每个凸包边的平行和垂直方向,作为两两之间支撑点不变的事件向量,先考虑这些情况(一个边在正方形上),然后考虑他们之间的情况——此时两方向投影必定相等。代码如下。
double minSqArea(vector<Pt> P)
{
P = convexHull(P);
if(P.size() <= 1) return 0;
if(P.size() == 2)
{
double d = (P[0] - P[1]).len();
return d * d / 2.0;
}
int n = P.size();
P.push_back(P[0]);
vector<Pt> vec;
for(int i = 0; i < n; i++)
{
Pt e = P[i + 1] - P[i];
vec.push_back(e);
vec.push_back(e.rot());
vec.push_back(-e);
vec.push_back((-e).rot());
}
sort(vec.begin(), vec.end(), polarCmp);
int i = n - 1, j = n - 1, k = n - 1, l = n - 1;
Pt v = vec[0];
while(i > 0 && v.cross(P[i - 1] - P[i]) <= 0) i--;
while(j > 0 && v.dot(P[j - 1] - P[j]) >= 0) j--;
while(k > 0 && v.cross(P[k - 1] - P[k]) >= 0) k--;
while(l > 0 && v.dot(P[l - 1] - P[l]) <= 0) l--;
double ans = 1e18;
for(int u = 0; u < vec.size(); u++)
{
Pt t = vec[u];
while(t.cross(P[i + 1] - P[i]) <= 0) i = (i + 1) % n;
while(t.dot(P[j + 1] - P[j]) >= 0) j = (j + 1) % n;
while(t.cross(P[k + 1] - P[k]) >= 0) k = (k + 1) % n;
while(t.dot(P[l + 1] - P[l]) <= 0) l = (l + 1) % n;
double w = t.cross(P[k] - P[i]) / t.len();
double h = t.dot(P[j] - P[l]) / t.len();
double side = max(w, h);
ans = min(ans, side * side);
Pt a = (P[i] - P[k]).rot(), b = P[j] - P[l];
Pt c = (b - a).rot();
if(a.dot(c) < 0) c = -c;
Pt t1 = (u + 1 == vec.size() ? vec[0] : vec[u + 1]);
if(c.dot(c) > 0 && t.cross(c) >= 0 && c.cross(t1) >= 0)
{
double s = a.dot(c) / c.len();
ans = min(ans, s * s);
}
}
return ans;
}

浙公网安备 33010602011771号