Drzewo punkt-przedział to struktura służąca do odczytywania wartości z danych
przedziałów a także aktualizacji wartości w danym ciągu. Dla przykładu, dla danego
ciągu chcemy w szybkim czasie móc:
1. Odczytać sumę na danym przedziale
2. Zmienić wartość w ciągu
Drzewo punkt-przedział dla tego typu operacji będzie wykonywało log2(n) operacji, gdzie n to długość ciągu. Zauważmy że przy brutalnym rozwiązaniu, czyli po prostu trzymaniu tablicy z wartościami i za każdym
razem liczeniu sumy od nowa złożoność byłaby taka:
-odczytanie sumy - O(n)
-zaaktualizowanie wartości - O(1)
Widzimy więc, że takie rozwiązanie nie
wystarcza..... Dlatego jest coś takiego jak drzewo przedziałowe!
Na początku tworzymy drzewo binarne, gdzie wierzchołek nr 1 jest korzeniem. Poszczególne wierzchołki będą odpowiadać
za poszczególne przedziały w taki sposób:
Na najniższych poziomach wierzchołki odpowiadają za jedną wartość w ciągu, a każdy wyższy wierzchołek odpowiada za
połączone przedziały synów. Tak zbudowane drzewo umożliwia nam w szybkim czasie wykonanie operacji:
Aktualizacji - zaczynamy w dolnym wierzchołku, które odpowiada za aktualizowany punkt. Aktualizujemy go. Następnie
przechodzimy do jego ojca, w którym wartość ustawiamy na sumę z synów (jeśli mamy drzewo sumy). Powtarzamy to, dopóki nie
zaaktualizujemy korzenia.
Odczytywania - mamy dwa wskaźniki, jeden jest tuż przed początkiem, a drugi tuż po końcu. Jeżeli pierwszy jest lewym synem
to dodajemy do wyniku wartość z wierzchołka o jeden większego. Jeżeli drugi jest prawym synem, to dodajemy do wyniku
wartość z wierzchołka o jeden mniejszego. Następnie obydwoma wskaźnikami przechodzimy do ich ojców i powtarzamy algorytm,
dopóki nie będą mieli tego samego ojca (czyli dopóki różnica ich numerów jest większa od 1).
Zauważmy, że podstawa drzewa jest potęgą dwójki. Ale co jeśli mamy długość ciągu, która nie jest potęgą dwójki? Po prostu
rozszerzamy nasze drzewo do najbliższej potęgi dwójki i wypełniamy to rozszerzenie elementami neutralnymi (w przypadku
sumy 0). Jak przechodzić między synem a ojcem, ojcem a synem? (v to numer wierzchołka, w którym się obecnie znajdujemy)
Do ojca: v podzielonie całkowicie przez 2; do lewego syna: v * 2, do prawego syna: v * 2 + 1. Takie łatwe przechodzenie
między ojcem a synem mamy dzięki numeracji wierzchołków w drzewie. Jak obliczyć jaki numer ma wierzchołek odpowiadający
za punkt x? Trzymamy wartość base, która nam mówi ile wierzchołków mamy w podstawie drzewa. Numer wierzchołka odpowiadającego
za punkt x to x + base (x+base-1).
Zasymulujmy działanie drzewa przedziałowego na przykładowych danych:
Początkowo ciąg to 5 3 4 8
Odczytanie sumy z przedziału 2-4
Aktualizacja elementu 3; z 4 zmienia się na 1
Odczytanie sumy z przedziału 2-3
Aktualizacja elementu 2; z 3 zmienia się na 10
Tak wygląda początkowe drzewo:
Base ma wartość 4.
Odczytanie sumy z przedziału 2-4:
pierwszy wskaźnik: 2 + 4 - 1 - 1 = 4
drugi wskaźnik: 4 + 4 + 1 - 1 = 8
4 jest lewym synem, więc dodajemy do wyniku 3
8 nie byłoby prawym synem, więc nie dodajemy do wyniku nic
4/2 = 2, 8/2 = 4
2 jest lewym synem, więc dodajemy do wyniku 12
4 nie jest prawym synem, więc nie dodajemy do wyniku nic
2/2 = 1, 4/2 = 2, różnica numerów wierzchołków jest równa 1, więc kończymy. Odpowiedź na to zapytanie to 15.
Aktualizacja w elemencie 3:
3+4-1 = 6
Zmieniamy wartość w wierzchołku 6 na 1; 6 / 2 = 3
Zmieniamy wartość w wierzchołku 3 na 1 + 8 = 9; 3 / 2 = 1
Zmieniamy wartość w wierzchołku 1 na 8 + 9 = 17
Zaaktualizowaliśmy 1, więc kończymy aktualizację. Tak wygląda drzewo po tej aktualizacji:
Odczytanie sumy z przedziału 2-3:
pierwszy wskaźnik: 2 + 4 - 1 - 1 = 4
drugi wskaźnik: 3 + 4 + 1 - 1 = 7
4 jest lewym synem, więc dodajemy do wyniku 3
7 jest prawym synem, więc dodajemy do wyniku 6
4/2 = 2, 7/2 = 3, różnica numerów wierzchołków jest równa 1, więc kończymy. Odpowiedź na to zapytanie to 9.
Aktualizacja w elemencie 2:
2+4-1 = 5
Zmieniamy wartość w wierzchołku 5 na 10; 5 / 2 = 2
Zmieniamy wartość w wierzchołku 2 na 10 + 5 = 15; 2 / 2 = 1
Zmieniamy wartość w wierzchołku 1 na 15 + 9 = 24
Zaaktualizowaliśmy 1, więc kończymy aktualizację. Tak wygląda drzewo po tej aktualizacji:
Drzewo przedział-punkt to struktura służąca do aktualizowania wartości z danych
przedziałów a także odczytywania wartości w danym indexie. Dla przykładu, dla danego
ciągu chcemy w szybkim czasie móc:
1. Odczytać wartość z danego punktu
2. Zmienić wartości na danym przedziale o x
Drzewo przedział-punkt dla tego typu operacji będzie wykonywało log(n) operacji, gdzie n to długość ciągu. Zauważmy że przy brutalnym rozwiązaniu, czyli po prostu trzymaniu tablicy z wartościami
i za każdym razem aktualizowaniu po kolei wartości złożoność byłaby taka:
-odczytanie sumy - O(1)
-zaaktualizowanie wartości - O(n)
Widzimy więc,
że takie rozwiązanie nie wystarcza... Ale tutaj przychodzi nam z pomocą właśnie
drzewo przedziałowe!
Na początku tworzymy takie samo drzewo binarne, jak w drzewie punkt-przedział. Jeśli chodzi o funkcje, to zamienimy je miejscami: funkcję aktualizującą zmienimy na funkcję odpowiadającą i na odwrót. Czyli funkcja odpowiadająca będzie szła od wierzchołka odpowiadającego za dany punkt i potem do jego ojca itd. Zsumuje przy tym wartości we wszystkich napotkanych wierzchołkach i zwróci tę sumę jako wynik. Z kolei aktualizacja będzie miała dwa wskaźniki. Jeżeli pierwszy jest lewym synem to dodajemy do wierzchołka o jeden większego wartość o ile mamy zaaktualizować. Jeżeli drugi jest prawym synem, to to dodajemy do wierzchołka o jeden mniejszego wartość o ile mamy zaaktualizować. Przechodzenie między wierzchołkami i base tak jest takie same jak w drzewie punkt-przedział.
Zasymulujmy drzewo przedział-punkt dla przykładowych danych:
Początkowy ciąg: 5 3 4 8
Operacja odczytania wyniku w 3
Operacja odczytania wyniku w 2
Operacja aktualizacji przedziału 1-3 o 3
Operacja odczytania wyniku w 3
Operacja odczytania wyniku w 2
base = 4
Tak wygląda początkowe drzewo:
Operacja odczytania wyniku w 3:
3+4-1 = 6
Dodajemy do wyniku 4. 6 / 2 = 3
Dodajemy do wyniku 0. 3 / 2 = 1
Dodajemy do wyniku 0. Kończymy algorytm bo odczytaliśmy już z jedynki. Wynik to 4.
Operacja odczytania wyniku w 2:
2+4-1 = 5
Dodajemy do wyniku 3. 5 / 2 = 2
Dodajemy do wyniku 0. 2 / 2 = 1
Dodajemy do wyniku 0. Kończymy algorytm bo odczytaliśmy już z jedynki. Wynik to 3.
Operacja aktualizacji przedziału 1-3 o 3:
pierwszy wskaźnik: 1 + 4 - 1 - 1 = 3
drugi wskaźnik: 3 + 4 + 1 - 1 = 7
3 nie jest lewym synem, więc nie aktualizujemy 4
7 jest prawym synem, więc aktualizujemy 6: 4 + 3 = 7
3/2 = 1, 7/2 = 3
1 nie jest lewym synem, więc nie aktualizujemy 2
3 jest prawym synem, więc aktualizujemy 2: 0 + 3 = 3
1/2 = 0, 3/2 = 1
Różnica numerów wierzchołków jest równa 1, więc kończymy. Tak wygląda drzewo po aktualizacji:
Operacja odczytania wyniku w 3:
3+4-1 = 6
Dodajemy do wyniku 7. 6 / 2 = 3
Dodajemy do wyniku 0. 3 / 2 = 1
Dodajemy do wyniku 0. Kończymy algorytm bo odczytaliśmy już z jedynki. Wynik to 7.
Operacja odczytania wyniku w 2:
2+4-1 = 5
Dodajemy do wyniku 3. 5 / 2 = 2
Dodajemy do wyniku 3. 2 / 2 = 1
Dodajemy do wyniku 0. Kończymy algorytm bo odczytaliśmy już z jedynki. Wynik to 6.