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


16

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

Однак діапазони - це не лише цілі числа, і я шукаю гідний алгоритм, який можна легко реалізувати в Javascript або Python тощо.

Приклад даних: Приклад даних

Приклад рішення: введіть тут опис зображення

Вибачте, якщо це дублікат, але я ще мушу знайти рішення.


Як визначити, що зелений колір знаходиться вгорі синього, але під жовтим та помаранчевим? Чи застосовуються кольорові діапазони в порядку? Якщо це так, алгоритм видається очевидним; просто ... Ем, застосуйте кольорові діапазони в порядку.
Роберт Харві

1
Так, вони застосовуються по порядку. Але це проблема - як би ви «застосували» діапазони?
Джолліватт

1
Чи часто ви додаєте / видаляєте кольори, чи вам потрібно оптимізувати швидкість запиту? Скільки «діапазонів» у вас зазвичай буде? 3? 3000?
Теластин

Не буде додавати / видаляти кольори дуже часто, і вони будуть десь між 10-20 діапазонами, з точністю до 4 знаків. Ось чому метод встановлення не дуже підходить, тому що набори повинні мати довжину 1000+ елементів. Метод, з яким я пішов, - це той, який я розмістив у Python.
Джолліватт

Відповіді:


10

Пройдіться зліва направо, використовуючи стек, щоб відстежувати, який колір ви маєте. Замість дискретної карти використовуйте 10 чисел у вашому наборі даних як точки перерви.

Починаючи з порожнього стека і встановлюючи start0, циклічно, поки не досягнемо кінця:

  • Якщо стек порожній:
    • Шукайте перший колір, починаючи з або після 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.

що ви маєте на увазі під «чим-небудь іншим на шляху до цього стека»?
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

За допомогою цієї функції решта можна зробити так: ("Проміжок" означає діапазон, оскільки "діапазон" - це ключове слово 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]]], що правильно.


Ваш результат у кінці не відповідає очікуваному результату у питанні ...
Ізката

@Izkata Gosh, я був недбалий. Це, мабуть, було результатом іншого тесту. Виправлено зараз, спасибі
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
Я думаю, ви могли б узагальнити це шляхом округлення значень та генерування карти, але позначивши розташування на краях як два кольори. Потім, побачивши місце з двома кольорами, поверніться до початкових даних, щоб визначити межу.
Гурт Робота

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приймає a 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.