Algoritmo para achatar faixas sobrepostas

16

Estou procurando uma boa maneira de achatar (dividir) uma lista de intervalos numéricos potencialmente sobrepostos. O problema é muito semelhante ao desta pergunta: A maneira mais rápida de dividir períodos de datas sobrepostos e muitos outros.

No entanto, os intervalos não são apenas números inteiros e estou procurando um algoritmo decente que possa ser facilmente implementado em Javascript ou Python, etc.

Dados de exemplo: Dados de exemplo

Solução de exemplo: insira a descrição da imagem aqui

Desculpas se for uma duplicata, mas ainda estou para encontrar uma solução.

Jollywatt
fonte
Como você determina que o verde está em cima do azul, mas está amarelo e laranja? As faixas de cores são aplicadas em ordem? Se for esse o caso, o algoritmo parece óbvio; apenas ... erm, aplique as faixas de cores em ordem.
Robert Harvey
1
Sim, eles são aplicados em ordem. Mas esse é o problema - como você 'aplicaria' os intervalos?
Jollywatt #
1
Você costuma adicionar / remover cores ou precisa otimizar a velocidade da consulta? Quantos "intervalos" você costuma ter? 3? 3000?
Telastyn
Não adicionamos / removemos cores com muita frequência e haverá algo entre 10 e 20 intervalos, com precisão de mais de 4 dígitos. É por isso que o método set não é muito adequado, porque os sets terão que ter mais de 1000 itens. O método que eu segui é o que eu postei no Python.
Jollywatt #

Respostas:

10

Caminhe da esquerda para a direita, usando uma pilha para acompanhar a cor em que está. Em vez de um mapa discreto, use os 10 números no seu conjunto de dados como pontos de interrupção.

Começando com uma pilha vazia e configurando startcomo 0, faça um loop até chegarmos ao fim:

  • Se a pilha estiver vazia:
    • Procure a primeira cor começando em ou depois starte empurre-a e todas as cores de classificação mais baixa na pilha. Na sua lista nivelada, marque o início dessa cor.
  • else (se não estiver vazio):
    • Encontre o próximo ponto de partida para qualquer cor de classificação mais alta em ou após starte encontre o final da cor atual
      • Se a próxima cor começar primeiro, empurre-a e qualquer outra coisa a caminho da pilha. Atualize o final da cor atual como o início desta e adicione o início dessa cor à lista nivelada.
      • Se não houver nenhuma, e a cor atual terminar primeiro, defina starto final dessa cor, retire-a da pilha e verifique a próxima cor com a classificação mais alta
        • Se startestiver dentro do intervalo da próxima cor, adicione-a à lista nivelada, começando em start.
        • Se a pilha esvaziar, continue o loop (volte para o primeiro marcador).

Este é um detalhamento mental, considerando seus dados de exemplo:

# 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
fonte
o que você quer dizer com "mais alguma coisa a caminho da pilha"?
Guillaume07
1
@ Guillaume07 Qualquer coisa entre a atual e a próxima partida escolhida. Os dados da amostra não mostram, mas imagine que o amarelo tenha sido deslocado para começar antes do verde - você deve inserir verde e amarelo na pilha para que, quando o amarelo terminar, o fim do verde ainda esteja no lugar certo na pilha para que ele ainda apareça no resultado final
Izkata 15/01/19
Outra coisa que eu não entendo, por favor, é o motivo pelo qual você diz primeiro "Se a pilha estiver vazia: procure a primeira cor iniciando no início ou antes do início" e, em seguida, no exemplo de código que você comentar "# Stack is empty. Procure o próximo ponto de partida em 0 ou posterior ". Assim, uma vez que é antes e uma vez que é mais tarde
Guillaume07
1
@ Guillaume07 Sim, um erro de digitação, a versão correta está no bloco de código duas vezes (o segundo é o comentário na parte inferior que inicia "A pilha está vazia."). Eu editei esse ponto de bala.
Izkata 16/01/19
3

Esta solução parece a mais simples. (Ou pelo menos, o mais fácil de entender)

Tudo o que é necessário é uma função para subtrair dois intervalos. Em outras palavras, algo que dará isso:

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

O que é bastante simples. Em seguida, você pode simplesmente percorrer cada um dos intervalos, começando pelo mais baixo e, para cada um, subtrair todos os intervalos acima, por sua vez. E aí está.


Aqui está uma implementação do subtrator de intervalo no 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 função, o resto pode ser feito da seguinte maneira: (Um 'span' significa um intervalo, pois 'range' é uma palavra-chave 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

Isso 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]]], o que está correto.

Jollywatt
fonte
Sua saída no final não corresponde à saída esperada na questão ...
Izkata 23/12/16
@ Izkata Puxa, eu fui descuidado. Essa deve ter sido a saída de outro teste. Corrigido agora, obrigado
Jollywatt
2

Se os dados realmente tiverem escopo semelhante aos dados de amostra, você poderá criar um mapa como este:

map = [0 .. 150]

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

Depois, basta percorrer este mapa para gerar os intervalos

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, os valores devem estar em um intervalo relativamente pequeno, como nos dados de amostra.

Editar: para trabalhar com carros alegóricos verdadeiros, use o mapa para gerar um mapeamento de alto nível e, em seguida, consulte os dados originais para criar os limites.

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
fonte
Esta é uma solução muito boa, eu já me deparei com isso antes. No entanto, eu estou procurando uma solução mais genérica que pode gerenciar os intervalos de flutuação arbitrária ... (este não seria o melhor para algo como 563,807-770,100)
Jollywatt
1
Eu acho que você poderia generalizá-lo, arredondando os valores e gerando o mapa, mas marcando um local nas bordas como tendo duas cores. Depois, quando vir um local com duas cores, volte aos dados originais para determinar o limite.
Gort the Robot
2

Aqui está uma solução relativamente simples no Scala. Não deve ser muito difícil portar para outro 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

applycaptura um Setde todos os intervalos já aplicados, localiza as sobreposições e retorna um novo conjunto menos as sobreposições e mais o novo intervalo e os novos intervalos divididos. foldLeftchama repetidamente applycom cada faixa de entrada.

Karl Bielefeldt
fonte
0

Basta manter um conjunto de intervalos classificados por início. Adicione um intervalo que cubra tudo (-oo .. + oo). Para adicionar um intervalo 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
fonte