Informatyczny kącik olimpijski
Multizbiory
W tym kąciku proponuję zadanie polecane przez mojego korespondenta w Jekaterynburgu, mieście znanym również z turnieju Ural Sport Programming Championship, którego zeszłoroczną atrakcją był bezwzględny pojedynek pięciu najlepszych drużyn z Rosji z pięcioma najlepszymi drużynami z Chin. Popatrzmy na zadanie, którego nie udało się rozwiązać żadnej z nich!
Mamy dany ciąg liczb
Rozważamy jego wszystkie spójne
podciągi, czyli fragmenty postaci
które sortujemy
w dość dziwny sposób. Mianowicie, aby porównać dwa fragmenty
i
szukamy najmniejszej liczby
która występuje
różną liczbę razy w
i
Fragment
jest mniejszy
niż
(co zapisujemy jako
) dokładnie wtedy, gdy owa
liczba
występuje więcej razy w
niż w
Jeśli
takiej liczby nie ma, to fragmenty są dla nas takie same; innymi słowy
porównujemy multizbiory liczb występujące w obu fragmentach. Naszym
zadaniem jest wyznaczenie multizbioru, który jest generowany przez
-ty
fragment w tym dziwnym porządku.
Pewnie warto przez chwilę zwątpić, czy ten porządek jest faktycznie porządkiem,
czyli czy
implikuje, że
W przeciwnym
przypadku ciężko byłoby bowiem mówić o sortowaniu. Na szczęście,
okazuje się, że tak jest w istocie (
jest w rzeczywistości porządkiem
leksykograficznym na fragmentach potraktowanych jako multizbiory).
Ograniczenia podane w treści to
i
czyli
wygenerowanie i posortowanie wszystkich fragmentów nie jest najlepszym
z możliwych pomysłów. Ba, problematyczne byłoby już nawet samo
wygenerowanie fragmentów, nie mówiąc o tym, że porównanie dwóch
fragmentów wydaje się wymagać czasu proporcjonalnego do ich długości.
Zaczyna się robić ciekawie!
Co prawda, interesuje nas wyznaczenie
-tego fragmentu, ale może
wypadałoby urealnić oczekiwania i zacząć od próby skonstruowania
efektywnego sposobu zliczania fragmentów, które są ściśle mniejsze
od danego? Jest to dość standardowe podejście: zamiast rozwiązywać problem
„znajdź najmniejsze dobre rozwiązanie”, zajmujemy się problemem„sprawdź,
czy dane rozwiązanie jest dobre”. Potem zwykle wystarczy tylko zastosować
wyszukiwanie binarne, choć w naszym przypadku sytuacja okaże się odrobinę
bardziej skomplikowana.
Mając dany fragment
chcemy zliczyć fragmenty
dla których
Dość naturalne jest
ustalenie
i przyjrzenie się wszystkim
które spełniają żądany
warunek. Po chwili namysłu można dostrzec, że dodając kolejne elementy,
możemy tylko zmniejszyć
które ma więcej wystąpień, zatem
Wynika stąd, że
spełniające
warunek tworzą spójny przedział
Ale jak wyznaczyć
to
Pewnie moglibyśmy zacząć od
i zwiększać je
o jeden, dopóki
Brzmi to całkiem rozsądnie, choć
pojawiają się co najmniej dwa problemy. Przede wszystkim potrzebujemy
efektywnej metody na sprawdzanie, czy aktualne
należy zwiększyć
o jeden. Jest to jednak dość łatwe: trzeba tylko skonstruować strukturę danych,
która umożliwi nam przechowywanie dla każdego
różnicy
między liczbą jego wystąpień w
i liczbą jego wystąpień w aktualnym
fragmencie
oraz znajdowanie najmniejszego
dla
którego ta różnica nie jest zerem. Musi również umożliwiać dodawanie
nowych elementów
(co wiąże się ze zmniejszaniem odpowiadających
im różnic). Taką strukturą może być zwykłe drzewo licznikowe,
które umożliwia wykonywanie zarówno aktualizacji, jak i pytań
w czasie
Dysponując takim narzędziem, będziemy w stanie
wyznaczyć każde
w czasie
Niby fajnie,
ale w tym momencie pojawia się drugi problem: przecież mamy aż
różnych
które wypadałoby znaleźć! Na szczęście
można zauważyć, że
czyli dla kolejnego
możemy
zacząć od
Jest to o tyle wygodne, że wspomniane przed
chwilą drzewo licznikowe bez większych problemów pozwala także na
zwiększenie
o jeden (czyli na usunięcie
z aktualnego
fragmentu). Zatem dla kolejnych
zwiększamy
o jeden tak
długo, jak aktualny fragment jest nie mniejszy niż
a następnie
usuwamy
ze struktury. Ponieważ
wykonamy
nie więcej niż
operacji na drzewie licznikowym, co daje sumaryczny
czas
Fantastycznie.
Umiemy więc obliczyć, ile fragmentów jest mniejszych od
danego
Używając bardzo podobnej metody, można też zliczyć
fragmenty, które są równe
O ile więc tylko ktoś podrzuciłby nam
fragment
umielibyśmy sprawdzić, czy faktycznie jest on
-tym w naszym porządku (jeśli mamy
fragmentów, które są
mniejsze od
i
takich, które są równe
wystarczy
sprawdzić czy
). Niestety, nie mamy co liczyć na żadną
życzliwą podpowiedź.
Spróbujemy zastosować strategię przypominającą wyszukiwanie binarne.
Na dobry początek możemy stwierdzić, że szukany fragment na pewno znajduje
się (w zdefiniowanym wyżej porządku) między
a (dowolnym)
pustym fragmentem. Załóżmy więc, że wiemy już, iż szukany
leży
między fragmentami
a
Co dalej? Przydałby nam się
fragment
który leży mniej więcej w połowie drogi między
a
Mając
moglibyśmy szybko zliczyć fragmenty,
które są od niego mniejsze, porównać tę liczbę z
i w zależności
od wyniku zastąpić
lub
przez
(lub stwierdzić,
że właśnie
jest szukanym fragmentem). Dobranie się
do takiego
wydaje się jednak dość problematyczne, gdyż zakłada,
że znamy cały porządek na fragmentach, a przecież tak nie jest.
Wyobraźmy sobie zbiór wszystkich fragmentów, które leżą (w naszym dziwnym
porządku) między
a
Dla każdego
prawe końce
takich
tworzą spójny przedział
co więcej,
używając opisanej wyżej metody, możemy efektywnie wyznaczyć
wszystkie
i
(po prostu rozważamy wszystkie fragmenty
mniejsze niż
i odrzucamy te, które są również mniejsze
niż
). Można więc przedstawić interesujący nas zbiór jako
sumę
mniejszych zbiorów
(każdy z nich
jest tak naprawdę posortowany, choć nie będzie to dla nas istotne).
Ale co z tego?
Skoro nie wiemy, co zrobić, może warto wpaść w panikę i zrobić coś
losowego. Spróbujmy więc wybrać losowy spośród fragmentów, które
leżą między
a
Wystarczy tylko wylosować jeden ze
zbiorów
(jako że ich rozmiary mogą być dość różne,
to wybieramy
z prawdopodobieństwem
),
a następnie element w zbiorze. Wylosowawszy fragment
możemy
sprawdzić, czy szukany fragment jest mniejszy czy większy (a może równy)
od
A dlaczego to działa?
Wybierając losowy element, mamy sporą szansę, że liczba elementów między
a
istotnie się zmniejszy. Najłatwiej wyobrazić sobie sytuację,
korzystając z poniższej ilustracji, na której kolejne kropki reprezentują
elementy między
a
ułożone w kolejności zgodnej
z naszym dziwnym porządkiem. Powiedzmy, że jest ich
a szukany
element to kolorowa kropka na pozycji
Skoro wybieramy losowy
element, to z prawdopodobieństwem
będzie on w środkowym
okienku długości
Tak naprawdę sprawdzamy, czy wybrany element
jest na prawo czy na lewo od szukanego, i w zależności od wyniku
porównania pozbywamy się elementów na lewo lub na prawo od tego
losowo wybranego. A to jest bardzo wygodne: zawsze pozbędziemy się
przynajmniej lewego lub prawego kawałka długości
Czyli
mamy przynajmniej
szansy na to, że liczba elementów między
a
spadnie przynajmniej o jedną czwartą.

Skoro zaczynamy z
kandydatami, szansa na to, że konieczne okaże
się dużo więcej niż, powiedzmy,
iteracji, wydaje się niewielka.
Darujemy sobie szczegółowe rachunki, wymagają one bowiem pewnej
elementarnej wiedzy z rachunku prawdopodobieństwa: uwierzcie mi, że
oczekiwana liczba rund to
Otrzymaliśmy więc rozwiązanie
o oczekiwanym czasie działania
które w dodatku nie jest
bardzo skomplikowane implementacyjnie.