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 = node1
        x2, y2 = node2
        x3, y3 = node3
        return (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和visited
    for 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] = False
        stack.append(i)
        visited[i] = True
    # 第六步,求凸包的上半部分。逆序枚举nodes[:i-1],对于没有访问过的结点,在stack长度大于等于凸包下半部分长度时,且stack[-1]->nodes[i]的向量在stack[-2]->stack[-1]向量的右边时(叉积判断),不断循环从stack中弹出结点;同时在循环中和循环后更新维护变量stack和visited
    m = 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] = False
            stack.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 = node1
        x2, y2 = node2
        x3, y3 = node3
        return (x2 - x1) * (y3 - y2) - (x3 - x2) * (y2 - y1)
    # 2.2.计算距离的平方
    def dist2(node1:list[int], node2:list[int]) -> int:
        x1, y1 = node1
        x2, y2 = node2
        return (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)
    # 第三步,获取最底部的结点bottom,并将bottom结点置换到nodes的第一个位置
    bottom = 0
    for i in range(1, n):
        if nodes[i][1] < nodes[bottom][1]:
            bottom = i
    nodes[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 - 1
    while right >= 0 and cross(nodes[0], nodes[n - 1], nodes[right]) == 0:
        right -= 1
    low, high = right + 1, n - 1
    # 5.1.将nodes[low,...,high]中结点进行翻转
    while low < high:
        nodes[low], nodes[high] = nodes[high], nodes[low]
        low += 1
        high -= 1
    # 第六步,正序枚举nodes[1:],在stack长度大于等于2,且stack[-1]->nodes[i]的向量在stack[-2]->stack[-1]向量的右边时(叉积判断),不断循环从stack中弹出结点;同时在循环中和循环后更新维护变量stack
    stack = [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 = node1
        x2, y2 = node2
        x3, y3 = node3
        return (x2 - x1) * (y3 - y2) - (x3 - x2) * (y2 - y1)
    # 第三步,获取最左边的最下面一个结点在nodes中的索引leftMost
    leftMost = 0
    for 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] * n
    p = leftMost
    # 第五步,循环遍历,寻找凸包上的结点添加到result数组中,并更新维护变量
    while True:
        # 5.1.找到一个结点q,使其余结点都在p->q向量的左边
        q = (p + 1) % n
        for 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结果数组中,并更新维护变量visited
        if not visited[q]:
            result.append(nodes[q])
            visited[q] = True
        # 5.4.将结点q赋值到p变量中,继续循环寻找下一个q结点;直到回到leftMost结点结束
        p = q
        if p == leftMost:
            break
    # 第六步,返回结果result
    return result

class Solution:
    # 思路2:jarvis算法
    def outerTrees2(self, trees: List[List[int]]) -> List[List[int]]:
        convexHull = jarvisConvexHull(trees)
        return convexHull

4.执行结果

posted @ 2025-05-25 18:41  Geek0070  阅读(19)  评论(0)    收藏  举报