寻找最大的点集,形成一个凸多边形。

16

我正在寻找一种算法,用于从给定点集中找到形成凸多边形的最大点集(所谓最大是指点数最多)。

我认为可以使用DP来解决这个问题,但我不确定。

能否在O(n^3)的时间复杂度内完成呢?

实际上,我只需要知道最大子集的大小,因此它不需要具有唯一的解决方案。

编辑:

为了简化问题,假设输入为一个二维平面上的点集。

期望输出:能够形成凸多边形的最大点数。例如,对于给定的数据,输出为5(ABHCD是可能的凸多边形之一)。

an example

这里有一个类似的问题 spoj.com/problems/MPOLY,可以使用DP在O(N^3)的时间复杂度内解决,我的问题是关于该问题的推广,即不一定包含(0,0)点。


那不是一个圆吗?还是说,给定一组点,它是最大的子集? - Valentin
我稍微编辑了一下我的问题,或许这样能帮助理解我的问题。 - zeulb
寻找所有可能的凸多边形是一个选项吗? - Valentin
就像找到所有可能的组合,然后在列表中搜索最大值。 - Valentin
如果你在谈论多边形ADFG,是的,但如果它只有4个顶点,那么它不是最大解决方案。 - zeulb
显示剩余2条评论
4个回答

4

我想通过扩展Andrew的凸包算法为它设计了一个算法。

起初,我提出了一个O(N^4)的算法,但是我注意到这样做过于复杂,将其改进为O(N^2)的算法。在至少存在竖直点线的情况下,代码中可能存在错误。这可能是一种特殊情况(需要更改几行代码)或算法中更大缺陷的标志。如果是后者,那么我倾向于说它是NP难问题,并将该算法作为启发式算法提供。我已经花费了所有我想要编码和调试的时间。无论如何,在其他情况下似乎都能正常工作。然而,当正确的算法似乎是O(2^N)时,很难测试其正确性。

def maximal_convex_hull(points):
    # Expects a list of 2D points in the format (x,y)


    points = sorted(set(points)) # Remove duplicates and sort
    if len(points) <= 1: # End early
        return points

    def cross(o, a, b): # Cross product
        return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])


    # Use a queue to extend Andrew's algorithm in the following ways:
    # 1. Start a new convex hull for each successive point
    # 2. Keep track of the largest convex hull along the way
    Q = []
    size = 0
    maximal_hull = None
    for i,p in enumerate(points):
        remaining = len(points) - i + 1
        new_Q = []
        for lower, upper in Q: # Go through the queue
            # Build upper and lower hull similtanously, slightly less efficent
            while len(lower) >= 2 and cross(lower[-2], lower[-1], p) < 0:
                lower.pop()
            lower.append(p)
            while len(upper) >= 2 and cross(upper[-2], upper[-1], p) > 0:
                upper.pop()
            upper.append(p)

            new_size = len(lower) + len(upper)
            if new_size > size: # Check for a new highest maximal convex hull
                size = new_size
                maximal_hull = (lower, upper)


        # There is an odd bug? that when one or both if statements are removed
        #  xqg237.tsp (TSPLIB) gives slightly different solutions and is
        #   missing a number of points it should have.
        #  Seems like if the opposite should be true if anything since they
        #   were intended to be easy optimizations not required code.
            if remaining + new_size >= size: # Don't bother if not enough
                new_Q.append((lower, upper)) # Add an updated convex hulls
        if remaining > size: # Don't bother if not enough
            new_Q.append(([p], [p])) # Add a new convex hull
        Q = new_Q # Update to the new queue

    # Extract and merge the lower and upper to a maximium convex hull
    # Only one of the last points is ommited because it is repeated
    #    Asserts and related variables used for testing
    #    Could just have "return lower[:-1] + list(reversed(upper[:-1]))[:-1]"
    lower, upper = maximal_hull
    max_hull_set = set(lower) | set(lower) | set(upper)
    lower = lower
    upper = list(reversed(upper[:-1]))[:-1]
    max_convex_hull = lower + upper
    assert len(lower) + len(upper) == len(max_hull_set)
    assert max_hull_set == set(max_convex_hull)
    return max_convex_hull

编辑:这个算法并不正确,但它启发了正确算法的诞生,因此我将其保留在这里。


安德鲁算法的时间复杂度为O(N)(但需要排序,使其变为O(N log N)),该算法运行了O(N)个版本的安德鲁算法。因此总的时间复杂度为O(N^2)。尽管如此,该算法可能无法满足需求。 - Nuclearman
是的,但我对该算法的正确性有些怀疑,你能否解释一下算法的工作原理,因为我对Python不是很熟悉。 - zeulb
我认为我理解了你的代码,但是当运行时,你的代码不是返回4吗? - zeulb
它确实存在,快速修复无法解决问题。这意味着如果问题不是NP难的话,需要使用动态规划来解决,因为它要求不将B和C添加到凸包中。如果可能,请发布您自己的解决方案。我很好奇。 - Nuclearman
谢谢你的帮助!我已经发布了我的解决方案,请告诉我是否有任何缺陷。 - zeulb
显示剩余5条评论

4
这个问题已经在以下比赛中发布过:

它的解决方案(O(N 3 )算法)可以在这个页面上找到:“USACO DEC08问题'fence'分析”由Bruce Merry和Jacob Steinhardt。

以下是尝试解释此算法。此外,这里是我在C ++ 11中实现的代码。此代码适用于N&lt; = 250和范围为0 .. 1023的整数坐标。不应有三个点在同一条线上。这里是Python脚本,可生成满足这些要求的测试数据。

简化问题的O(N 2 )算法

简化问题:查找包含给定集合的最左侧点(或如果有多个最左侧点,则此多边形应包含其中最高的点)的最大点子集。

要解决这个问题,我们可以通过有向线段连接每对点,然后(隐式地)将每个线段视为图节点,如下图所示:

representing point set as a graph

这里父节点和相应的线段具有蓝色。其左子节点(红色)对应的线段从“父”线段的末端开始,它是相对于“父”线段方向做最小可能左转的线段。其右子节点(绿色)对应的线段从“父”线段的起点开始,它也是相对于“父”线段方向做最小可能左转的线段。

以左侧最点结束的任何线段都对应于“叶”节点,即它没有子节点。这种图的构建保证没有循环,换句话说,这个图是一个DAG。

现在要找到最大的凸点子集,只需要在此图中找到最长路径即可。 DAG中的最长路径可以在O(E)的时间内使用动态编程算法找到,其中E是图中的边数。由于每个节点仅具有2个出站边,每个对应于一对点,因此可以在O(N 2 )时间内解决此问题。

如果我们预处理输入数据,将以相同点为起点的有向线段按角度排序,则可以实现这一点。这样可以在图中执行深度优先遍历。我们应该记住从每个处理节点开始的最长路径,以便每个图边最多被访问一次,并且我们有O(E) = O(N^2)时间复杂度算法(暂时忽略预处理时间)。空间要求也是O(N^2),因为我们需要为每对点存储排序的方向,并且记忆化使用每个节点一个值(这也是一对点)。
这是这种动态规划算法的C++实现:
unsigned dpStep(ind_t i_left, ind_t from, ind_t to_ind)
{
    ind_t child = sorted[from][to_ind];

    while (child < i_left)
        child = sorted[from][++to_ind];

    if (child == i_left)
        return 1;

    if (auto v = memorize[from][child])
        return v;

    const auto x = max(dpStep(i_left, child, lefts[from][child]) + 1,
                       dpStep(i_left, from, static_cast<ind_t>(to_ind + 1)));

    memorize[from][child] = static_cast<ind_t>(x);
    return x;
}

输入参数是最左边的点和形成当前线段的一对点(其中线段的起始点直接给出,但结束点是按角度排序的点数组中的索引)。在这段代码片段中,“left”这个词有点过度使用:它既表示最左边的点(i_left),也表示包含图形左侧子节点的预处理数组(lefts[][])。

O(N3)算法

通用问题(最左边的点不固定)可以通过以下方式解决:

  1. 按从左到右的方向对点进行排序。
  2. 预处理点以获取两个数组:(a)每个点相对于其他点的方向排序,以及(b)使得相对于“父”线段的方向做最小可能左转的线段的末尾在第一个数组中的位置。
  3. 将每个点用作最左边的点,并使用“简化”的算法找到最长凸多边形。
  4. 定期从预处理数组中删除“最左边”点左侧的所有点。
  5. 在步骤3中找到的路径中选择最长的路径。

第4步是可选的。它不会改善O(N3)时间复杂度。但是它通过排除不需要的点略微提高了DP算法的速度。在我的测试中,这可以提高33%的速度。

有几种预处理的替代方法。我们可以计算每个角度(使用atan2)并对它们进行排序,就像Bruce Merry和Jacob Steinhardt在示例代码中所做的那样。这是一种最简单的方法,但是atan2可能太昂贵了,特别是对于较小的点集。

或者,我们可以排除atan2,并对切线而不是角度进行排序,就像我实现的那样。这有点更复杂,但更快。

这两种替代方法都需要O(N2 log N)时间(除非使用某些分布排序)。第三种替代方法是使用众所周知的计算几何方法(排列和对偶性)进行O(N2)预处理。但我认为它对我们的问题没有用处:对于较小的点集,由于大的常数因子,它可能太慢了,因为对于较大的点集,步骤3将大大超过步骤2。而且它要更难实现。

其他一些结果:我尝试实现迭代DP而不是递归;这并没有明显改变速度。我还尝试同时执行两个DP搜索,交错每个搜索的步骤(类似于纤程或在软件中实现的超线程);这可能有所帮助,因为DP主要由具有高延迟的内存访问组成,阻止了CPU吞吐量的充分利用;结果并不是非常令人印象深刻,只有约11%的速度提升,最可能使用真正的超线程会快得多。


非常抱歉打扰您,但我无法理解给定URL http://cerberus.delos.com:790/TESTDATA/DEC08.fence.htm 上的一件事。它写道:“壳体中的第一个和最后两个点足以检查下一个点是否违反凸性条件”。您能否解释一下?为什么不需要所有点?这些点按特定顺序排列吗? - Naman
@Naman:看起来很清楚了。我只能用不同的话来解释一下。在每个DP步骤中,有3组点:(a)提到的4个点(第一个/最后一对),(b)已经在凸包中的点(它们已经被DP算法选择,因此它们满足凸性条件并且它们是最优的),(c)所有其他点。在每个步骤中,算法独立地尝试来自组(c)的每个点,并检查该点相对于组(a)的点的凸性条件。如果点符合条件,则无需检查其相对于组(b)的点的凸性,它保证会被满足。 - Evgeny Kluev
不知何故我仍无法理解为什么它是保证的。能否请给出一个案例解释一下。考虑第一对(2,2),(3,1),最后一对(8,2),(7,1),以及已在凸包中的点(6,6),(7,5)。现在有一个新点(5,3)。它将通过第一对和最后一对的凸性条件,但当我们将其与组(b)中包括的所有点一起考虑时,则不会满足条件。我知道我在理解上可能犯了一些微不足道的错误,但如果您能纠正我,我将不胜感激。 - Naman
@Naman:现在我明白了你问题的最后一部分。是的,点是排列好的。在你的例子中,成对的第一个点(也是序列中的第一个点)是(3,1),最后一个点是(7,1)。因此,(5,3)不能插入到(7,1)之后。如果这样做,你将得到{(8,2), (7,1), (5,3), (3,1), (2,2)},这不是凸的。 - Evgeny Kluev
那么您的意思是在给定算法之前,我需要对给定的点进行排序/排序吗?如果是这样,按什么顺序排序(按x、y还是顺时针)?如果我问了一些愚蠢的问题,我很抱歉。如果您能帮助我找到一个详细解释的链接,那将非常有帮助。 - Naman
@Naman:不需要为此算法预先排序点。但是它们是由算法按顺序选择的,这决定了它们的顺序。对于DP数组的不同部分,这个顺序可能是不同的。我不知道在哪里可以找到更详细的解释。也许你应该阅读(任何)算法书来理解DP原理,以及(任何)计算几何书来了解凸性…… - Evgeny Kluev

1
这是我使用Andrew算法的动态规划O(N^4)算法,由Nuclearman发布,我仍在寻找一个O(N^3)算法。
upper_hull(most left point, previous point, current point)
{
    ret = 0
    if (current point != most left point)   /* at anytime we can decide to end the upper hull and build lower_hull from current point ending at most left point */
        ret = min(ret,lower_hull(most left point, -1, current point)) 
    for all point on the right of current point /* here we try all possible next point that still satisfy the condition of convex polygon */
        if (cross(previous point,current point,next point) >= 0) max(ret,1+upper_hull(most left point, current point, next point))
    return ret;
}

lower_hull(most left point, previous point, current point)
{
    if (current point == most left point) return 0;
    ret = -INF /* it might be impossible to build a convex hull here, so if it does it will return -infinity */
    for all point on the left of current point and not on the left of most left point
        if (cross(previous point,current point,next point) >= 0) max(ret,1+lower_hull(most left point, current point, next point))
    return ret;
}

首先根据x轴对点进行排序,然后在平局的情况下按y轴排序,然后尝试将所有点作为最左边的点运行upper_hull(p,-1,p),请告诉我此算法是否有任何缺陷。


1
一种可能的方法是预先计算所有叉积结果(O(N ^ 3)),并根据结果是正数还是负数将它们分成两个图,然后从最左边的点开始使用深度优先搜索来查找最短路径中的最长路径。看起来应该可以在O(E)中完成,因为边缘是三元组(a,b),(b,c),所以是O(N ^ 3)。但是,您必须对O(N)个点(对于每个最左边的点)执行此操作,导致总时间复杂度为O(N ^ 4)。因此,我现在相当确定您无法获得比O(N ^ 4)更好的结果。 - Nuclearman
谢谢你的慷慨奖励,很高兴能够帮上忙。 - Nuclearman

0

您可以使用Delaunay三角剖分并删除最长的边和顶点。我使用类似的算法来查找凸包。您可以在人口数据基础上找到一个示例,链接为http://www.phpdevpad.de/geofence。我还编写了一个名为concave-hull的php类,可在phpclasses.org上找到。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接