<think>我们面对的问题:在Java中实现一个算法,用于找到不规则区域的最小外接四边形(最小面积外接矩形)。
注意:最小外接四边形通常指的是最小面积外接矩形(因为任意凸多边形的最小面积外接矩形都是矩形,但有时也指最小边数的外接多边形,这里根据上下文,我们理解为最小面积外接矩形)。
步骤:
1. 理解问题:给定一个不规则区域(通常由一组点表示,这些点构成一个多边形),我们需要找到包围这个多边形的最小面积的矩形(外接矩形)。
2. 常见方法:旋转卡壳法(Rotating Calipers)是计算凸多边形最小外接矩形的经典算法。但注意,该算法要求输入为凸多边形。如果输入是非凸的,我们需要先求凸包。
因此,步骤为:
a) 如果多边形不是凸的,先计算其凸包(Convex Hull)。
b) 使用旋转卡壳算法在凸包上寻找最小面积外接矩形。
3. 旋转卡壳算法原理:
- 最小外接矩形必有一条边与凸多边形的一条边重合(或与凸多边形的边平行)。
- 算法遍历凸多边形的每条边,作为矩形的候选边。
- 对于每条边,通过旋转卡壳确定其他三个方向上的极值点(最左、最右、最上、最下)?实际上,在旋转过程中,我们计算与当前边垂直的方向上的宽度,并记录对应的点。
- 具体步骤:
1) 计算凸包上三个关键方向:与当前边方向垂直的方向(即法向),然后计算凸包在这些方向上的投影,得到宽度和高度。
2) 实际上,我们可以通过点积来求投影。对于当前边,我们有两个点定义一条边,然后我们可以得到该边的方向向量,再旋转90度得到法向量。然后计算凸包上所有点在这两个方向(边方向和法方向)上的最大最小值,从而确定当前方向下的外接矩形面积。
3) 遍历凸包的所有边,记录面积最小的矩形。
4. 实现步骤:
a) 计算凸包:使用Andrew's Monotone Chain算法(或其他凸包算法)。
b) 对凸包上的点进行排序(按x坐标,如果x相同则按y)。
c) 初始化最小矩形面积为无穷大。
d) 遍历凸包的每条边(假设凸包有n个点,则边为i从0到n-1,边由点points[i]和points[(i+1)%n]构成)。
e) 对于当前边,计算其方向向量(dx, dy),并计算单位法向量(即垂直方向,有两种可能,取一个即可,因为矩形对称)?实际上,我们并不需要单位化,因为投影时用点积,我们只需要方向。
f) 计算凸包上所有点在该边的方向向量和法向量上的投影(即点积)。然后得到投影的最大值和最小值,从而得到宽度(在法向量上的投影差)和高度(在边方向上的投影差?实际上,矩形的一条边与当前边平行,另一条边与当前边垂直。所以矩形的宽度是法向量方向上的投影差,高度是边方向上的投影差?不对,实际上,矩形的面积由两个方向上的跨度决定。但注意,当前边方向上的跨度实际上就是当前边的长度?不是,因为整个凸包在边方向上的投影跨度就是当前边两个端点投影的差,但法向量方向上的投影跨度就是整个凸包在垂直方向上的宽度。因此,矩形的宽度为边向量方向上的投影差(即当前边的长度?)并不是,因为其他点在该方向上的投影可能超出当前边的端点。所以我们需要计算整个凸包在边向量方向上的最大最小值,以及法向量方向上的最大最小值。然后矩形的宽度为(边方向上的最大值-最小值),高度为(法方向上的最大值-最小值)。但是注意,这样计算出来的矩形并不是我们通常的宽高,但面积是(边方向投影差)乘以(法方向投影差)。
5. 具体计算:
设当前边的向量为:v = (dx, dy) = (x2-x1, y2-y1)
法向量:n = (-dy, dx) 或 (dy, -dx) 都可以,只要垂直。我们取n = (-dy, dx)(旋转90度)。
然后,对于凸包上的每个点p,计算:
proj_v = p·v (点积,即p.x*dx + p.y*dy)
proj_n = p·n (即p.x*(-dy) + p.y*dx)
然后,我们计算所有点的proj_v的最小值和最大值,以及proj_n的最小值和最大值。
那么,当前方向下的矩形面积 = (max_proj_v - min_proj_v) * (max_proj_n - min_proj_n)
但是注意:这个矩形的面积并不是我们通常的面积单位,因为v和n可能不是单位向量。但是,由于我们只是比较面积大小,而且每个矩形都是用同样的向量(只是方向不同)来计算的,所以不同方向下计算出的面积值可以直接比较(因为每个方向都用了相同的缩放因子?)。实际上,点积的结果与向量的模长有关,所以不同边的向量长度不同,这样比较是不对的。我们需要归一化吗?
6. 修正:实际上,我们不需要计算实际的面积,因为面积公式是:
area = (max_proj_v - min_proj_v) * (max_proj_n - min_proj_n) / (|v| * |n|) ?
但是,注意:我们计算点积时,投影的长度实际上是除以了|v|(如果我们用单位向量的话)。但我们没有用单位向量,所以投影的长度是:点积除以|v|。因此,实际宽度和高度应该是:
width = (max_proj_v - min_proj_v) / |v|
height = (max_proj_n - min_proj_n) / |n|
但是,由于|v| = |n|(因为n是由v旋转90度得到的,所以模长相等),所以面积 = width * height = (max_proj_v - min_proj_v) * (max_proj_n - min_proj_n) / (|v|^2)
然而,我们遍历所有边时,需要最小化面积,而除以|v|^2是一个正数,所以我们可以比较 (max_proj_v - min_proj_v) * (max_proj_n - min_proj_n) 除以 |v|^2 的值。但为了效率,我们可以比较 (max_proj_v - min_proj_v) * (max_proj_n - min_proj_n) 的值,同时记录对应的|v|^2?不行,因为不同边的|v|不同,所以不能直接比较这个乘积。
7. 因此,我们需要计算实际面积:area = ( (max_proj_v - min_proj_v) * (max_proj_n - min_proj_n) ) / (|v|^2) [因为|v|=|n|,所以|v|*|n|=|v|^2]
但是,由于点积的投影长度是除以|v|,所以:
实际宽度 = (max_proj_v - min_proj_v) / |v|
实际高度 = (max_proj_n - min_proj_n) / |v|
所以面积 = ( (max_proj_v - min_proj_v) * (max_proj_n - min_proj_n) ) / (|v|^2)
因此,我们在计算面积时,需要除以当前边长的平方(|v|^2)。
8. 另一种方法:为了避免除法,我们可以使用单位向量?但计算单位向量需要开方,而且每个点都要用单位向量点乘,效率低。
9. 实际上,我们可以不除以|v|^2,而是直接比较 (max_proj_v - min_proj_v) * (max_proj_n - min_proj_n) 的值,但这样比较是不对的,因为不同边的|v|不同。所以我们必须计算实际面积。
10. 优化:在循环之前计算当前边的长度平方,然后最后计算面积时除以这个值。
11. 步骤总结:
minArea = Double.MAX_VALUE
for (int i = 0, j = n-1; i < n; j = i, i++) {
Point p1 = points[j];
Point p2 = points[i];
// 计算边向量
double dx = p2.x - p1.x;
double dy = p2.y - p1.y;
double lengthSquared = dx*dx + dy*dy;
// 如果边长为0,跳过
if (lengthSquared < 1e-12) continue;
// 计算法向量 (-dy, dx)
double nx = -dy;
double ny = dx;
double minV = Double.MAX_VALUE, maxV = -Double.MAX_VALUE;
double minN = Double.MAX_VALUE, maxN = -Double.MAX_VALUE;
for (int k = 0; k < n; k++) {
Point p = points[k];
// 计算点p在边向量上的投影(点积)
double projV = (p.x - p1.x) * dx + (p.y - p1.y) * dy; // 这里以p1为参考点,避免整体偏移
// 计算点p在法向量上的投影
double projN = (p.x - p1.x) * nx + (p.y - p1.y) * ny;
if (projV < minV) minV = projV;
if (projV > maxV) maxV = projV;
if (projN < minN) minN = projN;
if (projN > maxN) maxN = projN;
}
// 计算实际面积:投影跨度在边方向上的实际长度 = (maxV - minV) / |v|,在法方向上的实际长度 = (maxN - minN) / |v|
// 所以面积 = [ (maxV-minV) * (maxN-minN) ] / lengthSquared
double area = ( (maxV - minV) * (maxN - minN) ) / lengthSquared;
if (area < minArea) {
minArea = area;
// 同时记录当前矩形?如果需要返回矩形,需要记录矩形的四个顶点
}
}
12. 如何得到矩形的四个顶点?
矩形的四个顶点由投影的极值点确定。但是,注意:投影的极值点不一定只有一个,而且矩形的顶点是由两个方向上的极值组合而成。通常,矩形的四个顶点是:
A: p1 + (minV / |v|^2) * v + (minN / |v|^2) * n ? 这样计算复杂。
另一种思路:矩形的四条边分别与当前边平行和垂直。矩形的底边在边向量v的方向上,长度为(maxV-minV)/|v|,高度为(maxN-minN)/|v|。但是,我们如何定位矩形?
我们可以这样:
从点p1出发,加上向量v方向的投影和n方向的投影:
矩形的一个顶点:p1 + (minV * v) / |v|^2 + (minN * n) / |v|^2
但是,这样计算比较麻烦。
实际上,我们注意到,投影值minV和maxV是相对于点p1的。所以:
在v方向上,矩形的起点位置是:p1 + (minV / |v|) * (v的单位向量)
但我们可以用向量运算:位置 = p1 + (minV * v) / |v|^2 (因为v的单位向量 = v/|v|,所以minV * (v/|v|) = (minV * v) / |v|,但是这里我们除以了|v|,而点积投影是相对于v向量,所以位置向量应该是 (minV / |v|) * (v/|v|) = minV * v / |v|^2)
因此,矩形的四个顶点:
base = p1
// 计算四个顶点
double factorV, factorN;
Point[] rect = new Point[4];
// 第一个顶点:对应minV和minN
factorV = minV / lengthSquared;
factorN = minN / lengthSquared;
rect[0] = new Point(p1.x + factorV * dx + factorN * nx, p1.y + factorV * dy + factorN * ny);
// 第二个顶点:minV和maxN
factorV = minV / lengthSquared;
factorN = maxN / lengthSquared;
rect[1] = new Point(p1.x + factorV * dx + factorN * nx, p1.y + factorV * dy + factorN * ny);
// 第三个顶点:maxV和maxN
factorV = maxV / lengthSquared;
factorN = maxN / lengthSquared;
rect[2] = new Point(p1.x + factorV * dx + factorN * nx, p1.y + factorV * dy + factorN * ny);
// 第四个顶点:maxV和minN
factorV = maxV / lengthSquared;
factorN = minN / lengthSquared;
rect[3] = new Point(p1.x + factorV * dx + factorN * nx, p1.y + factorV * dy + factorN * ny);
但是,注意:这样计算出来的点是否构成矩形?而且,我们是以p1为参考点的,但投影值minV, maxV, minN, maxN都是相对于p1的。这个公式成立吗?
13. 实际上,上述计算顶点的方法可能不是最优的,而且容易出错。另一种方法是利用投影的极值点,但是投影的极值点可能不是同一个点,我们需要组合。不过,在旋转卡壳中,通常我们记录极值点,然后通过极值点来构造矩形。
14. 简化:我们并不需要实时计算矩形的四个顶点,可以在找到最小面积矩形时再计算。而且,我们可以通过四个极值点(即投影极值对应的点)来构造矩形的边,但注意,矩形的边与当前边平行或垂直。
15. 然而,上述方法中,我们记录的是投影值,而不是点。所以我们需要在计算投影的过程中记录是哪些点产生了极值。但是,由于凸包是凸的,每个投影方向上的极值点都是凸包上的顶点,所以我们可以记录这些点的索引。
16. 修改内层循环:在计算投影时,记录达到minV, maxV, minN, maxN的点。但是注意,可能有多个点同时达到极值,但任取一个即可。
17. 但是,在旋转卡壳算法中,通常我们不需要显式地记录这些点,而是通过当前边的索引和另外三个卡壳点(通过旋转卡壳指针移动)来得到。不过,上述方法已经遍历了所有点,所以我们可以同时记录极值点。
18. 因此,我们可以在内层循环中记录四个极值点(minVPoint, maxVPoint, minNPoint, maxNPoint)。
19. 然后,矩形的四个顶点由这四个点决定?不对。因为矩形是轴对齐的(在当前坐标系下,即v和n构成的坐标系),所以矩形的四个顶点是这四个点投影形成的矩形的四个角。但是,这四个角不一定在原始点集上,而是需要计算。
20. 实际上,矩形的四个顶点可以通过四个极值点来构造:即分别由minV和minN、minV和maxN、maxV和minN、maxV和maxN这四个组合,然后通过反投影得到坐标。反投影公式:
P = p1 + (projV * v) / |v|^2 + (projN * n) / |v|^2
其中,projV和projN是投影值。
21. 因此,我们可以用上述公式计算四个顶点。
22. 但是,注意:我们之前计算的投影值minV, maxV, minN, maxN是相对于点p1的。所以,对于任意投影值(projV, projN),其对应的点为:
P = p1 + (projV * v + projN * n) / |v|^2
23. 所以,我们可以这样计算矩形的四个顶点(如第12点所示)。
24. 但是,这样计算出来的矩形是否就是外接矩形?是的,因为在这个局部坐标系下,矩形的边与v和n平行,而且投影值覆盖了整个凸包。
25. 注意:我们计算投影时,是以p1为原点的。所以,对于凸包上的点,其投影值可能为负。
26. 实现细节:
- 凸包计算:注意凸包点的顺序(逆时针或顺时针),我们的算法要求凸包点按逆时针顺序(这样法向量向外,但这里不影响投影计算)。
- 避免重复计算:在循环中,每次都要遍历所有点,所以时间复杂度为O(n^2)。但是,凸包的点数通常不多,而且有O(n)的旋转卡壳方法(通过双指针移动,因为凸包的有序性,极值点也是按顺序变化的)。我们可以优化内层循环,使用旋转卡壳技巧将内层循环优化到O(1)。
27. 优化:旋转卡壳的经典实现中,在遍历边时,其他三个极值点(左、右、对踵点)可以通过指针的移动来更新,而不需要每次遍历所有点。具体:
- 我们维护三个指针(除了当前边的两个点外),分别代表在v方向上的极值点(实际上就是当前边的两个端点,因为v方向就是边的方向,所以最大投影是当前边的终点,最小投影是起点?不对,因为投影是相对于p1的,所以p1的投影为0,p2的投影为|v|^2。但是,其他点可能投影更大或更小?所以我们需要计算整个凸包的投影极值。
- 但是,由于凸包的有序性,当我们旋转边时,投影极值点也是按顺序移动的。因此,我们可以用双指针技术,在O(n)时间内完成。
28. 经典旋转卡壳算法步骤(最小外接矩形):
a) 初始化:凸包点逆时针排列,记为P[0..n-1]。
b) 设置三个指针:left, bottom, right。初始时,left=0, bottom=0, right=0。同时,我们计算初始方向(如第一条边)下,凸包在x轴和y轴上的极值点(即最左、最下、最右点)。但是,这里我们以当前边为x轴。
c) 遍历每条边i(从0到n-1):
1) 当前边为P[i]到P[i+1](模n)。
2) 计算当前边的方向向量v。
3) 移动指针:当点P[right]在v方向上的投影小于下一个点(P[right+1])的投影时,right++(模n)。同理,移动left和bottom?实际上,我们需要移动三个指针:分别对应与当前边垂直的两个方向(法向)和当前边方向?不对。
29. 实际上,经典算法中,最小外接矩形通过三个卡壳点:一个在对面(对踵点),两个在侧边。但这里我们不详细展开优化,因为即使不优化,凸包点数不多时,O(n^2)也是可以接受的。
30. 考虑到实现简单,我们先使用O(n^2)的方法,因为凸包点数通常不会太多(如果输入点很多,但凸包点数可以控制)。
31. 非凸多边形:先求凸包。
32. 代码结构:
class Point {
double x, y;
Point(double x, double y) { this.x = x; this.y = y; }
}
// 计算凸包
public static List<Point> convexHull(List<Point> points) { ... }
// 计算最小外接矩形(返回矩形顶点,四个点,按逆时针顺序)
public static Point[] minBoundingRectangle(List<Point> points) {
// 1. 计算凸包
List<Point> hull = convexHull(points);
if (hull.size() < 3) {
// 点少于3,无法构成凸包,直接处理
// 如果只有一个点,则矩形退化为点;两个点则为线段
// 这里我们假设凸包点数>=3
// 对于小于3的情况,可以特殊处理,但这里省略
}
int n = hull.size();
// 将凸包转为数组,并确保逆时针(我们的凸包算法保证逆时针)
Point[] hullPoints = hull.toArray(new Point[0]);
double minArea = Double.MAX_VALUE;
Point[] minRect = null; // 四个点
// 遍历每条边
for (int i = 0; i < n; i++) {
int j = (i+1) % n;
Point p1 = hullPoints[i];
Point p2 = hullPoints[j];
double dx = p2.x - p1.x;
double dy = p2.y - p1.y;
double lengthSquared = dx*dx + dy*dy;
if (lengthSquared < 1e-12) continue; // 跳过零长度边
double nx = -dy; // 法向量
double ny = dx;
double minV = 0, maxV = 0;
double minN = 0, maxN = 0;
// 初始化,用第一个点(即p1)的投影
// 注意:以p1为参考点,所以p1的投影为0
minV = maxV = 0; // 因为p1的投影为0
minN = maxN = 0;
// 遍历凸包上所有点
for (int k = 0; k < n; k++) {
Point p = hullPoints[k];
// 计算相对于p1的向量
double vx = p.x - p1.x;
double vy = p.y - p1.y;
// 投影到v
double projV = vx * dx + vy * dy;
// 投影到n
double projN = vx * nx + vy * ny;
if (k == 0) {
minV = maxV = projV;
minN = maxN = projN;
} else {
if (projV < minV) minV = projV;
if (projV > maxV) maxV = projV;
if (projN < minN) minN = projN;
if (projN > maxN) maxN = projN;
}
}
// 计算面积
double area = (maxV - minV) * (maxN - minN) / lengthSquared;
if (area < minArea) {
minArea = area;
// 计算矩形的四个顶点
// 公式:顶点 = p1 + (projV * v + projN * n) / lengthSquared
Point[] rect = new Point[4];
// 注意:四个顶点的投影组合: (minV, minN), (minV, maxN), (maxV, maxN), (maxV, minN)
double[] projVs = {minV, minV, maxV, maxV};
double[] projNs = {minN, maxN, maxN, minN};
for (int idx = 0; idx < 4; idx++) {
double vFactor = projVs[idx] / lengthSquared;
double nFactor = projNs[idx] / lengthSquared;
double x = p1.x + vFactor * dx + nFactor * nx;
double y = p1.y + vFactor * dy + nFactor * ny;
rect[idx] = new Point(x, y);
}
minRect = rect;
}
}
return minRect;
}
33. 凸包算法:这里使用Andrew's Monotone Chain算法,返回逆时针凸包点(不包含重复点,且首尾不连接)。
34. 编写凸包算法:
public static List<Point> convexHull(List<Point> points) {
if (points.size() <= 1)
return points;
// 按x坐标排序,如果x相同按y
Collections.sort(points, (a, b) -> {
if (a.x < b.x) return -1;
if (a.x > b.x) return 1;
return Double.compare(a.y, b.y);
});
List<Point> lower = new ArrayList<>();
List<Point> upper = new ArrayList<>();
// 下凸包
for (Point p : points) {
while (lower.size() >= 2) {
Point a = lower.get(lower.size()-2);
Point b = lower.get(lower.size()-1);
if (crossProduct(a, b, p) <= 0) { // 顺时针或共线
lower.remove(lower.size()-1);
} else {
break;
}
}
lower.add(p);
}
// 上凸包
for (int i = points.size()-1; i>=0; i--) {
Point p = points.get(i);
while (upper.size() >= 2) {
Point a = upper.get(upper.size()-2);
Point b = upper.get(upper.size()-1);
if (crossProduct(a, b, p) <= 0) {
upper.remove(upper.size()-1);
} else {
break;
}
}
upper.add(p);
}
// 合并:去掉首尾重复点(下凸包最后一个点是上凸包的第一个点,上凸包最后一个点是下凸包的第一个点)
lower.remove(lower.size()-1);
upper.remove(upper.size()-1);
lower.addAll(upper);
return lower;
}
// 计算叉积:向量ab和向量ac的叉积
private static double crossProduct(Point a, Point b, Point c) {
return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
}
35. 注意:凸包算法返回的点是逆时针顺序,且起点是x最小(如果x相同则y最小)的点。
36. 测试:使用一些简单多边形测试,比如正方形、三角形等。
37. 注意:如果输入点共线,则凸包退化为线段,此时最小外接矩形会退化为一条线段(面积为0)。但算法仍然可以处理。
38. 以上代码没有考虑浮点精度问题,实际中可能需要调整。
39. 总结:先求凸包,然后遍历凸包的每条边,计算以该边为基准的矩形,记录最小面积的矩形。
40. 输出:矩形的四个顶点(按逆时针顺序)。
41. 注意:由于浮点运算,可能会出现精度问题,比如面积计算为负数,所以需要确保投影极值计算正确。
42. 另一种方法:使用现成的库,如Java3D或JTS,但这里我们要求自己实现。
43. 实现代码:按照上述结构编写Java代码。
44. 由于代码较长,我们这里只提供关键函数,并假设Point类已定义。
45. 注意:在计算投影时,我们以p1为参考点,这样p1的投影为0,其他点相对p1。
46. 最后,返回的矩形顶点顺序:我们按 (minV,minN), (minV,maxN), (maxV,maxN), (maxV,minN) 的顺序,这样是逆时针(取决于v和n的旋转方向,但这里n是v逆时针转90度,所以这样得到的四个点是逆时针的)。
47. 验证:例如,一个正方形区域,凸包就是四个点,最小外接矩形应该就是它自己。
48. 开始编写代码。
注意:由于这是一个算法实现,我们提供完整的函数,但需要用户自己整合到项目中。
代码实现:</think>### Java实现不规则区域最小外接四边形算法
在计算机图形学中,寻找不规则区域的最小外接四边形(最小面积外接矩形)是一个常见问题。以下是完整的Java实现方案,采用**旋转卡壳法**(Rotating Calipers)结合**凸包计算**,确保高效精确地找到最小面积外接矩形。
---
#### 算法步骤
1. **计算凸包**:将不规则区域转换为凸多边形(Andrew's Monotone Chain算法)。
2. **旋转卡壳法**:遍历凸包每条边作为矩形基准边,计算最小外接矩形面积。
3. **投影计算**:对每个基准边计算凸包点的投影极值,确定矩形边界。
4. **顶点生成**:根据投影值计算矩形的四个顶点。
---
#### Java代码实现
```java
import java.util.*;
class Point {
public double x, y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return String.format("(%.2f, %.2f)", x, y);
}
}
public class MinimumBoundingQuadrilateral {
// 计算叉积:向量AB × 向量AC
private static double crossProduct(Point a, Point b, Point c) {
return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
}
// 计算凸包(Andrew's Monotone Chain算法)
public static List<Point> convexHull(List<Point> points) {
if (points.size() <= 1) return points;
// 按x坐标排序(x相同则按y)
Collections.sort(points, (a, b) ->
Double.compare(a.x, b.x) != 0 ?
Double.compare(a.x, b.x) :
Double.compare(a.y, b.y)
);
List<Point> lower = new ArrayList<>();
List<Point> upper = new ArrayList<>();
// 计算下凸包
for (Point p : points) {
while (lower.size() >= 2) {
Point a = lower.get(lower.size() - 2);
Point b = lower.get(lower.size() - 1);
if (crossProduct(a, b, p) <= 0) {
lower.remove(lower.size() - 1);
} else break;
}
lower.add(p);
}
// 计算上凸包
for (int i = points.size() - 1; i >= 0; i--) {
Point p = points.get(i);
while (upper.size() >= 2) {
Point a = upper.get(upper.size() - 2);
Point b = upper.get(upper.size() - 1);
if (crossProduct(a, b, p) <= 0) {
upper.remove(upper.size() - 1);
} else break;
}
upper.add(p);
}
// 合并凸包(去除重复端点)
lower.remove(lower.size() - 1);
upper.remove(upper.size() - 1);
lower.addAll(upper);
return lower;
}
// 计算最小外接矩形(返回矩形的四个顶点)
public static Point[] minBoundingRectangle(List<Point> points) {
// 1. 计算凸包
List<Point> hull = convexHull(points);
int n = hull.size();
if (n < 3) throw new IllegalArgumentException("区域点数不足");
double minArea = Double.MAX_VALUE;
Point[] minRect = null;
// 2. 遍历凸包的每条边
for (int i = 0; i < n; i++) {
Point p1 = hull.get(i);
Point p2 = hull.get((i + 1) % n);
// 计算基准边向量
double dx = p2.x - p1.x;
double dy = p2.y - p1.y;
double lengthSq = dx * dx + dy * dy;
if (lengthSq < 1e-12) continue; // 跳过零长边
// 计算法向量(逆时针旋转90度)
double nx = -dy;
double ny = dx;
// 3. 计算投影极值
double minV = 0, maxV = 0;
double minN = 0, maxN = 0;
for (int k = 0; k < n; k++) {
Point p = hull.get(k);
double vx = p.x - p1.x;
double vy = p.y - p1.y;
// 投影到基准边向量
double projV = vx * dx + vy * dy;
// 投影到法向量
double projN = vx * nx + vy * ny;
// 更新极值
if (k == 0 || projV < minV) minV = projV;
if (k == 0 || projV > maxV) maxV = projV;
if (k == 0 || projN < minN) minN = projN;
if (k == 0 || projN > maxN) maxN = projN;
}
// 4. 计算矩形面积
double area = (maxV - minV) * (maxN - minN) / lengthSq;
if (area < minArea) {
minArea = area;
// 5. 计算矩形顶点
Point[] rect = new Point[4];
double[] projVs = {minV, minV, maxV, maxV};
double[] projNs = {minN, maxN, maxN, minN};
for (int j = 0; j < 4; j++) {
double factorV = projVs[j] / lengthSq;
double factorN = projNs[j] / lengthSq;
double x = p1.x + factorV * dx + factorN * nx;
double y = p1.y + factorV * dy + factorN * ny;
rect[j] = new Point(x, y);
}
minRect = rect;
}
}
return minRect;
}
// 示例用法
public static void main(String[] args) {
// 定义不规则区域(示例:L形多边形)
List<Point> points = Arrays.asList(
new Point(0, 0),
new Point(2, 0),
new Point(2, 1),
new Point(1, 1),
new Point(1, 2),
new Point(0, 2)
);
// 计算最小外接矩形
Point[] boundingRect = minBoundingRectangle(points);
// 输出结果
System.out.println("最小外接矩形的四个顶点:");
for (Point p : boundingRect) {
System.out.println(p);
}
}
}
```
---
#### 算法说明
1. **凸包计算**:
- 使用Andrew's Monotone Chain算法,时间复杂度 $O(n \log n)$。
- 将输入点集转换为凸多边形,确保后续计算正确性[^1]。
2. **旋转卡壳法**:
- 遍历凸包每条边作为矩形基准边。
- 对每条边计算凸包点的投影极值(基准边方向和法线方向)。
- 根据投影极值计算矩形面积,记录最小面积对应的矩形。
3. **顶点生成**:
- 通过投影值反推矩形的四个顶点坐标:
$$
\text{顶点} = P_1 + \frac{\text{proj}_V \cdot \vec{V} + \text{proj}_N \cdot \vec{N}}{|\vec{V}|^2}
$$
- 顶点按逆时针顺序排列,便于后续图形处理。
4. **复杂度**:
- 凸包计算:$O(n \log n)$
- 旋转卡壳:$O(n^2)$(凸包点数通常远小于原始点数)
---
#### 测试用例
输入一个L形多边形:
```java
List<Point> points = Arrays.asList(
new Point(0, 0),
new Point(2, 0),
new Point(2, 1),
new Point(1, 1),
new Point(1, 2),
new Point(0, 2)
);
```
输出结果:
```
最小外接矩形的四个顶点:
(0.00, 0.00)
(0.00, 2.00)
(2.00, 2.00)
(2.00, 0.00)
```
---
#### 应用场景
- **计算机视觉**:目标检测中的边界框生成。
- **地理信息系统**:地图要素的最小包围框计算。
- **游戏开发**:碰撞检测的包围盒生成。
- **工业设计**:零件的最小包装盒计算。