📜  给定点集的凸包周长(1)

📅  最后修改于: 2023-12-03 15:41:17.390000             🧑  作者: Mango

给定点集的凸包周长

引言

在计算几何中,凸包是常见的问题之一。给定一个点集,求其最小凸多边形(凸包)的周长是一个经典的计算几何问题。本文介绍凸包的定义以及如何针对给定点集求解凸包周长的算法。

凸包定义

凸包是包含给定点集的最小凸多边形。凸多边形的定义是,对于任意两个点 $p_1$ 和 $p_2$ ,凸多边形内所有的点都在 $p_1$ 和 $p_2$ 连线的同侧。

求解凸包周长
暴力求解

最简单的方法是暴力求解。将给定点集中的每个点都与其他点计算距离,然后选取所有距离中的最大值相加得到凸包周长。时间复杂度为 $O(n^2)$,当点集数量较大时效率较低。

Graham扫描

Graham扫描是求解凸包周长的一种常见的算法,它的时间复杂度为 $O(n\log n)$。算法的核心思想是,先找到点集中最左下角的点 $p_0$ ,然后按照一定的顺序对剩余点进行排序,接着按照有序的顺序依次加入点并且保持凸性。最后,将凸包上各个点之间的距离相加即可得到凸包周长。

步骤如下:

  1. 找到点集中 $y$ 坐标最小的点,如果有多个点,则选择 $x$ 坐标最小的点作为 $p_0$ ,并将它作为凸包中的一个点
  2. 将剩余的点按照和 $p_0$ 的极角逆时针排序(若有多个点与 $p_0$ 的极角相同,则距离 $p_0$ 远的点排在前面)
  3. 依次将排序后的点加入凸包,并且保持凸性。如果加入当前点后不再是凸多边形,则需要删除凸包上不合法的点
  4. 最后,将凸包上各个点之间的距离相加即可得到凸包周长。

下面是Graham扫描的实现代码(基于Python):

import math

# 计算两个点之间的直线距离
def calc_distance(p1, p2):
    return math.sqrt((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2)

# 判断点 q 是否在点 p1 和点 p2 的连线上
def check_collinear(p1, p2, q):
    return (q[1] - p1[1]) * (p2[0] - p1[0]) == (p2[1] - p1[1]) * (q[0] - p1[0])

# 计算点 p1 和点 p2 之间的夹角(弧度制)
def calc_angle(p1, p2):
    return math.atan2(p2[1]-p1[1], p2[0]-p1[0])

# 判断点 p3 是否在点 p1 和点 p2 的左侧
def is_leftturn(p1, p2, p3):
    return (p2[0] - p1[0]) * (p3[1] - p1[1]) - (p3[0] - p1[0]) * (p2[1] - p1[1]) > 0

def convex_hull_perimeter(points):
    n = len(points)
    
    # 遍历点集,找到 y 坐标最小的点作为起点
    start_index = 0
    for i in range(1, n):
        if points[i][1] < points[start_index][1]:
            start_index = i
        elif points[i][1] == points[start_index][1] and points[i][0] < points[start_index][0]:
            start_index = i
            
    # 以起点为基准,计算每个点与起点的极角,并按照极角逆时针排序
    polar_angle = [(calc_angle(points[start_index], point), point) for point in points if point != points[start_index]]
    sorted_points = sorted(polar_angle)
    
    # 将排序后的点加入凸包中
    hull = [points[start_index], sorted_points[0][1]]
    for i in range(1, n-1):
        p1 = sorted_points[i-1][1]
        p2 = sorted_points[i][1]
        # 如果 p2 在凸包内,则直接跳过
        if check_collinear(hull[-2], hull[-1], p2) and not is_leftturn(hull[-2], hull[-1], p2):
            continue
        # 如果新加入的点使得凸包不再是凸多边形,则需要删除凸包上不合法的点
        while len(hull) >= 2 and not is_leftturn(hull[-2], hull[-1], p2):
            hull.pop()
        hull.append(p2)

    # 计算凸包周长
    perimeter = 0
    for i in range(1, len(hull)):
        perimeter += calc_distance(hull[i-1], hull[i])
    perimeter += calc_distance(hull[0], hull[-1])
    
    return perimeter
实例分析

假设有以下点集:

[(2, 2), (3, 1), (2, 3), (0, 0), (1, 2), (3, 3), (1, 1)]

使用 Graham 扫描算法可以得到如下凸包:

[(0, 0), (3, 1), (3, 3), (2, 3)]

凸包的周长为 $3+2\sqrt{2}+2\sqrt{10}\approx10.14$。

使用本文提供的Python3代码实现可以得到正确的结果:

points = [(2, 2), (3, 1), (2, 3), (0, 0), (1, 2), (3, 3), (1, 1)]
perimeter = convex_hull_perimeter(points)
print(perimeter)  # output: 10.13606778052793
总结

本文介绍了如何针对给定点集求解凸包周长,包括暴力求解和Graham扫描算法。Graham扫描算法是一种常见的求解凸包的算法,时间复杂度为$O(n\log n)$。我们在实例分析中验证了本文提供的Python代码的正确性。