尽可能均匀地分布圆上的点

时间:2010-08-13 18:37:09

标签: python algorithm geometry

问题陈述

我有以下问题:我有一个圆圈,其上有一定数量(零个或多个)的点。这些职位是固定的。现在我必须在圆上放置另一组点,例如所有点在圆周上尽可能均匀分布。

目标

我的目标是开发一种算法,该算法采用角度列表(表示固定点)和int值(表示应放置多少个附加点)并再次返回角度列表(仅包含角度附加点应该是)。

这些点不必真正均匀分布(彼此距离相同),而是尽可能均匀分布。大多数时候可能不存在完美的解决方案,因为某些点是固定的。

所有角度的范围都在-pi和+ pi之间。

实施例

我想要实现的一些例子:

fixed_points = [-pi, -pi/2, pi/2]

 v         v                   v
 |---------|---------|---------|---------|
-pi     -pi/2        0        pi/2       pi

fill_circle(fixed_points, 1)
# should return: [0]

fill_circle(fixed_points, 2)
# should return: [-pi/6, pi/6]

或:

fixed_points = [-pi, -pi*3/4, -pi/4]

 v    v         v
 |---------|---------|---------|---------|
-pi     -pi/2        0        pi/2       pi

fill_circle(fixed_points, 6)

这最后一个例子应该返回如下内容:一点是在-pi * 3/4和-pi / 4之间设置,即:-pi / 2并在-pi / 4之间分配其他5个点+ pi(记住它是一个圆圈,所以在这种情况下-pi = + pi):

 v    v    x    v   x   x    x   x    x
 |---------|---------|---------|---------|
-pi     -pi/2        0        pi/2       pi

上一次尝试

我开始使用递归算法,该算法首先搜索两点之间的最大间隔,并在两者之间设置新点。然而,它没有给出令人满意的结果。例如,考虑这种配置,需要插入两个点:

 v         v                   v
 |---------|---------|---------|---------|
-pi     -pi/2        0        pi/2       pi

first step: insert right in the middle of largest interval
 v         v         x         v
 |---------|---------|---------|---------|
-pi     -pi/2        0        pi/2       pi

second step: insert right in the middle of largest interval 
-> all intervals are evenly large, so one of them will be taken
 v    x    v         v         v
 |---------|---------|---------|---------|
-pi     -pi/2        0        pi/2       pi

不是一个非常好的解决方案,因为它可以更好地分布(见上文:-pi / 6和+ pi / 6)。

很抱歉这个问题很长,我希望你能理解我想要达成的目标。

我不需要一个完整的工作算法,而是开发一个正确的想法。如果你愿意,也许有些伪代码。非常感谢一些提示让我朝着正确的方向前进。提前谢谢!

更新:已完成的算法:

谢谢大家的回答!它出现了我基本上只需要我已经存在的算法的非贪婪版本。我真的很喜欢haydenmuhls想法通过封装间隔/段类来简化问题:

class Segment:
    def __init__(self, angle, prev_angle, wrap_around):
        self.angle = angle
        self.length = abs(angle - prev_angle + \
                          (2*math.pi if wrap_around else 0))
        self.num_points = 0

    def sub_length(self):
        return self.length / (self.num_points + 1)

    def next_sub_length(self):
        return self.length / (self.num_points + 2)

    def add_point(self):
        self.num_points += 1

这使得实际算法非常简单易读:

def distribute(angles, n):
    # No points given? Evenly distribute them around the circle
    if len(angles) == 0:
        return [2*math.pi / n * i - math.pi for i in xrange(n)]

    # Sort the angles and split the circle into segments
    s, pi, ret = sorted(angles), math.pi, []
    segments = [Segment(s[i], s[i-1], i == 0) for i in xrange(len(s))]

    # Calculate the length of all subsegments if the point
    # would be added; take the largest to add the point to
    for _ in xrange(n):
        max(segments, key = lambda x: x.next_sub_length()).add_point()

    # Split all segments and return angles of the points
    for seg in segments:
        for k in xrange(seg.num_points):
            a = seg.angle - seg.sub_length() * (k + 1)
            # Make sure all returned values are between -pi and +pi
            ret.append(a - 2*pi if a > pi else a + 2*pi if a < -pi else a)

    return ret

9 个答案:

答案 0 :(得分:10)

假设您已经提供了M个点,并且需要添加N个点。如果所有点均匀分布,则它们之间会有2*pi/(N+M)的间隙。因此,如果你在M点切割以给出M个角度段,你当然可以将点放入一个段(彼此间隔均匀),直到空间小于或等于{{ 1}}。

因此,如果细分受众群的长度为2*pi/(N+M),那么您应该将L点放入其中。

现在你将剩下一些积分。如果添加了一个点,则按点之间的间距对线段进行排名。实际上将该点添加到排名最低的细分市场。将其中的一个重新插入到已排序的列表中,然后再次执行此操作,直到用完为止。

因为每次你将一个点放入一个段,其中结果将尽可能地间隔点,并且点之间的空间不依赖于你添加它们的顺序,你将最终得到最佳间距。

(编辑:其中“最佳”表示“点之间的最大最小距离”,即尽可能避免最坏情况下彼此重叠的点。)

(编辑:我希望很清楚,这个想法是决定每个段中有多少点,然后只有在最后,在数字全部被确定之后,你是否在每个段内平均分配它们。 )

答案 1 :(得分:5)

您可以使用Interval对象。间隔是两个原始不可移动点之间的圆弧。

以下只是伪代码。不要指望它可以在任何地方运行。

class Interval {

  private length;
  private point_count;

  constructor(length) {
    this.length = length;
    this.point_count = 0;
  }

  public add_point() {
    this.point_count++;
  }

  public length() {
    return this.length;
  }

  // The current length of each sub-interval
  public sub_length() {
    return this.length / (this.point_count + 1);
  }

  // The sub-interval length if you were to add another point
  public next_sub_length() { 
    return this.length / (this.point_count + 2);
  }

  public point_count() {
    return this.point_count;
  }
}

创建与圆上各点之间的间隔对应的这些对象的列表。每次添加一个点时,选择具有最大next_sub_length()的间隔。当你完成后,重建新圈子并不困难。

这应该为您提供最大可能的最小间隔。也就是说,如果您按照最小间隔的长度对解决方案进行评分,这将为您提供最高分数。我认为这就是你拍摄的内容。

编辑:刚刚意识到您在Python中专门询问了这一点。我是一个Python n00b,但你应该能够轻松地将它转换为Python对象,尽管你不需要getter,因为Python中的所有东西都是公共的。

答案 2 :(得分:4)

假设点之间的间隔是a_1 ... a_n。然后,如果我们将每个片段划分为最小尺寸d的片段,我们可以在片段中适合floor(a_i/d) - 1个点。这意味着sum(floor(a/d) for a in interval_lengths)必须大于或等于n + s,其中n是我们要添加的点数,s是已存在的点数。我们希望选择尽可能大的d,最好只进行最佳d的二进制搜索。

一旦我们选择了d,只需逐步通过每个段每隔d度添加点,直到段中剩下不到2度的度数

修改您只需找到sum(floor(a/d) for a in interval_lengths) == n + s,然后将floor(a_i/d) - 1分配给每a_i/(floor(a_i/d) - 1)度的段i。二进制搜索会很快找到。

进一步修改

以下是找到d

的代码
def get_d(n, a, lo=0, hi=None):
    s = len(a)
    lo = 360./(s + n)
    hi = 2. * lo
    d = (lo + hi)/2
    c = sum(floor(x/d) for x in a)
    while c != (n + s):
        if c < (n + s):
            hi = mid
        else:
            lo = mid
        d = (lo + hi)/2
        c = sum(floor(x/d) for x in a)
    return d

答案 3 :(得分:2)

首先,我们将术语重新定义如下: 找到N个点的这种分布,这两个点中的任何一个与M预定义之间的最小距离的长度是最大的。 所以你的任务是找到这个最小长度的最大值。称之为L 您有M段现有段,假设它们存储在列表s中。所以如果这个长度首先是L

min(s) > L

和最多额外点数是

 f(L) = sum(ls/L -1 for ls in s)

因此,您可以使用二进制搜索找到最优L,其中起始最小值L = 0且最大值L = min(s),并且如果sum(ls / L -1表示s中的ls)> = N,则检查条件。 然后对于每个段s [i],你可以均匀地放置s [i] / L -1个点。 我认为这是最佳解决方案。

已更新 min(s) > L存在漏洞。这对于重新定义的术语来说已经足够了,但对于原始术语来说却是错我已将此条件更改为max(s) > L。还在二进制搜索中添加了小于L的段的跳过。 这是完整的更新的代码:

from math import pi,floor
def distribute(angles,n):
    s = [angles[i] - angles[i-1] for i in xrange(len(angles))]
    s = [ls if ls > 0 else 2*pi+ls for ls in s]
    Lmin, Lmax = 0., max(s)
    while Lmax - Lmin >1e-9:
        L = (Lmax + Lmin)/2
        if sum(max(floor(ls/L) -1,0) for ls in s ) < n: Lmax = L
        else : Lmin = L
    L= Lmin
    p = []
    for i in xrange(len(s)):
        u = floor(s[i]/L) -1
        if u <= 0:continue
        d = s[i]/(u+1)
        for j in xrange(u):
            p.append(angles[i-1]+d*(j+1))
    return p[:n]
print distribute((0, pi/4),1)
print distribute((-pi,-pi/2,pi/2),2

答案 4 :(得分:1)

你从来没有说过精确测量“均匀间距”是什么。区间大小的完全均方根方差与完全间隔的区间大小或其他东西?

如果你在开始时查看任何特定的开放区间,我相信在该区间内放置 k 点的最佳解决方案将始终均匀地隔开它们。因此,问题减少到选择最小间隔大小的截止点,以获得一定数量的间隙点。完成后,如果你没有足够的分配点,从每个区间从最大到最小丢弃一个点并重复,直到你得到理智的东西。

但是,我不确定选择截止点的最佳方式。

答案 5 :(得分:0)

我建议您将问题视为:

  • 包裹线 - 允许您轻松确定点间距离,然后重新映射到圆圈

  • 考虑点与圆心之间的角度而不是弧距。同样,这简化了新点的定位,并且可能更容易重新映射到圆边点。找到已放置点之间的所有角度,然后将最大(或类似)平分,并将新点放在圆边上的适当点上。

答案 6 :(得分:0)

One idea, write angles as list (in degrees):
    [30, 80, 120, 260, 310]
Convert to differences:
    [ 50, 40, 140, 50, 80]
Note that we wrap around 310 + 80 (mod 360) = 30, the first angle
For each point to be added, split the largest difference:
n=1, split 140:
    [50, 40, 70, 70, 50, 80 ]
n=2, split 80:
    [50, 40, 70, 70, 50, 40, 40]
Convert back to angles:
    [30, 80, 120, 190, 260, 310, 350]

答案 7 :(得分:0)

我有一个名为“condition”的函数,它接受两个参数 - 分子(const)和分母(pass-by-ref)。它要么“增长”要么“缩小”分母的值,直到整数个“分母”适合分子,即分子/分母是整数。

分母是增长还是缩小取决于哪一个会导致分数变小。

将分子设置为2 * pi并将分母设置为接近所需间距的任何类型,并且您应该非常接近均匀分布。

请注意,我还有一个“比较”功能,它在一定容差范围内比较两个双打的相等性。

bool compare( const double num1, const double num2, const double epsilon = 0.0001 )
{
     return abs(num1 - num2) < epsilon;
}

然后条件函数是

void condition(const double numerator, double &denominator)
{
  double epsilon = 0.01;
  double mod = fmod( numerator, denominator );
  if( compare(mod, 0) )
    return;
  if( mod > denominator/2 ) // decide whether to grow or shrink
    epsilon *= -1;

  int count = 0;
  while( !compare( fmod( numerator, denominator ), 0, 0.1) )
  {
    denominator += epsilon;
    count++;
    if( count > 10000 ) // a safety net
      return;
  }
}

希望它有所帮助,我知道这个小算法对我来说非常方便了很多次。

答案 8 :(得分:0)

Starting with array  [30, 80, 120, 260, 310] and adding n = 5 angles,
the given algorithm (see below) gives [30, 55, 80, 120, 155, 190, 225, 260, 310, 350]
with a root mean square of the differences between angles
   rms(diff) = sqrt[sum(diff * diff)] / n = 11.5974135047,
which appears to be optimal for practical purposes.

相关问题