Der effizienteste Weg, um alle Nachkommen aller Knoten in einem Baum zu generieren

9

Ich suche nach dem effizientesten Algorithmus, um einen Baum zu erstellen (entweder als Liste von Kanten oder als Liste von Zuordnungen vom übergeordneten Knoten zu einer Liste von untergeordneten Knoten gespeichert). und für JEDEN Knoten eine Liste aller von ihm abstammenden Knoten (Blattebene und Nicht-Blattebene) erstellen.

Die Implementierung muss aufgrund der Skalierung über Schleifen statt über Reclusion erfolgen. und sollte idealerweise O (N) sein.

Diese SO-Frage behandelt eine einigermaßen offensichtliche Standardlösung, um die Antwort für EINEN Knoten in einem Baum zu finden. Aber offensichtlich ist es sehr ineffizient, diesen Algorithmus auf jedem Baumknoten zu wiederholen (von oben (O (NlogN) bis O (N ^ 2)).

Die Baumwurzel ist bekannt. Der Baum hat eine absolut willkürliche Form (z. B. nicht N-när, in keiner Weise ausgeglichen, Form oder Gestalt, keine einheitliche Tiefe) - einige Knoten haben 1-2 Kinder, andere 30K Kinder.

Auf praktischer Ebene (obwohl dies den Algorithmus nicht beeinflussen sollte) hat der Baum ~ 100K-200K-Knoten.

DVK
quelle
Sie können die Rekursion mithilfe einer Schleife und eines Stapels simulieren. Ist dies für Ihre Lösung zulässig?
Giorgio
@ Giorgio - natürlich. Das habe ich versucht, indem ich "via Schleifen statt Reklusion" impliziere.
DVK

Antworten:

5

Wenn Sie tatsächlich jede Liste als unterschiedliche Kopien PRODUZIEREN möchten, können Sie nicht hoffen, im schlimmsten Fall einen besseren Platz als n ^ 2 zu erreichen. Wenn Sie nur Zugriff auf jede Liste benötigen:

Ich würde den Baum ausgehend von der Wurzel in der richtigen Reihenfolge durchlaufen:

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

Speichern Sie dann für jeden Knoten im Baum die minimale In-Order-Nummer und die maximale In-Order-Nummer in seinem Teilbaum (dies kann leicht durch Rekursion beibehalten werden - und Sie können dies mit einem Stapel simulieren, wenn Sie dies wünschen).

Nun setzen Sie alle Knoten in ein Array A der Länge n, wobei sich der Knoten mit der Ordnungsnummer i an Position i befindet. Wenn Sie dann die Liste für einen Knoten X suchen müssen, schauen Sie in A [X.min, X.max] nach - beachten Sie, dass dieses Intervall den Knoten X enthält, der auch leicht repariert werden kann.

All dies wird in O (n) Zeit erreicht und nimmt O (n) Raum ein.

Ich hoffe das hilft.

Jesper Nielsen
quelle
2

Der ineffiziente Teil durchläuft nicht den Baum, sondern erstellt die Knotenlisten. Es erscheint sinnvoll, die Liste folgendermaßen zu erstellen:

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

Da jeder untergeordnete Knoten in die Liste jedes übergeordneten Knotens kopiert wird, ergibt sich für ausgeglichene Bäume im Durchschnitt eine Komplexität von O (n log n) und für entartete Bäume, bei denen es sich wirklich um verknüpfte Listen handelt, der Worst-Case von O (n²).

Wir können auf O (n) oder O (1) fallen, je nachdem, ob Sie ein Setup durchführen müssen, wenn wir den Trick der trägen Berechnung der Listen verwenden. Angenommen, wir haben eine child_iterator(node), die uns die Kinder dieses Knotens gibt. Wir können dann trivial so etwas definieren descendant_iterator(node):

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

Eine nicht rekursive Lösung ist viel komplizierter, da der Ablauf der Iteratorsteuerung schwierig ist (Coroutinen!). Ich werde diese Antwort später heute aktualisieren.

Da das Durchlaufen eines Baums O (n) ist und die Iteration über eine Liste ebenfalls linear ist, werden die Kosten durch diesen Trick vollständig verschoben, bis sie trotzdem bezahlt werden. Zum Beispiel hat das Ausdrucken der Liste der Nachkommen für jeden Knoten eine O (n²) -Komplexität im ungünstigsten Fall: Das Iterieren über alle Knoten ist O (n), und das Iterieren über die Nachkommen jedes Knotens, unabhängig davon, ob sie in einer Liste gespeichert oder ad hoc berechnet sind .

Dies funktioniert natürlich nicht, wenn Sie eine tatsächliche Sammlung benötigen, an der Sie arbeiten können.

amon
quelle
Entschuldigung, -1. Der gesamte Zweck des Aglorithmus besteht in der Vorberechnung der Daten. Lazy Computation besiegt den Grund für das Ausführen des Algo vollständig.
DVK
2
@ DVK Ok, ich habe Ihre Anforderungen möglicherweise falsch verstanden. Was machst du mit den resultierenden Listen? Wenn die Vorberechnung der Listen ein Engpass ist (aber nicht die Listen verwendet), würde dies bedeuten, dass Sie nicht alle von Ihnen aggregierten Daten verwenden, und eine verzögerte Berechnung wäre dann ein Gewinn. Wenn Sie jedoch alle Daten verwenden, ist der Algorithmus für die Vorberechnung weitgehend irrelevant - die algorithmische Komplexität der Verwendung der Daten entspricht mindestens der Komplexität der Erstellung der Listen.
Amon
0

Dieser kurze Algorithmus sollte es tun. Schauen Sie sich den Code an public void TestTreeNodeChildrenListing()

Der Algorithmus geht tatsächlich nacheinander durch die Knoten des Baums und behält die Liste der Eltern des aktuellen Knotens bei. Gemäß Ihrer Anforderung ist der aktuelle Knoten ein untergeordnetes Element jedes übergeordneten Knotens und wird jedem untergeordneten Knoten als untergeordnetes Element hinzugefügt.

Das Endergebnis wird im Wörterbuch gespeichert.

    [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; }
        }
    }
}
Niedrig fliegender Pelikan
quelle
Für mich sieht dies sehr nach der Komplexität von O (n log n) bis O (n²) aus und verbessert sich nur geringfügig gegenüber der Antwort, mit der DVK in ihrer Frage verknüpft hat. Wenn dies keine Verbesserung ist, wie beantwortet dies die Frage? Der einzige Wert, den diese Antwort hinzufügt, ist die Darstellung eines iterativen Ausdrucks des naiven Algorithmus.
Amon
Es ist O (n). Wenn Sie sich den Algorithmus genau ansehen, wird er einmal über die Knoten iteriert. Gleichzeitig wird die Sammlung von untergeordneten Knoten für jeden übergeordneten Knoten gleichzeitig erstellt.
Niedrig fliegender Pelikan
1
Sie durchlaufen alle Knoten, dh O (n). Dann durchlaufen Sie alle Kinder, die wir vorerst ignorieren (stellen wir uns vor, es ist ein konstanter Faktor). Anschließend durchlaufen Sie alle übergeordneten Elemente des aktuellen Knotens. In einem Bilanzbaum ist dies O (log n), aber in dem entarteten Fall, in dem unser Baum eine verknüpfte Liste ist, kann es O (n) sein. Wenn wir also die Kosten für das Durchlaufen aller Knoten mit den Kosten für das Durchlaufen ihrer Eltern multiplizieren, erhalten wir die Zeitkomplexität von O (n log n) bis O (n²). Ohne Multithreading gibt es keine „gleichzeitig“.
Amon
"Zur gleichen Zeit" bedeutet, dass die Sammlung in derselben Schleife erstellt wird und keine anderen Schleifen beteiligt sind.
Niedrig fliegender Pelikan
0

Normalerweise würden Sie nur einen rekursiven Ansatz verwenden, da Sie damit Ihre Ausführungsreihenfolge so ändern können, dass Sie die Anzahl der Blätter ab den Blättern aufwärts berechnen können. Da Sie das Ergebnis Ihres rekursiven Aufrufs verwenden müssten, um den aktuellen Knoten zu aktualisieren, wäre es besonders aufwändig, eine rekursive Endversion zu erhalten. Wenn Sie sich nicht darum bemühen, würde dieser Ansatz Ihren Stapel natürlich einfach für einen großen Baum explodieren lassen.

Angesichts der Tatsache, dass wir erkannt haben, dass die Hauptidee darin besteht, eine Schleifenreihenfolge von den Blättern bis zur Wurzel zu erhalten, ist die natürliche Idee, eine topologische Sortierung des Baums durchzuführen . Die resultierende Folge von Knoten kann linear durchlaufen werden, um die Anzahl der Blätter zu summieren (vorausgesetzt, Sie können überprüfen, ob ein Knoten ein Blatt ist O(1)). Die Gesamtzeitkomplexität der topologischen Sortierung beträgt O(|V|+|E|).

Ich gehe davon aus, dass dies Ndie Anzahl der Knoten ist, die |V|normalerweise (aus der DAG-Nomenklatur) stammen. Die Größe von Ehängt andererseits stark von der Arität Ihres Baumes ab. Beispielsweise hat ein Binärbaum O(|E|) = O(2*|V|) = O(|V|)in diesem Fall höchstens 2 Kanten pro Knoten , was zu einem O(|V|)Gesamtalgorithmus führen würde. Beachten Sie, dass Sie aufgrund der Gesamtstruktur eines Baums so etwas nicht haben können O(|E|) = O(|V|^2). Da jeder Knoten ein eindeutiges übergeordnetes Element hat, können Sie höchstens eine Kante pro Knoten zählen, wenn Sie nur übergeordnete Beziehungen berücksichtigen. Für Bäume haben wir also eine Garantie dafür O(|E|) = O(|V|). Daher ist der obige Algorithmus in der Größe des Baums immer linear.

Frank
quelle