Estrutura de dados para o mapa em intervalos


11

Seja n um número inteiro e Z denote o conjunto de todos os números inteiros. Deixe [a,b] denotar o intervalo de números inteiros {a,a+1,a+2,,b} .

Eu estou procurando uma estrutura de dados para representar um mapa f:[1,n]Z . Quero que a estrutura de dados suporte as seguintes operações:

  • get(i) deve retornarf(i) .

  • set([a,b],y) deve atualizarf para quef(a)=f(a+1)==f(b)=y , ou seja, atualizef para um novo mapaf tal quef(i)=y parai[a,b] ef(i)=f(i) parai[a,b] .

  • stab(i) deve retornar o maior intervalo[a,b] tal quei[a,b] ef é constante em[a,b] (isto é,f(a)=f(a+1)==f(b) ).

  • add([a,b],δ) deve atualizarf para um novo mapaf modo quef(i)=f(i)+δ parai[a,b] ef(i)=f(i) parai[a,b] .

Eu quero que cada uma dessas operações seja eficiente. Eu consideraria o tempo O(1) ou O(lgn) tão eficiente, mas o tempo O(n) é muito lento. Tudo bem se os tempos de execução forem amortizados. Existe uma estrutura de dados que simultaneamente torna eficiente todas essas operações?

(Eu notei um padrão semelhante surgir em vários desafios de programação. Essa é uma generalização que seria suficiente para todos esses problemas de desafio.)


Eu acho que as árvores espalhadas são o ponto de partida. addseria linear no número de subintervalos de ; você pensou em uma árvore de espalhamento com nós " + δ " adicionais unários , compactados preguiçosamente? [a,b]+δ
Gilles 'SO- stop be evil'

Considere tal que f ( i ) f ( j ) para todos os i , j . Então você precisa ter n valores armazenados em algum lugar. A execução do conjunto ( [ a , b ] , y ) precisa se livrar desses valores de alguma forma (reescrevendo-os ou jogando-os fora - você pode adiar com o GC, mas precisará executar operações O ( n ) em algum momento) . Como tal, a operação será O ( n ) .ff(i)f(j)ijnset([a,b],y)O(n)O(n)
Avakar

@avakar, eu ficaria feliz com uma solução que trata o GC como efetivamente "gratuito". De maneira mais geral, eu ficaria feliz com uma solução em que os tempos de execução são amortizados (portanto, o custo do GC pode ser amortizado no custo de criação do valor em primeiro lugar).
DW

Você observou que o tempo constante e logarítmico é eficiente e o tempo linear é lento. Será tempo é muito lento para as suas necessidades? O(nlgn)
Jbapple #

@ jbapple, ei, é um começo! Eu acho que vale a pena documentar como resposta.
DW

Respostas:


4

Acredito que o tempo logarítmico para todas as consultas seja possível. A idéia principal é usar uma árvore de intervalo, em que cada nó na árvore corresponde a um intervalo de índices. Criarei as idéias principais começando com uma versão mais simples da estrutura de dados (que pode suportar get e set, mas não as outras operações), depois adicionarei recursos para dar suporte aos outros recursos.

Um esquema simples (suporta obter e definir, mas não adicionar ou facada)

Digamos que um intervalo seja plano se a função f for constante em [ a , b ] , ou seja, se f ( a ) = f ( a + 1 ) = = f ( b ) .[a,b]f[a,b]f(a)=f(a+1)==f(b)

Nossa estrutura de dados simples será uma árvore de intervalo. Em outras palavras, temos uma árvore binária, em que cada nó corresponde a um intervalo (de índices). Armazenaremos o intervalo correspondente em cada nó v da árvore. Cada folha corresponderá a um intervalo fixo, e elas serão organizadas de modo que a leitura das folhas da esquerda para a direita nos dê uma sequência de intervalos planos consecutivos que são disjuntos e cuja união é toda [ 1 , n ] . O intervalo para um nó interno será a união dos intervalos de seus dois filhos. Além disso, em cada nó folha armazenaremos o valor V ( )I(v)v[1,n]V()da função no intervalo I ( ) correspondente a este nó (observe que esse intervalo é plano, portanto f é constante no intervalo, portanto, apenas armazenamos um único valor de f em cada nó da folha).fI()ff

Equivalentemente, você pode imaginar que particionamos em intervalos simples e, em seguida, a estrutura de dados é uma árvore de pesquisa binária em que as chaves são os pontos finais esquerdos desses intervalos. As folhas contêm o valor de f em algum intervalo de índices em que f é constante.[1,n]ff

Use métodos padrão para garantir que a árvore binária permaneça equilibrada, ou seja, sua profundidade é (onde m conta o número atual de folhas na árvore). Obviamente, m n , então a profundidade é sempre no máximo O ( lg n ) . Isso será útil abaixo.O(lgm)mmnO(lgn)

Agora podemos oferecer suporte às operações get e set da seguinte maneira:

  • é fácil: atravessamos a árvore para encontrar a folha cujo intervalo contém i . Basicamente, isso é apenas atravessar uma árvore de pesquisa binária. Como a profundidade é O ( lg n ) , o tempo de execução é O ( lg n ) .get(i)iO(lgn)O(lgn)

  • é mais complicado. Funciona assim:set([a,b],y)

    1. Primeiro, encontramos o intervalo de folhas contendo a ; se a 0 < a , dividimos esse intervalo de folha nos dois intervalos [ a 0 , a - 1 ] e [ a , b 0 ] (transformando esse nó de folha em um nó interno e introduzindo dois filhos).[a0,b0]aa0<a[a0,a1][a,b0]

    2. A seguir, encontramos o intervalo foliar contendo b ; se b < b 1 , dividimos esse intervalo foliar nos dois intervalos [ a 1 , b ] e [ b + 1 , b 1 ] (transformando esse nó foliar em um nó interno e introduzindo dois filhos).[a1,b1]bb<b1[a1,b][b+1,b1]

    3. Neste ponto, afirmo que o intervalo pode ser expresso como a união separada de intervalos O ( lg n ) correspondentes a algum subconjunto de nós O ( lg n ) na árvore. Portanto, exclua todos os descendentes desses nós (transformando-os em folhas) e defina o valor armazenado nesses nós como y .[a,b]O(lgn)O(lgn)y

    4. Finalmente, como modificamos a forma da árvore, realizaremos as rotações necessárias para reequilibrar a árvore (usando qualquer técnica padrão para manter uma árvore equilibrada).

    Como esta operação envolve algumas operações simples nos nós (e esse conjunto de nós pode ser facilmente encontrado no tempo O ( lg n ) ), o tempo total para esta operação é O ( lg n ) .O(lgn)O(lgn)O(lgn)

Isso mostra que podemos suportar as operações get e set no tempo por operação. De fato, o tempo de execução pode ser mostrado como O ( lg min ( n , s ) ) , onde s é o número de operações definidas até o momento.O(lgn)O(lgmin(n,s))s

Adicionando suporte para adicionar

Podemos modificar a estrutura de dados acima para que também possa suportar a operação de adição. Em particular, em vez de armazenar o valor da função nas folhas, ela será representada como a soma dos números armazenados em um conjunto de nós.

Mais precisamente, o valor da função na entrada i será recuperável como a soma dos valores armazenados nos nós no caminho da raiz da árvore até a folha cujo intervalo contém i . Em cada nó v , armazenaremos um valor V ( v ) ; se v 0 , v 1 , , v k representam os ancestrais de uma folha v k (incluindo a própria folha), então o valor da função em I ( v k ) seráf(i)iivV(v)v0,v1,,vkvkI(vk) .V(v0)++V(vk)

É fácil oferecer suporte às operações get e set usando uma variante das técnicas descritas acima. Basicamente, à medida que percorremos a árvore para baixo, mantemos o controle da soma dos valores em execução, de modo que, para cada nó que a passagem percorre, conhecemos a soma dos valores dos nós no caminho da raiz para x . Depois que fizermos isso, ajustes simples na implementação de get e set descritos acima serão suficientes.xx

E agora podemos suportar eficientemente. Primeiro, expressamos o intervalo [ a , b ] como a união de intervalos O ( lg n ) correspondentes a algum conjunto de nós O ( lg n ) na árvore (dividindo um nó no ponto final esquerdo e no ponto final direito, se necessário), exatamente como foi feito nas etapas 1 a 3 da operação definida. Agora, simplesmente adicionamos δ ao valor armazenado em cada um desses O ( lg n )add([a,b],δ)[a,b]O(lgn)O(lgn)δO(lgn)nós. (Nós não excluímos seus descendentes.)

Isso fornece uma maneira de oferecer suporte a obter, definir e adicionar, em tempo por operação. De fato, o tempo de execução por operação é O ( lg min ( n , s ) ), em que s conta o número de operações definidas mais o número de operações adicionadas.O(lgn)O(lgmin(n,s))s

Suporte à operação facada

A consulta de punhalada é a mais desafiadora de oferecer suporte. A idéia básica será modificar a estrutura de dados acima para preservar os seguintes invariantes adicionais:

(*) O intervalo correspondente a cada folha é um intervalo plano máximo.I()

Aqui eu digo que um intervalo é um intervalo máximo plano se (i) [ a , b ] for plano e (ii) nenhum intervalo contendo [ a , b ] for plano (em outras palavras, para todos a , b satisfazendo 1 a a b b n , [ a , b ] = [[a,b][a,b][a,b]a,b1aabbn ou [ a , b ] não é plano).[a,b]=[a,b][a,b]

Isso facilita a implementação da operação facada:

  • encontra a folha cujo intervalo contém i e retorna esse intervalo.stab(i)i

No entanto, agora precisamos modificar as operações set e add para manter a invariante (*). Cada vez que dividimos uma folha em duas, podemos violar a invariante se algum par adjacente de intervalos de folhas tiver o mesmo valor da função . Felizmente, cada operação de configuração / adição adiciona no máximo 4 novos intervalos de folhas. Além disso, para cada novo intervalo, é fácil encontrar o intervalo foliar imediatamente à esquerda e à direita dele. Portanto, podemos dizer se a invariante foi violada; se fosse, então mesclamos intervalos adjacentes onde f tem o mesmo valor. Felizmente, a fusão de dois intervalos adjacentes não aciona alterações em cascata (portanto, não precisamos verificar se a fusão pode ter introduzido violações adicionais da invariante). Ao todo, isso envolve examinar 12ff pares de intervalos e possivelmente mesclando-os. Finalmente, como uma fusão altera a forma da árvore, se isso viola os invariantes da balança, execute as rotações necessárias para manter a árvore equilibrada (seguindo as técnicas padrão para manter as árvores binárias equilibradas). No total, isso adiciona no máximo O ( lg n ) trabalho adicional às operações de configuração / adição.12=O(1)O(lgn)

Portanto, essa estrutura final de dados suporta todas as quatro operações e o tempo de execução para cada operação é . Uma estimativa mais precisa é o tempo O ( lg min ( n , s ) ) por operação, em que s conta o número de operações de ajuste e adição.O(lgn)O(lgmin(n,s))s

Pensamentos de despedida

Ufa, esse era um esquema bastante complexo. Espero não ter cometido erros. Por favor, verifique meu trabalho com cuidado antes de confiar nesta solução.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.