Professional Documents
Culture Documents
Uvek kada izvršavamo neku algebarsku operaciju, kao što je recimo množenje
dva broja ili njihovo stepenovanje, mi u stvari izvršavamo neki algoritam. Mi
te operacije koristimo kao gradivne elemente u razvijanju složenijih algoritama
i često ne zalazimo dublje u analizu njihove složenosti. Međutim, i sami al-
goritmi sabiranja, oduzimanja, množenja i deljenja brojeva (posebno ako su
brojevi dati nizovima svojih cifara) predstavljaju važne algebarske algoritme. U
algebarske algoritme spadaju i mnogi algoritmi sa kojima smo se već susreli kao
što su izračunavanje vrednosti broja na osnovu datih cifara ili, nasuprot tome,
određivanje cifara broja na osnovu njegove vrednosti, zatim razni algoritmi nad
polinomima kao što su izračunavanje vrednosti polinoma i množenje polinoma.
U ovom poglavlju bavićemo se algebarskim algoritmima sa kojima se do sada
nismo susreli. Mnogi od njih igraju važnu ulogu u oblasti kriptografije, ali i u
drugim oblastima.
Modularna aritmetika
1
unsigned int x = 123456789;
cout << x*x << endl;
Ukoliko je potrebno odrediti vrednost zbira ili proizvoda brojeva po modulu m
od pomoći mogu biti naredne relacije:
2
Važi i naredno tvrđenje:
Problem: Napiši program koji određuje poslednje tri cifre zbira i poslednje tri
cifre proizvoda četiri uneta cela broja manja od 999.
Ideja koja prirodno prva padne na pamet je da se izračunaju zbir i proizvod
unetih brojeva, a da se onda poslednje tri cifre odrede izračunavanjem ostatka
pri celobrojnom deljenju sa 1000. Kada se u obzir uzme raspon brojeva koji
se unose, takvo rešenje bi davalo korektne rezultate u slučaju zbira, međutim,
proizvod brojeva može biti mnogo veći od samih brojeva i zato je za izračunavanje
poslednje tri cifre proizvoda potrebno primeniti malo napredniji algoritam koji
koristi činjenicu da su za poslednje tri cifre proizvoda relevantne samo poslednje
tri cifre svakog od činilaca. Zato je pre množenja moguće odrediti ostatak pri
deljenju sa 1000 svakog od činilaca, izračunati proizvod, a onda odrediti njegove
tri poslednje cifre izračunavanjem njegovog ostatka pri deljenju sa 1000.
Funkcije za množenje i sabiranje po modulu zasnovaćemo direktno na prikazanim
relacijama, a onda ćemo ih primenjivati tako što ćemo proizvod (tj. zbir) po
modulu m svih prethodnih brojeva primenom funkcija kombinovati sa novim
brojem. Dakle, ako funkciju za sabiranje po modulu m označimo sa +m , a
množenje po modulu m sa ·m , izračunavaćemo ((a +m b) +m c) +m d tj. ((a ·m
b) ·m c) ·m d.
Ipak, naglasimo da je izračunavanje celobrojnog količnika i ostatka vremenski
zahtevna operacija (najčešće se izvršava dosta sporije nego osnovne aritmetičke
operacije), tako da je u praksi poželjno izbeći ih kada je to moguće. Na primer,
ako će zbir a + b biti moguće predstaviti odabranim tipom podataka, tada je
umesto (a mod m + b mod m) mod m ipak bolje koristiti (a + b) mod m.
int plus_mod(int a, int b, int m){
return ((a % m) + (b % m)) % m;
}
int main(){
int a, b, c, d;
cout << "Unesi cetiri broja" << endl;
cin >> a >> b >> c >> d;
cout << "Poslednje tri cifre njihovog zbira su "
<< plus_mod(plus_mod(plus_mod(a,b,1000),c,1000),d,1000) << endl;
cout << "Poslednje tri cifre njihovog proizvoda su "
3
<< puta_mod(puta_mod(puta_mod(a,b,1000),c,1000),d,1000) << endl;
return 0;
}
Moguća je i naredna optimizacija. Naime, na osnovu invarijante tekuća vrednost
je uvek u rasponu [0, 1000): ovo važi za ulazne vrednosti, a važi i za rezultat
funkcija plus_mod i puta_mod. Stoga nije neophodno računati ostatak tekuće
vrednosti pri deljenju sa 1000. Dakle, od tri pojave operacije % u funkcijama
plus_mod i puta_mod, dovoljno je ostaviti samo poslednju.
int plus_mod(int a, int b, int m){
return (a + b) % m;
}
4
}
// n je neparno
// prolazimo kroz neparne brojeve
for (int i = 3; i <= sqrt(n); i = i + 2){
// sve dok i deli n, stampamo i
while (n % i == 0){
cout << i << " ";
// azuriramo n
n /= i;
}
}
int main(){
int n;
cout << "Unesite broj n" << endl;
cin >> n;
ispisiProsteCinioce(n);
return 0;
}
Istaknimo jednu važnu stvar: naime, nigde u programu se posebno ne određuju
prosti brojevi. Na prvi pogled ovo može delovati zbunjujuće, međutim redosled
kojim tražimo proste faktore nam garantuje da nikada nećemo odštampati neki
složeni broj s = i · j, i, j < s jer ćemo pre ispitivanja deljivosti sa s proveravati
deljivost sa i i j, s obzirom da je njihova vrednost manja od s.
√
Složenost prikazanog algoritma je u najgorem slučaju O( n) – on se dešava kada
je n = p2 , gde je p neki prost broj ili kada je sam broj n prost. Primetimo da će
se za složene brojeve granica for petlje smanjivati sa svakim novim pronađenim
činiocem (jer će nova vrednost za n biti √ strogo manja od stare), te će broj
izvršavanja petlje uglavnom biti manji od n.
Ukoliko je potrebno veliki broj puta izvršiti faktorizaciju brojeva, npr. O(n)
puta, onda je potrebno O(n) √
puta izvršiti ispočetka ovaj algoritam, te će ukupna
složenost algoritma biti O(n n).
Faktorizaciju brojeva možemo izvršiti i korišćenjem Eratostenovog sita. Naime,
umesto da kao u originalnom algoritmu prilikom prolaska kroz umnoške nekog
5
prostog broja označavamo da su ti brojevi složeni, čuvaćemo vrednost najmanjeg
prostog činioca tog broja. Nakon toga izračunaćemo činioce broja n uzastopnim
deljenjem broja n njegovim najmanjim prostim činiocem.
const int MAX = 1000;
void eratosten(){
while (n != 1){
// stampamo najmanji cinilac broja n
cout << najmanjiCinilac[n] << " ";
// azuriramo vrednost n
n /= najmanjiCinilac[n];
}
cout << endl;
}
int main(){
int n;
cout << "Unesite broj n" << endl;
cin >> n;
eratosten();
ispisiProsteCinioce(n);
6
return 0;
}
Složenost prvog koraka algoritma – određivanja najmanjeg prostog činioca svih
brojeva do n jednaka je složenosti Eratostenovog sita O(n log log n), a ispisivanja
činioca datog broja je O(log n) jer je maksimalni broj činilaca broja n jednak
log2 n. Dakle, ukoliko je potrebno izvršiti faktorizaciju na proste činioce O(n)
puta onda će složenost ovog algoritma biti O(n log log n)+O(n log n) = O(n log n)
što je efikasnije od prethodnog pristupa.
Za veliko n prikazani algoritam nije dovoljno efikasan. Važi i sledeće: nije
jednako teško faktorisati sve brojeve jedne iste dužine. Najteže instance ovih
problema čine brojevi koji su proizvodi dva prosta broja. Kada su oba ova
prosta činioca velika i slične veličine, ovaj problem postaje jako teško rešiti. 2009.
godine okončao se dvogodišnji napor grupe istraživača da faktoriše broj od 232
cifre korišćenjem više stotina računa. Međutim, do danas nije ni dokazano da ne
postoji neki efikasan algoritam koji bi rešavao ovaj problem. Pretpostavljena
težina ovog problema čini srž velikog broja kriptografskih algoritama, kao što je
RSA.
Ojlerova funkcija
Ojlerova funkcija φ broja n označava broj prirodnih brojeva manjih ili jednakih
n koji su uzajamno prosti sa n. Na primer, φ(9) = 6 jer postoji šest brojeva
1, 2, 4, 5, 7, 8 manjih od 9 koji su uzajamno prosti sa 9.
Problem: Za dati prirodan broj n izračunati vrednost Ojlerove funkcije φ(n).
Direktan način da se reši problem bio bi da se redom prođe kroz sve brojeve od
1 do n − 1 i da se izbroji koliko je brojeva od njih uzajamno prosto sa n.
// funkcija koja racuna najveci zajednicki delilac dva broja
int nzd(int a, int b){
if (a == 0)
return b;
return nzd(b % a, a);
}
7
int main(){
int n;
cout << "Unesite n" << endl;
cin >> n;
cout << "fi(" << n << ")=" << ojlerovaFunkcija(n) << endl;
return 0;
}
Prilikom računanja vrednosti Ojlerove funkcije, funkcija za određivanje najvećeg
zajedničkog delioca se poziva O(n) puta. Kako znamo da je složenost određivanja
najvećeg zajedničkog delioca brojeva i i n jednaka O(log(i + n)) i kako je ovde
i < n, važi da je složenost računanja vrednosti nzd(i, n) jednaka O(log n), te je
ukupna složenost algoritma O(n log n).
Ojlerova funkcija ima primenu u teoriji brojeva, u algebri, a takođe igra ključnu
ulogu u definiciji RSA sistema za šifrovanje.
Razmotrimo sada efikasnije rešenje ovog problema. Iz same definicije Ojlerove
funkcije važi da je φ(1) = 1. Takođe, ako je n = pk , gde je p neki prost broj
onda sa njim nisu uzajamno prosti samo umnošci broja p, odnosno brojevi
p, 2p, 3p, pk−1 p kojih ima ukupno pk−1 . Dakle, važi
1
φ(n) = pk − pk−1 = pk−1 (p − 1) = pk (1 − )
p
Važi i naredno tvrđenje koje navodimo bez dokaza: φ(ab) = φ(a)φ(b) ako
su brojevi a i b uzajamno prosti. Funkcije za koje važi ovo svojstvo nazivamo
multiplikativne funkcije. Različite funkcije u teoriji brojeva su multiplikativne, što
omogućava da se na osnovu njihovih vrednosti za proste brojeve lako izračunaju
njihove proizvoljne vrednosti, što u kombinaciji sa efikasnim razlaganjem na
proste činioce daje efikasne algoritme.
Na osnovu prethodna dva tvrđenja, ako n u opštem slučaju predstavimo kao
n = pk11 · · · pkr r gde su p1 , p2 , . . . , pr prosti činioci broja n važi:
1 k2 1 1
φ(n) = φ(pk11 )φ(pk22 ) · · · φ(pkr r ) = pk11 (1 − )p2 (1 − ) · · · pkr r (1 − )
p1 p2 pr
1 1 1 Y 1
= pk11 pk22 · · · pkr r (1 − )(1 − ) · · · (1 − ) = n (1 − )
p1 p2 pr p
p|n
8
// rezultat inicijalizujemo na n
int br = n;
if (n % p == 0) {
while (n % p == 0)
n /= p;
br = br*(p-1)/p;
}
}
return br;
}
2
√ slučaja (kada je n = p , za neko prosto p ili ako je sam broj
Složenost najgoreg
n prost) je O( n).
p
S obzirom na to da važi br · p−1 = br · (1 − p1 ) = br − br
p , možemo izbeći množenje
u prethodnoj implementaciji tako što ćemo od n oduzeti broj umnožaka svakog
prostog činioca.
int ojlerovaFunkcija(int n){
// rezultat inicijalizujemo na n
int br = n;
if (n % p == 0){
while (n % p == 0)
n /= p;
// od br oduzimamo broj umnozaka cinioca p
br -= br / p;
}
}
// ako je n prost
9
if (n > 1)
br -= br / n;
return br;
}
Ukoliko bi zadatak bio da se izračuna vrednost Ojlerove funkcije
√ za sve vrednosti
manje ili jednake n, ova implementacija bi bila složenosti O(n n). Kao i ranije,
razmotrimo varijantu algoritma zasnovanu na Eratostenovom situ. Inicijalizo-
vaćemo sve vrednosti φ(i), 1 ≤ i ≤ n na i. Razmatraćemo redom vrednosti za
p počev od 2 do n i ukoliko je φ(p) = p, to znači da je broj p prost i vrednosti
Ojlerove funkcije svih umnožaka broja p pomnožićemo sa 1 − p1 .
void ispisiVrednostiOjleroveFunkcijeDoN(int n){
vector<int> ojlerovaFunkcija(n+1);
// za sve vrednosti od 2 do n
for (int p = 2; p <= n; p++){
int main(){
int n;
cout << "Unesite n" << endl;
10
cin >> n;
ispisiVrednostiOjleroveFunkcijeDoN(n);
return 0;
}
Složenost ovog algoritma odgovara složenosti Eratostenovog sita, odnosno jednaka
je O(n log log n).
Jedno važno svojstvo Ojlerove funkcije jeste Ojlerova teorema.
Ojlerova teorema: Ako je n ≥ 1 i a je ceo broj uzajamno prost sa n, onda važi
aφ(n) ≡ 1 (mod n)
ap ≡ a (mod p)
11
Primetimo da se delioci uglavnom javljaju u paru: u prethodnom primeru imamo
da je 24 = 1 · 24 = 2 · 12 = 3 · 8 = 4 · 6. Ovo ne važi samo u slučaju kvadrata nekog
broja, npr. delioci broja 16 su 1, 2, 4, 8, 16 gde srednji delilac nema svog para.
Nešto
√ efikasniji algoritam bi prolazio skupom svih brojeva manjih ili jednakih
n i za svaki broj d koji deli broj n inkrementirao bi ukupan broj delilaca za 2,
ako je d 6= nd , a inače za 1.
// funkcija koja racuna broj delilaca broja n
int brojDelilaca(int n){
int br = 0;
for (int i = 1; i <= sqrt(n); i++)
if (n % i == 0)
if (i != sqrt(n))
br += 2;
else
br++;
return br;
}
√
Složenost ovakvog algoritma bila bi O( n). Ukoliko bi bilo potrebno da√ovaj
postupak sprovedemo O(n) puta, ukupna složenost algoritma bila bi O(n n).
Neka je n = pk11 · · · pkr r , gde su p1 , p2 , . . . , pr svi prosti činioci broja n. Prosti
činioci svakog delioca broja n moraju biti iz skupa p1 , p2 , . . . , pr , te je opšti oblik
svakog delioca broja n jednak d = pl11 · · · plrr , gde važi 0 ≤ li ≤ ki , odnosno
postoji ki + 1 mogućnosti za eksponent činioca pi . Dakle, važi da je ukupan broj
njegovih delilaca jednak (k1 + 1)(k2 + 1) · · · (kr + 1).
Dakle ako bismo Eratostenovim sitom označili sve proste brojeve do neke vred-
nosti, onda bismo mogli da prolaskom kroz skup tih prostih brojeva proveravamo
da li dele n i ako ga dele k puta da ukupan broj pomnožimo sa k + 1.
void eratosten(vector<int> & prost){
int n = prost.size();
prost[0] = prost[1] = false;
// odredjujemo proste brojeve Eratostenovim sitom
for (int d = 2; d*d < n; d++)
if (prost[d])
for (int j = d+d; j<n; j+=d)
prost[j] = false;
}
12
// za sve vrednosti od 2 do n
for (int p = 2; p <= n; p++){
if (prost[p]){
int br = 0;
if (n % p == 0){
while (n % p == 0){
n = n/p;
br++;
}
brDelilaca = brDelilaca * (br + 1);
}
}
}
return brDelilaca;
}
int main(){
int n;
cout << "Unesi n" << endl;
cin >> n;
vector<int> prost(n+1,true);
eratosten(prost);
13
Malo efikasniji pristup bi delioce√otkrivao u paru, tako što bi prolazio skupom
svih brojeva manjih ili jednakih n i za svaki broj i koji deli broj n vrednost i i
vrednost njemu odgovarajućeg činioca √ n/i bi dodavao na ukupnu sumu, osim
kada je i = n/i, odnosno kada je i = n.
// funkcija koja racuna sumu delilaca broja n
int sumaDelilaca(int n){
int suma = 0;
for (int i = 1; i <= sqrt(n); i++)
if (n % i == 0)
if (i != sqrt(n))
suma += (i+n/i);
else
suma += i;
return suma;
}
Drugo rešenje istog problema bi se zasnivalo na sledećem razmatranju: neka
je n = pk11 · · · pkr r , gde su p1 , p2 , . . . , pr svi prosti činioci broja n. Opšti oblik
delioca broja n jednak je d = pl11 · · · plrr , gde važi 0 ≤ li ≤ ki , odnosno postoji
ki + 1 mogućnosti za eksponent činioca pi . Dakle važi da je suma svih delioca
jednaka:
int sumaDelilaca = 1;
// za sve vrednosti od 2 do n
for (int p=2; p<=n; p++){
if (prost[p]){
int br = 0;
if (n % p == 0){
while (n % p == 0){
n = n/p;
br++;
14
}
sumaDelilaca = sumaDelilaca * (pow(p,br+1) - 1)/(p-1);
}
}
}
return sumaDelilaca;
}
Iza ovih efikasnijih rešenja krije se to da su ove dve funkcije: broj delilaca i suma
delilaca multiplikativne, odnosno važi da je f (a · b) = f (a) · f (b) za uzajamno
proste brojeve a i b. Preciznije, σk (n) je funkcija delilaca i označava sumu
k-tih stepena svih pozitivnih delilaca broja n i za nju važi da je multiplikativna.
Specijalno, σ0 (n) označava broj delilaca, a σ1 (n) sumu delilaca broja n. Dakle,
ako sa B(n) označimo broj delilaca broja n, a sa S(n) sumu njegovih delilaca, onda
za uzajamno proste brojeve x i y važi B(x·y) = B(x)·B(y) i S(x·y) = S(x)·S(y)
(svaki činilac broja x može se iskombinovati sa svakim činiocem broja y i na taj
način dobijamo sve moguće činioce broja xy).
(pri deljenju ri−1 sa ri količnik je qi , a ostatak ri+1 ). Dobijeni niz je konačan jer
je opadajući i sastoji se od prirodnih brojeva. Neka je npr. rk+1 = 0 i neka je
rk 6= 0 poslednji član ovog niza različit od nule. Kako je
nzd(r0 , r1 ) = nzd(r1 , r2 ) = . . . = nzd(rk−1 , rk ) = nzd(rk , 0) = rk ,
vidimo da je d = nzd(a, b) upravo jednako rk , poslednjem ostatku različitom od
nule.
Uz malu dopunu, Euklidov algoritam se može iskoristiti i za rešavanje sledećeg
problema.
Problem: Najveći zajednički delilac d dva prirodna broja a i b izraziti kao
njihovu celobrojnu linearnu kombinaciju. Drugim rečima, odrediti cele brojeve x
i y, tako da važi d = nzd(a, b) = x · a + y · b.
Cilj je d izraziti u obliku celobrojne linearne kombinacije d = x · r0 + y · r1
ostataka r0 = a i r1 = b. Problem se može rešiti indukcijom. Polazi se od baze,
izraza za d u obliku linearne kombinacije ostataka rk−1 i rk−2 :
d = rk = rk−2 − qk−1 rk−1
15
Ovaj izraz je ekvivalentan pretposlednjem deljenju u Euklidovom algoritmu
(izraz (1) za i = k − 1).
Korak indukcije bio bi da se polazeći od izraza d = x0 ri + y 0 ri+1 kao celobrojne
linearne kombinacije ostataka ri i ri+1 , broj d izrazi u obliku celobrojne linearne
kombinacije ostataka ri−1 i ri , odnosno kao d = x00 ri−1 + y 00 ri . Zaista, zamenom
ri+1 u prethodnom izrazu sa ri−1 − qi ri , dobija se
odnosno:
x00 = y0
y 00 = x0 − qi y 0 (3)
16
return nzd;
}
int main(){
int a, b;
int x, y;
cout << "Unesite brojeve a i b" << endl;
cin >> a >> b;
int d = nzdProsireni(a, b, x, y);
cout << x << "*" << a << " + " << y << "*" << b
<< " = " << d << endl;
return 0;
}
Možemo napisati i odgovarajuću iterativnu verziju algoritma. Za r0 = n važi
da je x = 1 i y = 0, a za r1 = m važi x = 0 i y = 1. Želimo da ri+1
predstavimo kao celobrojnu linearnu kombinaciju brojeva a i b, ako znamo da
važi ri−1 = xi−1 ·a+yi−1 ·b i ri = xi ·a+yi ·b. Na osnovu veze ri+1 = ri−1 −qi ·ri
dobijamo:
// vrednost x i y za r_0 = n
int x_preth = 1;
int y_preth = 0;
// vrednost x i y za r_1 = m
int x_tek = 0;
int y_tek = 1;
while (b != 0){
// azuriramo vrednosti za a i b
// kao u standardnom Euklidovom algoritmu
int q = a/b;
int r = a - q*b;
a = b;
b = r;
17
x_tek = xpom;
int main(){
int a, b;
int x, y;
cout << "Unesite brojeve a i b" << endl;
cin >> a >> b;
int d = nzdProsireni(a, b, x, y);
cout << x << "*" << a << " + " << y << "*" << b
<< " = " << d << endl;
return 0;
}
Broj operacija je proporcionalan sa brojem operacija u Euklidovom algoritmu,
tj. jednak je O(log(a + b)).
Značaj proširenog Euklidovog algoritma je u tome što omogućuje rešavanja
jedne klase Diofantovih jednačina, tzv. linearnih Diofantovih jednačina koje
su oblika a · x + b · y = c, gde su a, b, c dati celi brojevi, a x i y su celi
brojevi koje treba odrediti. Bez smanjenja opštosti može se pretpostaviti da
je a, b > 0. Da bi navedena jednačina imala bar jedno rešenje, potrebno je
da d = nzd(a, b) deli c (pošto d deli levu stranu jednačine, mora da deli i
desnu). Ako je ovaj uslov ispunjen, jedno od rešenja lako se dobija opisanim
postupkom. Pošto se d izrazi u obliku d = ax0 + by 0 , množenjem sa celim brojem
c/d dobija se c = a(x0 c/d) + b(y 0 c/d), tj. vidi se da je jedno rešenje jednačine
par (x, y) = (x0 c/d, y 0 c/d).
Multiplikativni inverz broja a po modulu m je broj x za koji važi a·x ≡ 1 (mod m).
Na primer, multiplikativni inverz broja 3 po modulu 11 je 4 jer važi 3 · 4 ≡
18
1 (mod 11). Multiplikativni inverz po modulu m mora biti iz skupa vrednosti
{0, 1, 2 . . . , m − 1}.
Postavlja se pitanje da li modularni inverz uvek postoji. Pokazuje se da on
postoji ako i samo ako su brojevi a i m uzajamno prosti i tada je jedinstven.
Problem: Za data dva cela broja a i m odrediti multiplikativni inverz broja a
po modulu m.
Naivni pristup rešavanju ovog problema bi za x razmatrao redom brojeve od 1
do m i proveravao da li je ostatak pri deljenju broja a · x sa m jednak 1.
// multiplikativni inverz broja a po modulu m
int modInverz(int a, int m){
a = a % m;
// proveravamo redom kandidate od 1 do m-1
for (int x = 1; x < m; x++)
if ((a*x) % m == 1)
return x;
}
int main(){
int a, m;
cout << "Unesite broj a i broj m" << endl;
cin >> a >> m;
cout << "Modularni multiplikativni inverz broja " << a << " je "
<< modInverz(a, m) << endl;
return 0;
}
Složenost ove implementacije je O(m).
Da bismo dobili efikasniji algoritam koji radi u slučaju kada su brojevi a i
m uzajamno prosti, možemo iskoristiti prošireni Euklidov algoritam. Naime,
prošireni Euklidov algoritam određuje najveći zajednički delilac d i brojeve x i y
takve da za date brojeve a i b važi:
a·x+b·y =d
a·x+m·y =1
odnosno važi i:
19
a · x + m · y ≡ 1 (mod m)
a · x ≡ 1 (mod m)
int x, y;
// odredjujemo x i y tako da vazi a*x+m*y=d
int d = nzdProsireni(a,m,x,y);
int r = m;
int r1 = a;
x = 0;
int x1 = 1;
20
int q = r / r1;
int tmp;
tmp = r;
r = r1;
r1 = tmp - q*r1;
tmp = x;
x = x1;
x1 = tmp - q*x1;
}
21
Kineska teorema o ostacima
Prema jednoj kineskoj legendi, general Han Xin je da bi izbegao da špijuni saznaju
sa kolikom je vojskom krenuo u boj, svom kuriru je davao samo informaciju
o ostatku broja vojnika u pravougaonikoj formaciji. Njegove poruke su bile u
narednoj formi: “Kada se vojnici organizuju u redove od po 9, preostaje njih
4; ako su u redovima od po 11, niko ne preostaje; ako su u redovima od po 13,
samo jedan preostaje, a ako su u redovima od po 19, preostaje njih trojica”.
General je takođe svom kuriru preneo da je broj vojnika drugo najmanje rešenje
ovog problema. Pitanje je kolikim brojem vojnika je general zapovedao? Ovaj
problem odgovara tzv. Kineskoj teoremi o ostacima, jednom od standardnih
problema iz teorije brojeva. Formulišimo ovaj problem u standardnim terminima.
Problem: Data su dva niza brojeva r: r0 , r1 , . . . , rn−1 i m: m0 , m1 , . . . , mn−1
pri čemu za svaki par brojeva niza m važi da su uzajamno prosti. Odrediti
najmanji pozitivan broj x za koji važi:
x mod m0 = r0
x mod m1 = r1
...
x mod mn−1 = rn−1 (4)
int n = m.size();
// inicijalizujemo vrednost x
int x = 1;
int j;
// za svako i od 0 do n-1
// proveravamo da li je ostatak pri deljenju sa m_j jednak r_j
for (j = 0; j < n; j++)
if (x % m[j] != r[j])
break;
22
// ako su sve iteracije prosle uspesno
// nasli smo vrednost za x
if (j == n)
return x;
int main(void){
23
M M M
z0 = , z1 = , . . . , zn−1 =
m0 m1 mn−1
y0 ≡ z0−1 (mod m0 )
y1 ≡ z1−1 (mod m1 )
...
−1
yn−1 ≡ zn−1 (mod mn−1 )
w0 = y0 · z0 mod M
w1 = y1 · z1 mod M
...
wn−1 = yn−1 · zn−1 mod M
24
Druga jednačina važi na osnovu toga što je M mod mi = 0, treća na osnovu
toga što je yi · zi ≡ 0 (mod mj ) za j 6= i, a četvrta na osnovu yi · zi ≡ 1 (mod mi ).
Pretpostavimo da postoje dva različita rešenja u i v. Tada važi
long long M = 1;
// racunamo proizvod svih modula
for (int i = 0; i < n; i++)
M *= m[i];
rezultat = 0;
for (int i = 0; i < n; i++) {
long long zi = M / m[i];
long long zi_inv;
if (!modInverz(zi, m[i], zi_inv))
return false;
rezultat = zm(rezultat, pm(r[i], zi_inv, zi, M), M);
}
return true;
}
25
int main(){
// ucitavamo ulazne podatke
long long r[3], m[3];
for (int i = 0; i < 3; i++)
cin >> r[i] >> m[i];
// ispisujemo rezultat
cout << rezultat << endl;
return 0;
}
Postoji još jedan postupak manje matematički zahtevan i manje efikasan nego
prethodni. Pristup je zasnovan na pametnoj pretrazi. Ako broj r sa brojem n1
daje ostatak a1 onda je on sigurno jedan od članova aritmetičkog niza a1 , a1 + n1 ,
a1 + 2n1 , . . . U petlji redom proveravamo ove brojeve sve dok ne naiđemo na
prvi element koji pri deljenju sa n2 daje ostatak a2 . Kada pronađemo takav
broj a12 (on će biti najmanji pozitivan broj sa osobinom da pri deljenju sa n1 i
n2 daje redom ostatke a1 i a2 ), znamo da će svaki naredni broj koji zadovoljava
to svojstvo biti član aritmetičkog niza a12 , a12 + n1 · n2 , a12 + 2 · n1 · n2 , . . .
Redom pretražujemo elemente ovog niza dok ne naiđemo na element a123 koji pri
deljenju sa n3 daje ostatak a3 , a zatim posmatramo brojeve a123 , a123 +n1 ·n2 ·n3 ,
a123 + 2 · n1 · n2 · n3 i tako dalje.
26