1, 2, 3, ..., 1E100000

Aritmetică cu numere mari

Tiberiu Socaciu, Cluj

Încă din clasa întâi copilul ia cunoștință cu numerele. La început îi ajung cele zece degete de la mâini, pentru ca la absolvirea clasei întâi să poată deja manevra numere de ordinul miilor. Probabil că orice absolvent a cinci clase ar trebui să știe să opereze cu numere mult mai mari, chiar dacă nu "știe" să citească numere având peste 12-15 cifre. Dar acesta nu este un lucru care să descurajeze omenirea: numerele cu multe cifre au o largă aplicabilitate în știință și tehnică; și să nu uităm inflația...

Prezentăm în continuare câteva considerații legate de experiența proprie dobândită la implementarea aritmeticii cu numere mari.

Reprezentarea numerelor naturale

În funcție de precizia dorită propunem trei modalități de reprezentare în memorie a numerelor naturale:

1) reprezentarea în virgulă fixă

2) reprezentarea pe șir de bytes

3) reprezentarea ca șir de cifre

1) Reprezentarea în virgulă fixă

Este cea mai simplă modalitate de reprezentare a datelor întregi. Pentru aceasta se va folosi tipul întreg (eventual dublă precizie) de pe sistemul de calcul. Uzual dimensiunile de reprezentare sunt 2 sau 4 bytes, ceea ce ne asigură o plajă a valorilor distincte ce se pot reprezenta de 215, respectiv 231 valori pozitive (de obicei semnul se reprezintă pe un bit). Probabil că aceste plaje de valori pot mulțumi orice utilizator obișnuit.

2) Reprezentarea pe șir de bytes

Reprezentarea pe șir de bytes este de fapt o generalizare a reprezentării în virgulă fixă pe un număr de N bytes, adică pe un număr de 8*N biți, ceea ce ne asigură că se pot reprezenta 28*N valori distincte. În acest caz trebuie specificat care dintre cei N bytes este cel mai semnificativ și care dintre cei 8 biți ai unui byte este cel mai semnificativ (uzual cel mai din stânga).

3) Reprezentarea ca șir de cifre

Un număr natural X=u1 u2...un dat în reprezentarea în baza b (2) și având cifrele ui din mulțimea {0, ..., b-1} (i =1, ..., n), nu poate fi codificat în memoria calculatorului dacă n este mare (vezi paragrafele anterioare). Deci se impune memorarea lui X sub forma unui triplet de forma (b, n, L) unde:

  • b este baza sistemului de numerație
  • n este lungimea numărului X (numărul de cifre din X)
  • L este o listă liniară ale cărei elemente sunt cifrele lui X

Reprezentarea listei L se va face într-o zonă contiguă de memorie de dimensiune m bytes, astfel încât n Ł m. Prin urmare fiecare cifră se va memora pe un byte; evident numărul maxim care se poate reprezenta într-o listă de dimensiune m este Nmax (m, b)=bm -1.

Operații cu numere naturale

În cele ce urmează vom da algoritmii pentru principalele operații cu numere naturale (adunare, scădere, înmulțire, împărțire, comparare etc.) pentru cele trei metode de reprezentare.

1) Operații cu numere naturale reprezentate în virgulă fixă

Acesta este cel mai simplu caz, toate operațiile traducându-se direct în instrucțiuni ale limbajului suport, care la rândul lor sunt traduse în cel mult câteva instrucțiuni mașină adresate direct unității de calcul aritmetice și logice (ALU) din microprocesor (CPU).

2) Operații cu numere naturale reprezentate pe șir de bytes

Algoritmii prezentați aici sunt foarte eficienți, majoritatea lor putând fi implementați direct în limbaj de asamblare. Peste tot n semnifică numărul total de biți (evident el este multiplu de 8), iar ai, bi ... sunt cifrele binare de pe pozițiile i ale numerelor A, B etc. Algoritmii de înmulțire și de împărțire sunt preluați din [Szto], cu mențiunea că pentru înmulțire se poate implementa algoritmul a la russe prezentat și în [AnGa].

Algoritmul de adunare: Algoritmul de adunare este foarte simplu: se adună cifrele de același rang cu transportul de la adunarea de rang mai mic și se obține o nouă cifră a sumei, precum și transportul pentru următoarea adunare de rang mai mare. De remarcat că la prima adunare (cea corespunzătoare rangului unităților) transportul anterior (care în mod evident nu există, aceasta fiind prima adunare) trebuie inițializat cu 0 pentru omogenitatea buclei for.

Merită atenție specială cazul în care ultimul transport este diferit de 0. În acest caz apare fenomenul numit uzual depășire sau overflow.

Algoritmul de scădere: Algoritmul de scădere este evident asemănător celui de adunare. Cel mai dificil caz este cel în care se încearcă la scăderea de un anumit rang dintr-o cifră mai mică a unei cifre mai mari. Acest caz trebuie tratat asemănător cu transportul de cifră de la adunare. Acest lucru elevii îl numesc împrumut de la cifra de rang mai mare. Există și aici posibilitatea unei subdepășiri sau underflow.

Algoritmul de înmulțire: Pentru înmulțire s-a preferat algoritmul lui Booth care transformă înmulțirea în adunări și în scăderi. Considerând că avem de înmulțit numerele

și se poate scrie unde xn+1=0, în final obținându-se unde diferențele sunt în mulțimea {-1, 0, 1}, ceea ce determină să se adune sau să se scadă (eventual decalat cu un număr de biți) numărul Y.

Algoritmul de împărțire: Pentru împărțire prezentăm un algoritm foarte frumos, care este de regulă implementat hard în circuitele specializate pe aritmetică binară. Trebuie neapărat remarcată complexitatea liniară a acestui algoritm!

Intrare: Numerele X=X1...Xn, Y=Y1...Yn

Pasul 1: Y:=Y*2n

Pasul 2: R:=X

Pasul 3: Úpentru i:=0,n execută

ł R:=R-Y

ł Údacă R>=0

ł łÚatunci

ł łł Qi:=0

ł łł R:=R+Y

ł łÀț

ł ł altfel

ł ł Qi:=1

ł Àț

ł Údacă i:=n atunci

ł ł atunci R:=R*2

ł ł altfel Salt la Pasul 4

ł Àț

Àț

Pasul 4: R:=R*2-n

Ieșire: R,Q

Algoritmul de comparare: Evident că cea mai simplă modalitate de a compara două numere este compararea cifrelor de același rang de la stânga spre dreapta. În caz de egalitate se trece la compararea cifrelor de rang imediat mai mic. Dacă pentru un anumit rang am găsit că cifra curentă a unuia dintre numere este mai mare decât cifra curentă a celuilalt număr, putem spune că și între numere există aceeași relație. În cazul în care până la epuizarea cifrelor nu apare neegalitate, atunci putem spune că cele două numere sunt egale.

Alte operații: Celelalte operații care sunt implementate nu folosesc structura internă a datelor, ele apelând la operațiile primitive definite anterior. Aceste operații sunt calculul celui mai mare divizor comun și calculul celui mai mic multiplu comun (care se poate obține din cel mai mare divizor comun) și operațiile de conversie dintr-o bază în alta, mascate sub forma operațiilor de intrare-ieșire.

3) Operații cu numere naturale reprezentate ca șir de cifre

Au fost implementați algoritmii de adunare, scădere, înmulțire dați în [Knuth], iar pentru cel de împărțire a fost implementată îmbunătățirea dată în [Scala] a algoritmului similar dat în [Knuth]. De asemenea au fost implementate rutine de calcul al celui mai mare divizor comun și de comparare.

Algoritmii de adunare și scădere: Este evident că algoritmii de adunare și de scădere sunt analogi cu cei prezentați în cazul reprezentării pe biți, singurele diferențe fiind:

  • baza de numerație poate să difere;
  • nu mai apar operațiile de depășire și subdepășire, lungimea reprezentării operanzilor și a rezultatului fiind dinamică în timp (nu se rețin cifrele nule din fața numărului).

Algoritmul de înmulțire: Algoritmul de înmulțire cel mai simplu este bazat pe înmulțiri polinomiale. Astfel, fiecărui număr i se atașează un polinom ai cărui coeficienți sunt cifrele numărului. Evident că fiecare număr este valoarea polinomului atașat în baza dată. Deci după o înmulțire (ce comportă o complexitate pătratică) și o normalizare a coeficienților (un fel de "transport" ce are o complexitate liniară) se poate obține polinomul atașat rezultatului, de unde se poate obține rezultatul.

Algoritmul de împărțire: Distingem două cazuri:

- împărțitorul are o cifră;

- împărțitorul are mai mult de două cifre.

Am preferat "spargerea" algoritmului în doi algoritmi distincți din cel puțin două motive:

- cei doi algoritmi sunt bine individualizați;

- în caz contrar, algoritmul ar deveni "mai mare", oricum selectarea unuia din cei doi subalgoritmi trebuind făcută cu o instrucțiune de selecție simplă (un if).

Cazul împărțitorului cu o cifră: Prezentăm cazul împărțirii cu un număr format dintr-o singură cifră. O privire mai atentă asupra algoritmului ne va arăta că de fapt cele n iterații corespund determinării celor n cifre, prin clasica metodă de împărțire.

Intrare: A=a1...an, B în baza X

Pasul 1: i:=1; t:=0

Pasul 2: ci:=(t*X+ai) div B

Pasul 3: t:=(t*X+ai) mod B

Pasul 4: i:=i+1

Pasul 5: Údacă i>n

ł atunci Stop

ł altfel Salt la Pasul 2

Àț

Ieșire: c1...cn.

Cazul împărțitorului cu mai multe cifre: Prezentăm îmbunătățirea algoritmului din [Knuth] dată în [Scala]:

Intrare:A=a1...am+n, B=b1...bn, în baza X, b1<>0, n>1

Pasul 1: d:=X/(b1+1); A:=A*d

B:=B*d { Observație: se introduce încă o cifră a0 }

Pasul 2: j:=0

Pasul 3: Údacă aj=b1

ł atunci q?:=X-1

ł altfel

ł q?:=(aj*x+aj+1) div b1

ł Úcâttimp b2*q?>X*(a2*X+aj

-q?*bj+1)+aj+2 execută

ł ł q?:=q?-1

ł Àț

Àț

Pasul 4: W:=q?*B

Pasul 5: qj:=q?

Údacă aj...aj+n<W

ł atunci Salt la Pasul 6

ł altfel Salt la Pasul 7

Àț

Pasul 6: q?:=q?-1

W:=q?*B

qj:=q?

Pasul 7: aj...aj+n:=aj...aj+n-W

j:=j+1

Údacă j<=n

ł atunci Salt la Pasul 3

ł altfel Salt la Pasul 8

Àț

Pasul 8: rest:=0;

Úpentru i:=1,n execută

ł z:=X*rest+a

ł rest:=z mod d

ł ri:=z div di

Àț

Ieșire: Câtul Q=q0...qm, restul R=r1...rn

Algoritmul de comparare: În esență, algoritmul de comparare este identic cu cel dinainte, cu mențiunea că apare la început un test relativ la numărul de cifre. Dacă unul din numere are mai multe cifre decât celălalt, atunci acel număr este cu siguranță mai mare.

Calculul celui mai mare divizor comun: Cel mai simplu pentru implementare este algoritmul lui Euclid:

Intrare: A, B

Pasul 1: R:=A mod B

Pasul 2: Údacă R<>0

ł Úatunci

ł ł A:=B

ł ł B:=R

ł ł Salt la Pasul 1

ł Àț

Àț

Ieșire: R

Se știe (vezi [Knuth]) că deși este cel mai cunoscut și mai vechi algoritm nu este și cel mai performant. Pentru reprezentarea pe șir de bytes este însă de preferat implementarea algoritmului GCD binar. Acest algoritm iterativ încearcă coborârea valorilor absolute pentru care se calculează cel mai mare divizor comun prin folosirea proprietăților de paritate: (2a, 2b)=2(a, b), (2a, 2b+1)=(a, 2b+1), (2a+1, 2b)=(2a+1, b), unde testul de paritate se face de regulă prin testarea ultimului bit dacă este 0, iar trecerea de la un număr par la jumătatea lui se face printr-un shift la dreapta a întregii reprezentări cu 1 bit.

Calculul celui mai mic numitor comun: Pentru a calcula cel mai mic multiplu comun se poate folosi cu succes formula: (a,b)*[a,b]=a*b.

Conversia dintr-o bază în alta: Conversia dintr-o bază în alta se realizează prin algoritmul clasic de împărțire repetată la noua bază, toate calculele făcându-se în vechea bază.

Intrare: A în baza X, baza Y

Pasul 1: n:=0

Pasul 2: împarte pe A la Y și obține câtul C si restul R

Pasul 3: Údacă R=0

ł atunci Stop

ł altfel Salt la Pasul 4

Àț

Pasul 4: n:=n+1

cn:=R

A:=C

Salt la Pasul 2

Ieșire: c1...cn

Implementarea: Implementarea a fost făcută în limbajul Pascal și prezentată pe larg în [Socaciu94] și [Socaciu95b]. Cei interesați de obținerea surselor complete sunt rugați să contacteze autorul. Prin efectuarea de teste s-a ajuns la concluzia că raportul timp de calcul/număr maxim de cifre este în favoarea metodei de reprezentare ca șir de bytes, obținându-se un rezultat foarte apropiat de cazul aritmeticii cu întregi în virgulă fixă.

Calculul factorialelor: Programul FACTORIA.PAS calculează factorialele numerelor prin înmulțire, iar apoi prin împărțire verifică corectitudinea calculului. Prin verificare a rezultat că 146! are 255 de cifre. Cu titlu de exemplu oferim cele 158 de cifre ale lui 100!:

93326215443944152681699238856266700
49071596826438162146859296389521759
99932299156089414639761565182862536
97920827223758251185210916864000000
000000000000000000.

Calculul puterilor lui 2: Programul 2LAN.PAS calculează puterile lui 2 până la un exponent maxim dat făcând doar operații de adunare (pentru înmulțirea cu 2), iar apoi verifică calculele prin împărțiri succesive la 2. Prin verificare a rezultat că 2844 are 255 cifre. Cu titlu de exemplu oferim cele 31 de cifre ale lui 2100:

1267650600228229401496703205376.

Conjectura palindroamelor: Vom enunța Conjectura palindroamelor, așa cum se regăsește ea în [Gardner], după care vom prezenta programul pentru verificarea punctuală a conjecturii. Spunem că numărul natural: X=a1...an este un palindrom dacă ai=an-i, i=1, ..., n, adică cifrele sale sunt în oglindă. Astfel numărul 12321 este un palindrom. Dacă definim funcția oglindă:N?N dată de oglindă(a1...an )=an...a1, unde a1...an este un număr natural pentru care a1 <>0, atunci putem spune că palindroamele sunt punctele fixe ale aplicației oglindă, adică numărul natural X este palindrom <=> oglindă(X)=X. Fiind dat numărul natural X0 vom defini recursiv șirul (Xn )n>=0 astfel: Xn+1 =Xn+oglindă(Xn ). În [Gardner] se enunță următoarea conjectură (adică afirmație presupusă adevărată, încă nefiind demonstrat contrarul ei): "oricare ar fi X0 număr natural, există n natural, astfel încât Xn să fie palindrom", adică oricare ar fi numărul de pornire, după un număr finit de pași se obține un palindrom. După cum se observă, enunțul conjecturii nu depinde de baza sistemului de numerație ales. Se pare că totuși conjectura a fost rezolvată pentru cazul sistemului de numerație binar. Oferim în continuare rezultatul rulării programului PALINDRO.PAS pentru numărul 1993:

Pasul 1: 1993+3991=5984

Pasul 2: 5984+4895=10879

Pasul 3: 10879+97801=108680

Pasul 4: 108680+086801=195481

Pasul 5: 195481+184591=380072

Pasul 6: 380072+270083=650155

Pasul 7: 650155+551056=1201211

Pasul 8: 1201211+1121021=2322232

Problema nu este atât de simplă precum pare: există numere care pur și simplu se "prăbușesc" într-un palidrom după câteva iterații, dar există și numere care se lasă foarte greu "dobândite" lăsând impresia, după un anumit număr de pași, că nu ar fi adevărată conjectura. O astfel de bătălie s-a dat pentru numărul 196.

Ce mai putem face?

Răspunsul ar fi: multe. Evident că în funcție de problema practică ce urmează a fi rezolvată se pot "inventa" multe lucruri. Amintim câteva alte aplicații posibile, o parte din ele fiind prezentate și în anexele din [Socaciu96b]:

  • aritmetică cu întregi mari
  • calculator "științific" întreg cu posibilități de calcul și conversie în diferite baze de numerație.

Extinderi...

Evident că, în funcție de cerințele problemei se pot construi ierarhii de clase care să pornească de la aceste idei. Vă prezentăm câteva extinderi care au fost prezentate și implementate în [Socaciu95], [Socaciu96a], [Socaciu96b]:

  • aritmetică cu raționali;
  • algebră liniară cu raționali;
  • algebră cu complecși întregi.

...spre calculul simbolic

De regulă toate sistemele de calcul simbolic au implementați și așa numiții algoritmi seminumerici (cu alte cuvinte au motoare de lucru cu numere mari). Noi am folosit această aritmetică pentru modulul de calcul simbolic cu expresii Poisson care nu sunt altceva decât generalizări ale polinoamelor algebrice și ale polinoamelor trigonometrice (mai exact o sumă algebrică de termeni polinomiali ce pot fi însoțiți de o formă trigonometrică dintr-o combinație liniară întreagă și precedată de un coeficient rațional).

Referințe

[AnGa] Răzvan Andonie, Ilie Gârbacea, Algoritmi fundamentali. O perspectivă C++, Ed. Libris, Cluj-Napoca 1995

[Gardner] Martin Gardner, Ha ha ou la comprehension de la mathematique, Ed. Belin, Paris.

[Knuth] Donald E. Knuth, Tratat de programarea calculatoarelor. Algoritmi fundamentali (volumul I). Algoritmi seminumerici (volumul III)., Editura Tehnică, București 1983.

[Scala] Robert-Michel di Scala, SYPAC: Un system experimental de calcul formel en Pascal, Thèse de 3-ième cycle, l?Université Scientifique et Médicale de Grenoble.

[Socaciu94] Tiberiu Socaciu, POISSON - Sistem de calcul simbolic, Comunicare la Colocviul Național de Informatică INFO-IAȘI JUNIOR ?94, IAȘI 7-9 aprilie 1994

[Socaciu95a] Tiberiu Socaciu, Rezolvarea unor probleme de Mecanică cerească cu ajutorul procesorului POISSON, Comunicare la Colocviul Național de Informatică INFO-IAși JUNIOR ?94, IAșI 5-7 aprilie 1995

[Socaciu95b] Tiberiu Socaciu, Calcul simbolic simbolic cu aplicații în Mecanica Cerească, teză de licență, Universitatea "Babeș-Bolyai" Cluj, Facultatea de Matematică și Informatică, 1995

[Szto] I.Sztojanov și alții, De la poarta TTL la MICROPROCESOR, Editura Tehnică, București 1987

Domnul Tiberiu Socaciu este licențiat și master al Facultății de Matematică și Informatică a Universității "Babeș-Bolyai" Cluj-Napoca. Lucrează ca inginer de sistem la S.C. SAR Transilvania Asigurare-Reasigurare S.A. Cluj-Napoca. Poate fi contactat prin email: infodata@theoffice.net

Listing Doi_la_n.PAS

Listing TEST.PAS

Listing CONJECT.PAS

[cuprins]