Intuicja
Zastanówmy się, co się dzieje, gdy maszyna wylosuje dużą liczbę pierwszą? Jeśli jest pierwsze, to wynik naszego zapytania będzie różny od tylko, gdy zapytamy o jakąś wielokrotność . Wśród liczb od do liczb pierwszych jest rzędu , więc w trakcie rozgrywki powinniśmy choć raz na jakąś natrafić. Co więcej, powinniśmy trafić na jakąś, która jest większa niż , co oznacza, że tylko pytanie o dokładnie tę liczbę pierwszą zwróci wynik różny od . Szanse na trafienie dokładnie tej liczby są nikłe, co oznacza, że w takiej sytuacji na pewno będziemy musieli szturchnąć maszynę, aby wylosowała ponownie.
Podsumowując, istnieje dosyć spory zbiór liczb, dla których jedynym rozwiązaniem jest wylosowanie ponownie. Stąd wnioskujemy, że musimy ograniczyć się do jakiegoś podzbioru liczb, które będziemy próbowali zgadnąć, a resztę porzucimy i wylosujemy ponownie.
Z perspektywy rozkładów na liczby pierwsze
Mając już pewną intuicję, postaramy się ją nieco sformalizować i uogólnić.
Popatrzmy na rozkład ukrytej liczby na liczby pierwsze. Oznaczmy jako kolejne liczby pierwsze od do . Zapiszmy jako następujący iloczyn: . Każdą liczbę od do możemy jednoznacznie przedstawić jako właśnie taki ciąg wykładników . Jak wygląda zatem pojedyncze zapytanie w kontekście ciągów wykładników? Powiedzmy, że zadajemy pytanie o . W odpowiedzi otrzymujemy . Nietrudno sprawdzić, że !
Wśród liczb do , liczbą o największej liczbie dzielników pierwszych jest . Ma ona dzielników pierwszych. Stąd widzimy, że ciąg na wszystkich poza co najwyżej pozycjami jest równy . Za to, patrząc z innej strony, nasze pytanie wskazuje mały podzbiór pozycji (co najwyżej ) i pyta dla każdej, czy wartość na tej pozycji jest większa niż . Tak naprawdę dowiadujemy się trochę więcej, ale nie musimy na to zwracać większej uwagi. Faktycznie, mając w ręku wszystkie liczby pierwsze w rozkładzie , możemy już dojść do tego, ile wynosi dokładnie , w co najwyżej pytaniach, zwyczajnie pytając o , przy czym dla każdej z tych liczb pierwszych. Od tej pory nie będziemy skupiać się za bardzo nad tym, jaki dokładnie jakaś liczba pierwsza ma wykładnik w rozkładzie , a na tym czy jest od większy od . Mając już takie spojrzenie na nasze zadanie łatwo nam zauważyć dlaczego nasza intuicja była trafna. Pojedyncza liczba ma ukryty co najwyżej kilkunastoelementowy podzbiór liczb pierwszych (a rozważanych liczb pierwszych jest więcej niż ) i naszym zadaniem jest odgadnięcie tego podzbioru za pomocą pytań o jego przecięcie z także co najwyżej kilkunastoelementowym podzbiorem liczb pierwszych. Z tej perspektywy problem wydaje się on nie do rozwiązania... Na szczęście liczby pierwsze pojawiają się w rozkładzie z różnym prawdopodobieństwem!
Liczby gładkie
Jak wcześniej zauważyliśmy, mając naszą ograniczoną liczbę zapytań, nie jesteśmy w stanie zapytać o przeważającą większość liczb pierwszych, które mogą pojawić się w rozkładzie liczby . Z tego powodu musimy ograniczyć się do małego podzbioru liczb pierwszych i liczyć na to, że posiada tylko te liczby w swoim rozkładzie.
Naturalnym wyborem podzbioru liczb pierwszych będą wszystkie liczby pierwsze mniejsze niż jakaś ustalona przez nas stała . Intuicja za tym jest taka, że występuje w co drugiej liczbie, w co trzeciej, a występuje w rozkładzie naprawdę niewielu liczb w porównaniu do . W tym momencie możemy wykonać lokalny test i sprawdzić, ile jest mniej więcej liczb, które w swoim rozkładzie mają tylko liczby pierwsze mniejsze niż . W naszym teście wystarczy, że np. milion razy wylosujemy liczbę i sprawdzimy, czy po wydzieleniu z niej rozważanych czynników pierwszych otrzymamy . Zliczamy liczbę takich prób i patrzymy jaki odsetek nam wyszedł. Dla około liczb składało się tylko z dzielników pierwszych mniejszych niż . Takie liczby nazywają się liczbami -gładkimi.
Skąd mieć pewność?
No dobrze, wiemy już, że całkiem często (jak na nasze potrzeby) wylosowana liczba jest -gładka. Ale skąd mamy mieć pewność, że liczba, na którą aktualnie patrzymy, nie ma żadnego dzielnika pierwszego większego niż ? Teraz postaramy się odpowiedzieć na to pytanie.
Dla ustalenia uwagi powiedzmy, że nasz algorytm jest następujący:
- Losujemy .
- Iterujemy się po wszystkich liczbach pierwszych mniejszych niż .
- Dla każdej liczby pierwszej zadajemy pytanie o , takie, że .
- Mnożymy wszystkie odpowiedzi.
Niech iloczyn wszystkich odpowiedzi wynosi . W tym momencie użyjemy kolejnej własności tego, że podzbiór liczb pierwszych, które wybraliśmy, to wszystkie mniejsze niż . W naszym algorytmie spytaliśmy o wszystkie liczby pierwsze mniejsze niż , więc wiemy, że jeżeli , to musi mieć jakiś dzielnik pierwszy przynajmniej . Ale jeśli zachodzi nierówność , to mamy pewność, że taka dodatkowa liczba pierwsza w rozkładzie nie istnieje i jest rzeczywiście równy !
Możemy teraz podliczyć, ile rozgrywek spodziewamy się wygrać, stosując taką strategię z parametrem . Rozpatrzenie pojedynczej liczby kosztuje nas zapytań, ponieważ dokładnie tyle jest liczb pierwszych mniejszych niż . Będziemy więc w stanie przejrzeć około ukrytych liczb i dla nich stwierdzić, czy jest ona -gładka. Z naszej empirycznej próby wynika, że takich liczb, nie większych niż , powinno być . Za takie rozwiązanie powinniśmy już otrzymać około punktów!
Wszystkie rozwiązania od teraz będą opierały się na wyżej przedstawionym algorytmie z modyfikacjami w celu optymalizacji liczby zapytań. Zamysł będzie zawsze taki sam, czyli będziemy sprawdzać liczby pierwsze, aż iloczyn będzie większy niż , przy czym oznacza najmniejszą niesprawdzoną jeszcze liczbę pierwszą.
Grupowanie zapytań
Wróćmy na chwilę do reprezentacji naszego problemu z sekcji Z perspektywy rozkładów na liczby pierwsze. Tam powiedzieliśmy, że mając informację o tym, które liczby pierwsze są w rozkładzie , wystarczy nam co najwyżej zapytań, aby "dokończyć" rozwiązanie. Pójdziemy tym tropem i zmodyfikujemy nasz algorytm. Od teraz będziemy pytać o wiele liczb pierwszych naraz, dobierając je w grupy. W jednej grupie iloczyn wszystkich liczb pierwszych będzie nie większy niż , aby można było zadać o niego pytanie. Liczby dobierzemy w grupy idąc od najmniejszej liczby pierwszej i dorzucając do grupy, jeśli warunek dla grupy będzie dalej spełniony po dodaniu. W przeciwnym przypadku będziemy tworzyć nową grupę. Teraz, w naszym algorytmie, będziemy iterować się po grupach i pytać o iloczyn wszystkich liczb w grupie. Następnie sprawdzimy, które liczby pierwsze z grupy występują z dodatnim wykładnikiem, i dla nich już zapytamy osobno o najwyższą potęgę mieszczącą się w . W ten sposób redukujemy liczbę pytań kilkukrotnie, co daje nam dużo dodatkowych punktów.
Wczesne przerwania
Kluczową optymalizacją pozwalającą na zdobycie maksymalnej liczby punktów są wczesne przerwania. Wyobraźmy sobie sytuację, w której po zapytaniu o wszystkie liczby pierwsze mniejsze niż iloczyn odpowiedzi wynosi (lub jakaś mała liczba). Czy warto kontynuować sprawdzanie tej liczby? Aby sprawdzić te liczby zadaliśmy kilka pytań (dzięki grupowaniu), a żeby dokończyć sprawdzanie będziemy musieli wydać jeszcze kilkaset. Nasz na razie nie rokuje dobrze, ponieważ na podstawie naszych informacji wiemy, że musi być iloczynem liczb pierwszych większych niż , a takich jest bardzo mało. W takiej sytuacji rozsądnym byłoby przedwczesne przerwanie naszych poszukiwań i wylosowanie na nowo.
Postarajmy się przekuć powyższą obserwację na optymalizacje w naszym algorytmie. Chcielibyśmy po każdym wykonanym przez nas zapytaniu podjąć decyzję, czy szturchnąć maszynę, czy kontynuować poszukiwania. Rozwiązanie tego problemu optymalnie przekracza nasze możliwości, więc postaramy się wycisnąć tyle, ile będziemy w stanie. Następne kilka sekcji będzie poświęcone opisowi rozwiązania wzorcowego i optymalizacji w nim zawartych.
Zarys modelu
W rozwiązaniu wzorcowym grupujemy liczby pierwsze jak wyżej opisano. Przeglądamy kolejno te grupy i dla każdej z nich najpierw wykonujemy zapytanie, a następnie dopytujemy o dokładne wykładniki liczb pierwszych. Dokładny sposób dopytywania zostanie opisany poniżej. Po wykonaniu pierwszego pytania w grupie będziemy podejmować decyzję czy przerwać; to samo zrobimy po odkryciu wszystkich wykładników. Pojedyncza decyzja będzie polegała na stwierdzeniu, czy nasz aktualny dzielnik jest większy niż przewidziany próg. Wszystkie takie progi będą parametrami naszego rozwiązania i niżej zostanie opisany sposób ich znalezienia. Jak wcześniej, jeśli w jakimkolwiek momencie będziemy mieli pewność, że znamy , to odpowiadamy i kończymy interakcję.
Obliczenie progów dla momentów decyzyjnych
W rozwiązaniu wzorcowym zastosowano podejście korzystające z metody wyżarzania (polecamy wersję angielską artykułu). Tylko czym jest funkcja, dla której obliczamy optimum? Argumentami funkcji są oczywiście wartości progów dla momentów decyzyjnych. W idealnym świecie wynikiem funkcji byłoby , przy czym oznacza liczbę ukrytych wartości , które odgadliśmy, a oznacza liczbę monet potrzebną do uzyskania takiego wyniku. Oczywiście wliczamy monety, które zostały wydane w próbach, które zakończyły się odgadnięciem ukrytej liczby, jak i tych próbach, które zakończyliśmy przelosowaniem ukrytej liczby. Te dwie wartości obliczylibyśmy uwzględniając wyniki dla każdej z możliwych ukrytych wartości . Niestety, nie posiadamy wystarczająco mocy obliczeniowej, żeby to zrobić. Z tego powodu będziemy musieli ograniczyć się do stałego podzbioru wartości , którego rozmiar jest dla nas akceptowalny. Bardzo ważnym jest aby ten zbiór był niezmienny, ponieważ wyniki funkcji dla tych samych argumentów i losowych podzbiorów mają dostatecznie duże wahania, aby nasz algorytm optymalizacji mógł się zaciąć na jakimś punkcie, ponieważ przy jego ewaluacji trafił mu się bardzo fartowny podzbiór wartości .
Warto wspomnieć, że od tego momentu nasz algorytm powinien pracować na logarytmach wartości progów momentów decyzyjnych a nie na ich rzeczywistych wartościach. Powód tej, na pierwszy rzut oka dziwnej, reprezentacji jest następujący: Przemnożenie wartości aktualnie znanego dzielnika przez jakieś jest podobnie trudne, niezależnie od tego jaka jest wartość tego dzielnika. Taka reprezentacja jest dużo bardziej wiarygodna niż reprezentacja, w której przejście z do ma "trudność" , a przejście z do ma "trudność" . Algorytm wyżarzania będzie zmieniał wartości progów o wartości w tej samej skali i z tego powodu pojedynczy krok powinien zawsze reprezentować zmianę tego samego rzędu wielkości. Alternatywnie, można by było zmodyfikować algorytm wyżarzania, aby mnożył wartości przez ustalony współczynnik zamiast go dodawać, ale zmienienie skali na logarytmiczną jest bardziej ogólną metodą i warto ją znać, aby nie musieć zaglądać do wnętrzności samego algorytmu optymalizacyjnego.
Po krótkiej dygresji wracamy do naszego algorytmu. Po uruchomieniu optymalizacji dla dwóch momentów decyzyjnych na każdy blok, czyli około kilku tysięcy parametrów, możemy zauważyć, że na naszym zbiorze treningowym (stałym podzbiorze wartości definiującym optymalizowaną funkcję) osiągamy zaskakująco dobre rezultaty. Jednakże, gdy uruchomimy nasze rozwiązanie na losowych testach, to wyniki nie są już takie czarujące... Skąd to wynika? Zjawisko, które zaobserwowaliśmy, w ogólności nazywa się przeuczeniem, co oznacza, że nasz zbiór parametrów jest potencjalnie zbyt duży, bo podczas "treningu" był w stanie "zapamiętać" jakie przypadki testowe kończą się sukcesem, a jakie nie, i przy tych drugich nauczył się je odcinać zbyt wcześnie niż byłoby to w ogólności optymalne. Aby zaradzić temu problemowi możemy znacząco zmniejszyć liczbę parametrów. Teraz pytanie na które musimy sobie odpowiedzieć, to które parametry pozostawić? W tym zadaniu dużo rzeczy możemy zrobić eksperymentalnie – popróbować różne możliwości i wybrać te, które najlepiej działają na losowych testach. W tym momencie przedstawię pewne intuicje stojące za dobrym doborem podzbioru parametrów, przy którym unikamy przeuczenia, a jednak pozostawiamy naszemu algorytmowi wystarczającą siłę wyrazu pozwalającą na sprawne przerywanie słabo rokującej próby.
Intuicyjnie, największa wartość powinna być w przerywaniu prób po wykonaniu kilku zapytań niż po wykonaniu kilkuset, ponieważ zaoszczędzamy potencjalnie znacznie więcej. Stąd wnioskujemy, że powinniśmy przydzielić więcej momentów decyzyjnych wcześniejszym grupom niż późniejszym. W szczególności, na podstawie pierwszego pytania, w którym pytamy o to, które z wykładników są niezerowe, będziemy przerywać wykonanie kilku lub nawet kilkunastu procent wszystkich prób! Rozumowanie za tym stojące jest takie, że jest aż kilka procent szans na to, że jest dużą liczbą pierwszą, a jak już wcześniej powiedzieliśmy, próba odgadnięcia takiej liczby skończy się źle. W rozwiązaniu wzorcowym przydzieliliśmy momenty decyzyjne dla około pierwszych grup na moment po pierwszym pytaniu w grupie, dla około pierwszych grup moment decyzyjny po poznaniu dokładnych wartości wykładników i około momentów decyzyjnych po poznaniu dokładnych wartości wykładników dla grup o wykładniczo rosnących numerach, czyli np.
Bonus: Dopytywanie o wykładniki
W tej sekcji skupimy się na jak najlepszym dopytywaniu o dokładne wartości wykładników, jeśli wiemy, że są one dodatnie. Można by było zastanawiać się, po co nam taka optymalizacja, skoro już sobie wcześniej powiedzieliśmy, że przy udanej próbie dodatkowych pytań będzie co najwyżej . Jednakże przy nieudanych próbach, które często trwają kilka pytań, różnica między zadaniem pytań, a zadaniem pytań to oszczędność rzędu !
Dla ustalenia uwagi, powiedzmy, że rozwiązujemy ten problem dla pierwszej grupy zawierającej liczby . Dla pozostałych grup będzie to wyglądało analogicznie. Powiedzmy, że liczby oraz występują z dodatnim wykładnikiem. Nasze pierwsze rozwiązanie najpierw by zapytało o , potem o , potem o , itd. Jednakże szansa na to, że będzie podzielne przez np. , to około . Zamysł jest teraz taki, że nie chcemy marnować "miejsca" w naszym pojedynczym zapytaniu na rzeczy, które mają bardzo małą szansę wystąpienia. Dużo lepszym pomysłem jest najpierw zapytanie o i następnie dopytanie o , i z osobna, jeśli ich wykładniki to odpowiednio przynajmniej , i , niż zapytanie o nie od razu. Zwyczajnie szansa na to, że po tym pierwszym pytaniu już nie będziemy musieli o nic dopytywać, jest znacznie większa niż szansa na to, że będziemy musieli dopytać o wszystkie trzy.
Mówiąc dokładniej, w pojedynczym kroku algorytmu wiemy, że dla liczb wykładniki spełniają nierówności . Zastanawiamy się, jakie pytanie powinniśmy skonstruować. Dla ustalonego , prawdopodobieństwo, że , jest równe około . Patrząc ogólniej, prawdopodobieństwo, że , jest równe około . Pojedynczym pytaniem zawierającym w sobie dowiemy się, czy zachodzi, czy może . Dla każdej sytuacji, w której otrzymujemy dokładną wartość wykładnika, możemy obliczyć nasz zysk jako , jeśli od teraz wiemy, że . Szansa na uzyskanie dokładnie takiego zysku to około . Pierwszy czynnik wygląda znajomo, a drugi czynnik bierze się z tego, że ten wykładnik nie może być większy. Zysk z zapytania obliczamy jako suma po iloczynach zysku i prawdopodobieństw poszczególnych zdarzeń. Nasz algorytm będzie tworzyć następne zapytanie, zachłannie zwiększając odpowiedni wykładnik liczby pierwszej w zapytaniu do wartości, która zmaksymalizuje różnicę w zysku podzieloną przez koszt tej zmiany, gdzie kosztem jest mnożnik, przez który musieliśmy przemnożyć nasze zapytanie.
W praktyce ten algorytm działa szybko, ponieważ, poza pierwszymi kilkoma grupami, rozmiary grup są bardzo małe (rzędu kilku elementów). Po zastosowaniu tej optymalizacji przy bardzo dobrym, wczesnym przerywaniu osiągamy zauważalny wzrost w liczbie wygrywanych rozgrywek. To zadanie pozostaje jako pytanie otwarte, a autorzy nie wiedzą jak dobre rozwiązania tego problemu są możliwe, więc zachęcamy do własnych poszukiwań! Rozwiązanie wzorcowe osiąga stabilnie ponad wygranych gier.