Przeskocz do treści

Delta mi!

Bardzo oszczędne drzewa (I)

Jakub Radoszewski

o artykule ...

  • Publikacja w Delcie: marzec 2014
  • Publikacja elektroniczna: 02-03-2014
  • Wersja do druku [application/pdf]: (224 KB)
obrazek

Rys. 1

Rys. 1

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.

obrazek

Rys. 2 Reprezentacja drzewa z rysunku 1 za pomocą trzech tablic lsyn, psyn i ojciec. Liczba 0 w tablicy oznacza, że odpowiedni węzeł drzewa nie istnieje.

Rys. 2 Reprezentacja drzewa z rysunku 1 za pomocą trzech tablic lsyn, psyn i ojciec. Liczba 0 w tablicy oznacza, że odpowiedni węzeł drzewa nie istnieje.

obrazek

Rys. 3 Jest math ukorzenionych, nieetykietowanych drzew binarnych o trzech węzłach.

Rys. 3 Jest math ukorzenionych, nieetykietowanych drzew binarnych o trzech węzłach.

Czy jest to najbardziej oszczędna reprezentacja drzewa binarnego? Jeśli rozważane drzewo ma math węzłów, to łączny rozmiar wszystkich tablic wynosi math a jeśli nie są nam potrzebni ojcowie węzłów, to rozmiar jest równy math Co więcej, tablice lsynpsyn mają łącznie tylko math 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 math więc do reprezentacji każdego z nich w systemie binarnym wystarczy math cyfr. To oznacza, że cała reprezentacja wymaga z grubsza math bitów pamięci, gdzie math 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ż math 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ż math bitów? Aby odpowiedzieć na to pytanie, warto zadać inne: ile jest różnych drzew binarnych (ukorzenionych, nieetykietowanych) o  math węzłach? Oznaczmy tę liczbę przez math (patrz Rys. 3). Spróbujmy ułożyć wzór rekurencyjny na math  Drzewo o  math węzłach składa się z korzenia i dwóch jego poddrzew. Oznaczmy przez math liczbę węzłów w lewym poddrzewie math Wówczas:

display-math

Dodając do tego wzoru warunek początkowy math  otrzymujemy rekurencję definiującą tzw. liczby Catalana, o znanym wzorze ogólnym:

display-math

Znajomość wartości math  pozwala podać teoretyczne oszacowanie dolne na liczbę bitów w reprezentacji drzewa binarnego o  math węzłach. Otóż żeby każde drzewo binarne dało się reprezentować za pomocą math bitów, musi zachodzić math  gdyż w przeciwnym razie pewne dwa różne drzewa miałyby tę samą reprezentację. Ponieważ współczynnik dwumianowy występujący we wzorze na math  majoryzuje pozostałe współczynniki występujące w sumie:

display-math

math występujące w mianowniku jest niższego rzędu niż math więc możemy z niezłą dokładnością asymptotyczną przybliżyć math  przez math

obrazek

Stąd wysnuwamy wniosek, że do reprezentacji drzew binarnych powinno nam wystarczyć mniej więcej math bitów. Oczywiście, taka reprezentacja istnieje. Wystarczy wszystkie math-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 math bitach. Taka reprezentacja, jakkolwiek niezwykle oszczędna, jest, niestety, dużo mniej wygodna niż nasza początkowa reprezentacja wykorzystująca math 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 math 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 math-tej jedynki (względnie math-tego zera) w ciągu – operacja math oraz sprawdzanie, ile jedynek (względnie ile zer) znajduje się w ciągu do ustalonej pozycji – operacja math Formalnie, niech math będzie ustalonym ciągiem zero-jedynkowym. Wówczas dla math oraz math zapytania mają postać:

pict

Okazuje się, że istnieje struktura danych, która poza ciągiem math zużywa math 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:

display-math

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  math węzłów wewnętrznych. Stąd łączna liczba węzłów drzewa, a zarazem liczba bitów w ciągu kodowym to math

Sprawdźmy teraz, na ile użyteczny jest nasz ciąg kodowy. Każdy węzeł wewnętrzny drzewa math ma dwa numery, czarny math i kolorowy math Aby przeliczyć numer czarny na kolorowy, wystarczy znaleźć math-tą jedynkę w ciągu kodowym, czyli wykonać operację math:

display-math

Przyporządkowanie odwrotne wykonujemy za pomocą operacji math:

display-math

Musimy jeszcze opisać sposób poruszania się po drzewie. Jest on zaskakująco prosty:

display-math

Innymi słowy, lewym synem węzła o numerze czarnym math jest węzeł o numerze kolorowym math i podobnie w przypadku prawego syna; natomiast w przypadku ojca robimy odwrotnie: dla węzła o numerze kolorowym math ojcem jest węzeł o numerze czarnym math

Podane wzory zasługują na wyjaśnienie. Skoncentrujemy się na pierwszym z nich (dla operacji math), 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 math Każdy taki węzeł jest albo samym korzeniem drzewa, albo synem jednego z węzłów wewnętrznych o czarnych numerach math Zauważmy, że do tej drugiej grupy zaliczają się tak węzły wewnętrzne (w tym te o numerach czarnych math), jak i zewnętrzne. Wszystkich tych węzłów jest math więc rzeczywiście numerem kolorowym lewego syna węzła math jest math

Podsumujmy to, co wiemy o naszej reprezentacji. Ma ona rozmiar math 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 math-tej jedynki (względnie math-tego zera) w ciągu – operacja math oraz sprawdzanie, ile jedynek (względnie ile zer) znajduje się w ciągu do ustalonej pozycji – operacja math Formalnie, niech math będzie ustalonym ciągiem zero-jedynkowym. Wówczas dla math oraz math zapytania mają postać:

pict

Okazuje się, że istnieje struktura danych, która poza ciągiem math zużywa math 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).

obrazek

Rys. 4 Uzupełnienie drzewa binarnego z Rys. 1 węzłami zewnętrznymi.

Rys. 4 Uzupełnienie drzewa binarnego z Rys. 1 węzłami zewnętrznymi.

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:

display-math

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  math węzłów wewnętrznych. Stąd łączna liczba węzłów drzewa, a zarazem liczba bitów w ciągu kodowym to math

Sprawdźmy teraz, na ile użyteczny jest nasz ciąg kodowy. Każdy węzeł wewnętrzny drzewa math ma dwa numery, czarny math i kolorowy math Aby przeliczyć numer czarny na kolorowy, wystarczy znaleźć math-tą jedynkę w ciągu kodowym, czyli wykonać operację math:

display-math

Przyporządkowanie odwrotne wykonujemy za pomocą operacji math:

display-math

Musimy jeszcze opisać sposób poruszania się po drzewie. Jest on zaskakująco prosty:

display-math

Innymi słowy, lewym synem węzła o numerze czarnym math jest węzeł o numerze kolorowym math i podobnie w przypadku prawego syna; natomiast w przypadku ojca robimy odwrotnie: dla węzła o numerze kolorowym math ojcem jest węzeł o numerze czarnym math

Podane wzory zasługują na wyjaśnienie. Skoncentrujemy się na pierwszym z nich (dla operacji math), 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 math Każdy taki węzeł jest albo samym korzeniem drzewa, albo synem jednego z węzłów wewnętrznych o czarnych numerach math Zauważmy, że do tej drugiej grupy zaliczają się tak węzły wewnętrzne (w tym te o numerach czarnych math), jak i zewnętrzne. Wszystkich tych węzłów jest math więc rzeczywiście numerem kolorowym lewego syna węzła math jest math

Podsumujmy to, co wiemy o naszej reprezentacji. Ma ona rozmiar math 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.


O tym, jak zaimplementować strukturę danych z operacjami rank/select, opowiemy w następnym numerze.