Распределите точки по кругу как можно более равномерно - PullRequest
29 голосов
/ 13 августа 2010

Постановка задачи

У меня следующая проблема: у меня есть круг с определенным числом (ноль или более) точек на нем.Эти позиции фиксированы.Теперь мне нужно расположить другой набор точек на окружности, например, чтобы все точки были равномерно распределены по кругу.

Цель

Моя цель сейчас состоит в том, чтобы разработать алгоритм,список углов (представляющих фиксированные точки) и значение 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 и распределить остальные 5 точек между -pi / 4 и + 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 ]

10 голосов
/ 13 августа 2010

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

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

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

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

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

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

5 голосов
/ 14 августа 2010

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

4 голосов
/ 13 августа 2010

Предположим, что интервалы между точками 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 d градусов в сегменте

Редактировать Все, что вам нужно, это найти d такой, что sum(floor(a/d) for 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
2 голосов
/ 13 августа 2010

Сначала мы переопределим термин следующим образом: Найдите такое распределение 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) и проверка условия, если сумма (ls / L -1 для ls в s)> = 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
1 голос
/ 13 августа 2010

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

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

0 голосов
/ 14 августа 2010
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 голосов
/ 13 августа 2010

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

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

Установите числитель на 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 голосов
/ 13 августа 2010
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 голосов
/ 13 августа 2010

Я предлагаю вам рассмотреть проблему либо как:

  • Обернутая линия - позволяющая легко определить расстояние между точками, а затем переназначить карту на круг

или

  • Рассмотрим углы между точками и центром окружности, а не расстояние до дуги.Опять же, это упрощает позиционирование новых точек и, возможно, является более подходящим кандидатом для повторного сопоставления с точками окружности.Найдите все углы между уже размещенными точками, затем разделите пополам наибольшую (или подобную) и поместите новую точку в соответствующую точку на краю круга.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...