Betrachten Sie unbeschriftete, verwurzelte Binärbäume. Wir können solche Bäume komprimieren : wenn es Zeiger auf Teilbäume und mit (interpretieren T = T ' = als strukturelle Gleichheit), speichern wir (oBdA) und ersetzen Sie alle Verweise auf mit Zeigern auf . Siehe uli Antwort für ein Beispiel.
Geben Sie einen Algorithmus an, der einen Baum im obigen Sinne als Eingabe verwendet und die (minimale) Anzahl von Knoten berechnet, die nach der Komprimierung verbleiben. Der Algorithmus sollte in der Zeit (im einheitlichen Kostenmodell) mit der Anzahl der Knoten in der Eingabe ausgeführt werden.
Dies war eine Prüfungsfrage, und ich konnte weder eine schöne Lösung finden, noch habe ich eine gesehen.
algorithms
data-structures
trees
binary-trees
Raphael
quelle
quelle
Antworten:
Ja, Sie können diese Komprimierung in , aber es ist nicht einfach :) Wir machen zuerst einige Beobachtungen und stellen dann den Algorithmus vor. Wir gehen davon aus, dass der Baum zunächst nicht komprimiert ist - dies ist nicht wirklich erforderlich, erleichtert aber die Analyse.O(nlogn)
Erstens charakterisieren wir "strukturelle Gleichheit" induktiv. Sei und T ' zwei (Unter-) Bäume. Wenn T und T ' beide Nullbäume sind (überhaupt keine Eckpunkte haben), sind sie strukturell äquivalent. Wenn T und T ' beide keine Nullbäume sind, sind sie strukturell äquivalent, wenn ihre linken Kinder strukturell äquivalent sind und ihre rechten Kinder strukturell äquivalent sind. "Strukturelle Äquivalenz" ist der minimale Fixpunkt über diese Definitionen.T T′ T T′ T T′
Beispielsweise sind zwei beliebige Blattknoten strukturell äquivalent, da beide die Nullbäume als ihre beiden untergeordneten Objekte haben, die strukturell äquivalent sind.
Da es ziemlich ärgerlich ist zu sagen, dass "ihre linken Kinder strukturell gleichwertig sind und ihre rechten Kinder", werden wir oft sagen, dass "ihre Kinder strukturell gleichwertig sind" und dasselbe beabsichtigen. Beachten Sie auch, dass wir manchmal "diesen Scheitelpunkt" sagen, wenn wir "den an diesem Scheitelpunkt verwurzelten Teilbaum" meinen.
Die obige Definition gibt uns sofort einen Hinweis, wie die Komprimierung durchzuführen ist: Wenn wir die strukturelle Äquivalenz aller Teilbäume mit einer Tiefe von höchstens , können wir die strukturelle Äquivalenz der Teilbäume mit einer Tiefe von d + 1 leicht berechnen . Wir müssen diese Berechnung auf intelligente Weise durchführen, um eine Laufzeit von O ( n 2 ) zu vermeiden .d d+1 O(n2)
Der Algorithmus weist jedem Scheitelpunkt während seiner Ausführung Bezeichner zu. Ein Bezeichner ist eine Zahl in der Menge . Bezeichner sind eindeutig und ändern sich nie: Wir gehen daher davon aus, dass wir zu Beginn des Algorithmus eine (globale) Variable auf 1 setzen. Jedes Mal, wenn wir einem Scheitelpunkt einen Bezeichner zuweisen, weisen wir dem Scheitelpunkt und dem Inkrement den aktuellen Wert dieser Variablen zu der Wert dieser Variablen.{1,2,3,…,n}
Wir transformieren zuerst den Eingabebaum in (höchstens ) Listen, die Scheitelpunkte gleicher Tiefe enthalten, zusammen mit einem Zeiger auf deren Eltern. Dies ist leicht in O ( n ) Zeit möglich.n O(n)
Wir komprimieren zuerst alle Blätter (wir können diese Blätter in der Liste mit Scheitelpunkten der Tiefe 0 finden) zu einem einzigen Scheitelpunkt. Wir weisen diesem Vertex einen Bezeichner zu. Die Komprimierung von zwei Scheitelpunkten erfolgt durch Umleiten des übergeordneten Scheitelpunkts auf den anderen Scheitelpunkt.
Wir machen zwei Beobachtungen: Erstens hat jeder Scheitelpunkt Kinder mit einer streng kleineren Tiefe, und zweitens, wenn wir alle Scheitelpunkte mit einer Tiefe kleiner als komprimiert haben (und ihnen Bezeichner gegeben haben), dann sind zwei Scheitelpunkte mit der Tiefe d strukturell gleich und kann komprimiert werden, wenn die Bezeichner ihrer Kinder übereinstimmen. Diese letzte Beobachtung ergibt sich aus dem folgenden Argument: Zwei Eckpunkte sind strukturell äquivalent, wenn ihre Kinder strukturell äquivalent sind, und nach der Komprimierung bedeuten dies, dass ihre Zeiger auf dieselben Kinder zeigen, was wiederum bedeutet, dass die Bezeichner ihrer Kinder gleich sind.d d
Wir durchlaufen alle Listen mit Knoten gleicher Tiefe von kleiner bis großer Tiefe. Für jede Ebene erstellen wir eine Liste von ganzzahligen Paaren, wobei jedes Paar den Bezeichnern der untergeordneten Elemente eines Scheitelpunkts auf dieser Ebene entspricht. Wir haben, dass zwei Eckpunkte in dieser Ebene strukturell äquivalent sind, wenn ihre entsprechenden ganzzahligen Paare gleich sind. Unter Verwendung der lexikografischen Reihenfolge können wir diese sortieren und die Mengen von ganzzahligen Paaren erhalten, die gleich sind. Wir komprimieren diese Mengen wie oben beschrieben zu einzelnen Eckpunkten und geben ihnen Bezeichner.
Die obigen Beobachtungen beweisen, dass dieser Ansatz funktioniert und zum komprimierten Baum führt. Die Gesamtlaufzeit beträgt plus der Zeit, die zum Sortieren der von uns erstellten Listen benötigt wird. Da die Gesamtzahl der von uns erstellten Ganzzahlpaare n ist , ergibt sich , wie erforderlich , eine Gesamtlaufzeit von O ( n log n ) . Es ist trivial, zu zählen, wie viele Knoten am Ende des Vorgangs noch übrig sind (sehen Sie sich nur an, wie viele Identifikatoren wir ausgegeben haben).O(n) n O(nlogn)
quelle
degree
" Grad sollten wohl seindepth
? Und obwohl CS-Bäume nach unten wachsen, finde ich "Höhe eines Baumes" weniger verwirrend als "Tiefe eines Baumes".Das Komprimieren einer nicht veränderlichen Datenstruktur, so dass keine strukturell gleiche Teilmenge dupliziert wird, wird als Hash-Consing bezeichnet . Dies ist eine wichtige Technik bei der Speicherverwaltung in der funktionalen Programmierung. Hash Consing ist eine Art systematisches Merken von Datenstrukturen.
Wir werden den Baum Hash-konservieren und die Knoten nach dem Hash-konservieren zählen. Hashes, die eine Datenstruktur der Größe können immer in O ( n) durchgeführt werdenn Operationen; Das Zählen der Anzahl der Knoten am Ende ist linear zur Anzahl der Knoten.O(nlg(n))
Ich werde Bäume mit der folgenden Struktur betrachten (hier in Haskell-Syntax geschrieben):
Für jeden Konstruktor muss eine Zuordnung von den möglichen Argumenten zum Ergebnis der Anwendung des Konstruktors auf diese Argumente verwaltet werden. Blätter sind trivial. Bei Knoten, wir eine endliche Teil Karte aufrechtzuerhalten wobei T der Satz von Baum - Identifikatoren ist und N ist der Satz von Knotenkennungen; T = N ⊎ { ℓ } wobei ℓ die einzige Blattkennung ist. (Konkret ist ein Bezeichner ein Zeiger auf einen Speicherblock.)nodes:T×T→N T N T=N⊎{ℓ} ℓ
Wir können eine logarithmisch-zeitliche Datenstruktur für
nodes
beispielsweise einen ausgeglichenen binären Suchbaum verwenden. Im Folgenden werde ichlookup nodes
die Operation aufrufen, die einen Schlüssel in dernodes
Datenstruktur sucht , undinsert nodes
die Operation, die einen Wert unter einem neuen Schlüssel hinzufügt und diesen Schlüssel zurückgibt.Jetzt überqueren wir den Baum und fügen die Knoten hinzu. Obwohl ich in Haskell-ähnlichem Pseudocode schreibe, werde ich
nodes
als globale veränderbare Variable behandeln; Wir werden immer nur hinzufügen, aber die Einfügungen müssen durchgehend eingefädelt werden. Dieadd
Funktion rekursiert in einem Baum, fügt seine Unterbäume zurnodes
Karte hinzu und gibt den Bezeichner des Stamms zurück.Die Anzahl der
insert
Aufrufe, die auch die endgültige Größe dernodes
Datenstruktur darstellt, ist die Anzahl der Knoten nach maximaler Komprimierung. (Fügen Sie bei Bedarf eine für den leeren Baum hinzu.)quelle
nodes
insert
undadd
sollte explizit gemacht werden, und es sollte eine Funktion angegeben werden, die das Problem tatsächlich löst.nodes
aus Bequemlichkeitsgründen eine veränderbare Variable, aber Sie können sie durchlaufen. Ich werde keinen vollständigen Code angeben, das ist nicht SO.Hier ist eine andere Idee, die darauf abzielt, die Struktur von Bäumen (injektiv) in Zahlen zu kodieren, anstatt sie nur willkürlich zu kennzeichnen. Dafür verwenden wir, dass die Primfaktorisierung jeder Zahl eindeutig ist.
Diese letzte Annahme betrifft echte Maschinen. In diesem Fall würde man es vorziehen, etwas Ähnliches wie die Cantor-Paarungsfunktion anstelle der Potenzierung zu verwenden.
quelle
Da Bilder in Kommentaren nicht erlaubt sind:
oben links: ein eingabebaum
oben rechts: Die in Knoten 5 und 7 verwurzelten Teilbäume sind ebenfalls isomorph.
Unten links und rechts: Die komprimierten Bäume sind nicht eindeutig definiert.
quelle
Edit: Ich habe die Frage gelesen, als T und T ′ Kinder desselben Elternteils waren. Ich habe die Definition von Komprimierung auch als rekursiv angesehen, was bedeutet, dass Sie zwei zuvor komprimierte Unterbäume komprimieren können. Wenn das nicht die eigentliche Frage ist, funktioniert meine Antwort möglicherweise nicht.
Wo
hasSameStructure()
ist eine Funktion, die zwei bereits komprimierte Teilbäume in linearer Zeit vergleicht, um festzustellen, ob sie exakt die gleiche Struktur haben. Das Schreiben einer rekursiven linearen Zeitfunktion, die jede durchläuft und prüft, ob der eine Teilbaum jedes Mal ein linkes Kind hat, wenn der andere es tut, usw. sollte nicht schwierig sein.Lassennℓ und nr Sei die Größe des linken und rechten Teilbaums (nach der Komprimierung). Dann ist die Laufzeit
quelle