Considere árvores binárias enraizadas e sem rótulo. Nós podemos comprimir essas árvores: sempre que houver indicações de sub-árvores e com (interpretação a igualdade estrutural), armazenamos (WLOG) e substituir todos os ponteiros para com ponteiros para . Veja a resposta de uli para um exemplo.T ′ T = T ′ = T T ′ T
Forneça um algoritmo que use uma árvore no sentido acima como entrada e calcule o número (mínimo) de nós que permanecem após a compactação. O algoritmo deve ser executado no tempo (no modelo de custo uniforme) com o número de nós na entrada.n
Essa foi uma pergunta do exame e não consegui encontrar uma boa solução, nem a vi.
Respostas:
Sim, você pode executar essa compactação em , mas não é fácil :) Primeiro fazemos algumas observações e depois apresentamos o algoritmo. Assumimos que a árvore inicialmente não seja compactada - isso não é realmente necessário, mas facilita a análise.O ( n logn )
Primeiramente, caracterizamos indutivamente a "igualdade estrutural". Seja e T ′ duas (sub) árvores. Se T e T ' são ambas as árvores nulas (sem vértices), elas são estruturalmente equivalentes. Se T e T ' não são árvores nulas, elas são estruturalmente equivalentes se seus filhos esquerdos forem estruturalmente equivalentes e seus filhos direitos forem estruturalmente equivalentes. 'Equivalência estrutural' é o ponto fixo mínimo sobre essas definições.T T′ T T′ T T′
Por exemplo, quaisquer dois nós de folha são estruturalmente equivalentes, pois ambos têm as árvores nulas como seus filhos, que são estruturalmente equivalentes.
Como é bastante irritante dizer 'seus filhos esquerdos são estruturalmente equivalentes e também os filhos certos', freqüentemente diremos 'seus filhos são estruturalmente equivalentes' e pretendem o mesmo. Observe também que às vezes dizemos 'este vértice' quando queremos dizer 'a subárvore enraizada nesse vértice'.
A definição acima imediatamente nos dá uma dica de como realizar a compactação: se conhecermos a equivalência estrutural de todas as subárvores com profundidade no máximo , então podemos calcular facilmente a equivalência estrutural das subárvores com profundidade d + 1 . Precisamos fazer esse cálculo de maneira inteligente para evitar um tempo de execução de O ( n 2 ) .d d+ 1 O ( n2)
O algoritmo atribuirá identificadores a todos os vértices durante sua execução. Um identificador é um número no conjunto . Os identificadores são únicos e nunca mudam: portanto, assumimos que definimos alguma variável (global) como 1 no início do algoritmo e, toda vez que atribuímos um identificador a algum vértice, atribuímos o valor atual dessa variável ao vértice e incrementamos o valor dessa variável.{ 1 , 2 , 3 , … , n }
Primeiro, transformamos a árvore de entrada em (no máximo ) listas contendo vértices de igual profundidade, juntamente com um ponteiro para o pai. Isso é feito facilmente em O ( n ) tempo.n O ( n )
Primeiro compactamos todas as folhas (podemos encontrá-las na lista com vértices de profundidade 0) em um único vértice. Atribuímos a esse vértice um identificador. A compactação de dois vértices é feita redirecionando o pai de um dos vértices para apontar para o outro vértice.
Fazemos duas observações: primeiro, qualquer vértice tem filhos de profundidade estritamente menor e, segundo, se tivermos realizado compressão em todos os vértices de profundidade menores que (e os tivermos identificado), então dois vértices de profundidade d são estruturalmente equivalentes e podem ser compactados se os identificadores de seus filhos coincidirem. Essa última observação segue o seguinte argumento: dois vértices são estruturalmente equivalentes se seus filhos são estruturalmente equivalentes e, após a compressão, isso significa que seus ponteiros estão apontando para os mesmos filhos, o que, por sua vez, significa que os identificadores de seus filhos são iguais.d d
Repetimos todas as listas com nós de igual profundidade, de pequena profundidade a grande profundidade. Para cada nível, criamos uma lista de pares inteiros, onde cada par corresponde aos identificadores dos filhos de algum vértice nesse nível. Temos que dois vértices nesse nível são estruturalmente equivalentes se seus pares inteiros correspondentes forem iguais. Usando a ordenação lexicográfica, podemos classificá-los e obter os conjuntos de pares inteiros iguais. Comprimimos esses conjuntos em vértices únicos, como acima, e os identificamos.
As observações acima provam que essa abordagem funciona e resulta na árvore compactada. O tempo total de execução é mais o tempo necessário para classificar as listas que criamos. Como o número total de pares inteiros que criamos é n , isso nos dá que o tempo total de execução é O ( n log n ) , conforme necessário. Contar quantos nós ainda restam no final do procedimento é trivial (basta ver quantos identificadores entregamos).O ( n ) n O ( n logn )
fonte
degree
grau estritamente menor ” provavelmente deveria serdepth
? E, apesar das árvores CS crescerem para baixo, acho "altura de uma árvore" menos confusa do que "profundidade de uma árvore".A compactação de uma estrutura de dados não mutável para que não duplique nenhum subtermo estruturalmente igual é conhecida como hash consing . Essa é uma técnica importante no gerenciamento de memória na programação funcional. Hash consing é uma espécie de memorização sistemática para estruturas de dados.
Vamos hash-contras na árvore e contamos os nós após a hash. O hash que consiste em uma estrutura de dados do tamanho sempre pode ser feito em O ( nn operações; contar o número de nós no final é linear no número de nós.O ( nl g (n))
Considerarei as árvores como tendo a seguinte estrutura (escrita aqui na sintaxe Haskell):
Para cada construtor, precisamos manter um mapeamento de seus possíveis argumentos para o resultado da aplicação do construtor a esses argumentos. As folhas são triviais. Para os nós, que mantêm um mapa parcial finito , onde T é o conjunto de identificadores de árvores e N é o conjunto de identificadores de nós; T = N ⊎ { ℓ } onde ℓ é o identificador da única folha. (Em termos concretos, um identificador é um ponteiro para um bloco de memória.)n o d e s :t× T→ N T N T= N⊎ { ℓ } ℓ
Podemos usar uma estrutura de dados em tempo logarítmico
nodes
, como uma árvore de pesquisa binária equilibrada. Abaixo, chamareilookup nodes
a operação que procura uma chave nanodes
estrutura de dados einsert nodes
a operação que adiciona um valor sob uma chave nova e retorna essa chave.Agora, atravessamos a árvore e adicionamos os nós à medida que avançamos. Embora eu esteja escrevendo no pseudocódigo do tipo Haskell, tratarei
nodes
como uma variável mutável global; apenas adicionaremos a ele, mas as inserções precisam ser encadeadas. Aadd
função se repete em uma árvore, adicionando suas subárvores aonodes
mapa e retorna o identificador da raiz.O número de
insert
chamadas, que também é o tamanho final danodes
estrutura de dados, é o número de nós após a compactação máxima. (Adicione um para a árvore vazia, se necessário.)fonte
nodes
insert
eadd
deve ser explicitada e deve ser dada uma função que realmente resolva o problema.nodes
uma variável mutável por conveniência, mas você pode encadear isso por toda parte. Eu não vou dar o código completo, isso não é SO.Aqui está outra idéia que visa (injetivamente) codificar a estrutura das árvores em números, em vez de apenas rotulá-las arbitrariamente. Para isso, usamos que a fatoração principal de qualquer número é única.
Essa última suposição é um exagero em máquinas reais; neste caso, alguém prefere usar algo semelhante à função de emparelhamento do Cantor em vez da exponenciação.
fonte
Como as imagens não são permitidas nos comentários:
canto superior esquerdo: uma árvore de entrada
canto superior direito: as subárvores enraizadas nos nós 5 e 7 também são isomórficas.
inferior esquerdo e direito: as árvores compactadas não são definidas exclusivamente.
fonte
Edit: Eu li a pergunta como T e T 'eram filhos do mesmo pai. Considerei a definição de compactação também recursiva, o que significa que você pode compactar duas subárvores compactadas anteriormente. Se essa não é a pergunta real, minha resposta pode não funcionar.
Onde
hasSameStructure()
é uma função que compara duas subárvores já compactadas em tempo linear para ver se elas têm exatamente a mesma estrutura. Escrever uma função recursiva de tempo linear que atravessa cada uma e verifica se uma subárvore tem um filho esquerdo toda vez que a outra faz etc. não deve ser difícil.fonte