Подтвердить что ты не робот

Распределять точки по кругу как можно равномерно

Описание проблемы

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

Цель

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

Ответ 1

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

Ответ 2

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

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

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

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

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

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

Ответ 3

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

Сначала мы переопределяем термин следующим образом: Найдите такое распределение 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

Ответ 5

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

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

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

Ответ 6

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

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

или

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

Ответ 7

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]

Ответ 8

У меня есть функция, называемая "условие", которая принимает два аргумента - числитель (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;
  }
}

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

Ответ 9

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.