Złośliwy problem (MAX , +) i kubełkowe struktury danych
Dany jest ciąg złożony z nieujemnych liczb całkowitych Na ciągu chcemy wykonywać operacje dwóch rodzajów:
- update – modyfikuje wartości wyrazów ciągu o indeksach z przedziału od do w sposób zależny od parametru (dodatniej liczby całkowitej);
- query – podaje zagregowaną wartość dla podciągu o indeksach od do
W obydwu przypadkach zakładamy, że
Będziemy rozważali dwie różne operacje update, które oznaczymy symbolicznie przez i W operacji należy do każdego wyrazu podciągu dodać liczbę Z kolei operacja polega na zmianie wartości wyrazu na – innymi słowy, jeśli wyraz miał wcześniej wartość nie mniejszą niż to jej nie zmienia, a jeśli jego wartość była mniejsza od to jego nową wartością jest
Podobnie definiujemy dwie operacje query: i Na zapytanie odpowiadamy, podając sumę wyrazów z podciągu Odpowiedzią na zapytanie typu jest natomiast wartość największego wyrazu w rozważanym podciągu.
Biorąc pod uwagę wszystkie kombinacje poszczególnych operacji, otrzymujemy cztery różne warianty problemu. Każdy taki wariant oznaczamy za pomocą pary, której pierwszy wyraz opisuje rodzaj operacji modyfikacji, a drugi określa zapytanie. Mamy zatem następujące warianty: oraz
Okazuje się, że nasz problem ma kilka ciekawych zastosowań. Wariant można wykorzystać do implementacji systemu obsługi rezerwacji miejsc w pociągu na trasie łączącej stacji. W pociągu znajduje się ustalona liczba miejsc siedzących. Każda rezerwacja dotyczy konkretnej liczby pasażerów i wskazuje numery dwóch stacji, na których pasażerowie zamierzają wsiąść i wysiąść. Naszym celem jest przyjmować kolejno wszystkie rezerwacje, które nie powodują przepełnienia pociągu.
Oznaczmy przez aktualną liczbę pasażerów, którzy będą w pociągu na trasie pomiędzy stacjami oraz Rezerwację na podróż ze stacji do stacji dla pasażerów możemy przyjąć, jeśli wartość query jest nie większa niż liczba miejsc w pociągu. Po przyjęciu rezerwacji wykonujemy update
Inaczej można na to spojrzeć jak na problem kontroli przesyłania danych między serwerami połączonymi jedną linią danych o określonej przepustowości albo problem obsługi pobierania danych z Internetu przez użytkowników sieci lokalnej w zadanych przedziałach czasowych (sieć ma ograniczony transfer).
Wariant ma natomiast elegancką interpretację kombinatoryczną powiązaną z popularną grą Tetris. Odpowiada on mianowicie sytuacji, w której klocki, które opadają na planszę, mają regularną strukturę (np. mają kształt prostokątów), a naszym celem jest, dla każdego klocka, orzec, w którym miejscu się on zatrzyma, jeśli spuścimy go z zadanej pozycji początkowej (nie mamy możliwości wykonywania w locie obrotów ani przesunięć). Z kolei wariant reprezentuje dynamiczny problem obliczania sum częściowych ciągu.
Trzy z podanych wariantów wyjściowego problemu mają zatem jasno określoną interpretację. Co więcej, znane są ich efektywne rozwiązania, w których koszt obsługi ciągu zapytań to (szukaną strukturą danych są tzw. drzewa przedziałowe, o których więcej można przeczytać lub posłuchać na stronie http://was.zaa.mimuw.edu.pl).
Za to czwarty wariant, jest inny. Jego interpretacja kombinatoryczna nie jest wcale aż tak jasna, a do tego nie znamy sposobu rozwiązania go w czasie (w tym przypadku klasyczne drzewa przedziałowe nie sprawdzają się). Okazuje się jednak, że można go w miarę prosto rozwiązać, korzystając z pewnej kubełkowej struktury danych. Nasze rozwiązanie będzie działało w czasie
Zacznijmy od podziału wyrazów ciągu pomiędzy kubełki, umieszczając w każdym kubełku, z wyjątkiem ostatniego, wyrazów. Jeśli ostatni kubełek jest mniejszy, możemy uzupełnić go sztucznymi wyrazami do rozmiaru Oprócz kolejnych wyrazów ciągu kubełek będzie przechowywał pewne dane pomocnicze. Zanim opiszemy strukturę kubełka, zaproponujmy trochę inny sposób myślenia o operacji update Zamiast mówić, że dla każdego takiego że wykonujemy można wyobrazić sobie, że dodajemy nowe ograniczenie dolne na liczby na pozycjach od do Wtedy wartość liczby w kubełku będzie równa maksimum z jej początkowej wartości oraz wszystkich ograniczeń dolnych jej dotyczących. Takie ograniczenia dolne będą trzymane osobno dla każdego kubełka.
Struktura kubełka będzie następująca:
- – tablica kolejnych wyrazów ciągu, które znajdują się w kubełku;
- – posortowana tablica liczb
- – sumy prefiksowe tablicy czyli
- – dolne ograniczenie na wszystkie wyrazy ciągu przechowywane w kubełku, czyli maksimum z wartości z operacji update dotyczących całego przedziału kubełka; aktualna wartość -tej liczby z kubełka to zawsze
Każdy kubełek umożliwia wykonywanie następujących operacji pomocniczych (szczegółowy opis ich implementacji znajduje się w dalszej części artykułu):
- 1.
- – obliczenie sumy aktualnych wartości wszystkich wyrazów ciągu zawartych w kubełku (działa w czasie );
- 2.
- – obliczenie sumy wyrazów o indeksach od do dla (działa w czasie );
- 3.
- – aktualizacja dolnego ograniczenia na całym przedziale do wartości co najmniej (działa w czasie );
- 4.
- – zwiększenie wyrazów o indeksach od do dla do wartości co najmniej (działa w czasie ).
Zauważmy teraz, że dowolny zakres indeksów od do można rozbić na pewną liczbę pełnych kubełków (oczywiście, nie więcej niż ) oraz na co najwyżej dwa niepełne kubełki (te skrajne). Dzięki temu zapytanie o sumę liczb na przedziale od do można podzielić na nie więcej niż zapytań oraz co najwyżej dwa zapytania Stąd każde takie zapytanie obsługujemy w czasie Operację modyfikacji ciągu na indeksach od do można także wykonać w czasie podobnie rozbijając cały przedział na nie więcej niż kubełków, na których wykonujemy operację i co najwyżej dwa brzegowe kubełki z wykonywaną operacją
Przykład 1. W tabeli przedstawiono, jak zmienia się przykładowa kubełkowa struktura danych w wyniku pojedynczej modyfikacji. Warto zwrócić uwagę, że w środkowym kubełku zmienia się tylko dolne ograniczenie, tj.
Pozostaje opisać, jak korzystając z przechowywanych danych, efektywnie wykonać cztery operacje pomocnicze oferowane przez kubełek.
Ostatnią pozycję znajdujemy, wyszukując binarnie. Zauważmy, że w tablicy jest dokładnie liczb mniejszych od oraz dokładnie liczb nie mniejszych niż Skoro aktualna wartość każdej liczby to więc suma aktualnych wartości wynosi (sumujemy najmniejszych liczb) plus suma największych liczb, czyli
W powyższej funkcji sumujemy aktualne wartości wyrazów z kubełka z określonych pozycji.
Aktualizacja dolnego ograniczenia na cały kubełek jest bardzo łatwa:
Jeśli pojawia się nowe dolne ograniczenie, które nie dotyczy całego zakresu obejmowanego przez kubełek, lecz jedynie jego części, należy zaktualizować liczby tylko z tego zakresu. Niestety, po takiej operacji tablice oraz stają się nieaktualne, więc obliczamy je ponownie. Oto zapis stosownego algorytmu:
Zaprezentowane rozwiązanie jest dosyć szybkie, jednak nie widać powodu, dla którego problemu nie można by rozwiązać jeszcze szybciej. Być może któremuś z Czytelników uda się skonstruować takie rozwiązanie?
Czytelnik, który do końca lipca 2013 roku przyśle do redakcji rozwiązanie opisanego problemu o najlepszej złożoności, nie gorszej jednak niż zostanie nagrodzony kilogramem szwajcarskiej czekolady w przesyłce prosto z Zurychu. W przypadku wielu rozwiązań o tej samej złożoności nagrodzimy subiektywnie najładniejsze, w przypadku tego samego najładniejszego rozwiązania – pierwsze spośród otrzymanych.