Bardzo oszczędne drzewa (I)
Wiele struktur danych w komputerze można reprezentować w postaci drzewa binarnego. Aby przechować takie drzewo w pamięci komputera, należy dla każdego węzła zapamiętać numer jego lewego i prawego syna oraz, jeśli to potrzebne, numer węzła będącego jego ojcem. Wystarczą nam do tego trzy tablice.
Czy jest to najbardziej oszczędna reprezentacja drzewa binarnego? Jeśli rozważane drzewo ma węzłów, to łączny rozmiar wszystkich tablic wynosi a jeśli nie są nam potrzebni ojcowie węzłów, to rozmiar jest równy Co więcej, tablice lsyn i psyn mają łącznie tylko niezerowych komórek. Możemy przeanalizować to jeszcze dokładniej i spojrzeć na liczbę bitów w reprezentacji. Numery węzłów są u nas liczbami całkowitymi z zakresu od 1 do więc do reprezentacji każdego z nich w systemie binarnym wystarczy cyfr. To oznacza, że cała reprezentacja wymaga z grubsza bitów pamięci, gdzie jest stałą równą 1, 2 lub 3, zależnie od tego, które tablice przechowujemy. Kombinując dalej, można zauważyć, że do przechowywania małych numerów węzłów nie jest potrzebne aż bitów, jednak to spostrzeżenie nie pozwoli nam na pewno istotnie zredukować łącznej liczby bitów w reprezentacji.
A czy jest możliwe uzyskanie reprezentacji drzewa binarnego za pomocą istotnie mniej niż bitów? Aby odpowiedzieć na to pytanie, warto zadać inne: ile jest różnych drzew binarnych (ukorzenionych, nieetykietowanych) o węzłach? Oznaczmy tę liczbę przez (patrz Rys. 3). Spróbujmy ułożyć wzór rekurencyjny na Drzewo o węzłach składa się z korzenia i dwóch jego poddrzew. Oznaczmy przez liczbę węzłów w lewym poddrzewie Wówczas:
Dodając do tego wzoru warunek początkowy otrzymujemy rekurencję definiującą tzw. liczby Catalana, o znanym wzorze ogólnym:
Znajomość wartości pozwala podać teoretyczne oszacowanie dolne na liczbę bitów w reprezentacji drzewa binarnego o węzłach. Otóż żeby każde drzewo binarne dało się reprezentować za pomocą bitów, musi zachodzić gdyż w przeciwnym razie pewne dwa różne drzewa miałyby tę samą reprezentację. Ponieważ współczynnik dwumianowy występujący we wzorze na majoryzuje pozostałe współczynniki występujące w sumie:
a występujące w mianowniku jest niższego rzędu niż więc możemy z niezłą dokładnością asymptotyczną przybliżyć przez
Stąd wysnuwamy wniosek, że do reprezentacji drzew binarnych powinno nam wystarczyć mniej więcej bitów. Oczywiście, taka reprezentacja istnieje. Wystarczy wszystkie -węzłowe drzewa binarne ponumerować kolejnymi liczbami naturalnymi. Wówczas reprezentacją danego drzewa będzie jego numer, czyli liczba naturalna o co najwyżej bitach. Taka reprezentacja, jakkolwiek niezwykle oszczędna, jest, niestety, dużo mniej wygodna niż nasza początkowa reprezentacja wykorzystująca bitów. Nie pozwala ona nawigować po drzewie, tj. identyfikować węzłów drzewa i poruszać się po nich w naturalny sposób, czyli w kierunku do synów lub do ojca węzła. Okazuje się jednak, że istnieje inna, sprytna reprezentacja drzew binarnych, która wykorzystuje mniej więcej tyle samo bitów – dokładniej bitów, czyli więcej tylko o składnik niższego rzędu – i umożliwia łatwą nawigację po drzewie. W dalszej części artykułu przedstawimy taką właśnie bardzo oszczędną reprezentację.
Rank i select Na początek wprowadzimy pomocniczą strukturę danych operującą na ciągach binarnych. Chcielibyśmy umieć obsługiwać dwa typy zapytań dotyczące takich ciągów: wyznaczanie -tej jedynki (względnie -tego zera) w ciągu – operacja oraz sprawdzanie, ile jedynek (względnie ile zer) znajduje się w ciągu do ustalonej pozycji – operacja Formalnie, niech będzie ustalonym ciągiem zero-jedynkowym. Wówczas dla oraz zapytania mają postać:
Okazuje się, że istnieje struktura danych, która poza ciągiem zużywa bitów pamięci i pozwala odpowiadać na określone tu zapytania w czasie stałym. Odtąd będziemy używać tej struktury danych jako czarnej skrzynki (ang. black box).
Bardzo oszczędna reprezentacja drzew. Użyjemy teraz naszej pomocniczej struktury danych do konstrukcji bardzo oszczędnej reprezentacji drzew binarnych. Zacznijmy od uzupełnienia drzewa binarnego tzw. węzłami zewnętrznymi, tak aby każdy węzeł wewnętrzny miał dokładnie dwóch synów (Rys. 4). Węzły wewnętrzne drzewa ponumerujmy poziomami, a w ramach poziomów od lewej do prawej (czarne numery na rysunku 4). Ponadto w ten sam sposób ponumerujmy wszystkie węzły drzewa (kolorowe numery na rysunku 4).
Obejdźmy teraz wszystkie węzły drzewa w porządku numerów „kolorowych” i dla każdego z nich zapiszmy cyfrę 1, jeśli jest on węzłem wewnętrznym, a 0 w przeciwnym przypadku:
Tak otrzymany ciąg binarny wraz z powiązaną z nim strukturą danych do wykonywania operacji rank/select będzie stanowił bardzo oszczędną reprezentację drzewa, tzw. ciąg kodowy. Każdy węzeł uzupełnionego drzewa poza korzeniem jest synem jednego z węzłów wewnętrznych. Stąd łączna liczba węzłów drzewa, a zarazem liczba bitów w ciągu kodowym to
Sprawdźmy teraz, na ile użyteczny jest nasz ciąg kodowy. Każdy węzeł wewnętrzny drzewa ma dwa numery, czarny i kolorowy Aby przeliczyć numer czarny na kolorowy, wystarczy znaleźć -tą jedynkę w ciągu kodowym, czyli wykonać operację :
Przyporządkowanie odwrotne wykonujemy za pomocą operacji :
Musimy jeszcze opisać sposób poruszania się po drzewie. Jest on zaskakująco prosty:
Innymi słowy, lewym synem węzła o numerze czarnym jest węzeł o numerze kolorowym i podobnie w przypadku prawego syna; natomiast w przypadku ojca robimy odwrotnie: dla węzła o numerze kolorowym ojcem jest węzeł o numerze czarnym
Podane wzory zasługują na wyjaśnienie. Skoncentrujemy się na pierwszym z nich (dla operacji ), pozostałe otrzymuje się analogicznie. Aby go uzasadnić, wystarczy zbadać, ile węzłów uzupełnionego drzewa występuje w porządku kolejnych poziomów przed lewym synem węzła Każdy taki węzeł jest albo samym korzeniem drzewa, albo synem jednego z węzłów wewnętrznych o czarnych numerach Zauważmy, że do tej drugiej grupy zaliczają się tak węzły wewnętrzne (w tym te o numerach czarnych ), jak i zewnętrzne. Wszystkich tych węzłów jest więc rzeczywiście numerem kolorowym lewego syna węzła jest
Podsumujmy to, co wiemy o naszej reprezentacji. Ma ona rozmiar i pozwala identyfikować węzły drzewa i przemieszczać się w górę i w dół drzewa w czasie stałym. Nie wykazaliśmy jeszcze tylko, że jest ona poprawna, czyli że różne drzewa uzyskują różne reprezentacje. To jednakże wynika z faktu, że na podstawie reprezentacji drzewa, nawigując po nim, możemy jednoznacznie odtworzyć jego kształt. Tak więc nasza reprezentacja spełnia wszystkie oczekiwane własności.
Rank i select
Rank i select Na początek wprowadzimy pomocniczą strukturę danych operującą na ciągach binarnych. Chcielibyśmy umieć obsługiwać dwa typy zapytań dotyczące takich ciągów: wyznaczanie -tej jedynki (względnie -tego zera) w ciągu – operacja oraz sprawdzanie, ile jedynek (względnie ile zer) znajduje się w ciągu do ustalonej pozycji – operacja Formalnie, niech będzie ustalonym ciągiem zero-jedynkowym. Wówczas dla oraz zapytania mają postać:
Okazuje się, że istnieje struktura danych, która poza ciągiem zużywa bitów pamięci i pozwala odpowiadać na określone tu zapytania w czasie stałym. Odtąd będziemy używać tej struktury danych jako czarnej skrzynki (ang. black box).
Bardzo oszczędna reprezentacja drzew
Użyjemy teraz naszej pomocniczej struktury danych do konstrukcji bardzo oszczędnej reprezentacji drzew binarnych. Zacznijmy od uzupełnienia drzewa binarnego tzw. węzłami zewnętrznymi, tak aby każdy węzeł wewnętrzny miał dokładnie dwóch synów (Rys. 4). Węzły wewnętrzne drzewa ponumerujmy poziomami, a w ramach poziomów od lewej do prawej (czarne numery na rysunku 4). Ponadto w ten sam sposób ponumerujmy wszystkie węzły drzewa (kolorowe numery na rysunku 4).
Obejdźmy teraz wszystkie węzły drzewa w porządku numerów „kolorowych” i dla każdego z nich zapiszmy cyfrę 1, jeśli jest on węzłem wewnętrznym, a 0 w przeciwnym przypadku:
Tak otrzymany ciąg binarny wraz z powiązaną z nim strukturą danych do wykonywania operacji rank/select będzie stanowił bardzo oszczędną reprezentację drzewa, tzw. ciąg kodowy. Każdy węzeł uzupełnionego drzewa poza korzeniem jest synem jednego z węzłów wewnętrznych. Stąd łączna liczba węzłów drzewa, a zarazem liczba bitów w ciągu kodowym to
Sprawdźmy teraz, na ile użyteczny jest nasz ciąg kodowy. Każdy węzeł wewnętrzny drzewa ma dwa numery, czarny i kolorowy Aby przeliczyć numer czarny na kolorowy, wystarczy znaleźć -tą jedynkę w ciągu kodowym, czyli wykonać operację :
Przyporządkowanie odwrotne wykonujemy za pomocą operacji :
Musimy jeszcze opisać sposób poruszania się po drzewie. Jest on zaskakująco prosty:
Innymi słowy, lewym synem węzła o numerze czarnym jest węzeł o numerze kolorowym i podobnie w przypadku prawego syna; natomiast w przypadku ojca robimy odwrotnie: dla węzła o numerze kolorowym ojcem jest węzeł o numerze czarnym
Podane wzory zasługują na wyjaśnienie. Skoncentrujemy się na pierwszym z nich (dla operacji ), pozostałe otrzymuje się analogicznie. Aby go uzasadnić, wystarczy zbadać, ile węzłów uzupełnionego drzewa występuje w porządku kolejnych poziomów przed lewym synem węzła Każdy taki węzeł jest albo samym korzeniem drzewa, albo synem jednego z węzłów wewnętrznych o czarnych numerach Zauważmy, że do tej drugiej grupy zaliczają się tak węzły wewnętrzne (w tym te o numerach czarnych ), jak i zewnętrzne. Wszystkich tych węzłów jest więc rzeczywiście numerem kolorowym lewego syna węzła jest
Podsumujmy to, co wiemy o naszej reprezentacji. Ma ona rozmiar i pozwala identyfikować węzły drzewa i przemieszczać się w górę i w dół drzewa w czasie stałym. Nie wykazaliśmy jeszcze tylko, że jest ona poprawna, czyli że różne drzewa uzyskują różne reprezentacje. To jednakże wynika z faktu, że na podstawie reprezentacji drzewa, nawigując po nim, możemy jednoznacznie odtworzyć jego kształt. Tak więc nasza reprezentacja spełnia wszystkie oczekiwane własności.