1.什么是凸包
凸包是包裹给定点集的凸多边形,其中多边形的边界由点集中的点构成。凸包问题的解决对于计算机图形学、计算几何等领域非常重要。
2.凸包问题的应用场景
凸包在计算机图形中的应用非常广泛,通常我们可以利用一组点集的外轮廓去进行其他的操作,例如检测区域相交、覆盖范围等。
3.凸包问题如何求解
二维凸包的求解方法有多种,例如GrahamScan、Jarvis算法等,GrahamScan是一种利用扫描思想的算法,这里将对GrahamScan进行详细介绍。
时间复杂度为:O(nlogn)
数据结构:栈
主要思路如下:
(1)初始化起点,找出点集P中Y坐标最小的点p0作为起点,其一定在凸包上
(2)以p0为坐标起点,对剩下点集按照极角排序
(3) 将p1压入栈,依次遍历其他顶点
(4)当前顶点为p[i],如果p[i]在向量p[i-2]p[i-1]的左侧,则p[i]压入栈中
(5)判断三点走向,形成逆时针顺序。当前顶点为p[i],如果p[i]在向量p[i-2]p[i-1]的右侧,代表三点是顺时针走向,不是我们想要的走向,p[i-1]弹出栈顶。继续判断p[i]是否在相邻p[i-3]p[i-2]的左侧,如果在左侧,则代表三点是逆时针走向,满足要求,p[i]入栈,否则p[i-2]出栈,直至p[i]入栈。
(6)循环遍历剩下的点集,至此,我们可以完整找出点集的凸包。
4. 代码讲解
4.1 首先定义一个简单的 Point
类,表示平面上的一个点,有 X 和 Y 坐标。
public class Point
{
public int X { get; set; }
public int Y { get; set; }
public Point(int x, int y)
{
X = x;
Y = y;
}
}
4.2 定义一个 ConvexHull
类,其中包含一个用于计算两点之间极角的私有方法 Angle
public class ConvexHull
{
// 求极角,返回角度的弧度值
private static double Angle(Point p1, Point p2)
{
return Math.Atan2(p2.Y - p1.Y, p2.X - p1.X);
}
一个私有的比较器类 PolarAngleComparer
,用于在排序时按照极角比较点
// 用于排序的比较器,按照极角排序
private class PolarAngleComparer : IComparer<Point>
{
private Point referencePoint;
public PolarAngleComparer(Point referencePoint)
{
this.referencePoint = referencePoint;
}
public int Compare(Point p1, Point p2)
{
// ... 省略排序比较器中的代码
}
}
定义了一个私有的方法 Orientation
,用于判断三个点的走向,帮助确定是否需要进行栈的出栈操作
// 判断三个点的走向,用于确定是否需要进行栈的出栈操作
private static int Orientation(Point p, Point q, Point r)
{
// ... 省略判断走向的代码
}
算法入口:GrahamScan
方法,这是实际执行凸包算法的部分,以及 PrintConvexHull
方法,用于输出凸包的顶点
// Graham 扫描算法
public static List<Point> GrahamScan(List<Point> points)
{
// ... 省略 Graham 扫描算法中的代码
}
// 输出凸包的顶点
public static void PrintConvexHull(List<Point> convexHull)
{
// ... 省略输出凸包顶点的代码
}
最后我们来看看算法运行效果
5.完整代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace _01_ConvexHull
{
public class Point
{
public double X { get; set; }
public double Y { get; set; }
public Point(double x, double y)
{
X = x;
Y = y;
}
}
public class ConvexHull
{
// 求极角,返回角度的弧度值
private static double Angle(Point p1, Point p2)
{
return Math.Atan2(p2.Y - p1.Y, p2.X - p1.X);
}
// 用于排序的比较器,按照极角排序
private class PolarAngleComparer : IComparer<Point>
{
private Point referencePoint;
public PolarAngleComparer(Point referencePoint)
{
this.referencePoint = referencePoint;
}
public int Compare(Point p1, Point p2)
{
double angle1 = Angle(referencePoint, p1);
double angle2 = Angle(referencePoint, p2);
if (angle1 < angle2)
return -1;
else if (angle1 > angle2)
return 1;
else
{
// 如果两点的极角相同,距离较近的排在前面
double distance1 = Math.Pow(p1.X - referencePoint.X, 2) + Math.Pow(p1.Y - referencePoint.Y, 2);
double distance2 = Math.Pow(p2.X - referencePoint.X, 2) + Math.Pow(p2.Y - referencePoint.Y, 2);
if (distance1 < distance2)
return -1;
else if (distance1 > distance2)
return 1;
else
return 0;
}
}
}
// 判断三个点的走向,用于确定是否需要进行栈的出栈操作
private static int Orientation(Point p, Point q, Point r)
{
double val = (q.Y - p.Y) * (r.X - q.X) - (q.X - p.X) * (r.Y - q.Y);
if (val == 0)
return 0; // 三点共线
return (val > 0) ? 1 : 2; // 顺时针或逆时针
}
// Graham 扫描算法
public static List<Point> GrahamScan(List<Point> points)
{
int n = points.Count;
if (n < 3)
throw new ArgumentException("凸包需要至少三个点");
// 寻找最下方且最左边的点
Point referencePoint = points.OrderBy(p => p.Y).ThenBy(p => p.X).First();
// 根据极角排序其他点
List<Point> sortedPoints = points.Where(p => p != referencePoint).ToList();
sortedPoints.Sort(new PolarAngleComparer(referencePoint));
// 压入参考点和前两个点
Stack<Point> hull = new Stack<Point>();
hull.Push(referencePoint);
hull.Push(sortedPoints[0]);
hull.Push(sortedPoints[1]);
// 处理剩余的点
for (int i = 2; i < sortedPoints.Count; i++)
{
while (hull.Count > 1 && Orientation(hull.ElementAt(1), hull.Peek(), sortedPoints[i]) != 2)
hull.Pop();
hull.Push(sortedPoints[i]);
}
return hull.ToList();
}
}
}