引言
在计算机图形学、地理信息系统和机器人路径规划等领域,凸包(Convex Hull)是一个基础而重要的概念。凸包指的是包含给定点集的最小凸多边形,可以想象为用橡皮筋套住所有点时形成的形状。
凸包算法在现实世界中有着广泛的应用:
- 碰撞检测:在游戏开发中快速判断物体是否可能碰撞
- 路径规划:机器人导航中确定安全区域边界
- 图像处理:形状分析和模式识别
- 地理计算:确定多个地理位置的最小包围区域
本文将深入解析凸包计算的原理,并通过C++实现详细讲解安德鲁单调链算法,帮助中级开发者全面掌握这一经典几何算法。
算法原理详解
凸包的基本概念
凸包的定义很简单:对于平面上的点集S,凸包是包含S中所有点的最小凸多边形。凸多边形的特性是连接多边形内任意两点的线段都完全包含在多边形内部。
安德鲁单调链算法
本文实现的算法基于安德鲁(Andrew)单调链算法,这是一种高效且易于理解的凸包计算方法。算法的核心思想是通过两次扫描(从左到右和从右到左)来构建凸包的上下边界。
算法步骤:
-
预处理阶段
- 将所有点按x坐标排序(x相同时按y排序)
- 去除重复点
-
构建下凸包
- 从左到右扫描排序后的点
- 使用栈结构维护凸包候选点
- 通过叉积判断点的相对位置,确保凸性
-
构建上凸包
- 从右到左扫描排序后的点
- 同样使用栈结构维护凸包候选点
- 与下凸包合并形成完整凸包
伪代码表示:
function convexHull(points):
if points.size <= 3:
return points
sort(points) // 按x坐标排序
hull = [] // 凸包点集
// 构建下凸包
for i from 0 to n-1:
while hull.size >= 2 and cross(hull[-2], hull[-1], points[i]) <= 0:
hull.pop()
hull.push(points[i])
lower_size = hull.size
// 构建上凸包
for i from n-2 down to 0:
while hull.size > lower_size and cross(hull[-2], hull[-1], points[i]) <= 0:
hull.pop()
hull.push(points[i])
hull.pop() // 移除重复的起点
return hull
C++实现逐行解析
基础数据结构
首先让我们看看算法依赖的基础几何类型定义:
// CommonTypes.h
struct Point {
double x, y;
Point(double x = 0, double y = 0) : x(x), y(y) {}
// 重载运算符便于排序和比较
bool operator<(const Point& other) const {
if (x == other.x)
return y < other.y;
return x < other.x;
}
bool operator==(const Point& other) const {
return x == other.x && y == other.y;
}
};
设计考量:
- 重载
<运算符确保点可以按x坐标排序,这是算法的基础 - 重载
==运算符便于去重操作 - 使用double类型保证计算精度
核心算法实现
叉积计算
double ConvexHull::cross(const Point &p0, const Point &p1, const Point &p2) const
{
return (p1.x - p0.x) * (p2.y - p0.y) - (p2.x - p0.x) * (p1.y - p0.y);
}
关键技巧:
- 叉积的几何意义:正值表示p0->p1->p2是逆时针旋转,负值表示顺时针旋转
- 当叉积为0时,三点共线
- 这是判断点相对位置的核心工具
去重处理
void ConvexHull::removeDuplicatePoints()
{
if (points.empty()) return;
// 使用标准库算法去重(更高效)
auto last = unique(points.begin(), points.end());
points.erase(last, points.end());
}
边界条件处理:
- 空点集直接返回,避免无效操作
- 使用STL的unique算法提高效率
凸包计算主逻辑
void ConvexHull::computeHull()
{
int n = points.size();
hull.clear();
// 边界情况处理
if (n == 0) return;
if (n <= 3) {
hull = points;
return;
}
// 1. 按x坐标排序
sort(points.begin(), points.end());
removeDuplicatePoints();
n = points.size();
// 再次检查点数
if (n <= 3) {
hull = points;
return;
}
hull.reserve(n * 2); // 预留空间优化性能
// 2. 构建下凸包(从左到右扫描)
for (int i = 0; i < n; i++) {
while (hull.size() >= 2 &&
cross(hull[hull.size() - 2], hull[hull.size() - 1], points[i]) <= 0) {
hull.pop_back();
}
hull.push_back(points[i]);
}
// 3. 构建上凸包(从右到左扫描)
size_t lower_size = hull.size();
for (int i = n - 2; i >= 0; i--) {
while (hull.size() > lower_size &&
cross(hull[hull.size() - 2], hull[hull.size() - 1], points[i]) <= 0) {
hull.pop_back();
}
hull.push_back(points[i]);
}
// 移除最后一个点(与第一个点重复)
if (hull.size() > 1) {
hull.pop_back();
}
}
算法核心解析:
- 边界处理:对于3个或更少的点,所有点都在凸包上
- 排序重要性:排序确保我们可以按顺序扫描点集
- 栈的运用:使用vector模拟栈行为,动态维护凸包边界
- 凸性检查:通过叉积判断是否需要弹出栈顶元素
- 重复点处理:最后移除重复的起点,形成闭合多边形
面积和周长计算
double ConvexHull::computeArea() const
{
if (hull.size() < 3) return 0.0;
double area = 0.0;
int n = hull.size();
for (int i = 0; i < n; i++) {
int j = (i + 1) % n;
area += hull[i].x * hull[j].y - hull[j].x * hull[i].y;
}
return abs(area) / 2.0;
}
鞋带公式原理:
- 将多边形分解为多个梯形
- 通过顶点坐标的交叉乘积计算有向面积
- 取绝对值确保面积为正
点包含检测
bool ConvexHull::isPointInsideHull(const Point &p) const
{
if (hull.size() < 3) return false;
int n = hull.size();
int count = 0;
for (int i = 0; i < n; i++) {
Point p1 = hull[i];
Point p2 = hull[(i + 1) % n];
// 检查点是否在边上
if (cross(p1, p2, p) == 0) {
// 边界框检查
double minX = min(p1.x, p2.x);
double maxX = max(p1.x, p2.x);
double minY = min(p1.y, p2.y);
double maxY = max(p1.y, p2.y);
if (p.x >= minX && p.x <= maxX && p.y >= minY && p.y <= maxY) {
return true;
}
}
// 射线法:计算从点p向右发射的射线与边的交点
if ((p1.y > p.y) != (p2.y > p.y)) {
double intersectX = (p2.x - p1.x) * (p.y - p1.y) / (p2.y - p1.y) + p1.x;
if (p.x < intersectX) {
count++;
}
}
}
return count % 2 == 1;
}
射线法关键点:
- 从测试点向右发射射线
- 统计与凸包边的交点数量
- 奇数次相交表示点在内部,偶数次表示在外部
- 特殊处理点在边界上的情况
复杂度分析
时间复杂度
- 最好情况:O(n) - 当点集已经有序且无重复点时
- 最坏情况:O(n log n) - 主要由排序步骤决定
- 平均情况:O(n log n)
分析依据:
- 排序步骤:O(n log n)
- 两次扫描构建凸包:O(n)
- 总体复杂度由排序步骤主导
空间复杂度
- 主要存储:O(n) - 存储原始点集和凸包点集
- 辅助空间:O(1) - 算法使用常数额外空间
- 总空间复杂度:O(n)
优势与局限性
算法优势
- 高效稳定:O(n log n)时间复杂度,在实际应用中表现优秀
- 实现简洁:算法逻辑清晰,代码易于理解和维护
- 数值稳定:对浮点数精度问题相对鲁棒
- 功能完整:提供面积、周长、点包含检测等扩展功能
局限性
- 排序依赖:算法性能受排序效率影响
- 精度问题:浮点运算可能引入微小误差,需要容差处理
- 退化情况:当大量点共线时需要特殊处理
- 内存使用:需要存储排序后的点集副本
扩展思考与优化
算法变体比较
- Graham Scan:另一种O(n log n)算法,基于极角排序
- Jarvis March:O(nh)时间复杂度,h为凸包点数,在h较小时更高效
- QuickHull:基于分治思想,平均性能优秀
优化方向
- 增量更新:支持动态添加点而不重新计算整个凸包
// 可以优化的增量更新接口
void ConvexHull::addPointOptimized(const Point& p) {
// 检查新点是否在当前凸包内部
if (isPointInsideHull(p)) {
points.push_back(p);
return;
}
// 否则重新计算凸包
points.push_back(p);
computeHull();
}
- 并行化:利用多核处理器并行处理大规模点集
- 近似算法:对于实时性要求高的场景,可以使用近似凸包算法
实际应用建议
- 预处理:在实际应用中,可以先对点集进行空间划分
- 容差设置:根据具体应用场景调整数值比较的容差值
- 内存管理:对于大规模点集,考虑使用内存映射文件
总结
凸包计算是计算几何中的经典问题,安德鲁单调链算法以其简洁高效的特点成为实际应用中的首选方案。通过本文的详细解析,我们不仅理解了算法的数学原理,还掌握了如何在C++中实现一个功能完整的凸包计算类。
关键要点回顾:
- 凸包算法通过排序和两次扫描构建凸多边形边界
- 叉积计算是判断点相对位置的核心工具
- 合理的边界条件处理确保算法健壮性
- 射线法提供了高效的点包含检测方案
掌握凸包算法不仅有助于解决具体的几何计算问题,更能培养计算思维和算法设计能力。希望本文能为你在计算几何领域的学习和实践提供有价值的参考。
完整代码实现
CommonTypes.h
#pragma once
#include <algorithm>
#include <cmath>
#include <functional>
#include <vector>
// 公共几何类型定义
struct Point {
double x, y;
Point(double x = 0, double y = 0) : x(x), y(y) {}
// 重载运算符便于排序和比较
bool operator<(const Point& other) const {
if (x == other.x)
return y < other.y;
return x < other.x;
}
bool operator==(const Point& other) const {
return x == other.x && y == other.y;
}
};
/**
* @brief 直线段结构体
*
* 表示一个直线段,包含起点和终点坐标。
*/
struct LineSegment {
Point start; // 直线段起点
Point end; // 直线段终点
LineSegment(const Point& s = Point(), const Point& e = Point())
: start(s), end(e) {}
// 获取直线段长度
double getLength() const {
double dx = end.x - start.x;
double dy = end.y - start.y;
return std::sqrt(dx * dx + dy * dy);
}
// 检查点是否在直线段上(考虑容差)
bool isPointOnLine(const Point& p, double tolerance = 1e-6) const {
// 检查点是否在直线段的边界框内
double minX = std::min(start.x, end.x);
double maxX = std::max(start.x, end.x);
double minY = std::min(start.y, end.y);
double maxY = std::max(start.y, end.y);
if (p.x < minX - tolerance || p.x > maxX + tolerance ||
p.y < minY - tolerance || p.y > maxY + tolerance) {
return false;
}
// 检查点是否在直线上(使用叉积检查共线性)
double crossProduct = (p.x - start.x) * (end.y - start.y) -
(p.y - start.y) * (end.x - start.x);
return std::abs(crossProduct) < tolerance;
}
// 重载相等运算符
bool operator==(const LineSegment& other) const {
return start == other.start && end == other.end;
}
};
ConvexHull.h
#pragma once
#include <vector>
#include <string>
#include "CommonTypes.h"
using namespace std;
/**
* @brief 凸包计算类 - 使用安德鲁单调链算法
*
* 该类实现了计算点集凸包的功能,支持动态添加点、计算面积和周长,
* 以及检查点是否在凸包内部等功能。
*/
class ConvexHull {
private:
vector<Point> points;
vector<Point> hull;
// 计算叉积 (p1-p0) x (p2-p0)
double cross(const Point& p0, const Point& p1, const Point& p2) const;
// 去除重复点
void removeDuplicatePoints();
public:
// 构造函数
ConvexHull() = default;
// 通过点集初始化
ConvexHull(const vector<Point>& initialPoints);
// 添加单个点
void addPoint(const Point& p);
// 添加多个点
void addPoints(const vector<Point>& newPoints);
// 设置点集(替换现有点集)
void setPoints(const vector<Point>& newPoints);
// 获取原始点集
vector<Point> getPoints() const;
// 获取凸包点集
vector<Point> getHull() const;
// 计算凸包
void computeHull();
// 清空所有点
void clear();
// 获取凸包的点数
size_t getHullSize() const;
// 获取原始点数
size_t getPointCount() const;
// 计算凸包面积(使用鞋带公式)
double computeArea() const;
// 计算凸包周长
double computePerimeter() const;
// 检查点是否在凸包内部(使用射线法)
bool isPointInsideHull(const Point& p) const;
// 打印点集
void printPoints(const vector<Point>& points, const string& title) const;
// 打印原始点集
void printOriginalPoints() const;
// 打印凸包点集
void printHullPoints() const;
// 打印凸包信息
void printHullInfo() const;
};
ConvexHull.cpp
#include "ConvexHull.h"
#include <iostream>
// 凸包计算类
// 计算叉积 (p1-p0) x (p2-p0)
double ConvexHull::cross(const Point &p0, const Point &p1, const Point &p2) const
{
return (p1.x - p0.x) * (p2.y - p0.y) - (p2.x - p0.x) * (p1.y - p0.y);
}
// 去除重复点
void ConvexHull::removeDuplicatePoints()
{
if (points.empty()) return;
// 使用标准库算法去重(更高效)
auto last = unique(points.begin(), points.end());
points.erase(last, points.end());
}
// 通过点集初始化
ConvexHull::ConvexHull(const vector<Point> &initialPoints) : points(initialPoints)
{
computeHull();
}
// 添加单个点
void ConvexHull::addPoint(const Point &p)
{
points.push_back(p);
}
// 添加多个点
void ConvexHull::addPoints(const vector<Point> &newPoints)
{
points.insert(points.end(), newPoints.begin(), newPoints.end());
}
// 设置点集(替换现有点集)
void ConvexHull::setPoints(const vector<Point> &newPoints)
{
points = newPoints;
computeHull();
}
// 获取原始点集
vector<Point> ConvexHull::getPoints() const
{
return points;
}
// 获取凸包点集
vector<Point> ConvexHull::getHull() const
{
return hull;
}
// 计算凸包
void ConvexHull::computeHull()
{
int n = points.size();
// 清空之前的凸包结果
hull.clear();
// 处理边界情况
if (n == 0) {
return;
}
// 如果点数小于等于3,所有点都在凸包上
if (n <= 3)
{
hull = points;
return;
}
// 1. 按x坐标排序,x相同时按y排序
sort(points.begin(), points.end());
// 去除重复点
removeDuplicatePoints();
n = points.size();
// 如果去重后点数小于等于3
if (n <= 3)
{
hull = points;
return;
}
// 2. 构建下凸包和上凸包
hull.reserve(n * 2); // 预留足够空间
// 构建下凸包(从左到右扫描)
for (int i = 0; i < n; i++)
{
// 当栈中至少有两个点,且新点使得凸性被破坏(顺时针旋转)时,弹出栈顶点
while (hull.size() >= 2 &&
cross(hull[hull.size() - 2], hull[hull.size() - 1], points[i]) <= 0)
{
hull.pop_back();
}
hull.push_back(points[i]);
}
// 构建上凸包(从右到左扫描)
size_t lower_size = hull.size();
for (int i = n - 2; i >= 0; i--)
{
// 当栈中至少有两个点(包括下凸包的点),且新点使得凸性被破坏时,弹出栈顶点
while (hull.size() > lower_size &&
cross(hull[hull.size() - 2], hull[hull.size() - 1], points[i]) <= 0)
{
hull.pop_back();
}
hull.push_back(points[i]);
}
// 移除最后一个点(它与第一个点重复)
if (hull.size() > 1)
{
hull.pop_back();
}
}
// 清空所有点
void ConvexHull::clear()
{
points.clear();
hull.clear();
}
// 获取凸包的点数
size_t ConvexHull::getHullSize() const
{
return hull.size();
}
// 获取原始点数
size_t ConvexHull::getPointCount() const
{
return points.size();
}
// 计算凸包面积(使用鞋带公式)
double ConvexHull::computeArea() const
{
if (hull.size() < 3)
return 0.0;
double area = 0.0;
int n = hull.size();
for (int i = 0; i < n; i++)
{
int j = (i + 1) % n;
area += hull[i].x * hull[j].y - hull[j].x * hull[i].y;
}
return abs(area) / 2.0;
}
// 计算凸包周长
double ConvexHull::computePerimeter() const
{
if (hull.size() < 2)
return 0.0;
double perimeter = 0.0;
int n = hull.size();
for (int i = 0; i < n; i++)
{
int j = (i + 1) % n;
double dx = hull[j].x - hull[i].x;
double dy = hull[j].y - hull[i].y;
perimeter += sqrt(dx * dx + dy * dy);
}
return perimeter;
}
// 检查点是否在凸包内部(使用射线法)
bool ConvexHull::isPointInsideHull(const Point &p) const
{
if (hull.size() < 3)
return false;
int n = hull.size();
int count = 0;
for (int i = 0; i < n; i++)
{
Point p1 = hull[i];
Point p2 = hull[(i + 1) % n];
// 检查点是否在边上
if (cross(p1, p2, p) == 0)
{
double minX = min(p1.x, p2.x);
double maxX = max(p1.x, p2.x);
double minY = min(p1.y, p2.y);
double maxY = max(p1.y, p2.y);
if (p.x >= minX && p.x <= maxX && p.y >= minY && p.y <= maxY)
{
return true;
}
}
// 射线与边相交检查
if ((p1.y > p.y) != (p2.y > p.y))
{
double intersectX = (p2.x - p1.x) * (p.y - p1.y) / (p2.y - p1.y) + p1.x;
if (p.x < intersectX)
{
count++;
}
}
}
return count % 2 == 1;
}
// 打印点集
void ConvexHull::printPoints(const vector<Point> &points, const string &title) const
{
cout << title << ":" << endl;
for (const auto &p : points)
{
cout << "(" << p.x << ", " << p.y << ")" << endl;
}
cout << endl;
}
// 打印原始点集
void ConvexHull::printOriginalPoints() const
{
printPoints(points, "原始点集");
}
// 打印凸包点集
void ConvexHull::printHullPoints() const
{
printPoints(hull, "凸包点集");
}
// 打印凸包信息
void ConvexHull::printHullInfo() const
{
cout << "凸包信息:" << endl;
cout << "原始点数: " << getPointCount() << endl;
cout << "凸包点数: " << getHullSize() << endl;
cout << "凸包面积: " << computeArea() << endl;
cout << "凸包周长: " << computePerimeter() << endl;
cout << endl;
}
1075

被折叠的 条评论
为什么被折叠?



