Informatyczny kącik olimpijski
Problem (MAX, +) kontratakuje
Tym razem wyjątkowo nie omówimy żadnego nowego zadania. Wrócimy za to do problemu przedstawionego w w artykule Złośliwy problem
i kubełkowe struktury danych...
Problem ten wygląda następująco: Dany jest ciąg złożony z liczb całkowitych
Na tym ciągu chcemy wykonywać dwa typy operacji: (1) przypisanie wartości
każdemu elementowi ciągu o indeksie z przedziału
i wartości mniejszej niż
(operacja
) oraz (2) zapytanie o sumę elementów ciągu o indeksach z przedziału
(operacja
).
W oryginalnym artykule przedstawiona była struktura danych, która takich operacji potrafi wykonać w czasie
Autor tamtego opracowania (Wojciech Śmietanka) zorganizował wówczas konkurs na poprawę tej złożoności, który udało mi się wtedy wygrać, uzyskując wynik
Niedawno w algorytmicznej społeczności pojawił się jeszcze lepszy pomysł, który przedstawię w dzisiejszym kąciku. Nie dość, że rozwiązuje on nasz problem w czasie
to jeszcze korzysta wyłącznie ze zwykłego drzewa przedziałowego! (Choć to, jakie konkretnie informacje będziemy przechowywać w każdym przedziale bazowym, jest - według mnie - dość nieintuicyjne.)
Przejdźmy do sedna. W każdym wierzchołku drzewa przedziałowego będziemy utrzymywać następujące informacje o elementach zawartych w jego przedziale bazowym: - minimalna wartość wśród elementów z przedziału;
- liczba elementów o wartości równej
;
- najmniejsza wartość elementu różna od
lub nieskończoność, jeśli wszystkie wartości w przedziale są równe;
- suma wartości elementów;
- wartość
o jaką należy zaktualizować dzieci przedziału bazowego, aby wartości w nich przechowywane stały się aktualne, lub null, gdy wartości w dzieciach są aktualne.
Po pierwsze, zachęcamy Czytelnika do przekonania się, że z aktualnych wartości dla dzieci przedziału bazowego możemy odzyskać wartości dla ojca. Następnie razem przeanalizujmy, w jaki sposób aktualizujemy konkretny przedział bazowy o nową wartość Jeśli
to nie robimy nic. Nawet najmniejsza wartość nie zostanie zmieniona na
Jeśli
to wszystkie elementy o wartości
zamienią się na elementy o wartości
czyli przypisujemy
oraz
Przypisujemy również
być może nadpisując mniejszą wartość
Jeśli
to rekurencyjnie aktualizujemy dzieci przedziału bazowego i w czasie stałym odzyskujemy z nich informacje o ojcu.

Otrzymując zapytanie o przedział rekurencyjnie rozkładamy go na przedziały bazowe. Jeśli w rekurencji natrafimy na przedział z wartością
to - jak wyżej - aktualizujemy jego dzieci. Następnie można odzyskać sumę albo zaktualizować owe przedziały bazowe i odzyskać wartości ich przodków w drzewie.
Sama poprawność algorytmu jest dość oczywista. Jednak skąd bierze się tak dobra złożoność czasowa? Nietrudno skonstruować przykład, w którym jedna operacja zajmie czas
Problemem są zejścia rekurencyjne, gdy
Oznaczmy ich liczbę przez
Poza nimi wszystko działa jak w zwykłych drzewach przedziałowych, więc nasz algorytm działa w czasie
Przejdźmy do dokładniejszej analizy algorytmu. Weźmy przedział bazowy Jeśli w jakimś nadprzedziale przypisaliśmy wartość
to w nim całym była tylko jedna wartość nie większa niż
w szczególności
było mniejsze niż
dla
Dowodzi to, iż wartości
zawsze pozostają aktualne. Z tego powodu przy propagowaniu wartości
nie natrafimy na przypadek
Całe
pochodzi więc z aktualizacji przedziałów bazowych o nowe wartości
Aby oszacować wartość w terminach
oraz
wprowadzimy pojęcie potencjału. Dla danego przedziału bazowego możemy policzyć, ile jest w nim różnych wartości. Potencjałem dla ciągu nazwijmy sumę tych wartości, liczoną po wszystkich przedziałach bazowych. Potencjał ten będzie największy, jeśli wszystkie wartości w ciągu będą różne. Będzie on wtedy wynosił tyle, ile suma długości przedziałów bazowych, czyli

Najpierw zastanówmy się, kiedy nasz potencjał może się zwiększyć za względu na dany przedział bazowy Przy operacji
zbiór wartości w
może się powiększyć co najwyżej o jeden element - o element o wartości
Aby tak się stało, musi zachodzić
w przeciwnym razie żaden element w
nie otrzyma nowej wartości
Ponadto przedział
nie może zawierać całego
w przeciwnym razie usuniemy wartość
ze zbioru wartości z
i potencjał się nie zwiększy. Liczba przedziałów bazowych, które przecinają się z
a nie są zawarte w
wynosi
a są to dokładnie te przedziały, które odwiedzamy rekurencyjnie, rozkładając
na przedziały bazowe. Ze względu na każdy z tych
przedziałów potencjał zwiększy się o co najwyżej jeden.
Nasz potencjał na początku wynosił co najwyżej a podczas każdej z
operacji wzrośnie o co najwyżej
a więc może też zmaleć co najwyżej
razy.
Wróćmy do naszego problematycznego przypadku, czyli gdy w aktualizowanym o wartość przedziale zachodzi
Wszystkie elementy o wartościach
oraz
będę po aktualizacji miały wartość równą
czyli liczba różnych wartości w aktualizowanym przedziale bazowym zmaleje i zmaleje też potencjał. W ten sposób ograniczyliśmy
przez
a cały algorytm, tak jak postulowaliśmy, zadziała w czasie