图形几何算法 -- 凸包算法

前言

常用凸包算法包括Graham Scan 算法和Jarvis March (Gift Wrapping) 算法,在这里要简单介绍的是Graham Scan 算法。

1、概念

凸包是一个点集所包围的最小的凸多边形。可以想象用一根绳子围绕着一群钉子,绳子所形成的轮廓便是这些钉子的凸包。在计算几何中,凸包得到了广泛的应用,涉及领域包括模式识别、图像处理和优化问题等。

2、算法原理

凸包算法的目标是从给定的点集(在二维平面中)确定出一个最小的凸多边形(即凸包)。此多边形的所有顶点是在输入点集中。

算法的核心在于以下几个基本概念:

**2.1 、极角:**对于一个基准点(通常是集合中最底部或最左边的点),其余每个点与基准点的连线形成的角度。通过极角的比较,我们可以确定相对位置。

**2.2、栈结构:**使用栈来存储凸包上的点。栈的特性允许我们很容易地回退到前一个状态,并且在确定凸包的顶点时非常有效。

**2.3、方向判断(Orientation):**通过计算三点(一个基准点和两个参考点)形成的向量的叉积来判断它们的相对方向,包括:

  • 顺时针(Clockwise):两个参考点的连线向基准点转动的方式为顺时针。

  • 逆时针(Counterclockwise):两个参考点的连线向基准点转动的方式为逆时针。

  • 共线(Collinear):三点在同一条直线上。

3、算法步骤

以下是该算法的详细步骤:

3.1、选择基准点:遍历所有输入点,选择 Y 坐标最低的点作为基准点(如果有多个点具有相同的 Y 坐标,则选择 X 坐标最小的点),作为基准点(minPoint)。

3.2、极角排序: 对于每个点P,计算它与基准点的极角并记录。可以使用 Math.Atan2() 函数来计算极角。排序的同时,如果有两个点的极角相同,则应该依据它们到基准点的距离进行排序,从近到远进行排列。

3.3、初始化栈:

  • 创建一个栈并将基准点压入栈中。

  • 将排好序的点依次压入栈中,初始先将基准点后面的两个点压入栈,以便形成开始的边。

**3.4、构建凸包:**排序后的点集,从第一个用于构建凸包的点开始。

  • 检查栈顶的两个点和当前点的方向。使用前面提到的在 Orientation() 中实现的稳定性判断。

  • 如果方向判断为"右转",则弹出栈顶的点,直到遇到一个"左转"或"共线"的状态。

  • 将当前点压入栈中,继续根据下一个点执行该步骤。

**3.5、输出结果:**当所有点处理完毕后,栈中包含的点即为最终的凸包。

4、代码

cs 复制代码
class MyConvexHull  
{  
    // 定义一个点的类,包含 X 和 Y 坐标, 坐标类型为 double  
    public class Point2d  
    {  
        public double X { get; }  
        public double Y { get; }  

        public Point2d (double x, double y)  
        {  
            X = x;  
            Y = y;  
        }  
    }  

    // 计算两个点相对于基点的极角  
    private static double PolarAngle(Point2d p0, Point2d p1)  
    {  
        return Math.Atan2(p1.Y - p0.Y, p1.X - p0.X);  
    }  

    // 判断三个点的方向  
    private static int Orientation(Point2d p, Point2d q, Point2d  r)  
    {  
        double val = (q.Y - p.Y) * (r.X - q.X) - (q.X - p.X) * (r.Y - q.Y);  
        if (val == 0) return 0; // collinear  
        return (val > 0) ? 1 : 2; // clockwise or counterclockwise  
    }  

    // Graham Scan 主函数,返回凸包的顶点列表  
    public static List<Point2d> GetConvexHull(List<Point2d> points)  
    {  
        // 1. 找到最低的点  
        Point2d minPoint = points[0];  
        foreach (var point in points)  
        {  
            if (point.Y < minPoint.Y || (point.Y == minPoint.Y && point.X < minPoint.X))  
            {  
                minPoint = point;  
            }  
        }  
        // 2. 将点按相对于基准点的极角进行排序  
        points.Sort((p1, p2) =>  
        {  
            double angle1 = PolarAngle(minPoint, p1);  
            double angle2 = PolarAngle(minPoint, p2);  
            return angle1.CompareTo(angle2);  
        });  

        // 3. 初始化栈  
        Stack<Point2d> hull = new Stack<Point2d>();  
        hull.Push(minPoint);      // 压入基准点  
        hull.Push(points[0]);     // 压入第一个排序后的点  

        // 4. 遍历剩余点,构建凸包  
        for (int i = 1; i < points.Count; i++)  
        {  
            while (hull.Count >= 2)  
            {  
                Point2d top = hull.Pop(); // 弹出栈顶元素  
                Point2d nextToTop = hull.Peek(); // 获取当前栈顶的下一个元素  
                // 检查当前点是否为顺时针  
                if (Orientation(nextToTop, top, points[i]) == 2) // 右转  
                {  
                    hull.Push(top);    // 若是右转,维持栈顶元素  
                    break;  
                }  
            }  
            hull.Push(points[i]); // 压入当前点  
        }  

        // 结果为栈内点的列表  
        return new List<Point2d>(hull);  
    }  

    // 主程序  
    static void Main(string[] args)  
    {  
        // 示例点集  
        List<Point2d> points = new List<Point2d>  
        {  
            new Point2d(0, 3),  
            new Point2d(2, 2),  
            new Point2d(1, 1),  
            new Point2d(2, 1),  
            new Point2d(3, 0),  
            new Point2d(0, 0),  
            new Point2d(3, 3)  
        };  

        // 获取凸包  
        List<Point2d> convexHull = MyConvexHull::GetConvexHull(points);  

        // 输出凸包的顶点  
        Console.WriteLine("Convex Hull:");  
        foreach (var point in convexHull)  
        {  
            Console.WriteLine($"({Point2d.X}, {Point2d.Y})");  
        }  
    }  
}

5、代码步骤说明

5.1、定义 Point2d 类:

  • 该类用于表示二维空间中的点,包含 XY 坐标,以及一个构造函数用于初始化坐标。

5.2、极角计算:

  • PolarAngle(Point p0, Point p1):接受两个点,计算第二个点相对于第一个点形成的极角。使用 Math.Atan2() 函数来求取两点之间的角度,返回值为弧度。

5.3、方向判断:

  • Orientation(Point2d p, Point2d q, Point2d r):此函数通过计算三点的叉积来判断它们的相对方向。返回值表示这三点的关系,能够帮助我们判断当前点是向"左转"、"右转"还是"共线"。

5.4、获取凸包的主函数:

5.4.1、 **GetConvexHull(List<Point2d> points):**该函数实现了 Graham Scan 算法的核心逻辑,输入一个点的列表并返回构成凸包的点。、

  • 步骤一:

    找到最底部点:遍历点集,找到 Y 坐标最低的点,将其作为基准点。

  • 步骤二:

    对所有点(包括基准点)按相对于基准点的极角进行排序。

  • 步骤三:

    初始化栈:创建栈并将基准点和第一个排序后的点推入栈中。

  • 步骤四:

    构建凸包:遍历剩余的点:

    1. 弹出栈顶元素,检查当前点与栈顶前两个点的方向。

    2. 如果是右转,保留后面的点,继续弹出直到遇到左转或栈中的点少于两个。

    3. 将当前点压入栈中。

**5.4.2、**返回值:栈中的点即为构成凸包的点。

5.5、主程序:

  • 创建一个示例点集。

  • 调用 GetConvexHull(points) 获取凸包,接着打印输出结果。

6、总结

该代码通过 Graham Scan 算法实现了一个有效的二维空间的凸包计算。利用栈结构和极角排序,能够高效地构建并返回凸包顶点。通过对代码的逐步解析,我们可以清楚地理解每个步骤的目的和意义,能够较好扩展到其他相关的几何计算中。

更多学习内容,可关注公众号:

以上内容为个人测试过程的记录,供大家参考。

内容如有错欢迎批评指正,谢谢!!!!

相关推荐
XiaoLeisj21 分钟前
【递归,搜索与回溯算法 & 综合练习】深入理解暴搜决策树:递归,搜索与回溯算法综合小专题(二)
数据结构·算法·leetcode·决策树·深度优先·剪枝
Jasmine_llq40 分钟前
《 火星人 》
算法·青少年编程·c#
闻缺陷则喜何志丹1 小时前
【C++动态规划 图论】3243. 新增道路查询后的最短距离 I|1567
c++·算法·动态规划·力扣·图论·最短路·路径
Lenyiin1 小时前
01.02、判定是否互为字符重排
算法·leetcode
鸽鸽程序猿1 小时前
【算法】【优选算法】宽搜(BFS)中队列的使用
算法·宽度优先·队列
Jackey_Song_Odd1 小时前
C语言 单向链表反转问题
c语言·数据结构·算法·链表
Watermelo6171 小时前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
乐之者v2 小时前
leetCode43.字符串相乘
java·数据结构·算法
军训猫猫头3 小时前
20.抽卡只有金,带保底(WPF) C#
ui·c#·wpf