定义
旋转卡壳(Rotating Calipers)是一种基于几何优化的算法设计技术,主要用于在凸包上高效求解各类最值问题。
其核心思想是:在凸多边形外部构造两条平行线,使其与凸多边形相切,然后旋转这两条线,在旋转过程中维护某种极值关系。由于凸多边形的特殊性质,每个顶点最多被访问两次,因此总时间复杂度为 。
旋转卡壳可以解决以下问题:
- 凸包直径(最远点对)
- 凸包宽度
- 最小外接矩形
- 凸多边形直径
核心原理
对跖点性质
对于凸多边形上的任意一条边,其对跖点(antipodal point)是凸多边形上距离该边所在直线最远的顶点。当一条边旋转时,其对跖点沿着凸包边界单调移动。
算法流程
设凸包上有两条平行线 和 :
- 初始化时, 固定在某一条边, 找到与其平行的边
- 旋转 ,同时移动 寻找对跖点
- 在旋转过程中更新答案
- 当 旋转一周后,算法结束
复杂度分析
由于凸多边形的每个顶点最多作为对跖点被访问两次,因此总时间复杂度为 ,空间复杂度为 。
典型应用
凸包直径(最远点对)
给定平面上的 个点,求距离最远的两点之间的距离。
最远点对必然位于凸包的某两个顶点上。旋转卡壳在 时间内找到所有对跖点对,并维护最大距离。
凸包宽度
凸包宽度定义为凸多边形的一对平行支撑线之间的最小距离。通过旋转卡壳可以在 时间内找到凸包的宽度。
最小外接矩形
最小面积外接矩形(Minimum Area Bounding Rectangle)可以通过旋转卡壳求解。枚举凸包每条边作为矩形的某一边,构造与之平行的支撑线即可。
凸多边形直径
对于任意凸多边形,距离最远的两点必然在顶点上。旋转卡壳同样适用。
代码实现
凸包直径
#include <bits/stdc++.h>
using namespace std;
struct Point {
long long x, y;
bool operator<(const Point& other) const {
if (x != other.x) return x < other.x;
return y < other.y;
}
bool operator==(const Point& other) const {
return x == other.x && y == other.y;
}
};
Point operator-(const Point& a, const Point& b) {
return {a.x - b.x, a.y - b.y};
}
long long cross(const Point& a, const Point& b) {
return a.x * b.y - a.y * b.x;
}
long long cross(const Point& o, const Point& a, const Point& b) {
return cross(a - o, b - o);
}
long long dist2(const Point& a, const Point& b) {
long long dx = a.x - b.x;
long long dy = a.y - b.y;
return dx * dx + dy * dy;
}
// Andrew算法求凸包
vector<Point> convexHull(vector<Point>& pts) {
sort(pts.begin(), pts.end());
pts.erase(unique(pts.begin(), pts.end()), pts.end());
int n = pts.size();
if (n <= 1) return pts;
vector<Point> hull;
// 下半部分
for (int i = 0; i < n; i++) {
while (hull.size() >= 2 && cross(hull[hull.size()-2], hull[hull.size()-1], pts[i]) <= 0) {
hull.pop_back();
}
hull.push_back(pts[i]);
}
// 上半部分
int lower = hull.size();
for (int i = n - 2; i >= 0; i--) {
while (hull.size() > lower && cross(hull[hull.size()-2], hull[hull.size()-1], pts[i]) <= 0) {
hull.pop_back();
}
hull.push_back(pts[i]);
}
hull.pop_back();
return hull;
}
// 旋转卡壳求直径
long long rotatingCalipersDiameter(vector<Point>& hull) {
int n = hull.size();
if (n == 1) return 0;
if (n == 2) return dist2(hull[0], hull[1]);
long long maxDist = 0;
int j = 1;
for (int i = 0; i < n; i++) {
// 找到与边 (i, i+1) 对应的对跖点
int ni = (i + 1) % n;
Point edge = hull[ni] - hull[i];
while (abs(cross(edge, hull[(j + 1) % n] - hull[j])) > abs(cross(edge, hull[j] - hull[i]))) {
j = (j + 1) % n;
}
// 更新答案
maxDist = max(maxDist, dist2(hull[i], hull[j]));
maxDist = max(maxDist, dist2(hull[ni], hull[j]));
}
return maxDist;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<Point> pts(n);
for (int i = 0; i < n; i++) {
cin >> pts[i].x >> pts[i].y;
}
vector<Point> hull = convexHull(pts);
long long diameter = rotatingCalipersDiameter(hull);
cout << diameter << endl;
return 0;
}求最小外接矩形
long long rotatingCalipersMinAreaRectangle(vector<Point>& hull) {
int n = hull.size();
if (n == 1) return 0;
if (n == 2) {
long long d = dist2(hull[0], hull[1]);
return d; // 面积为0,但返回距离平方
}
long long minArea = LLONG_MAX;
int j = 1, k = 1;
for (int i = 0; i < n; i++) {
int ni = (i + 1) % n;
Point edge = hull[ni] - hull[i];
// 找宽度方向的对跖点
while (abs(cross(edge, hull[(j + 1) % n] - hull[j])) > abs(cross(edge, hull[j] - hull[i]))) {
j = (j + 1) % n;
}
// 找高度方向的对跖点
while (cross(edge, hull[(k + 1) % n] - hull[k]) > cross(edge, hull[k] - hull[i])) {
k = (k + 1) % n;
}
// 计算当前矩形的宽和高
long long width2 = dist2(hull[i], hull[ni]);
long long height = abs(cross(edge, hull[j] - hull[i])) / sqrt(width2);
long long width = abs(cross(edge, hull[k] - hull[i])) / sqrt(width2);
minArea = min(minArea, width * height);
}
return minArea;
}与凸包的关系
旋转卡壳算法依赖于凸包的存在,其工作流程如下:
- 构建凸包:使用 Andrew’s 算法或 Graham’s 算法在 时间内构建凸包
- 旋转卡壳:在凸包上执行旋转卡壳,通常是 的追加操作
因此,总时间复杂度为 ,其中 为输入点数。旋转卡壳是凸包算法的自然延伸,许多凸包相关问题都可以通过旋转卡壳高效解决。1
例题
【模板】旋转卡壳
题目描述:给定 个平面上的点 ,求最远点对的距离平方。
输入格式:第一行包含整数 ,接下来 行每行包含两个整数 。
输出格式:输出最远点对距离的平方。
解题思路:
- 使用 Andrew 算法构建凸包
- 使用旋转卡壳求凸包直径
- 输出直径(即最远点对距离的平方)
参考实现:见上方代码实现部分。
参考资料
Footnotes
-
本页面内容参考 CP-Algorithms - Rotating Calipers 及 Codeforces 相关博客。 ↩