Distribute points on a circle as evenly as possible

Problem statement

I have the following problem: I have a circle with a certain number (zero or more) of points on it. These positions are fixed. Now I have to position another set of points on the circle, such as all points together are as evenly distributed around the circle as possible.

Goal

My goal is now to develop a algorithm taking a list of angles (representing the fixed points) and an int value (representing how many additional points should be placed) and returning a list of angles again (containing only the angles where the additional points should lie).

The points dont have to be really evenly distributed (all same distance from each other), but rather as evenly as it is just possible. A perfect solution may not exist most of the time, as certain points are fixed.

The range of all angles lie in between -pi and +pi.

Examples

Some examples of what I am trying to archieve:

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]

or:

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

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

fill_circle(fixed_points, 6)

This last example should return something like: One point is to set right in between -pi*3/4 and -pi/4, that is: -pi/2 and distribute the other 5 points between -pi/4 and +pi (remember it is a circle, so in this case -pi = +pi):

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

Previous try

I started with a recursive algorithm that first searches for the largest interval between two points and sets the new point right in between. However it doesnt give satisfying results. Consider for example this configuration, with two points needed to be inserted:

 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

Not a very nice solution, as it could have been much better distributed (see above: -pi/6 and +pi/6).

Sorry for the long question, I hope you understand what I want to archieve.

I dont need a complete working algorithm, but rather the right idea for developing one. Maybe some pseudocode if you like. Would be very grateful for some hints to push me in the right direction. Thanks in advance!

Update: Completed algorithm:

Thank you all for your answers! It showed up I basically just needed a non-greedy version of my already existing algorithm. I really liked haydenmuhls idea to simplify the problem a little bit by encapsulating an interval/segment class:

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

This makes the actual algorithm incredibly easy and readable:

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
29
задан Community 23 May 2017 в 12:04
поделиться

9 ответов

Вы можете использовать объект 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, хотя вам не понадобятся геттеры, поскольку все в Python является общедоступным.

5
ответ дан 28 November 2019 в 02:06
поделиться

Вы никогда не говорили, что "насколько равномерно распределены" точно измеряется. Полное среднеквадратичное отклонение размера интервала от размеров идеально разнесенных интервалов или что-то еще?

Если вы посмотрите на любой конкретный открытый интервал в начале, я считаю оптимальным решением, которое помещает k точки в этом интервале всегда будут располагать их равномерно. Следовательно, проблема сводится к выбору точек отсечки для минимального размера интервала, чтобы получить определенное количество промежуточных точек. Когда закончите, если у вас недостаточно очков для распределения, отбросьте по одной точке с каждого интервала от наибольшего к наименьшему и повторяйте, пока не получите что-то разумное.

Я не уверен, как лучше выбрать отсечки.

1
ответ дан 28 November 2019 в 02:06
поделиться

Предположим, что интервалы между точками равны a_1 ... a_n. Затем, если мы разделим каждый сегмент на части минимального размера d, мы сможем уместить в сегменте floor (a_i / d) - 1 точек. Это означает, что сумма (пол (a / d) для a in interval_lengths) должна быть больше или равна n + s , где n - количество точек, которые мы хотим добавить, а s - количество уже имеющихся точек. Мы хотим выбрать d как можно больше, вероятно, лучше всего выполнить двоичный поиск лучшего d.

После того, как мы выбрали d, просто проходите через каждый сегмент, добавляя точки через каждые d градусов, пока в сегменте не останется менее 2 d градусов.

Править все, что вам нужно, это найти d такое, что sum (floor (a / d) для a in interval_lengths) == n + s , затем назначьте floor (a_i / d) - 1 сегменту i каждые a_i / (floor ( a_i / d) - 1) градусов. Бинарный поиск быстро это найдет.

Дальнейшее редактирование

Вот код для поиска 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
4
ответ дан 28 November 2019 в 02:06
поделиться

Сначала мы переопределяем термин следующим образом: Найдите такое распределение 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 for ls in s)> = N. Тогда для каждого отрезка s [i] вы можете просто равномерно разместить s [i] / L -1 точек. Считаю это оптимальным решением.

Обновлено Ошибка в мин (с)> 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
2
ответ дан 28 November 2019 в 02:06
поделиться
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]
0
ответ дан 28 November 2019 в 02:06
поделиться
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.

0
ответ дан 28 November 2019 в 02:06
поделиться

I propose that you consider the problem either as :

  • A wrapped line - allowing you to determine inter-point distance easily, then re-map to the circle

or

  • Consider the angles between points and the centre of the circle rather than arc distance. Again, this simplifies the positioning of new points, and is perhaps the easier candidate for re-mapping to circle-edge points. Find all angles between the already-placed points, then bisect the largest (or similar), and place the new point at the appropriate point on the circle edge.
0
ответ дан 28 November 2019 в 02:06
поделиться

Предположим, у вас уже есть M очков, и нужно добавить еще N . Если бы все точки были расположены равномерно, то между ними были бы промежутки 2 * pi / (N + M) . Итак, если вы разрежете свои точки M , чтобы получить сегменты угла M , вы, безусловно, можете поместить точки в сегмент (на равном расстоянии друг от друга), пока расстояние не станет меньше или равно 2 * пи / (N + M) .

Итак, если длина сегмента составляет L , тогда вы должны поместить этаж (L * (N + M) / (2 * pi)) - 1 точек в Это.

Теперь у вас останутся некоторые баллы. Ранжируйте сегменты по интервалу, который у вас был бы между точками, если бы была добавлена ​​еще одна точка. Фактически добавьте точку в сегмент с самым низким рейтингом. Снова вставьте его в отсортированный список и делайте это снова, пока у вас не закончатся баллы.

Поскольку каждый раз, когда вы помещаете точку в сегмент, результатом будут точки, расположенные как можно дальше друг от друга, а расстояние между точками не зависит от порядка, в котором вы их добавляли, вы получите оптимальный интервал.

(Правка: где «оптимальный» означает «максимальное минимальное расстояние между точками», то есть как можно лучше избегать наихудшего сценария, когда точки располагаются друг над другом.)

(Редактировать: Я надеюсь, что ясно, что идея состоит в том, чтобы решить, сколько точек входит в каждый сегмент, и затем только в самом конце, после того, как все числа будут определены, вы распределяете их поровну внутри каждого сегмента.)

10
ответ дан 28 November 2019 в 02:06
поделиться

У меня есть функция под названием «условие», которая принимает два аргумента - числитель (константа) и знаменатель (передача по ссылке). Он либо «увеличивает», либо «сжимает» значение знаменателя до тех пор, пока целое число «знаменателей» не поместится в числитель, то есть так, чтобы числитель / знаменатель был целым числом.

, увеличится или уменьшится знаменатель, зависит от того, какой из них приведет к его меньшему изменению.

Установите числитель на 2 * пи, а знаменатель на любое значение, близкое к желаемому интервалу, и у вас должно получиться довольно близкое к равномерному распределению.

Обратите внимание, что у меня также есть функция «сравнить», которая сравнивает два двойных значения на равенство в пределах определенного допуска.

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;
  }
}

Надеюсь, это поможет, я знаю, что этот маленький алгоритм мне очень пригодился несколько раз.

0
ответ дан 28 November 2019 в 02:06
поделиться
Другие вопросы по тегам:

Похожие вопросы: