Sei eine ganze Zahl und sei die Menge aller ganzen Zahlen. Es sei das Intervall der ganzen Zahlen .
Ich suche eine Datenstruktur , die eine Karte darstellen . Ich möchte, dass die Datenstruktur die folgenden Operationen unterstützt:
sollte .
sollte aktualisieren,so dass , dh auf eine neue Karte aktualisieren , so dass für und für i ∉ [ a , b ] .
sollte das größte Intervall [ a , b ] zurückgeben, so dass i ∈ [ a , b ] und f auf [ a , b ] konstant sind(dh f ( a ) = f ( a + 1 ) = ⋯ = f ( b ) ).
sollte f auf eine neue Abbildung f ' aktualisieren , so dass f ' ( i ) = f ( i ) + δ für i ∈ [ a , b ] und f ' ( i ) = f ( i ) für i ∉ [ a , b ] .
Ich möchte, dass jede dieser Operationen effizient ist. Ich würde die oder O ( lg n ) -Zeit als effizient betrachten, aber die O ( n ) -Zeit ist zu langsam. Es ist in Ordnung, wenn die Laufzeiten amortisierte Laufzeiten sind. Gibt es eine Datenstruktur, die alle diese Vorgänge gleichzeitig effizient macht?
(Ich habe festgestellt, dass ein ähnliches Muster bei mehreren Programmierherausforderungen auftritt. Dies ist eine Verallgemeinerung, die für all diese Herausforderungsprobleme ausreichen würde.)
add
wäre jedoch linear in der Anzahl der Teilintervalle von ; Haben Sie an einen Spreizbaum mit zusätzlichen unären „ + δ “ -Knoten gedacht , die träge verdichtet wurden?Antworten:
Ich glaube, dass logarithmische Zeit für alle Abfragen erreichbar ist. Die Hauptidee besteht darin, einen Intervallbaum zu verwenden, bei dem jeder Knoten im Baum einem Intervall von Indizes entspricht. Ich werde die Schlüsselideen aufbauen, indem ich mit einer einfacheren Version der Datenstruktur beginne (die das Abrufen und Festlegen unterstützt, aber nicht die anderen Operationen) und dann Funktionen hinzufüge, um auch die anderen Funktionen zu unterstützen.
Ein einfaches Schema (unterstützt get und set, aber nicht addieren oder stechen)
Angenommen, ein Intervall ist flach, wenn die Funktion f auf [ a , b ] konstant ist , dh wenn f ( a ) = f ( a + 1 ) = ⋯ = f ( b ) .[a,b] f [a,b] f(a)=f(a+1)=⋯=f(b)
Unsere einfache Datenstruktur wird ein Intervallbaum sein. Mit anderen Worten, wir haben einen Binärbaum, in dem jeder Knoten einem Intervall (von Indizes) entspricht. Wir speichern das entsprechende Intervall in jedem Knoten v des Baums. Jedes Blatt entspricht einem flachen Intervall, und sie werden so angeordnet, dass das Auslesen der Blätter von links nach rechts eine Folge aufeinanderfolgender flacher Intervalle ergibt, die disjunkt sind und deren Vereinigung alle [ 1 , n ] ist . Das Intervall für einen internen Knoten ist die Vereinigung der Intervalle seiner beiden untergeordneten Knoten. Außerdem speichern wir in jedem Blattknoten ℓ den Wert V ( ℓ )I(v) v [1,n] ℓ V(ℓ) der Funktion in dem Intervall I ( ℓ ) , das diesem Knoten entspricht (beachten Sie, dass dieses Intervall flach ist, so dass f in dem Intervall konstant ist, so dass wir nur einen einzelnen Wert von f in jedem Blattknoten speichern ).f I(ℓ) f f
Entsprechend können Sie sich vorstellen, dass wir in flache Intervalle unterteilen und die Datenstruktur dann ein binärer Suchbaum ist, bei dem die Schlüssel die linken Endpunkte dieser Intervalle sind. Die Blätter enthalten den Wert von f in einem Bereich von Indizes, in denen f konstant ist.[1,n] f f
Verwenden Sie Standardmethoden, um sicherzustellen, dass der Binärbaum ausgeglichen bleibt, dh seine Tiefe beträgt (wobei m die aktuelle Anzahl der Blätter im Baum zählt). Natürlich ist m ≤ n , also ist die Tiefe immer höchstens O ( lg n ) . Dies wird unten hilfreich sein.O(lgm) m m≤n O(lgn)
Wir können jetzt die get- und set-Operationen wie folgt unterstützen:
ist einfach: Wir durchqueren den Baum, um das Blatt zu finden, dessen Intervall i enthält. Dies ist im Grunde nur das Durchlaufen eines binären Suchbaums. Da die Tiefe O ( lg n ) ist , ist die Laufzeit O ( lg n ) .get(i) i O(lgn) O(lgn)
ist schwieriger. Es funktioniert so:set([a,b],y)
Zuerst finden wir das Blattintervall das a enthält ; Wenn a 0 < a ist , teilen wir dieses Blattintervall in die beiden Intervalle [ a 0 , a - 1 ] und [ a , b 0 ] auf (wodurch dieser Blattknoten in einen internen Knoten umgewandelt und zwei untergeordnete Knoten eingeführt werden).[a0,b0] a a0<a [a0,a−1] [a,b0]
Als nächstes finden wir das Blattintervall das b enthält ; Wenn b < b 1 , teilen wir dieses Blattintervall in die beiden Intervalle [ a 1 , b ] und [ b + 1 , b 1 ] auf (wodurch dieser Blattknoten in einen internen Knoten umgewandelt und zwei untergeordnete Knoten eingeführt werden).[a1,b1] b b<b1 [a1,b] [b+1,b1]
An dieser Stelle behaupte ich, dass das Intervall als disjunkte Vereinigung von O ( lg n ) -Intervallen ausgedrückt werden kann, die einer Teilmenge von O ( lg n ) -Knoten im Baum entsprechen. Löschen Sie also alle Nachkommen dieser Knoten (verwandeln Sie sie in Blätter) und setzen Sie den in diesen Knoten gespeicherten Wert auf y .[a,b] O(lgn) O(lgn) y
Da wir schließlich die Form des Baums geändert haben, führen wir alle erforderlichen Rotationen durch, um den Baum wieder auszugleichen (unter Verwendung einer beliebigen Standardtechnik, um einen Baum im Gleichgewicht zu halten).
Da diese Operation einige einfache Operationen an -Knoten umfasst (und dieser Satz von Knoten leicht in O ( lg n ) -Zeit gefunden werden kann ), beträgt die Gesamtzeit für diese Operation O ( lg n ) .O(lgn) O(lgn) O(lgn)
Dies zeigt, dass wir sowohl die get- als auch die set-Operation in pro Operation unterstützen können. Tatsächlich kann gezeigt werden, dass die Laufzeit O ( lg min ( n , s ) ) ist , wobei s die Anzahl der bis jetzt ausgeführten eingestellten Operationen ist.O(lgn) O(lgmin(n,s)) s
Unterstützung für Hinzufügen hinzufügen
Wir können die obige Datenstruktur so ändern, dass sie auch die Add-Operation unterstützt. Anstatt den Wert der Funktion in den Blättern zu speichern, wird er insbesondere als die Summe der in einer Reihe von Knoten gespeicherten Zahlen dargestellt.
Genauer gesagt kann der Wert der Funktion am Eingang i als die Summe der Werte wiederhergestellt werden, die in den Knoten auf dem Pfad von der Wurzel des Baums bis zum Blatt gespeichert sind, dessen Intervall i enthält . In jedem Knoten v speichern wir einen Wert V ( v ) ; wenn v 0 , v 1 , ... , v k die Vorfahren eines Blattes darstellen v k (einschließlich des Blattes selbst), dann wird der Wert der Funktion bei I ( v k ) wirdf(i) i i v V(v) v0,v1,…,vk vk I(vk) .V(v0)+⋯+V(vk)
Es ist einfach, die Get- und Set-Operationen unter Verwendung einer Variante der oben beschriebenen Techniken zu unterstützen. Grundsätzlich verfolgen wir beim Durchlaufen des Baums nach unten die laufende Summe der Werte, sodass wir für jeden Knoten , den der Durchlauf besucht, die Summe der Werte der Knoten auf dem Pfad von der Wurzel zu x kennen . Sobald wir dies tun, werden einfache Anpassungen an der oben beschriebenen Implementierung von get und set ausreichen.x x
Und jetzt können wir effizient unterstützen. Zuerst drücken wir das Intervall [ a , b ] als die Vereinigung von O ( lg n ) -Intervallen aus, die einer Menge von O ( lg n ) -Knoten im Baum entsprechen (wobei bei Bedarf ein Knoten am linken Endpunkt und am rechten Endpunkt aufgeteilt wird). genau wie in den Schritten 1-3 der eingestellten Operation ausgeführt. Nun addieren wir einfach δ zu dem Wert, der in jedem dieser O ( lg n ) gespeichert ist.add([a,b],δ) [a,b] O(lgn) O(lgn) δ O(lgn) Knoten. (Wir löschen ihre Nachkommen nicht.)
Unterstützung der Stichoperation
Die Stichabfrage ist am schwierigsten zu unterstützen. Die Grundidee besteht darin, die obige Datenstruktur zu ändern, um die folgende zusätzliche Invariante beizubehalten:
Here I say that an interval[a,b] is maximal flat interval if (i) [a,b] is flat, and (ii) no interval containing [a,b] is flat (in other words, for all a′,b′ satisfying 1≤a′≤a≤b≤b′≤n , either [a′,b′]=[a,b] or [a′,b′] is not flat).
This makes the stab operation easy to implement:
However, now we need to modify the set and add operations to maintain the invariant (*). Each time we split a leaf into two, we might violate the invariant if some adjacent pair of leaf-intervals have the same value of the functionf . Fortunately, each set/add operation adds at most 4 new leaf intervals. Also, for each new interval, it is easy to find the leaf interval immediately to the left and right of it. Therefore, we can tell whether the invariant was violated; if it was, then we merge adjacent intervals where f has the same value. Fortunately, merging two adjacent intervals does not trigger cascading changes (so we don't need to check whether the merger might have introduced additional violations of the invariant). In all, this involves examining 12=O(1) pairs of intervals and possibly merging them. Finally, since a merger changes the shape of the tree, if this violates the balance-invariants, perform any necessary rotations to keep the tree balanced (following standard techniques for keeping binary trees balanced). In total, this adds at most O(lgn) additional work to the set/add operations.
Thus, this final data structure supports all four operations, and the running time for each operation isO(lgn) . A more precise estimate is O(lgmin(n,s)) time per operation, where s counts the number of set and add operations.
Parting thoughts
Phew, this was a pretty complex scheme. I hope I didn't make any mistakes. Please check my work carefully before relying upon this solution.
quelle