Przeskocz do treści

Delta mi!

Bardzo oszczędne drzewa (II)

Jakub Radoszewski

o artykule ...

  • Publikacja w Delcie: kwiecień 2014
  • Publikacja elektroniczna: 31-03-2014
  • Wersja do druku [application/pdf]: (763 KB)

Skoro dotychczas szło nam tak dobrze, spróbujmy pójść za ciosem i zaproponować bardzo oszczędną reprezentację drzew już niekoniecznie binarnych (ale wciąż ukorzenionych)...

obrazek

Rys. 1 Przykładowe drzewo ukorzenione o math węzłach. Dla węzła o numerze 2 mamy math i math

Rys. 1 Przykładowe drzewo ukorzenione o math węzłach. Dla węzła o numerze 2 mamy math i math

Przykład takiego drzewa można znaleźć na rysunku 1. Tym razem będziemy nawigować po drzewie za pomocą operacji: math oraz math (ta ostatnia polega na przejściu do najbliższego po prawej brata danego węzła). Aby operacja math miała sens, musimy też umieć dla każdego węzła wyznaczyć jego stopień, czyli liczbę synów (operacja math). Spróbujmy ustalić, jak oszczędną reprezentację mamy tu w ogóle szansę uzyskać. W tym celu, podobnie jak w przypadku binarnym, musimy stwierdzić, ile jest różnych drzew dowolnych o  math węzłach. Zliczać będziemy drzewa z nienumerowanymi węzłami i w których porządek synów węzła ma znaczenie. Niech math oznacza liczbę takich drzew (patrz Rys. 2). Jeśli przez math oznaczymy liczbę węzłów w poddrzewie skrajnie lewego syna korzenia math otrzymamy następujący wzór rekurencyjny na math:

display-math

Mamy ponadto math Porównując ten wzór z definicją rekurencyjną liczb Catalana, otrzymujemy, że math Stąd, podobnie jak poprzednio, w oszczędnej reprezentacji drzew dowolnych powinno nam wystarczyć math bitów.

obrazek

Rys. 2 Jest math ukorzenionych, nieetykietowanych drzew o czterech węzłach.

Rys. 2 Jest math ukorzenionych, nieetykietowanych drzew o czterech węzłach.

Przy konstrukcji ciągu kodowego znów ponumerujemy wszystkie węzły drzewa poziomami, a w ramach poziomów od lewej do prawej (Rys. 1). Wypiszmy teraz w jednym ciągu stopnie wszystkich węzłów – dla powyższego przykładu będzie to:

display-math

Chcielibyśmy, żeby taki właśnie ciąg był naszym ciągiem kodowym. Niestety, nie jest to możliwe: występuje w nim math liczb, z których każda jest z zakresu od 0 do math więc reprezentacja takiego ciągu wymagałaby rzędu math bitów. Aby sobie z tym poradzić, zastosujemy pozornie beznadziejny manewr: zapiszemy wszystkie stopnie unarnie, czyli każdy stopień zamienimy na ciąg jedynek odpowiedniej długości zakończony zerem. Ponieważ suma stopni węzłów to zaledwie math w ten sposób uzyskamy ciąg złożony z  math bitów, np.:

display-math

Ze względów technicznych wygodnie nam będzie jeszcze dodać do drzewa sztuczny korzeń, którego jedynym synem będzie faktyczny korzeń drzewa. Odpowiada to dopisaniu na początku ciągu jedynki i zera:

display-math

Taki ciąg kodowy o długości math wraz ze strukturą danych do wykonywania operacji rank/select będzie naszą bardzo oszczędną reprezentacją drzewa.

Spróbujmy uzasadnić, że podana reprezentacja umożliwia efektywne wykonywanie wszystkich potrzebnych operacji. Zauważmy przede wszystkim, że w ciągu kodowym występuje math jedynek, które w naturalny sposób odpowiadają węzłom drzewa: węzeł o numerze math reprezentuje math-ta jedynka w ciągu, będąca zarazem odpowiednią jedynką w ciągu opisującym stopień jego ojca. Każdemu węzłowi, oprócz już przydzielonego numeru czarnego, przypiszemy znów numer kolorowy. Będzie to pozycja w ciągu kodowym odpowiadającej mu jedynki. Widać natychmiast, że za pomocą operacji rank oraz select możemy bez problemu przeliczać numery czarne na kolorowe i odwrotnie.

obrazek

Wiedząc teraz, gdzie w ciągu kodowym znajduje się jedynka odpowiadająca danemu węzłowi, możemy zliczyć zera występujące wcześniej w ciągu i w ten sposób wyznaczyć numer jego ojca. Prawy brat węzła będzie ni mniej, ni więcej jak sąsiednią jedynką w ciągu kodowym. Natomiast ciąg jedynek odpowiadający synom węzła możemy zidentyfikować, znajdując math-sze i  math-te zero w ciągu, gdzie math jest numerem czarnym tego węzła. Z tego ciągu łatwo odzyskamy zarówno numer lewego syna węzła, jak i łączną liczbę jego synów, czyli stopień węzła.

Dopracowanie szczegółów implementacji operacji pozostawiamy Czytelnikowi, który, wyposażony w strukturę danych rank/select, wykona to bez większego trudu.

Implementacja operacji rankselect. Przyszła pora, aby uzupełnić powyższe rozważania opisem implementacji pomocniczej struktury danych. Zajmijmy się najpierw operacją math Zauważmy, że wystarczy umieć odpowiadać na zapytania dla math gdyż math

Nasz ciąg bitów math podzielimy na bloki: najpierw na duże bloki o długości math a w drugiej kolejności na małe bloki o długości math W zapisach tych math oznacza część całkowitą logarytmu o podstawie dwójkowej z  math Zakładamy dla uproszczenia, że math jest całkowite. Bloki w podziale są rozłączne i składają się z kolejnych elementów ciągu. Ponadto, jeśli ostatni blok okaże się krótszy, uzupełniamy go sztucznymi zerami aż do pełnej długości. Zauważmy, że math jest całkowitą wielokrotnością math i małe bloki stanowią „podpodział” dużych bloków. Bloki każdego typu numerujemy od jedynki.

Dla każdego dużego bloku w tablicy math zapamiętamy łączną liczbę jedynek w ciągu znajdujących się przed początkiem tego bloku. Elementy tablicy math są mniejsze niż math więc są liczbami złożonymi z co najwyżej math bitów. Łączny rozmiar tej tablicy jest zatem rzędu:

display-math

Dalej, dla każdego małego bloku w podobnej tablicy math zapamiętamy liczbę jedynek znajdujących się przed początkiem tego bloku, ale w ramach dużego bloku zawierającego rozważany mały blok. Tablica math ma math elementów, jednak każdy jej element jest mniejszy niż math Tablica ma więc rozmiar rzędu:

display-math

Obie tablice zajmują zatem math bitów.

obrazek

Łatwo dostrzec, w jaki sposób możemy użyć podanych tablic do odpowiedzi na zapytanie math Widać też, że potrzebujemy do tego jeszcze jednej informacji: o liczbie jedynek znajdujących się przed danym bitem w ramach jego małego bloku. W tym miejscu wykorzystamy własność, że różnych co do wartości małych bloków nie ma zbyt wiele. Dokładniej, jest tylko math takich bloków i możemy je w naturalny sposób ponumerować kolejnymi liczbami naturalnymi – numerem bloku będzie liczba binarna zawarta w tym bloku. Teraz możemy dla każdego z małych bloków wyznaczyć zawczasu wszystkie wyniki. W tablicy math dla danego numeru małego bloku i dla każdego indeksu w ramach małego bloku zapamiętamy liczbę jedynek na pozycjach poprzedzających ten indeks (wraz z tym indeksem). Tablica ta będzie miała math elementów, każdy nie większy niż math czyli jej łączny rozmiar to:

display-math

Ostatecznie wszystkie przechowywane tablice mają rozmiar math i pozwalają odpowiedzieć na zapytanie math następująco:

display-math

Odpowiedź na zapytanie uzyskujemy więc w czasie stałym, przy założeniu, że standardowe operacje arytmetyczne i bitowe na liczbach nie większych niż math działają w czasie stałym. W tym celu wystarczy ciąg math reprezentować w tablicy liczb całkowitych odpowiadających paczkom kolejnych bitów ciągu (jeszcze łatwiej sobie wyobrazić, że w tablicy pamiętamy po prostu kolejne identyfikatory małych bloków).

Z operacją math można poradzić sobie podobnie, jednak jest to dużo bardziej skomplikowane technicznie. Tym razem bloki nie są stałej długości, lecz wszystkie zawierają tę samą liczbę jedynek. Ponadto są tu potrzebne trzy poziomy podziału na bloki – dokładny opis pomijamy. Natomiast Czytelnika Zainteresowanego pozostawiamy z następującym pytaniem: dlaczego nie dało się rozwiązać problemu zapytań math używając tylko jednego rozmiaru bloków – powiedzmy, math

Citius, altius, fortius. W tym artykule opisaliśmy bardzo oszczędne reprezentacje drzew binarnych i dowolnych pozwalające efektywnie realizować podstawowe operacje związane z nawigacją po drzewie. Za pomocą bardziej zaawansowanych narzędzi zaprojektowano inne bardzo oszczędne reprezentacje drzew (bazujące na wyrażeniach nawiasowych równoważnych drzewom), które pozwalają wykonywać całe mnóstwo operacji: wyznaczanie rozmiarów poddrzew, wysokości i głębokości węzłów, znajdowanie najniższych wspólnych przodków (LCA) par węzłów i przodków węzłów na określonej głębokości, zliczanie liści w poddrzewach itd. Co więcej, z podanych tutaj pomysłów rozwinęła się cała dziedzina badań zajmująca się bardzo oszczędnymi strukturami danych. Więcej informacji na ten temat Czytelnik znajdzie w Internecie pod hasłem succinct data structures.