Jak wyznaczać wyrazy ciągu EKG?
Po przeczytaniu artykułu Marcina Pilipczuka trudno nie odnieść wrażenia, że nasz zasób wiedzy o zachowaniu ciągu EKG opiera się przede wszystkim na wynikach eksperymentów komputerowych, natomiast dowody otrzymanych w ten sposób hipotez pojawiają się z pewnym opóźnieniem.
Co więcej, niektóre własności nie doczekały się jeszcze ścisłych dowodów, choć dane doświadczalne pokazują je na tyle wyraźnie, że ciężko byłoby nie wierzyć w ich prawdziwość. Widzimy tu, że informatyka zaczyna pełnić funkcję eksperymentalnej poddziedziny matematyki. Co ciekawe jednak, nie chodzi tu tylko o łatwiejsze stawianie hipotez – wyniki obliczeń mogą stanowić fragmenty dowodu odkrytych za ich pomocą własności! W przypadku ciągu EKG przejawiło się to w automatycznym sprawdzeniu prawdziwości hipotezy dla pewnej liczby początkowych wyrazów ciągu (dla pozostałych zadziałało rozumowanie czysto matematyczne), ale zdarzały się już w historii problemy, dla których cały dowód opierał się na komputerowej analizie przypadków – na przykład dowód faktu, że każdą mapę (graf planarny) można pokolorować czterema kolorami, tak aby żadne dwa sąsiednie państwa (wierzchołki) nie miały tego samego koloru. Niektórzy twierdzą, że takie rozumowanie, oparte w znacznej mierze na programach komputerowych i często obszernych wynikach ich działania, nie jest właściwie żadnym dowodem. Jest to argument poniekąd słuszny – ciężko ręcznie zweryfikować poprawność tego typu wywodu. Ten medal ma jednak dwie strony. Otóż przypomnijmy sobie chociażby liczne „dowody” Wielkiego Twierdzenia Fermata, publikowane bez mała przez trzy stulecia, które nie używały żadnych podejrzanych metod informatycznych, a mimo wszystko okazywały się nieprawdziwe (niepełne). Czy autorzy tych „dowodów” wierzyli w słuszność przedstawianych wyników? Podejrzewam, że tak. Swoją drogą, zapewne większość Czytelników wierzy w prawdziwość dowodu Andrew Wilesa, mimo iż jest on tak długi i skomplikowany, że nie mieli ochoty go samodzielnie sprawdzić. Jeśli dołożymy do tego podobną konkurencję rozgrywaną w obecnych czasach – próby odpowiedzi na słynne pytanie: ?, jedno z fundamentalnych w informatyce teoretycznej, których to prób – „dowodów” – były już dziesiątki (zainteresowany Czytelnik znajdzie ich systematycznie aktualizowaną listę na stronie P versus NP) – to zauważymy, że czysto matematyczne rozumowania wcale nie muszą być lepsze i mniej błędogenne od rozumowań opartych na wynikach badań eksperymentalnych.
W duchu powyższego, trochę przydługawego wstępu w tym artykule zastanowimy się, jak efektywnie wyznaczać kolejne wyrazy ciągu EKG. Nasz algorytm powinien być na tyle sprawny, aby dało się za jego pomocą sprawdzić poprawność wyników eksperymentu Lagariasa, Rainsa i Sloane’a, czyli powinien działać co najmniej dla indeksów około (w tych okolicach powinna pojawić się największa liczba pierwsza mniejsza od ). A im lepszy algorytm, tym lepiej pozwoli nam badać asymptotykę ciągu EKG – a nuż uda się postawić kolejne ciekawe hipotezy?
Początek nie będzie zbyt odkrywczy: otóż metoda wyznaczania wyrazów ciągu EKG jest obecna implicite w tekście Marcina Pilipczuka. Faktycznie, jeśli znamy to wiemy, że jeden z dzielników pierwszych tej liczby jest liczbą sterującą wyrazu Jeśli właśnie liczba pierwsza jest tą liczbą sterującą, to jest najmniejszą wielokrotnością niewystępującą wcześniej w ciągu. Aby wśród wszystkich dzielników pierwszych zidentyfikować liczbę wystarczy, rzecz jasna, przejrzeć wszystkie te dzielniki i wybrać ten o najmniejszej pierwszej wolnej wielokrotności.
Aby uzupełnić ten opis postępowania, musimy wymyślić sposób efektywnego przejrzenia wszystkich dzielników pierwszych danej liczby naturalnej oraz sposób znalezienia najmniejszej wolnej wielokrotności każdego takiego dzielnika. Teraz zajmiemy się kolejno tymi dwiema lukami.
Problem rozkładu liczby naturalnej na czynniki pierwsze brzmi bardzo klasycznie: wydaje się, że powinien istnieć jakiś sympatyczny, efektywny algorytm dający sobie z nim radę. I faktycznie, jest to znany problem faktoryzacji liczby, lecz, niestety, w ogólności nie umiemy sobie z nim łatwo radzić – nie jest znany żaden wielomianowy algorytm rozwiązujący ten problem. Słowo wielomianowy oznacza tu algorytm o złożoności czasowej będącej jakimś wielomianem względem długości zapisu rozkładanej liczby, która to długość zapisu to mniej więcej logarytm z tej liczby.
Na szczęście, w naszej sytuacji nie musimy uciekać się do ogólnych rozwiązań problemu faktoryzacji. Wystarczy, jeśli przypomnimy sobie, że rozkładane przez nas liczby nie są zbyt duże, co w teorii wynika z górnego oszacowania na wartość -tego wyrazu ciągu, a w praktyce z wykresu wartości pierwszych elementów ciągu zawartego w artykule Marcina Pilipczuka.
W przypadku tak niedużych liczb możemy użyć metody sita Eratostenesa. Podstawowa wersja tego algorytmu, służąca do wyznaczania kolejnych liczb pierwszych, jest na tyle znana, że w tym miejscu ograniczymy się do przypomnienia jej w postaci pseudokodu:
Obliczamy tu elementy boolowskiej tablicy pierwsza[2.. M], wykreślając wielokrotności kolejnych liczb pierwszych, tj. oznaczając je jako liczby złożone. Żeby było szybciej, przeglądanie wielokrotności liczby pierwszej rozpoczynamy od gdyż skądinąd wiemy, że wszystkie mniejsze musiały już zostać wcześniej wykreślone. Na końcu mamy zaznaczone (wartość true) wszystkie liczby pierwsze nie większe niż Zapewne nie wszyscy wiedzą, że ten sam algorytm można wykorzystać także do rozkładania liczb na czynniki pierwsze. Wystarczy, mianowicie, dołożyć dodatkową tablicę dzielnik[2..M], w której będziemy przechowywać najmniejszy dzielnik pierwszy każdej liczby złożonej z zakresu. Elementy tej tablicy inicjujemy na zera, a wypełniamy ją w momencie wykreślania liczb, tzn. wewnątrz pętli while, za pomocą instrukcji:
Za pomocą tablicy dzielnik możemy już łatwo faktoryzować liczby nieprzekraczające : dla danej liczby jako pierwszy podajemy dzielnik dzielnik[i], następnie dzielnik[i/dzielnik[i]] i tak dalej, aż dojdziemy do jedynki. Takich kroków wykonamy nie więcej niż gdyż za każdym razem zmniejszamy co najmniej dwukrotnie. Dzięki temu, że w tablicy pamiętamy najmniejsze dzielniki pierwsze liczb, uzyskana w tym postępowaniu lista dzielników będzie zawsze niemalejąca, więc łatwo usuniemy z niej powtórzenia.
Stosując podobny argument (do tego z logarytmem), można wykazać, że złożoność czasowa sita Eratostenesa to co pozostawiamy Czytelnikowi (tak naprawdę złożoność tę można oszacować nawet przez ). Jeśli przypomnimy sobie, iż przy czym to indeks wyrazu ciągu EKG, który chcemy obliczyć, to słusznie zauważymy, że z problemem faktoryzacji poradziliśmy już sobie całkiem sprawnie.
Do zakończenia opisu całego algorytmu pozostała nam już tylko kwestia wyznaczania najmniejszych wolnych wielokrotności poszczególnych liczb pierwszych z rozkładu. Z tą częścią rozwiązania możemy poradzić sobie całkiem prosto, o ile zastosujemy odpowiednio sprytną strukturę danych, przy czym nie będzie to wcale struktura wysublimowana.
Znów skorzystamy z tego, że zakres interesujących nas liczb pierwszych jest nieduży. Dla każdej liczby pierwszej nie większej niż będziemy przechowywać, jako wiel, jakąś wielokrotność liczby która wystąpiła już w ciągu. Zadbamy o to, aby w algorytmie jedynie powiększać te wartości, aktualizując je tylko w razie potrzeby. Początkowo wiel[p]=0. Dodatkowo, w tablicy boolowskiej użyte[0..M] będziemy pamiętali informację o tym, które liczby wystąpiły już w ciągu EKG. Początkowo, dla wygody, ustawiamy użyte[0]= true, a resztę komórek na false. Okazuje się, że to już wystarczy – poniżej pseudokod wyznaczania najmniejszej wolnej wielokrotności liczby w którym, w gruncie rzeczy, nic się nie dzieje.
Powyższa pętla może i wygląda bardzo nieoptymalnie, ale nie jest w rzeczywistości taka zła. Otóż wystarczy zauważyć, że dana liczba może wystąpić jako wiel[p] dla nie więcej niż liczb pierwszych (będą to, oczywiście, dzielniki pierwsze liczby ). To oznacza, że łączna liczba obrotów powyższej pętli while z pewnością nie przekroczy Możemy już podsumować cały algorytm wyznaczania -tego wyrazu ciągu EKG. Używamy w nim zaledwie czterech tablic rozmiaru co najwyżej przy czym dwie z nich ( pierwsza i dzielnik) wypełniamy na samym początku w czasie a dwie pozostałe ( wiel i użyte) uzupełniamy w trakcie obliczania kolejnych wyrazów ciągu EKG, również w łącznym czasie W takim samym czasie możemy wyznaczyć wszystkie dzielniki pierwsze wszystkich liczb z podanego zakresu. Stąd całe rozwiązanie działa w czasie i w pamięci przy czym Algorytm szybki, a zarazem prosty – i o to w tym sporcie chodzi.
Można by jednak spytać, czy nie da się lepiej. W szczególności, w podanej metodzie przy wyznaczaniu musimy najpierw obliczyć wszystkie wcześniejsze wyrazy ciągu EKG – a może dałoby się obejść szybciej i bez tego? Podobnie jak Lagarias, Rains i Sloane, którzy w swojej pracy opisują algorytm zbliżony do powyższego, autor tego artykułu nie zna odpowiedzi na to pytanie, ale może Czytelnik ma jakiś pomysł? Jeśli tak, zachęcamy do podzielenia się nim z Redakcją.