Documente Academic
Documente Profesional
Documente Cultură
Curs Logica Computationalapdf PDF
Curs Logica Computationalapdf PDF
CUPRINS
Prefaţă ..................................................................................vii
1. Introducere .......................................................................... 11
1.1. Noţiuni despre limbaj ........................................................................13
1.2. Noţiuni despre notaţia asimptotică..................................................14
2. Algoritmi de sortare............................................................. 21
2.1. Bubble sort .........................................................................................23
2.2. Insertion sort ......................................................................................24
2.3. Quicksort ............................................................................................27
2.4. Merge sort ..........................................................................................33
2.5. Heapsort .............................................................................................37
2.6. Counting sort......................................................................................45
2.7. Radix sort............................................................................................48
2.8. Concluzii .............................................................................................55
3. Tehnici de programare......................................................... 57
3.1. Recursivitate ......................................................................................59
3.2. Backtracking .......................................................................................68
3.3. Divide et impera ................................................................................82
3.4. Greedy ................................................................................................89
3.5. Programare dinamică ........................................................................96
4. Algoritmi matematici ......................................................... 109
4.1. Noţiuni despre aritmetica modulară ............................................. 111
4.2. Algoritmul lui Euclid........................................................................ 112
4.3. Algoritmul lui Euclid extins............................................................. 114
4.4. Numere prime ................................................................................. 116
4.5. Algoritmul lui Gauss........................................................................ 130
4.6. Exponenţierea logaritmică ............................................................. 136
4.7. Inverşi modulari, funcţia totenţială ............................................... 143
4.8. Teorema chineză a resturilor ......................................................... 145
4.9. Principiul includerii şi al excluderii ................................................ 150
iii
Algoritmică
iv
Cuprins
vi
Prefaţă
Prefaţă
Această carte este utilă tuturor celor care doresc să studieze
conceptele fundamentale ce stau la baza programării calculatoarelor,
îmbinând principalele direcţii de cercetare pe care un viitor programator sau
absolvent al domeniului informatică ar trebui să le parcurgă şi să le
cunoască.
Cartea este concepută ca o colecţie de probleme demonstrative a
căror rezolvare acoperă elemente de programare procedurală, tehnici de
programare, algoritmi şi structuri de date, inteligenţă artificială şi nu în
ultimul rând programare dinamică.
Pentru fiecare problemă în parte sunt construiţi algoritmii clasici de
rezolvare, completaţi cu explicaţia funcţionării acestora, iar în completare,
acolo unde este necesar, problemele dispun şi de prezentarea noţiunilor
teoretice, a conceptelor generale şi particulare aferente construirii unui
algoritm optimizat.
Organizare
vii
Algoritmică
viii
Prefaţă
Convenţii utilizate
ix
Algoritmică
Despre Autori
La data publicării acestei cărţi
x
Introducere
1. Introducere
Acest prim capitol are ca scop familiarizarea cititorului cu
elementele constructive ale cărţii, cunoştiinţele iniţiale necesare înţelegerii
materialul de faţă, convenţiile de scriere şi prezentare a secvenţelor de cod,
tot aici sunt cuprinse noţiunile generale privind analiza complexităţii
algoritmilor prin studiul timpului de execuţie şi cantităţii de memorie
utilizată de către aceştia (notaţia asimptotică).
11
Capitolul 1
CUPRINS
12
Introducere
13
Capitolul 1
14
Introducere
Secvenţa 1
Secvenţa 2
15
Capitolul 1
𝑵∙(𝑵−𝟏)
= 0.5 ∙ 𝑁 2 − 0.5 ∙ 𝑁.
𝟐
Secvenţa 3
...
int st[maxn], k = 1;
...
for ( int i = 1; i <= N; ++i )
{
while ( st[k] >= A[i] )
--k;
st[++k] = A[i];
}
16
Introducere
Secvenţa 4
Secvenţa 5
𝑙𝑜𝑔𝑏 𝑥
𝑙𝑜𝑔𝑎 𝑥 = , ∀ 𝑎, 𝑏 ≠ 1 ş𝑖 𝑎, 𝑏 > 0
𝑙𝑜𝑔𝑏 𝑎
17
Capitolul 1
1.
for ( int i = 1; i <= N; ++i )
for ( int j = 1; j <= M; ++j )
++k;
2.
for ( int i = 1; i <= N; ++i )
for ( int j = 1; j + i <= N; ++j )
++k;
3.
for ( int i = 1; i <= N; ++i )
for ( int j = 1; j <= N; ++j )
if ( (i + j) % 2 == 0 )
for ( int k = 1; k <= j; ++k )
cout << i + j + k <<'\n';
4.
for ( int i = 1; i * i <= N; ++i )
for ( int j = 1; j <= N; j *= 2 )
cout << i + j << '\n';
5.
for ( int i = 1; i <= N*N; i *= 2 ) cout << i << endl;
18
Introducere
6.
for ( int i = 1; i <= N; ++i )
for ( int j = 1; j * j <= N; j++ )
cout << i + j << '\n';
7.
int f(int k)
{
if ( !k ) return 1;
8.
for ( int i = 1; i <= 2010; ++i )
for ( int j = 1; j <= N; ++j )
for ( int k = 2010; k; --k )
if ( i + j > j + k )
cout << "Ok!\n";
9.
while ( st <= dr )
{
if ( A[st] == A[dr] )
{
for ( int i = st; i <= dr; ++i )
cout << A[i] << ' ';
cout << endl;
}
++st;
--dr;
}
10.
void f(int N)
{
if ( !N ) return;
19
Capitolul 1
20
Algoritmi de sortare
2. Algoritmi de
sortare
Problema sortării unor date după un anumit criteriu este una dintre
cele mai vechi probleme care face obiectul de studiu al informaticii. Există o
gamă foarte largă de algoritmi care rezolvă această problemă, cât şi nişte
rezultate teoretice importante cu privire la corectitudinea şi eficienţa acestor
algoritmi.
Acest capitol prezintă detaliat o serie de algoritmi de sortare
reprezentativi pentru clasele din care aceştia fac parte. Fiecare algoritm este
prezentat din punct de vedere al complexităţii asimptotice, al eficienţei
practice, al memoriei suplimentare folosite, al stabilităţii, al
optimizărilor suportate şi este însoţit de o implementare în limbajul C++.
Demonstraţiile unor rezultate la care se face referire nu vor fi prezentate,
punându-se accentul pe întelegerea modului de funcţionare al algoritmilor şi
al implementării acestora într-un limbaj de programare.
Prin memorie suplimentară înţelegem memoria necesară execuţiei
algoritmului, fără să luăm în considerare vectorul ce reţine numerele ce
trebuiesc sortate.
Prin stabilitate înţelegem proprietatea unui algoritm de sortare de a
păstra ordinea relativă a două elemente cu chei de sortare identice. De
exemplu, dacă ar trebui să sortăm perechile (2, 3), (1, 4), (2, 5), (1, 2) după
prima componentă, un algoritm care ar produce sortarea: (1, 4), (1, 2), (2, 3),
(2, 5) ar putea fi stabil, pe când un algoritm care ar produce orice altă sortare
sigur nu ar fi stabil.
21
Capitolul 2
CUPRINS
22
Algoritmi de sortare
23
Capitolul 2
i 1 2 3 4 5
A 6 4 3 8 7
i 1 2 3 4 5
A 4 4 6 8 7
24
Algoritmi de sortare
i 1 2 3 4 5
A 3 4 6 8 7
i 1 2 3 4 5
A 3 4 6 8 7
i 1 2 3 4 5
A 3 4 6 7 8
25
Capitolul 2
A[j+1] = V;
}
}
26
Algoritmi de sortare
2.3. Quicksort
Quicksort, sau sortarea rapidă, este cel mai eficient algoritm de
sortare, comparativ cu ceilalţi algoritmi de aceiaşi complexitate. Din păcate,
pentru a fi cu adevărat performant atât pe cazul mediu cât şi pe cazul cel mai
defavorabil, algoritmul necesită anumite optimizări care complică puţin
codul, rezultând un program mai complex decât pentru celelalte sortări.
Sortarea rapidă este un algoritm de tip divide et impera şi
funcţionează astfel:
Fie Quicksort(A, st, dr) o funcţie care sortează intervalul [x, y]
al vectorului A.
Fie Partitie(A, st, dr) o funcţie care reordonează intervalul [x, y]
al vectorului A astfel încât toate elementele mai mici sau egale
cu A[st] să se afle la începutul vectorului şi toate elementele mai
mari sau egale cu A[st] să se afle la sfârşitul vectorului.
Elementul A[st] se numeşte element pivot.
Funcţia Quicksort(A, st, dr) este implementată astfel:
o Dacă st < dr execută
P = Partitie(A, st, dr)
Apelează recursiv Quicksort(A, st, P)
Apelează recursiv Quicksort(A, P + 1, dr)
La finalul algoritmului, vectorul A va fi sortat.
27
Capitolul 2
i 1 2 3 4
A 1 2 3 4
i 1 2 3 4
A 1 2 3 4
28
Algoritmi de sortare
i 1 2 3 4
A 1 2 3 4
i 1 2 3 4
A 1 2 3 4
i 1 2 3 4
A 1 2 3 4
29
Capitolul 2
int Partitie(int A[], int st, int dr) void Quicksort(int A[], int st, int dr)
{ {
int V = A[st]; if ( st < dr )
--st; ++dr; {
while ( st < dr ) int P = Partitie(A, st, dr);
{ Quicksort(A, st, P);
do Quicksort(A, P+1, dr);
--dr; }
while ( st < dr && A[dr] > V ); }
do
++st; Execiţiu: modificaţi funcţia Partitie
while ( st < dr && A[st] < V ); astfel încât aceasta să returneze, în
O(N), al k-lea cel mai mic element al
if ( st < dr )
{ vectorului A. De exemplu, dacă
int tmp = A[st]; A = {1, 7, 5, 2, 4} şi k = 3, se va
A[st] = A[dr]; returna 4.
A[dr] = tmp;
}
}
return dr;
}
30
Algoritmi de sortare
31
Capitolul 2
int Partitie(int A[], int st, int dr) void Quicksort(int A[], int st, int dr)
{ // numar aleator din [st, dr] {
int poz = st + rand() % (dr-st+1); while ( st < dr )
int tmp = A[poz]; {
A[poz] = A[st]; int P = Partitie(A, st, dr);
A[st] = tmp; if ( P - st < dr - P - 1 )
{
int V = A[st]; Quicksort(A, st, P);
--st; ++dr; st = P + 1;
while ( st < dr ) }
{ else
do {
--dr; Quicksort(A, P + 1, dr);
while ( st < dr && A[dr] > V ); dr = P;
do }
++st; }
while ( st < dr && A[st] < V ); }
if ( st < dr )
{
int tmp = A[st];
A[st] = A[dr];
A[dr] = tmp;
}
}
return dr;
}
32
Algoritmi de sortare
33
Capitolul 2
34
Algoritmi de sortare
1 2 … 𝑁
𝑃=
𝑃(1) 𝑃(2) … 𝑃(𝑁)
35
Capitolul 2
36
Algoritmi de sortare
2.5. Heapsort
Heapsort, cunoscut şi sub numele de sortare prin ansamble, este
un algoritm de sortare cu timpul de execuţie O(N·log N) şi memorie
auxiliară O(1). Algoritmul este, practic, o optimizare a algoritmului de
sortare prin selecţie (Selection sort), algoritm care funcţionează
determinând la fiecare pas elementul de valoarea maximă şi mutându-l pe
ultima poziţie liberă a vectorului. Deoarece trebuie să determinăm N
maxime, iar determinarea unui maxim implică verificarea tuturor
elementelor vectorului, complexitatea acestui algoritm este O(N2). Heapsort
foloseşte structura de date numită heap pentru a determina cele N maxime,
rezultând un timp de execuţie de O(N·log N). Pentru a înţelege mai bine ce
este acela un heap, vom începe prin prezentarea unor noţiuni teoretice.
37
Capitolul 2
i 1 2 3 4 5 6 7
A 19 13 15 5 6 12 14
38
Algoritmi de sortare
i 1 2 3 4 5 6 7 8
A 19 13 15 5 6 12 14 18
i 1 2 3 4 5 6 7 8
A 19 13 15 18 6 12 14 5
i 1 2 3 4 5 6 7 8
A 19 18 15 13 6 12 14 5
39
Capitolul 2
i 1 2 3 4 5 6 7
A 19 13 15 5 6 12 14
i 1 2 3 4 5 6
A 14 13 15 5 6 12
i 1 2 3 4 5 6
A 15 13 14 5 6 12
40
Algoritmi de sortare
41
Capitolul 2
42
Algoritmi de sortare
Schimb = FiuSt;
if ( FiuDr <= N )
if ( A[FiuDr] > A[Schimb] )
Schimb = FiuDr;
43
Capitolul 2
void Introsort(int A[], int N, int st, int dr, int Adanc)
{
if ( st < dr )
{
if ( (1 << Adanc) > N )
Heapsort(A, st, dr);
else
{
int P = Partitie(A, st, dr);
Introsort(A, N, st, P, Adanc + 1);
Introsort(A, N, P+1, dr, Adanc + 1);
}
}
}
44
Algoritmi de sortare
45
Capitolul 2
i 1 2 3 4 5 6
A 4 7 2 2 1 3
i 0 1 2 3 4 5 6 7
V 0 1 2 1 1 0 0 1
Ceea ce înseamnă că avem zero elemente cu valoarea 0, un element
cu valoarea 1, două elemente cu valoarea 2, un element cu valoarea 3
ş.a.m.d.
Acum, pentru a sorta efectiv vectorul A, parcurgem toate poziţiile i
ale vectorului V şi punem în A valoarea i de V[i] ori.
Este clar că sortarea prin numărare este foarte eficientă atunci când
avem un număr mare de valori mici ce trebuiesc sortate. Dezavantajele
acestei metode sunt că nu putem sorta decât numere naturale.
Pentru a extinde metoda la numere întregi din intervalul
[-MaxV, MaxV], putem aduna fiecărui element valoarea MaxV,
transformând astfel toate numerele întregi în umere naturale. Astfel se
dublează însă memoria folosită.
Pentru a putea sorta numere reale pozitive, despre care ştim că au un
anumit număr X de cifre după virgulă, putem să le înmulţim pe fiecare cu
10X, după care să le sortăm ca fiind numere naturale. Dacă numerele pot fi şi
46
Algoritmi de sortare
negative, se pot combina cele două metode. Pentru numere reale însă,
algoritmul devine destul de ineficient, deoarece memoria folosită creşte de
10X ori, iar timpul de execuţie devine mai mare. Pentru alte tipuri de date,
folosirea acestei metode poate fi mult mai dificilă, sau chiar imposibilă. De
exemplu, cum putem sorta alfabetic nişte şiruri de caractere folosind această
metodă? Dar nişte perechi de numere după prima componentă?
delete []V;
}
𝑀𝑎𝑥𝑉𝑎𝑙
+1
8 ∙ 𝑠𝑖𝑧𝑒𝑜𝑓 (𝑖𝑛𝑡)
47
Capitolul 2
i 1 2 3 4 5 6 7 8 9 10
A 430 027 325 088 145 111 034 932 353 007
Vom începe prin a sorta mai întâi numerele după cifra unităţilor. Va
rezulta următorul vector:
i 1 2 3 4 5 6 7 8 9 10
A 430 111 932 353 034 325 145 027 007 088
i 1 2 3 4 5 6 7 8 9 10
A 007 111 325 027 430 932 034 145 353 088
48
Algoritmi de sortare
i 1 2 3 4 5 6 7 8 9 10
A 007 027 034 088 111 145 325 353 430 932
Definiţia 1: Un număr natural care are cea mai mare cifră C poate fi
considerat un număr în toate bazele mai mari decât C. De exemplu, numărul
5213 poate fi considerat un număr în toate bazele mai mari decât 5. Sistemul
(baza) în care un număr este scris se marchează prin trecerea bazei ca indice
al numărului.
Definiţia 2: O cifră a unui număr natural în baza 2 se numeşte bit. 8
biţi = 1 byte.
𝑋 = 𝑎1 𝑎2 … 𝑎𝑘 , 𝑎𝑖 ∈ {0,1, … ,9}
49
Capitolul 2
𝑋 = 𝑎1 𝑎2 … 𝑎𝑘 , 𝑎𝑖 ∈ {0,1, … ,9}
𝑋 = 𝑎1 𝑎2 … 𝑎𝑘 , 𝑎𝑖 ∈ {0,1, … ,9}
50
Algoritmi de sortare
p q p&q
1 1 1
1 0 0
0 1 0
0 0 0
p q p|q
1 1 1
1 0 1
0 1 1
0 0 0
51
Capitolul 2
i 1 2 3
A 01101110 00010111 11101001
i 1 2 3
A 00010111 11101001 01101110
Deoarece 01112 = 710, 10012 = 910 şi 11102 = 1410. Vom sorta acum
numerele după următoarea grupă de de patru biţi:
i 1 2 3
A 00010111 01101110 11101001
52
Algoritmi de sortare
i 0 1 2 3 4 5 6
A 3 4 3 2 1 1 7
i 0 1 2 3 4 5 6
A 1 4 8 11 13 14 15
53
Capitolul 2
110101101102 &
000000111112
000000101102 = 2210
Pentru a afla valoarea următorilor cinci biţi, vom deplasa mai întâi
numărul iniţial cu cinci poziţii către dreapta şi vom aplica operaţia ŞI pe
numărul rezultat prin deplasare:
110101101102 >> 000001101012 &
5 000000111112
000001101012 000000101012 = 2110
void Sortare(unsigned int A[], int N, unsigned int T[], int Gr, int V[],
int Poz[])
{
for ( int i = 0; i < MaxVal; ++i ) V[i] = 0;
for ( int i = 1; i <= N; ++i ) ++V[ AflaGrupa(A[i], Gr) ];
Poz[0] = 1;
for ( int i = 1; i < MaxVal; ++i ) Poz[i] = Poz[i - 1] + V[i - 1];
for ( int i = 1; i <= N; ++i ) T[ Poz[ AflaGrupa(A[i], Gr) ]++ ] = A[i];
}
Sortare(A, N, T, 0, V, Poz);
Sortare(T, N, A, 1, V, Poz);
delete []T; delete []V; delete []Poz;
}
54
Algoritmi de sortare
2.8. Concluzii
Am prezentat în acest capitol opt algoritmi de sortare, metode de
implementare a acestora, optimizări şi situaţiile în care fiecare se potriveşte
cel mai bine.
55
Capitolul 2
56
Tehnici de programare
3. Tehnici de
programare
Acest capitol prezintă principalele tehnici tehnici de programare
folosite în rezolvarea problemelor: recursivitatea, backtracking, divide et
impera, greedy şi programare dinamică. Aceste tehnici sunt însoţite de
aplicaţii practice clasice, cum ar fi problema turnurilor din Hanoi şi
generarea permutărilor, aranjamentelor, combinărilor etc.
Acest capitol este foarte important, întrucât orice problemă poate fi
rezolvată printr-un algoritm care se încadrează într-una dintre tehnicile
menţionate. Recomandăm aşadar stăpânirea acestora.
57
Capitolul 3
CUPRINS
58
Tehnici de programare
3.1. Recursivitate
Să pornim de la definiţia de bază: o funcţie este recursivă dacă în
definiţia ei se foloseşte o referire la ea însăşi.
Din această definiţie putem considera modelul general al unui
algoritm recursiv de forma:
rec(param_formali) { rec(param_formali) }
Funcţia factorial
1 𝑑𝑎𝑐ă 𝑛 = 0
𝑓 𝑛 =
𝑓 𝑛−1 ∗𝑛 𝑑𝑎𝑐ă 𝑛 > 0
int f(int n)
{
if ( n == 0 ) return 1;
else return f(n - 1) * n ;
}
𝑛+1 𝑑𝑎𝑐ă 𝑚 = 0
𝐴 𝑚, 𝑛 = 𝐴(𝑚 − 1, 1) 𝑑𝑎𝑐ă 𝑚 > 0 ş𝑖 𝑛 = 0
𝐴(𝑚 − 1, 𝐴 𝑚, 𝑛 − 1 ) 𝑑𝑎𝑐ă 𝑚 > 0 ş𝑖 𝑛 > 0
59
Capitolul 3
0 𝑑𝑎𝑐ă 𝑛 = 0
𝐹: 𝑁 → 𝑁, 𝐹 𝑛 = 1 𝑑𝑎𝑐ă 𝑛 = 1
𝐹 𝑛 − 1 + 𝐹(𝑛 − 2) 𝑑𝑎𝑐ă 𝑛 ≥ 2
Conform definiţiei:
int F(int n)
{
if ( n == 0 ) return 0;
else if ( n == 1) return 1;
else return F(n – 1) + F(n – 2);
}
𝑥 𝑑𝑎𝑐ă 𝑦 = 0
𝑐𝑚𝑚𝑑𝑐 𝑥, 𝑦 =
𝑐𝑚𝑚𝑑𝑐(𝑦, 𝑥 % 𝑦) 𝑑𝑎𝑐ă 𝑦 > 0
60
Tehnici de programare
a) Fractali
Fig. 3.1.1. a)
Următorul subprogram:
fractal (x, y, l)
{
patrat(x, y, l);
fractal((x – l) / 2, (y – l) / 2, l / 2)
}
Fig. 3.1.1. b)
61
Capitolul 3
fractal (x,y,l)
{
if ( l>5 ) //se referă la dimensiunea în pixeli
{
patrat(x, y, l)
fractal((x – l) / 2, (y – l) / 2, l / 2)
fractal((x + l) / 2, (y + l) / 2, l / 2)
}
}
Fig. 3.1.1. c)
62
Tehnici de programare
63
Capitolul 3
64
Tehnici de programare
65
Capitolul 3
#include <iostream>
using namespace std;
int main()
{
Hanoi (3,'A','B','C');
return 0;
}
66
Tehnici de programare
67
Capitolul 3
#include <iostream>
using namespace std;
int main()
{
Hanoi4tije(3, 'A', 'B', 'C', 'D');
return 0;
}
3.2. Backtracking
Tehnica backtracking este o tehnică de programare, implementată
de obicei recursiv, care construieşte treptat soluţia unei probleme, iar în
cazul în care soluţia construită se dovedeşte a fi invalidă (sau ne interesează
mai multe soluţii), revine la un pas precedent pentru a schimba o alegere
făcută. Acest lucru se continuă, de obicei, până când au fost explorate toate
posibilităţile sau până când am găsit una sau mai multe soluţii valide. De
multe ori nu este necesară explorarea tuturor posibilităţilor, putând elimina
68
Tehnici de programare
69
Capitolul 3
Exemplu:
perm.in perm.out
3 123
132
213
231
312
321
70
Tehnici de programare
1 1
K Sol
i 1 2 3
Fol true false false
2 2
1 1
K Sol
i 1 2 3
Fol true true false
3 3
2 2
1 1
K Sol
i 1 2 3
Fol true true true
71
Capitolul 3
3 3
2 2
1 1
K Sol
i 1 2 3
Fol true true false
i 1 2 3
Fol true false true
Se continuă în acest mod până când nu se mai pot depune valori noi
pe nicio poziţie a stivei, lucru ce se va întâmpla după generarea permutării 3
2 1.
Se poate observa că algoritmul acesta generează permutările în
ordine lexicografică. Spunem că un şir (Xn) este mai mic lexicografic decât
un şir (Y n) dacă şi numai dacă există un i (1 < i ≤ n) astfel încât:
X1 = Y1, X2 = Y2, ..., Xi – 1 = Yi – 1, Xi < Yi
#include <fstream>
using namespace std;
72
Tehnici de programare
Exerciţii:
a) Cât de rapid este algoritmul pentru valori mai mari decât 8?
b) Mai puteţi găsi optimizări pentru acest algoritm? Dar alt
algoritm, care abordează diferit problema? (Indiciu: folosiţi
interschimbări)
c) Modificaţi algoritmul astfel încât să găsească numai a P-a
permutare în ordine lexicografică.
d) Modificaţi algoritmul astfel încât să genereze toate permutările
unei mulţimi citite din fişier, mulţime care poate avea elemente
care se repetă.
b) Generarea aranjamentelor
Dându-se două numere naturale N şi P (1 ≤ P ≤ N), ne propunem să
generăm toate aranjamentele de N luate câte P ale mulţimii numerelor
𝑁!
naturale. Aranjamente de N luate câte P se notează 𝐴𝑃𝑁 , iar 𝐴𝑃𝑁 =
𝑁−𝑃 !
(numărul aranjamentelor de N luate câte P). Reamintim că 𝐴𝑃𝑁 reprezintă
modalităţile de a aranja N obiecte în P poziţii, ţinând cont de ordinea
acestora (de exemplu, aranjamentul 1 3 2 este diferit de aranjamentul 1 2 3).
73
Capitolul 3
Exemplu:
aran.in aran.out
32 12
13
21
23
31
32
i 1 2 3
Fol true false false
2 2
1 1
K Sol
i 1 2 3
Fol true true false
74
Tehnici de programare
2 3
1 1
K Sol
i 1 2 3
Fol true false true
#include <fstream>
75
Capitolul 3
c) Generarea combinărilor
Exemplu:
comb.in comb.out
42 12
13
14
23
24
34
76
Tehnici de programare
1 1
K Sol
77
Capitolul 3
return;
}
Exerciţii:
a) Cât de rapidă este generarea aranjamentelor şi a combinărilor
pentru valori mari, dar apropiate, ale lui N şi P?
b) Scrieţi un program care afişează toate numerele naturale de trei
cifre care pot forma cu cifrele 1, 3, 4 şi 5. Ce algoritm veţi
folosi?
c) Modificaţi ultimii doi algoritmi astfel încât să afişeze
aranjamentele, respectiv combinările, în ordine invers-
lexicografică.
78
Tehnici de programare
Exemplu:
part.in part.out
4 1111
112
13
22
4
Explicaţie: 1 + 1 + 1 + 1 = 2 + 1 + 1 = 3 + 1 = 4
79
Capitolul 3
80
Tehnici de programare
return;
}
Exerciţii:
a) Modificaţi algoritmul astfel încât doar să numere partiţiile
existente, nu să le şi afişeze.
b) Impuneţi condiţia ca numerele folosite într-o partiţie să fie
distincte.
c) Impuneţi condiţia ca diferenţa în modul a doi termeni consecutivi
să fie cel puţin 2
81
Capitolul 3
e) Concluzii
82
Tehnici de programare
a) Determinarea minimului
Exemplu:
minim.in minim.out
7 2
9 7 8 3 4 2 11
83
Capitolul 3
#include <fstream>
using namespace std;
const int maxN = 1001;
void citire(int &N, int A[])
{
ifstream in("minim.in");
in >> N;
for ( int i = 1; i <= N; ++i ) in >> A[i];
in.close();
}
int main()
{
int N, A[maxN];
citire(N, A);
ofstream out("minim.out");
out << Minim(A, 1, N);
out.close();
return 0;
}
84
Tehnici de programare
b) Căutarea binară
Se dau N numere naturale ordonate crescător şi M întrebări de
forma „numărul Xi se găseşte sau nu printre cele N numere?” la care trebuie
să se răspundă cât mai eficient.
Datele de intrare se citesc din fişierul cbinara.in: pe prima linie
numerele N şi M separate prin spaţiu, pe următoarea linie cele N numere
naturale separate prin spaţiu, iar pe următoarele M linii cele M întrebări. Pe
linia i a fişierului de ieşire cbinara.out veţi afişa DA sau NU, în funcţie de
răspunsul la întrebarea respectivă.
Exemplu:
cbinara.in cbinara.out
95 DA
4 6 8 9 12 15 15 16 21 NU
4 DA
5 DA
9 DA
21
15
85
Capitolul 3
86
Tehnici de programare
#include <fstream>
using namespace std;
87
Capitolul 3
ofstream out("cbinara.out");
for ( int i = 1, x; i <= M; ++i )
{
in >> x;
in.close(); out.close();
}
int main()
{
int N, M, A[maxN];
rezolvare(N, M, A);
return 0;
}
88
Tehnici de programare
Exerciţii:
a) Modificaţi algoritmul astfel încât să returneze cea mai mare
poziţie a unui element căutat.
b) Modificaţi algoritmul astfel încât să lucreze cu un şir sortat
descrescător.
c) Modificaţi algoritmul de sortare prin inserţie astfel încât să
folosească algoritmul de căutare binară pentru determinarea
poziţiei în care trebuie inserat un element.
a) Concluzii
3.4. Greedy
Tehnica de programare greedy (în traducere liberă: tehnica
lăcomiei) se bazează pe selectarea succesivă a unor optime locale pentru a
determina într-un final optimul global. Tehnica se foloseşte, de obicei, în
cazul problemelor în care se cere determinarea unui minim sau maxim
respectând anumite constrângeri dependente de problemă.
Datorită faptului că algoritmii greedy iau la fiecare pas decizia cea
mai favorabilă existentă la acel pas, fără a lua în considerare cum ar putea
afecta această decizie întreg traseul algoritmului, există posibilitatea ca
aceşti algoritmi să nu determine întotdeauna un optim, ci doar o soluţie care
respectă constrângerile problemei, dar care nici măcăr nu este neapărat
apropiată de optimul global pentru datele date. Din acest motiv, înainte de a
implementa o strategie greedy pentru rezolvarea unei probleme, este
recomandat să se demonstreze matematic corectitudinea strategiei alese.
Dacă se poate demonstra că metoda aleasă conduce întotdeauna la
găsirea unui optim global, folosirea unei strategii greedy este de cele mai
multe ori preferabilă celorlalte alternative, cum ar fi metodele backtracking
sau divide et impera, deoarece algoritmii greedy sunt, în general, mai
rapizi, având o complexitate polinomială, adică O(Nk), unde N reprezintă
dimensiunea datelor de intrare, iar k este o constantă naturală.
89
Capitolul 3
a) Problema spectacolelor
Exemplu:
spectacole.in spectacole.out
5 3
14
47
35
89
67
90
Tehnici de programare
XP nu face parte din soluţia optimă. Dacă XP ar face parte din soluţia
optimă, ar trebui să se afle cel puţin pe poziţia P + 1, ceea ce înseamnă că ar
începe după YP, contrazicându-se astfel modul de funcţionare al
algoritmului.
mai puţin important) atunci când se lucrează cu structuri, mai ales dacă
acestea sunt foarte mari, deoarece dacă parametrii nu sunt de tip referinţă,
fiecare apel al funcţiei va lucra cu o copie a obiectelor transmise ca
argument funcţiei, iar această copiere poate afecta drastic performanţa unui
program.
Exerciţii:
a) Modificaţi algoritmul astfel încât să afişeze indicii iniţiali ai
spectacolelor selectate.
b) Implementaţi un algoritm de sortare pentru sortarea datelor.
c) Dacă fiecare spectacol ar avea asociat un cost şi am dori să
organizăm un număr maxim de spectacole, dar care să coste cât
93
Capitolul 3
Exemplu:
suma.in suma.out
39 6
94
Tehnici de programare
c) Concluzii
Algoritmii greedy sunt de obicei mai rapizi decât alţi algoritmi, mai
ales decât metodele exhaustive cum este metoda backtracking. Un
dezavantaj al acestora este că programatorul trebuie să se bazeze foarte mult
pe intuiţie şi pe experienţă pentru a putea ajunge la un algoritm corect, iar
aparenta corectitudine a unui algoritm poate fi înşelătoare, aşa cum am
arătat la ultima problemă.
Dacă aveţi de ales între un algoritm greedy a cărui corectitudine
poate fi demonstrată şi un algoritm mai puţin eficient, este clar că alegerea
trebuie făcută în favoarea algoritmului greedy. Dacă nu se poate demonstra
corectitudinea algoritmului greedy însă, iar importanţă găsirii unui optim
pentru fiecare caz posibil este foarte mare, atunci este de preferat folosirea
altui algoritm, chiar dacă este mai puţin eficient.
95
Capitolul 3
96
Tehnici de programare
a) Şirul Fibonacci
97
Capitolul 3
Exemplu:
fibo.in fibo.out
6 8
i 0 1 2 3 4 5 6
F 0 1 1 2 3 5 8
#include <fstream>
98
Tehnici de programare
int main()
{
int N, F[maxN];
ifstream in("fibo.in");
in >> N;
in.close();
F[0] = 0, F[1] = 1;
for ( int i = 2; i <= N; ++i )
F[i] = F[i - 2] + F[i - 1];
ofstream out("fibo.out");
out << F[N] << endl;
out.close();
return 0;
}
Exerciţii:
a) Implementaţi algoritmul care foloseşte memorie O(1).
b) Numerele Fibonacci devin mari foarte rapid. Care este cel mai
mare număr Fibonacci care poate fi reţinut de tipul de date int?
c) Dacă s-ar da mai multe numere pentru care trebuie calculată
funcţia F, cel mai mare dintre acestea fiind K, cum s-ar putea
calcula funcţia eficient pentru fiecare?
b) Problema triunghiului
99
Capitolul 3
Exemplu:
triunghi.in triunghi.out
5 50
4 4 5 2 20 19
35
142
7 5 6 20
1 1 7 19 9
100
Tehnici de programare
𝐴 𝑖 [𝑗] 𝑑𝑎𝑐ă 𝑖 = 𝑗 = 1
𝐵 𝑖 𝑗 =
max 𝐵 𝑖 − 1 𝑗 , 𝐵 𝑖 − 1 𝑗 − 1 + 𝐴 𝑖 [𝑗] 𝑎𝑙𝑡𝑓𝑒𝑙
i\j 0 1 2 3 4 5
0
1 – inf 4 – inf
2 – inf 7 9 – inf
3 – inf 8 13 11 – inf
4 – inf 15 18 19 31 – inf
5 – inf 16 19 26 50 40
101
Capitolul 3
#include <fstream>
in.close();
}
102
Tehnici de programare
103
Capitolul 3
Exemplu:
pascal.in pascal.out
32 3
73 35
10 8 45
104
Tehnici de programare
𝐶𝑁𝐾 = 𝐶𝑁−1
𝐾 𝐾−1
+ 𝐶𝑁−1
Implementare
#include <fstream> int main()
{
using namespace std; int N, K, A[maxN][maxN];
const int maxN = 13; preprocesare(A);
Exerciţii:
a) Explicaţi de ce folosirea formulei combinărilor este o metodă
mai puţin eficientă.
b) Scrieţi o funcţie recursivă care foloseşte recurenţa combinărilor
pentru a calcula fiecare răspuns. Este această abordare eficientă?
c) Dacă am calcula mai multe linii ale triunghiului lui Pascal, care
este prima linie care conţine rezultate greşite? De ce se întâmplă
acest lucru?
d) Tehnica memoizării
Am prezentat până acum doi algoritmi care folosesc metoda înainte
pentru rezolvarea problemelor. Aşa cum am menţionat la începutul
capitolului, metoda înapoi poate fi îmbunătăţită folosind tehnica
105
Capitolul 3
106
Tehnici de programare
return memo[x][y];
}
107
Capitolul 3
să fim atenţi la mai multe cazuri particulare. Funcţia va trebui apelată pentru
fiecare element al ultimei linii din triunghiul dat.
Exerciţii:
a) Cum putem reconstitui soluţia la problema triunghiului dacă
folosim funcţia recursivă anterioară?
b) Dacă ne interesează drumul minim numai până la un singur
element de pe ultima linie a triunghiul, care abordare este mai
eficientă?
c) Scrieţi o funcţie recursivă ce foloseşte memoizare pentru calculul
combinărilor.
d) Explicaţi de ce memoizarea nu îmbunătăţeşte funcţia factorial
sau funcţia de rezolvare a problemei turnurilor din Hanoi.
e) Comparaţi o funcţie recursivă memoizată cu o funcţie iterativă
echivalentă acesteia. Care este mai eficientă?
e) Concluzii
Programarea dinamică este o tehnică de programare foarte
folositoare pentru rezolvarea problemelor de numărare sau de găsire a
optimelor. Este o alternativă mai rapidă decât metoda backtracking şi mai
corectă decât metoda greedy.
Dezavantajul principal al acestei metode stă în faptul că unele
formule de recurenţă pot fi neintuitive şi dificil de implementat eficient, dar,
de cele mai multe ori, efortul unei implementări corecte merită făcut,
datorită eficienţei acestor algoritmi.
Atunci când implementarea iterativă a unei formule de recurenţă ar fi
prea dificilă, se poate folosi tehnica memoizării pentru a păstra eficienţa
algoritmului şi a simplifica implementarea.
Corectitudinea unui algoritm de programare dinamică poate fi
verificată cu un algoritm backtracking, iar la rândul său, programarea
dinamică poate fi folosită pentru a testa dacă o strategie greedy este sau nu
corectă.
108
Algoritmi matematici
4. Algoritmi
matematici
Acest capitol prezintă algoritmii care au la bază noţiuni elementare
de matematică. Aceşti algoritmi sunt folosiţi, de cele mai multe, ori pentru
rezolvarea unor probleme strict matematice, cum ar fi rezolvarea unor
ecuaţii sau sisteme de ecuaţii, determinarea numerelor cu anumite
proprietăţi, rezolvarea unor probleme de geometrie sau calculul unor
formule complexe cu ajutorul calculatorului.
Algoritmii prezentaţi se axează mai mult pe teoria numerelor, aceştia
fiind cei mai des întâlniţi în domeniul informaticii şi totodată cei mai
studiaţi.
109
Capitolul 4
CUPRINS
110
Algoritmi matematici
111
Capitolul 4
Scăzând la fiecare pas numărul mai mic din numărul mai mare,
rămânem în final cu două numere egale cu 4, iar acest număr reprezintă
c.m.m.d.c. pentru 12 şi 8.
112
Algoritmi matematici
113
Capitolul 4
114
Algoritmi matematici
115
Capitolul 4
iar 460 ≡ 1 (mod 51). Aşa cum vom vedea în secţiunile ce urmează, acest
lucru are diverse aplicaţii şi în alţi algoritmi.
return ;
}
a = tb;
b = ta - tb*(X / Y);
}
Exerciţii:
a) Modificaţi funcţia astfel încât să returneze şi c.m.m.d.c. al
numerelor X şi Y.
b) Modificaţi funcţia astfel încât să returneze soluţiile ecuaţiei
a∙X + b∙Y = c, unde c este un număr dat, iar restul variabilelor au
aceeaşi semnificaţie de până acum. Există întotdeauna soluţie?
c) Scrieţi o funcţie echivalentă, dar care să nu folosească apeluri
recursive.
d) Scrieţi un program care citeşte două numere X şi Y şi determină
inversul multiplicativ al lui X modulo Y dacă acesta există, iar
dacă nu afişează un mesaj corspunzător.
116
Algoritmi matematici
𝑁
𝜋 𝑁 ≅
ln 𝑁
117
Capitolul 4
a) Metode clasice
Complexitatea acestei metode este O(N) pe cel mai rău caz. Dacă ar
fi să folosim această funcţie pentru a determina toate numerele prime până
la N am obţine complexitatea O(N2), ceea ce ar fi indezirabil pentru N mai
mare ca ~1 000.
118
Algoritmi matematici
return true;
}
119
Capitolul 4
120
Algoritmi matematici
2 3 5 7 11 13
17 19 23
121
Capitolul 4
void eratosthenes_clasic(int N)
{
bool *lista = new bool[N + 1]; // lista[i] = true daca numarul i
// este prim si false in caz contrar
delete []lista;
}
(operatorul ~ schimbă toţi biţii unui număr în opusul lor, adica 1 devine 0 şi
0 devine 1. Operatorul & se consideră cunoscut de la secţiunea Radix sort).
Pentru a verifica valoarea unui bit, vom folosi o funcţie
bool verif(unsigned char lista[], int nr)
lista[0] lista[1]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
123
Capitolul 4
11111111 &
11111101
––––––––––
11111101
lista[0] lista[1]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1
lista[0] lista[1]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
1 1 1 1 1 1 1 1 0 1 1 0 1 1 0 1
124
Algoritmi matematici
void eratosthenes_biti(int N)
{
int dim = N / 8 + 1;
unsigned char *lista = new unsigned char[dim];
delete []lista;
}
125
Capitolul 4
void eratosthenes_final(int N)
{
int dim = N / 8 / 2 + 1;
unsigned char *lista = new unsigned char[dim];
126
Algoritmi matematici
void eratosthenes_bitset(int N)
{
bitset<100000> lista; // clasa bitset nu suporta alocare dinamica
lista.set(); // seteaza toti bitii pe 1 (true)
for ( int i = 3; i*i <= N; i += 2 )
if ( lista[i] )
for ( int j = i*i; j <= N; j += i )
lista[j] = 0;
128
Algoritmi matematici
delete []prim;
}
129
Capitolul 4
𝐸1 : 2𝑥 + 3𝑦 = 4
𝐸2 : 𝑥 + 2𝑦 = 3
𝐸1 : 3𝑥 + 2𝑦 + 2𝑧 = −2
𝐸2 : 𝑥 + 𝑦 + 3𝑧 = −1
𝐸3 : 4𝑥 + 3𝑦 + 𝑧 = 5
1
𝐸2 ← − 𝐸1 + 𝐸2
3
4
𝐸3 ← − 𝐸1 + 𝐸3
3
130
Algoritmi matematici
𝐸1 : 3𝑥 + 2𝑦 + 2𝑧 = −2
1 7 1
𝐸2 : 𝑦+ 𝑧=−
3 3 3
1 5 23
𝐸3 : 𝑦− 𝑧=
3 3 3
Sistemul devine:
𝐸1 : 3𝑥 + 2𝑦 + 2𝑧 = −2
1 7 1
𝐸2 : 𝑦+ 𝑧=−
3 3 3
𝐸3 : −4𝑧 = 8
𝑥 = −8
𝑦 = 13
𝑧 = −2
131
Capitolul 4
132
Algoritmi matematici
1 0 0…0 𝑥1
0 1 0…0 𝑥2
0 0 1…0 𝑥3
𝐴= . .
. .
. .
0 0 0…1 𝑥𝑁
133
Capitolul 4
134
Algoritmi matematici
Exerciţii:
a) Adăugaţi condiţii care verifică dacă un sistem nu are soluţie şi
afişează un mesaj corespunzător în acest caz.
b) Încercaţi să găsiţi un sistem pentru care cele două variante
prezentate al algoritmului dau răspunsuri diferite.
135
Capitolul 4
return rez;
}
1 𝑑𝑎𝑐ă 𝑏 = 0
𝑏 𝑏
𝑎𝑏 = 𝑎2 ∙ 𝑎2 𝑑𝑎𝑐ă 𝑏 𝑛𝑢𝑚ă𝑟 𝑝𝑎𝑟
𝑎 ∙ 𝑎𝑏−1 𝑑𝑎𝑐ă 𝑏 𝑛𝑢𝑚ă𝑟 𝑖𝑚𝑝𝑎𝑟
136
Algoritmi matematici
137
Capitolul 4
𝑎𝑝 −1 ≡ 1 (𝑚𝑜𝑑 𝑝)
138
Algoritmi matematici
if ( exponentiere_log(a, p - 1, p) != 1 )
return false; // SIGUR nu e prim
}
139
Capitolul 4
𝑎𝑝 −1 ≡ 1 (𝑚𝑜𝑑 𝑝)
140
Algoritmi matematici
𝑎𝑑 ≢ 1 𝑚𝑜𝑑 𝑝 (1)
şi
𝑟 ∙𝑑
𝑎2 ≢ −1 𝑚𝑜𝑑 𝑝 ∀ 0 ≤ 𝑟 < 𝑠 (2)
141
Capitolul 4
bool miller_rabin_test(int p)
{
int a[] = {2, 7, 61, 0}, s = 0, d = p - 1;
while ( d % 2 == 0 )
{
d /= 2;
++s;
}
if ( x == 1 )
return false;
else if ( x == p - 1 )
{
doi = false;
break;
}
}
142
Algoritmi matematici
𝑘
𝑝𝑖 − 1
𝜑 𝑌 =𝑌∙
𝑝𝑖
𝑖=1
143
Capitolul 4
144
Algoritmi matematici
return R;
}
Exerciţii:
a) Scrieţi, folosind funcţia phi, un program care calculează inverşi
modulari. Verificaţi dacă această metodă este mai rapidă sau nu
decât folosirea algoritmului lui Euclid extins.
b) Scrieţi un program care calculează câte fracţii ireductibile cu
numitorul şi numărătorul mai mici decât un număr N există.
𝑋 ≡ 2 𝑚𝑜𝑑 3
𝑋 ≡ 3 𝑚𝑜𝑑 5
𝑋 ≡ 2 𝑚𝑜𝑑 7
145
Capitolul 4
Pentru exemplul dat, găsim prima dată 5∙7 = 35, care verifică prima
relaţie şi nu le afectează pe celelalte. Pasul următor este să găsim un
multiplu al numerelor 3 şi 7 care verifică a doua relaţie şi nu le afectează pe
prima şi pe ultima. Un astfel de număr este 63. La fel se găseşte şi numărul
30.
𝑋 ≡ 𝑎1 (𝑚𝑜𝑑 𝑛1 )
𝑋 ≡ 𝑎2 (𝑚𝑜𝑑 𝑛2 )
.
.
.
𝑋 ≡ 𝑎𝑘 (𝑚𝑜𝑑 𝑛𝑘 )
146
Algoritmi matematici
𝑁
Fie 𝑁 = 𝑛1 ∙ 𝑛2 ∙ … ∙ 𝑛𝑘 . Fie 𝑃𝑖 = ∀ 1 ≤ 𝑖 ≤ 𝑘. Calculăm
𝑛𝑖
𝑄𝑖 = 𝑃𝑖−1 (𝑚𝑜𝑑 𝑛𝑖 ). Soluţia sistemului este
𝑋= (𝑎𝑖 ∙ 𝑃𝑖 ∙ 𝑄𝑖 ) 𝑚𝑜𝑑 𝑁.
𝑖=1
Lucru echivalent cu
(𝑎𝑖 ∙ 𝑃𝑖 ∙ 𝑄𝑖 ) 𝑚𝑜𝑑 𝑛𝑖 = 𝑎𝑖
𝑖=1
𝑁
deoarece 𝑛𝑖 | 𝑁. Din 𝑃𝑖 = rezultă 𝑃𝑗 ≡ 0 𝑚𝑜𝑑 𝑛𝑖 ş𝑖
𝑛𝑖
𝑎𝑗 ∙ 𝑃𝑗 ∙ 𝑄𝑗 ≡ 0 𝑚𝑜𝑑 𝑛𝑖 , ∀ 𝑗 ≠ 𝑖. Este de ajuns aşadar să
demonstrăm că 𝑎𝑖 ∙ 𝑃𝑖 ∙ 𝑄𝑖 ≡ 𝑎𝑖 (𝑚𝑜𝑑 𝑛𝑖 ), lucru adevărat deoarece
𝑄𝑖 ≡ 𝑃𝑖−1 (𝑚𝑜𝑑 𝑛𝑖 ), iar 𝑃𝑖 ∙ 𝑃𝑖−1 ≡ 1 𝑚𝑜𝑑 𝑛𝑖 .
147
Capitolul 4
#include <fstream>
using namespace std;
struct congruenta
{
int a, n;
};
in >> k;
for ( int i = 1; i <= k; ++i )
in >> T[i].a >> T[i].n;
in.close();
}
148
Algoritmi matematici
a = tb;
b = ta - tb*(X / Y);
}
int a, b;
for ( int i = 1; i <= k; ++i )
{
euclid_ext(P[i], T[i].n, a, b);
Q[i] = a;
}
int X = 0;
for ( int i = 1; i <= k; ++i )
X += (T[i].a * P[i] * Q[i]) % N;
return X;
}
int main()
{
int k;
congruenta T[maxk];
citire(k, T);
ofstream out("TCR.out"); out << rezolvare(k, T); out.close();
return 0;
}
149
Capitolul 4
Unde S este numărul care se cere şi <x, y> reprezintă cel mai mic
multiplu comun al numerelor x şi y.
În rezolvarea problemei am folosit principiul includerii şi al
excluderii, principiu care ne ajută să determinăm cardinalul reuniunii mai
multor mulţimi. Dacă avem n mulţimi notate A1 , A2 , ..., An atunci:
150
Algoritmi matematici
𝐴𝑖 =
𝑖=1
𝑛
Exerciţii:
a) Scrieţi un program care rezolvă problema de mai sus pentru
cazul general.
b) Scrieţi un program care citeşte n mulţimi de numere întregi dintr-
un fişier şi calculează cardinalul reuniunii lor folosind principiul
includerii şi al excluderii.
c) Scrieţi un program care citeşte n mulţimi de numere întregi dintr-
un fişier şi calculează cardinalul reuniunii lor fără a folosi
principiul includerii şi al excluderii. Care metodă este mai simplu
de implementat? dar mai eficientă?
151
Capitolul 4
0 𝑛=0
𝑓 𝑛 = 1 𝑛=1
𝑓 𝑛−2 +𝑓 𝑛−1 𝑛>1
Atunci:
𝑛 𝑓(𝑛 + 1) 𝑓(𝑛)
1 1
=
1 0 𝑓(𝑛) 𝑓(𝑛 − 1)
𝑓(𝑛 + 2) 𝑎 𝑏 𝑓(𝑛 + 1)
= ∙
𝑓(𝑛 + 1) 𝑐 𝑑 𝑓(𝑛)
152
Algoritmi matematici
7. Dacă avem:
𝑒 𝑒 𝑒
𝑁 = 𝑝1 1 ∙ 𝑝2 2 ∙ … ∙ 𝑝𝑘 𝑘 , 𝑝𝑖 𝑛𝑢𝑚ă𝑟 𝑝𝑟𝑖𝑚 ∀ 1 ≤ 𝑖 ≤ 𝑘
atunci:
𝑘
153
Capitolul 4
atunci:
𝑘 𝑒 +1
𝑝𝑖 𝑖 − 1
𝑠 𝑁 = , 𝑢𝑛𝑑𝑒 𝑠 𝑥 = 𝑠𝑢𝑚𝑎 𝑑𝑖𝑣𝑖𝑧𝑜𝑟𝑖𝑙𝑜𝑟 𝑙𝑢𝑖 𝑥
𝑝𝑖 − 1
𝑖=1
154
Algoritmi matematici
0 1 2 3 4 5 6
6 5 4 1 0 9 2
while ( x )
{
X[ ++A[0] ] = x % 10;
x /= 10;
}
i 0 1 2 3 4 5 i 0 1 2 3 4 5
A 5 9 9 6 2 1 B 5 9 8 2 4 9
155
Capitolul 4
i 0 1 2 3 4 5 6
C 0 8
transport = 1
i 0 1 2 3 4 5 6
C 0 8 8
transport = 1
i 0 1 2 3 4 5 6
C 0 8 8 9 6 0
transport = 1
i 0 1 2 3 4 5 6
C 6 8 8 9 6 0 1
156
Algoritmi matematici
if ( transport )
C[i] = transport;
C[0] = i;
}
i 0 1 2 3 i 0 1 2 3
A 3 1 3 1 B 2 9 9 0
i 0 1 2 3
A 3 2 3 1
157
Capitolul 4
i 0 1 2 3
A 3 2 3 1
i 0 1 2 3
A 3 2 3 0
158
Algoritmi matematici
Practic, dacă un număr are mai multe cifre ca celălalt, acel număr
este mai mare. Dacă ambele numere au acelaşi număr de cifre, atunci se
compară numerele cifră cu cifră, începând de la cea mai semnificativă cifră.
Aceaste comparaţii fie vor determina care număr este mai mare, fie vor
determina că numerele sunt egale.
return 0; // A == B
}
159
Capitolul 4
i 0 1 2 3 4
A 4 3 7 1 3
transport = 0
i 0 1 2 3 4
A 4 9 7 1 3
transport = 3
i 0 1 2 3 4
A 4 9 4 2 1
transport = 4
i 0 1 2 3 4 5
A 5 9 4 2 1 4
160
Algoritmi matematici
while ( transport )
{
A[ ++A[0] ] = transport % 10;
transport /= 10;
}
}
1213 ∙
413
––––––– (*)
3639
1213
4852
––––––– (+)
500969
161
Capitolul 4
i 0 1 2 3 4 5 6 7
A 4 3 1 2 1 0 0 0
B 3 3 1 4 0 0 0 0
Rez 6 0 0 0 0 0 0 0
i 0 1 2 3 4 5 6 7
Rez 6 9 3 12 0 0 0 0
i 0 1 2 3 4 5 6 7
Rez 6 9 6 13 4 0 0 0
A treia cifră:
i 0 1 2 3 4 5 6 7
Rez 6 9 6 19 6 8 0 0
162
Algoritmi matematici
Ultima cifră:
i 0 1 2 3 4 5 6 7
Rez 6 9 6 19 9 9 4 0
i 0 1 2 3 4 5 6 7
Rez 6 9 6 9 9 9 4 0
i 0 1 2 3 4 5 6 7
Rez 6 9 6 9 0 9 4 0
i 0 1 2 3 4 5 6 7
Rez 6 9 6 9 0 0 4 0
i 0 1 2 3 4 5 6 7
Rez 6 9 6 9 0 0 5 0
163
Capitolul 4
int s = 0, transport = 0;
for ( int i = 1; i <= C[0]; ++i )
{
s = C[i] + transport;
C[i] = s % 10;
transport = s / 10;
}
if ( transport )
C[ ++C[0] ] = transport;
}
164
Algoritmi matematici
62117 : 13 = 04778
0
–––––––
62
52
–––––––
101
91
–––––––
101
91
–––––––
107
104
–––––––
3
165
Capitolul 4
return rest;
}
166
Algoritmi backtracking
5. Algoritmi
backtracking
Am prezentat până acum descrierea generală a tehnicii de
programare numită backtracking, împreună cu nişte probleme elementare
care se rezolvă cu ajutorul acestei tehnici. Problemele prezentate anterior nu
au însă nicio aplicabilitate intrinsecă, acestea aparând de cele mai multe ori
doar ca subprobleme în cadrul altor probleme mai complexe.
167
Capitolul 5
CUPRINS
168
Algoritmi backtracking
Exemplu:
labirint.in labirint.out
2 11
00 12
00 22
11
21
22
169
Capitolul 5
fie back(k, lin, col, N, A, st) funcţia la care am făcut referire mai sus.
Această funcţie poate fi implementată astfel:
Reţine (lin, col) în st[k]
Dacă (lin, col) == (N, N) afişează conţinuturile stivei st
Altfel execută:
o Pentru fiecare vecin valid (n_lin, n_col) execută
A[lin][col] = true
Apelează recursiv
back(k + 1, n_lin, n_col, N, A, st)
A[lin][col] = false
st A
true false
(1, 1)
false false
st A
(1, 2) true true
(1, 1) false false
st A
(2, 2) true true
(1, 2)
(1, 1) false false
171
Capitolul 5
if ( valid(n_lin, n_col, N, A) )
{
A[lin][col] = true;
back(k+1, n_lin, n_col, N, A, st, out);
A[lin][col] = false;
}
}
}
172
Algoritmi backtracking
Exemplu:
cal.in cal.out
5 304
173
Capitolul 5
void back(int k, int lin, int col, int N, bool A[maxn][maxn], stiva st[],
int &nr) // nr va contine rezultatul cerut
{
st[k].lin = lin; st[k].col = col;
if ( k == N*N )
{
++nr;
return;
}
for ( int i = 0; i < 8; ++i )
{
int n_lin = lin + dx[i];
int n_col = col + dy[i];
if ( valid(n_lin, n_col, N, A) )
{
A[lin][col] = true;
back(k + 1, n_lin, n_col, N, A, st, nr);
A[lin][col] = false;
}
}
}
174
Algoritmi backtracking
Exemplu:
sub.in sub.out
2 3
2
23
1
13
12
123
Vom folosi o stivă de valori booleane st, unde st[i] = 1 dacă numărul
i face parte din submulţimea curentă şi 0 în caz contrar. La fiecare pas k
vom depune în stivă valoarea 0, după care vom trece la pasul următor. La
revenire din recursivitate vom depune în stivă valoarea 1, după care vom
efectua încă un apel recursiv. Când am ajuns la pasul k > N, afişăm
numerele de ordine a poziţiilor pe care se găseşte 1 în stivă. Dacă există cel
puţin o poziţie pe care se găseşte 1, trecem la următoarea linie la sfârşit, în
caz contrar fiind vorba de mulţimea vidă. În pseudocod algoritmul este
următorul: fie back(k, N, st) funcţia care rezolvă problema:
Dacă k > N execută
o Pentru fiecare i de la 1 la N execută
Dacă st[i] == 1 afişează i
o Dacă s-a afişat cel puţin un număr, treci la linie nouă
Altfel execută
o Pentru fiecare i de la 0 la 1 execută
st[k] = i
Apelează recursiv back(k + 1, N, st)
175
Capitolul 5
176
Algoritmi backtracking
return;
}
Exemplu:
regine.in regine.out
8 92
177
Capitolul 5
O soluţie este:
return true;
}
// nr va contine rezultatul
void back(int k, int N, int st[], bool fol[], int &nr)
{
if ( k > N )
{
++nr;
return;
}
fol[i] = true;
back(k + 1, N, st, fol, nr);
fol[i] = false;
}
}
Exerciţii:
a) Implementaţi un program care foloseşte backtracking în plan
pentru rezolvarea problemei.
b) Comparaţi timpul de execuţie al celor doi algoritmi. Încercaţi să
găsiţi optimizări.
179
Capitolul 5
1. 𝑃=𝐴
2. 𝑋 ∩ 𝑌 = ∅, ∀𝑋, 𝑌 ∈ 𝑃 ş𝑖 𝑋 ≠ 𝑌
Exemplu:
partitii.in partitii.out
3 {1, 2, 3}
{1, 2} {3}
{1, 3} {2}
{1} {2, 3}
{1} {2} {3}
180
Algoritmi backtracking
181
Capitolul 5
if ( !primul )
out << "} ";
}
182
Algoritmi generali
6. Algoritmi
generali
Există algoritmi care nu pot fi încadraţi într-o anume categorie fără a
defini nişte categorii fie foarte restrictive, fie foarte vagi. Aceştia au, de
obicei, aplicabilitate în nişte probleme practice foarte specifice. În cele ce
urmează vom prezenta câţiva astfel de algoritmi, care considerăm că îşi
merită totuşi propria secţiune, datorită eleganţei acestora şi datorită
aplicaţiilor teoretice care pot fi găsite pentru aceştia.
183
Capitolul 6
CUPRINS
184
Algoritmi generali
Exemplu:
kmp.in kmp.out
abbbbbabaabbbaab 1
abbbaab
185
Capitolul 6
i 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
S1[i] a b b b b b a b a a b b b a a b
S2[j] a b b b a a b
j 1 2 3 4 5 6 7
i 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
S1[i] a b b b b b a b a a b b b a a b
S2[j] a b b b a a b
j 1 2 3 4 5 6 7
i 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
S1[i] a b b b b b a b a a b b b a a b
S2[j] a b b b a a b
j 1 2 3 4 5 6 7
186
Algoritmi generali
i 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
S1[i] a b b b b b a b a a b b b a a b
S2[j] a b b b a a b
j 1 2 3 4 5 6 7
187
Capitolul 6
i 1 2 3 4 5 6 7
S2[i] a b b b a a b
π[i] 0 0 0 0 1 1 2
188
Algoritmi generali
int main()
{
char S1[maxn + 1], S2[maxn + 1];
int pi[maxn];
ifstream in("kmp.in");
in.getline(S1 + 1, maxn - 1);
in.getline(S2 + 1, maxn - 1);
in.close();
Exemplu:
expr.in expr.out
7*2/3+6-(2+1) 7.66667
190
Algoritmi generali
191
Capitolul 6
return ret;
}
192
Algoritmi generali
return ret;
}
// +, -, * sau /
ret = eval(expr, nivel + 1, k);
while ( expr[k] == oper[nivel][0] ||
expr[k] == oper[nivel][1] )
{
int poz = k++;
ret = operatie(ret,
eval(expr, nivel + 1, k),
expr[poz]);
}
return ret;
}
193
Capitolul 6
2
înţelegem ab, este de fapt egal cu 21 = 2, nu cu (21 )2 = 4). Aceste
subtilităţi, precum şi implementarea suportului pentru funcţii, sunt intuitive
şi uşor de implementat în varianta recursivă, dar mai dificil de implementat
în varianta itertivă.
Prezentăm în continuare funcţiile relevante pentru varianta iterativă.
195
Capitolul 6
Exerciţii:
a) Modificaţi variantele recursive astfel încât să construiască într-un
vector dat ca parametru forma poloneză postfixată a expresiei
evaluate.
b) Aceeaşi cerinţă pentru forma poloneză prefixată. Forma poloneză
prefixată îşi are toţi operatorii urmaţi de operanzii asociaţi. De
exemplu, 2 + 3 * 2 ≡ + * 3 2 2.
c) Folosiţi o singură stivă atât pentru formarea formei poloneze cât
şi pentru evaluarea acesteia, în cadrul algoritmului iterativ.
d) Găsiţi un algoritm iterativ care construieşte forma poloneză
prefixată a unei expresii.
e) Implementaţi funcţiile sin şi cos atât în variantele recursive cât şi
în varianta iterativă prezentată.
f) Consideraţi existenţa parantezelor drepte în cadrul expresiei date.
Modificaţi algoritmii daţi astfel încât acestea să fie tratate ca
însemnând ridicarea la pătrat a subexpresiei din interior. De
exemplu, [3+2]*2-1 = 49.
196
Introducere în S.T.L.
7. Introducere
în S.T.L.
Biblioteca S.T.L. pune la dispoziţia programatorilor C++ mai multe
structuri de date generice, lucru care scuteşte programatorii de timpul şi
efortul implementării acestor structuri de la zero. În aceeaşi bibliotecă se
regăsesc şi diferiţi algoritmi care pot reduce timpul necesar rezolvării unei
probleme.
Avantajul folosii bibliotecii S.T.L. constă, în primul rând, în
reducerea timpului necesar implementării unui algoritm. În al doilea rând,
aceste containere au fost implementate de o echipă de profesionişti de-a
lungul unei perioade lungi de timp şi testate foarte riguros, deci putem fi
siguri de corectitudinea şi eficienţa acestora.
În cele ce urmează vom prezenta pe scurt principalele containere şi
algoritmi din S.T.L. şi modul de folosire a acestora în nişte situaţii concrete.
197
Capitolul 7
CUPRINS
198
Introducere în S.T.L.
a) Containerul vector
vector<T> aniPari;
vector<int> aniPari;
for ( int i = 2002; i <= 2012; i += 2 )
aniPari.push_back(i);
199
Capitolul 7
aniPari.pop_back();
vector<int>::iterator it = aniPari.begin();
while ( it != aniPari.end() && *it != 2010 )
++it;
aniPari.insert(it, 1990);
200
Introducere în S.T.L.
vector<int> aniPari;
for ( int i = 2002; i <= 2012; i += 2 )
aniPari.push_back(i);
Pentru a compara dacă doi vectori sunt egali (au toate elementele de
pe pe poziţii identice egale) putem folosi pur şi simplu operatorul ==.
Acesta se poate aplica şi altor containere.
sort(numere.begin(), numere.end());
b) Containerul deque
deque<int> minusPlus;
201
Capitolul 7
minusPlus.pop_front();
for ( int i = -9; i < 0; ++i )
minusPlus.push_front(i);
deque<int>::iterator it;
for ( it = minusPlus.begin(); it != minusPlus.end(); ++it )
cout << *it << " ";
-1 -2 -3 -4 -5 -6 -7 -8 -9 2 3 4 5 6 7 8
Un deque este mai eficient decât un vector atunci când avem mai
multe operaţii de inserare, deoarece nu au loc realocări de memorie. Deque-
urile au însă o implementare internă mai complexă, care poate să le facă mai
ineficiente în unele situaţii.
Un deque nu trebuie folosit decât dacă avem nevoie să ştergem şi să
adăugăm elemente în ambele capete ale unei structuri liniare, situaţie care
apare în unii algoritmi.
202
Introducere în S.T.L.
c) Containerul list
list este un container care are la bază o listă dublu înlănţuită. Acest
lucru înseamnă că fiecare element are o locaţie de memorie imprevizibilă şi
câte un pointer la elementul precedent şi următor din listă.
O listă suportă inserarea şi ştergerea elementelor de oriunde în timp
constant, mutarea elementelor în timp constant şi iterarea elementelor în
timp liniar.
Comparativ cu vectorii şi deque-urile, listele sunt mai eficiente
atunci când efectuăm multe inserări, mutări şi ştergeri de elemente din listă.
list<int> nrPrime;
nrPrime.push_back(2);
nrPrime.push_back(5);
cout << nrPrime.front() << " " << nrPrime.back(); // afiseaza 2 5
list<int>::iterator it = nrPrime.begin();
while ( it != nrPrime.end() && *it != 5 )
++it;
nrPrime.insert(it, 3);
203
Capitolul 7
nrPrime.erase(nrPrime.begin());
Putem şterge elemente din listă şi pe baza valorii lor, caz în care nu
trebuie să folosim iteratori. Pentru acest lucru vom folosi funcţia remove.
Secvenţa de mai jos şterge numărul 15 dintr-o listă de numere prime, dacă
acesta există. Dacă nu există, nu se întâmplă nimic.
nrPrime.remove(15);
int main()
{
list<int> nrPrime;
nrPrime.push_back(2);
nrPrime.push_back(5);
nrPrime.push_back(666013);
204
Introducere în S.T.L.
nrPrime.sort();
nrPrime.unique();
for ( list<int>::iterator it = nrPrime.begin(); it != nrPrime.end(); ++it )
cout << *it << " ";
Sau:
a) Containerul stack
205
Capitolul 7
Ultimul intrat
...
Primul intrat
Fig. 7.2.1.1. – o stivă L.I.F.O.
stack<string> studenti;
studenti.push("Ionescu");
studenti.push("Popescu");
studenti.push("Georgescu");
// Georgescu
cout << "Primul care isi va sti nota este: " << studenti.top();
206
Introducere în S.T.L.
b) Containerul queue
queue<string> cumparatori;
cumparatori.push("Vlad");
cumparatori.push("Alex");
cumparatori.push("George");
207
Capitolul 7
cumparatori.pop();
cout << cumparatori.front(); // afiseaza Alex
c) Containerul priority_queue
priority_queue<int> note;
208
Introducere în S.T.L.
struct cmp
{
bool operator () (const int &x, const int &y) const
{
return x % 17 < y % 17;
}
};
int main()
{
priority_queue<int, vector<int>, cmp> note;
note.push(80); note.push(97); note.push(100); note.push(30);
return 0;
}
209
Capitolul 7
set<int> nrUnice;
multiset<int> nrMultiple;
210
Introducere în S.T.L.
if ( rezultatSet.second == false )
cout << "Numarul " << *rezultatSet.first
<< " exista deja in set." << endl;
else
cout << "Numarul " << *rezultatSet.first
<< " a fost inserat in set." << endl;
set<int>::iterator it;
multiset<int>::iterator jt;
for ( int i = 0; i < 5; ++i )
{
if ( (it = nrUnice.find(rand() % 17)) == nrUnice.end() )
cout << "Elementul cautat nu exista in set" << endl;
else
cout << "Elementul " << *it << " exista in set" << endl;
211
Capitolul 7
nrUnice.insert(10); nrUnice.insert(12);
nrUnice.insert(9); nrUnice.insert(7);
cout << nrUnice.erase(10) << endl; // afiseaza 1 (s-a sters un 10)
cout << nrUnice.erase(13) << endl; // afiseaza 0 (nu s-a sters nimic)
cout << *nrUnice.begin() << endl; // afiseaza 7, cel mai mic numar
// conform relatiei <
nrUnice.erase(nrUnice.begin()); // se sterge 7, primul element
cout << *nrUnice.begin() << endl; // afiseaza 9
nrMultiple.insert(2010);
nrMultiple.insert(2011);
nrMultiple.insert(2010);
cout << nrMultiple.erase(2010) << endl; // afiseaza 2 (s-au sters
// ambele valori 2010)
nrMultiple.insert(1); nrMultiple.insert(1);
cout << nrMultiple.count(1) << endl; // afiseaza 2
212
Introducere în S.T.L.
struct cmp
{
bool operator () (const int &x, const int &y) const
{
return x % 17 < y % 17;
}
};
213
Capitolul 7
214
Introducere în S.T.L.
215
Capitolul 7
216
Introducere în S.T.L.
217
Capitolul 7
c) Containerul bitset
bitset<2011> aniBisecti;
218
Introducere în S.T.L.
bitset<5> test;
test.set(); // test = 11111 in baza 2
Atenţie: dacă valoarea dintr-un set de biţi este prea mare pentru a fi
reprezentabilă pe un întreg unsigned long, va apărea o eroare!
Alte metode importante sunt count, care numără câţi biţi au valoarea
1, any, care returnează true dacă există un bit cu valoarea 1 şi false altfel şi
metoda none care returnează true dacă toţi biţii au valoarea 0 şi false în caz
contrar.
bitset<32> numar(2010);
// afiseaza 00000000000000000000011111011010
cout << numar.to_string() << endl;
bitset<32> numar(2010);
numar >>= 1;
// afiseaza 00000000000000000000001111101101
cout << numar.to_string() << endl;
numar = ~numar;
219
Capitolul 7
// afiseaza 11111111111111111111110000010010
cout << numar.to_string() << endl;
numar ^= 31;
// afiseaza 11111111111111111111110000001101
cout << numar.to_string() << endl;
a) Algoritmul for_each
220
Introducere în S.T.L.
bool Impar(int x)
{
return x % 2 == 1;
}
int main()
{
vector<int> numere;
numere.push_back(10); numere.push_back(20);
numere.push_back(15); numere.push_back(35);
return 0;
}
221
Capitolul 7
d) Algoritmul equal
int main()
{
int vec1[4] = {1, 2, 3, 4}, vec2[4] = {18, 19, 20, 21};
cout << equal(vec1, vec1 + 4, vec2, EgalMod17) << endl; // afiseaza 1
return 0;
}
e) Algoritmul unique
222
Introducere în S.T.L.
De exemplu:
int main()
{
vector<int> numere;
for ( int i = 1; i <= 10; ++i )
{
numere.push_back(i - 1);
numere.push_back(i);
}
// numere contine 0 1 1 2 2 3 ... 9 9 10
return 0;
}
f) Algoritmul copy
223
Capitolul 7
g) Algoritmul reverse
vecInit = vec;
reverse(vec.begin(), vec.end());
if ( vec == vecInit ) // comparare folosind ==,
// putem folosi si functia equals
cout << "Vectorul dat este palindrom";
else
cout << "Vectorul dat nu este palindrom";
h) Algoritmul rotate
Funcţia rotate primeşte trei parametri: prim, mij, ult şi roteşte
secvenţa [prim, ult) în aşa fel încât elementul indicat de mij să devină
primul element. Nu se returnează nimic. De exemplu:
vector<int> numere;
for ( int i = 1; i <= 10; ++i )
numere.push_back(i);
i) Algoritmul random_shuffle
224
Introducere în S.T.L.
vector<int> nrPrime;
nrPrime.push_back(2); nrPrime.push_back(3);
nrPrime.push_back(5); nrPrime.push_back(7);
if ( binary_search(nrPrime.begin(), nrPrime.end(), 3) )
cout << "3 este numar prim!" << endl;
Returnează un pointer către cel mai mic, respectiv cel mai mare
element al unei colecţii. De exemplu:
vector<int> nrPrime;
nrPrime.push_back(2); nrPrime.push_back(3);
nrPrime.push_back(5); nrPrime.push_back(7);
226
Algoritmi genetici
8. Algoritmi
genetici
Pentru a înţelege algoritmii genetici, în primul rând trebuie să
înţelegem şi să cuantificăm modelul evoluţiei naturale (darwiniste). Modelul
evolutiv presupune existenţa unui habitat (a unui spaţiu de evoluţie)
guvernat de legi locale (condiţiile de mediu) în care speciile (populaţiile
reprezentate de indivizi) se supun următorului mecanism:
1. Pe baza selecţiei, un număr restrâns de indivizi din populaţia
iniţială vor constitui populaţia intermediară de părinţi (algoritmul
de selecţie trebuie să respecte paradigma conform căreia un
individ mai bine adaptat să aibe şanse mai mari de supravieţuire).
2. Din indivizii selectaţi ca şi părinţi, pe baza operatorilor genetici
(mutaţie, încrucişare, ...), se va reconstitui o nouă populaţie.
CUPRINS
228
Algoritmi genetici
𝑓𝑎𝑑𝑒𝑐 : 𝐶𝑟𝑜𝑚 → 𝑹
𝑓𝑎𝑑𝑒𝑐 𝑐1 = 0
229
Capitolul 8
înseamnă că c1 este soluţia cea mai bună (optimul global), iar pentru
230
Algoritmi genetici
Fig. 8.1.2.
Acest mod se poate realiza cel mai eficient prin procesarea mai
multor posibile soluţii simultan, cu diferite puncte de plecare aleatoare.
c) Selecţia
Există mai multe tipuri de selecţie, toate acestea având scopul ca
implementarea capacităţii de supravieţuire a unei soluţii să fie proporţională
cu valoarea funcţiei de adecvare, aici fiind de fapt implementată paradigma
evoluţiei darwiniste survival of the fittest. Una dintre cele mai simple
metode de selecţie este selecţia bazată pe ordonare (ierarhie), în care se
ordonează populaţia de soluţii astfel încât adaptarea lor să fie
descrescătoare, după care se selectează primii n indivizi doriţi.
232
Algoritmi genetici
1 2 3 4 5 6
1 1 1 1 1 1
233
Capitolul 8
d) Operatorii genetici
234
Algoritmi genetici
𝑋 = 𝑥0 , 𝑥1 , … , 𝑥𝑛−1
𝑜𝑝𝐼 𝑡 → 𝑥0, 𝑥1 , … , 𝑥𝑡−1 , 𝑦𝑡 , 𝑦𝑡 +1 … , 𝑦𝑛 −1
𝑌 = 𝑦0 , 𝑦1 , … , 𝑦𝑛 −1
sau
𝑦0 , 𝑦1 , … , 𝑦𝑡 −1 , 𝑥𝑡 , 𝑥𝑡+1 … , 𝑥𝑛−1
𝑋 = 𝑥0 , 𝑥1 , … , 𝑥𝑛−1
𝑜𝑝𝐼 𝑡0 , 𝑡1 , … , 𝑡𝑘−1 | 𝑡𝑖 < 𝑡𝑖+1 →
𝑌 = 𝑦0 , 𝑦1 , … , 𝑦𝑛 −1
→ 𝑥0 , 𝑥1 , … , 𝑥𝑡 0 −1 , 𝑦𝑡 0 , 𝑦𝑡 0 +1 , … , 𝑦𝑡 1 −1 , 𝑥𝑡 1 , 𝑥𝑡 1 +1 , … , 𝑥𝑡 2 −1 , 𝑦𝑡 2 , 𝑦𝑡 2 +1 , …
Exemplu:
expresie.in expresie.out
4 18 4412
**+
236
Algoritmi genetici
237
Capitolul 8
Cromozom Informaţie
A 4 * 4 * 2 + 3
B 1 * 4 * 1 + 2
C 4 * 4 * 1 + 2
Fig. 8.2.1. – Operatorul de recombinare aplicat problemei prezentate
#include <fstream>
#include <algorithm>
#include <cstdlib>
238
Algoritmi genetici
239
Capitolul 8
240
Algoritmi genetici
Exerciţii:
a) Comparaţi performanţa algoritmului cu performanţa celorlalţi doi
algoritmi menţionaţi.
b) Cum afectează constantele de la începutul programului timpul de
execuţie şi memoria folosită?
c) Cum am putea modifica operatorii genetici dacă numerele
folosite în expresie ar trebui să fie distincte?
Exemplu:
sistem.in sistem.out
32 312
A1 + A2 - A3 = 2
A1 * A2 / A3 = 1
Explicaţie:
3+1–2=2
3 *1/ 2 =1
Se poate observa că şi permutarea (1, 3, 2) ar fi fost validă.
241
Capitolul 8
𝑓 𝑖 −𝑔 𝑖
𝑖=1
unde f(i) este rezultatul evaluării expresiei i dacă înlocuim
fiecare necunoscută cu permutarea reprezentată de individul
curent, iar g(i) este rezultatul pe care trebuie să îl aibă expresia i,
adică numărul din dreapta egalului expresiei i. Am găsit o soluţie
atunci când există un individ X pentru care F2(X) = 0.
242
Algoritmi genetici
void calc_fitness()
{
for ( int cr = 1; cr < maxpop; ++cr )
{
A[cr].fitness = 0;
for ( int i = 1; i <= M; ++i )
{
int k = 1;
A[cr].fitness += abs(eval(k, cr, A, i) - egal[i]);
}
}
sort(A + 1, A + maxpop);
}
243
Capitolul 8
void noua_gen()
{
int v[maxn];
for ( int i = 1; i <= N; ++i )
v[i] = 0;
for ( int i = maxeli + 1; i < maxpop; ++i )
{
if ( rand() < prob_recomb )
{
int x1 = 1 + rand() % maxeli;
int poz = 1 + rand() % N;
for ( int j = 1; j <= poz; ++j )
{
v[ A[x1].var[j] ] = i;
A[i].var[j] = A[x1].var[j];
}
for ( int j = 1; j <= N; ++j )
if ( v[j] != i )
{
++poz;
A[i].var[poz] = j;
}
}
if ( rand() < prob_mutatie )
{
int x1 = 1 + rand() % N;
int x2 = 1 + rand() % N;
swap(A[i].var[x1], A[i].var[x2]);
}
}
}
Exerciţiu:
Implementaţi în întregime un program care rezolvă problema,
folosind, pe rând, ambii operatori genetici menţionaţi, precum şi ambele
funcţii de adecvare descrise. Comparaţi, pe mai multe date de intrare,
performanţele acestora.
244
Algoritmi de programare dinamică
9. Algoritmi de
programare
dinamică
Am prezentat într-un capitol anterior noţiunile de bază ale metodei
programării dinamice. În acelaşi capitol am prezentat câteva probleme
elementare rezolvate, urmând în acest capitol să prezentăm mai multe
aplicaţii, atât clasice cât şi mai avansate, ale programării dinamice. Tot aici
vom face tranziţia de la implementările mai apropiate de limbajul C folosite
până acum la implementări C++ care profită mai mult de avantajele oferite
de limbajul C++, cum ar fi librăria S.T.L.
245
Capitolul 9
CUPRINS
246
Algoritmi de programare dinamică
Exemplu:
lee.in lee.out
4 6
0111 11
0100 21
0000 31
1110 32
33
34
44
247
Capitolul 9
Iniţial avem:
A D
0111 0∞∞∞
0100 ∞∞∞∞
0000 ∞∞∞∞
1110 ∞∞∞∞
248
Algoritmi de programare dinamică
A D
0111 0∞∞∞
0100 1∞∞∞
0000 2∞∞∞
1110 ∞∞∞∞
Primul vecin al poziţiei (3, 1), este (4, 1), poziţia invalidă deoarece
conţine un zid. Al doilea vecin este poziţia (3, 2), poziţie validă. Funcţia se
autoapelează pentru această poziţie şi obţinem:
A D
0111 0∞∞∞
0100 1∞∞∞
0000 23∞∞
1110 ∞∞∞∞
Din poziţia (3, 2) prima dată se încearcă vecinul (4, 2), care este însă
un zid. Se va merge în continuare la stânga încă doi paşi, până obţinem:
A D
0111 0∞∞∞
0100 1∞∞∞
0000 23 4 5
1110 ∞∞∞∞
Din poziţia (3, 4) funcţia se va apela prima dată pentru poziţia (4, 4),
obţinându-se următoarea configuraţie:
A D
0111 0∞∞∞
0100 1∞∞∞
0000 2 3 4 5
1110 ∞∞∞6
249
Capitolul 9
A D
0111 0∞∞∞
0100 1∞∞6
0000 2 3 4 5
1110 ∞∞∞6
Singurul apel recursiv valid din poziţia (2, 4) este pentru poziţia (2,
3), obţinându-se:
A D
0111 0∞∞∞
0100 1 ∞ 7 6
0000 2 3 4 5
1110 ∞∞∞ 6
A D
0111 0∞∞∞
0100 1 ∞ 5 6
0000 2 3 4 5
1110 ∞∞∞ 6
#include <fstream>
250
Algoritmi de programare dinamică
251
Capitolul 9
Iniţial avem:
A D
0111 0∞∞∞
0100 ∞∞∞∞
0000 ∞∞∞∞
1110 ∞∞∞∞
p, u
Q: (1, 1)
252
Algoritmi de programare dinamică
A D
0111 0∞∞∞
0100 1∞∞∞
0000 ∞∞∞∞
1110 ∞∞∞∞
p, u
Q: (1, 1) (2,1)
La următorul pas se extrage Q[p], adică (2, 1). Singurul vecin valid
este (3, 1), care se actualizează şi se introduce în coadă:
A D
0111 0∞∞∞
0100 1∞∞∞
0000 2∞∞∞
1110 ∞∞∞∞
p, u
Q: (1, 1) (2,1) (3,1)
Similar, singurul vecin valid al poziţiei Q[p] = (3, 1) este (3, 2), care
se va introduce în coadă şi se va actualiza. La următorul pas se va extrage
(3, 2) din coadă şi se va introduce singurul vecin valid al acestei poziţii,
(3, 3):
A D
0111 0∞∞∞
0100 1∞∞∞
0000 2 34 ∞
1110 ∞∞∞∞
p, u
Q: (1, 1) (2,1) (3,1) (3, 2) (3, 3)
Se extrage (3, 3) din Q. Poziţia (3, 3) are doi vecini valizi: (3, 4) şi
(2, 3), care se actualizează şi se introduc amândoi în coadă:
253
Capitolul 9
A D
0111 0∞∞∞
0100 1∞5 ∞
0000 2 34 5
1110 ∞∞∞∞
p u
Q: (1, 1) (2,1) (3,1) (3, 2) (3, 3) (3, 4) (2, 3)
A D
0111 0∞∞∞
0100 1∞5 6
0000 2 34 5
1110 ∞∞∞6
p u
Q: (1, 1) (2,1) (3,1) (3, 2) (3, 3) (3, 4) (2, 3) (4, 4) (2, 4)
254
Algoritmi de programare dinamică
while ( p <= u )
{
pereche poz = Q[p++]; // extragerea primului element din coada
for ( int i = 0; i < 4; ++i )
{
int newx = poz.x + dx[i];
int newy = poz.y + dy[i];
256
Algoritmi de programare dinamică
while ( !Q.empty() )
{
pair<int, int> poz = Q.front(); // extragerea primului element
Q.pop(); // stergerea efectiva a primului element
Exerciţii:
a) Considerăm că o persoană porneşte din (1, 1) şi alta din (N, N).
Cele două persoane se mişcă exact în acelaşi timp. Scrieţi un
program care determină coordonatele spre care acestea ar trebui
să se îndrepte pentru a se întâlni cât mai rapid.
b) Daţi un exemplu pe care soluţia recursivă efectuează cu mult mai
mulţi paşi decât e necesar.
c) Modificaţi funcţia de afişare a drumului astfel încât să afişeze
toate drumurile minime existente.
257
Capitolul 9
Exemplu:
subsecv.in subsecv.out
10 11
-6 1 -3 4 5 -1 3 -8 -9 1
258
Algoritmi de programare dinamică
return max;
}
return max;
}
259
Capitolul 9
i 1 2 3 4 5 6 7 8 9 10
A[i] -6 1 -3 4 5 -1 3 -8 -9 1
S[i] -6
i 1 2 3 4 5 6 7 8 9 10
A[i] -6 1 -3 4 5 -1 3 -8 -9 1
S[i] -6 1
260
Algoritmi de programare dinamică
i 1 2 3 4 5 6 7 8 9 10
A[i] -6 1 -3 4 5 -1 3 -8 -9 1
S[i] -6 1 -2 4 9 8 11 3 -6 1
return max;
}
Exerciţii:
a) Modificaţi implementările date pentru a afişa şi poziţiile de
început şi de sfârşit a unei subsecvenţe de sumă maximă.
b) Se cere o subsecvenţă de produs maxim, iar numerele sunt reale.
Rezolvaţi problema atât pentru numere strict pozitive cât şi
pentru numere nenule (dar care pot fi negative).
c) Se dă o matrice şi se cere determinarea unui dreptunghi de sumă
maximă. Ultimul algoritm prezentat poate fi extins pentru
rezolvarea problemei în O(N3). Cum?
261
Capitolul 9
Exemplu:
subsir.in subsir.out
10 5
6 3 8 9 1 2 10 4 -1 11 6 8 9 10 11
262
Algoritmi de programare dinamică
i 1 2 3 4 5 6 7 8 9 10
A[i] 6 3 8 9 1 2 10 4 -1 11
L[i] 1 1 1 1 1 1 1 1 1 1
P[i] 0 0 0 0 0 0 0 0 0 0
i 1 2 3 4 5 6 7 8 9 10
A[i] 6 3 8 9 1 2 10 4 -1 11
L[i] 1 1 2 1 1 1 1 1 1 1
P[i] 0 0 1 0 0 0 0 0 0 0
263
Capitolul 9
i 1 2 3 4 5 6 7 8 9 10
A[i] 6 3 8 9 1 2 10 4 -1 11
L[i] 1 1 2 3 1 2 4 3 1 5
P[i] 0 0 1 3 0 5 4 6 0 7
#include <fstream>
in >> N;
for ( int i = 1; i <= N; ++i )
in >> A[i];
in.close();
}
264
Algoritmi de programare dinamică
int cmlscm(int A[], int N, int L[], int P[]) int main()
{ {
for ( int i = 1; i <= N; ++i ) int N;
L[i] = 1, P[i] = 0; int A[maxn], L[maxn], P[maxn];
return sol;
}
void reconst(int poz, int P[], int A[],
ofstream &out)
{
if ( P[poz] )
reconst(P[poz], P, A, out);
Vom considera A ca fiind vectorul citit şi vom folosi încă doi vectori
L şi P, dar care nu vor avea aceeaşi semnificaţie ca până acum.
Mai întâi iniţializăm vectorul L cu valoarea infinit. Aplicăm apoi
următorul algoritm:
265
Capitolul 9
La pasul (1), dacă A[i] este mai mare decât toate elementele diferite
de infinit din L, atunci A[i] se va suprascrie peste cea mai din stânga
valoare egală cu infinit. Putem implementa acest pas eficient în timp O(log
N) folosind o căutare binară.
i 1 2 3 4 5 6 7 8 9 10
A[i] 6 3 8 9 1 2 10 4 -1 11
L[i] inf inf inf inf inf inf inf inf inf inf
P[i]
i 1 2 3 4 5 6 7 8 9 10
A[i] 6 3 8 9 1 2 10 4 -1 11
L[i] 6 inf inf inf inf inf inf inf inf inf
P[i] 1
266
Algoritmi de programare dinamică
i 1 2 3 4 5 6 7 8 9 10
A[i] 6 3 8 9 1 2 10 4 -1 11
L[i] 3 inf inf inf inf inf inf inf inf inf
P[i] 1 1
i 1 2 3 4 5 6 7 8 9 10
A[i] 6 3 8 9 1 2 10 4 -1 11
L[i] -1 2 4 10 11 inf inf inf inf inf
P[i] 1 1 2 3 1 2 4 3 1 5
#include <fstream>
in >> N;
for ( int i = 1; i <= N; ++i )
in >> A[i];
in.close();
}
267
Capitolul 9
int cbin(int st, int dr, int val, int L[]) int cmlscm(int A[], int N, int L[],
{ int P[])
while ( st < dr ) {
{ int lg = 0;
int m = (st + dr) / 2; for ( int i = 1; i <= N; ++i )
if ( L[m] < val ) {
st = m + 1; L[i] = inf;
else
dr = m; int k = cbin(1, lg + 1, A[i], L);
}
// creste lungimea celui mai lung
return st; // subsir?
} if ( L[k] == inf )
++lg;
void reconst(int N, int A[], int P[],
int val, ofstream &out) L[k] = A[i];
{ P[i] = k;
for ( int i = N; i; --i ) }
if ( P[i] == val )
{ return lg;
reconst(i - 1, A, P, val - 1, out); }
out << A[i] << ' ';
int main()
break; {
} int N, A[maxn], L[maxn], P[maxn];
} citire(A, N);
ofstream out("subsir.out");
out.close();
return 0;
}
Exemplu:
sircom.in sircom.out
gatcbccgaatabbat 10
gcbcataabbaggaacba gcbcatabba
Rezolvare
Pentru a rezolva problema vom încerca să găsim o formulă de
recurenţă pentru calculul lungimii celui mai lung subşir comun. Fie L[i][j] =
lungimea celui mai lung subşir comun al secvenţelor A[1, i] şi B[1, j],
pentru 1 ≤ i ≤ lungime(A) şi 1 ≤ j ≤ lungime(B). Să presupunem că avem
calculate toate valorile matricii L care preced elementul (p + 1, q + 1) (sunt
fie pe aceeaşi linie şi pe o coloană precedentă, fie pe o linie precedentă). Se
disting două cazuri:
1. Dacă A[p + 1] == B[q + 1], atunci putem adăuga caracterul
A[p + 1] celui mai lung subşir comun al secvenţelor A[1, p] şi
B[1, q], obţinând, pentru secvenţele A[1, p + 1] şi B[1, q + 1] un
subşir comun de lungime maximă care este mai lung cu un
caracter. Aşadar, L[p + 1][q + 1] = L[p][q] + 1.
2. Dacă A[p + 1] != B[q + 1], atunci nu putem extinde niciun
subşir de lungime maximă calculat anterior şi va trebui să salvăm
în L[p + 1][q + 1] lungimea celui mai lung subşir de lungime
maximă calculat până acuma. Această valoare este dată de
maximul dintre L[p][q + 1] şi L[p + 1][q].
269
Capitolul 9
return L[A.length()][B.length()];
}
270
Algoritmi de programare dinamică
if ( A[x - 1] == B[y - 1] )
{
reconst(x - 1, y - 1, A, B, L, out);
out << A[x - 1];
}
else
{
if ( L[x - 1][y] > L[x][y - 1] )
reconst(x - 1, y, A, B, L, out);
else
reconst(x, y - 1, A, B, L, out);
}
}
int main()
{
string A, B;
int L[maxn][maxn];
citire(A, B);
ofstream out("sircom.out");
out.close();
return 0;
}
271
Capitolul 9
liniei curente, iar celălalt vector, L2, va reprezenta chiar linia curentă. Noua
formă a formulei de recurenţă este:
𝐿1 𝑗 − 1 + 1 𝑑𝑎𝑐ă 𝐴 𝑖 = 𝐵[𝑗]
𝐿2 𝑗 =
max
(𝐿1 𝑗 , 𝐿2 𝑗 − 1 ) 𝑎𝑙𝑡𝑓𝑒𝑙
return L1[B.length()];
}
Exerciţii:
a) Afişaţi întreaga matrice L pentru a înţelege mai bine formula de
recurenţă.
b) Afişaţi toate subşirurile comune de lungime maximă.
c) În implementarea de mai sus am transmis parametrii A şi B prin
referinţă. Unde era indicat să se folosească transmitere prin
referinţă constantă?
d) Scrieţi o implementare care foloseşte vectori clasici de caractere
în loc de tipul string.
e) Scrieţi un program care afişează acel subşir comun de lungime
maximă care este primul din punct de vedere alfabetic.
f) Se poate evita copierea vectorului L2? Dacă da, cum?
Exemplu:
inmopt.in inmopt.out
3 64
4325
273
Capitolul 9
#include <fstream>
274
Algoritmi de programare dinamică
return M[1][N];
}
Exerciţii:
a) De ce i trebuie să pornească de la N – 1 şi nu de la 1? Ce se
întâmplă dacă i merge de la 1 la N?
b) Concepeţi o modalitate de a reconstitui soluţia. Pentru exemplul
dat, o reconstituire a soluţiei ar putea fi (A1*A2)*A3.
275
Capitolul 9
Exemplu:
rucsac1.in rucsac1.out
4 13 22
10 9 32
4 10
5 12
13 20
276
Algoritmi de programare dinamică
F[j – Gi] + Vi este mai mare decât F[i], înseamnă că putem obţine o soluţie
mai bună adăugând obiectul i obiectelor cu greutatea j – Gi, a căror valoare
este F[j – Gi].
Este important să iterăm variabila j de la C la Gi şi nu invers
deoarece, în caz contrar, am putea ajunge în situaţia de a folosi un obiect de
mai multe ori: să presupunem că pentru a calcula un F[k] se foloseşte
valoarea F[k – Gi]. Atunci, dacă pentru a calcula F[k + Gi] se va folosi
valoarea F[k], obiectul i va fi folosit de două ori, lucru nepermis. Iterându-l
pe j de la C la Gi ne asigurăm că fiecare obiect va fi folosit o singură dată în
calculul lui F.
Complexitatea algoritmului este O(N∙C), deoarece parcurgem pentru
fiecare obiect citit vectorul F (de lungime C) pentru a-l actualiza.
Complexitatea este pseudopolinomială, dar în practică de cele mai multe
ori algoritmul este mai eficient decât ar sugera acest rezultat, deoarece nu se
parcurge aproape niciodată întreg vectorul F. Memoria suplimentară este
O(C).
Pentru a putea reconstitui soluţia, vom folosi un vector P unde
P[i] = ultimul element care a intrat în calculul valorii F[i]. Pentru a afla
soluţia, vom proceda similar cu celelalte probleme, atâta doar că nu mai
avem nevoie de o funcţie recursivă, deoarece de data aceasta nu ne
interesează ordinea de afişare şi că va trebui să pornim de la suma greutăţile
obiectelor alese de către algoritm şi nu de la C.
#include <fstream>
in.close();
}
277
Capitolul 9
while ( P[C] )
{
out << P[C] << ' ';
C -= A[ P[C] ].G;
}
}
Exerciţii:
a) Afişaţi indicii obiectelor crescător.
b) Afişaţi vectorii F şi P după fiecare actualizare a lor.
c) Daţi exemplu de un set de date de intrare pentru care algoritmul
execută un număr maxim de operaţii.
278
Algoritmi de programare dinamică
rucsac2.in rucsac2.out
4 13 32
10 9 223
4 10
5 12
13 20
279
Capitolul 9
Exerciţii:
a) Rezolvaţi o variantă a problemei în care fiecare obiect i poate fi
folosit de cel mult Nri ori.
b) Rezolvaţi o variantă a problemei în care obiectele pot avea
greutăţi negative.
c) Implementaţi un algoritm greedy pentru rezolvarea celor două
probleme. Cât de mare poate ajunge să fie diferenţa dintre soluţia
optimă şi soluţia dată de algoritmul greedy?
d) Implementaţi un algoritm genetic pentru rezolvarea celor două
probleme. Cât de aproape de soluţia optimă este acesta?
Comparaţi rezultatele algoritmului genetic cu rezultatele
algoritmului greedy.
Exemplu:
plata1.in plata1.out
7 23 1367
8 3 2 5 7 3 10
280
Algoritmi de programare dinamică
281
Capitolul 9
void dinamica(int A[], int N, int S) void random(int A[], int N, int S)
{ {
bool F[maxn]; int sel[maxn], nesel[maxn];
int P[maxn]; // sel[0], nesel[0] sunt numarul de
for ( int i = 0; i <= S; ++i ) // elemente
F[i] = P[i] = 0; // 0 == false sel[0] = sel[0] = 0;
int stmp = 0;
F[0] = true; srand((unsigned)time(0));
for ( int i = 1; i <= N; ++i ) for ( int i = 1; i <= N; ++i )
for ( int j = S; j >= A[i]; --j ) if ( rand() % 2 )
{ sel[ ++sel[0] ] = i, stmp += A[i];
F[j] |= F[j - A[i]]; else
nesel[ ++nesel[0] ] = i;
if ( F[j - A[i]] && !P[j] )
P[j] = i; while ( stmp != S )
} if ( stmp > S )
{
ofstream out("plata1.out"); int poz = 1 + (rand() % sel[0]);
while ( S ) stmp -= A[ sel[poz] ];
{ nesel[ ++nesel[0] ] = sel[poz];
out << P[S] << ' '; sel[poz] = sel[ sel[0]-- ];
S -= A[ P[S] ]; }
} else
out.close(); {
} int poz = 1 + (rand() % nesel[0]);
stmp += A[ nesel[poz] ];
sel[ ++sel[0] ] = nesel[poz];
nesel[poz] = nesel[ nesel[0]-- ];
}
ofstream out("plata1.out");
for ( int i = 1; i <= sel[0]; ++i )
out << sel[i] << ' ';
out.close();
}
Exerciţii:
a) Concepeţi un algoritm care afişează soluţia cu număr minim de
numere.
b) Cum se pot afişa toate soluţiile?
c) Ce se întâmplă dacă pot exista numere mai mari ca S? Dar dacă
există şi numere negative?
d) Încercaţi să găsiţi date de intrare pe care algoritmul randomizat
să ruleze mult timp.
282
Algoritmi de programare dinamică
Exemplu:
plata2.in plata2.out
7 23 11233
8 3 2 5 7 3 10
Exerciţii:
a) Scrieţi o implementare recursivă pentru ultimele patru probleme
prezentate. Folosiţi tehnica memoizării.
283
Capitolul 9
Exemplu:
nrpart.in nrpart.out
7 15
100 190569292
Explicaţie:
7=7
1+6=7
1+1+5=7
...
284
Algoritmi de programare dinamică
1. Numărăm doar partiţiile pantru care cel mai mic număr folosit
este K, acestea fiind în număr de nrpart(N – K, K), deoarece,
dacă adăugăm numărul K fiecărei partiţii a numărului N – K
(care nu va conţine termeni mai mici decât K) atunci obţinem
partiţii a numărului N.
2. Numărăm doar partiţiile lui N care conţin termeni strict mai mari
decât K. Acestea vor fi în număr de nrpart(N, K + 1), deoarece
o partiţie cu termeni de valoare cel puţin K care nu conţine
termeni de valoare K trebuie să aibă toţi termenii cel puţin
K + 1.
285
Capitolul 9
Exerciţii:
a) Scrieţi un program care afişează numărul de partiţii ale lui N
formate doar din numere prime.
b) Care este complexitatea algoritmului de numărare a partiţiilor?
c) Scrieţi un program care foloseşte o implementare iterativă a
formulei de recurenţă.
286
Algoritmi de programare dinamică
Exemplu:
lev.in lev.out
afara 3
afacere
287
Capitolul 9
Aşadar:
D[p + 1][q + 1] = 1 + minim(D[p][q],D[p + 1][q],D[p][q + 1]).
return D[A.length()][B.length()];
}
288
Algoritmi de programare dinamică
Exerciţii:
a) Complexitatea algoritmului de calcul a distanţei Levenshtein este
O(N∙M), unde N şi M reprezintă lungimile celor două şiruri.
Putem însă optimiza algoritmul dacă ştim că putem transforma
şirul A în şirul B într-un număr relativ mic de operaţii k. Cum ne
poate ajuta această informaţie?
b) Considerăm existenţa unor costuri pentru fiecare operaţie precum
şi pentru caracterele asupra cărora se efectuează operaţii. Scrieţi
un program care rezolvă această variantă a problemei.
c) Scrieţi un program care afişează noul şir A pentru fiecare
operaţie efectuată.
d) Scrieţi un program care determină numărul minim de caractere
care trebuie inserate într-un şir pentru a-l transforma într-un
palindrom.
Exemplu:
joc.in joc.out
3 115
4 5 6 3 2 1
9 100 6 8 4 7
289
Capitolul 9
Vom alege maximul celor două cazuri, deci S[i][j] = max(C1, C 2).
Ordinea de completare a matricei S este similară cu ordinea folosită
în algoritmul de rezolvare a recurenţei pentru problema înmulţirii optime a
unui şir de matrice. Vom începe cu i de la 2∙N – 2 şi cu j de la i + 2,
asigurându-ne în acest mod că nu vom folosi valori ale matricei necalculate
încă.
Iniţializarea valorilor S[i][i + 1] este necesară deoarece la fiecare pas
fie vom scădea 2 din j fie vom aduna 2 la i, această iniţializare având rolul
de a evita unele cazuri particulare care pot apărea din cauza acestui lucru.
#include <fstream>
using namespace std;
const int maxn = 101;
290
Algoritmi de programare dinamică
return S[1][N];
}
int main()
{
int N, V[maxn], S[maxn][maxn];
citire(V, N);
ofstream out("joc.out");
out << joc(V, N, S);
out.close();
return 0;
}
Exerciţii:
a) Modificaţi algoritmul astfel încât să afişeze fiecare număr ales
împreună cu jucătorul care a ales acel număr.
b) Rezolvaţi problema considerând că se pot alege k numere
consecutive dintr-un capăt al şirului.
c) Reduceţi memoria folosită de algoritm la O(N).
d) Modificaţi implementarea prezentată astfel încât să afişeze care
jucător câştigă jocul.
e) Rezolvaţi problema considerând 3 jucători şi 3∙N numere.
291
Capitolul 9
Exemplu:
rmq.inrmq.out
93 1
819344526 2
13 9
58
33
Fie M[i][j] = cel mai mic număr din subsecvenţa A[j, j + 2i – 1].
Altfel spus, M[i][j] reprezintă cel mai mic număr din subsecvenţa care
începe pe poziţia j şi are lungimea 2i. Vom prezenta mai întâi modul de
construcţie al acestei matrici iar apoi algoritmul prin care vom răspunde la
întrebări.
Pentru i = 0 obţinem subsecvenţe de forma A[j, j], aşadar avem
M[0][j] = A[j] pentru fiecare element j.
Fie log2[i] = parte întreagă din log 2(i) pentru fiecare i de la 0 la n.
Pentru fiecare i astfel încât 0 < i ≤ log2[N] vom calcula vectorul M[i] astfel:
pentru fiecare j ≥ 1 astfel încât să aibă loc j + 2i – 1 ≤ N vom efectua operaţia
M[i][j] = min(M[i – 1][j], M[i – 1][j + 2i – 1]).
Să demonstrăm că acest mod de calculare este corect:
292
Algoritmi de programare dinamică
293
Capitolul 9
i\j 1 2 3 4 5 6 7 8 9
0 8 1 9 3 4 4 5 2 6
1
1
3
3
1
4
4
2
2
1
1
3
2
3
2
2
1
3
1
Fig. 9.14.1. – Modul de execuţie al algoritmului R.M.Q.
294
Algoritmi de programare dinamică
#include <fstream>
using namespace std;
const int maxn = 101;
const int maxlog = 7;
int main()
{
int N, T, log2[maxn], M[maxlog][maxn];
ifstream in("rmq.in");
citire(M, N, T, in);
preproc(N, M, log2);
ofstream out("rmq.out");
int x, y;
while ( T-- )
{
in >> x >> y;
int L = log2[y - x + 1];
295
Capitolul 9
Exerciţii:
a) Modificaţi algoritmul astfel încât să afişeze poziţia numărului
minim în şirul dat.
b) Modificaţi algoritmul astfel încât să afişeze cel mai mare element
din şir, precum şi poziţia acestuia.
c) Extindeţi algoritmul pentru găsirea celui mai mic sau celui mai
mare element dintr-un dreptunghi al unei matrice.
d) Ce se întâmplă dacă interschimbăm cele două dimensiuni ale
matricei? Comparaţii timpii de execuţie a celor două variante de
implementare şi încercaţi să explicaţi eventualele diferenţe.
Exemplu:
paran.in paran.out
3 1
100
-2 -1
296
Algoritmi de programare dinamică
şi
𝑗 −1 𝐴 𝑖 𝑘 ∙ 𝐴 𝑘 + 1 𝑗 − 𝑇 𝑖 [𝑘] ∙ 𝑇[𝑘 + 1][𝑗] 𝑜𝑝 𝑘 = −1
𝐹𝑖 𝑗 = 𝐹 𝑖 𝑘 ∙ 𝐹 𝑘 + 1 [𝑗] 𝑜𝑝 𝑘 = −2
𝑘=𝑖 𝐹 𝑖 𝑘 ∙ 𝐹 𝑘 + 1 𝑗 + 𝑇 𝑖 [𝑘] ∙ 𝑇[𝑘 + 1][𝑗] 𝑜𝑝 𝑘 = −3
298
Algoritmi de programare dinamică
T[i][j] = t;
F[i][j] = f;
}
return T[1][N];
}
299
Capitolul 9
9.15. Concluzii
Am prezentat în acest capitol probleme a căror soluţii folosesc
programarea dinamică. Metoda programării dinamice este o metodă foarte
utilă pentru rezolvarea problemelor de informatică, dar este şi metoda cea
mai grea de stăpânit, întrucât problemele care se rezolvă printr-un algoritm
de programare dinamică pot fi foarte variate, deci este nevoie de experienţă
pentru a putea găsi anumite recurenţe..
Propunem aşadar spre rezolvare următoarele probleme:
300
Algoritmi de geometrie computaţională
10. Algoritmi de
geometrie
computaţională
Toate problemele de informatică au la bază probleme matematice,
informatica fiind de fapt o ramură a matematicii aplicate. Până acum am
prezentat probleme şi algoritmi care au o legături cu domenii precum
algebra, teoria numerelor şi logica matematică.
În acest capitol vom prezenta metode de rezolvare a unor probleme
de geometrie cu ajutorul calculatorului. Acest domeniu de studiu se numeşte
geometrie computaţională. Această ramură a informaticii are aplicaţii
practice importante în programe de grafică (aplicaţii CAD, aplicaţii de
modelare 2d şi 3d etc.), proiectarea circuitelor integrate şi altele.
301
Capitolul 10
CUPRINS
302
Algoritmi de geometrie computaţională
float t = 0.0;
for ( int i = 0; i < 20; ++i ) t += 0.1;
t *= 10000000;
cout << (int)t;
struct Punct
{
double x, y;
Punct(double abscisa, double ordonata) : x(abscisa), y(ordonata) {}
Punct() {}
};
...
Punct P(1, 2);
cout << P.x << “ “ << P.y; // afiseaza 1 2
304
Algoritmi de geometrie computaţională
struct Dreapta
{
double a, b, c;
Dreapta(double p, double q, double r) : a(p), b(q), c(r) {}
Dreapta() {}
};
...
Dreapta d(1, -1, 0); // reprezinta dreapta din figura anterioară
305
Capitolul 10
𝑦2 – 𝑦1
𝑚𝑑 = .
𝑥2 – 𝑥1
306
Algoritmi de geometrie computaţională
𝑑1 : 𝑎1 𝑥 + 𝑏1 𝑦 + 𝑐1 = 0
𝑑2 : 𝑎2 𝑥 + 𝑏2 𝑦 + 𝑐2 = 0
𝑎2 𝑐1 − 𝑎1 𝑐2
𝑦=
𝑎1 𝑏2 − 𝑎2 𝑏1
307
Capitolul 10
return true;
}
308
Algoritmi de geometrie computaţională
⟺ 𝐵. 𝑦 − 𝐴. 𝑦 𝐶. 𝑥 − 𝐴. 𝑥 − 𝐶. 𝑦 − 𝐴. 𝑦 𝐵. 𝑥 − 𝐴. 𝑥 < 0
Dacă aceste două condiţie sunt îndeplinite, atunci ştim sigur că cele
două segmente se intersectează. Ne-am putea pune acum problema
determinării punctului de intersecţie dintre acestea. Dacă ştim că două
309
Capitolul 10
struct Segment
{
Punct A, B;
Segment(Punct P1, Punct P2) : A(P1), B(P2) {}
Segment() {}
};
int Orientare(const Punct &A, const Punct &B, const Punct &C)
{
double temp = (B.y – A.y)*(C.x – A.x) – (C.y – A.y)*(B.x – A.x);
if ( temp < 0 )
return -1;
else if ( temp == 0 )
return 0;
else
return 1;
}
310
Algoritmi de geometrie computaţională
1 𝑋1 𝑋2 𝑋 𝑋3 𝑋 𝑋1
𝐴 𝑃 = + 2 + ⋯+ 𝑁
2 𝑌1 𝑌2 𝑌2 𝑌3 𝑌𝑁 𝑌1
Y1 Y2 Y3 ... YN Y1
+ + + + +
Fig. 10.6.2. – Vizualizarea formulei de arie a unui poligon
311
Capitolul 10
312
Algoritmi de geometrie computaţională
𝑙
|𝐴 𝑃 | = 𝑖 + −1
2
𝑙[𝐴𝐵] = 𝑐𝑚𝑚𝑑𝑐 𝐴. 𝑥 − 𝐵. 𝑥 , 𝐴. 𝑦 − 𝐵. 𝑦 + 1
𝑙
𝑖= 𝐴 𝑃 − +1
2
313
Capitolul 10
314
Algoritmi de geometrie computaţională
parcursul algoritmului, minim va conţine cel mai mic element din secvenţa
secv.
O analiză intuitivă a algoritmului este simplă: de fiecare dată când
dăm de un element mai mic decât cel presupus a fi minimul global, revizuim
presupunerea făcută, considerând acest nou element ca fiind minimul global.
După parcurgerea tuturor elementelor, vom avea evident adevăratul minim
global.
Algoritmul lui Jarvis are un raţionament aproape identic. Fie P1
punctul ales care face sigur parte din înfăşurătoarea convexă. Vom
presupune că segmentul [P1PnewPct] este o muchie a înfăşurătorii convexe.
Iniţial vom considera newPct = 2. Parcurgem acum toate celelalte puncte
date. Dacă există un punct Pq, astfel încât Orientare(P1, Pq, PnewPct) > 0
(sau mai mic ca zero, nu are importanţă atâta timp cât suntem consistenţi în
alegere) atunci segmentul [P1Pq] are mai multe şanse să aibă restul
punctelor într-o singură parte decât segmentul [P1PnewPct]. Acest lucru se va
clarifica imediat. Vom seta newPct = q şi vom continua algoritmul, căutând
(fără a reporni căutarea!) un alt q astfel încât Orientare(P 1, Pq, PnewPct) > 0
(atenţie, newPct este acuma egal cu vechiul q).
La finalul acestui pas, PnewPct va face sigur parte din înfăşurătoarea
convexă. Mai mult, segmentul [P1PnewPct] va avea toate punctele date într-o
singură parte.
Se reia algoritmul de la următorul punct de pe înfăşurătoarea
convexă, adică PnewPct. Acesta va juca acum rolul lui P1. Se continuă până
când se ajunge din nou la punctul de început. Deoarece se execută O(N) paşi
pentru fiecare punct de pe înfăşurătoarea convexă deducem că timpul de
execuţie a întregului algoritm este O(Nh).
316
Algoritmi de geometrie computaţională
#include <iostream>
#include <vector>
317
Capitolul 10
struct Punct
{
double x, y;
Punct(double abscisa, double ordonata) : x(abscisa), y(ordonata) {}
Punct() {}
// se vor compara puncte, deci trebuie sa
// supraincarcam operatorii de egalitate
bool operator ==(const Punct &other)
{
return x == other.x && y == other.y;
}
bool operator !=(const Punct &other)
{
return x != other.x || y != other.y;
}
};
int Orientare(const Punct &A, const Punct &B, const Punct &C)
{
double temp = (B.y - A.y)*(C.x - A.x) - (C.y - A.y)*(B.x - A.x);
if ( temp < 0 )
return -1;
else if ( temp == 0 )
return 0;
else
return 1;
}
double x, y;
for ( int i = 1; i <= N; ++i )
{
cin >> x >> y;
P.push_back(Punct(x, y));
}
}
318
Algoritmi de geometrie computaţională
vector<Punct> CH;
CH.push_back(P[start]);
for ( int i = 0; i < CH.size(); ++i )
{
Punct nextPct = (CH[i] == P[0]) ? P[1] : P[0];
if ( nextPct != CH[0] )
CH.push_back(nextPct);
}
return CH;
}
int main()
{
int N;
vector<Punct> P;
Citire(P, N);
vector<Punct> CH = Jarvis(P);
cout << endl;
for ( int i = 0 ; i < CH.size(); ++i )
cout << CH[i].x << " " << CH[i].y << endl;
return 0;
}
Exerciţii:
a) În ce ordine se afişează punctele de pe înfăşurătoare?
b) Cum se poate modifica algoritmul astfel încât punctele de pe
înfăşurătoare să fie afişate în altă ordine?
c) Daţi exemplu de un set de puncte pe care algoritmul efectuează
un număr nefavorabil de paşi.
319
Capitolul 10
320
Algoritmi de geometrie computaţională
321
Capitolul 10
vector<Punct> Graham(vector<Punct> P)
{
int start = 0; // vectorii incep de la 0
for ( int i = 1; i < P.size(); ++i )
if ( P[i].x<P[start].x || (P[i].x == P[start].x && P[i].y>P[start].y) )
start = i;
Sorter::Sort(P, start);
P.insert(P.begin(), P[P.size() - 1]);
int nrH = 2;
for ( int i = 3; i < P.size(); ++i )
{
while ( nrH > 1 && Orientare(P[nrH - 1], P[nrH], P[i]) > 0 )
--nrH;
++nrH;
swap(P[nrH], P[i]);
}
322
Liste înlănţuite
11. Liste
înlănţuite
Acest capitol prezintă noţiunile elementare despre liste înlănţuite
(simplu înlănţuite, dublu înlănţuite şi circulare). Vor fi prezentate noţiuni
teoretice şi detalii de implementare.
Listele înlănţuite au anumite avantaje şi dezavantaje relativ la
tablouri (vectori). Principalul dezavantaj este că nu suportă accesul aleator:
pentru a accesa al k-lea element al unei liste, este necesar să parcurgem
toate elementele anterioare. Principalul avantaj este că listele suportă
inserări mai eficiente, mai ales la sfârşitul şi începutul acestora (acestea se
pot face în timp constant.
Aşadar, alegerea dintre liste şi tablouri trebuie făcută în funcţie de
natura problemei pe care vrem să o rezolvăm. Vom prezenta în acest capitol
şi alte avantaje şi dezavantaje.
323
Capitolul 11
CUPRINS
324
Liste înlănţuite
int *V;
325
Capitolul 11
struct nod
{
T informatie;
nod *link_;
};
326
Liste înlănţuite
struct nod
{
int info;
nod * link_;
}
327
Capitolul 11
328
Liste înlănţuite
329
Capitolul 11
// legătura
Temp->link_ = New;
// valoarea de returnat
return Old;
}
330
Liste înlănţuite
//legătura
Temp->link_ = New;
//valoarea de returnat
return Old;
}
}
331
Capitolul 11
Este foarte importantă ordinea (1) şi (2), deoarece în caz contrar prin
Temp->link_ = New; (1)
Spre adresa nodului 3 nu mai pointează nimic şi
New->link_ = Temp->link_; (2)
Creează structura din figura de mai jos, în care se pierd toate datele
de la nodul 3 încolo.
332
Liste înlănţuite
// daca noua valoarea este mai mica sau egala cu cea a primului
// element, atunci aceasta se adauga la inceput
if ( New->info <= Old->info )
{
New->link_ = Old; Old = New;
}
else
{
// caut unde trebuie adaugat noul nod
nod *Temp;
for ( Temp = Old; Temp->link_ != NULL; Temp = Temp->link_ )
if ( New->info <= Temp->link_->info )
break; // am gasit pozitia pe care trebuie adaugat nodul
if ( Temp->link_ == NULL ) // adaugare la sfarsit
{
New->link_ = NULL; Temp->link_ = New;
}
else // nodul se adauga undeva in interior
{
New->link_ = Temp->link_; Temp->link_ = New;
}
}
}
return Old;
}
333
Capitolul 11
delete ToDel;
}
return Old;
}
Dacă lista are un singur element sau este NULL, atunci returnăm
NULL;
335
Capitolul 11
delete ToDel;
}
else
{
nod *Temp;
for ( Temp = Old; Temp->link_->link_; Temp = Temp->link_ );
delete ToDel;
}
}
return Old;
}
336
Liste înlănţuite
Fig. 11.2.9. – Ştergerea unui nod din interiorul unei liste înlănţuite
337
Capitolul 11
view_rev(Old->link_);
cout << Old->info << ' ';
}
338
Liste înlănţuite
a) Stiva
b) Coada
Se derivează din listă, prin adăugarea condiţiei prin care ultimul nod
nu va pointa la NULL, ci va pointa la începutul listei. Această condiţie
restricţionează adăugarea şi ştergerea unor elemente la funcţiile add_mid şi
del_mid, deoarece lista nu mai are început şi sfârşit.
340
Liste înlănţuite
Exemplu:
cavaleri.in cavaleri.out
6 24135
341
Capitolul 11
nod *Temp;
for ( Temp = LISTA; Temp->link_ != NULL; Temp = Temp->link_ );
Temp->link_ = LISTA; // lista devine circulara
while ( nr < N - 1 )
{
for ( int i = 0; i < nr; ++i )
LISTA = LISTA->link_;
LISTA->link_ = LISTA->link_->link_;
delete Tmp;
++nr;
}
out.close();
}
int main()
{
int N;
ifstream in("cavaleri.in");
in >> N;
in.close();
return 0;
}
342
Liste înlănţuite
struct nod
{
T info;
nod *link_;
nod *_link; // pointer catre nodul anterior
};
343
Capitolul 11
struct Lista
{
nod *adrp;
nod *adru;
};
344
Liste înlănţuite
struct nod
{
int info;
nod *link_;
nod *_link;
};
Fig. 11.4.4. – Adăugarea unui nod la începutul unei liste dublu înlănţuite
Old->_link = New;
Old = New;
return Old;
}
345
Capitolul 11
Fig. 11.4.5. – Adăugarea unui nod la sfârşitul unei liste dublu înlănţuite
nod *Temp;
for ( Temp = Old; Temp->link_ != NULL; Temp = Temp->link_ );
Temp->link_ = New;
New->_link = Temp;
return Old;
}
346
Liste înlănţuite
Fig. 11.4.6. – Adăugarea unui nod în interiorul unei liste dublu înlănţuite
347
Capitolul 11
348
Liste înlănţuite
După care:
349
Capitolul 11
350
Liste înlănţuite
#include <iostream>
struct nod
{
int info;
nod *link_;
nod *_link;
};
if ( Old != NULL )
Old->_link = New;
Old = New;
}
Old->link_ = New;
New->_link = Old;
Old = New;
}
351
Capitolul 11
Temp->link_->_link = New;
Temp->link_ = New;
}
}
}
}
delete ToDel;
}
352
Liste înlănţuite
if ( Temp->link_ != NULL )
{
nod *ToDel = Temp->link_;
Temp->link_ = Temp->link_->link_;
if ( Temp->link_ != NULL )
Temp->link_->_link = Temp;
delete ToDel;
}
}
}
353
Capitolul 11
1
Profesor de informatică la Universitatea Stanford.
354
Teoria grafurilor
12. Teoria
grafurilor
Teoria grafurilor este un domeniu al matematicii care se ocupă cu
studiul structurilor matematice numite grafuri. Un graf este o reprezentare
abstractă a relaţiilor existente între elementele unui set suport. Elementele
din setul suport se numesc noduri, iar relaţiile existente între acestea se
numesc muchii sau arce (termen folosit uneori în cazul grafurilor orientate).
355
Capitolul 12
CUPRINS
356
Teoria grafurilor
357
Capitolul 12
12. Un graf complet este un graf fără bucle care are muchie între
oricare două noduri. Numărul de muchii al unui graf neorientat
complet este egal cu:
𝑉 ∙ ( 𝑉 − 1)
2
𝑉 ∙ ( 𝑉 − 1)
.
Numărul de grafuri neorientate cu N noduri este dat de formula:
𝑁∙(𝑁−1)
2 2
13. Un graf se numeşte planar dacă poate fi desenat în aşa fel încât
muchiile sale să nu se intersecteze.
14. Un graf se numeşte nul dacă are 0 noduri.
15. Un graf se numeşte infinit dacă are un număr infinit de noduri.
16. Un subgraf G‟ = (V‟, E‟) al unui graf G = (V, E) este un graf
obţinut din G prin eliminarea unor noduri şi a muchiilor
incidente acestora. Aşadar, V‟ ⊆ V şi E‟ ⊆ E.
17. Un graf parţial G‟ = (V, E‟) al unui graf G = (V, E) este un graf
obţinut din G prin eliminarea unor muchii. Aşadar, E‟ ⊆ E.
18. Un drum (sau drum elementar) este o secvenţă de noduri
distincte N1, N2, ..., Nk pentru care există muchie în graf între
oricare două noduri consecutive.
19. Lungimea unui drum este egală cu numărul de muchii existente
în drum. Costul unui drum este egal cu suma costurilor asociate
fiecărei muchii din drum.
20. Un graf G = (V, E) se numeşte bipartit dacă nodurile sale pot fi
partiţionate în două mulţimi X şi Y astfel încât V = X ∪ Y, X ∩
Y = ∅ şi oricare muchie a lui G are o extremitate în X şi cealaltă
extremitate în Y.
21. Se numeşte ciclu (sau ciclu elementar) un drum în care ultimul
nod coincide cu primul.
22. Un graf care nu conţine cicluri se numeşte aciclic.
23. Un graf neorientat G se numeşte conex dacă oricum am alege
două noduri i şi j ale sale, există un drum de la i la j.
358
Teoria grafurilor
359
Capitolul 12
360
Teoria grafurilor
361
Capitolul 12
a) Declaraţii
b) Iniţializări
graf *G[maxn];
for ( int i = 1; i < maxn; ++i ) G[i] = NULL;
362
Teoria grafurilor
363
Capitolul 12
364
Teoria grafurilor
365
Capitolul 12
return 0;
}
366
Teoria grafurilor
367
Capitolul 12
#include <fstream>
using namespace std;
const int maxn = 101;
int main()
{
int N, T[maxn];
bool V[maxn];
ifstream in("graf.in"); ofstream out("graf.out");
in >> N;
for ( int i = 1; i <= N; ++i )
{
in >> T[i];
V[i] = false;
}
for ( int i = 1; i <= N; ++i )
V[ T[i] ] = true;
for ( int i = 1; i <= N; ++i )
if ( !V[i] )
out << i << ' ';
in.close();
out.close();
return 0;
}
d) Alte probleme
368
Teoria grafurilor
Programul următor citeşte din fişierul dfs.in un graf dat prin lista de
muchii şi afişează în fişierul dfs.out nodurile grafului în ordinea în care au
fost parcurse de către algoritm, pornind de la nodul 1.
370
Teoria grafurilor
Exemplu:
dfs.in dfs.out
45 1324
13
23
31
34
14
371
Capitolul 12
În cazul arborilor binari (arbori în care fiecare nod are cel mult doi
fii), mai există şi parcurgerea în ordine. Aceasta presupune prelucrearea
nodului curent după apelul recursiv pentru subarborele stâng şi înainte de
apelul recursiv pentru subarborele drept. O astfel de implementare este
lăsată ca exerciţiu pentru cititor.
Aceste trei parcurgeri au aplicaţii în algoritmica arborilor de expresii
şi în determinarea componentelor tare conexe a unui graf.
372
Teoria grafurilor
373
Capitolul 12
#include <fstream>
#include <vector>
int x, y;
for ( int i = 1; i <= M; ++i )
{
in >> x >> y;
G[x].push_back(y);
G[y].push_back(x);
}
in.close();
}
374
Teoria grafurilor
return false;
}
int main()
{
int N, M;
int pred[maxn];
vector<int> G[maxn];
citire(G, N, M);
for ( int i = 1; i <= N; ++i )
pred[i] = -1;
ofstream out("dfs.out");
out << DFS(G, 1, pred);
out.close();
return 0;
}
Exerciţii:
a) Modificaţi algoritmul în aşa fel încât să şi afişeze un ciclu în
cazul existenţei unuia.
b) Cum s-ar putea rezolva problema pentru grafuri orientate?
c) Scrieţi un program care afişează toate ciclurile existente într-un
graf.
375
Capitolul 12
Exemplu:
dfs.in dfs.out
5 3
12
23
24
45
376
Teoria grafurilor
#include <fstream>
#include <vector>
377
Capitolul 12
ofstream out("dfs.out");
out << max;
out.close();
return 0;
}
Exerciţii:
a) Rezolvaţi aceeaşi problemă pe un graf ponderat (fiecare muchie
are asociat un anumit cost).
b) Modificaţi programul în aşa fel încât să afişeze şi nodurile
drumului.
c) Implementare iterativă
378
Teoria grafurilor
ales dacă folosim vectori pentru reţinerea listelor de adiacenţă sau liste
înlănţuite pe care nu dorim să le distrugem în timpul prelucrării. Funcţia
prezentată este doar orientativă; cititorul este sfătuit să o studieze şi să o
îmbunătăţească. În practică, astfel de implementări sunt foarte rar întâlnite
sau necesare.
if ( !depus )
--k;
}
}
Exerciţii:
a) Folosiţi parcurgerea în adâncime pentru a determina
componentele conexe ale unui graf.
b) Implementaţi varianta iterativă a parcurgerii în adâncime
folosind liste înlănţuite.
379
Capitolul 12
380
Teoria grafurilor
381
Capitolul 12
C.pop();
}
out.close();
}
382
Teoria grafurilor
383
Capitolul 12
384
Teoria grafurilor
cul[1] = 1;
for ( int i = 2; i <= N; ++i )
cul[i] = 0;
C.push(1);
while ( !C.empty() )
{
int nod = C.front();
C.push( G[nod][i] );
}
else if ( cul[ G[nod][i] ] == cul[nod] ) // trebuie schimbata
// culoarea
return false;
C.pop();
}
return true;
}
Exerciţii:
a) Implementaţi algoritmul prezentat folosind parcurgerea în
adâncime.
b) Considerăm funcţiile f definite pe mulţimea numerelor naturale
cu valori tot în mulţimea numerelor naturale. Daţi exemple de
funcţii pentru care graful format din muchiile (x, f(x)) este
bipartit şi de funcţii pentru care acelaşi graf nu este bipartit, când
x parcurge pe rând numerele naturale.
385
Capitolul 12
b) Sortarea topologică
386
Teoria grafurilor
queue<int> C;
for ( int i = 1; i <= N; ++i )
if ( gr[i] == 0 )
C.push(i);
while ( !C.empty() )
{
int nod = C.front();
out << nod << ' ';
if ( gr[ G[nod][i] ] == 0 )
C.push( G[nod][i] );
}
C.pop();
}
}
387
Capitolul 12
388
Teoria grafurilor
389
Capitolul 12
390
Teoria grafurilor
391
Capitolul 12
Semnificaţia primului set de valori este evidentă. Cel de-al doilea set
de valori ne ajută să verificăm pentru un nod nod dacă ştergerea lui
păstrează subarborele său conectat de restul grafului (adică dacă nod este
sau nu punct de articulaţie). Dacă ştergerea lui nod păstrează graful conex,
atunci minim[i] trebuie să fie strict mai mic decât D[nod], unde i este un fiu
al lui nod (cu alte cuvinte, există o muchie de întoarcere de la unul dintre
descendenţii lui nod la unul dintre strămoşii săi). Altfel, dacă minim[i] este
mai mare sau egal cu D[nod], nod este nod critic.
392
Teoria grafurilor
393
Capitolul 12
void DFS(vector<int> G[], int D[], int minim[], int nod, int tata, int ad,
ofstream &out)
{
D[nod] = minim[nod] = ad;
int nrf = 0; // nr de fii ai lui *nod* in arborele DFS
bool critic = 0;
for ( int i = 0; i < G[nod].size(); ++i )
if ( G[nod][i] != tata ) // am grija sa nu merg inapoi de unde am venit
if ( D[ G[nod][i] ] == -1 )
{
++nrf;
DFS(G, D, minim, G[nod][i], nod, ad + 1, out);
minim[nod] = min(minim[nod], minim[ G[nod][i] ]);
Exerciţii:
a) Problema se poate rezolva şi mai intuitiv, dar mai puţin eficient.
Care ar fi un algoritm naiv de rezolvare?
b) Modificaţi programul prezentat astfel încât să afişeze muchiile
critice, adică acele muchii a căror înlăturare ar deconecta graful.
c) Modificaţi programul prezentat astfel încât să afişeze
componentele biconexe ale grafului.
394
Teoria grafurilor
395
Capitolul 12
396
Teoria grafurilor
397
Capitolul 12
398
Teoria grafurilor
399
Capitolul 12
400
Teoria grafurilor
apeluri ş.a.m.d. Complexitatea algoritmului în cel mai rău caz este aşadar
O(N!).
Deoarece complexitatea algoritmului este O(N!) nu ne vom permite
să lucrăm cu grafuri cu mai mult de ~10 noduri. Aşadar, pentru o
implementare mai simplă vom folosi matrici de adiacenţă pentru
reprezentarea grafului. Considerând că avem costuri diferite de 0, elementul
de pe linia i şi coloana j a matricii de adiacenţă va fi egal cu c dacă costul
muchiei (i, j) este c şi cu 0 în caz contrar.
Rezolvarea problemei este foarte asemănătoare cu o parcurgere în
adâncime. Va trebui doar să adăugăm câţiva parametri suplimentari funcţiei
de parcurgere. Fie hamilton(G, nod, nr, c, cmin, V, st, sol) o funcţie care
construieşte în sol un drum hamiltonian de cost minim cmin. Variabila nod
reprezintă nodul curent, nr reprezintă numărul de noduri deja parcurse, st
reprezintă drumul curent iar c reprezintă costul drumului curent. Această
funcţie poate fi implementată în felul următor:
Dacă c > cmin sau V[nod] == true se iese din funcţie
Adaugă i în st
Dacă nr == N şi c < cmin execută
o Actualizează sol şi cmin
o Ieşire din funcţie
V[nod] = true
Pentru fiecare vecin i a lui nod execută
o Apel recursiv
hamilton(G, i, nr + 1, c + G[nod][i], cmin, V, st, sol)
V[nod] = false
401
Capitolul 12
return;
}
V[nod] = true;
for ( int i = 1; i <= N; ++i )
if ( G[nod][i] )
hamilton(G, N, i, nr + 1,
c + G[nod][i], cmin,
V, st, sol);
V[nod] = false;
}
402
Teoria grafurilor
404
Teoria grafurilor
405
Capitolul 12
406
Teoria grafurilor
Exerciţii:
a) Ce se întâmplă dacă folosim const int inf = 1 << 30; ?
b) Modificaţi algoritmul astfel încât să determine dacă un graf este
conex (sau tare conex).
c) Memoraţi, pentru fiecare pereche (i, j) din cadrul algoritmului de
calculare a costurilor minime, nodul k intermediar ales. Scrieţi o
funcţie de reconstituire care foloseşte aceste informaţii pentru a
găsi mai eficient drumurile.
d) Modificaţi algoritmul de reconstituire astfel încât să afişeze
nodurile unui drum în loc de muchiile unui drum.
408
Teoria grafurilor
409
Capitolul 12
drum(P[N], P, out);
out << N << ' ';
}
410
Teoria grafurilor
int main()
{
int N, M, D[maxn], P[maxn];
vector<PER> G[maxn];
citire(G, N, M);
Dijkstra(G, N, D, P);
ofstream out("dijkstra.out");
out << D[N] << '\n'; // costul minim
drum(N, P, out); // drumul in sine
out.close();
return 0;
}
411
Capitolul 12
Noul algoritm este mult mai eficient atunci când nu avem foarte
multe muchii, dar este dificil de implementat. Din fericire, biblioteca S.T.L.
ne vine în ajutor cu tipul priority_queue, care este de fapt un heap. Acest
container a fost prezentat pe scurt deja. Singurul lucru care îngreunează
puţin implementarea este faptul că avem nevoie de o modalitate de a ordona
coada de priorităţi după un criteriu dat de noi, deoarece în aceasta vom
reţine etichetele nodurilor, iar ordonarea vrem să se facă după distanţele
minime până la acele noduri. Mai mult, ştim că priority_queue se comportă
ca un max-heap, iar noi avem nevoie de un min-heap.
Pentru a rezolva aceste probleme, vom insera în coadă perechi de
forma (D[nod], nod), folosind utilitarul pair. Ştim că acest container are
definite relaţii de ordine în funcţie de prima componentă (avem nevoie de
412
Teoria grafurilor
vector<PER>::iterator i;
for ( i = G[min].begin(); i != G[min].end(); ++i )
if ( D[min] + i->second < D[ i->first ] )
{
D[ i->first ] = D[min] + i->second;
P[ i->first ] = min;
413
Capitolul 12
414
Teoria grafurilor
415
Capitolul 12
416
Teoria grafurilor
Pasul 1
i 1 2 3 4
Q 1
D 0 inf inf inf
P 0
417
Capitolul 12
Pasul 2 Pasul 3
i 1 2 3 4 i 1 2 3 4
Q 1 2 3 Q 1 2 3
D 0 4 2 inf D 0 4 1 inf
P 0 1 1 P 0 1 2
Pasul 4 Pasul 5
i 1 2 3 4 i 1 2 3 4
Q 1 2 3 4 Q 1 2 3 4
D 0 4 1 5 D 0 4 1 5
P 0 1 2 3 P 0 1 2 3
418
Teoria grafurilor
queue<int> Q; Q.push(1);
while ( !Q.empty() )
{
int nod = Q.front();
V[nod] = false;
vector<PER>::iterator i;
for ( i = G[nod].begin(); i != G[nod].end(); ++i )
if ( D[nod] + i->second < D[ i->first ] )
{
D[ i->first ] = D[nod] + i->second;
P[ i->first ] = nod;
if ( !V[ i->first ] )
{
Q.push( i->first );
V[ i->first ] = true;
}
}
Q.pop();
}
}
419
Capitolul 12
420
Teoria grafurilor
cst, iar fiecare linie reprezintă nodurile care se află în fiecare găleată la pasul
respectiv. În practică, se trece şi peste găleţile goale.
421
Capitolul 12
Faptul că vom opri algoritmul atunci când toate cozile sunt goale va
face algoritmul mult mai rapid în practică, datorită faptului că nu vor exista
aproape niciodată un număr mare de distanţe minime distincte.
Implementarea prezentată conţine toate optimizările discutate. Acest
algoritm este indicat a fi folosit în cazurile în care costurile muchiilor sunt
mici sau distanţele minime se repetă des. Datorită faptului că folosim
indexarea după distanţe, este clar că algoritmul lui Dial nu va funcţiona
corect în cazul existenţei distanţelor negative. În cazul general, rămâne
aşadar preferabil algoritmul Bellman – Ford sau algoritmul lui Dijkstra
implementat cu heap-uri.
Menţionăm că în cadrul implementării prezentate am presupus că
lungimea maximă a unui arc este 1000. Mai mult, am folosit un vector de
1024 de cozi pentru ca operaţia modulo să se poată efectua mai eficient cu
ajutorul operaţiilor pe biţi. Implementarea conţine doar funcţia relevantă.
422
Teoria grafurilor
423
Capitolul 12
424
Teoria grafurilor
425
Capitolul 12
queue<int> Q;
Q.push(1);
while ( !Q.empty() )
{
int nod = Q.front();
vector<int>::iterator i;
for ( i = G[nod].begin(); i != G[nod].end(); ++i )
if ( F[nod][*i] < C[nod][*i] && !V[*i] )
{
P[*i] = nod;
Q.push(*i);
V[*i] = true;
}
Q.pop();
}
return V[N];
}
426
Teoria grafurilor
int flux_total = 0;
while ( Drum(G, N, C, F, P, V) )
{
int min = inf;
flux_total += min;
for ( int x = N; x != 1; x = P[x] )
{
F[ P[x] ][x] += min;
F[x][ P[x] ] -= min;
}
}
return flux_total;
}
427
Capitolul 12
vector<int>::iterator i;
for ( i = G[nod].begin(); i != G[nod].end(); ++i )
if ( F[nod][*i] < C[nod][*i] && !V[*i] )
{
P[*i] = nod;
Q.push(*i);
V[*i] = 1;
}
}
// ...
while ( Drum(G, N, C, F, P, V) )
{
vector<int>::iterator i;
for ( i = G[N].begin(); i != G[N].end(); ++i )
if ( F[*i][N] < C[*i][N] && V[*i] )
{
P[N] = *i;
int min = inf;
for ( int x = N; x != 1; x = P[x] )
if ( C[ P[x] ][x] - F[ P[x] ][x] < min )
min = C[ P[x] ][x] - F[ P[x] ][x];
if ( !min )
continue;
flux_total += min;
for ( int x = N; x != 1; x = P[x] )
{
F[ P[x] ][x] += min;
F[x][ P[x] ] -= min;
}
}
}
428
Teoria grafurilor
429
Capitolul 12
vector<int>::iterator i;
for ( i = G[min].begin(); i != G[min].end(); ++i )
if ( D[min] + CS[min][*i] < D[*i] &&
C[min][*i] - F[min][*i] > 0 )
{
D[*i] = D[min] + CS[min][*i];
P[*i] = min;
430
Teoria grafurilor
if ( tmp_cst == inf )
break;
return cst_min;
}
431
Capitolul 12
432
Teoria grafurilor
433
Capitolul 12
434
Teoria grafurilor
435
Capitolul 12
return false;
}
436
Teoria grafurilor
do
{
ok = false;
} while ( ok );
ofstream out("cuplaj.out");
for ( int i = 1; i <= N; ++i )
if ( dr[i] )
out << i << ' ' << dr[i] << '\n';
out.close();
}
int main()
{
int N, M;
vector<int> G[maxn];
citire(G, N, M);
HopcroftKarp(G, N);
return 0;
}
437
Capitolul 12
438
Teoria grafurilor
Mai mult, atunci când adăugăm o muchie (x, y), reunim practic
arborele din care face parte x cu arborele din care face parte y. Deoarece un
arbore este identificat în mod unic prin rădăcina sa, este de ajuns să unim
rădăcinile arborilor lui x şi y. Acest lucru îl vom face cu ajutorul unei funcţii
Merge(x, y) care fie setează T[x] = y fie T[y] = x, adică unul dintre arbori
devine subarbore al celuilalt, formându-se astfel un singur arbore. Când se
adaugă muchia (x, y), trebuie efectuat apelul Merge( Find(x), Find(y) ).
439
Capitolul 12
#include <fstream>
#include <vector>
#include <algorithm>
using namespace std;
const int maxn = 101;
const int maxm = 201;
440
Teoria grafurilor
int main()
{
int N, M;
muchie E[maxm];
citire(E, N, M);
Kruskal(E, N, M);
return 0;
}
441
Capitolul 12
Algoritmul lui Prim este un alt algoritm de tip greedy folosit pentru
determinarea arborelui parţial de cost minim. Acesta are complexităţile
O(N2) şi O(M∙log N), în funcţie de implementarea folosită. Vom prezenta
pe larg doar varianta de implementare în O(N2), deoarece aceasta este
preferabilă celorlalţi algoritmi atunci când avem de a face cu un graf dens,
iar varianta în complexitate O(M∙log N) diferă de complexitatea
algoritmului lui Kruskal doar printr-o constantă şi în plus algoritmul lui
Kruskal este mai uşor de implementat.
442
Teoria grafurilor
443
Capitolul 12
#include <fstream>
#include <vector>
#include <utility>
444
Teoria grafurilor
12.13. Concluzii
Am prezentat în acest capitol noţiunile elementare care stau la baza
algoritmilor de grafuri. Cititorii experimentaţi poate că au observat lipsa
abordării unor teme legate de arbori, cum ar fi determinarea L.C.A. sau cum
ar fi arborii binari de căutare. Aceste teme vor fi abordate în cadrul
capitolului Structuri avansate de date.
445
Capitolul 12
13. Structuri
avansate de
date
Am prezentat până acum o serie de algoritmi fundamentali însoţiţi de
aplicaţii ale acestora şi de probleme teoretice care se pot rezolva cu ajutorul
acestora. Au fost prezentate principalele tehnici de programare, biblioteca
S.T.L. şi diverse metode de optimizare a algoritmilor. În acest ultim capitol
vom prezenta câteva structuri de date care joacă un rol foarte important în
algoritmică, în special în probleme de optimizare.
Practic, acest capitol prezintă metode de a răspunde eficient la
interogări de genul se găseşte un anumit obiect într-o colecţie de obiecte
dată anterior?, întrebări însoţite şi de actualizări de tipul adugă un nou
obiect colecţiei date atnerior. Vom analiza cazurile favorabile, medii şi
defavorabile a mai multor structuri de date şi vom discuta situaţiile în care
fiecare structură este preferabilă celorlalte.
Acest capitol va folosi noţiuni de grafuri, liste, operaţii pe biţi,
recursivitate, tehnici de programare, matematică şi S.T.L., aşa că
recomandăm cu tărie stăpânirea tuturor capitolelor anterioare înainte de
parcurgerea acestui capitol final.
447
Capitolul 13
CUPRINS
448
Structuri avansate de date
L[4] 4 NULL
L[3] 4 8 NULL
L[2] 1 4 8 NULL
L[1] 1 4 6 8 10 NULL
L[0] 0 1 2 3 4 6 7 8 9 10 NULL
Fig. 13.1.1. – O listă de salt pentru un anumit set de date
449
Capitolul 13
L[4] 4 NULL
L[3] 4 8 NULL
L[2] 1 4 8 NULL
L[1] 1 4 6 8 10 NULL
L[0] 0 1 2 3 4 6 7 8 9 10 NULL
Fig. 13.1.2. – Modul de căutare a unei valori într-o listă de salt
450
Structuri avansate de date
d) Detalii de implementare
Deoarece codul complet al tuturor operaţiilor ar fi prea voluminos şi
greu de înţeles la prima vedere, vom prezenta pe rând şi cu explicaţii fiecare
structură şi metodă.
În primul rând vom folosi o structură Node, care va reprezenta un
nod, caracterizat prin informaţia reţinută şi legaturile acestuia.
În al doilea rând vom folosi o structură List, care va conţine
începutul listei, pentru a putea evita nişte cazuri particulare şi a simplifica
implementarea. Aşadar, structurile folosite sunt:
451
Capitolul 13
452
Structuri avansate de date
return false;
}
453
Capitolul 13
f) Îmbunătăţiri
454
Structuri avansate de date
Exerciţii:
a) Implementaţi variantă deterministă a listelor de salt.
b) Implementarea prezentată pune accentul pe simplitate. De
exemplu, ideal ar fi să şi eliberăm memoria aferentă unui nod
după ştergerea acestuia. Ce alte optimizări s-ar mai putea face şi
cum ar putea fi acestea implementate?
c) Listele de salt pot fi folosite pentru sortarea unui şir de numere.
Comparaţi sortarea prin liste de salt cu restul algoritmilor de
sortare.
d) Scrieţi un program care citeşte un şir de numere răspunde la mai
multe întrebări de genul care este al k-lea cel mai mic element
din şir? în timp O(log N) pentru fiecare întrebare.
e) Scrieţi o funcţie care returnează poziţia unui element în listă.
f) Scrieţi o funcţie care returnează valoarea elementului de pe o
anumită poziţie.
g) Implementaţi o clasă numită SkipList.
455
Capitolul 13
H[0] 5 10 0 NULL
H[1] 31 1 11 NULL
H[2] 132 17 2 7 NULL
H[3] 13 8 NULL
H[4] 4 29 NULL
Fig 13.2.1. – O tabelă de dispersie (hash table)
la începutul listei H[h(x)], iar inserarea unui element la începutul unei liste
înlănţuite se face în timp constant.
d) Detalii de implementare
În primul rând trebuie să stabilim dimensiunea tabelei de dispersie şi
funcţia pe care o vom folosi. Pentru majoritatea problemelor se foloseşte o
funcţie simplă de genul h(x) = x % P, unde P este un număr prim sau
h(x) = x % 2k, pentru a putea calcula mai rapid operaţia modulo folosind
operaţii pe biţi. Vom alege cea de-a doua variantă din motive de eficienţă.
Am putea folosi şi de data aceasta două structuri: una care va reţine
doar un membru val, care va reprezenta numărul reţinut de un anumit
obiect, şi un pointer către următorul element din listă, reprezentând practic o
listă înlănţuită şi o a doua structură care va declara şi iniţializa mai multe
liste înlănţuite, constituind tabela de dispersie. Recomandăm cititorilor să
incorporeze şi funcţiile de gestiune a tabelei într-o clasă, atât pentru această
structură de date cât şi pentru celelalte din acest capitol. În acest fel, veţi
putea refolosi foarte uşor şi intuitiv codul.
Putem folosi însă clasele list sau vector din S.T.L. pentru a obţine o
implementare mai scurtă. La grafuri am folosit vector deoarece nu aveam
457
Capitolul 13
458
Structuri avansate de date
f) Îmbunătăţiri şi aplicaţii
Putem extinde tabelele de disperse pentru numere raţionale şi pentru
şiruri de caractere. O funcţie de dispersie pentru numere reale poate fi
h(x) = [{A∙x}∙P], unde:
5−1
0 < A < 1, preferându-se (conform lui Knuth) 𝐴 = ≅
2
0.618033989 …
{x} partea fracţionară a lui x
459
Capitolul 13
460
Structuri avansate de date
if ( found )
return true; // S2 este subsecventa a lui S1
}
461
Capitolul 13
Iar în general:
if ( hashS1 == hashS2 )
return true; // PROBABIL true
return false;
}
463
Capitolul 13
Exerciţii:
a) Modificaţi funcţia de mai sus astfel încât să afişeze toate poziţiile
de potrivire.
b) Propuneţi algoritmi pentru testarea calităţii unei funcţii de
dispersie, atât pentru numere naturale cât şi pentru şiruri de
caractere.
c) Se dă un şir de N numere naturale aleatoare. Se cere a găsirea a
patru numere din şir a căror sumă este S. Cum se poate rezolva
problema în O(N2) cu ajutorul tabelelor de dispersie?
d) Elaboraţi un test pe care soluţia găsită pentru problema
anterioară să aibă timpul de execuţie O(N4).
Exemplu:
RMQ2.in RMQ2.out
73 -6
1 -6 8 10 13 5 4 1
125
251
146
464
Structuri avansate de date
465
Capitolul 13
466
Structuri avansate de date
467
Capitolul 13
b) Detalii de implementare
Ne mai interesează modalitatea de memorare a unui arbore de
intervale. Vom folosi aceeaşi idee ca la heap-uri: dacă A este arborele de
intervale, fiii unui nod k vor fi A[2∙k] respectiv A[2∙k + 1]. A va trebui să
fie de dimensiune cel puţin 2∙N – 1 (există N + N / 2 + N / 4 + ... noduri).
Arborele nu este neapărat să fie complet însă, aşa că va trebui să verificăm
dacă apelurile recursive se fac pentru un nod care chiar există în arbore.
Pentru a evita aceste verificări putem declara tabloul A ca fiind de
dimensiune 2P ≥ 2∙N – 1. Practic, vom completa arborele cu nişte
pseudonoduri până când acesta va deveni un arbore binar complet.
468
Structuri avansate de date
469
Capitolul 13
470
Structuri avansate de date
471
Capitolul 13
472
Structuri avansate de date
Euler[k++] = nod;
}
}
Exerciţii:
a) Scrieţi un program care citeşte un arbore ponderat şi răspunde
eficient la întrebări de genul care este lungimea drumului dintre
nodurile x şi y?
b) Scrieţi un program care citeşte un şir de numere întregi şi
răspunde eficient la întrebări de genul care este subsecvenţa de
sumă maximă dintre poziţiile x şi y? Implementaţi şi actualizări.
c) Scrieţi un program care citeşte un tablou cu N elemente din
mulţimea {0, 1}. Numărul 0 reprezintă faptul că acea poziţie este
473
Capitolul 13
Exemplu:
sume.in sume.out
83 22
1 -3 8 7 9 1 3 4 20
126
2 4 -4
135
474
Structuri avansate de date
475
Capitolul 13
476
Structuri avansate de date
S[i] pe v, unde i creşte cu 2k la fiecare pas, atâta timp cât i este mai mic sau
egal cu N.
Algoritmul funcţiei Actual(i, v) este:
Cât timp i <= N execută
o S[i] = S[i] + v
o i = i + 2k, unde k = numărul zerourilor terminale ale lui i
b) Detalii de implementare
int Calcul_k(int i)
{
int k = 0;
while ( (i & 1) == 0 ) // cat timp cel mai putin semnificativ bit (cel mai
// din dreapta) e 0
{
++k;
i >>= 1;
}
return k;
}
Această operaţie are complexitatea O(log i), aşa că, folosind această
abordare, se va obţine complexitatea totală O(log2 N) pentru fiecare
operaţie. Vom prezenta o metodă de a calcula valoarea 2k în timp constant.
Presupunem i = ...1000...02, unde după 1 apar doar valori de 0. Ne
interesează setarea tuturor biţilor din stânga bitului de valoare 1 pe valoarea
0, astfel încât restul biţilor sa rămână neschimbaţi. Dacă putem face acest
lucru, atunci vom avea calculată valoarea 2k. Avem nevoie de următoarele
propoziţii:
477
Capitolul 13
i 1011000 &
i–1 1010111
–––––––––
i & (i – 1) 1010000
#include <fstream>
478
Structuri avansate de date
// update
void Actual(int N, int i, int v, int S[])
{
for ( ; i <= N; i += i ^ (i & (i - 1)) )
S[i] += v;
}
479
Capitolul 13
d) Extinderi
480
Structuri avansate de date
Exerciţii:
a) Se consideră problemele prezentate, dar de data aceasta în loc de
sumă se cere produsul elementelor din subsecvenţă, respectiv
submatrice. Cum se poate evita lucrul cu numere mari?
b) Extindeţi arborii indexaţi binar pentru rezolvarea unei probleme
similare în spaţiul tridimensional.
c) Găsiţi forme echivalente ale expresiei i ^ (i & (i - 1)).
b) Detalii de implementare
În primul rând să vedem cum vom reţine acest arbore. Fiind vorba de
un arbore în care fiecare nod poate avea un număr relativ mare de fii
482
Structuri avansate de date
(considerăm că fiecare nod poate avea 26 de fii, câte unul pentru fiecare
literă din alfabet), vom folosi o structură nod cu următoarele câmpuri:
rasp – folosit doar de nodurile terminale, ne indică numărul
cuvintelor din trie care au acest nod terminal, adică numărul de
apariţii al unui anumit cuvânt.
nrf – folosit de toate nodurile, ne indică numărul de fii nevizi ai
nodului curent.
next[26] – folosit de toate nodurile, reprezintă un vector de
pointeri, fiecare indicând un anumit fiu. next[0] va indica fiul
etichetat cu a, next[1] fiul etichetat cu b şi aşa mai departe până
la next[25] care va indica fiul etichetat cu z.
nod *next[maxa];
483
Capitolul 13
procedează în acest fel până când cuv va indica sfârşitul cuvântului, adică \0
(terminatorul de şir). În acest moment se actualizează numărul de apariţii
(câmpul rasp) al acestui nod terminal şi funcţia se termină.
int val = *cuv - 'a'; // retine eticheta nodului urmator: 'a' - 'a' = 0,
// 'b' - 'a' = 1 etc.
if ( rad->next[val] == 0 ) // daca nodul nu exista, el trebuie creat
{
rad->next[val] = new nod;
++rad->nrf; // trebuie incrementat numarul de fii al nodului curent
}
484
Structuri avansate de date
485
Capitolul 13
int main()
{
int N, cod;
string cuv;
nod *trie = new nod;
ifstream in("trie.in");
in >> N;
while ( N-- )
{
in >> cod >> cuv;
486
Structuri avansate de date
switch ( cod )
{
case 0:
Insert(trie, cuv.c_str());
break;
case 1:
cout << Apar(trie, cuv.c_str()) << '\n';
break;
case 2:
Del(trie, trie, cuv.c_str());
break;
}
}
in.close();
return 0;
}
c) Aplicaţii
487
Capitolul 13
Exerciţii:
a) Scrieţi o funcţie care afişează toate cuvintele dintr-un trie în
ordine lexicografică.
b) Scrieţi o funcţie care primeşte ca argumente rădăcina unui trie şi
un cuvânt cuv şi afişează cel mai lung prefix comun dintre
cuvântul cuv şi orice alt cuvânt din trie.
c) Se dă un vector A cu N numere naturale. Scrieţi un program care
găseşte o subsecvenţă Ai, Ai + 1, ..., Aj, cu 1 ≤ i ≤ j ≤ N astfel
încât valoarea Ai xor Ai + 1 xor ... xor Aj să fie maximă.
Reamintim tabelul de adevăr al operaţiei xor:
x y x xor y
1 0 1
0 1 1
1 1 0
0 0 0
489
Capitolul 13
490
Structuri avansate de date
Complexitatea acestei funcţii este tot O(log N), din exact aceleaşi
motive enunţate pentru funcţia de căutare.
491
Capitolul 13
Cazul I
Nodul pe care vrem să-l ştergem nu are fii
Acesta este cel mai convenabil caz. Tot ce trebuie să facem este să
identificăm nodul care trebuie şters şi să-l eliminăm din arbore.
492
Structuri avansate de date
Cazul II
Nodul pe care vrem să-l ştergem are un singur fiu
Cazul III
Nodul pe care vrem să-l ştergem are doi fii
Alegând un nod care respectă una dintre cele două condiţii de mai
sus ne asigurăm că nu va exista niciun nod în arbore care să nu respecte
493
Capitolul 13
494
Structuri avansate de date
Se poate observa din cele două figuri anterioare că 8 este cea mai
mare valoare din arbore mai mică decât 9, iar 4 este cea mai mică valoare
din arbore mai mare decât 3.
Pentru a rezolva problema iniţială, adică ştergerea unui nod care are
doi fii, vom înlocui aşadar nodul respectiv fie cu predecesorul său în
parcurgerea în ordine, fie cu succesorul său în această parcurgere, care se
poate determina uşor aşa cum am arătat. Predecesorul sau succesorul cu care
înlocuim nodul pe care vrem să-l ştergem va fi la rândul său şters conform
algoritmilor aferenţi primelor două cazuri.
Sau:
d) Cazuri defavorabile
496
Structuri avansate de date
e) Detalii de implementare
struct nod
{
int val; // valoarea aferenta nodului curent
nod *st, *dr; // pointeri la subarborele stang respectiv drept
497
Capitolul 13
498
Structuri avansate de date
T->val = (*pred)->val;
if ( (*pred)->st == NULL )
RemoveCazI(*pred);
else
RemoveCazII(*pred);
}
499
Capitolul 13
f) Alţi algoritmi
500
Structuri avansate de date
Aşadar, putem afla cea mai mică valoare din arbore în timp mediu
O(log N) cu ajutorul următoarei funcţii:
return T->val;
}
Putem afla cea mai mare valoare din arbore aflând care este cel mai
din dreapta nod al arborelui. Acest lucru este corect deoarece algoritmul de
parcurgere în ordine furnizează ultimul rezultat umplând stiva cu apeluri
recursive pentru fiul drept al nodului curent.
return T->val;
}
501
Capitolul 13
Altfel
o returnează kMinim(T.st, k)
Exerciţii
a) Scrieţi o funcţie care determină al k-lea cel mai mare element
dintr-un arbore binar de căutare.
b) Prezentaţi două abordări pentru ca un arbore binar de căutare să
suporte inserarea mai multor valori identice. Care este mai
avantajoasă?
503
Capitolul 13
Un treap este un arbore binar în care fiecare nod are asociate două
entităţi: o valoare (sau cheie) şi o prioritate. Valorile nodurilor treap-ului
vor respecta proprietăţile unui arbore binar de căutare, iar priorităţile
nodurilor vor respecta proprietăţile unui heap. Valorile reprezintă datele
inserate de către utilizator, iar priorităţile vor fi nişte numere aleatoare
atribuite fiecăriui nod.
Vom presupune şi aici că oricare două valori din arbore sunt
distincte.
504
Structuri avansate de date
505
Capitolul 13
a) Echilibrarea arborelui
Echilibrarea arborelui este necesară atunci când inserarea sau
ştergerea unui nod face ca un nod al arborelui să nu mai respecte
proprietatea de heap. Pentru a restabili această proprietate vom efectua o
rotaţie a acelui nod spre dreapta sau spre stânga, după caz:
dacă prioritatea fiului stâng al lui T este mai mare decât
prioritatea lui T, atunci se efectuează o rotaţie spre dreapta a
fiului stâng.
dacă prioritatea fiului drept al lui T este mai mare decât
prioritatea lui T, atunci se efectuează o rotaţie spre stânga a fiului
drept.
506
Structuri avansate de date
Funcţia RotDr(T), care roteşte fiul drept al lui T spre stânga poate fi
scrisă astfel:
temp = T.dr
T.dr = temp.st
temp.st = T
T = temp
Iar funcţia RotSt(T), care roteşte fiul stâng al lui T spre dreapta
poate fi scrisă astfel:
temp = T.st
T.st = temp.dr
temp.dr = T
T = temp
507
Capitolul 13
508
Structuri avansate de date
509
Capitolul 13
e) Detalii de implementare
struct nod
{
int val; // valoarea nodului curent
int pr; // prioritatea nodului curent
nod *st, *dr; // fiul stang respectiv drept
nod(int v) : val(v)
{
pr = rand(); // fiecare nod primeste o prioritate aleatoare
st = dr = NULL;
}
};
510
Structuri avansate de date
Remove(x, T);
}
}
511
Capitolul 13
512
Structuri avansate de date
Exerciţii:
a) Scrieţi un program care determină numărul de treap-uri distincte
cu N valori de la 1 la N şi cu priorităţi distincte de la 1 la N. De
exemplu, pentru N = 3 există 6 astfel de treap-uri. Două treap-uri
T1 şi T2 se consideră diferite dacă:
T1.valoare este diferit de T2.valoare sau T1.prioritate
este diferit de T2.prioritate.
Treap-ul T1.stânga diferă de T2.stânga sau T1.dreapta
diferă de T2.dreapta.
b) Rezolvaţi aceleaşi probleme de la arbori binari de căutare
folosind treap-uri.
c) Scrieţi o funcţie Split care primeşte ca argument un număr întreg
x şi întoarce două treap-uri A şi B astfel încât A să conţină doar
valori mai mici decât x şi B doar valori mai mari decât x.
d) Scrieţi o funcţie Join care primeşte ca argumente două treap-uri
A, B şi o valoare x, cu semnificaţia de mai sus şi uneşte treap-
urile A şi B într-un singur treap.
513
Capitolul 13
13.8. Concluzii
Sperăm că acest ultim capitol, cât şi întreaga lucrare, v-au fost şi vă
vor fi în continuare folositoare în studiul algoritmilor. Cititorii care au
parcurs temeinic materialul pus la dispoziţie în această carte ar trebui să aibă
deja o înţelegere clară a noţiunilor algoritmice elementare şi a metodelor de
rezolvare a problemelor aferente acestui domeniu.
Autorii
514
Bibliografie
BIBLIOGRAFIE
1. Adrian Alexandrescu Programarea modernă în C++. Programare
generică şi modele de proiectare aplicate, Teora, Bucureşti, 2002.
2. Alfred V. Aho, John E. Hopcroft, Jeffrey D. Ullman, Data Structures
and Algorithms, Addison-Wesley, 1983.
3. Alfred V. Aho, John E. Hopcroft, Jeffrey D. Ullman, The Design and
Analysis of Computer Algorithms, Addison-Wesley, 1974.
4. Béla Bollobás, Random Graphs, Academic Press, 1985.
5. C. A. R. Hoare, Algorithm 63 (partition) and algorithm 65 (find),
Communications of the ACM, 4(7):321-322, 1961.
6. C. A. R. Hoare, Quicksort, Computer Journal, 5(1):10-15, 1962.
7. C. Y. Lee, An algorithm for path connection and its applications, IRE
Transactions on Electronic Computers, EC-10(3):346-365, 1961.
8. Cay Horstmann, Practical Object - Oriented Development in C++ and
Java, Wiley Computer Publishing, 2000, New York.
9. Cecilia R. Aragon, Raimund Seidel, Randomized Search Trees,
Algorithmica 16 (4/5): 464–497, 1996.
10. Cecilia R. Aragon, Raimund Seidel, Randomized Search Trees,
Proceedings of the 30th Symposium on Foundations of Computer
Science (FOCS 1989), Washington, D.C.: IEEE Computer Society
Press, pp. 540–545, 1989.
11. Constantin Popescu, Dan Noje, Ioan Mang, Horea Oros, Programarea
în limbajul C, Editura Universităţii din Oradea, 2002.
12. David E. Goldberg, The Design of Innovation: Lessons from and for
Competent Genetic Algorithms, Addison-Wesley, Reading, MA., 2002.
13. Donald E. Knuth, James H. Morris, Jr., Vaughan R. Pratt, Fast pattern
matching in strings, SIAM Journal on Computing, 6(2):323-350, 1977.
14. Edward F. Moore, The shortest path through a maze, Proceedings of the
International Symposium on the Theory of Switching, pages 285-292.
Harvard University Press, 1959.
15. Edward M. Reingold, Jürg Nievergelt, Narsingh Deo, Combinatorial
Algorithms: Theory and Practice, Prentice-Hall, 1977.
16. Eric Bach, Number-theoretic algorithms, în Annual Review of Computer
Science, volume 4, pages 119- 172. Annual Reviews, Inc., 1990.
17. Frank Harary, Graph Theory, Addison-Wesley, 1969.
515
Algoritmică
516
Bibliografie
517
Algoritmică
56. Robert E. Tarjan, Jan van Leeuwen, Worst-case analysis of set union
algorithms, Journal of the ACM, 31(2):245-281, 1984.
57. Robert S. Boyer, J. Strother Moore, A fast string-searching algorithm,
Communications of the ACM, 20(10):762-772, 1977.
58. Robert Sedgewick Implementing quicksort programs, Communications
of the ACM, 21(10):847-857, 1978.
59. Robert Sedgewick, Algorithms, Addison-Wesley, second edition, 1988.
60. Robert W. Floyd, Algorithm 97 (SHORTEST PATH), Communications
of the ACM, 5(6):345, 1962.
61. Robert W. Floyd, Ronald L. Rivest, Expected time bounds for selection,
Communications of the ACM, 18(3):165-172, 1975.
62. Sara Baase, Computer Algorithms: Introduction to Design and Analysis.
Addison-Wesley, second edition, 1988.
63. Shimon Even, Graph Algorithms, Computer Science Press, 1979.
64. Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford
Stein – Introduction to Algorithms, second edition, The MIT Press,
Cambridge, Massachusetts, 2001.
65. William Pugh, Skip lists: a probabilistic alternative to balanced trees,
Communications of the ACM 33 (6): 668-676, 1990.
66. Wolfgang Banzhaf, Peter Nordin, Robert Keller, Frank Francone,
Genetic Programming – An Introduction, Morgan Kaufmann, San
Francisco, CA., 1998.
518