Estou procurando o algoritmo mais eficiente para pegar uma árvore (armazenada como uma lista de arestas; OU como uma lista de mapeamentos do nó pai para uma lista de nós filhos); e produza, para TODOS os nós, uma lista de todos os nós dele descendentes (nível de folha e nível não-folha).
A implementação deve ser via loops, em vez de recusão, devido à escala; e idealmente deve ser O (N).
Essa pergunta do SO cobre uma solução padrão razoavelmente óbvia para encontrar a resposta para UM nó em uma árvore. Mas, obviamente, repetir esse algoritmo em todos os nós das árvores é altamente ineficiente (em primeiro lugar, O (NlogN) a O (N ^ 2)).
A raiz da árvore é conhecida. A árvore tem uma forma absolutamente arbitrária (por exemplo, não é N-nária, não é equilibrada de forma alguma, forma ou forma, não possui profundidade uniforme) - alguns nós têm 1-2 filhos, outros 30K filhos.
Em um nível prático (embora não deva afetar o algoritmo), a árvore possui ~ 100K-200K nós.
fonte
Respostas:
Se você realmente deseja PRODUZIR todas as listas como cópias diferentes, na pior das hipóteses, poderá obter melhor que n ^ 2 de espaço. Se você só precisa de ACESSO para cada lista:
Eu executaria uma travessia em ordem da árvore a partir da raiz:
http://en.wikipedia.org/wiki/Tree_traversal
Em seguida, para cada nó na árvore, armazene o número mínimo de pedido e o número máximo de pedidos em sua subárvore (isso é facilmente mantido por meio de recursão - e você pode simular isso com uma pilha, se desejar).
Agora você coloca todos os nós em uma matriz A de comprimento n onde o nó com o número em ordem i está na posição i. Então, quando você precisar encontrar a lista para um nó X, procure em A [X.min, X.max] - observe que esse intervalo incluirá o nó X, que também pode ser facilmente corrigido.
Tudo isso é realizado em O (n) tempo e ocupa O (n) espaço.
Eu espero que isso ajude.
fonte
A parte ineficiente não é atravessar a árvore, mas construir as listas de nós. Parece sensato criar a lista assim:
Como cada nó descendente é copiado na lista de cada pai, acabamos com O (n log n) complexidade em média para árvores balanceadas e O (n²) pior caso para árvores degeneradas que são realmente listas vinculadas.
Podemos passar para O (n) ou O (1), dependendo de você precisar fazer alguma configuração, se usarmos o truque de calcular as listas preguiçosamente. Suponha que temos um
child_iterator(node)
que nos dá os filhos desse nó. Podemos então definir trivialmente algodescendant_iterator(node)
como isto:Uma solução não recursiva está muito mais envolvida, pois o fluxo de controle do iterador é complicado (corotinas!). Atualizarei esta resposta hoje mais tarde.
Como a travessia de uma árvore é O (n) e a iteração sobre uma lista também é linear, esse truque adia completamente o custo até que seja pago de qualquer maneira. Por exemplo, a impressão da lista de descendentes para cada nó tem O (n²) na pior das hipóteses: A iteração em todos os nós é O (n) e a iteração nos descendentes de cada nó, estejam eles armazenados em uma lista ou calculados ad hoc .
Obviamente, isso não funcionará se você precisar de uma coleção real para trabalhar.
fonte
Este algoritmo curto deve fazê-lo. Dê uma olhada no código
public void TestTreeNodeChildrenListing()
Na verdade, o algoritmo passa pelos nós da árvore em sequência e mantém a lista de pais do nó atual. Conforme seu requisito, o nó atual é filho de cada nó pai e é adicionado a cada um deles como filho.
O resultado final é armazenado no dicionário.
fonte
Normalmente, você usaria apenas uma abordagem recursiva, pois permite alternar sua ordem de execução para que você possa calcular o número de folhas começando das folhas para cima. Como você precisaria usar o resultado da sua chamada recursiva para atualizar o nó atual, seria necessário um esforço especial para obter uma versão recursiva final. Se você não fizer esse esforço, é claro que essa abordagem simplesmente explodiria sua pilha em uma grande árvore.
Dado que percebemos que a idéia principal é obter uma ordem em loop começando nas folhas e voltando para a raiz, a idéia natural que vem à mente é executar uma classificação topológica na árvore. A sequência resultante de nós pode ser percorrida linearmente para somar o número de folhas (supondo que você possa verificar se um nó é uma folha
O(1)
). A complexidade geral do tempo da classificação topológica éO(|V|+|E|)
.Suponho que
N
seja o número de nós, o que|V|
normalmente seria (da nomenclatura do DAG). O tamanho de,E
por outro lado, depende muito da aridade da sua árvore. Por exemplo, uma árvore binária tem no máximo 2 arestas por nó, portantoO(|E|) = O(2*|V|) = O(|V|)
, nesse caso, o que resultaria em umO(|V|)
algoritmo geral . Observe que, devido à estrutura geral de uma árvore, você não pode ter algo parecidoO(|E|) = O(|V|^2)
. De fato, como cada nó tem um pai único, você pode ter no máximo uma borda para contar por nó quando considerar apenas as relações com os pais; portanto, para árvores, garantimos issoO(|E|) = O(|V|)
. Portanto, o algoritmo acima é sempre linear no tamanho da árvore.fonte