Algoritmo per l'appiattimento di intervalli sovrapposti


16

Sto cercando un buon modo per appiattire (dividere) un elenco di intervalli numerici potenzialmente sovrapposti. Il problema è molto simile a quello di questa domanda: il modo più veloce per dividere gli intervalli di date sovrapposti e molti altri.

Tuttavia, gli intervalli non sono solo numeri interi e sto cercando un algoritmo decente che possa essere facilmente implementato in Javascript o Python, ecc.

Dati di esempio: Dati di esempio

Esempio Soluzione: inserisci qui la descrizione dell'immagine

Mi scuso se questo è un duplicato, ma devo ancora trovare una soluzione.


Come si determina che il verde si trova sopra il blu, ma sotto il giallo e l'arancione? Le gamme di colori sono applicate in ordine? In tal caso, l'algoritmo sembra ovvio; solo ... erm, applica le gamme di colori in ordine.
Robert Harvey,

1
Sì, vengono applicati in ordine. Ma questo è il problema: come "applicheresti" le gamme?
Jollywatt,

1
Spesso aggiungi / rimuovi colori o devi ottimizzare la velocità delle query? Quante "gamme" avrai di solito? 3? 3000?
Telastyn,

Non aggiungerà / rimuoverà i colori molto frequentemente e ci sarà ovunque tra 10-20 intervalli, con una precisione di 4+ cifre. Ecco perché il metodo set non è del tutto adatto, perché i set dovranno essere lunghi oltre 1000 articoli. Il metodo con cui ho seguito è quello che ho pubblicato in Python.
Jollywatt,

Risposte:


10

Cammina da sinistra a destra, usando una pila per tenere traccia di che colore sei. Invece di una mappa discreta, utilizzare i 10 numeri nel set di dati come punti di interruzione.

Iniziando con uno stack vuoto e impostando startsu 0, esegui il loop fino a raggiungere la fine:

  • Se lo stack è vuoto:
    • Cerca il primo colore che inizia da o dopo start, quindi spingilo e tutti i colori di livello inferiore nella pila. Nel tuo elenco appiattito, segna l'inizio di quel colore.
  • altro (se non vuoto):
    • Trova il punto di partenza successivo per qualsiasi colore di livello superiore ao dopo start e trova la fine del colore corrente
      • Se il colore successivo inizia per primo, spingilo e qualsiasi altra cosa sulla strada verso lo stack. Aggiorna la fine del colore corrente come l'inizio di questo e aggiungi l'inizio di questo colore all'elenco appiattito.
      • Se non ce ne sono e il colore corrente termina per primo, impostalo startalla fine di questo colore, estrailo dalla pila e controlla il colore con il punteggio più alto successivo
        • Se startrientra nell'intervallo del colore successivo, aggiungi questo colore all'elenco appiattito, iniziando da start.
        • Se lo stack si svuota, continua il ciclo (torna al primo punto elenco).

Questo è un run-through mentale dati i tuoi dati di esempio:

# 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.

cosa intendi con "qualcos'altro sulla strada verso lo stack"?
Guillaume07,

1
@ Guillaume07 Qualsiasi cosa tra i ranghi tra il prossimo inizio corrente e quello scelto. I dati di esempio non lo mostrano, ma immagina che il giallo sia stato spostato per iniziare prima del verde: devi spingere sia il verde che il giallo sulla pila in modo che quando il giallo finisce, la fine del verde è ancora nel posto giusto nella pila quindi appare ancora nel risultato finale
Izkata,

Un'altra cosa che non capisco, per favore, è il motivo per cui prima dici "Se lo stack è vuoto: cerca il primo colore che inizia all'inizio o prima dell'inizio", quindi nell'esempio di codice che commenti "# Lo stack è vuoto. Cerca il prossimo punto iniziale a 0 o successivo ". Quindi una volta è prima e una volta dopo
Guillaume07,

1
@ Guillaume07 Sì, un errore di battitura, la versione corretta è nel blocco di codice due volte (il secondo è il commento nella parte inferiore che inizia "Stack is empty"). Ho modificato quel punto elenco.
Izkata,

3

Questa soluzione sembra la più semplice. (O almeno, il più facile da capire)

Tutto ciò che serve è una funzione per sottrarre due intervalli. In altre parole, qualcosa che darà questo:

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

Il che è abbastanza semplice. Quindi puoi semplicemente scorrere attraverso ciascuno degli intervalli, a partire dal più basso, e per ciascuno, sottrarre a sua volta tutti gli intervalli sopra di esso, a sua volta. E il gioco è fatto.


Ecco un'implementazione del sottrattore di gamma in 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

Usando questa funzione, il resto può essere fatto in questo modo: (Un 'intervallo' significa un intervallo, poiché 'intervallo' è una parola chiave 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

Questo dà [['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]]], che è corretto.


L'output alla fine non corrisponde all'output previsto nella domanda ...
Izkata,

@Izkata Accidenti, ero incurante. Deve essere stato l'output di un altro test. Risolto ora, grazie
Jollywatt il

2

Se i dati sono davvero simili nell'ambito dei dati di esempio, è possibile creare una mappa come questa:

map = [0 .. 150]

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

Quindi cammina attraverso questa mappa per generare gli intervalli

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

Per funzionare, i valori devono essere in un intervallo relativamente piccolo come nei dati di esempio.

Modifica: per lavorare con float veri, usa la mappa per generare una mappatura di alto livello e quindi fai riferimento ai dati originali per creare i confini.

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

Questa è una soluzione molto bella, l'ho già trovata prima. Tuttavia, sto cercando una soluzione più generica in grado di gestire qualsiasi intervallo di float arbitrario ... (questo non sarebbe il migliore per qualcosa come 563.807 - 770.100)
Jollywatt

1
Penso che potresti generalizzarlo arrotondando i valori e generando la mappa, ma contrassegnando una posizione sui bordi con due colori. Quindi quando vedi una posizione con due colori, torna ai dati originali per determinare il confine.
Gort il robot

2

Ecco una soluzione relativamente semplice in Scala. Non dovrebbe essere troppo difficile port in un'altra lingua.

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

applyinclude uno Setdi tutti gli intervalli che sono già stati applicati, trova le sovrapposizioni, quindi restituisce un nuovo set meno le sovrapposizioni e più il nuovo intervallo e gli intervalli appena divisi. foldLeftchiama ripetutamente applycon ciascun intervallo di input.


0

Tieni semplicemente un set di intervalli ordinati per inizio. Aggiungi una gamma che copre tutto (-oo .. + oo). Per aggiungere un intervallo 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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.