引言
旋转卡壳(Rotating Calipers)是计算几何中一种优雅而高效的算法技术,由Shamos于1978年提出。它常用于解决与凸包相关的各种问题,如凸包直径、最小面积外接矩形、凸包间距离等。算法的核心思想如同用一对平行线”夹住”凸包,然后绕凸包旋转这对平行线,在旋转过程中统计所需信息。
算法原理
1. 对踵点(Antipodal Points)
对于凸多边形的每条边,都存在一个或两个顶点,使得过该顶点与边平行的直线距离这条边最远。这样的顶点与边(或边的两个端点)称为对踵点对。
2. 旋转过程
- 先计算点集的凸包(使用Graham扫描或Andrew单调链)
- 找到初始对踵点对(如y坐标最小和最大的点)
- 依次旋转平行线,每次旋转角度等于当前边与下一条边的夹角
- 在旋转过程中维护所需的最优值
3. 核心性质
- 对踵点对的数量不超过3n/2个(n为凸包顶点数)
- 可在O(n)时间内找到所有对踵点对
Java实现
import java.util.*;
/**
* 旋转卡壳算法实现
* 用于求解凸包直径、最小外接矩形等问题
*/
public class RotatingCalipers {
/**
* 二维点类
*/
static class Point {
double x, y;
Point(double x, double y) { this.x = x; this.y = y; }
@Override
public String toString() {
return String.format("(%.2f, %.2f)", x, y);
}
}
/**
* 向量减法
*/
static Point sub(Point a, Point b) {
return new Point(a.x - b.x, a.y - b.y);
}
/**
* 叉积:OA × OB
*/
static double cross(Point o, Point a, Point b) {
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
}
/**
* 向量叉积
*/
static double cross(Point a, Point b) {
return a.x * b.y - a.y * b.x;
}
/**
* 点积
*/
static double dot(Point a, Point b) {
return a.x * b.x + a.y * b.y;
}
/**
* 向量长度平方
*/
static double dist2(Point a, Point b) {
double dx = a.x - b.x, dy = a.y - b.y;
return dx * dx + dy * dy;
}
/**
* 向量长度
*/
static double dist(Point a, Point b) {
return Math.sqrt(dist2(a, b));
}
/**
* Andrew单调链算法求凸包
* 时间复杂度:O(n log n)
*/
static List<Point> convexHull(List<Point> points) {
int n = points.size();
if (n <= 1) return new ArrayList<>(points);
// 按x排序,x相同按y排序
List<Point> sorted = new ArrayList<>(points);
sorted.sort((a, b) -> {
if (Math.abs(a.x - b.x) > 1e-9) return Double.compare(a.x, b.x);
return Double.compare(a.y, b.y);
});
List<Point> hull = new ArrayList<>();
// 下凸包
for (Point p : sorted) {
while (hull.size() >= 2 &&
cross(hull.get(hull.size()-2), hull.get(hull.size()-1), p) <= 1e-9) {
hull.remove(hull.size() - 1);
}
hull.add(p);
}
// 上凸包
int lowerSize = hull.size();
for (int i = n - 2; i >= 0; i--) {
Point p = sorted.get(i);
while (hull.size() > lowerSize &&
cross(hull.get(hull.size()-2), hull.get(hull.size()-1), p) <= 1e-9) {
hull.remove(hull.size() - 1);
}
hull.add(p);
}
hull.remove(hull.size() - 1); // 移除重复的第一个点
return hull;
}
/**
* 旋转卡壳求凸包直径(最远点对距离)
* 时间复杂度:O(n),n为凸包顶点数
*/
static double diameter(List<Point> hull) {
int n = hull.size();
if (n == 0) return 0;
if (n == 1) return 0;
if (n == 2) return dist(hull.get(0), hull.get(1));
// 找到y坐标最大和最小的点作为初始对踵点
int j = 1;
double maxDist = 0;
for (int i = 0; i < n; i++) {
int ni = (i + 1) % n;
// 当i-j边与j+1点的距离不增加时停止
while (true) {
int nj = (j + 1) % n;
double cur = Math.abs(cross(hull.get(i), hull.get(ni), hull.get(j)));
double nxt = Math.abs(cross(hull.get(i), hull.get(ni), hull.get(nj)));
if (nxt > cur + 1e-9) {
j = nj;
} else {
break;
}
}
// 更新最大距离
double d = dist2(hull.get(i), hull.get(j));
if (d > maxDist) maxDist = d;
d = dist2(hull.get(ni), hull.get(j));
if (d > maxDist) maxDist = d;
}
return Math.sqrt(maxDist);
}
/**
* 旋转卡壳求最小面积外接矩形
* 返回:{最小面积, 最小周长, 矩形四个顶点}
*/
static Object[] minAreaEnclosingRectangle(List<Point> hull) {
int n = hull.size();
if (n <= 1) return new Object[]{0.0, 0.0, null};
if (n == 2) {
double len = dist(hull.get(0), hull.get(1));
return new Object[]{0.0, 2 * len, null};
}
// 预计算每条边的方向向量长度
int[] nxt = new int[n];
for (int i = 0; i < n; i++) nxt[i] = (i + 1) % n;
// 找到四条边上的最远点(初始状态)
int i = 0, j = 0, k = 0, l = 0;
// 找到x最大、y最大、x最小的点
for (int idx = 1; idx < n; idx++) {
if (hull.get(idx).x > hull.get(i).x) i = idx;
if (hull.get(idx).y > hull.get(j).y) j = idx;
if (hull.get(idx).x < hull.get(k).x) k = idx;
if (hull.get(idx).y < hull.get(l).y) l = idx;
}
double minArea = Double.MAX_VALUE;
double minPerimeter = Double.MAX_VALUE;
List<Point> bestRect = null;
// 当前四条边的角度(与x轴的夹角)
double[] angles = new double[n];
for (int idx = 0; idx < n; idx++) {
Point edge = sub(hull.get(nxt[idx]), hull.get(idx));
angles[idx] = Math.atan2(edge.y, edge.x);
}
// 旋转n次,每次处理一条边
for (int edgeIdx = 0; edgeIdx < n; edgeIdx++) {
Point edge = sub(hull.get(nxt[edgeIdx]), hull.get(edgeIdx));
double edgeLen = Math.sqrt(dot(edge, edge));
Point unitEdge = new Point(edge.x / edgeLen, edge.y / edgeLen);
Point perp = new Point(-unitEdge.y, unitEdge.x); // 垂直单位向量
// 计算四个方向的投影范围
double minProj = Double.MAX_VALUE, maxProj = -Double.MAX_VALUE;
double minPerp = Double.MAX_VALUE, maxPerp = -Double.MAX_VALUE;
for (Point p : hull) {
double proj = dot(sub(p, hull.get(0)), unitEdge);
double perpProj = dot(sub(p, hull.get(0)), perp);
minProj = Math.min(minProj, proj);
maxProj = Math.max(maxProj, proj);
minPerp = Math.min(minPerp, perpProj);
maxPerp = Math.max(maxPerp, perpProj);
}
double width = maxProj - minProj;
double height = maxPerp - minPerp;
double area = width * height;
double perimeter = 2 * (width + height);
if (area < minArea) {
minArea = area;
minPerimeter = perimeter;
// 计算矩形四个顶点
bestRect = new ArrayList<>();
Point origin = hull.get(0);
Point base = new Point(origin.x + minProj * unitEdge.x + minPerp * perp.x,
origin.y + minProj * unitEdge.y + minPerp * perp.y);
bestRect.add(base);
bestRect.add(new Point(base.x + width * unitEdge.x, base.y + width * unitEdge.y));
bestRect.add(new Point(base.x + width * unitEdge.x + height * perp.x,
base.y + width * unitEdge.y + height * perp.y));
bestRect.add(new Point(base.x + height * perp.x, base.y + height * perp.y));
}
}
return new Object[]{minArea, minPerimeter, bestRect};
}
public static void main(String[] args) {
// 测试案例:随机点集
List<Point> points = new ArrayList<>();
points.add(new Point(0, 0));
points.add(new Point(2, 0));
points.add(new Point(3, 1));
points.add(new Point(2, 2));
points.add(new Point(0, 2));
points.add(new Point(1, 1)); // 内部点
List<Point> hull = convexHull(points);
System.out.println("凸包顶点:" + hull.size() + "个");
for (Point p : hull) System.out.println(" " + p);
double diam = diameter(hull);
System.out.println("凸包直径:" + String.format("%.4f", diam));
Object[] result = minAreaEnclosingRectangle(hull);
System.out.println("最小外接矩形面积:" + String.format("%.4f", (Double)result[0]));
System.out.println("最小外接矩形周长:" + String.format("%.4f", (Double)result[1]));
}
}
复杂度分析
| 问题 | 时间复杂度 | 说明 |
|---|---|---|
| 凸包构造 | O(n log n) | Andrew单调链 |
| 凸包直径 | O(n) | 旋转卡壳 |
| 最小外接矩形 | O(n) | 旋转卡壳 |
| 凸包间距离 | O(n + m) | 两个凸包各旋转一次 |
应用场景
- 碰撞检测:计算物体包围盒,快速排除不相交的情况
- 模式识别:形状描述与特征提取
- 计算机视觉:物体定位与姿态估计
- GIS系统:区域覆盖与边界分析
总结
旋转卡壳算法展示了计算几何中”旋转扫描”思想的强大威力。通过与凸包结合,它能在O(n)时间内解决许多看似复杂的几何问题。掌握这一技巧对于处理二维几何问题具有重要价值。