[python刷题模板] 二维凸包(Andrew
一、 算法&数据结构
1. 描述
凸多边形是指所有内角大小都在 [0,π] 范围内的 简单多边形。
在平面上能包含所有给定点的最小凸多边形叫做凸包。
实际上可以理解为用一个橡皮筋包含住所有给定点的形态。
- 从给定的点集中,找出“最外围”的点集,这些点连成的凸多边形,可以包含集合中所有点。这个形状就是凸包。
- Andrew算法可以用O(nlgn)时间求出凸包上所有的点(可以选择是否保留边上的点)。
- 核心思想:
- 按照(x,y)二维排序。
- 显然p[0]和p[-1]都是凸包上的点,且是最左和最右的点。
- 这两个点连线后,下半部分的所有凸包上的点和线段叫做下秃壳;同理上边是上秃壳。
- 从p0出发逆时针访问秃壳上的点,发现所有遇到的新方向都是向左转(逆时针)。这个比较是指最后一条线和新线;因此可以用一个单调栈维护;那么剩余的问题就是如何判断方向,这可以用叉乘来算。
- 先求下秃壳,从p0出发,从顺序遍历排好序的点,用单调栈维护下秃壳上的点,即a=st[-2],b=st[-1],c=p[i] (新点)。则判断ac是否在ab的右边,若是右边,说明ac在ab外围,b不是下秃壳上的点,弹出。
- 同理求下秃壳,从p[-1]出发逆序遍历。
- 注意由于上下秃壳不能复用点,因此需要一个额外数组used;
- 注意求上秃壳时,弹栈的边界条件是栈大小>1;但求下秃壳时,栈大小要>m,其中m=下秃壳大小。
- 同时注意最后求上秃壳时最后一个点是0,最后要弹出。即0算了两次。
- 这个算法本质维护的是凸包的边。即栈中最后两个元素组成的直线方向 与 新边的方向比较。
2. 复杂度分析
- O(nlog2n)
3. 常见应用
- 求最大面积。
- 求最大周长。
4. 常用优化
- 凸包问题的五种解法。这里介绍了集中nb的算法。甚至还有在线算法,但看不懂。
- 三维凸包不会。
二、 模板代码
1. 二维凸包模板题。
例题: 587. 安装栅栏
- 本题要求保留秃壳边上的点。
def substraction(a,b):
# 向量a-向量b
return a[0]-b[0],a[1]-b[1]
def cross(a,b):
# 向量叉乘
return a[0]*b[1] - a[1]*b[0]
def get_area(a,b,c):
# 向量ab转为向量ac过程中扫过的面积;
# 返回负代表ac在ab的左边,即逆时针旋转(向左)
# 返回0代表共线;
# 返回正代表ac在ab的右边,即顺时针旋转(向右)
return cross(substraction(b,a),substraction(c,a))
def AnrewHull(points,keep_point_on_edges=False):
n = len(points)
if n < 4:
return points
points.sort()
hull = [0]
used = [False] * n
def when_pop(a,b,c): # 如果要保留边上的点,则>才弹栈
return (get_area(a,b,c)>0) if keep_point_on_edges else (get_area(a,b,c)>=0)
def make_hull(i,limit):
while len(hull)>limit and when_pop(points[hull[-2]],points[hull[-1]],points[i]):
used[hull.pop()] = False
used[i] = True
hull.append(i)
for i in range(1,n):
make_hull(i,1)
m = len(hull)
for i in range(n-2,-1,-1):
if not used[i]:
make_hull(i,m)
# hull.pop()
return [points[i] for i in hull[:-1]]
class Solution:
def outerTrees(self, trees: List[List[int]]) -> List[List[int]]:
return AnrewHull(trees,True)
2. 先筛选秃壳上的点再后续计算
链接: 812. 最大三角形面积
- 首先最大三角形三个点一定在凸包上;这一步可以筛选一部分点;但复杂度最坏还是O(n3)的。
- 然后发现性质:固定i,j后,面积相对于k是秃曲线,有极点,最优k就是极点。
- 固定i时,k和j是同向关系,因此可以双指针,优化掉一层维度。
- 最终复杂度O(n2)。
def substraction(a,b):
# 向量a-向量b
return a[0]-b[0],a[1]-b[1]
def cross(a,b):
# 向量叉乘
return a[0]*b[1] - a[1]*b[0]
def get_area(a,b,c):
# 向量ab转为向量ac过程中扫过的面积;
# 返回负代表ac在ab的左边,即逆时针旋转(向左)
# 返回0代表共线;
# 返回正代表ac在ab的右边,即顺时针旋转(向右)
return cross(substraction(b,a),substraction(c,a))
def AnrewHull(points,keep_point_on_edges=False):
n = len(points)
if n < 4:
return points
points.sort()
hull = [0]
used = [False] * n
def when_pop(a,b,c): # 如果要保留边上的点,则>才弹栈
return (get_area(a,b,c)>0) if keep_point_on_edges else (get_area(a,b,c)>=0)
def make_hull(i,limit):
while len(hull)>limit and when_pop(points[hull[-2]],points[hull[-1]],points[i]):
used[hull.pop()] = False
used[i] = True
hull.append(i)
for i in range(1,n):
make_hull(i,1)
m = len(hull)
for i in range(n-2,-1,-1):
if not used[i]:
make_hull(i,m)
# hull.pop()
return [points[i] for i in hull[:-1]]
def calc_triangle_area(a,b,c):
# 给三个点,求这三个点组成的三角形面积
return abs(a[0]*b[1]+b[0]*c[1]+c[0]*a[1]-a[0]*c[1]-b[0]*a[1]-c[0]*b[1])/2
class Solution:
def largestTriangleArea(self, p: List[List[int]]) -> float:
p = AnrewHull(p)
n = len(p)
# ans = 0
# for i in range(n-2):
# for j in range(i+1,n-1):
# for k in range(j+1,n):
# ans = max(ans,calc_triangle_area(p[i],p[j],p[k]))
# return max(calc_triangle_area(a,b,c) for a,b,c in combinations(p,3))
ans = 0
for i in range(n-2):
k = i + 2
for j in range(i+1,n-1):
while k+1<n:
cur = calc_triangle_area(p[i],p[j],p[k])
nxt = calc_triangle_area(p[i],p[j],p[k+1])
if cur > nxt:
break
k += 1
ans = max(ans,calc_triangle_area(p[i],p[j],p[k]))
return ans