Il modo più efficiente per generare tutti i discendenti di tutti i nodi in un albero


9

Sto cercando l'algoritmo più efficiente per prendere un albero (memorizzato come un elenco di spigoli; O come un elenco di mappature dal nodo padre a un elenco di nodi figlio); e produrre, per OGNI nodo, un elenco di tutti i nodi discendenti da esso (livello foglia e livello non foglia).

L'implementazione deve avvenire tramite loop anziché recusion, a causa della scala; e dovrebbe idealmente essere O (N).

Questa domanda SO copre una soluzione ragionevolmente ovvia standard per trovare la risposta per UN nodo in un albero. Ma ovviamente, ripetere questo algoritmo su ogni nodo dell'albero è altamente inefficiente (dalla parte superiore della mia testa, da O (NlogN) a O (N ^ 2)).

La radice dell'albero è nota. L'albero ha una forma assolutamente arbitraria (ad es. Non N-nary, non bilanciato in alcun modo, forma o forma, profondità non uniforme) - alcuni nodi hanno 1-2 bambini, altri 30K bambini.

A livello pratico (anche se non dovrebbe influire sull'algoritmo) l'albero ha nodi ~ 100K-200K.


Puoi simulare la ricorsione usando un loop e uno stack, è consentito per la tua soluzione?
Giorgio,

@Giorgio - certo. Questo è ciò che ho cercato di insinuare "attraverso i loop invece della recusione".
DVK,

Risposte:


5

Se in realtà vuoi PRODURRE ogni elenco come copie diverse, non puoi sperare di ottenere uno spazio migliore di n ^ 2 nel caso peggiore. Se hai solo bisogno di accedere a ciascun elenco:

Eseguirò un attraversamento in ordine dell'albero a partire dalla radice:

http://en.wikipedia.org/wiki/Tree_traversal

Quindi, per ogni nodo nella struttura ad albero, memorizza il numero minimo in ordine e il numero massimo in ordine nella sua sottostruttura (questo è facilmente gestibile attraverso la ricorsione - e puoi simularlo con una pila se lo desideri).

Ora metti tutti i nodi in un array A di lunghezza n in cui il nodo con il numero in ordine i è nella posizione i. Quindi, quando è necessario trovare l'elenco di un nodo X, si guarda in A [X.min, X.max] - si noti che questo intervallo includerà il nodo X, che può anche essere facilmente risolto.

Tutto ciò viene realizzato nel tempo O (n) e occupa O (n) spazio.

Spero che questo possa essere d'aiuto.


2

La parte inefficiente non sta attraversando l'albero, ma costruendo gli elenchi di nodi. Sembrerebbe sensato creare l'elenco in questo modo:

descendants[node] = []
for child in node.childs:
    descendants[node].push(child)
    for d in descendants[child]:
        descendants[node].push(d)

Dato che ciascun nodo discendente viene copiato nell'elenco di ciascun genitore, si finisce con la complessità O (n log n) in media per gli alberi bilanciati e il caso peggiore di O (n²) per gli alberi degenerati che sono elenchi realmente collegati.

Possiamo passare a O (n) o O (1) a seconda che sia necessario eseguire qualsiasi impostazione se utilizziamo il trucco del calcolo degli elenchi pigramente. Supponiamo di avere un child_iterator(node)che ci dà i figli di quel nodo. Possiamo quindi banalmente definire un descendant_iterator(node)simile:

def descendant_iterator(node):
  for child in child_iterator(node):
    yield from descendant_iterator(child)
  yield node

Una soluzione non ricorsiva è molto più coinvolta, poiché il flusso di controllo dell'iteratore è complicato (coroutine!). Aggiornerò questa risposta più tardi oggi.

Poiché l'attraversamento di un albero è O (n) e anche l'iterazione su un elenco è lineare, questo trucco difende completamente il costo fino a quando non viene pagato comunque. Ad esempio, stampare l'elenco dei discendenti per ciascun nodo presenta la complessità del caso peggiore O (n²): l'iterazione su tutti i nodi è O (n) e quindi sta iterando sui discendenti di ciascun nodo, sia che siano memorizzati in un elenco o calcolati ad hoc .

Naturalmente, questo non funzionerà se hai bisogno di una vera collezione su cui lavorare.


Spiacente, -1. L'intero scopo dell'agloritmo è pre-calcolare i dati. Il calcolo pigro sta completamente sconfiggendo il motivo per cui si esegue anche l'ego.
DVK,

2
@DVK Ok, potrei aver frainteso le tue esigenze. Cosa stai facendo con gli elenchi risultanti? Se precompilare gli elenchi è un collo di bottiglia (ma non utilizzare gli elenchi), ciò indicherebbe che non stai usando tutti i dati che aggreghi, e il calcolo pigro sarebbe quindi una vittoria. Ma se usi tutti i dati, l'algoritmo per il pre-calcolo è in gran parte irrilevante: la complessità algoritmica dell'uso dei dati sarà almeno uguale alla complessità della costruzione degli elenchi.
amon,

0

Questo breve algoritmo dovrebbe farlo, dai un'occhiata al codice public void TestTreeNodeChildrenListing()

L'algoritmo in realtà attraversa i nodi dell'albero in sequenza e mantiene l'elenco dei genitori del nodo corrente. Secondo il vostro requisito, il nodo corrente è un figlio di ciascun nodo padre che viene aggiunto a ciascuno di essi come figlio.

Il risultato finale è memorizzato nel dizionario.

    [TestFixture]
    public class TreeNodeChildrenListing
    {
        private TreeNode _root;

        [SetUp]
        public void SetUp()
        {
            _root = new TreeNode("root");
            int rootCount = 0;
            for (int i = 0; i < 2; i++)
            {
                int iCount = 0;
                var iNode = new TreeNode("i:" + i);
                _root.Children.Add(iNode);
                rootCount++;
                for (int j = 0; j < 2; j++)
                {
                    int jCount = 0;
                    var jNode = new TreeNode(iNode.Value + "_j:" + j);
                    iCount++;
                    rootCount++;
                    iNode.Children.Add(jNode);
                    for (int k = 0; k < 2; k++)
                    {
                        var kNode = new TreeNode(jNode.Value + "_k:" + k);
                        jNode.Children.Add(kNode);
                        iCount++;
                        rootCount++;
                        jCount++;

                    }
                    jNode.Value += " ChildCount:" + jCount;
                }
                iNode.Value += " ChildCount:" + iCount;
            }
            _root.Value += " ChildCount:" + rootCount;
        }

        [Test]
        public void TestTreeNodeChildrenListing()
        {
            var iteration = new Stack<TreeNode>();
            var parents = new List<TreeNode>();
            var dic = new Dictionary<TreeNode, IList<TreeNode>>();

            TreeNode node = _root;
            while (node != null)
            {
                if (node.Children.Count > 0)
                {
                    if (!dic.ContainsKey(node))
                        dic.Add(node,new List<TreeNode>());

                    parents.Add(node);
                    foreach (var child in node.Children)
                    {
                        foreach (var parent in parents)
                        {
                            dic[parent].Add(child);
                        }
                        iteration.Push(child);
                    }
                }

                if (iteration.Count > 0)
                    node = iteration.Pop();
                else
                    node = null;

                bool removeParents = true;
                while (removeParents)
                {
                    var lastParent = parents[parents.Count - 1];
                    if (!lastParent.Children.Contains(node)
                        && node != _root && lastParent != _root)
                    {
                        parents.Remove(lastParent);
                    }
                    else
                    {
                        removeParents = false;
                    }
                }
            }
        }
    }

    internal class TreeNode
    {
        private IList<TreeNode> _children;
        public string Value { get; set; }

        public TreeNode(string value)
        {
            _children = new List<TreeNode>();
            Value = value;
        }

        public IList<TreeNode> Children
        {
            get { return _children; }
        }
    }
}

Per me, questo assomiglia molto alla complessità da O (n log n) a O (n²), e migliora solo marginalmente rispetto alla risposta a cui DVK ha collegato la loro domanda. Quindi se questo non è un miglioramento, come risponde alla domanda? L'unico valore aggiunto da questa risposta è mostrare un'espressione iterativa dell'algoritmo ingenuo.
amon,

È O (n), se guardi da vicino l'algoritmo, scorre una volta sopra i nodi. Allo stesso tempo, crea la raccolta di nodi figlio per ciascun nodo padre contemporaneamente.
Low Flying Pelican

1
Si passa in rassegna tutti i nodi, ovvero O (n). Quindi fai un ciclo tra tutti i bambini, che per ora ignoreremo (immaginiamo che sia un fattore costante). Quindi esegui il ciclo attraverso tutti i genitori del nodo corrente. In un albero dei saldi, questo è O (log n), ma nel caso degenerato in cui il nostro albero è un elenco collegato, può essere O (n). Quindi, se moltiplichiamo il costo del ciclo attraverso tutti i nodi con il costo del ciclo attraverso i loro genitori, otteniamo una complessità temporale da O (n log n) a O (n²). Senza multithreading, non esiste "allo stesso tempo".
amon,

"allo stesso tempo" significa che crea la raccolta nello stesso loop e non sono coinvolti altri loop.
Low Flying Pelican

0

Normalmente, useresti solo un approccio ricorsivo, perché ti consente di cambiare l'ordine di esecuzione in modo tale da poter calcolare il numero di foglie a partire dalle foglie verso l'alto. Poiché, per aggiornare il nodo corrente, è necessario utilizzare il risultato della chiamata ricorsiva, sarebbe necessario uno sforzo speciale per ottenere una versione ricorsiva della coda. Se non fai questo sforzo, ovviamente questo approccio farebbe semplicemente esplodere il tuo stack per un grande albero.

Dato che ci siamo resi conto che l'idea principale è quella di ottenere un ordine circolare che parte dalle foglie e risalga verso la radice, l'idea naturale che viene in mente è quella di eseguire un ordinamento topologico sull'albero. La sequenza di nodi risultante può essere attraversata linearmente per sommare il numero di foglie (supponendo che sia possibile verificare che un nodo sia una foglia O(1)). La complessità temporale complessiva dell'ordinamento topologico è O(|V|+|E|).

Presumo che il tuo Nsia il numero di nodi, che sarebbe |V|tipicamente (dalla nomenclatura DAG). La dimensione d' Ealtra parte dipende fortemente dall'arità del tuo albero. Ad esempio, un albero binario ha al massimo 2 bordi per nodo, quindi O(|E|) = O(2*|V|) = O(|V|)in quel caso, il che comporterebbe un O(|V|)algoritmo globale . Nota che a causa della struttura generale di un albero, non puoi avere qualcosa di simile O(|E|) = O(|V|^2). Infatti, poiché ogni nodo ha un genitore univoco, puoi avere al massimo un fronte da contare per nodo quando consideri solo le relazioni genitore, quindi per gli alberi abbiamo una garanzia che O(|E|) = O(|V|). Pertanto, l'algoritmo sopra è sempre lineare nella dimensione dell'albero.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.