【问题标题】:Minimum number of circles with radius r to cover n points覆盖 n 个点的半径为 r 的最小圆数
【发布时间】:2013-04-08 14:47:41
【问题描述】:

覆盖所有 n 个点所需的半径为 r 的最小圆数是多少? r 和 n 将作为输入给出,然后是 n 对整数,表示 n 个点的 x-y 坐标。 r 为实数,大于 0。n

如果点位于圆内,则圆覆盖该点。 如果该点与圆心之间的距离小于或等于 r,则该点位于圆内。

【问题讨论】:

  • 这不是编程题吗?你知道有一种东西叫做竞争性编程吗?我试图解决一个问题,我意识到我必须先解决这个子问题。为什么你认为每个人都要求你为他们做作业?
  • 这样的问题通常是可以接受的,但首先尝试自己解决问题总是有帮助的。 @PhillipSchmidt 这对我来说听起来像是一个编程问题。
  • @Dukeling 我想到了一个 2^n * 2^n 的解决方案,但这行不通,因为 n 可以是 20。我认为存在 n * 2^n 或 n^2 * 2^n 算法,但无法弄清楚。
  • @Dukeling 在 SO 上提出的问题旨在能够在编程环境中回答。见here。这个问题更适合math.SO
  • 我很乐意看到所显示的一些努力或一些上下文来表明这不仅仅是“请做我的功课”。

标签: algorithm computational-geometry


【解决方案1】:

这可能不是最好的解决方案,但并尝试对其进行优化。

算法基于随机抽样:

  1. 在地图上生成 N 个圆圈
  2. 删除所有未覆盖任何点的圆圈
  3. 按覆盖点数降序对圆进行排序
  4. Foreach 圆圈(已排序) - 将圆圈覆盖的点标记为已覆盖。如果圆圈没有覆盖任何新点,则从列表中删除。

这是您可以实时预览的代码:http://jsfiddle.net/rpr8qq4t/ 示例结果(每 30 个点 13 个圆圈):

参数化:

  var POINTS_NUMBER = 30;
  var RADIUS = 50;
  var SAMPLE_COUNT = 400;

可能会添加一些优化(例如某些圆圈可能会过早地从列表中排除)

编辑

  1. 步骤 1 中的更改带来更好的结果:为每个点生成 N 个圆(至少覆盖一个点的圆) 新版本:http://jsfiddle.net/nwvao72r/3/

编辑 2(最终算法)

最后:

  1. Foreach 点生成 N=10 个圆,随机距离小于点 R(圆的半径,因此我们确定每个圆至少有一个点属于它,每个点至少属于一个圆)
  2. 重复直到覆盖所有点:
    • 获得覆盖最大未覆盖点数的圆。将点标记为已覆盖。

这是对我来说效果最好的版本,你可以在这里查看 http://jsfiddle.net/nwvao72r/4/ 这里平均每 30 个点 12 个圆圈。

【讨论】:

  • 您的算法 1 和 2 似乎没有给出最小圆圈。但3似乎。尝试摆弄半径和点数。干得好。为 JSfiddle +1。
【解决方案2】:

我确定这个问题是 NP 难题,但我不打算在这里尝试证明这一点。

如果它是 NP 难的,那么为了找到一个有保证的最优解决方案,我推荐以下方法:

  1. 查找所有“良好”的潜在圆圈位置,并为每个记录找到其中包含的点。
  2. 使用这些点集解决集覆盖问题。 (这个问题是 NP 难的。)

良好的圈子展示位置

给定距离小于 2r 的任意 2 个点,正好有两个半径为 r 的圆通过这些点:

[编辑:我最初对“最佳”圈子的描述是错误的,尽管这不会导致问题——感谢评论者乔治描述了思考这个问题的正确方法。]

如果一个圆覆盖了最大的一组点(这意味着该圆不能重新定位以覆盖相同的一组点加上至少 1 个以上),那么该圆可以滑动直到其边界恰好接触到其中两个点它覆盖——比如说,向左滑动直到它接触到一个已经覆盖的点,然后围绕这个接触点顺时针旋转它直到它接触到另一个已经覆盖的点。这个移动的圆圈将完全覆盖原始圆圈所覆盖的点集。此外,我们永远不需要考虑覆盖非最大点集的圆,因为覆盖这些点和更多点的最大圆至少是有用的并且不会花费更多。这意味着我们只需要考虑接触两点的圆。如果我们为输入中每个足够接近的点对生成两个圆,我们将生成我们可能需要的所有圆。

因此,我们的潜在圈子池每对点最多包含 2 个圈子,总共最多 n*(n-1) 个潜在圈子。 (通常会更少,因为某些点对通常相距超过 2r,因此不能被半径为 r 的单个圆所覆盖。)此外,对于距离 任何其他点——这些圆圈也可能以这些远程点为中心。

设置封面

我们真正关心的是每个潜在圆圈所覆盖的点集。因此,对于每个潜在的圆圈,找到它所覆盖的点。这可以在 O(n^3) 时间内完成,对每个潜在的圆圈使用 O(n) 次。为了稍微加快速度,如果我们发现两个不同的圆圈覆盖完全相同的一组点,我们只需要保留其中一个圆圈(一组覆盖的点)。我们也可以丢弃任何作为其他覆盖点集子集的覆盖点集——在这种情况下,总是最好选择更大的覆盖点集。

最后我们有一个覆盖点集的集合,我们想要找到覆盖每个点的这些集合的最小子集。这是set cover problem。我不知道解决这个问题的具体算法,但branch and bound 是解决此类问题的标准方法——它通常比简单的详尽回溯搜索快得多。我首先会通过首先找到一个(或多个)启发式解决方案来启动搜索,希望能产生一个好的上限,从而减少分支和边界搜索时间。我认为即使是最好的算法在最坏的情况下也需要指数级的时间,尽管我认为这对于 n

【讨论】:

  • 很好,一种简单的思考方式——假设你有一个解决方案。解圆可以以任何方式任意移动(保持有效解),只要它们包含它们的原始点。因此它们都可以移动到接触两个,因此上述集合必须包含一个解决方案。
  • @george:你说得对,我目前对“最佳可能”的解释实际上是错误的——我会编辑更新。
【解决方案3】:

来自 Gautam K. Das 等人的论文“关于离散单元磁盘盖问题”。人:

最小几何磁盘盖。在最小几何圆盘覆盖问题中,输入由平面上的一组点组成,问题是找到一组最小基数的单位圆盘,其并集覆盖这些点。与 DUDC 不同,磁盘中心不限于从给定的离散集中选择,而是可以以平面中的任意点为中心。同样,这个问题是 NP-hard [9] 并且有一个 PTAS 解决方案 [11, 12]。

参考资料:

  1. R. Fowler、M. Paterson 和 S. Tanimoto,平面中的最优包装和覆盖是 NP 完全的,信息处理快报,第 12 卷,第 133-137 页,1981 年。
  2. G. Frederickson,平面图中最短路径的快速算法及其应用,SIAM J. on Computing,第 16 卷,第 1004-1022 页,1987 年。
  3. T. Gonzalez,涵盖多维空间中的一组点,信息处理快报,第 40 卷,第 181-188 页,1991 年。
  4. D. Hochbaum 和 W. Maass,图像处理和 VLSI 中覆盖和打包问题的近似方案,J. ACM,第 32 卷,第 130-136 页,1985 年。

【讨论】:

    【解决方案4】:

    我意识到圆不必以点为中心,因此计算通过两点任意组合的所有圆,包括以每个点为中心的圆。 然后我找到每个圆覆盖的点并使用贪心算法找到一个最小的圆集来覆盖所有点,但同样,它可能不是 最小的圆集,但很容易计算.

    from collections import namedtuple
    from itertools import product
    from math import sqrt
    from pprint import pprint as pp
    
    Pt = namedtuple('Pt', 'x, y')
    Cir = namedtuple('Cir', 'x, y, r')
    
    def circles_from_p1p2r(p1, p2, r):
        'Following explanation at http://mathforum.org/library/drmath/view/53027.html'
        (x1, y1), (x2, y2) = p1, p2
        if p1 == p2:
            #raise ValueError('coincident points gives infinite number of Circles')
            return None, None
        # delta x, delta y between points
        dx, dy = x2 - x1, y2 - y1
        # dist between points
        q = sqrt(dx**2 + dy**2)
        if q > 2.0*r:
            #raise ValueError('separation of points > diameter')
            return None, None
        # halfway point
        x3, y3 = (x1+x2)/2, (y1+y2)/2
        # distance along the mirror line
        d = sqrt(r**2-(q/2)**2)
        # One answer
        c1 = Cir(x = x3 - d*dy/q,
                 y = y3 + d*dx/q,
                 r = abs(r))
        # The other answer
        c2 = Cir(x = x3 + d*dy/q,
                 y = y3 - d*dx/q,
                 r = abs(r))
        return c1, c2
    
    def covers(c, pt):
        return (c.x - pt.x)**2 + (c.y - pt.y)**2 <= c.r**2
    
    if __name__ == '__main__':
        for r, points in [(3, [Pt(*i) for i in [(1, 3), (0, 2), (4, 5), (2, 4), (0, 3)]]),
                          (2, [Pt(*i) for i in [(1, 3), (0, 2), (4, 5), (2, 4), (0, 3)]]),
                          (3, [Pt(*i) for i in [(-5, 5), (-4, 4), (3, 2), (1, -1), (-3, 2), (4, -2), (6, -6)]])]:
            n, p = len(points), points  
            # All circles between two points (which can both be the same point)
            circles = set(sum([[c1, c2]
                               for c1, c2 in [circles_from_p1p2r(p1, p2, r) for p1, p2 in product(p, p)]
                               if c1 is not None], []))
            # points covered by each circle 
            coverage = {c: {pt for pt in points if covers(c, pt)}
                        for c in circles}
            # Ignore all but one of circles covering points covered in whole by other circles
            #print('\nwas considering %i circles' % len(coverage))
            items = sorted(coverage.items(), key=lambda keyval:len(keyval[1]))
            for i, (ci, coveri) in enumerate(items):
                for j in range(i+1, len(items)):
                    cj, coverj = items[j]
                    if not coverj - coveri:
                        coverage[cj] = {}
            coverage = {key: val for key, val in coverage.items() if val}
            #print('Reduced to %i circles for consideration' % len(coverage))
    
            # Greedy coverage choice
            chosen, covered = [], set()
            while len(covered) < n:
                _, nxt_circle, nxt_cov = max((len(pts - covered), c, pts)
                                             for c, pts in coverage.items())
                delta = nxt_cov - covered
                covered |= nxt_cov
                chosen.append([nxt_circle, delta])
    
            # Output
            print('\n%i points' % n)
            pp(points)
            print('A minimum of circles of radius %g to cover the points (And the extra points they covered)' % r)
            pp(chosen)
    

    显示三个运行的输出是:

    5 points
    [Pt(x=1, y=3), Pt(x=0, y=2), Pt(x=4, y=5), Pt(x=2, y=4), Pt(x=0, y=3)]
    A minimum of circles of radius 3 to cover the points (And the extra points they covered)
    [[Cir(x=2.958039891549808, y=2.5, r=3),
      {Pt(x=4, y=5), Pt(x=0, y=3), Pt(x=1, y=3), Pt(x=0, y=2), Pt(x=2, y=4)}]]
    
    5 points
    [Pt(x=1, y=3), Pt(x=0, y=2), Pt(x=4, y=5), Pt(x=2, y=4), Pt(x=0, y=3)]
    A minimum of circles of radius 2 to cover the points (And the extra points they covered)
    [[Cir(x=1.9364916731037085, y=2.5, r=2),
      {Pt(x=0, y=3), Pt(x=1, y=3), Pt(x=0, y=2), Pt(x=2, y=4)}],
     [Cir(x=4, y=5, r=2), {Pt(x=4, y=5)}]]
    
    7 points
    [Pt(x=-5, y=5),
     Pt(x=-4, y=4),
     Pt(x=3, y=2),
     Pt(x=1, y=-1),
     Pt(x=-3, y=2),
     Pt(x=4, y=-2),
     Pt(x=6, y=-6)]
    A minimum of circles of radius 3 to cover the points (And the extra points they covered)
    [[Cir(x=3.9951865152835286, y=-0.8301243435223524, r=3),
      {Pt(x=3, y=2), Pt(x=1, y=-1), Pt(x=4, y=-2)}],
     [Cir(x=-2.0048134847164714, y=4.830124343522352, r=3),
      {Pt(x=-4, y=4), Pt(x=-3, y=2), Pt(x=-5, y=5)}],
     [Cir(x=6.7888543819998315, y=-3.1055728090000843, r=3), {Pt(x=6, y=-6)}]]
    

    【讨论】:

    • 我无法遵循所有的 Python,但这看起来是一个很好的启发式解决方案。 +1。
    • 感谢@j_random_hacker 指出圆圈不必以点为中心——阅读规范是一门艺术,我正在不断学习 :-)
    【解决方案5】:

    平铺然后摇晃

    1. TILE:找到包围所有点的矩形
    2. 用间隔为 r * sqrt(2) 的圆圈平铺矩形区域。
    3. 为每个点计算它们是哪些圆圈以及每个圆圈中有哪些点。
    4. 删除任何没有点的圆圈。
    5. 删除仅包含多个圆中所含点的任何圆。
    6. 重复 5 直到没有更多。
    7. Jiggle:对于每个圆圈:尝试移动它,看看它是否可以覆盖原来的点以及最多的新点,然后这样做。
    8. 再次执行 4 和 5。
    9. 重复 7 直到摇晃不改变哪些圆圈点在时间用完。

    第 2 步,可以通过遍历每个点并仅计算/保留包含一个点的圆(如果平铺非常稀疏)来优化平铺。

    【讨论】:

      【解决方案6】:

      我不确定这是否正确,但如果我们不需要解圈的确切位置,在我看来,我们可以通过查看点簇来解决这个问题:在任何解圆,任意两点之间的距离应小于或等于2*r。

      算法:

      1. j_random_hacker indicated that any solution-circle could be shifted so that
         two of its covered-points lay on its circumference without changing the 
         original covered-points. Since the solution-circle radius is given, for each 
         point: (a) calculate potential circle-centers using the point, radius, and 
         each other point that is at a distance of 2*r or less, (b) for each circle, 
         list the cluster of points that it could cover. Sort each cluster and, for
         each point, remove duplicate clusters. 
      
      2. For each cluster group in 1., choose the cluster that has the greatest point-
         count, that is, the cluster that is most shared.
      
      3. Remove duplicates and clusters that are sub-sequences of other clusters 
         from 2., and present the resulting size of 2. (perhaps together with the 
         chosen clusters) as the solution.
      


      等边三角形的输出,r=3, [(0,0),(5.196152422706632,3),(5.196152422706632,-3)]

      *Main> solve
      (2,[[(0.0,0.0),(5.196152422706632,3.0)],[(0.0,0.0),(5.196152422706632,-3.0)]])
      


      Paddy3118 示例的输出,r=3, [(1,3),(0,2),(4,5),(2,4),(0,3)]:

      *Main> solve
      (1,[[(0.0,2.0),(0.0,3.0),(1.0,3.0),(2.0,4.0),(4.0,5.0)]])
      


      r=3, [(-5,5),(-4,4),(3,2),(1,-1),(-3,2),(4, -2),(6,-6)]:

      *Main> solve
      (3,[[(-5.0,5.0),(-4.0,4.0),(-3.0,2.0)],[(1.0,-1.0),(3.0,2.0),(4.0,-2.0)],
          [(4.0,-2.0),(6.0,-6.0)]])
      


      Haskell 代码:

      import Data.List (delete, nub, nubBy, isInfixOf, sort, sortBy, maximumBy)
      
      points = [(0,0),(5.196152422706632,3),(5.196152422706632,-3)]--[(1,3),(0,2),(4,5),(2,4),(0,3)]--[(-5,5),(-4,4),(3,2),(1,-1),(-3,2),(4,-2),(6,-6)]
      r = 3
      twoR = 2*r
      
      circleCenters (x1,y1) (x2,y2) =
        let q = sqrt $ (x2-x1)^2 + (y2-y1)^2
            (x3, y3) = ((x1+x2)/2,(y1+y2)/2)
            first = (x3 + sqrt(r^2-(q/2)^2)*(y1-y2)/q, y3 + sqrt(r^2-(q/2)^2)*(x2-x1)/q)
            second = (x3 - sqrt(r^2-(q/2)^2)*(y1-y2)/q, y3 - sqrt(r^2-(q/2)^2)*(x2-x1)/q)
        in [first,second]
      
      isInCircle (center_x,center_y) (x,y) = (x-center_x)^2 + (y - center_y)^2 <= r^2
      
      findClusters (px,py) = 
        nub [sort $ [(px,py)] ++ filter (isInCircle a) potentialPoints | a <- potentialCircleCenters]
          where
            potentialPoints = filter (\(x,y) -> (x-px)^2 + (y-py)^2 <= twoR^2) (delete (px,py) points)
            potentialCircleCenters = concatMap (circleCenters (px,py)) potentialPoints
      
      solve = (length bestClusters, bestClusters) where
        clusters = map findClusters points
        uniqueClusters = nub . concat $ clusters
        bestClusterForEachPoint = map (maximumBy (\a b -> compare (length a) (length b))) clusters
        bestClusters = nub . nubBy (\a b -> isInfixOf a b) . sortBy (\a b -> compare (length b) (length a)) 
                       $ bestClusterForEachPoint
      

      【讨论】:

      • 我还不明白你的第 2 步和第 3 步,但是有一个更大的问题:虽然同一个圆所覆盖的任何两个点的距离肯定是
      • 搜索孤立的集群(可以在步骤
      • 我怀疑 groovy 的算法给了我们一个有效的上限近似值。您可能会先这样做,然后解决 j_random 的设置覆盖问题。如果你想证明一个绝对最小值。
      • 不客气!我似乎总是在找错误——请不要灰心,这些艰难的打击正在让你成为一个更好的程序员:)
      • @j_random_hacker 一点也不,我很高兴有机会了解更多信息,这就是我征求您的反馈意见的原因。
      【解决方案7】:

      如果圆心C(cx, cy) 覆盖点P(px, py),则距离|CP| &lt; rr - 半径)。 所以圆心可能是覆盖点P的区域是圆心在P和半径r的圆。现在让我们绘制所有圆心在给定点和半径r。如果某些圆相交,那么我们可以在覆盖相应点的这种相交处绘制以中心为中心的新圆。 因此,对于每一对输入点,我们检查圆是否相交。

      假设输入点是顶点,而交点是它们之间的边。 现在我们有一个已知的图问题最小边覆盖http://en.wikipedia.org/wiki/Edge_cover 可以在多项式时间内解决(尽管有限制n &lt; 20 蛮力可能是可以接受的)

      更新。那不是边缘覆盖。我的错。

      【讨论】:

      • 不幸的是,它不等同于 Edge Cover,因为一个圆可以覆盖不止一对点(即一个圆可以对应多个边)。此外,它们不需要以现有点为中心,并且不这样做可能是有利的(例如,对于一些小的 epsilon 值,等边三角形中的 3 个点的边长等于 r + epsilon)。
      【解决方案8】:

      这是我的第一个答案,我将在另一个答案中提到它。 但请参阅我稍后的回答,该回答考虑了两点之间的圆,而不是这个。 这是一个用 Python 编写的贪心算法,它会找到 a 最小值,但我不知道它是否是 最小解。

      dbg = False
      if not dbg:
          r, n = (int(s) for s in input('r n: ').split())
          points = p = [ tuple(int(s) for s in input('x%i y%i: ' % (i, i)).split())
                         for i in range(n) ]
      else:
          r, n, points = 3, 5, [(1, 3), (0, 2), (4, 5), (2, 4), (0, 3)]; p = points
      
      # What a circle at each point can cover
      coverage = { i: frozenset(j
                                for j in range(i, n)
                                if (p[i][0] - p[j][0])**2 + (p[i][1] - p[j][1])**2 <= r**2)
                   for i in range(n)}
      
      # Greedy coverage choice
      chosen, covered = [], set()
      while len(covered) < n:
          # Choose the circle at the point that can cover the most ADDITIONAL points.
          _, nxt_point, nxt_cov = max((len(pts - covered), i, pts)
                                      for i, pts in coverage.items())
          covered |= nxt_cov
          chosen.append(nxt_point)
      print('Cover these points:\n  %s' % '\n  '.join('%s, %s' % p[i] for i in chosen))
      

      这是一个示例运行:

      r n: 3 5
      x0 y0: 1 3
      x1 y1: 0 2
      x2 y2: 4 5
      x3 y3: 2 4
      x4 y4: 0 3
      Cover these points:
        1, 3
        4, 5
      

      注意:数据输入/输出是初步的,但算法应该是清晰的

      【讨论】:

      • 肯定不会每次都返回最佳解决方案(例如,想象一个只有 2 个点的数据集,它们之间的距离为 r + epsilon,对于一些小的 epsilon - 这些可以由 a单圈,但您的算法将需要 2)。尽管如此,还是一个不错的启发式方法。
      • 是的。我知道在从两个点和上面的半径帖子阅读了两个圆圈之后,我正在更新。
      【解决方案9】:

      如果您将n 圆(半径为r)全部以每个点为中心,则查找最大重叠的区域/点并将新的圆(半径为r)以该区域为中心。我不确定这是否是解决解决方案的最佳方法(如果这是一种解决方法,除了蛮力方法),我相信你可以用相当多的数学来实现它,并且从而降低解决方案的运行时复杂性。希望这可以帮助。请提供反馈。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-01-14
        • 2011-03-14
        • 2014-01-17
        • 1970-01-01
        • 1970-01-01
        • 2021-06-01
        • 2010-11-27
        相关资源
        最近更新 更多