📜  凸包的 Quickhull 算法(1)

📅  最后修改于: 2023-12-03 14:50:10.263000             🧑  作者: Mango

凸包的 Quickhull 算法

介绍

凸包是一种重要的计算几何问题,它指的是覆盖所有给定点的凸多边形。Quickhull 算法是一种高效的求解凸包问题的算法,它的时间复杂度为 $O(n \log n)$。

Quickhull 算法基于分治的思想,将凸包分成左右两个部分,递归地处理这两个部分。对于每个部分,首先选取当前集合中最左和最右的两个点作为凸壳边界上的点,然后找到其他点中离这条边界线最远的点,将其加入凸壳边界中。递归处理左右两个部分,最终得到凸包。

下面介绍 Quickhull 算法的具体实现。

实现
函数接口

Quickhull 算法的函数接口如下:

def quick_hull(points):
    """
    Quickhull 算法求凸包

    Args:
        points: 点集,格式为 [(x1, y1), (x2, y2), ...]

    Returns:
        凸包点组成的列表,格式为 [(x1, y1), (x2, y2), ...]
    """

函数输入是点集,格式为一个包含若干个坐标元组的列表。函数的返回值是凸包点组成的列表,格式为一个包含若干个坐标元组的列表。

辅助函数

在实现 Quickhull 算法之前,需要先实现一些辅助函数:

def dist(p1, p2, p):
    """
    计算点 p 到 p1, p2 所在直线的距离

    Args:
        p1: 直线上的点,格式为 (x1, y1)
        p2: 直线上的点,格式为 (x2, y2)
        p:  需要计算距离的点,格式为 (x, y)

    Returns:
        点 p 到 p1, p2 所在直线的距离
    """
    
def find_furthest_point(points, p1, p2):
    """
    在点集 points 中找到距离 p1, p2 所在直线最远的点,返回该点和距离

    Args:
        points: 点集,格式为 [(x1, y1), (x2, y2), ...]
        p1: 直线上的点,格式为 (x1, y1)
        p2: 直线上的点,格式为 (x2, y2)

    Returns:
        最远点和距离,格式为 ((x, y), distance)
    """
    
def divide(points, p1, p2):
    """
    将点集 points 分成 p1, p2 两侧和 p1, p2 之间三个部分,返回这三个部分构成的点集

    Args:
        points: 点集,格式为 [(x1, y1), (x2, y2), ...]
        p1: 直线上的点,格式为 (x1, y1)
        p2: 直线上的点,格式为 (x2, y2)

    Returns:
        三个部分分别构成的点集,格式为 (left, on, right)
    """

这些辅助函数会在 Quickhull 算法的实现中使用到。

Quickhull 算法

根据 Quickhull 算法的思想,可以递归地求解凸包问题:

def quick_hull(points):
    if len(points) <= 3:
        return points
    
    # 找到最左和最右的点
    left = min(points, key=lambda p: p[0])
    right = max(points, key=lambda p: p[0])

    # 将点集分成左右两部分和中间一部分
    left_points, on_points, right_points = divide(points, left, right)

    # 递归求解左右两部分的凸包
    left_hull = quick_hull(left_points)
    right_hull = quick_hull(right_points)

    # 合并左右两部分的凸包
    hull = left_hull + right_hull

    # 将中间一部分添加到凸包中
    if on_points:
        hull.append(on_points[0])

    # 递归处理左右两部分之间的凸包边界
    for p1, p2 in [(left, right), (right, left)]:
        # 找到 p1, p2 两侧距离最远的点
        p, _ = find_furthest_point(points, p1, p2)

        # 将该点添加到凸包边界中
        if p not in hull:
            hull.append(p)

    return hull

这里采用了递归的方式求解凸包问题,算法的复杂度为 $O(n \log n)$。

完整代码实现如下:

def dist(p1, p2, p):
    return abs((p[1] - p1[1]) * (p2[0] - p1[0]) - (p2[1] - p1[1]) * (p[0] - p1[0])) / \
           ((p2[1] - p1[1]) ** 2 + (p2[0] - p1[0]) ** 2) ** 0.5


def find_furthest_point(points, p1, p2):
    max_dist = -1
    max_point = None
    for p in points:
        if p in (p1, p2):
            continue
        d = dist(p1, p2, p)
        if d > max_dist:
            max_dist = d
            max_point = p
    return max_point, max_dist


def divide(points, p1, p2):
    left, on, right = [], [], []
    for p in points:
        if p in (p1, p2):
            on.append(p)
        elif (p[0] - p1[0]) * (p2[1] - p1[1]) > (p[1] - p1[1]) * (p2[0] - p1[0]):
            right.append(p)
        else:
            left.append(p)
    return left, on, right


def quick_hull(points):
    if len(points) <= 3:
        return points
    left = min(points, key=lambda p: p[0])
    right = max(points, key=lambda p: p[0])
    left_points, on_points, right_points = divide(points, left, right)
    left_hull = quick_hull(left_points)
    right_hull = quick_hull(right_points)
    hull = left_hull + right_hull
    if on_points:
        hull.append(on_points[0])
    for p1, p2 in [(left, right), (right, left)]:
        p, _ = find_furthest_point(points, p1, p2)
        if p not in hull:
            hull.append(p)
    return hull
参考资料