Przykład optymalizacji sita Eratostenesa w języku C++
Przykład optymalizacji sita Eratostenesa w języku C++
Algorytm sita Eratostenesa możemy optymalizować. Przeanalizujmy jeden z przykładów takiej optymalizacji.
Zanim przystąpimy do pisania kodu, trzeba zauważyć, że jedyną parzystą liczbą pierwszą jest 2 - każda kolejna liczba parzysta: 4, 6, 8, ... jest złożona.
Utwórzmy tablicę , składającą się z 26 komórek, której elementy będą przechowywać kolejne liczby nieparzyste, zaczynając od liczby 3. W języku C++ tablice indeksujemy od 0 – na potrzeby omawianego algorytmu komórkę kryjącą się pod tym indeksem wypełnimy wartością 0; pomoże nam to w lepszym zrozumieniu algorytmu. Tablica będzie więc składała się z następujących elementów: pod indeksem 1 będzie kryła się wartość 3. Możemy przyjąć, że: , ... .

Zauważmy, że dzięki wypełnieniu tablicy danymi od indeksu 1 możemy wprowadzić korelacje między numerem indeksu, a wartością, jaką ten indeks przechowuje.
Komórka o indeksie będzie przechowywać liczbę o wartości:
Np. komórka o indeksie 9 będzie przechowywać wartość , z kolei komórka o indeksie 15: .
Wyprowadzenie wzoru przedstawionego powyżej pozwala nam w łatwy sposób obliczyć indeks komórki, w której została zapisana wartość .
Ponownie rozpatrzmy kilka przykładów. Liczba 59 będzie przechowywana pod indeksem 29, ponieważ ; z kolei liczba 49 będzie przechowywana pod indeksem 24.
Na potrzeby niniejszego algorytmu wprowadźmy dwie zmienne indeksowane:
– indeks kwadratu liczby, której wielokrotność usuwamy z tablicy ,
– odstęp (liczba komórek w tablicy ) między kolejnymi wielokrotnościami, które będziemy wykreślać.
Algorytm zaprezentowany w poprzedniej sekcji tłumaczył, dlaczego pierwszą „wykreślaną” wielokrotnością jest kwadrat przetwarzanej liczby. Tablica pod indeksem 1 przechowuje liczbę 3 – jej kwadratem jest liczba 9, kryjąca się pod indeksem . Zauważmy, że kolejne wielokrotności liczbywielokrotności liczby 3 znajdują się w odległości – te liczby nie są liczbami pierwszymi; wykreślamy je.

Kolejną niewykreśloną liczbą jest 5. Jej kwadrat – 25 kryje się pod indeksem . Zauważmy, że wartość możemy przedstawić również jako (dokładne omówienie wzoru nastąpi poniżej). Dystans między kolejnymi wielokrotnościami liczby 5 wynosi i możemy go przedstawić jako . Ponownie wykreślamy wielokrotności.

Proces powtarzamy dla liczby 7. Jej kwadrat – 49 został zapisany pod indeksem (). Dystans między jej kolejnymi wielokrotnościami wynosi ().

Następna liczba (9) została wykreślona już wcześniej. Aby lepiej zrozumieć algorytm, omówmy i ją. Wskazujemy indeks komórki, w której umieszczono kwadrat liczby 9, czyli 81: (). Odległość między wielokrotnościami wynosi (). Zauważmy, że liczba 81 (kwadrat liczby 9) znajduje się poza zakresem tablicy . Jest to moment, w którym przerywamy algorytm.

Na podstawie przedstawionych rozważań możemy zbudować tabelę:
0 | 3 | 4 | |
1 | 3 + 2 = 5 | 2 * (5 - 1) = 8 | 4 + 8 = 12 |
2 | 5 + 2 = 7 | 2 * (7 - 1) = 12 | 12 + 12 = 24 |
3 | 7 + 2 = 9 | 2 * (9 - 1) = 16 | 24 + 16 = 40 |
Zauważmy, że kolejne powstaje poprzez dodanie do poprzedniej wartości liczby 2. Podobną zależność jesteśmy w stanie wyprowadzić dla – do poprzedniej wartości dodajemy .
W ten sposób dotarliśmy do momentu, w którym możemy wyprowadzić następujący wzór rekurencyjny:
,
,
dla każdego > :
.
Implementacja algorytmu
Implementacje algorytmu rozpocznijmy od zadeklarowania funkcji main, wczytania biblioteki iostream oraz poinformowania kompilatora o wykorzystaniu typów pochodzących z biblioteki standardowej.
Tworzymy zmienną n, która będzie przechowywać rozmiar generowanego sita. Zapisujemy w niej liczbę naturalną dodatnią pobraną od użytkownika.
Przedstawiony algorytm nie wymaga do swojego działania tablicy o długości n. Już na wstępie do omawiania algorytmu założyliśmy wykreślenie wszystkich liczb parzystych większych od 2 – potrzebujemy tylko połowy komórek, które użylibyśmy w przypadku niezoptymalizowanego algorytmu sita Eratostenesa.
Sprawdzamy, czy wprowadzona przez użytkownika liczba n jest nieparzysta. Jeżeli badany warunek jest prawdziwy, wówczas zwiększamy wartość n o 1.
Tworzymy zmienną pomocniczą polowa, którą wykorzystamy do tworzenia tablicy, a także jako warunek wykonania kolejnych pętli.
Następnie tworzymy tablicę wartości logicznych tab o długości polowa. Wypełniamy ją wartościami true. Dlaczego w komórkach tablicy przechowujemy wartości logiczne, skoro podczas omawiania algorytmu przez cały czas pokazywaliśmy liczby naturalne?
Zauważmy, że dzięki wyprowadzeniu korelacji między – indeksem komórki tablicy oraz – wartością przechowywaną w komórce, nie musimy zapisywać informacji o liczbie kryjącej się pod indeksem. Podobnie jak w przypadku niezoptymalizowanego algorytmu sita Eratostenesa, jeżeli komórka kryjąca się pod indeksem i przechowuje wartość false, oznacza to wykreślenie liczby k. Co więcej, sam algorytm opiera się na badaniu odległości między kolejnymi indeksami zapisanymi w tablicy – same liczby są wyłącznie dodatkową reprezentacją.
Dodajemy kolejne zmienne:
i– będzie wskazywać akualnie przetwarzany indeks tablicytab,d– będzie wskazywać odległość między kolejnymi wykreślanymi indeksami,a– będzie przechowywać pierwszą wielokrotność liczby kryjącej się pod indeksemi.
Zmienną d inicjalizujemy wartością 3, z kolei zmienną a wartością 4. Są to wartości początkowe wcześniej przedstawionego wzoru rekurencyjnego.
Dodajemy pierwszą pętlę – while. Będzie się wykonywać, dopóki nie przekroczymy badanego zakresu. W jej wnętrzu sprawdzamy, czy komórka kryjąca się pod indeksem i przechowuje wartość true – oznacza to, że badana liczba nie została wykreślona wcześniej, czyli jest pierwsza.
Jeżeli liczba kryjąca się pod indeksem i jest pierwsza, czyli przedstawiony warunek jest prawdziwy, musimy wykreślić jej wielokrotności. Tworzymy zmienną pomocniczą z, do której przypisujemy indeks pierwszej wielokrotności liczby kryjącej się pod indeksem i. Następnie w zagnieżdżonej pętli while ustawiamy wartość false kolejnym indeksom oddalonym o d.
Poza instrukcją warunkową zwiększamy wartość iteratora i. Zgodnie z przedstawionym wcześniej wzorem wyznaczamy nowe wartości zmiennych d oraz a.
Implementacje algorytmu kończymy, wypisując wszystkie niewykreślone liczby pierwsze, zaczynając od liczby 2. Wewnątrz pętli for sprawdzamy, czy wartość zapisana pod indeksem i przechowuje wartość true. Jeżeli warunek jest prawdziwy, wówczas liczymy wartość zmiennej k zgodnie z wcześniej przedstawionym wzorem i drukujemy na standardowym wyjściu.
Przykład działania kodu dla .
Słownik
liczba naturalna większa od 1, która dzieli się tylko przez jeden i przez samą siebie
algorytm, który przesiewa liczby z określonego zakresu w taki sposób, że zostają w nim tylko liczby pierwsze; służy on do znajdywania wszystkich liczb pierwszych w podanym przedziale
wielokrotność liczby a to taka liczba b, która powstaje przez pomnożenie liczby a przez liczbę naturalną n