1. 二维凸包问题概述
二维凸包(Convex Hull)是计算几何中的经典问题,要求给定平面点集的最小凸多边形,使得所有点都在多边形内部或边上。这个问题在实际应用中非常广泛,比如机器人路径规划、图像处理中的物体轮廓识别、地理信息系统中的区域划分等。
洛谷P2742题目"Fencing the Cows"正是USACO训练题库中的一道经典二维凸包问题。题目描述很简单:农夫约翰需要建造一个围栏来围住他的所有奶牛,要求围栏长度尽可能短。这实际上就是求这些奶牛位置点的二维凸包。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 凸包算法比较与选择
2.1 常见凸包算法
解决二维凸包问题主要有以下几种算法:
- 暴力法(Brute Force):时间复杂度O(n^4),完全不实用
- Jarvis步进法(Gift Wrapping):时间复杂度O(nh),h是凸包上的点数
- Graham扫描法:时间复杂度O(nlogn)
- Andrew算法(单调链法):时间复杂度O(nlogn)
- 分治法:时间复杂度O(nlogn)
2.2 算法选择建议
对于USACO竞赛和洛谷P2742这样的题目,推荐使用Andrew算法或Graham扫描法。这两种算法的时间复杂度都是O(nlogn),在实际编程竞赛中表现优异。
Andrew算法相比Graham扫描法有几个优势:
- 不需要计算极角,避免了浮点数精度问题
- 代码实现更简洁
- 排序规则更简单(先按x坐标,x相同按y坐标)
3. Andrew算法详解
3.1 算法步骤
Andrew算法的核心思想是通过维护上下凸链来构造凸包:
- 将所有点按照x坐标从小到大排序(x相同则按y坐标)
- 构造下凸链:
- 初始化空栈
- 依次加入每个点,检查最后三个点是否构成"右转",如果是则删除中间点
- 构造上凸链:
- 从右向左处理点集
- 类似下凸链的方法,但检查"左转"
- 合并上下凸链(注意去除重复的端点)
3.2 关键代码实现
以下是Andrew算法的C++实现核心部分:
cpp复制struct Point {
double x, y;
bool operator<(const Point& p) const {
return x < p.x || (x == p.x && y < p.y);
}
};
// 计算叉积
double cross(const Point& O, const Point& A, const Point& B) {
return (A.x-O.x)*(B.y-O.y) - (A.y-O.y)*(B.x-O.x);
}
vector<Point> convexHull(vector<Point> points) {
int n = points.size(), k = 0;
if (n <= 1) return points;
vector<Point> hull(2*n);
// 构造下凸链
sort(points.begin(), points.end());
for (int i = 0; i < n; ++i) {
while (k >= 2 && cross(hull[k-2], hull[k-1], points[i]) <= 0) k--;
hull[k++] = points[i];
}
// 构造上凸链
for (int i = n-2, t = k+1; i >= 0; --i) {
while (k >= t && cross(hull[k-2], hull[k-1], points[i]) <= 0) k--;
hull[k++] = points[i];
}
hull.resize(k-1);
return hull;
}
3.3 算法正确性证明
Andrew算法的正确性基于以下观察:
- 排序后,最左和最右的点必定在凸包上
- 下凸链维护的是从最左点到最右点的下半部分凸包
- 上凸链维护的是从最右点到最左点的上半部分凸包
- 合并后就得到完整的凸包
4. 洛谷P2742题解
4.1 题目分析
题目要求计算给定点集的凸包周长。根据Andrew算法求出凸包顶点后,只需计算这些顶点依次连接形成的多边形的周长即可。
4.2 完整AC代码
cpp复制#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
#include <iomanip>
using namespace std;
struct Point {
double x, y;
bool operator<(const Point& p) const {
return x < p.x || (x == p.x && y < p.y);
}
};
double cross(const Point& O, const Point& A, const Point& B) {
return (A.x-O.x)*(B.y-O.y) - (A.y-O.y)*(B.x-O.x);
}
double dist(const Point& a, const Point& b) {
return sqrt((a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y));
}
double convexHullPerimeter(vector<Point> points) {
int n = points.size(), k = 0;
if (n <= 1) return 0;
vector<Point> hull(2*n);
sort(points.begin(), points.end());
for (int i = 0; i < n; ++i) {
while (k >= 2 && cross(hull[k-2], hull[k-1], points[i]) <= 0) k--;
hull[k++] = points[i];
}
for (int i = n-2, t = k+1; i >= 0; --i) {
while (k >= t && cross(hull[k-2], hull[k-1], points[i]) <= 0) k--;
hull[k++] = points[i];
}
hull.resize(k-1);
if (hull.size() == 1) return 0;
double perimeter = 0;
for (int i = 0; i < hull.size(); ++i) {
int j = (i+1) % hull.size();
perimeter += dist(hull[i], hull[j]);
}
return perimeter;
}
int main() {
int N;
cin >> N;
vector<Point> points(N);
for (int i = 0; i < N; ++i) {
cin >> points[i].x >> points[i].y;
}
double ans = convexHullPerimeter(points);
cout << fixed << setprecision(2) << ans << endl;
return 0;
}
4.3 代码解析
Point结构体定义了点的坐标和排序规则cross函数计算三点构成的向量的叉积,用于判断转向dist函数计算两点间距离convexHullPerimeter函数实现Andrew算法并计算凸包周长- 主函数处理输入输出
5. 算法优化与注意事项
5.1 浮点数精度问题
计算几何问题中浮点数精度是需要特别注意的。在本题中:
- 比较叉积时使用
<=0而不是<0,这样可以包含共线点 - 输出时保留两位小数,使用
fixed和setprecision(2) - 对于严格凸包(不包含共线点),可以改为
<0
5.2 特殊情况处理
需要处理几种特殊情况:
- 点数少于3个时,周长显然为0
- 所有点共线时,凸包退化为线段
- 重复点需要正确处理(排序后会自动处理)
5.3 性能优化
虽然Andrew算法已经是O(nlogn)复杂度,但还可以进一步优化:
- 使用内置的快速排序而非自己实现
- 预分配足够大的hull数组避免多次扩容
- 对于已知点数范围的情况,可以使用数组而非vector
6. 凸包问题的扩展应用
6.1 动态凸包
如果需要支持动态添加点并维护凸包,可以使用平衡二叉搜索树来维护凸包点集,每次插入时间复杂度O(logn)。
6.2 三维凸包
三维凸包算法更为复杂,常用的是增量法,时间复杂度O(n^2)。竞赛中较少出现。
6.3 旋转卡壳算法
基于凸包可以解决许多其他问题,如:
- 最远点对
- 最小面积外接矩形
- 最小周长外接矩形
这些都可以通过旋转卡壳算法在O(n)时间内解决。
7. 竞赛中的常见错误
在编程竞赛中解决凸包问题时,选手常犯以下错误:
- 没有正确处理共线点,导致漏掉凸包顶点或包含非顶点
- 浮点数比较时没有考虑精度问题
- 忘记排序或排序规则错误
- 没有处理点数少的特殊情况
- 计算周长时没有正确处理首尾相连
提示:在竞赛中,建议先写出凸包算法的模板代码并充分测试,比赛时可以直接使用。
