Leetcode 587. 安装栅栏
1.题目基本信息
1.1.题目描述
给定一个数组 trees,其中 trees[i] = [xi, yi] 表示树在花园中的位置。
你被要求用最短长度的绳子把整个花园围起来,因为绳子很贵。只有把 所有的树都围起来,花园才围得很好。
返回恰好位于围栏周边的树木的坐标。
1.2.题目地址
https://leetcode.cn/problems/erect-the-fence/description/
2.解题方法
2.1.解题思路
求凸包算法:andraw算法、Graham算法、jarvis算法
2.2.解题步骤
andraw算法(O(nlogn))
-
第一步,特殊情况处理。如果nodes的长度小于等于3,则结点都在凸包中,直接返回即可
-
第二步,定义求叉积函数cross,函数功能:求node1->node2和node2->node3两向量的叉积;根据叉积性质,如果axb二维叉积标量小于0,则向量b在向量a的右侧
-
第三步,将nodes按坐标位置进行升序排列
-
第四步,构建维护变量。stack维护遍历到的结点集合的凸包;visited维护stack中已经存在的结点,避免在求上半部分的凸包时重复(visited初始化时不标记起始结点,因为最后求上半闭包时也需要将起始结点入栈)
-
第五步,求凸包的下半部分。正序枚举nodes[1:],在stack长度大于等于2,且stack[-1]->nodes[i]的向量在stack[-2]->stack[-1]向量的右边时(叉积判断),不断循环从stack中弹出结点;同时在循环中和循环后更新维护变量stack和visited
-
第六步,求凸包的上半部分。逆序枚举nodes[:i-1],对于没有访问过的结点,在stack长度大于等于凸包下半部分长度时,且stack[-1]->nodes[i]的向量在stack[-2]->stack[-1]向量的右边时(叉积判断),不断循环从stack中弹出结点;同时在循环中和循环后更新维护变量stack和visited
-
第七步,将stack栈顶的起始结点进行弹出;根据stack中的结点索引构建凸包,返回即可
graham算法(O(nlogn))
-
第一步,特殊情况处理。如果nodes的长度小于等于3,则结点都在凸包中,直接返回即可
-
第二步,定义求叉积函数cross,函数功能:求node1->node2和node2->node3两向量的叉积;根据叉积性质,如果axb二维叉积标量小于0,则向量b在向量a的右侧
-
2.1.计算叉积
-
2.2.计算距离的平方
-
-
第三步,获取最底部的结点bottom,并将bottom结点置换到nodes的第一个位置
-
第四步,将nodes中各个结点进行角排序,通过叉积的正负来比较大小,叉积相同则通过与nodes[0]的距离排序
-
第五步,对于凸包上的最后一条边,如果上面存在共线,那么距离需要从大到小排列,所以需要翻转
- 5.1.将nodes[low,...,high]中结点进行翻转
-
第六步,正序枚举nodes[1:],在stack长度大于等于2,且stack[-1]->nodes[i]的向量在stack[-2]->stack[-1]向量的右边时(叉积判断),不断循环从stack中弹出结点;同时在循环中和循环后更新维护变量stack
-
第七步,根据stack中的结点索引构建凸包,返回即可
jarvis算法(O(n^2))
-
第一步,特殊情况处理。如果nodes的长度小于等于3,则结点都在凸包中,直接返回即可
-
第二步,定义求叉积函数cross。函数功能:求node1->node2和node2->node3两向量的叉积;根据叉积性质,如果axb二维叉积标量小于0,则向量b在向量a的右侧
-
第三步,获取最左边的最下面一个结点在nodes中的索引leftMost
-
第四步,构建维护变量。result数组存储凸包中已经找到的结点;visited维护各个结点是否已经在result中;p维护最后找到的一个凸包结点在nodes中的位置
-
第五步,循环遍历,寻找凸包上的结点添加到result数组中,并更新维护变量
-
5.1.找到一个结点q,使其余结点都在p->q向量的左边
-
5.2.找到q后,判断是否存在没有访问过且与p->q向量共线的结点,如果存在则将结点i添加到result数组中
-
5.3.如果q没有访问过,则将结点q添加到result结果数组中,并更新维护变量visited
-
5.4.将结点q赋值到p变量中,继续循环寻找下一个q结点;直到回到leftMost结点结束
-
-
第六步,返回结果result
3.解题代码
andraw算法代码
# ==> Andrew算法(求凸包算法)(O(nlogn))
def andrewConvexHull(nodes:list[list[int]]) -> list[list[int]]:n = len(nodes)# 第一步,特殊情况处理。如果nodes的长度小于等于3,则结点都在凸包中,直接返回即可if n < 4:return nodes# 第二步,定义求叉积函数cross,函数功能:求node1->node2和node2->node3两向量的叉积;根据叉积性质,如果axb二维叉积标量小于0,则向量b在向量a的右侧def cross(node1:list[int], node2:list[int], node3:list[int]) -> int:x1, y1 = node1x2, y2 = node2x3, y3 = node3return (x2 - x1) * (y3 - y2) - (x3 - x2) * (y2 - y1)# 第三步,将nodes按坐标位置进行升序排列nodes.sort()# 第四步,构建维护变量。stack维护遍历到的结点集合的凸包;visited维护stack中已经存在的结点,避免在求上半部分的凸包时重复(visited初始化时不标记起始结点,因为最后求上半闭包时也需要将起始结点入栈)stack = [0]visited = [False] * n# 第五步,求凸包的下半部分。正序枚举nodes[1:],在stack长度大于等于2,且stack[-1]->nodes[i]的向量在stack[-2]->stack[-1]向量的右边时(叉积判断),不断循环从stack中弹出结点;同时在循环中和循环后更新维护变量stack和visitedfor i in range(1, n):while len(stack) > 1 and cross(nodes[stack[-2]], nodes[stack[-1]], nodes[i]) < 0:item = stack.pop()visited[item] = Falsestack.append(i)visited[i] = True# 第六步,求凸包的上半部分。逆序枚举nodes[:i-1],对于没有访问过的结点,在stack长度大于等于凸包下半部分长度时,且stack[-1]->nodes[i]的向量在stack[-2]->stack[-1]向量的右边时(叉积判断),不断循环从stack中弹出结点;同时在循环中和循环后更新维护变量stack和visitedm = len(stack) # 凸包下半部分的结点个数for i in range(n - 2, -1, -1):if not visited[i]:while len(stack) > m and cross(nodes[stack[-2]], nodes[stack[-1]], nodes[i]) < 0:item = stack.pop()visited[item] = Falsestack.append(i)visited[i] = True# 第七步,将stack栈顶的起始结点进行弹出;根据stack中的结点索引构建凸包,返回即可stack.pop()return [nodes[i] for i in stack]class Solution:# 思路1:andraw算法def outerTrees1(self, trees: List[List[int]]) -> List[List[int]]:convexHull = andrewConvexHull(trees)return convexHull
graham算法代码
# ==> Graham算法(求凸包算法)(O(nlogn))
from functools import cmp_to_key
def grahamConvexHull(nodes:list[list[int]]) -> list[list[int]]:n = len(nodes)# 第一步,特殊情况处理。如果nodes的长度小于等于3,则结点都在凸包中,直接返回即可if n < 4:return nodes# 第二步,定义求叉积函数cross,函数功能:求node1->node2和node2->node3两向量的叉积;根据叉积性质,如果axb二维叉积标量小于0,则向量b在向量a的右侧# 2.1.计算叉积def cross(node1:list[int], node2:list[int], node3:list[int]) -> int:x1, y1 = node1x2, y2 = node2x3, y3 = node3return (x2 - x1) * (y3 - y2) - (x3 - x2) * (y2 - y1)# 2.2.计算距离的平方def dist2(node1:list[int], node2:list[int]) -> int:x1, y1 = node1x2, y2 = node2return (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)# 第三步,获取最底部的结点bottom,并将bottom结点置换到nodes的第一个位置bottom = 0for i in range(1, n):if nodes[i][1] < nodes[bottom][1]:bottom = inodes[bottom], nodes[0] = nodes[0], nodes[bottom]# 第四步,将nodes中各个结点进行角排序,通过叉积的正负来比较大小,叉积相同则通过与nodes[0]的距离排序def compare(node1:list[int], node2:list[int]) -> int:diff = -cross(nodes[0], node1, node2)return diff if diff else dist2(nodes[0], node1) - dist2(nodes[0], node2)nodes[1:] = sorted(nodes[1:], key = cmp_to_key(compare))# 第五步,对于凸包上的最后一条边,如果上面存在共线,那么距离需要从大到小排列,所以需要翻转right = n - 1while right >= 0 and cross(nodes[0], nodes[n - 1], nodes[right]) == 0:right -= 1low, high = right + 1, n - 1# 5.1.将nodes[low,...,high]中结点进行翻转while low < high:nodes[low], nodes[high] = nodes[high], nodes[low]low += 1high -= 1# 第六步,正序枚举nodes[1:],在stack长度大于等于2,且stack[-1]->nodes[i]的向量在stack[-2]->stack[-1]向量的右边时(叉积判断),不断循环从stack中弹出结点;同时在循环中和循环后更新维护变量stackstack = [0]for i in range(1, n):while len(stack) > 1 and cross(nodes[stack[-2]], nodes[stack[-1]], nodes[i]) < 0:stack.pop()stack.append(i)# 第七步,根据stack中的结点索引构建凸包,返回即可return [nodes[i] for i in stack]class Solution:# 思路3:Graham算法def outerTrees(self, trees: List[List[int]]) -> List[List[int]]:convexHull = grahamConvexHull(trees)return convexHull
jarvis算法代码
# ==> Jarvis算法(求凸包算法)(O(n^2))
def jarvisConvexHull(nodes:list[list[int]]) -> list[list[int]]:n = len(nodes)# 第一步,特殊情况处理。如果nodes的长度小于等于3,则结点都在凸包中,直接返回即可if n < 4:return nodes# 第二步,定义求叉积函数cross。函数功能:求node1->node2和node2->node3两向量的叉积;根据叉积性质,如果axb二维叉积标量小于0,则向量b在向量a的右侧def cross(node1:list[int], node2:list[int], node3:list[int]) -> int:x1, y1 = node1x2, y2 = node2x3, y3 = node3return (x2 - x1) * (y3 - y2) - (x3 - x2) * (y2 - y1)# 第三步,获取最左边的最下面一个结点在nodes中的索引leftMostleftMost = 0for i in range(n):if nodes[i][0] < nodes[leftMost][0] or (nodes[i][0] == nodes[leftMost][0] and nodes[i][1] < nodes[leftMost][1]):leftMost = i# 第四步,构建维护变量。result数组存储凸包中已经找到的结点;visited维护各个结点是否已经在result中;p维护最后找到的一个凸包结点在nodes中的位置result = []visited = [False] * np = leftMost# 第五步,循环遍历,寻找凸包上的结点添加到result数组中,并更新维护变量while True:# 5.1.找到一个结点q,使其余结点都在p->q向量的左边q = (p + 1) % nfor j, node in enumerate(nodes):if cross(nodes[p], nodes[q], node) < 0:q = j# 5.2.找到q后,判断是否存在没有访问过且与p->q向量共线的结点,如果存在则将结点i添加到result数组中for i in range(n):if not visited[i] and i != p and i != q and cross(nodes[i], nodes[p], nodes[q]) == 0:result.append(nodes[i])visited[i] = True# 5.3.如果q没有访问过,则将结点q添加到result结果数组中,并更新维护变量visitedif not visited[q]:result.append(nodes[q])visited[q] = True# 5.4.将结点q赋值到p变量中,继续循环寻找下一个q结点;直到回到leftMost结点结束p = qif p == leftMost:break# 第六步,返回结果resultreturn resultclass Solution:# 思路2:jarvis算法def outerTrees2(self, trees: List[List[int]]) -> List[List[int]]:convexHull = jarvisConvexHull(trees)return convexHull