Przeskocz do treści

Delta mi!

Informatyczny kącik olimpijski

Multizbiory

Paweł Gawrychowski

o artykule ...

  • Publikacja w Delcie: maj 2014
  • Publikacja elektroniczna: 01-05-2014
  • Autor: Paweł Gawrychowski
    Afiliacja: Instytut Informatyki, Uniwersytet Wrocławski
  • Wersja do druku [application/pdf]: (227 KB)

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 math Rozważamy jego wszystkie spójne podciągi, czyli fragmenty postaci math które sortujemy w dość dziwny sposób. Mianowicie, aby porównać dwa fragmenty math  i  math szukamy najmniejszej liczby  math która występuje różną liczbę razy w  math  i  math Fragment  math  jest mniejszy niż  math (co zapisujemy jako math ) dokładnie wtedy, gdy owa liczba  math występuje więcej razy w  math  niż w  math 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 math-ty fragment w tym dziwnym porządku.

Pewnie warto przez chwilę zwątpić, czy ten porządek jest faktycznie porządkiem, czyli czy math  implikuje, że math  W przeciwnym przypadku ciężko byłoby bowiem mówić o sortowaniu. Na szczęście, okazuje się, że tak jest w istocie ( math  jest w rzeczywistości porządkiem leksykograficznym na fragmentach potraktowanych jako multizbiory). Ograniczenia podane w treści to math i  math 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 math-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  math  chcemy zliczyć fragmenty math dla których math  Dość naturalne jest ustalenie  math i przyjrzenie się wszystkim  math które spełniają żądany warunek. Po chwili namysłu można dostrzec, że dodając kolejne elementy, możemy tylko zmniejszyć  math które ma więcej wystąpień, zatem math Wynika stąd, że math spełniające warunek tworzą spójny przedział math Ale jak wyznaczyć to  math Pewnie moglibyśmy zacząć od math i zwiększać je o jeden, dopóki math  Brzmi to całkiem rozsądnie, choć pojawiają się co najmniej dwa problemy. Przede wszystkim potrzebujemy efektywnej metody na sprawdzanie, czy aktualne  math 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  math różnicy między liczbą jego wystąpień w  math  i liczbą jego wystąpień w aktualnym fragmencie math oraz znajdowanie najmniejszego  math dla którego ta różnica nie jest zerem. Musi również umożliwiać dodawanie nowych elementów  math (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 math  Dysponując takim narzędziem, będziemy w stanie wyznaczyć każde  math w czasie math  Niby fajnie, ale w tym momencie pojawia się drugi problem: przecież mamy aż math różnych  math które wypadałoby znaleźć! Na szczęście można zauważyć, że math czyli dla kolejnego  math możemy zacząć od math Jest to o tyle wygodne, że wspomniane przed chwilą drzewo licznikowe bez większych problemów pozwala także na zwiększenie  math o jeden (czyli na usunięcie  math z aktualnego fragmentu). Zatem dla kolejnych  math zwiększamy  math o jeden tak długo, jak aktualny fragment jest nie mniejszy niż  math  a następnie usuwamy  math ze struktury. Ponieważ math wykonamy nie więcej niż math operacji na drzewie licznikowym, co daje sumaryczny czas math  Fantastycznie.

Umiemy więc obliczyć, ile fragmentów jest mniejszych od danego  math  Używając bardzo podobnej metody, można też zliczyć fragmenty, które są równe  math  O ile więc tylko ktoś podrzuciłby nam fragment  math  umielibyśmy sprawdzić, czy faktycznie jest on math-tym w naszym porządku (jeśli mamy math fragmentów, które są mniejsze od  math  i  math takich, które są równe  math  wystarczy sprawdzić czy math). 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 math a (dowolnym) pustym fragmentem. Załóżmy więc, że wiemy już, iż szukany  math  leży między fragmentami math  a  math  Co dalej? Przydałby nam się fragment  math  który leży mniej więcej w połowie drogi między math a  math  Mając  math  moglibyśmy szybko zliczyć fragmenty, które są od niego mniejsze, porównać tę liczbę z  math i w zależności od wyniku zastąpić math  lub  math  przez  math (lub stwierdzić, że właśnie  math  jest szukanym fragmentem). Dobranie się do takiego  math  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 math  a  math  Dla każdego  math  prawe końce takich math tworzą spójny przedział math  co więcej, używając opisanej wyżej metody, możemy efektywnie wyznaczyć wszystkie math i  math (po prostu rozważamy wszystkie fragmenty mniejsze niż  math i odrzucamy te, które są również mniejsze niż  math ). Można więc przedstawić interesujący nas zbiór jako sumę math mniejszych zbiorów math (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 math  a  math  Wystarczy tylko wylosować jeden ze zbiorów math (jako że ich rozmiary mogą być dość różne, to wybieramy math z prawdopodobieństwem math), a następnie element w zbiorze. Wylosowawszy fragment  math  możemy sprawdzić, czy szukany fragment jest mniejszy czy większy (a może równy) od  math  A dlaczego to działa?

Wybierając losowy element, mamy sporą szansę, że liczba elementów między math a  math  istotnie się zmniejszy. Najłatwiej wyobrazić sobie sytuację, korzystając z poniższej ilustracji, na której kolejne kropki reprezentują elementy między math  a  math  ułożone w kolejności zgodnej z naszym dziwnym porządkiem. Powiedzmy, że jest ich  math  a szukany element to kolorowa kropka na pozycji  math Skoro wybieramy losowy element, to z prawdopodobieństwem  math będzie on w środkowym okienku długości  math 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  math Czyli mamy przynajmniej math szansy na to, że liczba elementów między math a  math  spadnie przynajmniej o jedną czwartą.

obrazek

Skoro zaczynamy z  math kandydatami, szansa na to, że konieczne okaże się dużo więcej niż, powiedzmy, math 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  math  Otrzymaliśmy więc rozwiązanie o oczekiwanym czasie działania math  które w dodatku nie jest bardzo skomplikowane implementacyjnie.