• Nie Znaleziono Wyników

W tym rozdziale poruszymy problematykę zwi¸azan¸a z teori¸a Literatura [SZP] - 4 liczb. Przedstawimy implementacje takich algorytmów, jak

wyz-naczanie największego wspólnego dzielnika, metodę szybkiego potę-gowania, czy wyznaczanie odwrotności modularnej. Zaprezentowane

zostan¸a również metody testowania pierwszości liczb oraz implementacja arytmetyki wielkich liczb, pozwalaj¸aca na wykonywanie operacji zarówno na liczbach całkowitych niemieszcz¸acych się w ograniczeniach standardowych typów zmiennych, jak również liczb wymiernych, reprezen-towanych w postaci ułamka nieskracalnego pq.

5.1. Współczynnik dwumianowy Newtona

Każdy zapewne zna definicję współczynniku dwumianowego Literatura [MK] - 5 Newtona — nk= k!(n−k)!n! . Istnieje wiele zadań, w których należy

wyznaczyć liczbę sposobów wyboru podzbioru jakiegoś zbioru, speł-niaj¸acego pewne dodatkowe założenia. W zadaniach tego typu

za-wsze pojawia się w którymś momencie współczynnik dwumianowy Newtona. Wyznaczenie wartości nk jest proste — wystarczy policzyć wartość liczby n!, a następnie podzielić j¸a przez k!(n − k)!. W tym miejscu jednak pojawia się pewien problem. W zadaniach często powiedziane jest, że obliczany wynik mieści się w pewnym standardowym typie zmiennych.

Nie gwarantuje to jednak, że wszystkie wykonywane pośrednie obliczenia nie spowoduj¸a przepełnienia wartości zmiennej. Taka sytuacja może mieć miejsce podczas wyznaczania wartości współczynnika dwumianowego Newtona — wartość n! może być istotnie większa od wyznaczanej wartości nk. Jednym z rozwi¸azań tego problemu jest zastosowanie arytmety-ki wielarytmety-kich liczb, ale nie jest to w wielu przypadkach najlepsze rozwi¸azanie, ze względu na efektywność oraz złożoność implementacji. Zmiana sposobu wyliczania wyniku może okazać się znacznie lepszym pomysłem — tak jest w przypadku współczynnika dwumianowego New-tona.

Sposób wyliczania współczynnika dwumianowego Newtona, który nie powoduje przepełnie-nia zmiennych, o ile sama wartość nk mieści się w arytmetyce, polega na wyznaczeniu rozkładu na liczby pierwsze poszczególnych liczb n!, k! oraz (n − k)!, a następnie wyko-naniu dzielenia poprzez skracanie czynników uzyskanych rozkładów. Implementacja takiego algorytmu została przedstawiona na listingu 5.1. FunkcjaLL Binom(int, int)przyjmuje jako parametry liczby n oraz k, a zwraca jako wynik wartość nk.

Listing 5.1: Implementacja funkcjiLL Binom(int, int) // Funkcja wyznacza wartość dwumianu Newtona

01LL Binom(intn, int k) {

02#define Mark(x, y) for(int w = x, t = 2; w > 1; t++) \ 03 while(!(w % t)) {w /= t; p[t] += y;}

04 if (n < k || n < 0) return 0;

05 int p[n + 1];

06 REP(x, n + 1) p[x] = 0;

// Wyznacz wartość liczby n!/(n-k)!=(n-k+1)*...*n w postaci rozkładu // na liczby pierwsze

07 FOR(x, n - k + 1, n) Mark(x, 1);

// Podziel liczbę, której rozkład znajduje się w tablicy // p przez k!

08 FOR(x, 1, k) Mark(x, -1);

// Wylicz wartość współczynnika dwumianowego na podstawie // jej rozkładu na liczby pierwsze i zwróć wynik

09 LL r = 1;

10 FOR(x, 1, n) while(p[x]--) r *= x;

11 return r;

12}

Listing 5.2: Przykład działania funkcjiLL Binom(int, int) Binom(40,20) = 137846528820

Binom(100000,3) = 166661666700000 Binom(10,23) = 0

Listing 5.3: Kod źródłowy programu użytego do wyznaczenia wyniku z listingu 5.2. Pełny kod źródłowy programu znajduje się w pliku binom.cpp

1 int main() { 2 intn, k;

// Dla wszystkich par liczb wyznacz wartość dwumianu Newtona 3 while(cin >> n >> k)

4 cout << "Binom(" << n << "," << k << ") = " << Binom(n, k) << endl;

5 return 0;

6 }

Ćwiczenia

Proste Średnie Trudne

acm.uva.es - zadanie 369 spoj.sphere.pl - zadanie 78 acm.uva.es - zadanie 10219 acm.uva.es - zadanie 10338

5.2. Największy wspólny dzielnik

Największy wspólny dzielnik (ang. greatest common divisor) Literatura [WDA] - 33.2

[SZP] - 4.5.3 [MD] - 4.6 [MK] - 4.1 [TLK] - I.2 dwóch liczb a i b (NW D(a, b)), jest to największa liczba

nat-uralna, która dzieli bez reszty obie liczby a i b. Przykładowo, N W D(10, 15) = 5, NW D(7, 2) = 1. Dwoma interesuj¸acymi włas-nościami największego wspólnego dzielnika, pozwalaj¸acymi na jego efektywne wyliczanie s¸a:

N W D(a, b) = a, dla b = 0

N W D(a, b) = NW D(a mod b, b), dla b 6= 0

Maj¸ac dane dwie liczby naturalne a oraz b, dla których należy wyznaczyć wartość na-jwiększego wspólnego dzielnika, wystarczy powtarzać proces zamiany ich wartości, przyp-isuj¸ac liczbie b wartość a mod b, a liczbie a wartość liczby b, dopóki b 6= 0. Po zakończeniu, wartość wspólnego największego dzielnika jest równa liczbie a. Łatwo można wykazać, że dwukrotne zastosowanie kroku zamiany powoduje co najmniej dwukrotne zmniejszenie sumy liczb a i b, a co za tym idzie, liczba wszystkich wykonywanych kroków jest logarytmiczna ze względu na sumę a + b.

Opisany algorytm znany jest jako wyznaczanie największego wspólnego dzielnika metod¸a Euklidesa. Realizuje go funkcja LL GCD(LL, LL), której implementacja przedstawiona jest na listingu 5.4.

Listing 5.4: Implementacja funkcji int GCD(int, int)

// Funkcja służąca do wyznaczania największego wspólnego dzielnika dwóch liczb 1 LL GCD(LL a, LL b) {

2 while(b) swap(a %= b, b);

3 return a;

4 }

Rozpatruj¸ac różne ważne własności największego wspólnego dzielnika, należy wspomnieć o tym, że dla każdej pary dwóch liczb naturalnych a oraz b, istniej¸a liczby całkowite l oraz k, takie że

N W D(a, b) = a ∗ l + b ∗ k

Fakt istnienia (a dokładniej możliwości obliczenia) tych dwóch liczb jest użyteczny pod-czas rozwi¸azywania wielu problemów, na przykład wyznaczania odwrotności modularnej, co stanowi temat kolejnego rozdziału. Wyznaczenie liczb l oraz k jest możliwe poprzez mody-fikację algorytmu Euklidesa. Załóżmy, że znamy wartości liczb l0 oraz k0, występuj¸ace w równaniu postaci:

n= (b mod a) ∗ l0+ a ∗ k0 Rozpatruj¸ac równanie postaci:

n= a ∗ l + b ∗ k

możemy uzależnić wartości zmiennych l i k od l0 i k0. Poszukiwane podstawienie ma postać:

( l= k0− babc ∗ l0 k= l0

Stosuj¸ac algorytm Euklidesa, dochodzimy pod koniec jego działania do równania postaci:

a= l ∗ a + k ∗ 0

zatem wartościami zmiennych l oraz k, spełniaj¸acymi to równanie, s¸a l = 1, k = 0. Cofaj¸ac wszystkie zamiany wartości zmiennych a oraz b wykonane przez algorytm Euklidesa oraz wykonuj¸ac za każdym razem odpowiednie podstawienia zmiennych l i k, otrzymamy w końcu poszukiwane współczynniki pocz¸atkowego równania:

N W D(a, b) = a ∗ l + b ∗ k

Algorytm realizuj¸acy tę metodę został zaimplementowany jako funkcja intGCDW(int, int, LL&, LL&), której implementacja została przedstawiona na listingu 5.5. Funkcja ta przyjmu-je jako parametry dwie liczby a i b, dla których wyznaczany przyjmu-jest największy wspólny dzielnik oraz referencje na dwie dodatkowe zmienne, którym przypisywane s¸a wyznaczane wartości współczynników l oraz k. Złożoność czasowa algorytmu nie uległa zmianie w stosunku do ory-ginalnej wersji algorytmu Euklidesa i wynosi O(log(a + b)). Funkcja int GCDW(int, int,LL&, LL&) jest rekurencyjna (w odróżnieniu od int GCD(int, int)), a co za tym idzie, zużycie pamięci jest również logarytmiczne.

Listing 5.5: Implementacja funkcji int GCDW(int, int,LL&, LL&)

// Funkcja wyznacza największy wspólny dzielnik dwóch liczb oraz współczynniki // l i k

01 intGCDW(inta, int b, LL & l, LL & k) { 02 if (!a) {

// gcd(0, b) = 0 * 0 + 1 * b

03 l = 0;

04 k = 1;

05 returnb;

06 }

// Wyznacz rekurencyjnie wartość największego wspólnego dzielnika oraz // współczynniki l oraz k

07 int d = GCDW(b % a, a, k, l);

// Zaktualizuj wartości współczynników oraz zwróć wynik 08 l -= (b / a) * k;

09 return d;

10}

Listing 5.6: Przykład działania funkcji int GCDW(int, int, LL&, LL&) gcd(10, 15) = 5 = -1*10 + 1*15

gcd(123, 291) = 3 = -26*123 + 11*291

Listing 5.7: Kod źródłowy programu użytego do wyznaczenia wyniku z listingu 5.6. Pełny kod źródłowy programu znajduje się w pliku gcdw.cpp

// Funkcja Pokaz wypisuje wynik wyznaczony przez funkcję GCDW // dla pary liczb a i b

01 void Pokaz(inta, int b) {

Listing 5.7: (c.d. listingu z poprzedniej strony) 02 LL l,k;

03 intgcd=GCDW(a,b,l,k);

04 cout << "gcd(" << a << ", " << b << ") = " << GCDW(a,b,l,k);

05 cout << " = " << l << "*" << a << " + " << k << "*" << b << endl;

06}

07 int main() { 08 Pokaz(10,15);

09 Pokaz(123,291);

10 return0;

11}