蛮力法姊妹篇 | Python分治法解决凸包问题并用matplotlib实现可视化以及与蛮力法的对比

之前写了一篇Python蛮力法解决凸包问题并用matplotlib实现可视化,最后也给出了同样是在1000个点的情况下蛮力法和分治法的差距有多大(蛮力法1154秒,分治法0.125秒…)

先解释一下为什么吧:
因为蛮力法的重点在于中间有三重循环,所以时间复杂度为O(n3),而分治法需要对点集进行一次排序还有一次遍历,排序算法的复杂度为O(logn),遍历一遍复杂度为O(n),所以分治法的时间复杂度为O(nlogn)

二者相比,完全就不是一个档次啊…🌚

然后说说我的思路:

分治法,顾名思义,分而治之。

对于平面上的一个点集:
我们很容易找到最左边的点和最右边的点,将他们连起来,就会将整个点集分为上半部分和下半部分,同时这两个点也必定是凸包的边界点
在这里插入图片描述
接下来在其它点中找到两个点,使之与这两个点组成的三角形面积最大或最小(最大的是上半包的顶点;最小即为负的最大,说明这个点是下半包的顶点),并将这两个点添加到边界点集合中。

在这里插入图片描述
找到两个顶点后,继续对上/下半包的点进行递归遍历,对于上半包来说,以新形成的那两条边为底,如果有点新编围成的三角形面积为正,那就找到最大的继续分三角形;下班包也是一样的道理,只是下半包需要找最小的。同样将顶点添加到边界点集合中去。
在这里插入图片描述
完了继续递归,知道没有符合条件的为止。此时的边界点集合再加上最左边和最右边的两个点,就是凸包所有的边界点。
( BUT,你认为真实的过程就是这样的吗?不不不,这只是一个便于我们理解的过程,真实的递归过程是这样的:😏
在这里插入图片描述
如果你对这张图是怎么画出来的有兴趣,可以去我的这篇文章:分治法进阶篇 | 利用matplotlib画出凸包问题分治递归策略实现过程动态图看看<有源码欸>)

然后说代码:

随机生成具有n个点的点集:

def rand_point_set(n, range_min=0, range_max=101):
    """
    随机生成具有 n 个点的点集
    :param range_max: 生成随机点最小值,默认 0
    :param range_min: 生成随机点最大值,默认 100
    :param n: int
    :return: list [(x1,y1)...(xn,yn)]
    """
    try:
        return list(zip([random.uniform(range_min, range_max) for _ in range(n)],
                        [random.uniform(range_min, range_max) for _ in range(n)]))
    except IndexError as e:
        print("\033[31m" + ''.join(e.args) + "\n输入范围有误!" + '\033[0m')

判断三角形的面积:

def calc_area(a, b, c):
    """
    判断三角形面积
    """
    x1, y1 = a
    x2, y2 = b
    x3, y3 = c
    return x1 * y2 + x3 * y1 + x2 * y3 - x3 * y2 - x2 * y1 - x1 * y3

递归寻找上半部分的边界点:

def border_point_up(left_point, right_point, lists, border_points):
    """
    寻找上半部分边界点
    :param left_point: tuple, 最左边的点
    :param right_point: tuple, 最右边的点
    :param lists: 所有的点集
    :param border_points: 边界点集
    :return:
    """
    area_max = 0
    max_point = ()
    for item in lists:
        if item == left_point or item == right_point:
            continue
        else:
            max_point = item if calc_area(left_point, right_point, item) > area_max else max_point
            area_max = calc_area(left_point, right_point, item) if calc_area(left_point, right_point,item) > area_max else area_max
    if area_max != 0:
        border_points.append(max_point)
        border_point_up(left_point, max_point, lists, border_points)
        border_point_up(max_point, right_point, lists, border_points)

递归寻找下半部分的边界点:

def border_point_down(left_point, right_point, lists, border_points):
    """
    寻找下半部分边界点
    :param left_point: tuple, 最左边的点
    :param right_point: tuple, 最右边的点
    :param lists: 所有的点集
    :param border_points: 边界点集
    :return:
    """
    area_max = 0
    max_point = ()
    for item in lists:
        if item == left_point or item == right_point:
            continue
        else:
            max_point = item if calc_area(left_point, right_point, item) < area_max else max_point
            area_max = calc_area(left_point, right_point, item) if calc_area(left_point, right_point,item) < area_max else area_max
    if area_max != 0:
        border_points.append(max_point)
        border_point_down(left_point, max_point, lists, border_points)
        border_point_down(max_point, right_point, lists, border_points)

返回顺时针的边界点集:

def order_border(lists):
    """
    返回顺时针的边界点集
    :param lists: 无序边界点集
    :return: list [( , )...( , )]
    """
    lists.sort()
    first_x, first_y = lists[0]  # 最左边的点
    last_x, last_y = lists[-1]  # 最右边的点
    list_border_up = []  # 上半边界
    for item in lists:
        x, y = item
        if y > max(first_y, last_y):
            list_border_up.append(item)
        if min(first_y, last_y) < y < max(first_y, last_y):
            if calc_area(lists[0], lists[-1], item) > 0:
                list_border_up.append(item)
            else:
                continue
    list_border_down = [_ for _ in lists if _ not in list_border_up]  # 下半边界
    list_end = list_border_up + list_border_down[::-1]  # 最终顺时针输出的边界点
    return list_end

matplotlib画图:

def draw(list_points, list_borders):
    """
    画图
    :param list_points: 所有点集
    :param list_borders: 所有边界点集
    :return: picture
    """
    list_all_x = []
    list_all_y = []
    for item in list_points:
        a, b = item
        list_all_x.append(a)
        list_all_y.append(b)
    list_borders.append(list_borders[0])
    for i in range(len(list_borders) - 1):
        one_, oneI = list_borders[i]
        two_, twoI = list_borders[i + 1]
        plt.plot([one_, two_], [oneI, twoI])
    plt.scatter(list_all_x, list_all_y)
    plt.show()

最后写一个输入函数:
因为我的随机点集生成函数 rand_point_set(n, range_min=0, range_max=101) 指定的后两个参数是用来选择生成点集范围的,所以,这是我的输入函数:

def main():
    """
    :return: 所有点
    """
    inputs = list(map(int, input().split()))
    if len(inputs) == 1:
        return rand_point_set(inputs[0])
    elif len(inputs) == 2:
        return rand_point_set(inputs[0], inputs[1])
    elif len(inputs) == 3:
        return rand_point_set(inputs[0], inputs[1], inputs[2])
    else:
        print("\033[31m输入数据太多,请重新输入!\033[0m")
        main()

最后把这些函数组合起来,
我把完整代码(复制可用的那种)贴到这里:

import random
import numpy as np
import matplotlib.pyplot as plt


def calc_area(a, b, c):
    """
    判断三角形面积
    """
    x1, y1 = a
    x2, y2 = b
    x3, y3 = c
    return x1 * y2 + x3 * y1 + x2 * y3 - x3 * y2 - x2 * y1 - x1 * y3


def rand_point_set(n, range_min=0, range_max=101):
    """
    随机生成具有 n 个点的点集
    :param range_max: 生成随机点最小值,默认 0
    :param range_min: 生成随机点最大值,默认 100
    :param n: int
    :return: list [(x1,y1)...(xn,yn)]
    """
    try:
        return list(zip([random.uniform(range_min, range_max) for _ in range(n)],
                         [random.uniform(range_min, range_max) for _ in range(n)]))
    except IndexError as e:
        print("\033[31m" + ''.join(e.args) + "\n输入范围有误!" + '\033[0m')


def border_point_up(left_point, right_point, lists, border_points):
    """
    寻找上半部分边界点
    :param left_point: tuple, 最左边的点
    :param right_point: tuple, 最右边的点
    :param lists: 所有的点集
    :param border_points: 边界点集
    :return:
    """
    area_max = 0
    max_point = ()
    for item in lists:
        if item == left_point or item == right_point:
            continue
        else:
            max_point = item if calc_area(left_point, right_point, item) > area_max else max_point
            area_max = calc_area(left_point, right_point, item) if calc_area(left_point, right_point,
                                                                             item) > area_max else area_max
    if area_max != 0:
        border_points.append(max_point)
        border_point_up(left_point, max_point, lists, border_points)
        border_point_up(max_point, right_point, lists, border_points)


def border_point_down(left_point, right_point, lists, border_points):
    """
    寻找下半部分边界点
    :param left_point: tuple, 最左边的点
    :param right_point: tuple, 最右边的点
    :param lists: 所有的点集
    :param border_points: 边界点集
    :return:
    """
    area_max = 0
    max_point = ()
    for item in lists:
        if item == left_point or item == right_point:
            continue
        else:
            max_point = item if calc_area(left_point, right_point, item) < area_max else max_point
            area_max = calc_area(left_point, right_point, item) if calc_area(left_point, right_point,
                                                                             item) < area_max else area_max

    if area_max != 0:
        border_points.append(max_point)
        border_point_down(left_point, max_point, lists, border_points)
        border_point_down(max_point, right_point, lists, border_points)


def order_border(lists):
    """
    返回顺时针的边界点集
    :param lists: 无序边界点集
    :return: list [( , )...( , )]
    """
    lists.sort()
    first_x, first_y = lists[0]  # 最左边的点
    last_x, last_y = lists[-1]  # 最右边的点
    list_border_up = []  # 上半边界
    for item in lists:
        x, y = item
        if y > max(first_y, last_y):
            list_border_up.append(item)
        if min(first_y, last_y) < y < max(first_y, last_y):
            if calc_area(lists[0], lists[-1], item) > 0:
                list_border_up.append(item)
            else:
                continue
    list_border_down = [_ for _ in lists if _ not in list_border_up]  # 下半边界
    list_end = list_border_up + list_border_down[::-1]  # 最终顺时针输出的边界点
    return list_end


def draw(list_points, list_borders):
    """
    画图
    :param list_points: 所有点集
    :param list_borders: 所有边界点集
    :return: picture
    """
    list_all_x = []
    list_all_y = []
    for item in list_points:
        a, b = item
        list_all_x.append(a)
        list_all_y.append(b)
    list_borders.append(list_borders[0])
    for i in range(len(list_borders) - 1):
        one_, oneI = list_borders[i]
        two_, twoI = list_borders[i + 1]
        plt.plot([one_, two_], [oneI, twoI])
    plt.scatter(list_all_x, list_all_y)
    plt.show()


def main():
    """
    :return: 所有点
    """
    inputs = list(map(int, input().split()))
    if len(inputs) == 1:
        return rand_point_set(inputs[0])
    elif len(inputs) == 2:
        return rand_point_set(inputs[0], inputs[1])
    elif len(inputs) == 3:
        return rand_point_set(inputs[0], inputs[1], inputs[2])
    else:
        print("\033[31m输入数据太多,请重新输入!\033[0m")
        main()


if __name__ == "__main__":
    print("""输入规则:
最少一个最多三个
后面可以跟数字用来指定生成区间(默认[0,100]),中间用空格隔开
例如:
    输入 10   ---即为在默认区间[0,100]生成10个随机点
    输入 10 50   ---即为在区间[50,100]生成10个随机点
    输入 10 50 200   ---即为在区间[50,200]生成10个随机点
请输入:\t""")
    list_points = main()  # 所有点
    list_points.sort()
    border_points = []  # 边界点集
    border_point_up(list_points[0], list_points[-1], list_points, border_points)  # 上边界点集
    border_point_down(list_points[0], list_points[-1], list_points, border_points)  # 下边界点集
    border_points.append(list_points[0])
    border_points.append(list_points[-1])  # 将首尾两个点添加到边界点集中
    # print(order_border(border_points))  # 顺时针输出边界点
    draw(list_points, order_border(border_points))

下面是我用pyecharts画的蛮力法和分治法实间对比图:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
可以看到,蛮力法随着点数的增加,时间几乎是呈指数型增长;而分治法相对来说增长比较平缓,甚至于100个点的蛮力法耗时比100000个点的分治法耗时还要多!


PS:

上面的曲线是根据我的蛮力法分治法得出来的数据,而且由于电脑配置、网速、后台程序等等一系列原因,不同的设备结果可能会略有差异。但是总体增长趋势趋势一定是不变的!

🆗
最后在放一张分治法100000个点的图:(也没啥看头…🌚)
在这里插入图片描述

  • 17
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值