Algoritmo para aplanar rangos superpuestos

16

Estoy buscando una buena manera de aplanar (dividir) una lista de rangos numéricos potencialmente superpuestos. El problema es muy similar al de esta pregunta: la forma más rápida de dividir rangos de fechas superpuestos , y muchos otros.

Sin embargo, los rangos no son solo enteros, y estoy buscando un algoritmo decente que se pueda implementar fácilmente en Javascript o Python, etc.

Datos de ejemplo: Datos de ejemplo

Solución de ejemplo: ingrese la descripción de la imagen aquí

Disculpas si esto es un duplicado, pero aún no he encontrado una solución.

Jollywatt
fuente
¿Cómo se determina que el verde está encima del azul, pero debajo del amarillo y el naranja? ¿Se aplican los rangos de color en orden? Si ese es el caso, el algoritmo parece obvio; solo ... erm, aplica los rangos de color en orden.
Robert Harvey
1
Sí, se aplican en orden. Pero ese es el problema: ¿cómo 'aplicarías' los rangos?
Jollywatt
1
¿A menudo agrega / elimina colores, o necesita optimizar la velocidad de consulta? ¿Cuántos "rangos" tendrá habitualmente? 3? 3000?
Telastyn
No agregará / eliminará colores con mucha frecuencia, y habrá entre 10 y 20 rangos, con una precisión de más de 4 dígitos. Es por eso que el método set no es muy adecuado, ya que los sets tendrán que tener más de 1000 elementos de largo. El método que utilicé es el que publiqué en Python.
Jollywatt

Respuestas:

10

Camina de izquierda a derecha, usando una pila para hacer un seguimiento del color en el que estás. En lugar de un mapa discreto, use los 10 números en su conjunto de datos como puntos de quiebre.

Comenzando con una pila vacía y estableciendo starta 0, repite hasta llegar al final:

  • Si la pila está vacía:
    • Busque el primer color que comience en o después start, y empújelo junto con todos los colores de menor rango en la pila. En su lista aplanada, marque el comienzo de ese color.
  • más (si no está vacío):
    • Encuentre el siguiente punto de inicio para cualquier color de mayor rango en o después start, y encuentre el final del color actual
      • Si el siguiente color comienza primero, empújalo y cualquier otra cosa en el camino hacia la pila. Actualice el final del color actual como el comienzo de este y agregue el inicio de este color a la lista aplanada.
      • Si no hay ninguno y el color actual termina primero, configúrelo startal final de este color, sáquelo de la pila y verifique el siguiente color mejor clasificado
        • Si startestá dentro del rango del siguiente color, agregue este color a la lista aplanada, comenzando en start.
        • Si la pila se vacía, simplemente continúa el ciclo (vuelve al primer punto).

Este es un repaso mental dados sus datos de ejemplo:

# 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
fuente
¿Qué quieres decir con "cualquier otra cosa en el camino hacia la pila"?
Guillaume07
1
@ Guillaume07 Cualquier rango entre el inicio actual y el próximo elegido. Los datos de la muestra no lo muestran, pero imagine que el amarillo se desplazó para comenzar antes que el verde: debe empujar tanto el verde como el amarillo en la pila para que cuando el amarillo termine, el final del verde todavía esté en el lugar correcto en la pila así que todavía aparece en el resultado final
Izkata
Otro pensamiento que no entiendo, por favor, es por qué dice primero "Si la pila está vacía: busque el primer color que comience en el inicio o antes", luego, en la muestra de código, comente "# La pila está vacía. Busque el siguiente punto de partida en 0 o posterior ". Entonces, una vez que es antes y una vez que es más tarde
Guillaume07
1
@ Guillaume07 Sí, un error tipográfico, la versión correcta está en el bloque de código dos veces (el segundo es el comentario cerca de la parte inferior que comienza "La pila está vacía"). He editado esa viñeta.
Izkata
3

Esta solución parece la más simple. (O al menos, el más fácil de entender)

Todo lo que se necesita es una función para restar dos rangos. En otras palabras, algo que dará esto:

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

Lo cual es bastante simple. Luego, puede simplemente recorrer cada uno de los rangos, comenzando por el más bajo, y para cada uno, restar de él todos los rangos por encima, a su vez. Y ahí lo tienes.


Aquí hay una implementación del sustractor de rango en 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 esta función, el resto se puede hacer así: (Un 'span' significa un rango, ya que 'range' es una palabra clave de 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

Esto da [['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]]], que es correcto.

Jollywatt
fuente
Su salida al final no coincide con la salida esperada en la pregunta ...
Izkata
@Izkata Gosh, fui descuidado. Esa debe haber sido la salida de otra prueba. Reparado ahora, gracias
Jollywatt
2

Si los datos realmente son similares en alcance a sus datos de muestra, podría crear un mapa como este:

map = [0 .. 150]

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

Luego simplemente recorra este mapa para generar los rangos

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

Para funcionar, los valores deben estar en un rango relativamente pequeño como en los datos de muestra.

Editar: para trabajar con flotadores verdaderos, use el mapa para generar un mapeo de alto nivel y luego consulte los datos originales para crear los límites.

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
Gort the Robot
fuente
Esta es una solución muy buena, la he encontrado antes. Sin embargo, estoy buscando una solución más genérica que pueda administrar cualquier rango flotante arbitrario ... (esto no sería lo mejor para algo como 563.807 - 770.100)
Jollywatt
1
Creo que podría generalizarlo redondeando los valores y generando el mapa, pero marcando una ubicación en los bordes con dos colores. Luego, cuando vea una ubicación con dos colores, vuelva a los datos originales para determinar el límite.
Gort the Robot
2

Aquí hay una solución relativamente sencilla en Scala. No debería ser demasiado difícil portar a otro idioma.

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

applytoma uno Setde todos los rangos que ya se han aplicado, encuentra las superposiciones, luego devuelve un nuevo conjunto menos las superposiciones y más el nuevo rango y los rangos recién divididos. foldLeftLlama repetidamente applycon cada rango de entrada.

Karl Bielefeldt
fuente
0

Simplemente mantenga un conjunto de rangos ordenados por inicio. Añadir rango que cubre todo (-oo .. + oo). Para agregar un rango 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
Kevin Cline
fuente