Nu obții 100 de puncte sau ai nelămuriri în privința problemelor? Scrie-mi pe Instagram.
Ai găsit o greșeală, vrei să raportezi un utilizator sau vrei să comunici altceva? Folosește formularul de contact.
Vrei să ne transmiți o părere despre platformă? Folosește formularul de feedback.
Folosește următoarele shortcuturi pentru a naviga mai ușor pe platformă.
Meniu shortcuturi | ? |
Căutare probleme sau utilizatori | / |
Navigare printre rezultatele căutării | ↑, ↓ |
Meniu de contact și feedback | CTRL + Shift + F |
Ieșire din meniuri | Esc |
Setări editor | CTRL + Shift + S |
Schimbare stil editor | CTRL + Shift + E |
Șabloane de cod | CTRL + Shift + 1/2/3 |
Golire editor | CTRL + Shift + 4 |
Există situații în care vrem să memorăm în limbajele de programare precum C++ numere întregi foarte mari (cu sute de cifre, de exemplu), iar în aceste situații, tipuri de date int
și long long
nu sunt destul de încăpătoare pentru a memora astfel de valori (de pildă, long long
reține până la 19
cifre).
Din această nevoie a apărut conceptul de numere mari, adică o modalitate de a reține valori uriașe și de a efectua operații aritmetice cu acestea (precum adunarea, scăderea, înmulțirea, împărțirea, etc.). Vom numi în continuare scalar/număr mic un număr obișnuit, de tip int
sau long long
.
Operațiile aritmetice sunt ca cele din clasele primare, așadar se pot memora ușor logic, însă le vom și explica în această lecție. Fiecare operație va fi o funcție, pentru a le putea apela unde vrem în program cu ușurință.
Un număr mare este defapt un vector de cifre ce reține un număr natural. Mai precis, dacă a[]
este un număr mare, salvăm pe poziția a[0]
lungimea sa (numărul de cifre ale sale), iar pe pozițiile 1, 2, 3, …, a[0]
memorăm cifrele sale în ordine inversă.
Spre exemplu, dacă am vrea să memorăm numărul 302561
, vectorul ar arăta în felul următor:
i |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
a[i] |
6 |
1 |
6 |
5 |
2 |
0 |
3 |
Se respectă structura menționată mai devreme: avem a[0] = 6
cifre, cifra unităților este 1
, cifra zecilor este 6
, cifra sutelor este 5
și așa mai departe.
Motivul pentru care se memorează cifrele în ordine inversă este că se gestionează mult mai ușor în acest fel. Este mult mai ușor să știm mereu că pe poziția 1
reținem cifra unităților, pe poziția 2
reținem cifra zecilor și așa mai departe — altfel, cifrele acestea ar depinde de lungimea numărului, ceea ce este mai complicat.
Pentru a declara un număr mare, creăm pur și simplu un vector, unde lungimea depinde de numărul de cifre. Crearea unui număr mare de lungime 1000
este:
int numarMare[1003];
Unii preferă să creeze propria structură de date cu numere mari. Așadar, o alternativă este:
typedef int numarMare[1003];
…
numarMare a; //a este acum un vector număr mare cu 1000 de cifre
Un număr mare poate fi inițializat în două moduri.
Dacă avem un număr mic într-un int
și vrem să îl punem într-un număr mare, avem următoarea funcție:
void atribuire_nrmic(int a[], int b) {
//a <- b
a[0] = 0;
do {
int cif = b % 10;
b = b / 10;
a[0]++;
a[a[0]] = cif;
} while(b != 0);
}
Practic, resetăm lungimea numărului mare a
cu 0
, iar cât timp b
are cifre (folosind proprietățile cifrelor unui număr), punem ultima cifră a lui b
pe poziția ultimei cifre a lui a
, cifra zecilor și așa mai departe.
Dacă avem un număr mare b
și vrem să îl duplicăm într-un alt număr a
, avem următoarea funcție, care pur și simplu copiază tot vectorul dintr-unul în altul:
void atribuire_nrmare(int a[], int b[]) {
//a <- b
a[0] = b[0];
for(int i = 1; i <= a[0]; i++) {
a[i] = b[i];
}
}
Funcția de afișare a numerelor mari este foarte simplă: afișăm cifrele din vector, însă în ordine inversă, de la prima cifră către ultima:
void afisare_nrmare(int a[]) {
for(int i = a[0]; i >= 1; i--) {
cout << a[i];
}
}
Compararea a două numere mari este simplă, exact ca cea învățată în clasele mici:
Funcția de comparare a două numere mari returnează 0
dacă numerele sunt egale, 1
dacă primul număr este mai mare și -1
dacă al doilea număr este mai mare. Trebuie la început să ne asigurăm că vectorii memorează corect numărul de cifre (mai precis, că nu avem cifre de 0
la început, de pildă la numărul 00123 = 123
, lucru ce se poate întâmpla în timpul programului):
int comparare_nrmare(int a[], int b[]) {
//a == b -> 0 | a > b -> 1 | a < b -> -1
while(a[0] > 1 && a[a[0]] == 0) //avem cel puțin o cifră, iar prima cifră este 0
a[0]--;
while(b[0] > 1 && b[b[0]] == 0)
b[0]--;
if(a[0] > b[0]) return 1;
else if(a[0] < b[0]) return -1;
int lungime = a[0];
for(int i = lungime; i >= 1; i--) {
if(a[i] > b[i]) return 1;
else if(a[i] < b[i]) return -1;
}
return 0; //numerele sunt egale
}
Funcția de adunare așteaptă două numere a
și b
(cu număr variabil de cifre) și salvează, la final, în a
, suma dintre a
și b
. Suma este ca cea din clasele primare: luăm cifrele de la coadă la cap și adunăm cifrele pe rând. Dacă cifra curentă este 10
sau mai mult, atunci trebuie adunat 1
la următoarea cifră, iar pe poziția curentă luăm ultima cifră a sumei cifrelor. Pentru a evita posibilele erori, pentru numărul cu mai puține cifre, completăm cifrele de după cu 0
. Iată funcția:
void adunare_nrmare(int a[], int b[]) {
//a <- a + b
int lungime;
if(a[0] > b[0]) lungime = a[0];
else lungime = b[0];
for(int i = a[0] + 1; i <= lungime; i++)
a[i] = 0;
for(int i = b[0] + 1; i <= lungime; i++)
b[i] = 0;
a[0] = lungime;
int t = 0;
for(int i = 1; i <= a[0]; i++) {
a[i] = a[i] + b[i] + t;
t = a[i] / 10;
a[i] = a[i] % 10;
}
//este posibil să rămânem cu cifre în plus de adăugat (exemplu 999 + 1 = 1000, nu 000)
if(t != 0) {
a[0]++;
a[a[0]] = t;
}
}
Similar, scăderea se face cifră cu cifră începând cu ultima. În acest caz, presupunem din start că b ≤ a
, pentru că lucrăm cu numere naturale (putem verifica cu funcția comparare_nrmare
). Din nou, avem un transport t
, care asigură scăderi peste ordin (de pildă 1000 - 1 = 999
):
void scadere_nrmare(int a[], int b[]) {
//a <- a - b
for(int i = b[0] + 1; i <= a[0]; i++)
b[i] = 0;
int t = 0;
for(int i = 1; i <= a[0]; i++) {
a[i] = a[i] - b[i] - t;
if(a[i] < 0) {
t = 1;
a[i] += 10;
} else {
t = 0;
}
}
while(a[0] > 1 && a[a[0]] == 0) //dacă mai avem cifre de 0 la coadă, de exemplu 1000 - 1 = 0999 = 999
a[0]--;
}
Înmulțirea cu un număr mic se face la fel, ca în clasele mici. Când numărul mic are o singură cifră, rezultatul este simplu, iar un lucru interesant este că funcția merge și când numărul mic are mai mult de o cifră (funcționează la fel). Trebuie doar să ținem cont că transportul poate avea mai mult de o cifră, așadar, la final, în loc să verificăm cu un if
dacă transportul este gol sau nu, verificăm cu un while
, extragând mereu ultima cifră.
void inmultire_nrmic(int a[], int b) {
// a <- a * b
int t = 0;
for(int i = 1; i <= a[0]; i++) {
a[i] = a[i] * b + t;
t = a[i] / 10;
a[i] = a[i] % 10;
}
while(t != 0) {
a[0]++;
a[a[0]] = t % 10;
t /= 10;
}
}
Înmulțirea a două numere mari a
și b
se face, la fel, ca în clasele mici. Algoritmul în acest caz este mai dificil de înțeles, deci să vedem cum funcționează:
4 1 2 ×
7 4
a
cu fiecare cifră din b
, însă așezăm rezultatele deplasate, ca mai jos: 4 1 2 ×
7 4
----------------
16 4 8
28 7 14
4 1 2 ×
7 4
----------------
16 4 8
28 7 14
----------------
28 23 18 8
9
cu o poziție spre stânga: 4 1 2 ×
7 4
-------------------
16 4 8
28 7 14
-------------------
28 23 18 8
-------------------
3 0 4 8 8
Vedem că numărul de cifre ale rezultatului este în jur de a[0] + b[0] - 1
(depinzând de transport). Pentru ușurință, memorăm rezultatul într-un al treilea număr mare, c
.
void inmultire_nrmare(int a[], int b[], int c[]) {
//c <- a * b
c[0] = a[0] + b[0] - 1;
for(int i = 1; i <= a[0] + b[0]; i++) //golim vectorul c
c[i] = 0;
for(int i = 1; i <= a[0]; i++) {
for(int j = 1; j <= b[0]; j++) {
c[i + j - 1] += a[i] * b[j];
}
}
int t = 0;
for(int i = 1; i <= c[0]; i++) {
c[i] = c[i] + t;
t = c[i] / 10;
c[i] = c[i] % 10;
}
if(t != 0) {
c[0]++;
c[c[0]] = t;
}
}
Operația de împărțire este similară ca algoritmul de pe hârtie (învățat în casele mici). Luăm cifrele începând cu prima, găsim restul curent și calculăm rezultatul. Funcția întoarce restul și salvează în numărul mare câtul împărțirii întregi:
int impartire_nrmic(int a[], int b) {
//a <- a / b | se returnează restul
int r = 0;
for(int i = a[0]; i >= 1; i--) {
r = r * 10 + a[i];
a[i] = r / b;
r = r % b;
}
while(a[0] > 1 && a[a[0]] == 0) {
a[0]--;
}
return r;
}
Bazat pe operațiile menționate mai sus, se pot implementa alte operații mai complexe (ridicarea la putere, ridicarea la putere în timp logaritmic, împărțitrea a două numere mari, înmulțiri și împărțiri mai eficiente cu 10
și multe altele).
Cu toate acestea, numerele mari nu se folosesc foarte des, iar în situațiile în care se folosesc, numerele rezultat de obicei se construiesc doar cu operațiile menționate mai devreme.
Toate operațiile pe care le-am menționat mai sus folosesc operații în baza 10
, bază pe care o folosim în viața de zi cu zi. Cu toate acestea, nu este o necesitate să folosim această bază, dacă folosim operații cu alt număr în loc de 10
. De pildă, folosind o bază precum baza 100
, 1000
, …, putem să facem operațiile mai rapid, însă în detrimentul simplității. Trebuie avut grijă ca baza pe care o folosim în acest caz și operațiile folosite să nu depășească tipul de date int
, deoarece numerele mari sunt vectori de numere int
.
Completează următoarele secvențe de cod:
Să se elimine prima cifră a numărului mare a
:
a[???]--;
Să se calculeze suma cifrelor numărului mare a
în s
:
int s = 0;
for(int i = ???; i <= a[0]; i++) {
s += a[???];
}
cout << s;