Алгоритм выравнивания диапазонов перекрытия

16

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

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

Пример данных: Пример данных

Пример решения: введите описание изображения здесь

Извините, если это дубликат, но я еще не нашел решение.

Jollywatt
источник
Как вы определяете, что зеленый сверху синего, но под желтым и оранжевым? Цветовые диапазоны применяются в порядке? Если это так, алгоритм кажется очевидным; просто ... примените цветовые диапазоны по порядку.
Роберт Харви
1
Да, они применяются по порядку. Но это проблема - как бы вы «применили» диапазоны?
Jollywatt
1
Вы часто добавляете / удаляете цвета или вам нужно оптимизировать скорость запросов? Сколько «диапазонов» у вас обычно будет? 3? 3000?
Теластин
Не будет добавлять / удалять цвета очень часто, и будет где-то между 10-20 диапазонами, с точностью 4+ цифры. Вот почему метод набора не совсем подходит, потому что наборы должны быть длиной более 1000 элементов. Метод, который я использовал, - тот, который я опубликовал в Python.
Jollywatt

Ответы:

10

Идите слева направо, используя стопку, чтобы отследить, какого цвета вы находитесь. Вместо дискретной карты используйте 10 чисел в вашем наборе данных в качестве точек останова.

Начиная с пустого стека и устанавливая startв 0, цикл до конца:

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

Это мысленный прогон данных вашего примера:

# Initial data.
flattened = []
stack = []
start = 0
# Stack is empty.  Look for the next starting point at 0 or later: "b", 0 - Push it and all lower levels onto stack
flattened = [ (b, 0, ?) ]
stack = [ r, b ]
start = 0
# End of "b" is 5.4, next higher-colored start is "g" at 2 - Delimit and continue
flattened = [ (b, 0, 2), (g, 2, ?) ]
stack = [ r, b, g ]
start = 2
# End of "g" is 12, next higher-colored start is "y" at 3.5 - Delimit and continue
flattened = [ (b, 0, 2), (g, 2, 3.5), (y, 3.5, ?) ]
stack = [ r, b, g, y ]
start = 3.5
# End of "y" is 6.7, next higher-colored start is "o" at 6.7 - Delimit and continue
flattened = [ (b, 0, 2), (g, 2, 3.5), (y, 3.5, 6.7), (o, 6.7, ?) ]
stack = [ r, b, g, y, o ]
start = 6.7
# End of "o" is 10, and there is nothing starting at 12 or later in a higher color.  Next off stack, "y", has already ended.  Next off stack, "g", has not ended.  Delimit and continue.
flattened = [ (b, 0, 2), (g, 2, 3.5), (y, 3.5, 6.7), (o, 6.7, 10), (g, 10, ?) ]
stack = [ r, b, g ]
start = 10
# End of "g" is 12, there is nothing starting at 12 or later in a higher color.  Next off stack, "b", is out of range (already ended).  Next off stack, "r", is out of range (not started).  Mark end of current color:
flattened = [ (b, 0, 2), (g, 2, 3.5), (y, 3.5, 6.7), (o, 6.7, 10), (g, 10, 12) ]
stack = []
start = 12
# Stack is empty.  Look for the next starting point at 12 or later: "r", 12.5 - Push onto stack
flattened = [ (b, 0, 2), (g, 2, 3.5), (y, 3.5, 6.7), (o, 6.7, 10), (g, 10, 12), (r, 12.5, ?) ]
stack = [ r ]
start = 12
# End of "r" is 13.8, and there is nothing starting at 12 or higher in a higher color.  Mark end and pop off stack.
flattened = [ (b, 0, 2), (g, 2, 3.5), (y, 3.5, 6.7), (o, 6.7, 10), (g, 10, 12), (r, 12.5, 13.8) ]
stack = []
start = 13.8
# Stack is empty and nothing is past 13.8 - We're done.
Izkata
источник
что вы подразумеваете под "что-нибудь еще на пути к нему в стек"?
Guillaume07
1
@ Guillaume07 Все звенья между текущим и выбранным следующим началом. Образцы данных не показывают этого, но представьте, что желтый сместился, чтобы начать раньше зеленого - вы должны поместить зеленый и желтый в стек так, чтобы, когда желтый кончался, конец зеленого цвета все еще находился в нужном месте в стеке. так что это все еще появляется в конечном результате
Изката
Еще одна мысль, которую я не понимаю, пожалуйста, вот почему вы сначала говорите: «Если стек пуст: ищите первый цвет, начинающийся с или до начала», затем в примере кода, который вы комментируете, «Стек пуст. Посмотрите на следующий начальная точка на 0 или позже ". Так что когда-то это раньше, а когда-то позже
Guillaume07
1
@ Guillaume07 Да, опечатка, правильная версия находится в блоке кода дважды (второй - комментарий внизу, который начинается с «Стек пуст.»). Я редактировал этот пункт.
Изката
3

Это решение кажется самым простым. (Или, по крайней мере, легче всего понять)

Все, что нужно, это функция для вычитания двух диапазонов. Другими словами, кое-что, что даст это:

A ------               A     ------           A    ----
B    -------    and    B ------        and    B ---------
=       ----           = ----                 = ---    --

Что достаточно просто. Затем вы можете просто перебирать каждый из диапазонов, начиная с самого низкого, и для каждого вычитать из него все диапазоны по очереди, по очереди. И там у вас есть это.


Вот реализация вычитателя диапазона в Python:

def subtractRanges((As, Ae), (Bs, Be)):
    '''SUBTRACTS A FROM B'''
    # e.g, A =    ------
    #      B =  -----------
    # result =  --      ---
    # Returns list of new range(s)

    if As > Be or Bs > Ae: # All of B visible
        return [[Bs, Be]]
    result = []
    if As > Bs: # Beginning of B visible
        result.append([Bs, As])
    if Ae < Be: # End of B visible
        result.append([Ae, Be])
    return result

Используя эту функцию, все остальное можно сделать так: («span» означает диапазон, поскольку «range» - ключевое слово Python)

spans = [["red", [12.5, 13.8]],
["blue", [0.0, 5.4]],
["green", [2.0, 12.0]],
["yellow", [3.5, 6.7]],
["orange", [6.7, 10.0]]]

i = 0 # Start at lowest span
while i < len(spans):
    for superior in spans[i+1:]: # Iterate through all spans above
        result = subtractRanges(superior[1], spans[i][1])
        if not result:      # If span is completely covered
            del spans[i]    # Remove it from list
            i -= 1          # Compensate for list shifting
            break           # Skip to next span
        else:   # If there is at least one resulting span
            spans[i][1] = result[0]
            if len(result) > 1: # If there are two resulting spans
                # Insert another span with the same name
                spans.insert(i+1, [spans[i][0], result[1]])
    i += 1

print spans

Это дает [['red', [12.5, 13.8]], ['blue', [0.0, 2.0]], ['green', [2.0, 3.5]], ['green', [10.0, 12.0]], ['yellow', [3.5, 6.7]], ['orange', [6.7, 10.0]]], что правильно.

Jollywatt
источник
Ваш результат в конце не соответствует ожидаемому результату в вопросе ...
Изката
@Izkata Гоша, я был неосторожен. Это должно было быть результатом другого теста. Исправлено сейчас, спасибо
Jollywatt
2

Если данные действительно похожи по объему на ваши примеры данных, вы можете создать карту следующим образом:

map = [0 .. 150]

for each color:
    for loc range start * 10 to range finish * 10:
        map[loc] = color

Затем просто пройдите по этой карте, чтобы сгенерировать диапазоны

curcolor = none
for loc in map:
    if map[loc] != curcolor:
        if curcolor:
            rangeend = loc / 10
        make new range
        rangecolor = map[loc]
        rangestart = loc / 10

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

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

map = [0 .. 15]

for each color:
   for loc round(range start) to round(range finish):
        map[loc] = color

curcolor = none
for loc in map
    if map[loc] != curcolor:

        make new range
        if loc = round(range[map[loc]].start)  
             rangestart = range[map[loc]].start
        else
             rangestart = previous rangeend
        rangecolor = map[loc]
        if curcolor:
             if map[loc] == none:
                 last rangeend = range[map[loc]].end
             else
                 last rangeend = rangestart
        curcolor = rangecolor
Горт Робот
источник
Это очень хорошее решение, я сталкивался с ним раньше. Тем не менее, я ищу более общее решение, которое может управлять любыми произвольными диапазонами с плавающей запятой ... (это не было бы лучше для чего-то вроде 563.807 - 770.100)
Jollywatt
1
Я думаю, что вы могли бы обобщить это, округлив значения и сгенерировав карту, но пометив местоположение по краям как имеющее два цвета. Затем, когда вы видите местоположение с двумя цветами, вернитесь к исходным данным, чтобы определить границу.
Gort the Robot
2

Вот относительно простое решение в Scala. Не должно быть слишком сложно портировать на другой язык.

case class Range(name: String, left: Double, right: Double) {
  def overlapsLeft(other: Range) =
    other.left < left && left < other.right

  def overlapsRight(other: Range) =
    other.left < right && right < other.right

  def overlapsCompletely(other: Range) =
    left <= other.left && right >= other.right

  def splitLeft(other: Range) = 
    Range(other.name, other.left, left)

  def splitRight(other: Range) = 
    Range(other.name, right, other.right)
}

def apply(ranges: Set[Range], newRange: Range) = {
  val left     = ranges.filter(newRange.overlapsLeft)
  val right    = ranges.filter(newRange.overlapsRight)
  val overlaps = ranges.filter(newRange.overlapsCompletely)

  val leftSplit  =  left.map(newRange.splitLeft)
  val rightSplit = right.map(newRange.splitRight)

  ranges -- left -- right -- overlaps ++ leftSplit ++ rightSplit + newRange
}

val ranges = Vector(
  Range("red",   12.5, 13.8),
  Range("blue",   0.0,  5.4),
  Range("green",  2.0, 12.0),
  Range("yellow", 3.5,  6.7),
  Range("orange", 6.7, 10.0))

val flattened = ranges.foldLeft(Set.empty[Range])(apply)
val sorted = flattened.toSeq.sortBy(_.left)
sorted foreach println

applyберет Setиз всех примененных диапазонов, находит перекрытия, затем возвращает новый набор минус перекрытия и плюс новый диапазон и новые разделенные диапазоны. foldLeftнеоднократно звонит applyс каждым входным диапазоном.

Карл Билефельдт
источник
0

Просто держите набор диапазонов, отсортированных по началу. Добавьте диапазон, который охватывает все (-oo .. + oo). Чтобы добавить диапазон r:

let pre = last range that starts before r starts

let post = earliest range that starts before r ends

now iterate from pre to post: split ranges that overlap, remove ranges that are covered, then add r
Кевин Клайн
источник