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}