Documente Academic
Documente Profesional
Documente Cultură
CUPRINS
Prefa ..................................................................................vii
1. Introducere .......................................................................... 11
1.1.
1.2.
2. Algoritmi de sortare............................................................. 21
2.1.
2.2.
2.3.
2.4.
2.5.
2.6.
2.7.
2.8.
3. Tehnici de programare......................................................... 57
3.1.
3.2.
3.3.
3.4.
3.5.
Recursivitate ......................................................................................59
Backtracking .......................................................................................68
Divide et impera ................................................................................82
Greedy ................................................................................................89
Programare dinamic ........................................................................96
Algoritmic
4.10. Formule i tehnici folositoare ........................................................ 151
4.11. Operaii cu numere mari ................................................................ 154
Cuprins
9.15. Concluzii .......................................................................................... 300
Algoritmic
13.7. Arbori binari de cutare cutare echilibrai ................................. 504
13.8. Concluzii .......................................................................................... 514
vi
Prefa
Prefa
Aceast carte este util tuturor celor care doresc s studieze
conceptele fundamentale ce stau la baza programrii calculatoarelor,
mbinnd principalele direcii 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 colecie de probleme demonstrative a
cror rezolvare acoper elemente de programare procedural, tehnici de
programare, algoritmi i structuri de date, inteligen artificial i nu n
ultimul rnd programare dinamic.
Pentru fiecare problem n parte sunt construii algoritmii clasici de
rezolvare, completai cu explicaia funcionrii acestora, iar n completare,
acolo unde este necesar, problemele dispun i de prezentarea noiunilor
teoretice, a conceptelor generale i particulare aferente construirii unui
algoritm optimizat.
Organizare
Cartea este structurat pe 13 capitole, fiecare dintre acestea tratnd
una dintre temele specifice ale algoritmicii:
Capitolul 1 cuprinde noiunile generale referitoare la modul n care trebuie
citit aceast carte, ce cuprinde aceast carte i ce trebuie avut n vedere n
evaluarea algoritmilor.
Capitolul 2 trateaz algoritmii de sortare cei mai cunoscui (reprezentativi).
Fiecare dintre acetia a fost prezentat din punct de vedere al complexitii
asimptotice, eficienei, memoriei suplimentare folosite, stabilitii i a
optimizrilor suportate.
Capitolul 3 descrie tehnicile de programare i principalele probleme
asociate acestora: recursivitate cu dezvoltarea complet a problemei
turnurile din Hanoi, backtracking cu generarea permutrilor,
aranjamentelor, combinrilor, ... divide et impera, cutarea binar, tehnica
vii
Algoritmic
greedy cu problema spectacolelor..., noiuni i tehnici de programare
dinamic.
Capitolul 4 prezint o serie de algoritmi care au la baz noiuni elementare
de matematic i teoria numerelor dintre care amintim cei mai cunoscui:
algoritmul lui Euclid, algoritmii de determinare a numerelor prime,
algoritmul lui Gauss i ali algoritmi mai puin cunoscui cum ar fi teorema
chinez a resturilor. Am completat acest capitol cu un paragraf destinat
numerelor mari i operaiile asociate acestora.
Capitolul 5 trateaz problemele clasice asociate tehnicii de programare
backtracking: problema labirintului, problema sriturii calului, generarea
submulimilor, problema reginelor, generarea partiiilor unei mulimi...(n
general, acele probleme care apar n examenele asociate cu materia tehnici
de programare, n.a.)
Capitolul 6 dezvolt algoritmul K.M.P. (Knuth Morris Pratt) i
algoritmul de evaluare a expresiilor matematice. Am folosit un capitol
special destinat acestor algoritmi deoarece acetia nu pot fi ncadrai ntr-o
categorie aparte i sunt totui algoritmi necesari i cu aplicabilitate teoretic
vast.
Capitolul 7 prezint pe scurt principalele containere i algoritmi din
Biblioteca S.T.L. i modul de folosire a acestora n cteva situaii concrete.
Capitolul 8 prezint algoritmii genetici, modul de construire a acestora,
conceptele de evoluie i optimizare ce stau la baza construirii unui astfel de
algoritm, de asemenea i implementarea, att din punct de vedere
demonstrativ, n analogie cu o problem clasic, ct i implementarea n
probleme a cror rezolvare se preteaz la aceste clase de algoritmi.
Capitolul 9 prezint mai multe aplicaii ale programrii dinamice. Tot n
acest capitol se insist mai mult pe facilitile limbajului C++.
Capitolul 10 prezint metode de rezolvare a unor probleme de geometrie
computaional. Aceast ramur a informaticii are aplicaii practice
importante n programe de grafic, aplicaii CAD, aplicaii de modelare,
proiectarea circuitelor integrate i altele.
viii
Prefa
Capitolul 11 prezint noiunile elementare despre liste nlnuite, att la
nivel teoretic, modul de construire, tipuri, ct i implementarea acestora.
Dei listele nlnuite exist deja implementate n cadrul librriei S.T.L.
(containerul list), este important pentru orice programator s cunosc modul
de construire al unui tip abstract de date de tip list, tipurile de date derivate
(stiv, coad, list circular), respectiv domeniul de aplicabilitate al
acestora.
Capitolul 12 trateaz n detaliu principalele structuri de date folosite n
teoria grafurilor i algoritmii cei mai des folosii pentru rezolvarea
problemelor cu grafuri.
Capitolul 13 prezint, n ncheiere, structurile avansate de date, deoarece
acestea necesit noiuni de grafuri, liste, operaii pe bii, recursivitate,
tehnici de programare, matematic i S.T.L., aa c recomandm stpnirea
tuturor capitolelor anterioare nainte de parcurgerea acestui capitol final.
Convenii utilizate
Liniile de cod surs prezentate n aceast carte respect standardele
C++ n vigoare la data publicrii. Au fost testate pe compilatoarele g++ i
Visual Studio Express (versiunea minim testat este 2005) i pe sistemul de
operare Windows (XP i 7) pe 32 de bii.
Programele prezentate sunt scrise n aa fel nct s fie uor de
neles pentru cineva care cunoate bine bazele limbajului de programare
C++.
Fragmentele de cod vor fi scrise cu italic i colorate sintactic pentru
a fi uor de recunoscut.
ix
Algoritmic
Despre Autori
La data publicrii acestei cri
Laslo E. Eugen este asistent la Facultatea de tiine a Universitii din
Oradea pe laboratoarele i seminariile de Algoritmi i structuri de date,
Tehnici de programare i Inteligen artificial. Masterat n domeniul
matematicii cu specializarea Analiz Real i Complex. A fost inginer de
software la SC SoftParc, unde a ajutat la proiectarea i realizarea produselor
software ale acestei firme. Laslo Eugen locuiete n Oradea mpreun cu
soia i fiica lui.
Ionescu Vlad Sebastian este student n anul II la Facultatea de tiine a
Universitii din Oradea. ncepnd cu clasa a X-a a obinut diverse premii i
meniuni la olimpiade i concursuri judeene i naionale de informatic.
Este pasionat de informatic nc din clasele primare. Domeniile sale
principale de interes sunt optimizarea algoritmilor, programarea funcional
i inteligena artificial.
Introducere
1. Introducere
Acest prim capitol are ca scop familiarizarea cititorului cu
elementele constructive ale crii, cunotiinele iniiale necesare nelegerii
materialul de fa, conveniile de scriere i prezentare a secvenelor de cod,
tot aici sunt cuprinse noiunile generale privind analiza complexitii
algoritmilor prin studiul timpului de execuie i cantitii de memorie
utilizat de ctre acetia (notaia asimptotic).
11
Capitolul 1
CUPRINS
1.1. Noiuni despre limbaj ...............................................................................13
1.2. Noiuni despre notaia asimptotic .........................................................14
12
Introducere
13
Capitolul 1
Implementrile unor subalgoritmi, care se repet des, nu vor fi
ntotdeauna prezentate exact la fel ca nainte. Pot s difere nume de variabile
i chiar modul de implementare (structurile de date folosite, funciile, stilul
de scriere a codului etc.). Acest lucru se datoreaz faptului c scopul acestei
lucrri este s dezvolte o gndire algoritmic liber i deschis la nou. Nu
trebuie niciodat nvat pe de rost o anumit metod de rezolvare, ci
trebuie neles un algoritm, care apoi poate fi implementat n mai multe
moduri. Considerm c prin diversificarea implementrilor contribuim la
educarea cititorului n acest scop.
Se va evita, pe ct posibil, folosirea conceptelor avansate de
programare orientat pe obiecte. Implementrile prezentate vor folosi, n
general, doar partea procedural a limbajului C++. Unele programe care
sunt simplificate prin folosirea claselor sau structurilor vor folosi aceste
faciliti, dar nu sunt necesare dect cunotiine de baz a programrii
orientate pe obiecte pentru nelegerea acestora.
Fragmentele de cod vor fi scrise cu italic i colorate sintactic pentru
a fi uor de recunoscut.
Introducere
ctre un algoritm dac dimensiunea datelor de intrare este N. Putem scrie
f(N) este O(g(N)), f(N) = O(g(N)) sau f(N) O(g(N)) i citi complexitatea
algoritmului este de ordinul g(N), dac i numai dac exist un numr real
pozitiv C i un numr natural N0 astfel nct:
|f(N)| C|g(N)|, N N 0, unde g este o funcie care (de obicei) nu
conine constante.
Cnd funcia f este o constant, complexitatea algoritmului se scrie
O(1).
Pentru a nelege mai bine aceast notaie i pentru a evidenia modul
de folosire al acesteia n aceast carte, vom prezenta cteva secvene de cod
pentru care vom calcula complexitatea.
Secvena 1
for ( int i = 1; i <= N; ++i )
for ( int j = 1; j <= N; ++j )
cout << i << " * " << j << " = " << i * j << '\n';
Analiza complexitii aestei secvene este foarte uoar: pentru
fiecare din cele N valori ale lui i, variabila j va lua la rndul ei tot N valori,
afindu-se aadar N2 linii, fiecare linie coninnd 6 atomi lexicali (i, * ,
j, = , i * j, \n). Aadar, f(N) = 6N2. Pentru C = 6, obinem
complexitatea algoritmului O(N2). Nu ntotdeauna putem preciza cu
exactitate numrul de operaii efectuate de ctre algoritm. Chiar i pe acest
exemplu, nu putem fi siguri c instruciunea cout efectueaz exact 6
operaii, deoarece nu tim cum este implementat aceast funcie (sau, mai
corect spus, obiect). n orice caz, nu ne intereseaz dect termenul care l
conine pe N la puterea cea mai mare, aa cum va reiei din secvena
urmtoare.
Secvena 2
for ( int i = 1; i <= N; ++i )
for ( int j = i + 1; j <= N; ++j )
cout << i << " * " << j << " = " << i * j << '\n';
De aceast dat vom ignora numrul de operaii introduse de cout,
deoarece acest numr este oricum constant i nu va influena n niciun fel
rezultatul, deoarece constantele nu au nicio semnificaie atunci cnd N tinde
15
Capitolul 1
la infinit. Vom numra doar numrul de incrementri ale variabilelor din
cadrul celor dou for-uri.
Cnd i = 1, j se va incrementa de N 1 ori.
Cnd i = 2, j se va incrementa de N 2 ori.
...
Cnd i = N, j se va incrementa de 0 ori.
Se observ ca i se va incrementa de N ori.
Aadar, numrul de incrementri ale ambelor variabile este egal cu
N + (N 1) + (N 2) + ... + 2 + 1, sum egal cu
()
= 0.5 2 0.5 .
Secvena 3
...
int st[maxn], k = 1;
...
for ( int i = 1; i <= N; ++i )
{
while ( st[k] >= A[i] )
--k;
st[++k] = A[i];
}
i de aceast dat avem o structur repetitiv n cadrul altei structuri
repetitive, aa c am putea fi tentai s spunem c i acest algoritm are
complexitatea O(N2 ). Structura while nu se va executa de fiecare dat de N
ori, ci n total de N ori, aa c acest algoritm are complexitatea O(N). O alt
modalitate de a argumenta aceast complexitate este prin faptul c n tabloul
st se rein valori din tabloul A, iar la fiecare pas i se scot elemente din st
atta timp ct valoarea acestora este mai mare dect A[i]. Aadar, fiecare
element va fi introdus n st i ters din st cel mult o singur dat, deci se vor
efectua cel mult 2N operaii.
16
Introducere
Fcnd o analogie cu timpul de execuie, putem spune c memoria
folosit de ctre algoritm este de ordinul lui N, sau c memoria folosit
este O(N), deoarece tabloul st va conine N elemente n cel mai ru caz.
Secvena 4
for ( int i = 1; i * i <= N; ++i )
cout << i << '\n';
Complexitatea este O(sqrt(N)), unde sqrt(x) reprezint radical din
x, deoarece i * i <= N poate fi rescris ca i <= (int)sqrt(N).
Secvena 5
for ( int i = 1; i <= N; i *= 2 )
cout << i << '\n';
Complexitatea este O(log N), deoarece i se dubleaz la fiecare pas.
Am putea fi tentai s scriem complexitatea ca O(log2 N), dar acest lucru ar
fi o greeal, deoarece tim c:
=
, , 1 , > 0
Capitolul 1
Cu alte cuvinte, notaia Theta nseamn c funcia f este mrginit
att superior ct i inferior de funcia g, iar notaia Omega nseamn c
funcia f este mrginit inferior de ctre funcia g. Notaia O-mare nseamn
c funcia f este mrginit superior de ctre funcia g.
n aceast carte vom folosi, pentru simplitate, doar notaia O-mare,
dar vom da de fiecare dat limite superioare strnse (exacte) n cadrul
acesteia. Aceast notaie are avantajul de a fi mai uor de calculat dect
notaia Theta, deoarece de multe ori este mai uor de gsit o limit
superioar oarecare dect o limit superioar exact. Totui, pentru
algoritmii prezentai n aceast carte gsirea unei limite exacte nu va fi un
lucru foarte dificil, aa c recomandm cititorilor s exprime toate
complexitile prezentate att n notaia Theta ct i n notaia Omega, pe
lng notaia asimptotic oferit.
Exerciii precizai complexitile urmtoarelor secvene de cod,
folosind toatecele trei notaii prezentate
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;
return k * f(k - 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;
for ( int i = 1; i <= N; ++i )
cout << i << ' ';
cout << endl;
f(N / 2);
}
19
Capitolul 1
Fiecare algoritm prezentat n continuare pe parcursul crii va fi
nsoit de complexitatea sa asimptotic, uneori fr a mai prezenta
deducerea acesteia!
Tabelul de mai jos prezint cteva categorii asimptotice, denumirile
acestora i exemple de algoritmi pentru fiecare categorie. Majoritatea
algoritmilor menionai se vor regsi n capitolele urmtoare.
Tabelul 1.2.1. Principalele categorii de complexitate a algoritmilor
Denumirea
Complexitate
Exemple de algoritmi
complexitii
Determinarea minimului dintr-un
liniar
ir, afiarea unui ir, problema
O(N)
majoritii votului.
Sortri naive, prelucrarea
ptratic
matricelor, generarea tuturor
O(N2)
subsecvenelor unui ir.
Algoritmul
Roy-Floyd, nmulirea
cubic
O(N3)
optim a matricelor.
fracional
Determinarea primalitii.
O(sqrt(N))
Cutarea binar, cutarea ntr-un
arbore binar de cutare echilibrat,
logaritmic
O(log N)
aproximarea unor funcii
matematice.
supraliniar,
Majoritatea algoritmilor divide et
liniaritmic,
O(Nlog N)
impera.
pseudoliniar
Determinarea tuturor
N
submulimilor, gsirea tuturor
O(c ), c
exponenial
ieirilor dintr-un labirint, probleme
constant
la care se cer toate soluiile.
Accesarea unui element dintr-un
tablou, interschimbarea a dou
valori, apelarea unei funcii,
constant
O(1)
efectuarea unei operaii de un
numr finit de ori care nu depinde
de N.
inversa
Operaii optime pe mulimi
funciei
O((N))
disjuncte.
Ackermann
Ciurul lui Eratosthenes
O(Nlog(log N))
20
Algoritmi de sortare
2. Algoritmi de
sortare
Problema sortrii 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, ct i nite
rezultate teoretice importante cu privire la corectitudinea i eficiena acestor
algoritmi.
Acest capitol prezint detaliat o serie de algoritmi de sortare
reprezentativi pentru clasele din care acetia fac parte. Fiecare algoritm este
prezentat din punct de vedere al complexitii asimptotice, al eficienei
practice, al memoriei suplimentare folosite, al stabilitii, al
optimizrilor suportate i este nsoit de o implementare n limbajul C++.
Demonstraiile unor rezultate la care se face referire nu vor fi prezentate,
punndu-se accentul pe ntelegerea modului de funcionare al algoritmilor i
al implementrii acestora ntr-un limbaj de programare.
Prin memorie suplimentar nelegem memoria necesar execuiei
algoritmului, fr s lum n considerare vectorul ce reine numerele ce
trebuiesc sortate.
Prin stabilitate nelegem proprietatea unui algoritm de sortare de a
pstra ordinea relativ a dou elemente cu chei de sortare identice. De
exemplu, dac ar trebui s sortm 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 cnd un algoritm care ar produce orice alt sortare
sigur nu ar fi stabil.
Implementrile algoritmilor vor fi prezentate sub forma unei funcii
ce poart numele algoritmului descris, funcie care, dac nu se precizeaz
alfel, accept ca parametri un ir de numere ntregi A, reprezentnd irul
care trebuie sortat cresctor i un numr natural N, reprezentnd numrul de
elemente ale irului A. Funcia sorteaz cresctor elementele irului A. Pot
exista i alte funcii ajuttoare, dar nu va fi prezentat un program ntreg,
considernd acest lucru inutil pentru problema de fa.
21
Capitolul 2
CUPRINS
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
22
Algoritmi de sortare
Capitolul 2
Algoritmi de sortare
Observm c, practic, deplasarea const n operaia de atribuire
fiecrui element a valorii elementului precedent. Valoarea iniial a lui A[3]
a fost reinut n variabila V. Se insereaz V pe poziia 1, rezultnd
urmtorul vecctor:
i 1 2 3 4 5
A 3 4 6 8 7
Se trece la elementul de pe poziia 4, care se insereaz tot pe poziia
4, rezultnd urmtorul vector:
i 1 2 3 4 5
A 3 4 6 8 7
Se trece la ultimul element, cel de pe pozia 5, care va fi inserat pe
poziia 4:
i 1 2 3 4 5
A 3 4 6 7 8
Vectorul final este astfel sortat.
Trebuie precizat c algoritmul de sortare prin inserie este cel mai
natural algoritm de sortare i cel mai des folosit n viaa de zi cu zi. De
exemplu, dac avem de sortat nite cri de joc, probabil c vom folosi
(chiar i fr s ne dm seama) sortarea prin inserie.
Alt aspect interesant al algoritmului este faptul c nu efectueaz
nicio interschimbare. Acesta este i motivul superioritii sale fa de ali
algoritmi de aceiai complexitate. Performana sa pentru vectori de
dimensiune mic poate fi exploatat de algoritmul Quicksort, care poate
folosi sortarea prin inserie cnd ajunge la intervale foarte mici, eliminnd
astfel apeluri recursive.
25
Capitolul 2
void Insertion_sort(int A[], int N)
{
for ( int i = 2; i <= N; ++i )
{
int V = A[i];
// elementele A[1], A[2], ..., A[i-1] sunt deja sortate, deci
// caut pozitia in care trebuie sa inserez elementul V = A[i]
// astfel incat vectorul sa ramana in continuare sortat
int j = i - 1;
while ( j > 0 && A[j] > V )
{
A[j+1] = A[j];
--j;
}
A[j+1] = V;
}
}
26
Algoritmi de sortare
2.3. Quicksort
Quicksort, sau sortarea rapid, este cel mai eficient algoritm de
sortare, comparativ cu ceilali algoritmi de aceiai complexitate. Din pcate,
pentru a fi cu adevrat performant att pe cazul mediu ct i pe cazul cel mai
defavorabil, algoritmul necesit anumite optimizri care complic puin
codul, rezultnd un program mai complex dect pentru celelalte sortri.
Sortarea rapid este un algoritm de tip divide et impera i
funcioneaz astfel:
Fie Quicksort(A, st, dr) o funcie care sorteaz intervalul [x, y]
al vectorului A.
Fie Partitie(A, st, dr) o funcie care reordoneaz intervalul [x, y]
al vectorului A astfel nct 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 sfritul vectorului.
Elementul A[st] se numete element pivot.
Funcia 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.
Ne punem aadar problema implementrii funciei Partitie. Eficiena
i corectitudinea algoritmului depind n cea mai mare parte de aceast
funcie.
Funcia Partitie(A, st, dr) poate fi implementat, ntr-o prim
form, astfel:
Fie V = A[st]
st = st 1 i dr = dr + 1
Ct timp st < dr execut
o Execut
dr = dr 1
o Ct timp st < dr i A[dr] > V
o Execut
st = st + 1
o Ct timp st < dr i A[st] < V
o Dac st < dr execut
Interschimb A[st] cu A[dr]
Returneaz poziia pivotului, adic dr.
27
Capitolul 2
Deja devine clar ideea din spatele algoritmului: la fiecare pas se
mparte subsecvena [st, dr] n alte dou subsecvene (pe care le vom numi
subsecvena stng respectiv subsecvena dreapt): prima cu elemente mai
mici sau egale cu pivotul, iar cealalt cu elemente mai mari sau egale cu
pivotul. Acest lucru este fcut de ctre funcia Partitie, care returneaz la
sfrit poziia care delimiteaz mprirea menionat mai sus. Atenie:
algoritmul nu ofer nicio informaie folositoare despre poziia pe care
ajunge elementul pivot!
Dup ce funcia Partitie returneaz poziia ce delimiteaz mprirea
fcut n funcie de pivot, funcia Quicksort se autoapeleaz pentru
subsecvena stng i pentru subsecvena dreapt. Datorit recursivitii, i
aceste subsecvene vor trece prin funcia Partitie, fapt ce va duce n final la
sortarea vectorului.
Funcia Partitie are complexitatea O(N), iar complexitatea
ntregului algoritm depinde de pivotul ales. Pe cazul mediu i pe cazul
favorabil, complexitatea algoritmului de sortare rapid este de O(Nlog N),
elementul pivot mprind o subsecven n dou subsecvene de dimensiuni
relativ apropiate. Dar dac elementul pivot mparte fiecare subsecven
[st, dr] ntr-o subsecven de dimensiune 1 i o subsecven de dimensiune
dr st? n cazul acesta, complexitatea algoritmului este de O(N2), cu nimic
mai bun dect cea a algoritmului de sortare prin inserie! Mai mult, i
memoria suplimentar folosit este O(N), mai mult dect a algoritmilor
ptratici.
Pentru a v convinge de complexitatea ptratic a algoritmului
Quicksort n cazul n care partiionarea irului se face ntotdeauna
dezechilibrat, urmrii modul de funcionare al algoritmului pe vectorul:
i 1 2 3 4
A 1 2 3 4
La primul pas, se apeleaz funcia Quicksort(A, 1, 4), care va apela
Partitie(A, 1, 4). Funcia Partitie va alege ca pivot pe V = A[1] = 1, iar st i
dr se vor iniializa cu 0 respectiv 5. Acum, se va executa prima bucl
execut ... ct timp, ajungndu-se n final la dr = 1. Elementele marcate cu
rou n tabelele ce urmeaz se compar cu V = 1. La primul pas, se
decrementeaz dr, lund valoarea 4.
i 1 2 3 4
A 1 2 3 4
28
Algoritmi de sortare
st = 0 < dr = 4 i A[dr] = 4 > A[V] = 1, deci se ajunge la dr = 3.
i 1 2 3 4
A 1 2 3 4
st = 0 < dr = 3 i A[dr] = 3] > A[V] = 1, deci se ajunge la dr = 2.
i 1 2 3 4
A 1 2 3 4
st = 0 < dr = 2 i A[dr] = 2 > A[V] = 1, deci se ajunge la dr = 1.
i 1 2 3 4
A 1 2 3 4
st = 0 < dr = 1, dar A[dr] = A[V] = 1, deci se va iei din prima bucl
execut ... ct timp. A doua bucl nu va apuca dect s incrementeze
variabila st, ajungndu-se la st = dr i ieindu-se i din aceast bucl.
Condiia st < dr nu se verific, deci nu se efectueaz nicio interschimbare i
se iese i din bucla principal ct timp ... execut. Se returneaz dr = 1.
Dup ieirea din funcia Partitie, se atribuie valoarea returnat
variabilei P, dup care se efectueaz dou apeluri recursive ale funciei
Quicksort. Mai nti se apeleaz Quicksort(A, 1, 1), funcie din care se va
iei foarte rapid, deoarece nu va fi respectat condiia st < dr. Se reveni la
pasul precedent i se apeleaz funcia Quicksort(A, 2, 4). Aceast apel se va
comporta similar cu apelul iniial. Lsm desluirea tuturor pailor efectuai
pe seama cititorului.
Exemplul prezentat ascunde o deficien major a algoritmului. Se
poate observa c, pentru un ir care este deja sortat, funcia partiie va
mpri ntotdeauna o subsecven [st, dr] n dou subsevene de lungime 1
respectiv dr st. Cum am spus mai devreme, acest lucru duce la o
complexitate ptratic, adic O(N2). Se va returna ntotdeauna un pivot care
va mpri subsecvena curent ntr-o subsecven de dimensiune 1, care
poate fi considerat sortat (orice ir cu un singur element este gata sortat) i
subsecvena iniial, mai puin un singur element. Acest lucru se repet de N
ori. Se efectueaz aadar N + (N 1) + + 1 operaii. Aceast sum este o
progresie aritmetic clasic, avnd valoarea N * (N + 1) / 2. Complexitatea
timp este aadar O(N2). Memoria suplimentar este O(N), deoarece avem N
niveluri de recursivitate, iar informaia memorat pe fiecare nivel este O(1).
29
Capitolul 2
Vom prezenta forma clasic a algoritmului, aa cum a fost prezentat
n pseudocod, dup care vom prezenta metode de mbuntire a timpului de
execuie i a memoriei folosite pe orice ir de intrare posibil.
Metoda clasica poate fi implementat astfel:
int Partitie(int A[], int st, int dr)
{
int V = A[st];
--st; ++dr;
while ( st < dr )
{
do
--dr;
while ( st < dr && A[dr] > V );
do
++st;
while ( st < dr && A[st] < V );
if ( st < dr )
{
int tmp = A[st];
A[st] = A[dr];
A[dr] = tmp;
}
}
return dr;
}
Algoritmi de sortare
complexitate este una probabilist i c, n cazul n care cineva cu intenii
maliioase ar avea acces la generatorul de numere aleatoare folosit n
alegerea pivotului, acesta ar putea teoretic genera date de intrare pentru care
complexitatea algoritmului s fie O(N2). Pentru toate scopurile practice ns,
putem considera c algoritmul are complexitatea O(Nlog N).
Aa cum am artat mai devreme, memoria suplimentar folosit de
algoritm este O(N) pe cazuri defavorabile. Putem reduce memoria la
O(log N) pentru toate cazurile, schimbnd ordinea apelurilor recursive i
reducnd numrul acestora. Observm ca apelurile recursive se fac la
sfritul algoritmului, neexistnd nicio instruciune care s se execute dup
acestea. Acest lucru ne permite s folosim, atunci cnd este posibil, tehnici
iterative n loc de apeluri recursive.
Primul lucru pe care l vom face este s apelm funcia recursiv
pentru subsecvena mai mic. Al doilea lucru este s renunm la al doilea
apel recursiv, pentru cea de-a doua secven, i s rezolvm aceast
subsecven iterativ. Astfel, memoria folosit va fi ntotdeauna O(log N).
Precizm c pentru a folosi funcia rand() trebuie inclus fiierul
antet <cstdlib>, iar nainte de apelarea funciei Quicksort, trebuie iniializat
generatorul de numere aleatoare folosind apelul srand((unsigned)time(0)).
Algoritmul optimizat arat acuma n felul urmtor:
31
Capitolul 2
int Partitie(int A[], int st, int dr)
{ // numar aleator din [st, dr]
int poz = st + rand() % (dr-st+1);
int tmp = A[poz];
A[poz] = A[st];
A[st] = tmp;
int V = A[st];
--st; ++dr;
while ( st < dr )
{
do
--dr;
while ( st < dr && A[dr] > V );
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
Funcia Interclasare efectueaz ntotdeauna dr st + 1 operaii,
deci complexitatea sa este O(N) pe nivel de recursivitate. Funcia
Merge_sort mparte ntotdeauna subsecvena curent n dou subsecvene
de dimensiuni egale sau care difer prin cel mult 1. Aadar, arborele
apelurilor recursive va avea O(log N) niveluri, rezultnd timpul O(Nlog N).
Se observ c, pentru a interclasa dou subsecvene, avem nevoie de
un vector auxiliar, pe care l-am notat cu B. Acest lucru nseamn c
memoria auxiliar folosit de algoritm este ntotdeauna O(N), mai mult
dect memoria folosit de orice alt algoritm prezentat pn acum. Totui,
asta nu nseamn c sortarea prin interclasare este ntotdeauna inferioar
algoritmilor precedeni. De multe ori, dac ne permitem timpul de execuie
pentru a sorta N obiecte, ne permitem i memoria auxiliar.
Pentru a nelege mai bine cum am dedus complexitatea algoritmului
i pentru a vizualiza modul de funcionare al acestuia, urmrii apelurile
funciilor n urmtorul exemplu. Acesta se execut conform cu numerotarea
de pe desen. Apelurile recursive i secvenele pe care acestea se fac sunt
marcate cu rou, iar apelurile funciei de interclasare, intervalele asociate
acesteia i ordinea elementelor rezultat dup interclasare sunt marcate cu
albastru.
Algoritmi de sortare
Dei este mai dificil de dedus numrul de operaii efectuate folosind
acest exemplu, putem observa c arborele rezultat n urma aplicrii
algoritmului de sortare prin interclasare are doar dou niveluri complete pe
care se apeleaz funcia Interclasare. Astfel, avem dou niveluri pe care se
execut O(N) operaii, unde N = 5 n cazul de fa, iar 2 5 = 2, deci
putem folosi acest exemplu pentru a intui complexitatea algoritmului ca
fiind O(Nlog N), iar memoria auxiliar folosit ca fiind O(N).
Am putea fi tentai s argumentm c se efectueaz, de fapt, patru
apeluri ale funciei de interclasare, fiecare executnd O(N) operaii,
rezultnd astfel o complexitate timp de O(N2). Acest lucru este fals ns,
deoarece, dei se fac ntr-adevr patru apeluri ale acestei funcii, doar apelul
de pe primul nivel efectueaz N operaii. Apelurile de pe un nivel oarecare
efectueaz mpreun N operaii. Deoarece algoritmul mparte ntotdeauna
subsecvena curent n dou subsecvene egale, adncimea arborelui va fi
ntotdeauna direct proporional cu 2 ( + 1), rezultnd complexitatea
menionat. Trebuie ns inut cont de faptul c, n practic, acest algoritm
este mai puin eficient dect Quicksort.
Pentru a observa mai bine numrul de operaii efectuate, construii
un arbore similar cu cel prezentat anterior, dar pentru N o putere a lui 2, de
exemplu 32 sau 64. Aa se va observa mult mai clar complexitatea
algoritmului.
Sortarea prin interclasare are aplicabiliti i n unele probleme de
numrare. Un exemplu clasic este aflarea numrului de inversiuni ale unei
permutri. O inversiune a unei permutri
=
1
(1)
2
(2)
()
Capitolul 2
void Interclasare(int A[],
int st, int m, int dr)
{
// folosim numerotare de la 1,
// deci trebuie declarat un element
// in plus
int *B = new int[dr - st + 2];
int i = st, j = m + 1, k = 0;
while ( i <= m && j <= dr )
if ( A[i] <= A[j] )
B[++k] = A[i++];
else
B[++k] = A[j++];
36
Algoritmi de sortare
2.5. Heapsort
Heapsort, cunoscut i sub numele de sortare prin ansamble, este
un algoritm de sortare cu timpul de execuie O(Nlog N) i memorie
auxiliar O(1). Algoritmul este, practic, o optimizare a algoritmului de
sortare prin selecie (Selection sort), algoritm care funcioneaz
determinnd la fiecare pas elementul de valoarea maxim i mutndu-l pe
ultima poziie liber a vectorului. Deoarece trebuie s determinm N
maxime, iar determinarea unui maxim implic verificarea tuturor
elementelor vectorului, complexitatea acestui algoritm este O(N2). Heapsort
folosete structura de date numit heap pentru a determina cele N maxime,
rezultnd un timp de execuie de O(Nlog N). Pentru a nelege mai bine ce
este acela un heap, vom ncepe prin prezentarea unor noiuni teoretice.
Definiia 1: Un arbore binar este un arbore n care fiecare nod are
cel mult doi descendeni direci.
Definiia 2: Un arbore binar complet este un arbore binar n care
fiecare nivel al arborelui, eventual mai puin ultimul, are numr maxim de
noduri (nivelul h al arborelui are 2h noduri, numerotarea ncepnd de la
zero). n cazul n care ultimul nivel nu are numr maxim de noduri,
completarea cu noduri a ultimului nivel trebuie s se fac de la stnga spre
dreapta.
Definiia 3-4: Un max-heap este un arbore binar complet n care
orice nod are asociat o valoare mai mare sau egal (nu neaprat n sensul
clasic) cu valorile asociate descendenilor acestui nod, dac acest nod are
descendeni. Dac orice nod are asociat o valoare mai mic sau egal cu
valorile asociate descendenilor si, structura de date poart numele de
min-heap. De exemplu, desenul urmtor reprezint un max-heap:
Capitolul 2
Aceast structur de date suport operaiile de inserare a unui nod i
de tergere a rdcinii n compexitate O(log N), unde N este numrul
elementelor din heap. Operaia de aflare a maximului (sau a minimului) are
complexitatea O(1), deoarece tot ce trebuie s facem este s verificm nodul
rdcin.
Un heap poate fi reprezentat foarte uor folosind un vector A cu N
elemente, fiecare element reprezentnd valoarea unui nod al heap-ului.
Rdcina va fi reinut n A[1], iar descendenii direci ai acesteia n A[2]
pentru fiul stng i A[3] pentru fiul drept. n cazul general, fiii unui nod
reprezentat prin A[k] se vor afla n A[2k] pentru fiul stng i A[2k+1]
pentru fiul drept. Tatl unui nod A[k] se va afla pe poziia A[k / 2] (se ia
ntotdeauna partea ntreag a rezultatului mpririi).
De exemplu, heap-ul din figura precedent poate fi reprezentat
printr-un vector n modul urmtor:
i
1 2 3 4 5 6 7
19
13 15 5 6 12 14
A
Pentru a putea implementa algoritmul Heapsort, avem nevoie de
urmtoarele trei operaii: inserarea unei valori n heap, tergerea rdcinii
din heap i transformarea unui vector oarecare ntr-un vector care reprezint
un heap valid.
Inserarea unei valori x ntr-un max-heap se face n felul urmtor:
Se creaz un nod cu valoarea x la sfritul heap-ului.
Ct timp x este mai mare dect valoarea tatlui nodului asociat
lui x execut
o Interschimb tatl nodului lui x cu nodul lui x.
De exemplu, dac vrem s adugm n heap-ul prezentat anterior
valoarea 18, se procedeaz ca n desenul de mai jos.
38
Algoritmi de sortare
Sau, folosind un vector:
i
1 2 3 4 5 6 7 8
A 19 13 15 5 6 12 14 18
Se compar A[8] = 18 cu A[8 / 2] = A[4] = 5. 18 > 5 deci se
interschimb A[8] cu A[5] i rezult vectorul:
i
1 2 3 4 5 6 7 8
A 19 13 15 18 6 12 14 5
Se compar A[4] = 18 cu A[4 / 2] = A[2] = 13. 18 > 13 deci se
interschimb A[4] cu A[2] i rezult vectorul:
i
1 2 3 4 5 6 7 8
A 19 18 15 13 6 12 14 5
Se compar A[2] = 18 cu A[2 / 2] = A[1] = 19. 18 < 19, deci algoritmul se
ncheie i valoarea inserat se afl pe poziia pe care trebuie.
Inserarea unui element ntr-un heap are complexitatea O(log N),
deoarece nlimea unui heap este dat de logaritmul binar al lui N.
tergerea rdcinii se poate face folosind urmtorul algoritm:
Se nlocuiete rdcina cu ultimul nod al heap-ului i se terge
ultimul nod. Fie x noua rdcin.
Ct timp valoarea lui x este mai mic dect cel puin unul dintre
fiii si execut
o Interschimb x cu fiul care are valoarea cea mai mare.
Este important s efectum interschimbarea cu fiul care
are valoarea cea mai mare, deoarece n caz contrar am
obine un heap invalid, existnd un nod care este printe
pentru un nod cu valoare mai mare. Astfel, se reface
structura de heap.
Datorit nlimii unui heap, aceast operaie are complexitatea tot
O(log N).
n figura urmtoare putei vizualiza modul n care se terge rdcina
unui heap.
39
Capitolul 2
Algoritmi de sortare
primele N 1 poziii ale vectorului. Procednd n acest fel pn ce am
extras N maxime, vectorul va ajunge s fie sortat.
Pentru a transforma un vector oarecare ntr-un heap, vom considera
o funcie Downheap(A, poz, N) care aplic algoritmul de tergere a
rdcinii, prezentat anterior, subarborelui cu rdcina pe poziia poz a
vectorului A. N reprezint numrul de noduri ale heap-ului.
Avnd aceast funcie, putem construi o alt funcie,
Transformare(A, N), care transform vectorul A (cu N elemente) ntr-un
heap. Aceast funcie poate fi implementat n felul urmtor:
Pentru fiecare i de la
pn la 1 execut
2
o Apeleaz Downheap(A, i, N)
Funcia este foarte simplu de implementat, dar poate s nu fie clar
cum am ajuns la aceast metod sau de ce aceasta este corect.
Algoritmul de transformare a unui vector oarecare ntr-un vector
2
+1 = 2
+ 2 > . Dar asta ar nsemna c fiul se afl n afara
2
2
vectorului, deci presupunerea fcut este fals, elementul analizat fiind deci
frunz. Rezult de aici c i celelalte elemente sunt frunze, avnd indici i
mai mari.
O proprietate exploatat de acest algoritm este aceea c orice
subarbore al unui heap este la rndul su heap. Acest afirmaie se poate
demonstra prin inducie. Astfel, ne propunem s transform vectorul dat ntrun heap considernd c acesta reprezint la nceput un arbore binar complet
oarecare i transformnd pe rnd fiecare subarbore ntr-un heap. Pentru
acest lucru, apelm funcia Downheap pentru fiecare element care nu
reprezint o frunz. Aa ajungem la algoritmul prezentat anterior n
pseudocod. Complexitatea acestei funcii este O(N), dei am putea fi tentai
s spunem c este O(Nlog N). Demonstraia acestei afirmaii este lsat pe
seama cititorului.
41
Capitolul 2
Avem acum toate noiunile necesare pentru a implementa eficient
algoritmul Heapsort. Dei teoria din spatele algoritmului poate fi mai greu
de neles dect teoria din spatele algoritmilor prezentai anterior, merit
fcut efortul necesar nelegerii acesteia, Heapsort fiind un algoritm care,
dei este mai ncet n practic dect Quicksort, are avantajul de a folosi
memorie suplimentar constant i de a nu folosi funcii recursive. Aceste
lucruri pot fi foarte importante dac avem nevoie de un algoritm de
complexitate O(Nlog N) pe cel mai ru caz i care s foloseasc memorie
ct mai puin.
Funcia Heapsort(A, N) poate fi implementat n felul urmtor:
Apeleaz Transformare(A, N)
Pentru fiecare i de la N la 1 execut
o Interschimb A[i] cu A[1]
o Apeleaz Downheap(A, 1, i 1)
Am menionat la algoritmul Quicksort existena unui algoritm care
combin Quicksort cu Heapsort, rezultnd un algoritm foarte eficient numit
Introsort. Ideea din spatele acestui algoritm este s ncepem prin a folosi
Quicksort, dar numai pn cnd nivelul recursivitii nu depete o anumit
limit, egal, de exemplu, cu 2 , unde N este numrul de elemente ale
vectorului care trebuie sortat. Dac nivelul recursivitii depete aceast
valoare, vom folosi Heapsort pentru a sorta subsecvena curent.
Pentru cele mai bune rezultate, este recomandat s se testeze mai
multe limite pe ct mai multe date de intrare.
Dac se ajunge la subsecvene de dimensiuni mici, dar adncimea
recursivitii nu a depit limita impus, Introsort poate folosi algoritmul de
sortare prin inserie pentru a sorta aceste subsecvene, eliminndu-se i mai
multe apeluri recursive.
Un fapt ce merit menionat este c funcia std::sort din fiierul
antet algorithm este, pe majoritatea compilatoarelor, implementat folosind
algoritmul Introsort. Un model de implementare este dat n seciunea
urmtoare.
42
Algoritmi de sortare
void Downheap(int A[], int poz, int N)
{
int FiuSt, FiuDr, Schimb;
while ( 2*poz <= N )
{
FiuSt = 2*poz;
FiuDr = 2*poz + 1;
Schimb = FiuSt;
if ( FiuDr <= N )
if ( A[FiuDr] > A[Schimb] )
Schimb = FiuDr;
if ( A[Schimb] > A[poz] )
{
swap(A[Schimb], A[poz]); // poate fi folosita prin includerea
// <fstream> sau <iostream>
// si a namespace-ului std
poz = Schimb;
}
else
break;
}
}
void Transformare(int A[], int N)
{
for ( int i = N / 2; i >= 1; --i )
Downheap(A, i, N);
}
void Heapsort(int A[], int N)
{
Transformare(A, N);
for ( int i = N; i >= 1; --i )
{
swap(A[i], A[1]);
Downheap(A, 1, i - 1);
}
}
Capitolul 2
seama cititorului. Partitie este funcia prezentat n cadrul algoritmului
Quicksort, iar apelul initial este:
Introsort(A, N, 1, N, 0);
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
Am prezentat pn acum doi algoritmi de complexitate ptratic i
patru de complexitate liniar-logaritmic, acetia patru fiind cei mai des
folosii algoritmi n practic. Aa cum probabil c ai observat,
complexitatea acestor algoritmi nu a sczut niciodat sub O(Nlog N) pe
cazurile medii i defavorabile. Alt asemnare a algoritmilor prezentai pn
n acest moment este c fiecare se bazeaz pe comparaii ntre elemente.
Un algoritm de sortare bazat pe comparaii trebuie s efectueze ntotdeauna
minim O(Nlog N) operaii pe cazul defavorabil, deci putem considera
algoritmii Quicksort, Merge sort, Heapsort i Introsort ca fiind optimi.
Aceti algoritmi sunt ns optimi doar n cadrul clasei de algoritmi
bazai pe comparaii. Aa cum vom vedea, putem obine algoritmi mai
eficieni dac folosim alte tehnici de sortare care nu compar elementele.
Dezavantajul acestor tehnici este c se bazeaz pe anumite
particulariti ale datelor ce trebuiesc sortate i ale criteriului dup care
acestea trebuiesc sortate. Sortrile bazate pe comparaii sunt uor de
modificat pentru a sorta tipuri de date neelementare, singura schimbare
major ce trebuie fcut este nlocuirea operatorilor de comparare cu funcii
care compar tipurile de date ce trebuiesc sortate. O alt posibilitate este
suprancrcarea acestor operatori pentru tipurile date necesare.
Pentru algoritmii ce urmeaz a fi prezentai, Counting sort i Radix
sort, modificrile necesare pentru a sorta orice altceva n afar de numere
naturale sunt mai dificil de realizat, sau chiar imposibile n unele situaii.
Aceti doi algoritmi au ns avantajul de a fi mult mai rapizi dac datele ce
trebuiesc sortate au anumite particulariti.
Capitolul 2
Pentru fiecare i de la 0 la MaxV execut
o Pentru fiecare j de la 1 la V[i] execut
Adaug-l pe i la sfritul vectorului A.
La finalul execuiei acestor instruciuni, vectorul A va conine
elemente iniiale n ordine cresctoare. Algoritmul numr practic cte
elemente exist din fiecare valoare posibil, dup care parcurge n ordine
fiecare valoare posibil i o adaug n vectorul soluie de cte ori este
nevoie. V[i] reprezint aadar numrul de elemente egale cu i din vectorul
A.
De exemplu, daca avem vectorul:
i 1 2 3 4 5 6
A 4 7 2 2 1 3
Vectorul V ar trebui s aib dimensiunea 7+1=8, deoarece 7 este
valoarea maxim a unui element din A. V se iniializeaz cu 0 i se
construiete n felul urmtor: primul element analizat este A[1], deci
V[ A[1] ] primee valoarea V[ A[1] ] + 1. A[1] = 4, deci V[4] pimete
valoarea V[4] + 1 = 0 + 1 = 1. Al doilea element analizat este A[2], deci,
procednd ca i la primul element, V[7] va primi tot valoarea 1.
n final, V va arta n felul urmtor:
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 poziiile i
ale vectorului V i punem n A valoarea i de V[i] ori.
Este clar c sortarea prin numrare este foarte eficient atunci cnd
avem un numr mare de valori mici ce trebuiesc sortate. Dezavantajele
acestei metode sunt c nu putem sorta dect numere naturale.
Pentru a extinde metoda la numere ntregi din intervalul
[-MaxV, MaxV], putem aduna fiecrui element valoarea MaxV,
transformnd 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 numr X de cifre dup virgul, putem s le nmulim pe fiecare cu
10X, dup care s le sortm 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 crete de
10X ori, iar timpul de execuie 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 nite iruri de caractere folosind aceast
metod? Dar nite perechi de numere dup prima component?
const int MaxVal = 1000;
void Counting_sort(int A[], int N)
{
int *V = new int[MaxVal + 1];
for ( int i = 0; i <= MaxVal; ++i )
V[i] = 0;
for ( int i = 1; i <= N; ++i )
++V[ A[i] ];
for ( int i = 0, k = 0; i <= MaxVal; ++i )
for ( int j = 1; j <= V[i]; ++j )
A[++k] = i;
delete []V;
}
+1
8 ()
unde sizeof(int) reprezint numrul de bytes (octei) folosii de tipul de date
int pe sistemul pe care se lucreaz. Deoarece 1 byte = 8 bii, iar pentru
reprezentarea valorilor 0 i 1 (echivalente cu false respectiv true) avem
nevoie de un singur bit, putem gestiona mai eficient memoria, chiar dac
vom avea un cod mai greu de neles. Detalii despre efecutarea operaiilor pe
bii putei gsi n seciunea urmtoare, la algoritmul Radix sort.
47
Capitolul 2
Tabelul 2.6.1. Proprietile algoritmului de sortare prin numrare
Caz favorabil Caz mediu Caz defav.
Timp de execuie
O(N+MaxVal)
Memorie suplimentar
O(MaxVal)
Stabil
DA
Dei am considerat algoritmul ca fiind optim pe toate cazurile,
trebuie inut cont de faptul ca acest lucru nu se aplic dect dac MaxVal
este cu mult mai mic dect N.
Algoritmi de sortare
Ultimul pas este s sortm numerele dup cifra sutelor:
i
1
2
3
4
5
6
7
8
9
10
A 007 027 034 088 111 145 325 353 430 932
Astfel, vectorul ajunge s fie sortat. Deja am putea implementa
algoritmul n aceast form fr prea mari complicaii. Ar trebui doar s
modificm sortarea prin numrare pentru a sorta numerele dup o anumit
cifr, lucru care va fi explicat n detaliu dup ce vom prezenta o optimizare
care va reduce cu mult timpul de execuie. Dac am implementa algoritmul
n forma sa actual, timpul de execuie ar fi O(Nlog MaxVal), unde
MaxVal reprezint valoarea maxim a numerelor din vector. Acest
complexitate se datoreaz faptului c aplicm sortarea prin numrare de
NrCif ori, unde NrCif reprezint numrul de cifre ale celui mai mare numr
din vector, iar = 1 + 10 . Deoarece sortarea prin
numrare se aplic ntotdeauna unor numere de o singur cifr, putem
considera timpul de execuie al acesteia ca fiind O(N). Memoria
suplimentar folosit este O(N), avnd nevoie, aa cum vom vedea, de un
vector auxiliar de dimensiune N.
Avem deja un algoritm eficient chiar i n cazul n care avem
MaxVal > N, deoarece baza logaritmului este 10, nu doi aa cum este n
cazul algoritmilor prezentai pn acum.
Putem ns obine un algoritm i mai rapid. Pentru acest lucru, vom
prezenta mai nti cteva noiuni teoretice despre reprezentarea numerelor n
sistemul zecimal i sistemul binar cu ajutorul cror vom putea optimiza
numrul de apeluri ale sortrii prin numrare.
Definiia 1: Un numr natural care are cea mai mare cifr C poate fi
considerat un numr n toate bazele mai mari dect C. De exemplu, numrul
5213 poate fi considerat un numr n toate bazele mai mari dect 5. Sistemul
(baza) n care un numr este scris se marcheaz prin trecerea bazei ca indice
al numrului.
Definiia 2: O cifr a unui numr natural n baza 2 se numete bit. 8
bii = 1 byte.
Proprietatea 1: Orice numr natural notat n felul urmtor:
= 1 2 , {0,1, ,9}
este scris n sistemul zecimal (baza 10), n urmtoarea form:
49
Capitolul 2
10 = 1 10 1 + 2 10 2 + + 100
De exemplu, numrul 362 este scris n felul urmtor:
36210 = 3102 + 610 + 2
Proprietatea 2: Orice numr natural
= 1 2 , {0,1, ,9}
care reprezint un numr n sistemul binar (baza 2) poate fi
transformat n echivalentul su din baza 10 n felul urmtor:
2 = (1 21 + 2 22 + + 20 )10
.
De exemplu, 10112 = 1110
Proprietatea 3: Orice numr natural notat n felul urmtor:
= 1 2 , {0,1, ,9}
care reprezint un numr n baza 10, poate fi transformat n baza 2
prin urmtorul algoritm:
Ct timp X diferit de 0 execut
o Noteaz restul mpririi lui X la 2.
o X=X/2
Resturile notate, citite de la dreapta la stnga, reprezint
numrul transformat din baza 10 n baza 2.
De exemplu, dac vrem s transformm numrul 1110 n baza 2, vom
proceda n felul urmtor: 11 este diferit de 0, deci notm restul mpririi
sale la 2, acesta fiind 1. l mprim pe 11 la 2 i reinem partea ntreag a
mpririi, adic 5. 5 este diferit de 0, deci notm restul mpirii sale la 2,
care este 1. Reinem partea ntreag a mpririi lui 5 la 2, adic 2. Restul
mpririi lui 2 la 2 este 0, care se noteaz. 2 mprit la 2 este 1, iar restul
mpririi lui 1 la 2 este 1. Partea ntreag a mpririi lui 1 la 2 este 0, deci
am terminat. Reprezentarea binar este dat de citirea resturilor obinute: 1,
1, 0, 1 de la dreapta la stnga 10112 = 1110.
Cele dou transformri prezentate se pot aplica i altor baze, dar de
cele mai multe ori ne intereseaz doar baza 2.
50
Algoritmi de sortare
Pentru a implementa eficient algoritmul Radix sort vom avea nevoie
s efectum anumite operaii logice pe reprezentarea binar a numerelor
date spre sortare. Calculatorul reine automat numerele n baza 2, deci nu va
trebui s efectum vreo transformare, ci doar s aplicm operaiile necesare.
Aceste operaii sunt:
Operaia I (AND):
Operaia I (operatorul &) se aplic asupra a dou valori numere
naturale (unsigned int). Rezultatul este un numr natural obinut prin
conjuncia tuturor biilor primului numr cu biii de pe aceleai poziii ai
celui de-al doilea numr. Tabelul de adevr al conjunciei este:
p
1
1
0
0
q
1
0
1
0
p&q
1
0
0
0
q
1
0
1
0
p|q
1
1
1
0
Capitolul 2
deci este reprezentat pe 32 de bii. De fiecare dat cnd avem de gnd s
efectum operaii pe bii este de preferat s lucrm cu tipuri de date fr
semn (unsigned), pentru a nu avea grija bitului de semn i pentru a putea
profita astfel de toi cei 32 de bii ai tipului de date int.
Operaiile pe bii ne ajut s optimizm algoritmul Radix sort,
aducndu-l la complexitatea timp O(N) pentru numere naturale
reprezentabile pe 32 de bii. Nu vom mai sorta numerele dup valorile
fiecarei cifre, ci dup valorile fiecrei grupe de bii. Vom alege un numr
natural MaxG care va reprezenta numrul de bii dintr-o grup. Algoritmul
prezentat anterior rmne nemodificat, doar c de data aceasta vom
considera o cifr ca fiind format din MaxG bii. Vom avea deci nevoie
de un vector de dimensiune 2MaxG n cadrul sortrii prin numrare, dar i de
un vector auxiliar de dimensiune N. Memoria folosit va fi astfel O(N).
n implementarea prezentat vom considera MaxG = 16, deci pentru
numere naturale din intervalul [0, 232 1] va trebui s aplicm sortarea prin
numrare de dou ori. n cazul acesta, timpul de execuie poate fi considerat
O(N), dar n cazul general, cnd nu tim ct de mari sunt numerele pe care
trebuie s le sortm, timpul de execuie este O(kN), unde k reprezint de
cte ori trebuie apelat procedura de sortare prin numrare.
De exemplu, pe urmtorul vector, n care numerele sunt date n baza
2, reprezentate pe 8 bii, iar MaxG = 4:
i
A
1
01101110
2
00010111
3
11101001
1
00010111
2
11101001
3
01101110
Deoarece 01112 = 710, 10012 = 910 i 11102 = 1410. Vom sorta acum
numerele dup urmtoarea grup de de patru bii:
i
A
1
00010111
2
01101110
3
11101001
52
Algoritmi de sortare
Mai trebuie s rezolvm doar subproblema sortrii numerelor dup o
anumit grup de bii. Pentru aceasta vom folosi o funcie ajuttoare numit
Sortare(A, N, T, Gr, V, Poz) care sorteaz numerele din vectorul A, de
dimensiune N, dup grupa de bii cu numrul Gr i care reine rezultatul n
vectorul T. Funcia va folosi doi vectori de caracterizare de dimensiune
MaxG: un vector V, unde V[i] reprezint cte numere exist n vectorul A
care au grupa Gr de bii egal cu i i un vector Poz, unde Poz[i] reprezint
poziia pe care trebuie pus n vectorul T primul numr pentru care grupa Gr
are valoarea i.
Poz[0] = 1, iar Poz[i] = Poz[i 1] + V[i 1], pentru orice i > 0.
De exemplu, dac avem urmtorul vector V:
i 0 1 2 3 4 5 6
A 3 4 3 2 1 1 7
Atunci vectorul Poz va fi urmtorul:
i 0 1 2 3 4 5 6
A 1 4 8 11 13 14 15
Ceea ce nseamn c cele trei numere care au grupa Gr egal cu 0
vor fi puse n vectorul T pe poziiile 1, 2 i 3. Cele patru numere cu grupa
Gr egal cu 1 vor fi puse n vectorul T pe poziiile 4, 5, 6 i 7. Se
procedeaz la fel i cu celelalte numere.
Pentru a afla valoarea unei grupe, vom numerota grupele de la
dreapta spre stnga ncepnd de la 0 i vom folosi operaiile i operatorii pe
bii prezentate anterior pentru a afla valoarea grupei curente. Mai exact, vom
folosi o funcie AflaGrupa(Nr, Gr) care va returna valoarea X grupei Gr a
numrului Nr. Aceast valoare poate fi calculat folosind urmtoarea
formul: X = ((Nr >> (Gr * MaxG)) & (MaxVal - 1)); unde MaxVal este
egal cu 2MaxG 1 = 216 1.
Formula rezult din faptul c orice putere a lui doi se reprezint n
sistemul binar ca un ir de bii cu un singur bit de valoare 1 urmat numai de
bii de valoare 0. Astfel, sczndu-l pe 1 dintr-o putere a lui 2, reprezentarea
binar a rezultatului va fi alctuit numai din bii de valoare 1. Efectund
operaia I ntre un numr X i un alt numr care are toi biii de valoare 1,
rezultatul va fi ntotdeauna numrul X.
53
Capitolul 2
De exemplu, dac ne propunem s aflm valoarea primilor cinci bii
(de la dreapta spre stnga) ai numrului 11010110110 2, tot ce trebuie s
facem este s aplicm operaia I ntre acest numr i numrul 11111 2:
110101101102 &
000000111112
000000101102 = 2210
Pentru a afla valoarea urmtorilor cinci bii, vom deplasa mai nti
numrul iniial cu cinci poziii ctre dreapta i vom aplica operaia I pe
numrul rezultat prin deplasare:
110101101102 >>
000001101012 &
5
000000111112
000001101012
000000101012 = 2110
const int MaxG = 16;
const unsigned int MaxVal = 1 << 16; // 2 la puterea 16
unsigned int AflaGrupa(unsigned int Nr, unsigned int Gr)
{
// se afla valoarea grupei Gr a numarului Nr
return ((Nr >> (Gr * MaxG)) & (MaxVal - 1));
}
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];
}
void Radix_sort(unsigned int A[], int N)
{
unsigned int *T = new unsigned int[N + 1];
int *V = new int[MaxVal];
int *Poz = new int[MaxVal];
Sortare(A, N, T, 0, V, Poz);
Sortare(T, N, A, 1, V, Poz);
delete []T; delete []V; delete []Poz;
}
54
Algoritmi de sortare
Pentru consisten, am putea alege s declarm toate variabilele fr
semn, dar acest lucru nu afecteaz n vreun fel algoritmul.
Menionm c, n practic, Quicksort rmne n continuare
algoritmul mai rapid, Radix sort fiind mai rapid doar dac numrul
elementelor ce trebuie sortate este foarte mare.
Tabelul 2.7.1. Proprietile algoritmului Radix sort
Caz favorabil Caz mediu Caz defav.
Timp de execuie
O(kN)
Memorie suplimentar
O(N)
Stabil
DA
2.8. Concluzii
Am prezentat n acest capitol opt algoritmi de sortare, metode de
implementare a acestora, optimizri i situaiile n care fiecare se potrivete
cel mai bine.
Tabelul 2.8.1. Comparaie ntre toi algoritmii de sortare prezentai
Timp de execuie
Memorie suplimentar
Nume
Caz
Caz
Caz
Caz
Caz
Caz
Stabil
algoritm
fav. mediu defav. fav.
mediu
defav.
Bubble
O(N) O(N2) O(N2)
O(1)
DA
sort
Insertion
O(N) O(N2) O(N2)
O(1)
DA
sort
Quicksort
O(Nlog N)1
O(1) O(log N) O(log N)
NU
Merge
O(Nlog N)
O(N)
DA
sort
Heapsort
O(Nlog N)
O(1)
NU
Introsort
O(Nlog N)
O(log N)
NU
Counting
O(N + MaxVal)
O(MaxVal) 2
DA
sort
Radix
O(kN) 3
O(N)
DA
sort
1
55
Capitolul 2
Problema sortrii unor date este aadar o problem studiat de foarte
mult timp i pentru care s-au gsit muli algoritmi, unii eficieni indiferent
de natura datelor care trebuie sortate, alii proiectai special pentru anumite
tipuri de date. Algoritmii de sortare reprezint o introducere perfect n
informatic datorit faptului c sunt uor de neles i pot fi implementai
fr prea mari dificulti. Implementarea acestora nu necesit dect
cunotine elementare ale limbajului de programare n care se lucreaz, n
acest caz C++, fapt care permite studierea i nelegerea acestora i de ctre
persoane care abia au nceput studiul limbajului C++.
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 nsoite de
aplicaii practice clasice, cum ar fi problema turnurilor din Hanoi i
generarea permutrilor, aranjamentelor, combinrilor etc.
Acest capitol este foarte important, ntruct orice problem poate fi
rezolvat printr-un algoritm care se ncadreaz ntr-una dintre tehnicile
menionate. Recomandm aadar stpnirea acestora.
57
Capitolul 3
CUPRINS
3.1. Recursivitate ..............................................................................................59
3.2. Backtracking ..............................................................................................68
3.3. Divide et impera ........................................................................................82
3.4. Greedy........................................................................................................89
3.5. Programare dinamic................................................................................96
58
Tehnici de programare
3.1. Recursivitate
S pornim de la definiia de baz: o funcie este recursiv dac n
definiia ei se folosete o referire la ea nsi.
Din aceast definiie putem considera modelul general al unui
algoritm recursiv de forma:
rec(param_formali) { rec(param_formali) }
1
1
= 0
> 0
int f(int n)
{
if ( n == 0 ) return 1;
else
return f(n - 1) * n ;
}
59
= 0
> 0 = 0
> 0 > 0
Capitolul 3
int A(int m, int n)
{
if ( m == 0 )
else if ( n == 0 )
else
}
return n + 1;
return A(m - 1, 1);
return A(m - 1, A(m, n - 1));
0
1
1 + ( 2)
= 0
= 1
2
Conform definiiei:
int F(int n)
{
if ( n == 0 ) return 0;
else if ( n == 1)
return 1;
else
return F(n 1) + F(n 2);
}
(, % )
60
= 0
> 0
Tehnici de programare
a) Fractali
O problem important a recursivitii o constituie fractalii. n
aceast seciune vom aminti doar sumar conceptul fractalilor geometrici n
care modelul de baz al funciei recursive va genera figuri geometrice (n
diferite ipostaze i de diferite dimensiuni), iar condiia de oprire va fi dat de
posibilitatea desenrii acestor figuri geometrice (latura > 1 pixel)
S presupunem un exemplu n care figura de baz este un ptrat
(x, y, l). Procedura patrat deseneaz figura geometric numit ptrat cu
centrul n (x, y) i de latur l. (Fig. 3.1.1. a, b, c)
Fig. 3.1.1. a)
Urmtorul 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
Genernd eroare datorit lipsei condiiei de oprire.
S implementm condiia de oprire i s construim nc o ramur.
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)
n acelai mod se obin i figurile clasice Koch, Sierpinski, Kepler,
Cantor...
62
Tehnici de programare
Obiectivul problemei (jocului) const n mutarea celor n discuri de
pe tija surs pe tija destinaie folosind tija intermediar cu urmtoarele trei
restricii:
1. Se poate muta o dat o singur pies de pe o anumit tij (cea
mai de sus). n Fig. 3.1.2. singura pies disponibil este cea
roie.
2. Nu se poate pune un disc de dimensiune mai mare peste un disc
de dimensiune (circumferin) mai mic.
3. Numrul de mutri trebuie s fie minim.
Capitolul 3
A doua etap const n mutarea unui disc de pe surs pe destinaie,
lucru ce se poate face foarte uor.
64
Tehnici de programare
Capitolul 3
La acest nivel putem concepe urmtorul model care va constitui i
baza de funcionare a algoritmului recursiv.
Funcia de rezolvare Hanoi(n, A, B, C) se poate descompune n trei
subprobleme n ordinea urmtoare:
1. Hanoi (n 1, A, C, B)
2. Hanoi (1, A, B, C)
3. Hanoi (n 1, B,A,C)
Hanoi(1, A, B, C) este soluia trivial: mut de pe A pe C.
#include <iostream>
using namespace std;
void Hanoi(int nDiscuri, char tSursa, char tInter, char tDest)
{
if( nDiscuri > 0 )
{
Hanoi(nDiscuri 1, tSursa, tDest, tInter);
cout << tSursa << " --> " << tDest << endl;
Hanoi(nDiscuri 1, tInter, tSursa, tDest);
}
}
int main()
{
Hanoi (3,'A','B','C');
return 0;
}
66
Tehnici de programare
Capitolul 3
Mai trebuie s construim condiia de oprire a algoritmului recursiv.
Acesta va trebui oprit fie cnd n ajunge la valoarea 1 fie cnd ajunge la 2, n
funcie de valoarea iniial a lui n (par sau impar).
#include <iostream>
using namespace std;
void Hanoi4tije(int nDiscuri,
char tSursa, char tInter1, char tInter2, char tDest)
{
if ( nDiscuri == 1 )
cout << tSursa << " --> " << tDest << endl;
else if ( nDiscuri == 2 )
{
cout << tSursa << " --> " << tInter1 << endl;
cout << tSursa << " --> " << tDest << endl;
cout << tInter1 << " --> " << tDest << endl;
}
else
{
Hanoi4tije(nDiscuri - 2, tSursa, tInter2, tDest, tInter1);
cout << tSursa << " --> " << tInter2 << endl;
cout << tSursa << " --> " << tDest << endl;
cout << tInter2 << " --> " << tDest << endl;
Hanoi4tije(nDiscuri - 2, tInter1, tSursa, tInter2, tDest);
}
}
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 construiete treptat soluia unei probleme, iar n
cazul n care soluia construit se dovedete a fi invalid (sau ne intereseaz
mai multe soluii), revine la un pas precedent pentru a schimba o alegere
fcut. Acest lucru se continu, de obicei, pn cnd au fost explorate toate
posibilitile sau pn cnd am gsit una sau mai multe soluii valide. De
multe ori nu este necesar explorarea tuturor posibilitilor, putnd elimina
68
Tehnici de programare
alegeri care ar conduce la o soluie invalid fr a genera o soluie complet.
Astfel, se reduce foarte mult numrul de operaii efectuate.
Tehnica backtracking are la baz structura de date numit stiv.
Stiva este reprezentat de un vector n care se depune, la fiecare pas,
alegerea fcut la acel pas. Astfel, stiva va reprezenta ntotdeauna soluia
(eventual parial) la care s-a ajuns ntr-un anumit moment.
Forma general a metodei poate fi scris n pseudocod n felul
urmtor: definim o funcie Back(K, N, P, X, Sol), unde K reprezint pasul
curent al algoritmului, N reprezint dimensiunea stivei, adic dimensiunea
maxim a vectorului care poate reprezenta o soluie, P reprezint numrul
maxim de alegeri existente la un anumit pas, X reprezint vectorul ce
conine alegerile posibile pentru fiecare pas, iar Sol reprezint stiva folosit,
adic un vector. Funcia poate fi implementat n felul urmtor:
Dac K > N execut
o Dac soluia reprezentat de vectorul Sol este valid, se
afieaz vectorul Sol i se ncheie algoritmul (n caz c ne
intereseaz o singur soluie).
o Altfel, dac ne intereseaz mai multe soluii sau soluia
curent nu este valid, se continu algoritmul.
Altfel execut
o Pentru fiecare i de la 1 la P, execut
Sol[K] = Xi
Apeleaz recursiv Back(K + 1, N, P, X, Sol)
Dup cum se poate deduce din aceast generalizare a metodei,
timpul de execuie al unui algoritm de tip backtracking este O(NPN ),
deoarece avem P posibiliti pentru fiecare din cei N pai, iar validarea unei
soluie are, de obicei, complexitatea O(N). n practic ns, aceast
complexitate este de multe ori supraestimat, existnd diverse optimizri
euristice pentru problemele care nu admit dect rezolvri de tip
backtracking.
Evident, forma algoritmului se poate schimba de la o problem la
alta. De exemplu, este posibil s nu avem nevoie de vectorul X n cazul n
care alegerile posibile reprezint o mulime care se poate accesa i dac nu
este reinut ntr-un vector, cum ar fi mulimea numerelor naturale.
n cele ce urmeaz vom prezenta detaliat patru probleme a cror
rezolvare nu se poate face dect folosind aceast tehnic de programare:
generarea permutrilor unei mulimi, generarea aranjamentelor, generarea
combinrilor i generarea partiiilor unui numr.
69
Capitolul 3
70
Tehnici de programare
Privii cum funcioneaz algoritmul pentru N = 3. Iniial, vectorul
Fol se iniializeaz cu 0, iar vectorul Sol poate rmne neiniializat.
La primul pas, K = 1, se depune mai nti n Sol[K] valoarea i = 1,
care se marcheaz apoi ca fiind folosit:
1
Sol
1
K
i
Fol
1
2
3
true false false
2
1
K
i
Fol
1
2
3
true true false
3
2
1
K
i
Fol
1
2
3
true true true
Capitolul 3
3
2
1
Sol
3
2
1
K
i
Fol
1
2
3
true true false
1
2
3
true false true
72
Tehnici de programare
void perm(int K, int N, bool Fol[],
int Sol[], ofstream &out)
{
if ( K > N )
{
for ( int i = 1; i <= N; ++i )
out << Sol[i] << ' ';
out << endl;
return;
}
int main()
{
int N, Sol[maxN];
bool Fol[maxN];
citire(N);
for ( int i = 1; i <= N; ++i )
Fol[i] = false;
ofstream out("perm.out");
perm(1, N, Fol, Sol, out);
out.close();
return 0;
}
Fol[i] = true;
perm(K + 1, N, Fol, Sol, out);
Fol[i] = false;
}
}
Exerciii:
a) Ct de rapid este algoritmul pentru valori mai mari dect 8?
b) Mai putei gsi optimizri pentru acest algoritm? Dar alt
algoritm, care abordeaz diferit problema? (Indiciu: folosii
interschimbri)
c) Modificai algoritmul astfel nct s gseasc numai a P-a
permutare n ordine lexicografic.
d) Modificai algoritmul astfel nct s genereze toate permutrile
unei mulimi citite din fiier, mulime care poate avea elemente
care se repet.
b) Generarea aranjamentelor
Dndu-se dou numere naturale N i P (1 P N), ne propunem s
generm toate aranjamentele de N luate cte P ale mulimii numerelor
!
naturale. Aranjamente de N luate cte P se noteaz , iar =
Capitolul 3
N i P se citesc din fiierul aran.in, iar aranjamentele gsite se scriu
n fiierul aran.out, fiecare pe cte o linie.
Exemplu:
aran.in aran.out
32
12
13
21
23
31
32
Rezolvarea problemei este aproape la fel ca rezolvarea problemei
anterioare: generarea permutrilor. Vom folosi aceeai funcie, doar c, de
data aceasta, vom avea o soluie (un aranjament) atunci cnd K > P,
deoarece avem P poziii pe care trebuiesc aranjate obiectele, nu N, ca n
cazul permutrilor. Complexitatea algoritmului va fi O(NP), dar i aceasta
este supraestimat, n practic efectundu-se un numr de operaii mai
apropiat de numrul aranjamentelor.
Privii modul de funcionare al algoritmului pentru exemplul dat: Se
iniializeaz Fol cu false, iar la pasul K = 1, se depune n stiv valoarea 1 i
Fol[1] devine true:
1
1
K
Sol
i
Fol
1
2
3
true false false
2
1
K
i
Fol
1
2
3
true true false
74
Tehnici de programare
Se trece la pasul K = 3, moment n care condiia K > P devine
adevrat, deci afim coninuturile stivei: 1 2.
Se revine la pasul K = 2, Fol[2] devine false, iar pe poziia 2 a stivei
se depune urmtoarea valoare, adic 3, iar Fol[3] devine true:
3
1
Sol
2
1
K
i
Fol
1
2
3
true false true
75
Capitolul 3
void aran(int K, int N, int P, bool Fol[],
int Sol[], ofstream &out)
{
if ( K > P )
{
for ( int i = 1; i <= P; ++i )
out << Sol[i] << ' ';
out << endl;
return;
}
for ( int i = 1; i <= N; ++i )
if ( !Fol[i] )
{
Sol[K] = i;
Fol[i] = true;
aran(K + 1, N, P, Fol, Sol, out);
Fol[i] = false;
}
}
int main()
{
int N, P, Sol[maxP];
bool Fol[maxN];
citire(N, P);
for ( int i = 1; i <= N; ++i )
Fol[i] = false;
ofstream out("aran.out");
aran(1, N, P, Fol, Sol, out);
out.close();
return 0;
}
c) Generarea combinrilor
Dndu-se dou numere naturale N i P (1 P N), dorim s
generm toate combinrile de N luate cte P ale mulimii numerelor
!
naturale. Combinri de N luate cte P se noteaz , iar =
! !
Tehnici de programare
Modul de rezolvare este similar cu cel al problemelor anterioare.
Mai mult, datorit faptului c ordinea elementelor nu are importan,
algoritmul se simplific mult. S presupunem c suntem la pasul K
(1 K P). Asta nseamn c avem depuse valori n Sol[1], Sol[2], ...,
Sol[K 1]. Considerm Sol[0] = 0. Deoarece ordinea elementelor nu
conteaz, putem depune numere pe poziia actual a stivei ncepnd de la
valoarea precedent, la care se adun unu, adic Sol[K 1] + 1. Astfel nu
mai avem nevoie de vectorul Fol, deoarece numerele din stiv vor fi
ntotdeauna ordonate cresctor i nu vor avea cum s se repete. Mai mult,
acest artificiu ne asigur i c nu vom genera mai multe combinri dect
este nevoie.
Algoritmul este cel mai rapid de pn acum, complexitatea sa fiind
O( ), datorit faptului c nu se va ncerca niciodata depunerea unei valori
invalide n stiv.
Pentru exemplul dat, algoritmul funcioneaz n felul urmtor: mai
nti se iniializeaz Sol[0] cu 0. La pasul K = 1, se depune mai nti
valoarea 1 n stiv:
1
Sol
1
K
77
Capitolul 3
#include <fstream>
using namespace std;
int main()
{
int N, P, Sol[maxP];
Sol[0] = 0;
ofstream out("comb.out");
comb(1, N, P, Sol, out);
out.close();
in.close();
}
return 0;
void comb(int K, int N, int P, int Sol[],
ofstream &out)
{
if ( K > P )
{
for ( int i = 1; i <= P; ++i )
out << Sol[i] << ' ';
out << endl;
return;
}
for ( int i = Sol[K - 1] + 1; i <= N; ++i )
{
Sol[K] = i;
comb(K + 1, N, P, Sol, out);
}
}
Exerciii:
a) Ct de rapid este generarea aranjamentelor i a combinrilor
pentru valori mari, dar apropiate, ale lui N i P?
b) Scriei un program care afieaz toate numerele naturale de trei
cifre care pot forma cu cifrele 1, 3, 4 i 5. Ce algoritm vei
folosi?
c) Modificai ultimii doi algoritmi astfel nct s afieze
aranjamentele, respectiv combinrile, n ordine inverslexicografic.
78
Tehnici de programare
d) Complexitatea algoritmilor prezentai crete de N (pentru
permutri) respectiv P (pentru aranjamente i combinri) ori
datorit faptului c afiarea necesit parcurgerea stivei.
Remediai acest lucru dac este posibil.
Capitolul 3
La pasul K = 1 se depune mai nti n Sol[1] valoarea Sol[0], adic
1:
1
1
K
Sol
N=4
Dup ce se depune valoarea 1 n stiv, se trece la urmtorul pas, dar
cu N = N 1, adic N = 3.
La pasul K = 2 se depune n stiv valoarea Sol[1], adic 1, i se
apeleaz funcia pentru N = 2. Se continu n acest mod pn cnd se ajunge
la N = 0 i K = 5, dup care se afieaz coninuturile stivei, care va arta n
felul urmtor:
5
1
4
1
3
1
2
1
1
K
Sol
N=0
Se revine la pasul K = 4 i se ncearc depunerea valorii 2 pe aceast
poziie a stivei, lucru imposibil, deoarece la acest pas N = 1 iar 1 2 < 0.
Se revine la pasul K = 3 i se ncearc depunerea valorii 2. La acest
pas N = 2, iar 2 2 = 0, deci se poate depune aceast valoare. Stiva va arta
n felul urmtor la acest pas:
2
3
1
2
1
1
K
Sol
N=2
Se trece napoi la pasul K = 4, cu N = N 2 = 2 2 = 0, deci am mai
gsit o soluie. Se procedeaz n acest mod pn ce am gsit toate soluiile,
adic pn ce pe prima poziie a stivei se depune chiar valoarea N.
80
Tehnici de programare
#include <fstream>
int main()
{
int N, P, Sol[maxN];
citire(N);
Sol[0] = 1;
ofstream out("part.out");
part(1, N, Sol, out);
out.close();
return 0;
}
Exerciii:
a) Modificai algoritmul astfel nct doar s numere partiiile
existente, nu s le i afieze.
b) Impunei condiia ca numerele folosite ntr-o partiie s fie
distincte.
c) Impunei condiia ca diferena n modul a doi termeni consecutivi
s fie cel puin 2
81
Capitolul 3
e) Concluzii
Am prezentat patru algoritmi reprezentativi pentru tehnica de
programare backtracking. Acetia pun n eviden cel mai bine structura
general a acestei metode i stau la baza majoritii problemelor care se pot
rezolva cu aceast metod.
Trebuie inut cont de faptul c algoritmii de tip backtracking sunt, de
cele mai multe ori, foarte ineficieni, metode precum divide et impera,
greedy, programare dinamic, tehnici aleatoare sau algoritmi genetici
fiind de multe ori de preferat atunci cnd o problem poate fi rezolvat
printr-una din aceste metode, chiar dac implementarea unui algoritm
backtracking este de multe ori mai uoar. Metoda backtracking se
folosete, de obicei, fie cnd o rezolvare eficient folosind alte metode
(polinomiale sau probabiliste) nu este cunoscut, fie cnd dorim s aflm
toate soluiile unei probleme, aa cum a fost cazul problemelor prezentate
anterior.
Aa cum am vzut, putem optimiza de multe ori un algoritm de tip
backtracking, eliminnd astfel multe soluii care s-ar dovedi la un moment
dat a fi invalide. Aceste optimizri depind foarte mult de natura problemei
pe care o rezolvm. De multe ori, un algoritm backtracking optimizat poate
fi cu mult mai eficient n practic dect unul neoptimizat.
82
Tehnici de programare
Un exemplu de problem la care nu este bine s folosim aceast
metod este determinarea minimului dintr-un ir de numere, aa cum vom
vedea.
Structura general a unui algoritm divide et impera este urmtoarea:
Dac problema curent este suficient de uoar pentru a fi
rezolvat, aceasta se rezolv.
Altfel, se mparte problema curent n dou sau mai multe
subprobleme care se rezolv recursiv.
La revenirea din recursivitate, se combin rezultatele tuturor
subproblemelor pentru a rezolva problema curent.
Aceti algoritmi au, de obicei, complexitatea O(Nlog N) datorit
faptului c fiecare problem se mparte de cele mai multe ori n dou
subprobleme de dimensiuni apropiate i pe fiecare nivel al arborelui de
recursivitate astfel obinut se efectueaz O(N) operaii.
Nu este neaprat s se rezolve toate subproblemele recursiv. Exist
algoritmi care pot elimina la fiecare pas una sau mai multe subprobleme,
complexitatea acestora fiind de obicei O(log N). Unul dintre aceti algoritmi
este chiar cutarea binar.
a) Determinarea minimului
Se dau N numere naturale. Se cere determinarea celui mai mic
numr dintre cele N.
Datele de intrare se citesc din fiierul minim.in: pe prima linie
numrul N, iar pe urmtoarea linie N numere naturale. Rezultatul se va afia
n fiierul minim.out.
Exemplu:
minim.in
7
9 7 8 3 4 2 11
minim.out
2
83
Capitolul 3
numerele din fiierul de intrare. Rezultatul care ne intereseaz va fi dat de
apelul Minim(A, 1, N). Funcia poate fi implementat n felul urmtor:
Dac st = dr returneaz A[st]
Altfel, fie m = (st + dr) / 2
Fie min_st = Minim(A, st, m) i min_dr = Minim(A, m+1, dr)
Returneaz minimul dintre min_st i min_dr
Algoritmul este similar cu modul de funcionare al algoritmului de
sortare prin interclasare, aa c nu vom insista prea mult asupra sa. Acesta
are scop pur didactic, n practic algoritmul clasic fiind mai eficient i mai
simplu de scris.
#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 Minim(int A[], int st, int dr)
{
if ( st == dr )
return A[st];
int m = (st + dr) / 2;
int min_st = Minim(A, st, m);
int min_dr = Minim(A, m + 1, dr);
return min_st<min_dr ? min_st : min_dr;
}
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
Exerciiu: desenai arborele de recursivitate al algoritmului pentru
exemplul dat.
b) Cutarea binar
Se dau N numere naturale ordonate cresctor i M ntrebri de
forma numrul Xi se gsete sau nu printre cele N numere? la care trebuie
s se rspund ct mai eficient.
Datele de intrare se citesc din fiierul cbinara.in: pe prima linie
numerele N i M separate prin spaiu, pe urmtoarea linie cele N numere
naturale separate prin spaiu, iar pe urmtoarele M linii cele M ntrebri. Pe
linia i a fiierului de ieire cbinara.out vei afia DA sau NU, n funcie de
rspunsul la ntrebarea respectiv.
Exemplu:
cbinara.in
95
4 6 8 9 12 15 15 16 21
4
5
9
21
15
cbinara.out
DA
NU
DA
DA
DA
85
Capitolul 3
Se poate observa foarte uor c am ales o abordare nerecursiv.
Algoritmul poate fi implementat i recursiv, dar acest lucru nu are niciun
scop practic.
Modul de funcionare al cutrii binare este foarte simplu: la fiecare
pas, se fixeaz mijlocul subsecvenei [st, dr] curente.
Dac valoarea cutat este egal cu elementul din mijlocul secvenei,
se returneaz true. n caz contrar, dac valoarea cutat este mai mic dect
elementul din mijloc, datorit faptului c numerele sunt ordonate cresctor,
este clar c orice element de dup elementul din mijloc va fi i el mai mare
dect valoarea cutat, deci putem elimina toate aceste elemente, adic
putem atribui lui dr valoarea m.
Dac valoarea cutat este mai mare dect elementul din mijloc, se
procedeaz similar: este clar c toate elementele cu indici mai mici dect
mijlocul secvenei sunt mai mici i ele dect valoarea cutat, deci putem
face atribuire st = m + 1.
La sfrit, cnd st == dr, verificm dac A[st] este elementul cutat.
Privii modul de funcionare al algoritmului pentru primele dou
ntrebri din exemplul dat. Am marcat cu rou elementele care sigur nu pot
conine valoarea cutat.
Pentru prima ntrebare, val = 4, st = 1, dr = 9.
Fie m = (st+dr)/2 = 5.
st
m
dr
i 1 2 3 4 5 6 7 8 9
A 4 6 8 9 12 15 15 16 21
A[m] = 12 > val = 4, deci
m, adic putem seta dr = m:
st
m
i 1 2 3 4
A 4 6 8 9
Tehnici de programare
A[m] = val, deci algoritmul se oprete. S-au efectuat patru pai.
Pentru a doua ntrebare, val == 5, st = 1, dr = 9. m = (st+dr)/2 = 5.
st
m
dr
i 1 2 3 4 5 6 7 8 9
A 4 6 8 9 12 15 15 16 21
A[m] = 12 > val = 5, deci dr = m:
st
m
dr
i 1 2 3 4 5 6 7 8 9
A 4 6 8 9 12 15 15 16 21
A[m] = 8 > val = 5, deci dr = m:
st m dr
i 1 2 3 4 5 6 7 8 9
A 4 6 8 9 12 15 15 16 21
A[m] = 6 > val = 5, deci dr = m:
st m dr
i
1
2 3 4 5 6 7 8 9
4
6 8 9 12 15 15 16 21
A
A[m] = 4 < val = 5, deci st = m + 1:
st dr
i 1
2
3 4 5 6 7 8 9
6
8 9 12 15 15 16 21
A 4
Deja st = dr, iar A[st] != val, deci valoarea cutat nu se regsete n
ir.
Complexitatea algoritmului este uor de dedus: O(log N), deoarece
la fiecare pas reducem spaiul de cutare la jumtate.
#include <fstream>
using namespace std;
const int maxN = 1001;
87
Capitolul 3
bool cbinara(int A[], int st, int dr, int val)
{
while ( st < dr )
{
int m = (st + dr) / 2;
if ( val == A[m] )
else if ( val < A[m] )
else
return true;
dr = m;
st = m + 1;
}
return A[st] == val;
}
Tehnici de programare
Exerciii:
a) Modificai algoritmul astfel nct s returneze cea mai mare
poziie a unui element cutat.
b) Modificai algoritmul astfel nct s lucreze cu un ir sortat
descresctor.
c) Modificai algoritmul de sortare prin inserie astfel nct s
foloseasc algoritmul de cutare binar pentru determinarea
poziiei n care trebuie inserat un element.
a) Concluzii
Algoritmii divide et impera pot fi foarte folositori n gsirea unor
soluii optime la o problem. Dac acest lucru nu este prea dificil,
implementarea acestor algoritmi trebuie fcut nerecursiv, deoarece
apelurile recursive pot ncetini n practic performana algoritmilor.
Aceti algoritmi au aplicabilitate atunci cnd rezolvarea unei
probleme identice, dar de dimensiuni mai mici, poate contribui la rezolvarea
unei probleme mai mari.
3.4. Greedy
Tehnica de programare greedy (n traducere liber: tehnica
lcomiei) se bazeaz pe selectarea succesiv a unor optime locale pentru a
determina ntr-un final optimul global. Tehnica se folosete, de obicei, n
cazul problemelor n care se cere determinarea unui minim sau maxim
respectnd anumite constrngeri dependente de problem.
Datorit faptului c algoritmii greedy iau la fiecare pas decizia cea
mai favorabil existent la acel pas, fr a lua n considerare cum ar putea
afecta aceast decizie ntreg traseul algoritmului, exist posibilitatea ca
aceti algoritmi s nu determine ntotdeauna un optim, ci doar o soluie care
respect constrngerile problemei, dar care nici mcr nu este neaprat
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
gsirea 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, avnd o complexitate polinomial, adic O(Nk), unde N reprezint
dimensiunea datelor de intrare, iar k este o constant natural.
89
Capitolul 3
n caz c demonstrarea corectitudinii unei metode nu este posibil,
sau n caz c putem gsi un contraexemplu pentru metoda aleas, este
recomandat s folosim alte metode de rezolvare a problemei.
Forma general a algoritmilor greedy este urmtoarea:
Fie X1, X2, , XN datele de intrare ale problemei i S un optim
care trebuie gsit, iniializat cu o valoare oarecare, sau cu
mulimea vid n caz c se cere gsirea unei mulimi.
Pentru fiecare i de la 1 la N execut
o Dac la pasul i putem lua o decizie care mbuntete
optimul S, vom lua aceast decizie.
n continuare vom prezenta dou probleme abordabile prin metoda
greedy: problema spectacolelor i problema plii unei sume. Cea din urm
va evidenia i cazul n care metoda greedy nu furnizeaz ntotdeauna
rspunsul optim.
a) Problema spectacolelor
Patronul unei sli de spectacole dorete s organizeze, ntr-un
interval de timp oarecare, un numr ct mai mare de spectacole. tiind c
exist N artiti interesai s susin un spectacol i c fiecare artist i poate s
suin spectacolul doar n intervalul de timp [ai, bi], determinai numrul
maxim de spectacole care pot fi organizate astfel nct intervalele de
desfurare a oricror dou spectacole s nu se suprapun.
Datele de intrare se citesc din fiierul spectacole.in: pe prima linie
numrul natural N, iar pe urmtoarele N linii se afl perechi de numere [ai,
bi] avnd semnificaia din enun. n fiierul spectacole.out vei afia
numrul maxim de spectacole care pot fi programate respectnd condiiile
problemei.
Exemplu:
spectacole.in spectacole.out
5
3
14
47
35
89
67
90
Tehnici de programare
Explicaie: se vor organiza spectacolele cu numerele de ordine 1, 4
i 5.
Vom folosi urmtorul algoritm greedy, a crui corectitudine va fi i
demonstrat:
Se sorteaz spectacolele date dup momentul terminrii lor. n
continuare se va lucra pe irul sortat al intervalelor.
Se programeaz primul spectacol.
Pentru fiecare i de la 2 la N execut
o Dac momentul de nceput al spectacolului i este mai
mare dect momentul terminrii ultimului spectacol
programat atunci
Programeaz spectacolul i.
Afieaz numrul spectacolelor programate.
n continuare, vom demonstra faptul c acest algoritm gsete
ntotdeauna numrul maxim de spectacole care pot avea loc.
Fie X1, X2, ..., XK spectacolele programate de ctre algoritmul de
mai sus i Y1 , Y2, ..., YL spectacolele programate de ctre un algoritm
optim. Dac putem demonstra c L = K, atunci algoritmul prezentat este
correct.
Vom presupune prin absurd c L > K.
Presupunem ca Y1 X1. n acest caz, este posibil s-l nlocuim pe
Y1 cu X1 , deoarece X1 este spectacolul care se termin primul, deci nu are
cum s figureze pe alt poziie n cadrul soluiei optime. Dac Y1 = X 1 ,
atunci acest pas nu mai este necesar. Soluia X1, Y2 , ..., YL rmne aadar n
continuare optim.
Fie 1 < P K primul indice pentru care YP XP. Soluia X1, X2 ,
..., XP 1, YP, , YL este optim dintr-un raionament asemntor cu cel de
mai sus.
XP nu face parte din soluia optim. Dac XP ar face parte din soluia
optim, ar trebui s se afle cel puin pe poziia P + 1, ceea ce nseamn c ar
ncepe dup YP, contrazicndu-se astfel modul de funcionare al
algoritmului.
Astfel, la un moment dat se va ajunge la o soluie de forma X1 , X2,
..., XK, ..., YL. Asta ar nsemna c dup XK mai este posibil s selectm
91
Capitolul 3
spectacole, dar conform algoritmului, acest lucru nu este posibil, deci am
ajuns la o contradicie. Rezult ca presupunerea fcut este fals i L = K.
Privii modul de funcionare al algoritmului pentru exemplul dat.
Mai nti se sorteaz spectacolele dup momentul de terminare al acestora,
rezultnd urmtorul ir de intervale:
14
35
47
67
89
Se selecteaz primul spectacol, adic cel cu intervalul [1, 4].
Se trece la al doilea spectacol, dar momentul de nceput al acestuia
(3) nu este mai mare dect momentul terminrii ultimului spectacol selectat
(4), deci nu putem selecta acest spectacol.
Se trece la al treilea spectacol, dar nici acesta nu poate fi selectat din
acelai motiv.
Se trece la al patrulea spectacol, care poate fi selectat, deoarece
6 > 4.
Se trece la ultimul spectacol, care iari poate fi selectat, deoarece
8 > 7.
Astfel s-au selectat trei spectacole, numr care se afieaz.
Complexitatea algoritmului este O(Nlog N) datorit sortrii. Dac datele de
intrare se dau sortate, complexitatea devine O(N). Memoria folosit este
O(N) dac datele de intrare nu se dau sortate i O(1) n caz contrar.
Pentru aceast implementare, vom folosi ca algoritm de sortare
funcia pus la dispoziie de ctre limbajul C++ sort, funcie prezentat pe
scurt i n cadrul capitolului despre algoritmi de sortare.
n cazul acestei probleme vom avea nevoie s sortm un tip de date
definit de ctre utilizator, deoarece vom folosi o structur pentru a reine
momentele de nceput i de sfrit ale fiecrui spectacol. Pentru a putea
sorta structuri folosind funcia sort, trebuie s avem o funcie de comparare
care primete doi parametri (de tipul datelor pe care vrem s le sortm) i
returneaz true dac primul parametru se consider mai mic dect al doilea,
i false n caz contrar. Funcia aceasta se transmite ca i parametru funciei
sort.
Este foarte important ca aceast funcie de comparare s foloseasc
parametri de tip referin (de obicei referin constant, dar acest lucru este
92
Tehnici de programare
mai puin important) atunci cnd se lucreaz cu structuri, mai ales dac
acestea sunt foarte mari, deoarece dac parametrii nu sunt de tip referin,
fiecare apel al funciei va lucra cu o copie a obiectelor transmise ca
argument funciei, iar aceast copiere poate afecta drastic performana unui
program.
#include <fstream>
#include <algorithm>
using namespace std;
struct spectacol
{
int inc, sf;
};
bool cmp(const spectacol &x,
const spectacol &y)
{
return x.sf < y.sf;
}
void citire(int &N, spectacol A[])
{
ifstream in("spectacole.in");
in >> N;
ofstream out("spectacole.out");
out << nr << endl;
out.close();
}
int main()
{
int N;
spectacol A[maxN];
citire(N, A);
rezolvare(N, A);
sort(A + 1, A + N + 1, cmp);
return 0;
}
in.close();
}
Exerciii:
a) Modificai algoritmul astfel nct s afieze indicii iniiali ai
spectacolelor selectate.
b) Implementai un algoritm de sortare pentru sortarea datelor.
c) Dac fiecare spectacol ar avea asociat un cost i am dori s
organizm un numr maxim de spectacole, dar care s coste ct
93
Capitolul 3
mai puin, ar mai funciona strategia greedy? Dac da, ce ar
trebui modificat?
d) Se d un interval [P, Q] i se cere programarea unui numr
minim de spectacole care s acopere intervalul [P, Q]. Elaborai
un algoritm greedy care rezolv problema.
Tehnici de programare
moned de valoare 9, o moned de valoare 2 i o moned de valoare 1, adic
trei monede n total. Soluia optim este ns folosirea a dou monede de
valoare 6. Acesta este un contraexemplu ce demonstreaz c strategia
greedy nu funcioneaz dect pentru anumite tipuri de monede.
Cazul general al problemei se rezolv folosind fie backtracking (dar
acest lucru este foarte ineficient), fie programare dinamic.
#include <fstream>
void rezolvare(int S)
{
int nr = 0;
for ( int i = 0; monede[i]; ++i )
while ( S - monede[i] >= 0 )
{
S -= monede[i];
++nr;
}
ofstream out("suma.out");
out << nr << endl;
out.close();
}
int main()
{
int S;
citire(S);
rezolvare(S);
return 0;
}
c) Concluzii
Algoritmii greedy sunt de obicei mai rapizi dect ali algoritmi, mai
ales dect metodele exhaustive cum este metoda backtracking. Un
dezavantaj al acestora este c programatorul trebuie s se bazeze foarte mult
pe intuiie i pe experien pentru a putea ajunge la un algoritm corect, iar
aparenta corectitudine a unui algoritm poate fi neltoare, aa cum am
artat la ultima problem.
Dac avei de ales ntre un algoritm greedy a crui corectitudine
poate fi demonstrat i un algoritm mai puin eficient, este clar c alegerea
trebuie fcut n favoarea algoritmului greedy. Dac nu se poate demonstra
corectitudinea algoritmului greedy ns, iar importan gsirii unui optim
pentru fiecare caz posibil este foarte mare, atunci este de preferat folosirea
altui algoritm, chiar dac este mai puin eficient.
95
Capitolul 3
O metod uzual de verificare a unui algoritm greedy, cnd nu
putem gsi o demonstraie i nici un contraexemplu, este implementarea
unui algoritm de tip backtracking care rezolv aceeai problem. Generai
aleator date de intrare i rezolvai-le att cu programul backtracking ct i cu
programul greedy. Dac nu apar diferene ntre rezultate pentru mult timp,
atunci probabil c metoda greedy este corect.
Tehnici de programare
Spre deosebire de tehnica greedy, programarea dinamic pstreaz
toate strile necesare lurii unor decizii care vor conduce sigur la gsirea
unui optim global. Programarea dinamic are aadar o aplicabilitate mai
larg dect tehnicile prezentate pn acum, rmnnd de cele mai multe ori
i o alternativ eficient.
n cele ce urmeaz vom prezenta cteva concepte i probleme
elementare rezolvabile folosind tehnici de programare dinamic: irul
Fibonacci, problema triunghiului, triunghiul lui Pascal i tehnica
memoizrii.
a) irul Fibonacci
irul Fibonacci, sau numerele Fibonacci sunt numerele generate
de funcia:
0
= 0
1
= 1
=
1 + ( 2) 2
Aa cum am vzut la capitolul despre recursivitate, aceast funcie
poate fi implementat recursiv ntr-un mod foarte uor. Practic doar se
traduce definiia matematic a funciei n C++. Timpul de execuie al unei
astfel de implementri este foarte mare, deoarece se vor efectua multe
apeluri recursive care vor rezolva aceeai subproblem. De exemplu, dac
am folosi o asemenea funcie pentru a calcula F(7), ar rezulta urmtorul
arbore de recursivitate (apelurile care s-au mai efectuat deja cel puin o dat
apar n rou):
Capitolul 3
Se poate vedea foarte uor c aceast metod efectueaz un numr
mult mai mare de operaii dect este necesar. Pentru a v convinge c
aceast metod este ineficient, folosii implementarea recursiv i apelai
F(100).
Ne propunem s scriem un program eficient care citete din fiierul
fibo.in un numr natural N i afieaz n fiierul fibo.out valoarea F(N).
Exemplu:
fibo.in fibo.out
6
8
Pentru a rezolva eficient problema, vom folosi metoda programrii
dinamice. Fie F un vector a cror elemente au urmtoarea semnificaie:
F[i] = F[i 2] + F[i 1], dac i > 1 i F[0] = 0, F[1] = 1. Putem construi
acest vector n timp O(N), iniializnd primele dou elemente i nsumnd
apoi, pentru fiecare poziie i, valorile de pe poziiile i 1 i i 2. Pentru
N = 6 se va construi urmtorul vector:
i 0 1 2 3 4 5 6
F 0 1 1 2 3 5 8
Rspunsul se va afla n F[N], n cazul acesta n F[6].
Problema prezint substructur optim deoarece, dac avem
calculate corect valorile F[N 2] i F[N 1], putem calcula F[N]. Problema
prezint i subprobleme suprapuse deoarece, aa cum am vzut, o
abordare recursiv va rezolva aceeai subproblem de mai multe ori. Acest
lucru se ntmpl din cauza faptului c ntotdeauna doi termeni consecutivi
ai irului Fibonacci vor avea un termen precedent n comun. De exemplu,
F[6] = F[5] + F[4], iar F[5] = F[4] + F[3].
Se folosete metoda nainte, deoarece valorile funciei se calculeaz
treptat, de la cea mai mic la cea cerut.
#include <fstream>
using namespace std;
const int maxN = 1001;
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;
}
b) Problema triunghiului
Se d un triunghi de numere naturale de latur N pe care se joac
urmtorul joc: se alege numrul din colul de sus al triunghiului. Dup
aceea, la fiecare pas, ne putem deplasa fie cu o poziie n jos, fie cu o poziie
n jos i una ctre dreapta fa de poziia ultimului numr ales. Numrul pe
care se face deplasarea este ales. Acest lucru se continu pn cnd se
ajunge pe ultima linie a triunghiului. Ne intereseaz un traseu pentru care
suma numerelor alese este maxim.
99
Capitolul 3
Datele de intrare se citesc din fiierul triunghi.in: pe prima linie N,
iar pe fiecare linie i a urmtoarelor linii, cte i numere naturale,
reprezentnd o linie a triunghiului. n fiierul triunghi.out se va afia, pe
prima linie, suma maxim gsit. Pe a doua linie, se vor afia n ordine
numerele alese, separate printr-un spaiu.
Exemplu:
triunghi.in triunghi.out
5
50
4 5 2 20 19
4
35
142
7 5 6 20
1 1 7 19 9
Vom folosi o matrice A care va reine numerele date, adic
A[i][j] = numrul de pe linia i i coloana j a triunghiului de numere. Din
poziia A[i][j] ne putem deplasa pe poziiile A[i + 1][j] i A[i + 1][j + 1],
atta timp ct acestea nu depesc limitele matricei. Problema se reduce la a
gsi un drum n matricea A a crui elemente s aib suma maxim, folosind
numai aceste dou tipuri de deplasri.
La prima vedere, problema pare abordabil folosind metoda greedy.
Vom ncepe prin a iniializa S cu primul numr, dup care vom selecta
numrul de valoare maxim dintre numerele A[i + 1][j] i A[i + 1][j + 1] i
ne vom deplasa pe poziia numrului de valoare maxim. Aceast soluie nu
funcioneaz ns ntotdeauna, deoarece nu inem cont de faptul c o alegere
neoptim la pasul curent ne poate duce n final la soluia optim, aa cum
este cazul n exemplul dat.
Problema poate fi rezolvat folosind metoda programrii dinamice.
Vom calcula o matrice B, unde B[i][j] = suma maxim a unui traseu care
se termin cu numrul A[i][j]. Relaiile de recuren sunt uor de dedus:
pentru a ajunge pe A[i][j], trebuie s fim deja fie pe A[i 1][j], fie pe
A[i 1][j 1]. Dac tim cte un traseu optim care se termin cu fiecare
dintre aceste dou numere, putem determina un traseu optim pentru a ajunge
pe A[i][j]: fie adugm numrul A[i][j] la traseul optim care se termin cu
A[i 1][j], fie adugm A[i][j] la traseul optim care se termin cu
A[i 1][j 1].
Acest lucru poate fi scris matematic n felul urmtor:
100
Tehnici de programare
=
[]
max 1 , 1 1
+ []
= = 1
inf 4 inf
inf 7
9
inf
inf 8
13
11 inf
inf 15 18
19
31 inf
inf 16 19
26
50
40
101
Capitolul 3
Se verific B[3 1][3 1] = B[3][3] A[3][3], deci noul X devine
9. Se reine n stiv valoarea A[2][2] = 5.
Se verific B[2 1][2 1] = B[2][2] A[2][2], deci noul X devine
4. Se reine n stiv valoarea A[1][1] = 4.
Am ajuns la elementul de pe linia 1 i coloana 1, care se adaug n
stiv i algoritmul se ncheie. Mai trebuie doar afiat stiva.
n practic, reconstituirea soluiei se poate face i recursiv. Deoarece
numrul apelurilor recursive este mic, aceast abordare este preferabil de
multe ori, datorit simplitii implementrii.
Timpul de execuiei al algoritmului este O(N2), deoarece se parcurg
(+1)
toate numerele date, adic 1 + 2 + 3 + + =
numere. Memoria
2
2
folosit este tot O(N ) deoarece folosim dou matrici ptratice de
dimensiune N.
Rezolvarea folosete metoda nainte. Problema se poate rezolva i
folosind metoda napoi, dar n acest caz timpul de execuie este exponenial,
datorit subproblemelor suprapuse.
#include <fstream>
using namespace std;
const int maxN = 101;
const int inf = -2000000000;
void citire(int &N,
int A[maxN][maxN])
{
ifstream in("triunghi.in");
in >> N;
for ( int i = 1; i <= N; ++i )
for ( int j = 1; j <= i; ++j )
in >> A[i][j];
in.close();
}
102
Tehnici de programare
void init(int N,
int B[maxN][maxN])
{
for ( int i = 1; i <= N; ++i )
B[i][0] = inf;
for ( int i = 1; i < N; ++i )
B[i][i + 1] = inf;
}
int main()
{
int N, A[maxN][maxN];
int B[maxN][maxN];
citire(N, A); init(N, B);
rez(N, A, B);
void rez(int N,
int A[maxN][maxN],
int B[maxN][maxN])
{
B[1][1] = A[1][1];
for ( int i = 2; i <= N; ++i )
for ( int j = 1; j <= i; ++j )
B[i][j] = max(B[i - 1][j],
B[i - 1][j - 1]) + A[i][j];
}
void reconst(int Xi, int Xj,
int A[maxN][maxN],
int B[maxN][maxN],
ofstream &out)
{
if ( Xi == 1 && Xj == 1 )
{
out << A[1][1] << ' ';
return;
}
ofstream out("triunghi.out");
int Xj = 1;
for ( int i = 2; i <= N; ++i )
if ( B[N][i] > B[N][Xj] )
Xj = i;
out << B[N][Xj] << endl;
reconst(N, Xj, A, B, out);
out.close();
return 0;
}
103
Capitolul 3
pascal.out
3
35
45
104
Tehnici de programare
De aici rezult i formula de recuren a combinrilor:
1
= 1
+ 1
Implementare
#include <fstream>
int main()
{
int N, K, A[maxN][maxN];
preprocesare(A);
ifstream in("pascal.in");
ofstream out("pascal.out");
while ( in >> N >> K )
out << A[N][K] << endl;
A[0][0] = 1;
for ( int i = 1; i < maxN; ++i )
for ( int j = 1; j <= i; ++j )
A[i][j]=A[i - 1][j] + A[i - 1][j - 1];
}
in.close();
out.close();
return 0;
}
Exerciii:
a) Explicai de ce folosirea formulei combinrilor este o metod
mai puin eficient.
b) Scriei o funcie recursiv care folosete recurena combinrilor
pentru a calcula fiecare rspuns. Este aceast abordare eficient?
c) Dac am calcula mai multe linii ale triunghiului lui Pascal, care
este prima linie care conine rezultate greite? De ce se ntmpl
acest lucru?
d) Tehnica memoizrii
Am prezentat pn acum doi algoritmi care folosesc metoda nainte
pentru rezolvarea problemelor. Aa cum am menionat la nceputul
capitolului, metoda napoi poate fi mbuntit folosind tehnica
105
Capitolul 3
memoizrii. Aceast tehnic presupune folosirea unui tabel n care se
memoreaz rezultatele fiecrui apel recursiv. La efectuarea unui apel
recursiv, vom verifica mai nti dac rezultatul acelui apel se afl n tabel:
dac da, atunci returnm pur i simplu acest rezultat, iar dac nu, continum
n modul obinuit, iar la sfrit memorm rezultatul funciei n acest tabel.
Structura general a algoritmilor recursivi care folosesc memoizare
este urmtoarea: fie F(X1 , X2, ..., XN) o funcie recursiv pentru care se
aplic memoizare. Aceast funcie poate fi implementat n felul urmtor:
Dac avem deja calculat rezultatul pentru F(X1, X2, ..., XN), se
returneaz acest rezultat.
Altfel, fie R rezultatul calculat de apelul F(X 1, X2, ..., XN), de
obicei pe baza unor apeluri recursive. Se reine R ca rspunsul
pentru apelul (starea) curent i se returneaz acest rspuns.
De exemplu, putem implementa o funcie recursiv care folosete
memoizare pentru a calcula eficient al N-lea numr fiboacci n felul
urmtor:
#include <fstream>
using namespace std;
const int maxN = 1001;
int main()
{
int N, memo[maxN];
for ( int i = 0; i < maxN; ++i )
memo[i] = -1;
ifstream in("fibo.in");
in >> N;
in.close();
ofstream out("fibo.out");
out << fibo(N, memo) << endl;
out.close();
return 0;
}
Tehnici de programare
Arborele apelurilor recursive pentru apelul F(6), unde funcia F
returneaz al N-lea numr Fibonacci poate fi desenat n felul urmtor.
Apelurile pentru care se returneaz direct rezultatul calculat deja apar n
albastru.
107
Capitolul 3
s fim ateni la mai multe cazuri particulare. Funcia va trebui apelat pentru
fiecare element al ultimei linii din triunghiul dat.
Exerciii:
a) Cum putem reconstitui soluia la problema triunghiului dac
folosim funcia recursiv anterioar?
b) Dac ne intereseaz drumul minim numai pn la un singur
element de pe ultima linie a triunghiul, care abordare este mai
eficient?
c) Scriei o funcie recursiv ce folosete memoizare pentru calculul
combinrilor.
d) Explicai de ce memoizarea nu mbuntete funcia factorial
sau funcia de rezolvare a problemei turnurilor din Hanoi.
e) Comparai o funcie recursiv memoizat cu o funcie iterativ
echivalent acesteia. Care este mai eficient?
e) Concluzii
Programarea dinamic este o tehnic de programare foarte
folositoare pentru rezolvarea problemelor de numrare sau de gsire a
optimelor. Este o alternativ mai rapid dect metoda backtracking i mai
corect dect 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 implementri corecte merit fcut,
datorit eficienei acestor algoritmi.
Atunci cnd implementarea iterativ a unei formule de recuren ar fi
prea dificil, se poate folosi tehnica memoizrii pentru a pstra eficiena
algoritmului i a simplifica implementarea.
Corectitudinea unui algoritm de programare dinamic poate fi
verificat cu un algoritm backtracking, iar la rndul su, programarea
dinamic poate fi folosit pentru a testa dac o strategie greedy este sau nu
corect.
Programarea dinamic este o tehnic foarte des folosit n
proiectarea algoritmilor, avnd aplicaii n toate ramurile iinelor exacte:
teoria grafurilor, teoria numerelor, biologie computaional, combinatoric
i altele.
108
Algoritmi matematici
4. Algoritmi
matematici
Acest capitol prezint algoritmii care au la baz noiuni elementare
de matematic. Aceti algoritmi sunt folosii, de cele mai multe, ori pentru
rezolvarea unor probleme strict matematice, cum ar fi rezolvarea unor
ecuaii sau sisteme de ecuaii, determinarea numerelor cu anumite
proprieti, rezolvarea unor probleme de geometrie sau calculul unor
formule complexe cu ajutorul calculatorului.
Algoritmii prezentai se axeaz mai mult pe teoria numerelor, acetia
fiind cei mai des ntlnii n domeniul informaticii i totodat cei mai
studiai.
109
Capitolul 4
CUPRINS
4.1. Noiuni 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. Exponenierea logaritmic .................................................................... 136
4.7. Inveri modulari, funcia totenial ...................................................... 143
4.8. Teorema chinez a resturilor ................................................................ 145
4.9. Principiul includerii i al excluderii ........................................................ 150
4.10. Formule i tehnici folositoare ............................................................. 151
4.11. Operaii cu numere mari ..................................................................... 154
110
Algoritmi matematici
2. =
3. =
4. Dac 0
5. Existena inversului fa de nmulire: nu exist ntotdeauna
1 astfel nct 1 1 .
Acest 1 exist dac i numai dac i sunt coprime.
Dou numere se consider coprime dac nu au niciun factor prim
n comun (cel mai mare divizor comun al lor este 1).
111
Capitolul 4
De exemplu, inversul modular al lui 4 (modulo 9) este 41 = 7,
deoarece 47 1 (mod 9), dar inversul lui 6 (modulo 9) este
inexistent, deoarece 6 i 9 nu sunt coprime.
6. O alt metod de a calcula , care se poate deduce din
formula uzual de calcul, este s-l scdem pe din atta timp
ct este mai mare sau egal cu .
Exerciiu: scriei un program care citete un ir de numere ntregi i
calculeaz suma lor modulo un alt numr citit. n ce caz v poate ajuta
proprietatea 2?
n cele ce urmeaz vom prezenta algoritmi care au la baz aceste
formule i proprieti. Calculul inversului modular este o problem
netrivial pentru care exist mai muli algoritmi, a cror eficien i
aplicabilitate difer.
Algoritmi matematici
Dei acest algoritm este uor de implementat i intuitiv, exist cazuri
n care este chiar mai ineficient dect algoritmul naiv, care parcurge
numerele de la min(X, Y) n jos i se oprete cnd gsete un numr care
divide att pe X ct i pe Y. Un astfel de caz este X = 1 000 000 i Y = 1. La
fiecare pas, vom scdea din X valoarea 1, efectund n total un milion de
operaii! Complexitatea n cel mai ru caz este aadar O(max(X, Y)). S
vedem cum putem mbunti algoritmul.
Aa cum am precizat n seciunea anterioar, putem calcula X mod
Y folosind scderi repetate ale lui Y din X. Aceste scderi se efectueaz i
n cazul algoritmului prin scderi repetate, fiind chiar cauza ineficienei
acestuia pe anumite cazuri. Vom elimina aceste scderi folosind operaia
modulo. Noul algoritm devine acum:
Ct timp Y diferit de 0 execut:
o Fie r = X mod Y
o X=Y
o Y=r
Returneaz X
Demonstraia acestui algoritm este foarte similar cu demonstraia
algoritmului iniial, deoarece ideea este aceeai, doar c efectum mai multe
scderi o dat folosind operaia modulo. Numrul de pai efectuai de acest
algoritm nu este niciodat mai mare dect de cinci ori numrul de cifre al
numrului mai mic (acest lucru a fost demonstrat de ctre matematicianul
francez Gabriel Lam n 1844), deci putem considera c este
O(log
(min(X, Y))). Pentru exemplul de mai sus n care X = 1 000 000 i Y = 1,
se efectueaz un singur pas (adic o singur iteraie a ciclului ct timp).
Algoritmul poate fi scris i recursiv n felul urmtor: fie
cmmdc(X, Y) o funcie care returneaz cel mai mare divizor comun al celor
dou numere avute ca parametri. Aceast funcie poate fi implementat
astfel:
Dac Y == 0 returneaz X
Altfel, apeleaz recursiv cmmdc(Y, X mod Y)
Implementarea recursiv are avantajul de a fi foarte compact i uor
de inut minte. Mai mult, aceast implementare este folositoare la extinderea
algoritmului, aa cum vom vedea n seciunea urmtoare.
Prezentm doar funciile relevante. Fiecare funcie returneaz (direct
sau printr-un parametru de tip referin) c.m.m.d.c al numerelor date ca
parametru.
113
Capitolul 4
int euclid_scaderi(int X, int Y)
{
while ( X != Y )
if ( X > Y )
X -= Y;
else
Y -= X;
return X;
}
int euclid_rec(int X, int Y)
{
if ( !Y )
return X;
return euclid_rec(Y, X % Y);
}
o Returneaz (b, a b )
114
Algoritmi matematici
Dac X mod Y = 0, nseamn c d = Y, iar din valorile returnate
de algoritm n acest caz observm c ecuaia devine
0X + 1Y = d, adic d = Y, ceea ce este corect.
n caz contrar, datorit apelului recursiv efectuat, tim c
aY + b(X mod Y) = d. Acest lucru a fost demonstrat anterior.
o Folosind formula uzual de calcul, putem scrie ecuaia de
mai sus n felul urmtor:
aY + b(X Y ) = d.
aY + bX - bY = d.
o Dm factor comun pe Y:
bX + (a b ) Y = d.
(b, a b ).
Capitolul 4
iar 460 1 (mod 51). Aa cum vom vedea n seciunile ce urmeaz, acest
lucru are diverse aplicaii i n ali algoritmi.
void euclid_extins(int X, int Y, int &a, int &b)
{
if ( X % Y == 0 )
{
a = 0;
b = 1;
return ;
}
int ta, tb;
euclid_extins(Y, X % Y, ta, tb);
a = tb;
b = ta - tb*(X / Y);
}
Algoritmi matematici
Numerele prime au numeroase aplicaii n criptografie i n teoria
numerelor n general. Exist diverse metode de a determina dac un numr
este sau nu prim (aceste verificri poart numele de teste de primalitate) i
de a determina toate numerele prime pn la o anumit limit. Algoritmii de
testare a primalitii se mpart n dou mari categorii: algoritmi
determiniti, care determin cu o probabilitate de 100% dac un numr este
sau nu prim i algoritmi probabiliti, care determin cu o probabilitate mai
mic de 100% dac un numr este sau nu prim. De obicei, algoritmii
probabiliti sunt cu mult mai eficieni, fiind aplicabili numerelor cu sute de
cifre, dar exist posibilitatea ca un numr gsit ca fiind prim de un astfel de
algoritmi s nu fie cu adevrat prim.
O proprietate important a numerelor prime este c orice numr are
un invers multiplicativ modulo orice numr prim. Acest lucru se datoreaz
faptului c orice numr este coprim cu un numr prim.
Alt rezultat important care implic numerele prime este conjectura
lui Goldbach, care afirm c orice numr natural par mai mare ca 2 poate fi
scris ca sum de dou numere prime. De exemplu, 4 = 2 + 2, 6 = 3 + 3,
8 = 3 + 5, 10 = 5 + 5, 12 = 5 + 7...
Exist foarte multe clasificri i rezultate teoretice i practice n
domeniul numerelor prime. De exemplu, la momentul scrierii acestei cri,
cel mai mare numr prim cunoscut are aproape 13 milioane de cifre. Acesta
e un numr prim de tip Mersenne (numerele prime de tip Mersenne sunt
acele numere prime care pot fi scrise ca o putere a lui 2 minus 1) i este egal
cu 243 112 609 1.
O notaie important care va fi folosit pe parcursul acestei seciuni
este funcia , unde (N) reprezint cte numere prime cel mult egale cu N
exist. O aproximaie pentru aceast funcie este urmtoarea:
ln
Capitolul 4
a) Metode clasice
Prima metod la care ne gndim pentru a determina dac un numr
N
N este sau nu prim este s parcurgem toate numerele de la 2 pn la
i s
2
verificm dac numrul N se divide la vreunul dintre aceste numere. Dac
da, atunci N nu este prim, iar dac nu, atunci N este prim. Astfel, o funcie
e_prim1(N) poate fi implementat n felul urmtor:
N
Pentru fiecare i de la 2 la
execut:
2
o Dac N mod i == 0 returneaz false
Returneaz true (dac nu s-a returnat false n ciclul de mai sus,
numrul sigur este prim).
Complexitatea acestei metode este O(N) pe cel mai ru caz. Dac ar
fi s folosim aceast funcie pentru a determina toate numerele prime pn
la N am obine complexitatea O(N2), ceea ce ar fi indezirabil pentru N mai
mare ca ~1 000.
Putem obine un algoritm mai eficient observnd c pentru orice
numr natural N nu are rost s verificm dac este divizibil cu numere mai
mari dect N. Acest lucru este uor de demonstrat: dac X N este un
divizor al lui N, atunci putem opri algoritmul deoarece N nu este prim. n
N
caz contrar, nici
N nu poate fi un divizor al lui lui N. Aadar, este de
X
ajuns s verificm doar numerele pn la radicalul numrului a crui
primalitate ne intereseaz.
ntr-un limbaj de programare se obinuiete s se foloseasc
urmtorul algoritm (e_prim2(N)), deoarece nu necesit folosirea unei
funcii de aflare a radicalului:
Pentru fiecare i de la 2 pn cnd i2 este mai mare ca N execut:
o Dac N mod i == 0 returneaz false
Returneaz true
Complexitatea acestui algoritm este mult mai bun: doar O( ).
Dei din punct de vedere asimptotic aceast limit este greu de mbuntit
pentru un algoritm determinist, mai putem aduce o mbuntire practic
substanial.
Se poate observa c nu are rost s verificm dac un numr este
divizibil cu numere pare mai mari dect 2, deoarece dac este divizibil cu 2
atunci nu este prim (dect dac numrul verificat este chiar 2), iar dac nu
este divizibil cu 2 atunci nu va fi divizibil cu niciun multiplu al lui 2.
118
Algoritmi matematici
Noul algoritm, e_prim3(N), devine acum:
Dac N == 2 returneaz true
Dac N mod 2 == 0 returneaz false
Pentru fiecare i de la 3 pn cnd i2 este mai mare ca N execut
i incrementeaz-l pe i cu 2:
o Dac N mod i == 0 returneaz false
Returneaz true
Acest algoritm este de obicei suficient de rapid pentru a verifica
primalitatea oricrui numr reprezentabil pe tipurile de date existente n
C++ i pentru a genera toate numerele prime pn la o anumit limit (sau
pentru a calcula funcia ).
Funciile prezentate returneaz true dac numrul transmis ca
parametru este prim i false n caz contrar. Funcia pi returneaz cte
numere prime exist mai mici ca parametrul su, folosind toate optimizrile
menionate.
bool e_prim1(int N)
{
for ( int i = 2; i <= N / 2; ++i )
if ( N % i == 0 )
return false;
bool e_prim2(int N)
{
for ( int i = 2; i*i <= N; ++i )
if ( N % i == 0 )
return false;
return true;
return true;
bool e_prim3(int N)
{
if ( N == 2 )
return true;
int pi(int N)
{
int nr = 0;
for ( int i = 2; i <= N; ++i )
if ( e_prim3(i) )
++nr;
if ( N % 2 == 0 )
return false;
for ( int i = 3; i*i <= N; i += 2 )
if ( N % i == 0 )
return false;
return nr;
}
return true;
}
119
Capitolul 4
Primele dou funcii au un scop pur didactic, n practic cea de-a
treia funcie fiind cu mult mai eficient i la fel de uor de implementat.
Funcia pi nu prea se folosete n practic, dect dac N nu este foarte mare.
120
13
25
Algoritmi matematici
Se terge numrul marcat i i devine 7. Deja i2 = 49 > 25, deci
algoritmul se ncheie. Numerele prime mai mici sau egale cu 25 sunt
urmtoarele (adic cele rmase):
2 3
5
17
7
19
11
23
13
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
// vom folosi si aici optimizarea de a nu lua in considerare
// numerele pare
lista[2] = true;
for ( int i = 3; i <= N; i += 2 )
lista[i] = true;
for ( int i = 3; i*i <= N; i += 2 )
if ( lista[i] ) // daca i nu a fost sters
for ( int j = i*i; j <= N; j += i )
lista[j] = false; // stergem multiplii lui
// afiseaza numerele prime
cout << 2 << ' ';
for ( int i = 3; i <= N; i += 2 )
if ( lista[i] )
cout << i << ' ';
delete []lista;
}
Algoritmi matematici
(operatorul ~ schimb toi biii unui numr n opusul lor, adica 1 devine 0 i
0 devine 1. Operatorul & se consider cunoscut de la seciunea Radix sort).
Pentru a verifica valoarea unui bit, vom folosi o funcie
bool verif(unsigned char lista[], int nr)
123
Capitolul 4
11111111 &
11111101
11111101
Deci noul vector lista arat acum n felul urmtor:
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
Se procedeaz n acest fel i cu urmtorii multiplii ai lui 3: 12 i 15,
fiecare avnd un bit care le corespunde conform algoritmului prezentat.
Vectorul lista va arta n final n felul urmtor:
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
Acesta este i ultimul pas al algoritmului, deoarece 5 2 > 16 i
algoritmul se ncheie. Se poate observa c toate numerele impare a cror bii
nu sunt teri reprezint numere prime. Numerele pare nu vor fi luate n
considerare, aa c nu prezint interes.
inline void del(unsigned char lista[], int nr)
{
lista[nr / 8] &= ~(1 << (nr % 8));
}
inline bool verif(unsigned char lista[], int nr)
{
return lista[nr / 8] & (1 << (nr % 8));
}
124
Algoritmi matematici
void eratosthenes_biti(int N)
{
int dim = N / 8 + 1;
unsigned char *lista = new unsigned char[dim];
for ( int i = 0; i < dim; ++i )
lista[i] = 0xFF;
for ( int i = 3; i*i <= N; i += 2 )
if ( verif(lista, i) ) // daca i e numar prim
for ( int j = i*i; j <= N; j += i )
del(lista, j); // sterg multiplii lui i
// afisez numerele prime
cout << 2 << ' ';
for ( int i = 3; i <= N; i += 2 )
if ( verif(lista, i) )
cout << i << ' ';
delete []lista;
}
125
Capitolul 4
Algoritmul mai suport o optimizare important: deoarece nu ne
intereseaz numerele pare, putem s le eliminm din list, folosind astfel de
dou ori mai puin memorie. Semnificaia iniial a vectorului lista devine
lista[i] = true dac 2i + 1 este prim i false n caz contrar. Algoritmul
final arat n felul urmtor (restul funciilor rmn neschimbate):
void eratosthenes_final(int N)
{
int dim = N / 8 / 2 + 1;
unsigned char *lista = new unsigned char[dim];
for ( int i = 0; i < dim; ++i )
lista[i] = 0xFF;
for ( int i = 1; ((i*i) << 1) + (i << 1) <= N; ++i )
if ( verif(lista, i) ) // daca 2i + 1 e numar prim
for ( int j = ((i*i) << 1) + (i << 1);
(j << 1) + 1 <= N; j += (i << 1) + 1 )
del(lista, j); // sterg multiplii lui 2i + 1
// afisez numerele prime
cout << 2 << ' ';
for ( int i = 1; (i << 1) + 1 <= N; ++i )
if ( verif(lista, i) )
cout << (i << 1) + 1 << ' ';
delete []lista;
}
4 2 +4+11
calculnd
2+1
=
= 2 + 2 . Transformnd
2
nmulirile cu 2 n operaii be bii rezult formula folosit.
Urmtorul tabel prezint timpii de execuie a celor trei variante ale
algoritmului prezentate pentru mai multe valori ale lui N. Msurtorile au
fost fcute pe un calculator perfomant, cu afiarea numerelor prime scoas.
126
Algoritmi matematici
Tabelul 4.4.1. Comparaie ntre variantele de implementare
a ciurului lui Eratosthenes
Timpul de execuie n secunde
N eratosthenes_clasic eratosthenes_bii eratosthenes_final
107
0.2
0.16
0.07
108
2.78
2.53
0.91
9
10
33.09
31.72
15.54
Se observ imediat c algoritmul este foarte performant dac se
folosesc toate optimizrile posibile, iar memoria folosit este de 16 ore mai
mic dect N.
Ciurul lui Eratosthenes poate fi implementat i folosind clasa
bitset din Standard Template Library. Prezentm o implementare ce nu
conine i ultimele optimizri, acestea fiind lsate pe seama cititorului n
aceast variant de implementare:
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;
cout << 2 << ' ';
for ( int i = 3; i <= N; i += 2 )
if ( lista[i] )
cout << i << ' ';
}
Capitolul 4
intervalul i afim numerele prime, innd cont de semnificaia vectorului
prim.
Se pune problema determinrii celui mai mic multiplu al lui i care
este mai mare sau egal cu st. Fie acest numr m. Putem folosi urmtorul
algoritm pentru a-l calcula eficient pe m:
Fie r = st mod i
Atunci m = st + [(i r) mod i]
Implementarea prezentat poate fi mbuntit innd cont de
aspectele menionate pe parcursul acestei seciuni. Codul ar putea fi mprit
i n mai multe funcii, de exemplu o funcie care genereaz numerele prime
pn la radical din dr i o funcie care genereaz numele prime din
intervalul dat prin cei doi parametrii.
n practic, ciurul lui Eratosthenes este un algoritm foarte rapid de
generare a numerelor prime i poate fi folosit i pentru testarea primalitii
unor numere.
Optimizrile care se pot aduce metodei prezentate sunt lsate ca
exerciiu pentru cititor.
128
Algoritmi matematici
void eratosthenes_interval(int st, int dr)
{
// generez numere prime pana la radical din dr
int N = sqrt(dr);
int dim = N / 8 + 1;
unsigned char *lista = new unsigned char[dim];
for ( int i = 0; i < dim; ++i )
lista[i] = 0xFF;
for ( int i = 3; i*i <= N; i += 2 )
if ( verif(lista, i) ) // daca i e numar prim
for ( int j = i*i; j <= N; j += i )
del(lista, j); // sterg multiplii lui i
// generez numerele prime din intervalul [st, dr];
if ( st <= 1 )
st = 2;
unsigned char *prim = new unsigned char[dr - st + 2];
for ( int i = st; i <= dr; ++i )
prim[i - st] = 1;
for ( int i = 3; i*i <= dr; i += 2 )
if ( verif(lista, i) ) // daca i e prim
{
int r = st % i;
int m = st + ((i - r) % i);
if ( m == i ) // am grija sa nu elimin chiar un numar prim
m += i;
for ( int j = m; j <= dr; j += i )
prim[j - st] = 0;
}
delete []lista;
if ( st <= 2 ) // 2 e caz particular la afisare
cout << 2 << ' ';
for ( int i = st; i <= dr; ++i )
if ( prim[i - st] && (i % 2) == 1 )
cout << i << ' ';
delete []prim;
}
129
Capitolul 4
2 + 3 = 4
+ 2 = 3
3 + 2 + 2 = 2
+ + 3 = 1
4 + 3 + = 5
130
Algoritmi matematici
1 :
2 :
3 :
3 + 2 + 2 = 2
1
7
1
+ =
3
3
3
1
5
23
=
3
3
3
3 + 2 + 2 = 2
1
7
1
+ =
3
3
3
4 = 8
131
Capitolul 4
11
21
12
22
.
.
.
=
1
1 1
2 2
.
.
.
1
0
132
Algoritmi matematici
Practic, se nlocuiete la fiecare pas necunoscuta determinat n
restul ecuaiilor. La sfritul algoritmului, coloana termenilor liberi va
conine valorile necunoscutelor, deci sistemul va fi rezolvat. Matricea final
va arta n felul urmtor:
1 0 00
0 1 00
0 0 10
=
.
.
.
0 0 01
1
2
3
.
.
.
133
Capitolul 4
#include <fstream>
using namespace std;
const int maxn = 101;
int main()
{
int N;
double A[maxn][maxn];
citire(A, N);
Gauss(A, N);
ofstream out("gauss.out");
for ( int i = 1; i<=N; ++i )
out << A[i][N+1] << ' ';
out.close();
in.close();
}
return 0;
void Gauss(double A[maxn][maxn], int N)
{
for ( int i = 1; i <= N; ++i )
for ( int j = N + 1; j >= i; --j )
for ( int k = i + 1; k <= N; ++k )
A[k][j] -= (A[k][i] / A[i][i]) * A[i][j];
Algoritmi matematici
cu nimic corectitudinea algoritmului, deoarece ordinea ecuaiilor n sistem
este irelevant.
Noua funcie Gauss este urmtoarea:
void gauss(double A[maxn][maxn], int N)
{
for ( int i = 1; i <= N; ++i )
{
// interschimb linia i cu linia care are al i-lea element de
// valoare maxima
int max = i;
for ( int j = i + 1; j <= N; ++j )
if ( A[max][i] < A[j][i] )
max = j;
for ( int j = 1; j <= N + 1; ++j )
swap(A[max][j], A[i][j]);
for ( int j = N + 1; j >= i; --j )
for ( int k = i + 1; k <= N; ++k )
A[k][j] -= (A[k][i] / A[i][i]) * A[i][j];
}
// aflarea necunoscutelor de la ultimul rand spre primul
for ( int i = N; i; --i )
{
A[i][N + 1] /= A[i][i];
A[i][i] = 1;
for ( int j = i - 1; j; --j )
{
A[j][N + 1] -= A[j][i] * A[i][N + 1];
A[j][i] = 0;
}
}
}
Exerciii:
a) Adugai condiii care verific dac un sistem nu are soluie i
afieaz un mesaj corespunztor n acest caz.
b) ncercai s gsii un sistem pentru care cele dou variante
prezentate al algoritmului dau rspunsuri diferite.
135
Capitolul 4
= 0
136
Algoritmi matematici
int exponentiere_log(int a, int b, int N)
{
if ( b == 0 ) return 1;
if ( (b & 1) == 0 ) // daca b e numar par
{
int temp = exponentiere_log(a, b >> 1, N);
return (temp * temp) % N;
}
return ((a % N) * exponentiere_log(a, b - 1, N)) % N;
}
Capitolul 4
n total ase operaii de nmulire. Am putea folosi ns numai cinci:
Tabelul 4.6.2. O serie optim de nmuliri pentru a
ridica un numr la puterea 15
Nr b nmuliri
a3a12
1 15
a2a6
2 12
6
a2a3
3
3
aa2
4
2
aa
5
Nu se cunoate niciun algoritm care s efectueze un numr minim de
operaii. O problem des studiat este gsirea numrului minim de nmuliri
necesare pentru un anumit exponent, problem care suport diverse
optimizri, dar pentru care nu se cunoate niciun algoritm polinomial.
Exponenierea logaritmic are diverse aplicaii n implementarea
algoritmilor din teoria numerelor, aa cum vom vedea n cele ce urmeaz.
138
Algoritmi matematici
Algoritmul presupune generarea aleatoare a mai multor valori pentru
a i testarea congruenei. Dac aceasta se verific pentru mai multe valori
ale lui a, atunci p este probabil prim (sau pseudoprim). Dac n schimb
gsim un a care nu verific ecuaia, atunci p sigur nu este prim.
De exemplu, dac vrem s verificm primalitatea lui 15, cu
a {4, 11, 12}, vom gsi:
414 1 15 15
1114 1 15 15
1214 9 15 15
Este uor de observat c dac am fi verificat doar 4 i 11, rspunsul
ar fi fost greit.
Complexitatea algoritmului este O(klog p) folosind exponeniere
logaritmic, unde k este numrul de valori pe care le ncercm pentru a.
bool fermat_test(int p, int k)
{
for ( int i = 1; i <= k; ++i )
{
int a = 2 + rand() % (p - 2);
if ( exponentiere_log(a, p - 1, p) != 1 )
return false; // SIGUR nu e prim
}
return true;
// PROBABIL e prim
139
Capitolul 4
Se scrie p 1 n forma 2sd, unde s este maxim.
Se repet de k ori (unde k are aceeai semnificaia ca la testul
Fermat; reprezint practic acurateea testului)
o Se alege aleator un a din [2, p 1].
o x = ad mod p
o Dac x == 1 sau x == p 1
Se trece la urmtoarea iteraie a lui k
o Pentru r = 1 pn la s 1 execut
x = x2 mod p
Dac x == 1 returneaz NEPRIM
Dac x == p 1 se trece la urmtoarea iteraie a
lui k
o Returneaz NEPRIM
Returneaz PROBABIL PRIM
Algoritmul se bazeaz pe urmtoarele observaii: fie p > 2 este un
numr prim. Observm c 1 i -1, ridicate la ptrat, vor fi ntotdeauna
congruente cu 1 modulo p. n alte cuvinte, putem scrie:
2 1 1 + 1 0
+1 1 ( )
Deoarece p este un numr prim impar, nseamn c p 1 va fi
ntotdeauna par i poate fi scris ca 2sd, unde s este maxim, iar d este evident
impar. Pentru orice a natural din [2, p 1], una dintre urmtoarele afirmaii
trebuie s fie adevrat:
1. 1
2. 0 < . . 2 1 1 ( )
Demonstraia acestor afirmaii se bazeaz pe torema lui Fermat:
1 1 ( )
Din observaia de mai sus, dac continum s extragem radical din
1 , vom rmne la sfrit fie cu -1 (adic p 1) fie cu 1, modulo p. Dac
obinem -1, atunci a doua egalitate este adevrat. n caz c a doua egalitate
nu a fost niciodat adevrat, nseamn c prima egalitate trebuie s fie
0
adevrat, deoarece avem 2 = 1 ( ).
Testul Miller-Rabin se bazeaz pe opusele celor afirmate mai sus.
Dac putem gsi un a astfel nct
140
Algoritmi matematici
1 (1)
i
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;
}
for ( int k = 0; a[k] && a[k] < p; ++k )
{
int x = exponentiere_log(a[k], d, p);
if ( x == 1 || x == p - 1 )
continue; // urmatoarea iteratie a lui k
bool doi = true; // verificam daca relatia (2) este adevarata.
// Initial presupunem ca da
for ( int r = 1; r < s; ++r )
{
x = (x*x) % p;
if ( x == 1 )
return false;
else if ( x == p - 1 )
{
doi = false;
break;
}
}
if ( doi ) return false; // daca (2) este adevarata,
// atunci p este compus
}
return true; // PROBABIL prim (in practica putem fi SIGURI
// deoarece p este de tip int)
}
142
Algoritmi matematici
=
=1
Capitolul 4
Algoritmul este aplicarea direct a formulei de mai sus. Explicaia
parcurgerii pn la radical din Y este intuitiv: un numr poate avea cel mult
un singur factor prim mai mare dect radicalul su. Gsind toi factorii primi
mai mici dect radicalul i eliminndu-i (mprind numrul la acetia de
cte ori este posibil), vom rmne la sfrit doar cu acest factor prim, n caz
c exist. Acelai algoritm poate fi folosit pentru descompunerea efectiv a
unui numr n factori primi.
Se mai poate aplica optimizarea de a nu lua n considerare multiplii
lui 2, dar acest lucru complic inutil codul pentru scopul gsirii unui
algoritm eficient de determinare a inverilor modulari.
Pentru a determina efectiv un invers modular vom folosi teorema lui
Euler, care afirm c () 1 ( ). De aici rezult
1 1 ( ), ceea ce nseamn c 1 este inversul
modular al lui X. Dac se lucreaz modulo un numr prim, atunci
= 1, deoarece toate numerele mai mici dect un numr prim sunt
coprime cu acesta. n acest caz, inversul multiplicativ este 2 , valoare
care se poate calcula n timp O(log Y) folosind exponeniere logaritmic. n
cazul n care Y nu este numr prim, este necesar calculul indicatorului lui
Euler, ceea ce necesit timp O( Y).
De exemplu, dac dorim s calculm inversul lui 6 modulo 13, vom
proceda de data aceasta n felul urmtor: tim ca 13 = 12 deoarece 13
este numr prim. Inversul modular al lui 6 modulo 13 este aadar
611 11 ( 13). Un calculator ajunge la acest rezultat n mai puin de
12 operaii, cte sunt necesare pentru metoda clasic.
n general, algoritmului lui Euclid extins este mai rapid pentru
gsirea inverilor modulari, dar aceast metod are avantajul de a fi mai
uor de implementat.
Prezentm dou funcii: o funcie phi, care calculeaz indicatorul lui
Euler pentru un numr dat ca parametru i o funcie phi_interval, care
calculeaz valorile indicatorului lui Euler pentru toate numerele mai mici
dect numrul dat ca parametru.
144
Algoritmi matematici
int phi(int Y)
{
int R = Y, Yt = Y;
for ( int i = 2; i*i <= Y; ++i )
if ( Yt % i == 0 )
{
R /= i;
R *= i 1;
while ( Yt % i == 0 )
Yt /= i;
}
}
if ( Yt > 1 )
{
R /= Yt;
R *= Yt 1;
}
return R;
}
Capitolul 4
Aceast problem a fost propus de matematicianul chinez Sun Tsu
n secolul IV. Putem rezolva problema n felul urmtor: ne propunem s
gsim trei numere naturale a cror sum s fie o soluie a sistemului. De
exemplu, aceast sum ar putea fi 35 + 63 + 30 = 128. Se poate verfica uor
c X = 128 este o soluie a sistemului. Vom vedea n continuare cum am
ajuns la aceast soluie.
Primul numr, 35, d restul 2 la mprirea cu 3 i este un multiplu al
numerelor 5 i 7, deci nu va afecta celelalte dou congruene. Al doilea
numr, 63, d restul corect la mprirea cu 5 i este un multiplu al
numerelor 3 i 7, deci nu va afecta celelalte dou congruene. Al treilea
numr, 30, d restul corect la mprirea cu 7 i este un multiplu al
numerelor 3 i 5, deci nici acesta nu va afecta restul congruenelor. Aadar,
problema se reduce la a gsi, pentru fiecare congruen, un numr care
satisface acea congruen i care este un multiplu al numerelor modulo care
se cer rezolvate celelalte congruene.
Pentru exemplul dat, gsim prima dat 57 = 35, care verific prima
relaie i nu le afecteaz pe celelalte. Pasul urmtor este s gsim un
multiplu al numerelor 3 i 7 care verific a doua relaie i nu le afecteaz pe
prima i pe ultima. Un astfel de numr este 63. La fel se gsete i numrul
30.
n cazul general, se d urmtorul sistem:
1
2
( 1 )
( 2 )
.
.
.
( )
146
Algoritmi matematici
Fie
= 1 2 .
Fie
1 .
Calculm
( )
=1
( )
=1
Lucru echivalent cu
( )
=1
deoarece | . Din =
rezult 0
Capitolul 4
s calculm funcia totenial (n caz c numerele nu sunt prime), caz n
care algoritmul devine mai ncet.
Programul prezentat presupune c datele de intrare se citesc din
fiierul TCR.in, fiier ce are urmtorul format: pe prima linie numrul k, iar
pe urmtoarele k linii perechi de numere , avnd semnificaia din
enun. n fiierul de ieire TCR.out se va afia soluia sistemului.
Trebuie precizat c programul prezentat nu ine cont de faptul c un
anumit set de date poate s nu aib soluie sau poate s determine unele
calcule intermediare s depeasc valoarea maxim reprezentabil pe tipul
de date int. Tratarea acestor cazuri este lsat ca exerciiu pentru cititor.
#include <fstream>
using namespace std;
const int maxk = 100;
struct congruenta
{
int a, n;
};
void citire(int &k, congruenta T[])
{
ifstream in("TCR.in");
in >> k;
for ( int i = 1; i <= k; ++i )
in >> T[i].a >> T[i].n;
in.close();
}
148
Algoritmi matematici
void euclid_ext(int X, int Y, int &a, int &b)
{
if ( X % Y == 0 )
{
a = 0;
b = 1;
return ;
}
int ta, tb;
euclid_extins(Y, X % Y, ta, tb);
a = tb;
b = ta - tb*(X / Y);
}
int rezolvare(int k, congruenta T[])
{
int N = 1, P[maxk], Q[maxk];
for ( int i = 1; i <= k; N *= T[i++].n );
for ( int i = 1; i <= k; ++i ) P[i] = N / T[i].n;
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
100
5
= 20 de numere divizibile cu
5 i
= 16 numere divizibile cu 6. Am putea fi tentai s dm rspunsul
6
20 + 16 = 36 de numere divizibile cu 5 sau cu 6, dar acest rspuns este
greit, deoarece am numrat anumite numere de dou ori. Mai exact, am
numrat toate numerele care sunt multipli att pentru 5 ct i pentru 6, adic
100
multiplii numrului 56 = 30. Exist
= 3 numere divizibile cu 30.
30
Pentru ca rspunsul nostru s fie corect, trebuie s scdem aceast cantitate
din rezultatul obinut anterior, obinnd astfel 20 + 16 3 = 33 de numere
divizibile cu 5 sau cu 6 n intervalul [1, 100].
S lum nc un exemplu: cte numere din acelai interval sunt
divizibile cu 2, 3 sau 7? De data aceasta avem 50 de numere divizibile cu 2,
33 de numere divizibile cu 3 i 14 numere divizibile cu 7, dar este clar c
rspunsul nu este 50 + 33 + 14 = 97, deoarece am numrat multiplii
numerelor 6, 14 i 21 de dou ori. Trebuie s scdem aadar 16 multipli de
6, 7 multipli de 14 i 4 multipli de 21. Am putea fi tentai s considerm
rspunsul final ca fiind 50 + 33 + 14 16 7 4 = 70, dar acest rezultat este
greit, deoarece multiplii numrului 237 = 42 au fost adunai de trei ori (o
dat pentru fiecare dintre numerele 2, 3 i 7) i sczui de trei ori (o dat
pentru fiecare dintre numerele 6, 14 i 21). Aadar am sczut prea mult i
trebuie s adunm din nou numerele divizibile cu 42, care sunt dou.
Rspunsul final este aadar 50 + 33 + 14 16 7 4 + 2 = 72 de numere
divizibile cu 2 sau cu 3 sau cu 7.
S vedem dac putem deduce o regul de calcul pentru cazul n care
trebuie s determinm cte numere sunt divizibile cu cel puin unul dintre
numerele dintr-un ir dat. Dac N(x) este o funcie egal cu numrul
multiplilor lui x din intervalul dat, atunci putem scrie rspunsul la problema
precedent n felul urmtor:
= 2 + 3 + 7 < 2,3 > < 2,7 > < 3,7 > +
+(< 2,3,7 >)
Unde S este numrul 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 determinm cardinalul reuniunii mai
multor mulimi. Dac avem n mulimi notate A1 , A2 , ..., An atunci:
150
Algoritmi matematici
=
=1
1 2 + + (1)1 1 2
=1
11 <2
Capitolul 4
2. Calcularea celui mai mic multiplu comun (cmmmc) folosind
algoritmul lui Euclid:
, =
(, )
3. Pentru calcularea celui mai mic multiplu comun al mai multor
numere, se poate aplica aceeai formul ca la cmmdc, dar uneori
exist posibilitatea folosirii unui algoritm mai simplu i anume:
se nmulete cel mai mic numr din ir cu 0, 1, 2, ..., pn cnd
rezultatul se mparte fr rest la toate celelalte numere.
4. Folosirea operaiilor be bii pentru a optimiza memoria folosit i
timpul de execuie:
a. 2k = 1 << k
b. X2k = X << k
c. X / 2k = X >> k
d. X % 2k = X & (2k 1)
e. Afl dac X este divizivbil cu 2: (X & 1) == 0
f. Afl dac X este putere a lui 2: (X & (X 1)) == 0
g. Seteaz al k-lea bit (de la stnga spre dreapta) al
ntregului X pe 1: X |= 1 << k.
h. Seteaz al k-lea bit al ntregului X pe 0: X &= (1 << k)
1;
5. Calcularea eficient a numerelor Fibonacci: dac irul Fibonacci
este definit astfel:
0
1
=
2 + 1
=0
=1
>1
Atunci:
1 1
1 0
( + 1)
()
()
( 1)
=
( + 1)
152
( + 1)
()
Algoritmi matematici
6. Pentru ca metoda de mai sus s fie eficient, este necesar s
folosim exponeniere logaritmic. Pentru acest lucru putem lucra
direct cu matrici i cu o funcie de nmulire a matricilor, sau
putem scrie o clas care s gestioneze lucrul cu matrici. Vom
introduce lucrul cu clase n seciunea urmtoare. Deocamdat
prezentm doar o secven de cod ce nmulete dou matrici
ptratice (X i Y) de ordin N date ca parametru ntr-o a treia
matrice T, iniializat cu 0.
for ( int k = 0; k < N; ++k )
for ( int i = 0; i < N; ++i )
for ( int j = 0; j < N; ++j )
T[i][j] += X[i][k] * Y[k][j];
7. Dac avem:
= 1 1 2 2 ,
atunci:
+ 1 , =
=1
153
Capitolul 4
void F(const T ¶m) { ... }
int main() { T x; F(x); return 0; } // se transmite adresa
// obiectului x, nu o copie a sa.
9. Numrul zerourilor terminale ale lui n!:
+ 2 + 3 +
5
5
5
=
10. Dac avem:
= 1 1 2 2 ,
atunci:
=
=1
+1
1
, =
1
154
Algoritmi matematici
i 0 1 2 3 4 5
B 5 9 8 2 4 9
155
Capitolul 4
La primul pas i = 1, transport = 0. Efectum
C[i] = A[i] + B[i] + transport, ceea ce nseamn C[1] = 18.
transport devine C[i] / 10, adic 1, iar C[1] devine C[1] % 10,
adic 8.
i 0 1 2 3 4 5 6
C 0 8
transport = 1
La pasul i = 2, efectum aceleai operaii, obinnd
C[2] = A[2] + B[2] + transport, adic C[2] = 18.
transport devine 1, iar C[2] devine 8.
i 0 1 2 3 4 5 6
C 0 8 8
transport = 1
Se procedeaz n acest mod pn ce se ajunge la pasul i = 5, la
sfritul cruia vom avea urmtorul vector:
i
C
0
0
1 2 3 4
8 8 9 6
transport = 1
5
0
0
6
1
8
2
8
3
9
4
6
5
0
6
1
156
Algoritmi matematici
void adunare(int A[], int B[], int C[])
{
int i, transport = 0;
for ( i = 1; i <= A[0] || i <= B[0]; ++i )
{
C[i] = A[i] + B[i] + transport;
transport = C[i] / 10;
C[i] %= 10;
}
if ( transport )
C[i] = transport;
C[0] = i;
}
0
3
1
1
2
3
3
1
i
B
0
2
1
9
2
9
3
0
0
3
1
2
2
3
3
1
Capitolul 4
i
A
0
3
1
2
2
3
3
1
0
3
1
2
2
3
3
0
158
Algoritmi matematici
Capitolul 4
fiecare cifr a numrului mare cu numrul mic, se adun transportul la
rezultat (care iniial este 0), se pstreaz restul mpririi la 10, iar noul
transport devine rezultatul mprit la 10.
De exemplu, dac vrem s nmulim numrul mare A = 3173 cu
numrul mic B = 13, vom proceda n felul urmtor:
i 0 1 2 3 4
A 4 3 7 1 3
transport = 0
La pasul i = 1 efectum A[1] = A[1] * 13 + transport, adic
A[1] = 39. transport devine A[1] / 10 adic 3, iar A[1] devine A[1] % 10,
adic 9:
i 0 1 2 3 4
A 4 9 7 1 3
transport = 3
Se continu procedeul pn la pasul i = 4, cnd vectorul A va arta
n felul urmtor:
i 0 1 2 3 4
A 4 9 4 2 1
transport = 4
Ct timp mai exist transport, cifrele acestuia trebuie adugate
numrului A. Rezultatul final va fi aadar:
i 0 1 2 3 4 5
A 5 9 4 2 1 4
Adic 3173 13 = 41 249.
O funcie care nmulete un numr mare cu un numr mic poate fi
implementat n felul urmtor:
160
Algoritmi matematici
void inm_mic(int A[], int B)
{
int transport = 0;
for ( int i = 1; i <= A[0]; ++i )
{
A[i] = A[i] * B + transport;
transport = A[i] / 10;
A[i] %= 10;
}
while ( transport )
{
A[ ++A[0] ] = transport % 10;
transport /= 10;
}
}
Capitolul 4
Algoritmul propus este urmtorul:
Se nmulete fiecare cifr i a primului numr cu fiecare cifr j a
celui de-al doilea numr, iar rezultatul se adun la cifra i + j 1 a
rezultatului (care este iniial nul)
La sfrit se face corectarea rezultatului (care este evident greit
n aceast form, doarece putem avea o cifr mai mare ca 9) cifr
cu cifr astfel:
o Fie Y = X + transport, unde X este cifra curent
o X va deveni Y % 10
o transport va deveni Y / 10
S vedem ce obinem prin aceast metod pe exemplul anterior:
Rezultatul are fie 4 + 3 = 7 fie 4 + 3 1 = 6 cifre (vom demonstra n
cele ce urmeaz aceast afirmaie), deci vom declara un vector de lungime
7, iniializat cu 0, care va reine rezultatul:
i
A
B
Rez
0
4
3
6
1
3
3
0
2
1
1
0
3
2
4
0
4
1
0
0
5
0
0
0
6
0
0
0
7
0
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
Efectum corectarea rezultatului: Rez[1] rmne 9, iar transportul
devine 0. Rez[2] rmne 6, iar transportul rmne 0. Rez[3] devine 9, iar
transportul devine 1:
i
0 1 2 3 4 5 6 7
Rez 6 9 6 9 9 9 4 0
Rez[4] devine (9 + transport) mod 10, adic 0, iar transportul
devine (9 + transport) / 10, adic 1:
i
0 1 2 3 4 5 6 7
Rez 6 9 6 9 0 9 4 0
Rez[5] devine (9 + transport) mod 10, adic 0, iar transportul
devine (9 + transport) / 10, adic 1:
i
0 1 2 3 4 5 6 7
Rez 6 9 6 9 0 0 4 0
Rez[6] devine 5, iar transportul 0:
i
0 1 2 3 4 5 6 7
Rez 6 9 6 9 0 0 5 0
n final mai trebuie s verificm dac transportul este diferit de 0,
caz n care trebuie s cretem numrul de cifre i s mai adugm o cifr
egal cu transportul. Pe acest exemplu nu este ns cazul.
Mai trebuie s demonstm corectitudinea acestei metode. n primul
rnd, produsul a dou numere X i Y, avnd P respectiv Q cifre este egal fie
cu P + Q fie cu P + Q 1. Acest lucru reiese din urmtoarele relaii:
101 < 10
101 < 10
163
Capitolul 4
Dac nmulim cele dou relaii obinem:
10+2 < 10+
De unde rezult afirmaia anterioar.
Avnd demonstrat numrul de cifre, corectitudinea acestei metode
este uor de dedus: la fiecare pas se adun rezultatul nmulirii cifrelor
curente n poziia corespunztoare a vectorului rezultat. Exact acelai lucru
se ntampl i n forma clasic a algoritmului, atta doar c forma clasic
este mai uor de efectuat pentru om (deoarece nu trebuie s inem cont de
poziii i nici de corectarea rezultatului), iar aceast form este mai simplu
de implementat pentru un programator, deoarece nu trebuie reinute i
adunate rezultatele intermediare.
void inm_mare(int A[], int B[], int C[]) // Se presupune C initializat cu 0
{
C[0] = A[0] + B[0] - 1;
for ( int i = 1; i <= A[0]; ++i )
for ( int j = 1; j <= B[0]; ++j )
C[i + j - 1] += A[i] * B[j];
int 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
Algoritmul se ncheie n momentul n care toate cifrele ale
dempritului au fost coborte. Zerourile din faa rezultatului se ignor.
void impartire(int A[], int B) // rezultatul va fi retinut in A
{
int transport = 0;
for ( int i = A[0]; i > 0; --i )
{
transport = transport * 10 + A[i];
A[i] = transport / B;
transport %= B;
}
while ( !A[ A[0] ] && A[0] > 1 )
--A[0];
}
Capitolul 4
parcurgem numrul mare cifr cu cifr i s construim treptat restul innd
cont de proprietile operaiei modulo.
int modulo(int A[], int B)
{
int rest = 0;
for ( int i = A[0]; i > 0; --i )
{
rest = rest * 10 + A[i];
rest %= B;
}
return rest;
}
166
Algoritmi backtracking
5. Algoritmi
backtracking
Am prezentat pn acum descrierea general a tehnicii de
programare numit backtracking, mpreun cu nite probleme elementare
care se rezolv cu ajutorul acestei tehnici. Problemele prezentate anterior nu
au ns nicio aplicabilitate intrinsec, acestea aparnd de cele mai multe ori
doar ca subprobleme n cadrul altor probleme mai complexe.
167
Capitolul 5
CUPRINS
5.1. Problema labirintului ............................................................................. 169
5.2. Problema sriturii calului ....................................................................... 173
5.3. Generarea submulimilor ...................................................................... 175
5.4. Problema reginelor ................................................................................ 177
5.5. Generarea partiiilor unei mulimi ........................................................ 180
168
Algoritmi backtracking
labirint.out
11
12
22
11
21
22
Capitolul 5
fie back(k, lin, col, N, A, st) funcia la care am fcut referire mai sus.
Aceast funcie poate fi implementat astfel:
Reine (lin, col) n st[k]
Dac (lin, col) == (N, N) afieaz coninuturile 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
n primul rnd trebuie s explicm cteva lucruri care pot prea
ciudate la prima vedere.
Un vecin (n_lin, n_col) se consider valid doar dac
A[n_lin][n_col] este 0 i dac linia i coloana acestuia nu este mai mic
dect 1 sau mai mare dect N.
Pentru a parcurge mai uor vecinii unui element, putem folosi
vectori de direcie. Vectorii de direcie sunt nite vectori dx i dy cu valori
alese n aa fel nct dac adunm dx[0] la lin i dy[0] la col s obinem
primul vecin (n_lin, n_col). Dac adunm dx[1] respectiv dy[1] vom obine
al doilea vecin etc. (ordinea obinerii vecinilor nu are importan atta timp
ct nu se obine acelai vecin de mai multe ori). Deoarece avem 4 vecini,
vectorul va avea patru elemente. Se poate observa uor c:
(n_lin, n_col) {(lin + 1, col); (lin, col + 1); (lin 1, col); (lin, col 1)}
aa c
dx = {1, 0, -1, 0}
dy = {0, 1, 0, -1}.
Deoarece nu avem voie s parcurgem un element de mai multe ori,
trebuie s marcm cumva elementele deja vizitate. Acest lucru l facem prin
nchiderea camerelor prin care am trecut deja, adic prin marcarea lor cu
true. La revenire din recursivitate este clar c acestea trebuie redeschise
deoarece cutm nc un drum, adic trebuie marcate cu 0.
Pentru exemplul dat algoritmul funcioneaz n felul urmtor: prima
dat se adaug (1, 1) n stiv. Pentru fiecare vecin valid al su, adic pentru
(1, 2) i (2, 1), efectum A[1][1] = true i apelm funcia recursiv, prima
dat pentru (1, 2). nainte de apelul recursiv stiva i matricea arat n felul
urmtor:
170
Algoritmi backtracking
st
A
true false
(1, 1)
false false
La apelul recursiv pentru (1, 2), adugm aceast pereche n stiv,
marcm elementul curent ca fiind vizitat i apelm funcia recursiv pentru
singurul vecin valid al lui (1, 2), adic (2, 2). nainte de apel avem
urmtoarea configuraie:
st
A
(1, 2) true true
(1, 1) false false
La apelul recursiv pentru (2, 2) vom ajunge la urmtoarea
configuraie, care ne va da primul traseu:
st
A
(2, 2) true true
(1, 2)
(1, 1) false false
Deoarece (2, 2) reprezint sfritul traseului, acesta nu se mai
seteaz pe true. La revenire din recursivitate se vor redeschide (seta pe
false) celelalte camere i se va gsi cellalt traseu.
Prezentm n continuare implementarea algoritmului de rezolvare a
problemei labirintului.
#include <fstream>
using namespace std;
in >> N;
for ( int i = 1; i <= N; ++i )
for ( int j = 1; j <= N; ++j )
in >> A[i][j];
in.close();
}
171
Capitolul 5
bool valid(int lin, int col, int N,
bool A[maxn][maxn])
{
if ( lin > 0 && lin <= N &&
col > 0 && col <= N )
if ( A[lin][col] == false )
return true;
int main()
{
int N;
bool A[maxn][maxn];
stiva st[maxn*maxn];
citire(N, A);
ofstream out("labirint.out");
back(1, 1, 1, N, A, st, out);
return false;
}
void back(int k, int lin, int col, int N,
bool A[maxn][maxn], stiva st[],
ofstream &out)
{
st[k].lin = lin;
st[k].col = col;
out.close();
return 0;
}
Algoritmi backtracking
Capitolul 5
Trebuie menionat faptul c, dac ne intereseaz doar un singur
traseu, exist un algoritm liniar n dimensiunea matricii care poate rezolva
problema. Acest algoritm i se datoreaz lui Warnsdorff, iar pseudocodul
su este urmtorul:
Fie C poziia curent a calului. Iniial C = (1, 1)
Se marcheaz poziia C cu 1
Pentru i de la 2 pn la N2 execut
o Deplaseaz calul ntr-o poziie valid (x, y) astfel nct
(x, y) s permit un numr minim de deplasri ulterioare.
o C = (x, y).
o Se marcheaz poziia C cu i.
Numerele din matrice reprezint, n ordine, elementele traseului.
Implementarea algoritmului Warnsdorff este lsat pe seama
cititorului. Prezentm aici doar implementarea cu ajutorul metodei
backtracking. Citirea i afiarea sunt lsate pe seama cititorului.
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;
}
}
}
Algoritmi backtracking
175
Capitolul 5
32 de elemente pentru niciun scop practic (de fapt 32 este chiar o
supraestimare).
Dac analizm algoritmul de mai sus, observm c valorile stivei pot
fi interpretate ca un numr n baza 2. Putem aadar s folosim reprezentarea
numerelor n baza 2 pentru generarea submulimilor astfel: ncepem cu
numrul 1, care n baza doi se reprezint astfel: 000...01 (unde 0 apare de
N 1 ori). Aceast reprezentare semnific faptul c avem o submulime
format fie din numrul N, fie din numrul 1, depinde cum interpretm
ordinea biilor. n continuare vom interpreta biii ca reprezentnd, de la
stnga la dreapta, numerele N, N 1, ..., 3, 2, 1. Pentru a genera urmtoarea
submulime, tot ce trebuie s facem este s adunm 1 pentru a obine
reprezentarea 000...10, reprezentnd submulimea {2}. Mai adunm 1 i
obinem reprezentarea binar 000...11, reprezentnd submulimea {1, 2}. Se
procedeaz n acest mod pn ajungem la reprezentarea 111...11 (N de 1),
care reprezint submulimea final, adic {1, 2, 3, ..., N}.
Observaie: deseori se confund metoda backtracking cu metodele
exhaustive de rezolvare a unei probleme. Doar pentru c ncercm toate
posibilitile nu nseamn c folosim backtracking! Tehnica backtracking
presupune revenirea la un pas anterior pentru a schimba o decizie luat (n
cazul acesta schimbarea unei valori din 0 n 1). Dei algoritmul care
folosete operaii pe bii are aceeai complexitate ca algoritmul
backtracking, cel pe bii nu revine niciodat la un pas precedent pentru a
schimba o alegere fcut, deci nu este un algoritm de tip backtracking!
Prezentm doar funciile relevante, programele complete se
consider uor de realizat, urmnd un tipar care deja ar trebui s fie
cunoscut.
176
Algoritmi backtracking
folosind backtracking
if ( afisat )
out << '\n';
return;
}
for ( int i = 0; i <= 1; ++i )
{
st[k] = i;
back(k + 1, N, st, out);
}
}
177
Capitolul 5
O soluie este:
Algoritmi backtracking
// st ajunge sa aiba dimensiunea 32
bool valid(int k, int i, int st[])
{
for ( int j = 1; j < k; ++j )
if ( k - j == i - st[j] || k - j == st[j] - i )
return false;
return true;
}
// nr va contine rezultatul
void back(int k, int N, int st[], bool fol[], int &nr)
{
if ( k > N )
{
++nr;
return;
}
for ( int i = 1; i <= N; ++i )
if ( !fol[i] )
if ( valid(k, i, st) )
{
st[k] = i;
fol[i] = true;
back(k + 1, N, st, fol, nr);
fol[i] = false;
}
}
179
Capitolul 5
2. = , ,
Dndu-se un numr natural N, ne propunem s scriem un program
care genereaz toate partiiile mulimii {1, 2, 3, ..., N}.
Numrul N se citete din fiierul partitii.in, iar partiiile gsite se
afieaz n fiierul partitii.out, fiecare partiie pe o singur linie, cu
mulimile partiiei ntre acolade, separate ntre ele prin spaiu i elementele
unei mulimi separate prin virgul i spaiu, aa cum se vede n exemplul de
mai jos.
Exemplu:
partitii.in partitii.out
3
{1, 2, 3}
{1, 2} {3}
{1, 3} {2}
{1} {2, 3}
{1} {2} {3}
Vom rezolva problema ntr-o manier similar cu problemele
precedente. Vom folosi o stiv st care va codifica o partiie. Fiecare element
i al stivei va reprezenta numrul de ordine al mulimii din care face parte
elementul i n cadrul partiiei curente. De exemplu, codificarea urmtoare:
st = {1, 1, 2} reprezint partiia {1, 2} {3}, codificarea st = {1, 2, 2}
reprezint partiia {1} {2, 3} etc.
O prim idee ar fi s generm pe rnd N posibiliti pentru fiecare
poziie, dar putem gsi uor un contraexemplu la aceast abordare:
codificrile {1, 1, 2} i {2, 2, 1}, la care se va ajunge prin aceast metod,
sunt de fapt identice. Acestea reprezint {1, 2} {3} i {3} {1, 2}, care sunt de
fapt acelai lucru. Avem aadar nevoie fie de o funcie de validare fie de o
metoda de a genera configuraii care produce numai configuraii valide.
180
Algoritmi backtracking
Vom ncerca s generm partiii astfel nct primul element al unei
configuraii valide s fie ntotdeauna 1. Cu alte cuvinte, numrul 1 va face
ntotdeauna parte din prima mulime a unei partiii. Numrul 2 se va afla
ntotdeauna fie n prima mulime a unei partiii, fie n a doua. n cazul
general, numrul i va face parte ntotdeauna dintr-o mulime dintre primele i
mulimi ale unei partiii. Acest lucru este corect, deoarece oricum am genera
partiii putem observa c un element i va face parte din i mulimi distincte.
Tot ce facem este s impunem o anume ordine de generare acestora.
Din cele de mai sus rezult c st[k] va lua valori ntre 1 i
1 + max{st[i] | i < k}. Avem aadar o metod de a genera partiii care va
genera numai partiii valide, deci nu avem nevoie de o funcie de validare.
Menionm c exist o metod eficient de a numra cte partiii
exist. Aceasta va fi prezentat n capitolul dedicat programrii dinamice.
Singura diferen ntre codul ce urmeaz i codul pentru problemele
anterioare este c aici mai introducem un parametru max care ne d
maximul ce ne intereseaz la pasul curent. Astfel, la pasul k tim c max
este maximul elementelor st[1], st[2], ..., st[k 1]. Este clar c atunci cnd
trecem la pasul k + 1, noul maxim va fi maximul dintre max i st[k].
Prezentm doar funcia de generare a partiiilor. Restul programului
este asemntor cu programele prezentate anterior. Observai ct cod s-a
scris numai pentru afiarea n formatul cerut...
181
Capitolul 5
void back(int k, int N, int max, int st[], ofstream &out)
{
if ( k > N )
{
for ( int i = 1; i <= N; ++i )
{
bool primul = true;
for ( int j = 1; j <= N; ++j )
if ( st[j] == i )
{
if ( primul )
{
out << '{' << j; primul = false;
}
else
out << ", " << j;
}
if ( !primul )
out << "} ";
}
out << '\n'; return;
}
for ( int i = 1; i <= max + 1; ++i )
{
st[k] = i;
int n_max = max;
if ( st[k] > max )
n_max = st[k];
back(k + 1, N, n_max, st, out);
}
}
182
Algoritmi generali
6. Algoritmi
generali
Exist algoritmi care nu pot fi ncadrai ntr-o anume categorie fr a
defini nite categorii fie foarte restrictive, fie foarte vagi. Acetia au, de
obicei, aplicabilitate n nite probleme practice foarte specifice. n cele ce
urmeaz vom prezenta civa astfel de algoritmi, care considerm c i
merit totui propria seciune, datorit eleganei acestora i datorit
aplicaiilor teoretice care pot fi gsite pentru acetia.
183
Capitolul 6
CUPRINS
6.1. Algoritmul K.M.P. (Knuth Morris Pratt) .......................................... 185
6.2. Evaluarea expresiilor matematice......................................................... 190
184
Algoritmi generali
Capitolul 6
Metoda clasic prezentat pn acum poate fi vizualizat n felul
urmtor, unde cu rou am marcat caracterele analizate (n ciclul cu j) care
nu se potrivesc, cu albastru cele care nu au fost parcurse n ciclul cu j, iar
cu verde caracterele care au fost analizate i se potrivesc. Cnd se potrivesc
toate caracterele irului S2 avem o soluie:
i
S1[i]
S2[j]
j
1
a
a
1
2
b
b
2
3
b
b
3
4
b
b
4
5
b
a
5
6
b
a
6
7 8 9 10 11 12 13 14 15 16
a b a a b b b a a b
b
7
5
b
b
4
6
b
a
5
7
a
a
6
8 9 10 11 12 13 14 15 16
b a a b b b a a b
b
7
Algoritmi generali
reprezint ns o optimizare netrivial, aa c l vom considera un algoritm
total distinct. n cadrul algoritmului naiv, primul pas conduce la urmtoarele
operaii:
p
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
Dac ne uitm atent la acest tabel, obervm c primul caracter al
ablonului (irul S1) se potrivete cu primul caracter (p) al textului cutat, al
doilea caracter al ablonului se potrivete cu al doilea caracter (p + 1) al
textului cutat .a.m.d. pn la al 5-lea caracter al ablonului, care se
potrivete cu caracterul p + 3 al textului cutat. Cu alte cuvinte, avem
potrivite q = 4 caractere ale textului cutat. tiind acest lucru, putem
determina urmtoarea poziie de la care putem avea o potrivire. Spunem c
p se numete poziia de nceput a unei poteniale potriviri (care poate se
va dovedi ca fiind potrivire sau nu). Se observ uor c, dac deplasm irul
cutat la poziia p = p + 1, primul caracter al textului cutat nu se va potrivi
cu al doilea caracter al ablonului, deoarece tim c acesta trebuie potrivit cu
al doilea caracter al textului cutat. Deplasarea la p = p + 4 n schimb va
conduce la o nou potenial potrivire:
p
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
n general, dac tim c prefixul irului cutat S2[1, q] se potrivete
cu secvena irului ablon S1 [p, p + q 1], trebuie s tim care este cea mai
mic deplasare p > p astfel nct s aib loc egalitatea
S2[1, k] = S1[p, p + k 1], unde p + k = p + q.
Deoarece la ultimul pas nu s-a potrivit niciun caracter, se va efectua
o deplasare cu o singur poziie, dup care se procedeaz similar pn cnd
se ajunge la o potrivire
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
Pentru a putea calcula deplasrile avem nevoie de calculul funciei
prefix (), unde [i] = lungimea celui mai lung prefix al irului S2 , care
este prefix al secvenei S2[1, i].
Mai mult, vom avea :{1, 2, ..., |S 2 |} {0, 1, ..., |S2 | 1}.
Funcia se calculeaz folosind valorile deja calculate. Evident,
[1] = 0. Vom considera o variabil k = 0. Parcurgem irul cutat de la
stnga la dreapta, ncepnd cu i = 2: dac primul caracter (k + 1) este egal
cu al doilea (i), [2] = 1, iar k se incrementeaz cu 1, deoarece primul
caracter reprezint un sufix de lungime 1 pentru secvena format din
primele 2 caractere. n caz contrar, [2] = 0, din motive evidente.
La al treilea caracter putem deja observa c este de ajuns s
verificm egalitatea dintre S2[k + 1] i S2[i], deoarece toate caracterele pn
la k reprezint un prefix de lungime maxim care este sufix al subsecvenei
formate din primele i 1 caracterele ale textului cutat, deci trebuie doar s
verificm dac putem extinde acest prefix. Aadar, dac aceste dou
caractere sunt egale, se incrementeaz k i [3] ia valoarea lui k.
Dac cele dou caractere nu sunt egale, nu trebuie s ncepem totul
de la zero, ci ne putem folosi de valorile deja calculate ale funciei pentru
a determina un prefix care poate reprezenta, extins cu un caracter, un sufix
de lungime maxim a secvenei curente. Acest lucru l vom face atribuindu-i
lui k valoarea [k] atta timp ct k > 0 i S2[k + 1] != S2[i]. Pentru exemplul
de mai sus, funcia prefix este urmtoarea:
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
Mai trebuie s vedem cum exact ne ajut aceste valori n
determinarea apriiilor textului cutat n textul ablon. Metoda este foarte
similar cu metoda folosit pentru a calcula funcia prefix.
Vom parcurge cu i textul ablon de la stnga la dreapta. Vom
considera k = 0. Dac S1[i] este egal cu S2[k + 1], l incrementm pe k.
Dac cele dou caractere sunt diferite ns, trebuie s vedem care este
urmtoarea poziie la care putem avea o potrivire, atribuindu-i lui k valoarea
[k] atta timp ct k > 0 i S2[k + 1] S1[i]. Dac la un pas k devine egal
cu |S2 |, am gsit o potrivire pe care trebuie s o numrm.
188
Algoritmi generali
#include <fstream>
#include <cstring>
using namespace std;
const int maxn = 100001;
int nr = 0, k = 0;
for ( int i = 1; S1[i]; ++i )
{
while ( k > 0 &&
S2[k + 1] != S1[i] )
k = pi[k];
if ( S2[k + 1] == S1[i] )
++k;
if ( S2[k + 1] == S2[i] )
++k;
if ( k == lgS2 )
++nr;
}
out << nr;
out.close();
pi[i] = k;
}
}
}
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();
KMP_potrivire(S1, S2, pi);
return 0;
}
Capitolul 6
190
Algoritmi generali
adic de nmulire i mprire. Aceast funcie va fi apelat de
ctre funcia anterioar, din motivul explicat mai sus.
3. Pentru a putea schimba prioritile naturale ale operatorilor,
avem la dispoziie paranteze. Putem considera o subexpresie
ncadrat ntre paranteze ca fiind la rndul ei o expresie, pentru
evaluarea crei vom porni un nou ciclu recursiv prin apelarea
funciei plus_minus. Vom avea aadar o funcie paran, care va
fi apelat de funcia inm_imp i care va verifica dac pe poziia
curent se gsete o parantez deschis: dac da, se sare peste
aceasta, se evalueaz paranteza apelnd funcia plus_min, se
sare peste paranteza nchis i se returneaz rezultatul evalurii
expresiei. Dac n schimb caracterul curent nu este o parantez
deschis, atunci acesta trebuie s fie un operand, adic o cifr,
care se returneaz i se trece la urmtoarea poziie.
Practic, ne putem imagina algoritmul ca o succesiune de ntrebri de
genul: la pasul curent, dac ne aflm pe un operator, putem aplica acest
operator operanzilor asociai lui, sau trebuie s verificm existena unor
paranteze i a operatorilor de prioritate mai mare?
S exemplificm algoritmul pe exemplul dat. Avem expresia
7*2/3+6-(2+1).
Parcurgem expresia de la stnga la dreapta. l reinem pe 7 la primul
pas. La al doilea pas ne aflm pe operatorul *, care se afl pe cel mai nalt
nivel de prioritate. Deoarece dupa * nu exist parantez, aplicm operatorul
operanzilor asociai i obinem 14. Procedm la fel pentru /, obinnd
14 / 3 = 4.66. Procedm la fel i pentru +, obinnd 10.66. Ajungem pe
operatorul , care este urmat de o parantez, pe care va trebui s o evalum
separat. Evaluarea parantezei ne d 3, care se scade din 10.66, obinnd
rezultatul final 7.66.
O prim metod (clasic) de implementare a acestei metode este
urmtoarea:
191
Capitolul 6
#include <fstream>
const int maxn = 1001;
using namespace std;
return ret;
}
int main()
{
char expr[maxn];
return ret;
ifstream in("expr.in");
in >> expr;
in.close();
}
// returnez operandul
return expr[k++] - '0';
}
ofstream out("expr.out");
out << plus_min(expr, k);
out.close();
return 0;
}
return ret;
}
192
Algoritmi generali
#include <fstream>
int main()
{
char expr[maxn];
ifstream in("expr.in");
in >> expr;
in.close();
int k = 0;
ofstream out("expr.out");
out << eval(expr, 0, k);
out.close();
return 0;
}
Capitolul 6
expresii (algoritmii recursivi de mai sus folosesc implicit aceast form).
Forma polonez postfixat a unei expresii este o modalitate de a scrie
expresia respectiva n aa fel nct s putem evalua expresia rezultat
printr-o simpl parcurgere a sa de la stnga la dreapta, fr a mai fi nevoii
s inem cont de paranteze i de prioritile operatorilor (care nu mai exist
n cadrul formei poloneze). n cadrul formei poloneze postfixate, un
operator este precedat de operanzii asociai acestuia (care pot fi la rndul lor
subexpresii).
De exemplu, dac avem expresia 2 + 3 * 2, forma sa polonez va fi
3 2 * 2 +, care se va evalua de la stnga la dreapta foarte uor: pentru
fiecare operator ntlnit, aplicm operatorul respectiv celor doi operanzi din
urma sa (ntotdeauna vor fi doi operanzi n urm) i nlocuim operatorul i
operanzii respectivi cu rezultatul operaiei. Pentru exemplul dat, dup
ntlnirea primului operator vom rmne cu 6 2 +, iar dup ntlnirea
ultimului operator vom rmne cu 8, care reprezint rezultatul final. Putem
implementa aceste operaii cu ajutorul unei stive.
Pentru a construi forma polonez a expresiei S, vom considera o
stiv st i un vector fpol n care vom forma rezultatul folosind urmtorul
algoritm, propus de Edsger Dijkstra:
Pentru fiecare i de la 1 pn la |S| execut
o Dac S[i] este un operand, se adaug n vectorul soluie
fpol.
o Dac S[i] este parantez deschis, se adaug n stiva st.
o Dac S[i] este parantez nchis, se scot toate elementele
din vrful stivei i scriu n vectorul fpol, pn la
ntlnirea unei paranteze deschise n st, parantez care se
scoate din stiv, dar nu se scrie n fpol.
o Dac S[i] este operator execut
Ct timp n vrful stivei se afl un operator de
prioritate mai mare sau egal dect S[i], se
scoate acest operator din stv i se trece n fpol.
Se scot din stiv toate elementele rmase i se trec n vectorul
fpol. Acesta va reprezenta forma polonez postfixat a expresiei.
Acest algoritm are avantajul de a fi iterativ i dezavantajul de a fi
mai complicat i mai greu de implementat corect. Varianta nerecursiv se
complic i mai mult dac avem de evaluat expresii mai complicate, care
pot conine i funcii i operatori cu proprieti diferite de ale operatorilor
elementari, cum ar fi factorialul (operator unar) i operatorul de ridicare la
putere (care trebuie evaluat de la dreapta spre stnga: 2^1^2, unde prin a^b
194
Algoritmi generali
2
st[k++] = expr[i];
}
while ( k - 1 >= 0 )
fpol[p++] = st[--k];
fpol[p] = '\0';
return st[0];
}
195
Capitolul 6
Exerciii:
a) Modificai variantele recursive astfel nct s construiasc ntr-un
vector dat ca parametru forma polonez postfixat a expresiei
evaluate.
b) Aceeai cerin pentru forma polonez prefixat. Forma polonez
prefixat i are toi operatorii urmai de operanzii asociai. De
exemplu, 2 + 3 * 2 + * 3 2 2.
c) Folosii o singur stiv att pentru formarea formei poloneze ct
i pentru evaluarea acesteia, n cadrul algoritmului iterativ.
d) Gsii un algoritm iterativ care construiete forma polonez
prefixat a unei expresii.
e) Implementai funciile sin i cos att n variantele recursive ct i
n varianta iterativ prezentat.
f) Considerai existena parantezelor drepte n cadrul expresiei date.
Modificai algoritmii dai astfel nct acestea s fie tratate ca
nsemnnd ridicarea la ptrat 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 dispoziia programatorilor C++ mai multe
structuri de date generice, lucru care scutete programatorii de timpul i
efortul implementrii acestor structuri de la zero. n aceeai bibliotec se
regsesc i diferii algoritmi care pot reduce timpul necesar rezolvrii unei
probleme.
Avantajul folosii bibliotecii S.T.L. const, n primul rnd, n
reducerea timpului necesar implementrii unui algoritm. n al doilea rnd,
aceste containere au fost implementate de o echip de profesioniti de-a
lungul unei perioade lungi de timp i testate foarte riguros, deci putem fi
siguri de corectitudinea i eficiena acestora.
n cele ce urmeaz vom prezenta pe scurt principalele containere i
algoritmi din S.T.L. i modul de folosire a acestora n nite situaii concrete.
Multe dintre metodele acestor containere sunt comune, deci
cunoaterea tuturor acestor structuri nu este un lucru greu de realizat.
Recomandm cititorului familiarizarea cu acestea, ntruct vor fi folosit mai
des n capitolele ce urmeaz.
Atenie: toate containerele prezentate de acum ncolo sunt, practic,
variabile. Cnd sunt transmise funciilor trebuie transmise prin referin ca
s poate suferi modificri. Chiar dac nu vrem s sufere modificri, este
preferabil transmiterea prin referin constant, pentru se a evita copierea
lor, lucru care poate scdea drastic performana programelor, mai ales n
cazul funciilor recursive.
197
Capitolul 7
CUPRINS
7.1. Containere secveniale .......................................................................... 199
7.2. Containere adaptoare ............................................................................ 205
7.3. Containere asociative ............................................................................ 210
7.4. Algoritmi S.T.L......................................................................................... 220
198
Introducere n S.T.L.
a) Containerul vector
Un vector este foarte similar cu un tablou unidimensional, n sensul
c reprezint o colecie de elemente de acelai tip stocate n locaii
consecutive de memorie. Vectorii rezolv dou mari probleme i surse de
erori pe care o au tablourile obinuite i anume:
- programatorul trebuie s se asigure c are ntotdeauna stocat
undeva dimensiunea fiecrui tablou.
- programatorul trebuie s se asigure c tablourile declarate au
dimensiuni suficient de mari.
Pentru a folosi un vector trebuie inclus fiierul antet <vector> i
folosit spaiul de nume std. Pentru a declara un vector se folosete sintaxa:
vector<T> aniPari;
199
Capitolul 7
Atenie: n cazul vectorilor, numerotarea ncepe de la 0, i este bine
s nu schimbm forat acest lucru, deoarece pot aprea erori.
O alt modalitate de parcurgere a unui vector este folosind iteratori.
Iteratorii sunt un fel de pointeri care pot simplifica uneori lucrul cu
containerele S.T.L. Exemplul urmtor parcurge acelai vector folosind
iteratori.
// declara un iterator pentru iterarea unui vector de intregi
vector<int>::iterator it;
for ( it = aniPari.begin(); it != aniPari.end(); ++it )
cout << *it << endl; // sintaxa similara cu pointerii
200
Introducere n S.T.L.
Cnd declarm un vector, putem s-l iniializm din start cu un alt
vector:
vector<int> aniPari;
for ( int i = 2002; i <= 2012; i += 2 )
aniPari.push_back(i);
vector<int> totiAnii(aniPari); // copiaza toti anii pari in vectorul
// tuturor anilor
Pentru a compara dac doi vectori sunt egali (au toate elementele de
pe pe poziii identice egale) putem folosi pur i simplu operatorul ==.
Acesta se poate aplica i altor containere.
Asupra vectorilor putem apela funcia sort din <algorithm> pentru a
sorta elementele acestuia:
sort(numere.begin(), numere.end());
b) Containerul deque
Un deque (Double-Ended Queue) este similar cu un vector,
diferenele fiind c un deque permite inserarea i tergerea elementelor de la
nceputul acestuia n timp constant, dar cu dezavantajul de a nu avea
elementele n locaii consecutive de memorie.
Pentru a declara un deque este necesar s includem antetul <deque>.
Sintaxa de declarare este exact ca cea pentru vectori:
deque<int> minusPlus;
201
Capitolul 7
Metodele push_back i pop_back sunt folosite pentru a aduga,
respectiv terge, elemente de la sfritul unui deque. Urmtoarea secven
adaug numere la sfritul containerului:
for ( int i = 1; i < 9; ++i )
minusPlus.push_back(i);
202
Introducere n S.T.L.
c) Containerul list
list este un container care are la baz o list dublu nlnuit. Acest
lucru nseamn c fiecare element are o locaie de memorie imprevizibil i
cte un pointer la elementul precedent i urmtor 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 cnd efectum multe inserri, mutri i tergeri de elemente din list.
Pentru a declara o list trebuie inclus fiierul antet <list>. Sintaxa de
declarare ar trebui s fie deja uor de intuit:
list<int> nrPrime;
203
Capitolul 7
Pentru a afia coninutul unei liste este obligatoriu s folosim
iteratori, deoarece listele nu suport accesul aleator la elemente.
Pentru a terge un element se folosete metoda erase, care primete
ca argument un iterator ctre elementul care trebuie ters. Secvena de mai
jos terge primul element al unei liste:
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 funcia remove.
Secvena de mai jos terge numrul 15 dintr-o list de numere prime, dac
acesta exist. Dac nu exist, nu se ntmpl nimic.
nrPrime.remove(15);
Introducere n S.T.L.
nrPrime.push_back(5); nrPrime.push_back(3); nrPrime.push_back(3);
nrPrime.push_back(2); nrPrime.push_back(5); nrPrime.push_back(7);
nrPrime.sort();
nrPrime.unique();
for ( list<int>::iterator it = nrPrime.begin(); it != nrPrime.end(); ++it )
cout << *it << " ";
Sau:
listaUnu.merge(listaDoi, comparator); // unde comparator este o functie
// booleana care compara
// elementele date ca parametri
a) Containerul stack
Un stack este o stiv, adic o structur de date care opereaz dup
principiul L.I.F.O. ultimul intrat, primul ieit. Stivele sunt folosite des n
viaa de zi cu zi. De exemplu, dup un examen fiecare student pune foile pe
catedr, ntr-o ordine oarecare. Dup ce fiecare student i-a predat foile,
profesorul corecteaz examenul ultimului student care a predat foile, adic
foile de examen din vrful stivei.
n aceste situaii au prioritate obiectele care au fost depuse mai trziu
n stiv.
205
Capitolul 7
Intrare n stiv
206
Introducere n S.T.L.
Programul anterior va afia:
Georgescu
Popescu
Ionescu
Pn n acest moment implementam stivele cu ajutorul tablourilor.
Containerul stack ne scutete de necesitatea gestionrii indicilor i a
dimensiunii, reducnd posibilitatea apariiei erorilor. Recomandm
rescrierea programelor prezentate pn acum i care folosesc stive cu
ajutorul containerului stack.
b) Containerul queue
Un queue este o coad, adic o structur de date care opereaz dup
principiul F.I.F.O. primul intrat, primul ieit. O astfel de coad se
ntlnete de exemplu la magazinele aglomerate. Fiecare cumprtor st i
i ateapt rndul la cas dup ce i-a terminat cumprturile. Primul care a
terminat este i primul care va plti i va putea pleca acas.
n aceste situaii au prioritate obiectele care au intrat mai devreme n
coad.
Intrare n coad
Ultimul intrat
...
Primul intrat
207
Capitolul 7
Pentru a accesa primul element al cozii (primul intrat n coad) se
folosete metoda front, iar pentru a accesa ultimul element al cozii (ultimul
intrat n coad) se folosete metoda back. De exemplu:
// afiseaza Vlad George
cout << cumparatori.front() << " " << cumparatori.back();
c) Containerul priority_queue
priority_queue este o coad de prioriti care are la baz un heap.
Cozile de prioriti suport interogarea valorii maxime din acestea (maxime
dup o anumit relaie de ordine) n timp constant, inserarea unui element n
timp logaritmic i tergerea valorii maxime tot n timp logaritmic.
Pentru a folosi o coad de prioriti trebuie inclus fiierul antet
<queue>. Sintaxa de declarare este:
priority_queue<int> note;
208
Introducere n S.T.L.
Putem ns s definim propriul criteriu de prioritizare al elementelor
scriind o clas (sau structur) care suprancarc operatorul (), operator care
primete doi parametri i returneaz true dac primul parametru are o
prioritate mai mic dect al doilea i false n caz contrar. Aceast clas se
folosete n declararea obiectului de tip priority_queue. Se va schimba
puin declararea containerului.
Exemplul de mai jos prioritizeaz elementele dup restul mpririi
la numrul 17. Elementele cu un rest mai mare vor avea prioritate mai mare.
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);
while ( note.size() > 0 )
{
cout << note.top() << " ";
note.pop();
}
return 0;
}
209
Capitolul 7
Acest container uureaz foarte mult implementarea algoritmului
Heapsort. Recomandm cititorilor s implementeze acest algoritm folosind
containerul priority_queue.
210
Introducere n S.T.L.
pair<set<int>::iterator, bool> rezultatSet;
multiset<int>::iterator rezultatMultiSet;
for ( int i = 0; i < 5; ++i )
{
rezultatSet = nrUnice.insert(rand() % 17);
rezultatMultiSet = nrMultiple.insert(rand() % 17);
if ( rezultatSet.second == false )
cout << "Numarul " << *rezultatSet.first
<< " exista deja in set." << endl;
else
cout << "Numarul " << *rezultatSet.first
<< " a fost inserat in set." << endl;
cout << "Numarul " << *rezultatMultiSet
<< " a fost inserat in multiset." << endl;
}
211
Capitolul 7
Pentru a terge un element din set se folosete metoda erase. Aceasta
primete ca parametru fie un iterator ctre elementul care trebuie ters, fie
valoarea acestuia. n cazul n care parametrul este valoarea care trebuie
tears, funcia returneaz numrul de elemente care au acea valoare i care
au fost terse (relevant doar n cazul multiseturilor, n cazul seturilor este
ntotdeauna 1).
Exemplul urmtor evideniaz funciile de tergere:
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)
Introducere n S.T.L.
Alte dou metode importante a seturilor sunt metodele lower_bound
i upper_bound. Ambele se execut n timp logaritmic i primesc o valoare
ca parametru. lower_bound returneaz un iterator ctre cea mai mare
valoare din set mai mic sau egal dect parametrul, iar cea din
upper_bound returneaz cea mai mic valoare din set strict mai mare dect
parametrul. Acestea sunt comune att seturilor ct i multiseturilor.
Exemplul urmtor evideniaz comportamentul acestor dou metode.
for ( int i = 1; i <= 10; ++i ) nrUnice.insert(i * 10);
set<int>::iterator low = nrUnice.lower_bound(20);
set<int>::iterator up = nrUnice.upper_bound(80);
set<int>::iterator saveUp = up;
cout << *low << endl; // afiseaza 30
cout << *up << endl; // afiseaza 90
while ( up != low ) // afiseaza descrescator numerele
// de la 90 la 30 (iterare inversa)
cout << *up-- << " ";
cout << endl;
nrUnice.erase(low, saveUp); // sterge numerele de la 30 la 80
// afiseaza 10 90 100
for ( low = nrUnice.begin(); low != nrUnice.end(); ++low )
cout << *low << " ";
213
Capitolul 7
set<int, cmp> numeSet; // modul de declarare
Introducere n S.T.L.
n cazul containerului multimap putem avea mai multe valori pentru
aceeai cheie. De exemplu, anumite persoane s-ar putea s aib mai multe
numere de telefon. Din acest motiv, multimap nu are implementat
operatorul [ ] i necesit folosirea metodelor insert i find. Aceste metode
sunt implementate i de ctre containerul map, dar folosite mai rar datorit
existenei operatorului [ ].
Metoda insert primete ca parametru elementul pe care vrem s-l
inserm. Reamintim c un element este o pereche (pair) format din cheie
i valoare. Valoarea returnat de funcie este, n cazul containerului map, o
pereche pair<iterator, bool> unde primul element este un iterator ctre
valoarea inserat i al doilea o valoare boolean care indic dac elementul a
fost inserat sau exista deja n colecie. n cazul containerului multimap, se
returneaz doar un iterator.
Metoda find returneaz un iterator ctre elementul care are cheia
dat ca parametru. Exemplul urmtor evideniaz aceste dou metode.
map<string, int> agenda;
multimap<string, int> agendaMulti;
agenda.insert(pair<string, int>("John Doe", 1352));
agendaMulti.insert(pair<string, int>("John Doe", 1352));
// afiseaza unicul numar de telefon al unei persoane
map<string, int>::iterator it1 = agenda.find("John Doe");
cout << it1->first << " are numarul de telefon "
<< it1->second << endl;
// insereaza mai multe numere de telefon pentru aceeasi persoana
agendaMulti.insert(pair<string, int>("John Doe", 6314));
agendaMulti.insert(pair<string, int>("John Doe", 4272));
// afiseaza toate numerele de telefon ale unei persoane
multimap<string, int>::iterator it2 = agendaMulti.find("John Doe");
cout << it2->first << " are numerele de telefon: ";
// afiseaza 1352 6314 4272
while ( it2 != agendaMulti.end() && it2->first == Ionescu Vlad )
{
cout << it2->second << " ";
++it2;
}
215
Capitolul 7
Pentru a terge elemente dintr-un map sau multimap se folosete
metoda erase. Aceast metod este similar cu cea de la seturi. Exist trei
versiuni: una care primete ca parametru un iterator ctre elementul pe care
vrem s-l tergem i care nu returneaz nimic, una care primete ca
parametru o cheie, terge toate elementele cu acea cheie i returneaz
numrul de elemente terse i una care primete ca parametru doi iteratori i
terge toate valorile dintre cei doi iteratori.
Exemplul urmtor prezint metoda erase.
map<string, int> agenda;
multimap<string, int> agendaMulti;
agenda.insert(pair<string, int>("John Doe", 1352));
agendaMulti.insert(pair<string, int>("John Doe", 1352));
agendaMulti.insert(pair<string, int>("John Doe", 6314));
agendaMulti.insert(pair<string, int>("John Doe", 4272));
// se va sterge un singur element
cout << agenda.erase("John Doe") << endl;
agenda.insert(pair<string, int>("John Doe", 1352));
map<string, int>::iterator it1 = agenda.find("John Doe");
agenda.erase(it1);
it1 = agenda.find("John Doe");
if ( it1 == agenda.end() )
cout << "John Doe nu exista in agenda" << endl; // se va afisa
// se sterg toate numerele
cout << agendaMulti.erase("John Doe") << endl;
agendaMulti.insert(pair<string, int>("John Doe", 1352));
agendaMulti.insert(pair<string, int>("John Doe", 6314));
agendaMulti.insert(pair<string, int>("John Doe", 4272));
multimap<string, int>::iterator it2 = agendaMulti.find("John Doe");
agendaMulti.erase(it2); // sterge doar primul numar: 1352
it2 = agendaMulti.find("John Doe");
// afiseaza 6314 4272
while ( it2 != agendaMulti.end() && it2->first == "John Doe" )
{
cout << it2->second << " ";
++it2;
}
216
Introducere n S.T.L.
Putem numra cte elemente au aceeai cheie (folositor n cazul unui
multimap) folosind metoda count, care primete ca parametru o cheie. De
exemplu:
cout << agendaMulti.count("John Doe") << endl;
217
Capitolul 7
Containerele map i multimap pot servi ca nlocuitori pentru tabele
de dispersie i arbori trie, dar trebuie inut cont de unele lucruri, cum ar fi
timpul de execuie, care este ntotdeauna logaritmic pentru operaiile de
cutare, inserare i tergere. Acest lucru poate reprezenta un dezavantaj n
faa tabelelor de dispersie, sau un avantaj dac dorim s evitm cel mai ru
caz al tabelelor de dispersie, caz n care operaiile de cutare i tergere
devin liniare. De obicei, map i multimap sunt mai puin eficiente dect un
trie implementat manual.
c) Containerul bitset
Containerul bitset ne permite s lucrm cu bii, lucru care poate
reduce semnificativ memoria folosit de un program care nu are nevoie
dect de un tablou a crui elemente poate lua doar dou valori: adevrat (1)
i fals (0). Aadar, fiecare element ocup un singur bit, spre deosebire de
tipurile bool sau char care ocup opt bii.
Pentru a folosi un set de bii trebuie inclus fiierul antet <bitset>.
Sintaxa de declarare este puin diferit fa de sintaxa containerelor
prezentate pn acum, n sensul c ntre parantezele unghiulare nu se mai
trece tipul datelor din container, ci numrul de bii pe care vrem s-l avem la
dispoziie. De exemplu:
bitset<2011> aniBisecti;
Introducere n S.T.L.
Putem lucru cu valoarea binar reinut ntr-un set de bii folosind
metodele to_ulong i to_string. Acestea transform biii dintr-un set de bii
ntr-o valoare numeric fr semn, respectiv ntr-un string. De exemplu:
bitset<5> test;
test.set(); // test = 11111 in baza 2
cout << test.to_ulong() << endl; // afiseaza 31 (11111 in baza 2)
test.flip(3); // test = 10111 in baza 2
string testString = test.to_string();
cout << testString << endl; // afiseaza 10111
Atenie: dac valoarea dintr-un set de bii este prea mare pentru a fi
reprezentabil pe un ntreg unsigned long, va aprea o eroare!
Alte metode importante sunt count, care numr ci bii au valoarea
1, any, care returneaz true dac exist un bit cu valoarea 1 i false altfel i
metoda none care returneaz true dac toi biii au valoarea 0 i false n caz
contrar.
Constructorul unui set de bii ne permite s iniializm un astfel de
set cu ajutorul unui ntreg sau a unui string. Obinem astfel o metod foarte
simpl i direct de a converti orice numr n baza doi:
bitset<32> numar(2010);
// afiseaza 00000000000000000000011111011010
cout << numar.to_string() << endl;
219
Capitolul 7
// afiseaza 11111111111111111111110000010010
cout << numar.to_string() << endl;
numar ^= 31;
// afiseaza 11111111111111111111110000001101
cout << numar.to_string() << endl;
a) Algoritmul for_each
Practic, for_each este o funcie cu ajutorul creia putem aplica o alt
funcie asupra unor anumite elemente (identificate prin doi iteratori) ale unui
container. Funcia aplicat trebuie s accepte un singur parametru de tipul
elementelor din containerul asupra cruia se va aplica. Dac funcia
returneaz ceva, valoarea returnat va fi ignorat.
Exemplul urmtor afieaz dublul elementelor unui vector folosind
aceast metod.
void Afisare(int x) { cout << 2*x << " "; }
int main()
{
vector<int> numere;
numere.push_back(1005); numere.push_back(13);
numere.push_back(9); numere.push_back(4);
// afiseaza 2010 26 18 8
for_each(numere.begin(), numere.end(), Afisare);
return 0;
}
220
Introducere n S.T.L.
Nu este obligatoriu s aplicm funcia asupra unui container S.T.L.
Putem folosi i un tablou clasic:
int numere[4] = {1005, 13, 9, 4};
for_each(numere, numere + 4, Afisare); // afiseaza tot 2010 26 18 8
221
Capitolul 7
d) Algoritmul equal
Compar elementele a dou secvene aparinnd a dou containere
distincte (doi vectori de exemplu) i returneaz true dac cele dou secvene
sunt egale i false n caz contrar.
Funcia accept fie trei parametri fie patru: prim1 un iterator ctre
primul element din prima secven, ultim1 un iterator ctre primul
element care nu va fi inclus n comparaie, prim2, un iterator ctre primul
element din a doua secven i un parametru opional predicat care
reprezint o funcie cu dou argumente conform creia se va testa egalitatea.
Informal, se va compara secvena [prim1, ultim1) cu [prim2, prim2 +
ultim1 prim1).
Exemplul urmtor testeaz dac dou tablouri sunt congruente
modulo 17.
bool EgalMod17(int x, int y) { return x % 17 == y % 17; }
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
Funcia unique terge toate repetiiile unui element dintr-o colecie
sortat. Aceasta primete doi parametri care reprezint graniele n care se
va aplica funcia (iteratori sau pointeri). Se mai poate transmite un
parametru opional: o funcie cu doi parametri care determin dac dou
222
Introducere n S.T.L.
elemente sunt egale, funcie similar cu cea de la funcia equal. Funcia
returneaz un iterator ctre noul sfrit al coleciei.
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
// numere va contine doar 0 1 2 3 ... 10
vector<int>::iterator it = unique(numere.begin(), numere.end());
// vectorul trebuie redimensionat, altfel
// vor aparea elementele duplicate la sfarsit
numere.resize(it - numere.begin());
for ( it = numere.begin(); it != numere.end(); ++it )
cout << *it << " ";
return 0;
}
f) Algoritmul copy
Funcia copy copiaz o colecie n alta. Primii doi parametri sunt
iteratori care definesc prima secven, iar al treilea parametru este un iterator
ctre poziia n care se va copia primul element. De exemplu:
int numere1[5] = {1, 2, 3, 4, 5};
int numere2[5];
copy(numere1, numere1 + 5, numere2); // copiaza numere1 in numere2
for ( int i = 0; i < 5; ++i ) // afiseaza 1 2 3 4 5
cout << numere2[i] << " ";
223
Capitolul 7
g) Algoritmul reverse
Funcia reverse oglindete o secven dat prin doi iteratori. Aceasta
nu returneaz numic. Exemplul urmtor determin dac un vector este
palindrom:
vector<int> vec, vecInit;
vec.push_back(5); vec.push_back(10); vec.push_back(5);
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
Funcia rotate primete trei parametri: prim, mij, ult i rotete
secvena [prim, ult) n aa fel nct 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);
rotate(numere.begin(), numere.begin() + 5, numere.end());
for ( int i = 0; i < 10; ++i ) // afiseaza 6 7 8 9 10 1 2 3 4 5
cout << numere[i] << " ";
i) Algoritmul random_shuffle
Permut aleator elementelor unei colecii. De exemplu:
int nrPrime[5] = {2, 3, 5, 7, 11};
random_shuffle(nrPrime, nrPrime + 5);
224
Introducere n S.T.L.
Capitolul 7
226
Algoritmi genetici
8. Algoritmi
genetici
Pentru a nelege algoritmii genetici, n primul rnd trebuie s
nelegem i s cuantificm modelul evoluiei naturale (darwiniste). Modelul
evolutiv presupune existena unui habitat (a unui spaiu de evoluie)
guvernat de legi locale (condiiile de mediu) n care speciile (populaiile
reprezentate de indivizi) se supun urmtorului mecanism:
1. Pe baza seleciei, un numr restrns de indivizi din populaia
iniial vor constitui populaia intermediar de prini (algoritmul
de selecie trebuie s respecte paradigma conform creia un
individ mai bine adaptat s aibe anse mai mari de supravieuire).
2. Din indivizii selectai ca i prini, pe baza operatorilor genetici
(mutaie, ncruciare, ...), se va reconstitui o nou populaie.
Capitolul 8
CUPRINS
8.1. Descrierea algoritmilor genetici ............................................................ 229
8.2. Problema gsirii unei expresii ............................................................... 236
8.3. Rezolvarea sistemelor de ecuaii .......................................................... 241
228
Algoritmi genetici
Capitolul 8
Algoritmi genetici
Fig. 8.1.2.
Acest mod se poate realiza cel mai eficient prin procesarea mai
multor posibile soluii simultan, cu diferite puncte de plecare aleatoare.
Conceptul asociat cu mbinarea celor dou metode de cutare n
spaiul soluiilor se numete echilibrul explorare exploatare i tratarea
celor dou concepte simultan reprezint principalul avantaj al algoritmilor
genetici relativ la celelalte metode de optimizare.
Capitolul 8
6. Se verific condiiile de oprire n funcie de tEvol = tEvolMAX
sau dac s-a gsit o soluie acceptabil
a. Dac da algoritmul se ncheie
b. Altfel se revine la pasul 3
Paii 3 i 4 reprezint nucleul algoritmului genetic.
c) Selecia
Exist mai multe tipuri de selecie, toate acestea avnd scopul ca
implementarea capacitii de supravieuire a unei soluii s fie proporional
cu valoarea funciei de adecvare, aici fiind de fapt implementat paradigma
evoluiei darwiniste survival of the fittest. Una dintre cele mai simple
metode de selecie este selecia bazat pe ordonare (ierarhie), n care se
ordoneaz populaia de soluii astfel nct adaptarea lor s fie
descresctoare, dup care se selecteaz primii n indivizi dorii.
Metoda cea mai natural de selecie este metoda de selecie Monte
Carlo (proporional). Aceast metod presupune construirea unei rulete,
fiecare individ din populaie fiind reprezentat sub forma unui sector de cerc
proporional cu o pondere. Pentru a avea sens din punct de vedere evolutiv,
ponderea trebuie s fie cu att mai mare cu ct adecvarea individului soluie
este mai bun. n figura 8.1.3. avem o populaie format din cinci indivizi i
adaptarea cea mai bun o are crom5.
Algoritmi genetici
reprezentate de numrul de puncte, iar ponderile (de apariie a unei fee) ar
trebui s fie identice (1). Astfel avem matricea de intrare:
1 2
1 1
3
1
4
1
5
1
6
1
233
Capitolul 8
Exist mai multe tipuri de selecie, pe lng cele dou amintite
anterior. Avantajele, dezavantajele, modul lor de implementare i
particularitile acestora le vom trata ntr-un manual dedicat inteligenei
artificiale.
d) Operatorii genetici
Pentru a continua construcia unui algoritm genetic funcional, avem
nevoie de o modalitate de generare a soluiilor-copii din populaia de
soluii-prini. Aceasta se realizeaz prin operatorii genetici. Exist doi
operatori genetici fundamentali: mutaia (notat n continuare opM),
respectiv ncruciarea (opI). Avem:
i
Se observ c mutaia este un operator unar i acioneaz prin
schimbarea uneia sau a mai multor valori din cromozomul printe: n forma
cea mai simpl fie = (0 , 1 , , 1 ) un cromozom cu valorile
0 , 1 , , 1. Atunci un operator de mutaie ar genera o valoare aleatoare t
ntre 0 i n 1, iar valoarea corespunztoare lui t ar fi schimbat cu o alt
valoare.
, 0 , 1 , , 1 , , +1 , , 1
S considerm cromozomul 1110001010011111, n codificarea
binar. pentru a obine diversitatea populaiei este necesar s inem cont c
vom modifica n 1 bitul ales aleator dac valoarea acestuia este 0, respectiv
n 0 dac aceast valoare este 1.
1110001010011111 4, 1 0, 0 1 11101010011111
Toate variantele operatorilor de mutaie au ca scop diversificarea
populaiei, n efect contrar cu selecia.
Operatorul de ncruciare este un operator binar i are ca scop
schimbarea valorilor existente ntre cromzomii prini. n forma cea mai
simpl (numit ncruciarea cu un punct de tietur) se genereaz un
234
Algoritmi genetici
numr aleator t, acesta reprezentnd punctul n care se rup cei doi
cromozomi i se recombin.
= 0 , 1 , , 1
= 0 , 1 , , 1
0, 1 , , 1 , , +1 , 1
sau
0 , 1 , , 1 , , +1 , 1
Forma cea mai ntlnit a operatorului de ncruciare este aceea n
care se genereaz un ir de numere aleatoare (t i), care vor reprezenta
punctele de tietur:
= 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 ,
Capitolul 8
236
Algoritmi genetici
Gndii-v ce se ntmpl dac generm un ir de numere a crui
rezultat este foarte aproape de S. Asta nseamn c irul respectiv ar putea fi
o soluie valid, cu mici modificri. n cazul algoritmului anterior ns, acest
ir se va pierde la pasul urmtor, generndu-se alt ir, care va avea mai
multe anse s fie mai ndeprtat de soluie dect mai apropiat ca irul
curent
Ideea din spatele algoritmilor genetici este s reinem mai multe
astfel de iruri (o populaie sau ecosistem) generate aleator, pe care s le
sortm dup o funcie de adecvare (funcie de fitness) care ia valori tot mai
apropiate de 0 pentru iruri (indivizi sau cromozomi) care tind spre o
soluie. Cnd am gsit un ir pentru care funcia de adecvare ia exact
valoarea 0, am gsit o soluie.
Algoritmul nu se oprete ns la sortarea unor iruri generate aleator.
Vom genera un anumit numr de iruri o singur dat, dup care vom aplica
anumii operatori genetici asupra lor. Aceti operatori asigur faptul c
informaia dintr-o generaie nu se va pierde n generaiile urmtoare. O
generaie este o stare a populaiei la un moment dat.
Se pune problema alegerii indivizilor asupra crora vom aplica
operatorii genetici i alegerii indivizilor a cror informaie dorim s o
pstrm i n generaia urmtoare. Evident, dac un individ a fost foarte
aproape de soluie ntr-o generaie, acesta va merita pstrat aa cum e i n
generaia viitoare. Vom menine o list cu elite pentru fiecare generaie,
elite care vor trece nemodificate n generaia urmtoare. Operatorii genetici
se vor aplica asupra elitelor, combinnd calitile acestora n sperana
obinerii unor soluii din ce n ce mai bune.
Operatorii genetici se aplic, fiecare, cu o anumit probabilitate, n
funcie de necesitatea aplicrii lor.
Operatorii cei mai des ntlnii sunt operatorii de recombinare i de
mutaie. Operatorul de recombinare combin informaia reinut de doi
cromozomi A i B ce fac parte din elite ntr-un singur cromozom ce va face
parte din generaia urmtoare. Modul de desfurare al operaiei este similar
cu procedeul biologic: se alege un punct (o gen) oarecare P de pe unul
dintre cei doi cromozomi din elite. Cromozomul C rezultat prin recombinare
va avea primele P gene identice cu primele P gene ale cromozomului A, iar
urmtoarele gene identice cu genele de dup poziia P a cromozomului B
Pentru problema de fa, lucrnd pe exemplul dat, recombinarea s-ar putea
face astfel:
237
Capitolul 8
Cromozom
Informaie
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
Operatorul de mutaie modific aleator valoarea unei gene alese tot
aleator. n cazul problemei de fa, operatorul de mutaie trebuie s fie
implementat n aa fel nct s nu modifice valoarea unui operator.
Exemplu:
nainte de mutaie
Dup mutaie
4 * 3 * 1 + 2 4 * 4 * 1 + 2
Funcie de adecvare este, n acest caz, foarte simplu de construit.
Aceasta va calcula, pentru fiecare cromozom, diferena n modul dintre
suma S i valoarea expresiei reinute de cromozomul curent.
Astfel, algoritmul de rezolvare este urmtorul:
Iniializeaz aleator maxpop cromozomi / indivizi.
Execut:
o Creaz o nou populaie aplicnd operatorii de
recombinare i de mutaie (fiecare cu probabiliti
prestabilite).
o Sorteaz indivizii cresctor dup funcia de adecvare.
Ct timp valoarea funciei de adecvare pentru primul cromozom
este diferit de 0.
Afieaz operanzii primului cromozom.
Pentru mai multe detalii despre funcia de evaluare a unei expresii,
vedei capitolul Algoritmi generali.
#include <fstream>
#include <algorithm>
#include <cstdlib>
using namespace std;
238
Algoritmi genetici
const int maxN = 1001;
const int maxpop = 400;
const int maxlg = 2*maxN + 1;
const int maxeli = 50;
const int prob_recomb =
(int)((double)0.80 * RAND_MAX);
const int prob_mutatie =
(int)((double)0.95 * RAND_MAX);
const int mod = 16381;
// <EVALUARE>
int paran(int &k, int cr, info
A[])
{
return A[cr].P[k++];
}
int inm(int &k, int cr, info A[])
{
int ret = paran(k, cr, A);
struct info
{
int P[maxlg], fitness;
};
while ( A[cr].P[k] == -3 )
{
++k;
239
Capitolul 8
void calc_fitness(int N, int S, info A[])
{
for ( int cr = 1; cr < maxpop; ++cr )
{
int k = 1;
A[cr].fitness = abs(eval(k, cr, A) - S);
}
sort(A+1, A+maxpop);
}
void noua_gen(int N, info A[])
{
for ( int i = maxeli + 1; i < maxpop; ++i )
{
if ( rand() < prob_recomb ) // recombinare
{
int i1, i2;
do
{
i1 = 1 + rand() % maxeli;
i2 = 1 + rand() % maxeli;
} while ( i1 == i2 );
int poz;
do
{
poz = 1 + (rand() % (2*N - 1));
} while ( poz % 2 == 0 );
for ( int j = 1; j < poz; j += 2 )
A[i].P[j] = A[i1].P[j];
for ( int j = poz; j < 2*N; j += 2 )
A[i].P[j] = A[i2].P[j];
}
if ( rand() < prob_mutatie ) // mutatie
{
int poz;
do
{
poz = 1 + (rand() % (2*N - 1));
} while ( poz % 2 == 0 );
A[i].P[poz] = 1 + (rand() % N);
}
}
}
240
Algoritmi genetici
Exerciii:
a) Comparai performana algoritmului cu performana celorlali doi
algoritmi menionai.
b) Cum afecteaz constantele de la nceputul programului timpul de
execuie i memoria folosit?
c) Cum am putea modifica operatorii genetici dac numerele
folosite n expresie ar trebui s fie distincte?
241
Capitolul 8
verific toate ecuaiile date, am gsit o soluie a sistemului i putem opri
cutarea.
Aceast metod are avantajul de a fi relativ uor de implementat i
de a gsi rapid o soluie pentru un sistem oarecare. Alt avantaj este
posibilitatea gsirii tuturor soluiilor unui sistem.
Dezavantajele acestei metode constau n eficien. Complexitatea
asimptotic va fi ntotdeauna O(N!) deoarece trebuie s generm toate
permutrile. Totui, exist optimizri care pot face ca algoritmul s ruleze
foarte rapid n practic. Cteva astfel de optimizri sunt:
Sortarea ecuaiilor dup numrul de necunoscute care apar n
acestea i rezolvarea ecuaiilor cu numr mai mic de variabile
mai nti.
Verificarea ecuaiilor nainte de generarea unei permutri ntregi,
fapt ce ne poate ajuta s respingem o permutare mai devreme.
Diverse optimizri legate de modul de generare al permutrilor.
Aceste optimizri nu garanteaz ns ntotdeauna o mbuntire i
pot fi dificil de implementat.
Problema se poate rezolva mai eficient folosind algoritmi genetici.
Deoarece se cere o singur permutare care s verifice anumite constrngeri
(ecuaiile sistemului), putem ncepe cu un numr prestabilit (populaia) de
permutri generate aleator (indivizi), pe care vom aplica apoi anumii
operatori genetici i pe care le vom sorta dup o funcie de adecvare.
Procedeul se repet pn cnd se ajunge la o soluie.
Corectitudinea i eficiena acestei metode st aadar n alegerea
operatorilor genetici i a funciei de adecvare (fitness).
Propunem urmtoarele dou funcii de adecvare:
1. Prima funcie, F1 , calculeaz, pentru fiecare individ, numrul de
ecuaii ale sistemului pe care permutarea le verific. Evident, am
gsit o soluie atunci cnd exist un individ X pentru care
F1(X) = M.
2. A doua funcie, F2, calculeaz, pentru fiecare individ,
=1
Algoritmi genetici
Ambele funcii de adecvare se comport similar din punct de vedere
al timpului de execuie. Acelai lucru nu poate fi spus ns i despre
operatorii genetici.
Primul lucru care trebuie observat este c nu putem pstra modelul
clasic al algoritmilor genetici, deoarece nu putem folosi nici operatorul de
recombinare (n caz contrar am genera permutri invalide, cu elemente care
se repet), nici operatorul clasic de mutaie (din acelai motiv).
O prim idee ar fi s folosim un operator de inversare: alegem
aleator dou poziii x1 i x2 , cu 1 x1 < x2 N i inversm secvena cuprins
ntre x1 i x2. Acest lucru ncalc ns ideea principal din spatele
algoritmilor genetici: pstrarea unor trsaturi ale elitelor din generaia
curent pentru a mbunti generaiile urmtoare. Folosind operatorul de
inversare, se pierde informaia din generaia curent.
Propunem urmtorul operator genetic, similar cu operatorul de
recombinare: se alege un individ oarecare din elitele generaiei precedente,
din care se copiaz primele x gene n cromozomul curent. Urmtoarele gene
se completeaz aleator, avnd grij s nu avem dou gene (o gen
reprezint, practic, un element al permutrii) identice. Astfel, generaiile
urmtoare au anse mai mari s fie mai aproape de rezolvarea problemei
dect generaia curent, iar mbuntirea timpului de execuie este evident
pentru un volum mai mare al datelor de intrare.
Putem folosi i operatorul de mutaie, dar i acesta trebuie modificat
pentru necesitile problemei. Mutaia nu va mai avea loc asupra unei
singure gene, ci asupra a dou gene. Vom alege dou gene pe care le vom
interschimba, pstrnd astfel proprietatea de permutare.
Structura de baz a algoritmilor genetici rmne la fel. n consecin,
prezentm doar acele funcii care sufer modificri.
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]);
}
}
}
9. Algoritmi de
programare
dinamic
Am prezentat ntr-un capitol anterior noiunile de baz ale metodei
programrii dinamice. n acelai capitol am prezentat cteva probleme
elementare rezolvate, urmnd n acest capitol s prezentm mai multe
aplicaii, att clasice ct i mai avansate, ale programrii dinamice. Tot aici
vom face tranziia de la implementrile mai apropiate de limbajul C folosite
pn acum la implementri C++ care profit mai mult de avantajele oferite
de limbajul C++, cum ar fi librria S.T.L.
245
Capitolul 9
CUPRINS
9.1. Problema labirintului algoritmul lui Lee ............................................ 247
9.2. Problema subsecvenei de sum maxim ............................................ 258
9.3. Problema subirului cresctor maximal ............................................... 262
9.4. Problema celui mai lung subir comun ................................................. 269
9.5. Problema nmulirii optime a matricelor .............................................. 273
9.6. Problema rucsacului 1............................................................................ 276
9.7. Problema rucsacului 2............................................................................ 279
9.8. Problema plii unei sume 1.................................................................. 280
9.9. Problema plii unei sume 2.................................................................. 283
9.10. Numrarea partiiilor unui numr ...................................................... 284
9.11. Distana Levenshtein ........................................................................... 286
9.12. Determinarea strategiei optime ntr-un joc ....................................... 289
9.13. Problema R.M.Q. (Range Minimum Query) ....................................... 292
9.14. Numrarea parantezrilor booleane .................................................. 296
9.15. Concluzii ................................................................................................ 300
246
lee.out
6
11
21
31
32
33
34
44
247
Capitolul 9
D[p][q] = 1+min(D[p 1][q],D[p + 1][q],D[p][q 1],D[p][q + 1]),
unde D[x][y] reprezint distana minim de la (1, 1) la (x, y). Pentru indici
invalizi sau care reprezint un zid, distana minim va fi infinit.
Exist mai multe metode de implementare a acestui algoritm. Fie A
matricea dat. Prima metod reprezint implementarea relaiei de recuren
exact aa cum este dat, cu ajutorul unei funcii recursive. Vom folosi pentru
aceast metod o matrice D cu semnificaia anterioar i o funcie recursiv
Lee(A, N, x, y, D) care va construi aceast matrice. Vom iniializa D[1][1]
cu 0, iar restul matricei cu infinit, semnificnd faptul c acele valori nu au
fost calculate nc.
Funcia Lee poate fi implementat astfel:
Pentru fiecare vecin (newx, newy) al lui (x, y) execut
o Dac (newx, newy) este o poziie valid, nu reprezint un
zid i D[newx][newy] > D[x][y] + 1 execut
D[newx][newy] = D[x][y] + 1
Apel recursiv Lee(A, N, newx, newy, D)
Dup apelul iniial lee(A, N, 1, 1, D), matricea D va fi calculat
conform definiiei sale, deci D[N][N] va conine distana minim.
Dei aceast metod este cel mai uor de implementat, nu este cea
mai eficient, deoarece funcia lee poate fi apelat de mai multe ori pentru
aceeai poziie. Pentru a evidenia acest lucru vom prezenta modul de
execuie al funciei de mai sus pe exemplul dat. Vom considera c vectorii
de direcie (acest concept a fost definit la seciunea dedicat metodei
backtracking) sunt:
dx[] = {1, 0, -1, 0};
dy[] = {0, 1, 0, -1};
Iniial avem:
A
0111
0100
0000
1110
D
0
D
0
1
2
Primul vecin al poziiei (3, 1), este (4, 1), poziia invalid deoarece
conine un zid. Al doilea vecin este poziia (3, 2), poziie valid. Funcia se
autoapeleaz pentru aceast poziie i obinem:
A
0111
0100
0000
1110
D
0
1
23
Din poziia (3, 2) prima dat se ncearc vecinul (4, 2), care este ns
un zid. Se va merge n continuare la stnga nc doi pai, pn obinem:
A
0111
0100
0000
1110
D
0
1
23 4 5
Din poziia (3, 4) funcia se va apela prima dat pentru poziia (4, 4),
obinndu-se urmtoarea configuraie:
A
0111
0100
0000
1110
D
0
1
2 3 4 5
6
249
Capitolul 9
A
0111
0100
0000
1110
D
0
16
2 3 4 5
6
Singurul apel recursiv valid din poziia (2, 4) este pentru poziia (2,
3), obinndu-se:
A
0111
0100
0000
1110
D
0
1 7 6
2 3 4 5
6
D
0
1 5 6
2 3 4 5
6
250
in.close();
}
int main()
{
int N;
bool A[maxn][maxn];
int D[maxn][maxn];
citire(A, N);
init(D, N);
Lee(A, N, 1, 1, D);
ofstream out("lee.out");
out << D[N][N] << '\n';
out.close();
return 0;
}
Capitolul 9
ntlnete un obstacol i abia apoi se revine la un pas anterior sau se schimb
direcia. Parcurgerile grafurilor vor fi prezentate ntr-un alt capitol, aa c nu
vom detalia aici parcurgerea n lime. Ideea de baz este s verificm la
fiecare pas toi vecinii poziiei curente, iar apoi toi vecinii acestora i aa
mai departe, pn cnd se parcurgere ntreaga matrice. Datorit faptului c
vom parcurge matricea uniform, fiecare element va fi analizat o singur
dat.
Ideea de baz rmne aceeai, diferind doar implementarea. Pentru a
putea implementa parcurgerea descris anterior vom folosi o structur de
date numit coad F.I.F.O. Aceast structur de date a fost descris n
capitolul Introduce n S.T.L.
Vom prezenta n continuare noul algoritm n pseudocod. Notaiile
rmn aceleai, iar Q reprezint coada F.I.F.O. folosit, p reprezint poziia
primului element din coada, iar u poziia ultimului element din coad:
p=u=1
Q[p] = (1, 1)
Ct timp p u execut
o (x, y) = Q[p++]
o Pentru fiecare vecin (newx, newy) al lui (x, y) execut
Dac (newx, newy) este o poziie valid, nu
reprezint un zid i D[newx][newy] > D[x][y] + 1
execut
D[newx][newy] = D[x][y] + 1
Q[++u] = (newx, newy)
Datorit modului n care parcurgem matricea, toate drumurile
posibile vor fi parcurse n acelai timp, deci nu va exista posibilitatea
completrii unei pri a matricei D cu valori care vor trebui ulterior
corectate, aa cum a fost cazul n implementarea iniial. Pentru a evidenia
acest lucru vom prezenta modul de execuie al algoritmului pe exemplul dat.
Iniial avem:
A
0111
0100
0000
1110
p, u
Q: (1, 1)
D
0
252
D
0
1
2
p, u
Q: (1, 1) (2,1) (3,1)
Similar, singurul vecin valid al poziiei Q[p] = (3, 1) este (3, 2), care
se va introduce n coad i se va actualiza. La urmtorul pas se va extrage
(3, 2) din coad i se va introduce singurul vecin valid al acestei poziii,
(3, 3):
A
0111
0100
0000
1110
D
0
1
2 34
p, u
Q: (1, 1) (2,1) (3,1) (3, 2) (3, 3)
Se extrage (3, 3) din Q. Poziia (3, 3) are doi vecini valizi: (3, 4) i
(2, 3), care se actualizeaz i se introduc amndoi n coad:
253
Capitolul 9
A
0111
0100
0000
1110
D
0
15
2 34 5
p
u
Q: (1, 1) (2,1) (3,1) (3, 2) (3, 3) (3, 4) (2, 3)
Se extrage elementul (3, 4), care va actualiza poziiile (4, 4) i (2, 4)
i le va introduce n coad. Se observ c dup acest pas matricea este deja
completat corect.
A
0111
0100
0000
1110
D
0
15 6
2 34 5
6
p
u
(1,
1)
(2,1)
(3,1)
(3,
2)
(3,
3)
(3,
4)
(2,
3)
(4,
4)
(2,
4)
Q:
Dac ne intereseaz doar poziia (N, N), putem returna D[N][N]
imediat ce aceast valoare a fost calculat. Dac ne intereseaz ntreaga
matrice D, algoritmul trebuie continuat pn cnd p devine mai mare dect
u.
Datorit faptului c am introdus fiecare poziie a matricei (care nu
reprezint un zid) n coad exact o singur dat i pentru c am evitat
recursivitatea, timpul de execuie al acestei implementri este cu mult mai
bun dect cel al implementrii recursive.
Pentru a determina coordonatele care alctuiesc traseul vom folosi o
funcie recursiv drum(x, y). Observm c dac D[x][y] == k (k > 0,
k != ) atunci poziia imediat anterioar lui (x, y) n cadrul drumului minim
este acel vecin (p, q) al lui (x, y) pentru care D[p][q] == k 1. Dac
D[x][y] este 0, atunci (x, y) == (1, 1); aceast condiie este chiar condiia de
ieire din recursivitate. Aadar funcia drum(x, y) poate fi scris astfel:
Dac D[x][y] == 0 afieaz (1, 1) i oprete execuia
Caut un singur vecin (p, q) al lui (x, y) pentru care
D[p][q] == D[x][y] 1
Apel recursiv drum(p, q)
Afieaz (x, y)
254
Capitolul 9
void Lee(bool A[maxn][maxn], int N, int D[maxn][maxn])
{
pereche Q[maxn*maxn];
int p = 1, u = 1;
Q[p].x = 1, Q[p].y = 1;
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];
if ( valid(N, newx, newy) )
if ( !A[newx][newy] && D[newx][newy] > D[poz.x][poz.y]+1 )
{
D[newx][newy] = D[poz.x][poz.y] + 1;
// adaugarea vecinului in coada
++u;
Q[u].x = newx;
Q[u].y = newy;
}
}
}
}
Capitolul 9
subsecv.out
11
258
259
Capitolul 9
Aceast implementare este deja relativ eficient pentru vectori cu un
numr de elemente de pn la ordinul miilor, spre deosebire de prima
implementare care este aplicabil doar pentru un numr de elemente de
ordinul sutelor. Putem obine ns o rezolvare i mai eficient, care
funcioneaz rapid pe vectori cu sute de mii sau chiar milioane de elemente.
Cea de-a treia metod folosete paradigma programrii dinamice
pentru a obine o rezolvare n O(N). Fie S[i] = suma maxim a unei
subsecvene care se termin cu elementul i. S presupunem c, pentru un
anume 1 k < N, cunoatem valoarea lui S[k]. Ne intereseaz s-l obinem
pe S[k + 1] din S[k]. Observm c avem dou posibiliti:
1. Adugm elementul k + 1 la sfritul subsecvenei de sum
maxim care se termin cu elementul k, obinnd o subsecven
de sum A[k + 1] + S[k].
2. Ignorm subsecvena de sum maxim care se termin cu
elementul k i considerm subsecvena format doar din
elementul k + 1, aceasta avnd suma A[k + 1].
Evident vom alege maximul sumelor aferente celor dou cazuri.
Aadar, obinem urmtoarea formul de recuren:
S[k + 1] = max(A[k + 1] + S[k], A[k + 1]).
Singura iniializare care trebuie fcut este S[1] = A[1]. Rspunsul
problemei este dat de valoarea maxim din S.
n implementarea prezentat am folosit un vector S pentru
implementarea recurenei. Deoarece pentru calculul lui S[k + 1] avem
nevoie doar de S[k], n loc de vectorul S putem folosi doar nite variabile.
Aceast ultim optimizare este lsat ca exerciiu pentru cititor.
S evideniem modul de execuie al algoritmului pe exemplul dat.
Iniial avem:
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
Pentru S[2] lum maximul dintre S[1] + A[2] i A[2].
S[1] + A[2] = -5, iar A[2] = 1. Maximul este aadar A[2] = 1:
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
Exerciii:
a) Modificai implementrile date pentru a afia i poziiile de
nceput i de sfrit a unei subsecvene de sum maxim.
b) Se cere o subsecven de produs maxim, iar numerele sunt reale.
Rezolvai problema att pentru numere strict pozitive ct 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
1
6
1
0
2
3
1
0
3
8
1
0
4
9
1
0
5
1
1
0
6 7 8 9 10
2 10 4 -1 11
1 1 1 1 1
0 0 0 0 0
1
6
1
0
2
3
1
0
3
8
2
1
4
9
1
0
5
1
1
0
263
6 7 8 9 10
2 10 4 -1 11
1 1 1 1 1
0 0 0 0 0
Capitolul 9
Se procedeaz n acest fel pn la completarea vectorilor L i P.
Forma lor final este prezentat mai jos. Este uor de verificat corectitudinea
calculrii acestor vectori conform definiiei lor.
i
A[i]
L[i]
P[i]
1
6
1
0
2
3
1
0
3
8
2
1
4
9
3
3
5
1
1
0
6 7 8 9 10
2 10 4 -1 11
2 4 3 1 5
5 4 6 0 7
264
int main()
{
int N;
int A[maxn], L[maxn], P[maxn];
int sol = 1;
for ( int i = 2; i <= N; ++i )
{
for ( int j = 1; j < i; ++j )
if ( L[j]+1 > L[i] && A[j] < A[i] )
{
L[i] = L[j] + 1;
P[i] = j;
}
if ( L[i] > L[sol] )
sol = i;
citire(A, N);
ofstream out("subsir.out");
int sol = cmlscm(A, N, L, P);
out << L[sol] << '\n';
reconst(sol, P, A, out);
out.close();
return 0;
}
}
return sol;
}
void reconst(int poz, int P[], int A[],
ofstream &out)
{
if ( P[poz] )
reconst(P[poz], P, A, out);
out << A[poz] << ' ';
}
265
Capitolul 9
Pentru fiecare i de la 1 la N execut
o Suprascrie A[i] peste cel mai mic element din L, dar care
este strict mai mare dect A[i]. (1)
o Fie k poziia peste care a fost suprascris A[i]. P[i] ia
valoarea k.
Lungimea vectorului L (fcnd abstracie de poziiile marcate cu
infinit) reprezint lungimea celui mai lung subir cresctor
maximal.
Fie lg lungimea vectorului L. Pentru a reconstitui soluia, se
caut n vectorul P poziia ultimei apariii a valorii lg. Fie aceast
poziie klg . Se caut apoi ultima apariie a valorii lg 1, dar care
apare nainte de poziia klg . Aceasta va fi aadar pe o poziie
klg 1 < klg. Se procedeaz similiar pentru valorile lg 2, lg 3,
..., 2, 1. Soluia va fi dat de subirul: A[k1], A[k2], ..., A[klg ].
Putem implementa reconstituirea tot recursiv.
La pasul (1), dac A[i] este mai mare dect toate elementele diferite
de infinit din L, atunci A[i] se va suprascrie peste cea mai din stnga
valoare egal cu infinit. Putem implementa acest pas eficient n timp O(log
N) folosind o cutare binar.
S prezentm modul de execuie al algoritmului pe exemplul dat.
Iniial avem:
i
1
2
3
4
5
6
7
8
9 10
3
8
9
1
2 10 4 -1 11
A[i] 6
L[i] inf inf inf inf inf inf inf inf inf inf
P[i]
La pasul i = 1, se suprascrie A[1] = 6 peste cea mai mic valoare din
L, dar care este strict mai mare dect 6. Singura posibilitate este s
suprascriem elementul A[1] peste primul inf. n P[1] vom reine 1:
i
A[i]
L[i]
P[i]
1 2
3
4
5
6
7
8
9 10
6 3
8
9
1
2 10 4 -1 11
6 inf inf inf inf inf inf inf inf inf
1
1 2
3
4
5
6
7
8
9 10
6 3
8
9
1
2 10 4 -1 11
3 inf inf inf inf inf inf inf inf inf
1 1
3 4 5 6
7
8
9 10
8 9 1 2 10 4 -1 11
4 10 11 inf inf inf inf inf
3
1
2 3 1 2
4
5
267
Capitolul 9
int cbin(int st, int dr, int val, int L[])
{
while ( st < dr )
{
int m = (st + dr) / 2;
if ( L[m] < val )
st = m + 1;
else
dr = m;
}
return st;
}
void reconst(int N, int A[], int P[],
int val, ofstream &out)
{
for ( int i = N; i; --i )
if ( P[i] == val )
{
reconst(i - 1, A, P, val - 1, out);
out << A[i] << ' ';
break;
}
}
L[k] = A[i];
P[i] = k;
}
return lg;
}
int main()
{
int N, A[maxn], L[maxn], P[maxn];
citire(A, N);
ofstream out("subsir.out");
int sol = cmlscm(A, N, L, P);
out << sol << '\n';
reconst(N, A, P, sol, out);
out.close();
return 0;
}
Rezolvare
Pentru a rezolva problema vom ncerca s gsim o formul de
recuren pentru calculul lungimii celui mai lung subir comun. Fie L[i][j] =
lungimea celui mai lung subir comun al secvenelor 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 aceeai 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 aduga caracterul
A[p + 1] celui mai lung subir comun al secvenelor A[1, p] i
B[1, q], obinnd, pentru secvenele A[1, p + 1] i B[1, q + 1] un
subir comun de lungime maxim care este mai lung cu un
caracter. Aadar, L[p + 1][q + 1] = L[p][q] + 1.
2. Dac A[p + 1] != B[q + 1], atunci nu putem extinde niciun
subir de lungime maxim calculat anterior i va trebui s salvm
n L[p + 1][q + 1] lungimea celui mai lung subir de lungime
maxim calculat pn acuma. Aceast valoare este dat de
maximul dintre L[p][q + 1] i L[p + 1][q].
Timpul de execuie al acestei metode este O(NM), unde N este
lungimea primului ir, iar M este lungimea celui de-al doilea ir. Memoria
folosit de algoritm este tot O(NM), deoarece matricea L are N linii i M
coloane. Putem reduce memoria folosit la O(N + M), dar sacrificm astfel
posibilitatea reconstituirii soluiei. Vom descrie ns i aceast metod.
269
Capitolul 9
Pentru a reconstitui soluia vom proceda similar cu metoda de
reconstituire a unui drum a crui lungime minim a fost calculat cu
algoritmul lui Lee. Vom folosi o funcie recursiv reconst(x, y) care va
afia un subir comun de lungime maxim. n primul rnd, condiia de ieire
din recursivitate va fi dac x < 1 sau y < 1. Dac nu, verificm dac
A[x] == B[y], iar dac da, apelm recursiv reconst(x 1, y 1) i afim
A[x]. Dac A[x] != B[y] atunci apelm recursiv fie reconst(x 1, y), n
cazul n care L[x 1][y] > L[x][y 1], fie reconst(x, y 1) altfel.
Pentru a simplifica implementarea am folosit tipul de date string
pentru reinerea irurilor de caractere. Indicii irurilor de caractere ncep de
la 0, aa c trebuie s fim ateni la cum comparm caracterele individuale,
deoarece indicii matricei L ncep de la 1. Singura iniializare care trebuie
fcut este completarea liniei i coloanei 0 a matricei L cu valoarea 0,
pentru a evita cazurile particulare ale recurenei descrise.
Prezentm n continuare implementarea algoritmului de rezolvare.
#include <fstream>
#include <string>
using namespace std;
const int maxn = 101;
void citire(string &A, string &B)
{
ifstream in("sircom.in");
in >> A >> B;
in.close();
}
270
Capitolul 9
liniei curente, iar cellalt vector, L2, va reprezenta chiar linia curent. Noua
form a formulei de recuren este:
2 =
1 1 + 1
max
(1 , 2 1 )
= []
273
Capitolul 9
Vom ncerca s exprimm recursiv problema. Notm matricele date
cu A1, A2, ..., AN. S presupunem c tim un k ntre 1 i N astfel nct
parantezarea (A1A2...Ak)(Ak+1...AN) s fie o parte a soluiei optime.
Atunci, pentru a obine soluia optim, trebuie s tim cum putem paranteza
optim nmulirile A1 A2...Ak i Ak+1...AN.
Pentru a putea exprima matematic acest lucru, fie M[i][j] = numrul
minim de nmuliri scalare necesare nmulirii secvenei de matrice
[i, j]. Dac tim calcula matricea M, atunci rspunsul problemei va fi
M[1][N].
Se disting urmtoarele cazuri:
1. Dac avem o singur matrice, atunci nu trebuie efectuat nicio
nmulire scalar. Acesta este cazul de baz al recurenei, deci
M[i][i] = 0 pentru orice 1 i N.
2. Pentru aflarea numrului minim de nmuliri scalare necesare
pentru nmulirea unei secvene de matrice [i, j], 1 i < j N
este necesar s tim poziia i k < j n care vom mpri
secvena [i, j] n dou secvene parantezate separat [i, k] i
[k + 1, j]. Dimensiunea matricei rezultate din nmulirea
matricelor din secvena [i, k] va fi dat de (D[i 1], D[k]), iar
dimensiunea celei rezultate din nmulirea matricelor din
secvena [k + 1, j] va fi dat de (D[k], D[j]). Avem aadar:
M[i][j] = min(M[i][k] + M[k + 1][j] + D[i 1]D[k]D[j]).
ik<j
274
int main()
{
int N, D[maxn];
in >> N;
for ( int i = 0; i <= N; ++i )
in >> D[i];
citire(D, N);
ofstream out("inmopt.out");
out << rezolvare(D, N);
out.close();
in.close();
}
return 0;
int rezolvare(int D[], int N)
{
int M[maxn][maxn];
Exerciii:
a) De ce i trebuie s porneasc de la N 1 i nu de la 1? Ce se
ntmpl dac i merge de la 1 la N?
b) Concepei o modalitate de a reconstitui soluia. Pentru exemplul
dat, o reconstituire a soluiei ar putea fi (A1*A2)*A3.
275
Capitolul 9
276
277
Capitolul 9
void rezolvare(obiect A[], int N, int C,
int F[], int P[])
{
for ( int i = 0; i <= C; ++i )
F[i] = P[i] = 0;
int main()
{
int N, C;
obiect A[maxn];
citire(A, N, C);
}
out.close();
void reconst(obiect A[], int F[], int P[],
int C, ofstream &out)
{
int max = F[C];
while ( F[C] == max )
--C;
++C;
return 0;
}
while ( P[C] )
{
out << P[C] << ' ';
C -= A[ P[C] ].G;
}
}
Exerciii:
a) Afiai indicii obiectelor cresctor.
b) Afiai vectorii F i P dup fiecare actualizare a lor.
c) Dai exemplu de un set de date de intrare pentru care algoritmul
execut un numr maxim de operaii.
278
279
Capitolul 9
Exerciii:
a) Rezolvai o variant a problemei n care fiecare obiect i poate fi
folosit de cel mult Nri ori.
b) Rezolvai o variant a problemei n care obiectele pot avea
greuti negative.
c) Implementai un algoritm greedy pentru rezolvarea celor dou
probleme. Ct de mare poate ajunge s fie diferena dintre soluia
optim i soluia dat de algoritmul greedy?
d) Implementai un algoritm genetic pentru rezolvarea celor dou
probleme. Ct de aproape de soluia optim este acesta?
Comparai rezultatele algoritmului genetic cu rezultatele
algoritmului greedy.
281
Capitolul 9
void dinamica(int A[], int N, int S)
{
bool F[maxn];
int P[maxn];
for ( int i = 0; i <= S; ++i )
F[i] = P[i] = 0; // 0 == false
F[0] = true;
for ( int i = 1; i <= N; ++i )
for ( int j = S; j >= A[i]; --j )
{
F[j] |= F[j - A[i]];
while ( stmp != S )
if ( stmp > S )
{
int poz = 1 + (rand() % sel[0]);
stmp -= A[ sel[poz] ];
nesel[ ++nesel[0] ] = sel[poz];
sel[poz] = sel[ sel[0]-- ];
}
else
{
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();
}
ofstream out("plata1.out");
while ( S )
{
out << P[S] << ' ';
S -= A[ P[S] ];
}
out.close();
}
Exerciii:
a) Concepei un algoritm care afieaz soluia cu numr minim de
numere.
b) Cum se pot afia toate soluiile?
c) Ce se ntmpl dac pot exista numere mai mari ca S? Dar dac
exist i numere negative?
d) ncercai s gsii date de intrare pe care algoritmul randomizat
s ruleze mult timp.
282
Capitolul 9
b) Se consider N numere. Scriei un program care adun sau scade
fiecare numr astfel nct s se obin o sum S. Considerai N i
S de ordinul sutelor de mii.
c) n soluiile de la ultimele dou probleme, vectorul F este un
vector boolean. Asta nseamn c putem optimiza memoria
folosit folosind operaii pe bii. Scriei un program care face
acest lucru.
d) Scriei un program care determin n cte moduri se poate obine
suma S.
e) ncercai adaptarea algoritmului randomizat pentru aceast
variant a problemei. Cum se compar acesta cu un algoritm
genetic?
285
Capitolul 9
#include <fstream>
using namespace std;
const int maxn = 101;
int main()
{
int N, memo[maxn][maxn];
if ( memo[N][K] != -1 )
return memo[N][K];
ofstream out("nrpart.out");
out << nrpart(N, 1, memo);
out.close();
return 0;
Capitolul 9
Aadar:
D[p + 1][q + 1] = 1 + minim(D[p][q],D[p + 1][q],D[p][q + 1]).
Implementarea prezentat folosete tipul de date string, n care
caracterele sunt numerotate de la 0. Rezolvarea ns nu sufer nicio
modificare major, fiind necesar doar s scdem 1 cnd accesm un
caracter. Prezentm doar funcia relevant, restul programului fiind aproape
identic cu cel prezentat n cadrul problemei celui mai lung subir comun.
int levenshtein(const string &A, const string &B, int D[maxn][maxn])
{
for ( int i = 0; i <= A.length(); ++i )
D[i][0] = i;
for ( int i = 0; i <= B.length(); ++i )
D[0][i] = i;
for ( int i = 1; i <= A.length(); ++i )
for ( int j = 1; j <= B.length(); ++j )
if ( A[i - 1] == B[j - 1] )
D[i][j] = D[i - 1][j - 1];
else
D[i][j] = 1 + min(min(D[i - 1][j], D[i][j - 1]), D[i - 1][j - 1]);
return D[A.length()][B.length()];
}
Capitolul 9
Pentru a rezolva problema vom considera numerele date ca fiind
reinute n vectorul V i vom folosi o matrice S cu semnificaia
S[i][j] = suma maxim care poate fi obinut de primul juctor dac
lum n considerare doar secvena de numere V[i, j]. Vom iniializa
pentru fiecare i de la 1 la 2N pe S[i][i] cu V[i] i pe S[i][i + 1] cu
max(V[i], V[i + 1]). Apoi distingem urmtoarele cazuri pentru a calcula
S[i][j], cu j > i + 1:
1. Alegem numrul V[i]. La pasul urmtor, oponentul va putea
alege ntre V[i + 1] i V[j], aducndu-ne fie n starea S[i + 2][j]
fie n starea S[i + 1][j 1]. Deoarece tim c oponentul joac
optim, acesta ne va aduce cu siguran n starea cea mai
defavorabil, adic din care vom obine o sum ct mai mic.
Aadar, dac alegem numrul V[i], atunci obinem ctigul
V[i] + min(S[i + 2][j], S[i + 1][j 1]) = C1.
2. Alegem numrul V[j]. Dintr-un raionament identic cu cel de mai
sus rezult c obinem ctigul
V[j] + min(S[i][j 2], S[i + 1][j 1]) = C2.
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 recurenei pentru problema nmulirii optime a
unui ir de matrice. Vom ncepe cu i de la 2N 2 i cu j de la i + 2,
asigurndu-ne n acest mod c nu vom folosi valori ale matricei necalculate
nc.
Iniializarea valorilor S[i][i + 1] este necesar deoarece la fiecare pas
fie vom scdea 2 din j fie vom aduna 2 la i, aceast iniializare avnd rolul
de a evita unele cazuri particulare care pot aprea din cauza acestui lucru.
#include <fstream>
using namespace std;
const int maxn = 101;
void citire(int V[], int &N)
{
ifstream in("joc.in");
in >> N;
N *= 2; // mai simplu decat sa lucram cu 2*N
for ( int i = 1; i <= N; ++i ) in >> V[i];
in.close();
}
290
Exerciii:
a) Modificai algoritmul astfel nct s afieze fiecare numr ales
mpreun cu juctorul care a ales acel numr.
b) Rezolvai problema considernd c se pot alege k numere
consecutive dintr-un capt al irului.
c) Reducei memoria folosit de algoritm la O(N).
d) Modificai implementarea prezentat astfel nct s afieze care
juctor ctig jocul.
e) Rezolvai problema considernd 3 juctori i 3N numere.
291
Capitolul 9
rmq.out
93
1
819344526 2
13
9
58
33
O soluie n care parcurgem fiecare secven dat i determinm
minimul se dovedete a fi foarte ineficient, avnd complexitatea O(NT) n
cel mai ru caz.
Problema prezentat este cunoscut sub numele de problema Range
Minimum Query (traducere: problema interogrilor de minim pe
intervale). Aceast problem poate fi rezolvat n timp O(Nlog N + T) i
folosind memorie O(Nlog N) calculnd o matrice pe care o vom folosi apoi
pentru a rspunde la fiecare ntrebare n timp O(1).
Fie M[i][j] = cel mai mic numr din subsecvena A[j, j + 2i 1].
Altfel spus, M[i][j] reprezint cel mai mic numr din subsecvena care
ncepe pe poziia j i are lungimea 2i. Vom prezenta mai nti modul de
construcie al acestei matrici iar apoi algoritmul prin care vom rspunde la
ntrebri.
Pentru i = 0 obinem subsecvene de forma A[j, j], aadar 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 nct 0 < i log2[N] vom calcula vectorul M[i] astfel:
pentru fiecare j 1 astfel nct s aib loc j + 2i 1 N vom efectua operaia
M[i][j] = min(M[i 1][j], M[i 1][j + 2i 1]).
S demonstrm c acest mod de calculare este corect:
292
Capitolul 9
i\j
0
1
8
2
1
3
9
4
3
5
4
6
4
7
5
8
2
9
6
1
1
3
3
4
4
2
2
1
1
3
3
2
2
1
1
Fig. 9.14.1. Modul de execuie al algoritmului R.M.Q.
294
295
Capitolul 9
Exerciii:
a) Modificai algoritmul astfel nct s afieze poziia numrului
minim n irul dat.
b) Modificai algoritmul astfel nct s afieze cel mai mare element
din ir, precum i poziia acestuia.
c) Extindei algoritmul pentru gsirea celui mai mic sau celui mai
mare element dintr-un dreptunghi al unei matrice.
d) Ce se ntmpl dac interschimbm cele dou dimensiuni ale
matricei? Comparaii timpii de execuie a celor dou variante de
implementare i ncercai s explicai eventualele diferene.
296
Capitolul 9
3. Dac op[k] = -3 (xor), atunci cele dou subexpresii trebuie s
aib valori diferite (una adevrat, iar cealalt fals). Adunm
aadar la T[i][j] valoarea:
T[i][k] * F[k + 1][j] + F[i][k] * T[k + 1][j]
Valorile matricei F se calculeaz similar. Avem aadar recurenele:
1
=
=
[] [ + 1][]
= 1
+ 1 [] [ + 1][] = 2
+ 1 + [] [ + 1][] = 3
i
1
=
=
+ 1 [] [ + 1][] = 1
+ 1 []
= 2
+ 1 + [] [ + 1][] = 3
int main()
{
// op[i] = operatorul dintre
// simbolul i si i + 1
int N, op[maxn];
bool S[maxn];
citire(N, S, op);
ofstream out("paran.out");
out << rezolvare(N, S, op) << endl;
out.close();
return 0;
}
298
299
Capitolul 9
9.15. Concluzii
Am prezentat n acest capitol probleme a cror soluii folosesc
programarea dinamic. Metoda programrii dinamice este o metod foarte
util pentru rezolvarea problemelor de informatic, dar este i metoda cea
mai grea de stpnit, ntruct problemele care se rezolv printr-un algoritm
de programare dinamic pot fi foarte variate, deci este nevoie de experien
pentru a putea gsi anumite recurene..
Propunem aadar spre rezolvare urmtoarele probleme:
1. Dndu-se N i K scriei un program care determin cte numere
de N cifre cu suma cifrelor K exist. Analog pentru produs.
2. Scriei un program care rspunde eficient la mai multe interogri
privind suma unor dreptunghiuri ale unei matrice.
3. Dndu-se un ir de numere naturale A scriei un program care
determin numrul minim de numere din ir a cror sum d
restul R la mprirea la K.
4. Scriei un program care determin cte drumuri exist ntr-un
caroiaj cu obstacole de la poziia (1, 1) la poziia (N, N), drumuri
care pot conine cel mult K pai i care pot parcurge de mai
multe ori orice poziie (drumuri neelementare).
5. Scriei un program care determin cte drumuri elementare exist
ntr-un caroiaj fr obstacole de la poziia (1, 1) la poziia (N, N),
tiind c dintr-o poziie oarecare ne putem deplasa doar n jos sau
la dreapta.
6. Scriei un program care determin numrul irurilor de lungime
N formate cu caracterele a, b, c i d cu proprietatea c a nu poate
fi lng b i c nu poate fi lng d. Problema admite o rezolvare
clasic i una eficient.
7. Scriei un program care determin cel mai lung drum ntr-un graf
orientat aciclic.
8. Scriei un program care afl cel mai mic numr cu K divizori.
9. Scriei un program care citete o matrice binar i determin
dreptunghiul de arie maxim care conine numai valoarea 1.
10. Scriei un program care determin subsecvena de sum maxim
a unui ir circular (dup ultimul element urmeaz primul
element)
11. Scriei un program care determin subsecvena de sum maxim
de lungime cel puin L.
300
10. Algoritmi de
geometrie
computaional
Toate problemele de informatic au la baz probleme matematice,
informatica fiind de fapt o ramur a matematicii aplicate. Pn acum am
prezentat probleme i algoritmi care au o legturi 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 numete
geometrie computaional. Aceast ramur a informaticii are aplicaii
practice importante n programe de grafic (aplicaii CAD, aplicaii de
modelare 2d i 3d etc.), proiectarea circuitelor integrate i altele.
n acest capitol nu vom avea ca obiectiv optimizarea algoritmilor cu
privire la stabilitatea numeric a acestora (limitarea erorilor numerice
generate de ctre algoritm). Vom folosi pur i simplu variabile de tip
double, iar operaiile cu acestea le vom efectua n mod obinuit.
Considerm c acest lucru este suficient pentru nite implementri didactice,
accentul fiind pus pe validitatea teoretic a algoritmilor i pe uurina de
nelegere a acestora.
Scopul acestui capitol nu este de a prezenta pe larg algoritmii de
geometrie computaional, deoarece acest lucru ar necesita un spaiu mult
prea larg i nu face obiectul acestei lucrri. Capitolul acesta are ca scop doar
introducerea unor noiuni i algoritmi de baz, cu ajutorul crora cititorul s
poat cuta i nelege lucruru mai avansate.
301
Capitolul 10
CUPRINS
10.1. Convenii de implementare ................................................................. 303
10.2. Reprezentarea punctului i a dreptei ................................................. 304
10.3. Panta i ecuaia unei drepte................................................................ 305
10.4. Intersecia a dou drepte .................................................................... 306
10.5. Intersecia a dou segmente ............................................................... 308
10.6. Calculul ariei unui poligon ................................................................... 311
10.7. Determinarea nfurtorii convexe (convex hull) ............................ 313
302
Capitolul 10
= 0.5.
n general, dac tim c dou puncte (x1 , y1) i (x2, y2) aparin dreptei
d, atunci panta dreptei va fi
305
Capitolul 10
=
2 1
.
2 1
2 1 1 2
1 2 2 1
307
Capitolul 10
bool Intersect(const Dreapta &d1, const Dreapta &d2,
Punct &intersectPct)
{
double m1 = -d1.a / d1.b, m2 = -d2.a / d2.b;
if ( m1 == m2 ) return false;
double x = (d2.b*d1.c - d1.b*d2.c) / (d2.a*d1.b - d1.a*d2.b);
double y = (d2.a*d1.c - d1.a*d2.c) / (d1.a*d2.b - d2.a*d1.b);
intersectPct = Punct(x, y);
return true;
}
. . . .
. . . . . . . . < 0
Folosind aceast formul se reduc erorile de calcul i este mai uor
de scris funcia ajuttoare Orientare(A, B, C):
Dac (B.y A.y)*(C.x A.x) (C.y A.y)*(B.x A.x) < 0
execut
o returneaz -1
Altfel dac expresia este egal cu 0 execut
o returneaz 0
Altfel execut
o returneaz 1
Aceast funcie va juca un rol important n simplificarea i
optimizarea multor algoritmi de geometrie computaional.
Acum, pentru ca un segment [AB] s intersecteze un segment [CD],
sunt necesare urmtoarele condiii:
Orientare(A, B, C) diferit de Orientare(A, B, D). Aceast
condiie ne asigur c punctele C i D nu sunt de aceeai parte a
dreptei AB.
Orientare(C, D, A) diferit de Orientare(C, D, B). Aceast
condiie ne asigur c punctele A i B nu sunt de aceeai parte a
dreptei CD.
Dac aceste dou condiie sunt ndeplinite, atunci tim sigur c cele
dou segmente se intersecteaz. Ne-am putea pune acum problema
determinrii punctului de intersecie dintre acestea. Dac tim c dou
309
Capitolul 10
segmente se intersecteaz, atunci punctul n care acestea se intersecteaz va
coincide cu punctul n care dreptele asociate acestora se intersecteaz, aa c
putem pur i simplu s aplicm algoritmul de determinare a punctului de
intersecie din cazul dreptelor, algoritm prezentat anterior.
Implementarea algoritmului prezentat nu este dificil. n primul rnd
vom folosi urmtoarea structur pentru a memora un segment:
struct Segment
{
Punct A, B;
Segment(Punct P1, Punct P2) : A(P1), B(P2) {}
Segment() {}
};
310
1
2
1
1
+ 2
2
2
+ +
3
1
1
X1
Y1
X2
X3
...
XN
X1
Y2
+
Y3
+
...
+
YN
+
Y1
+
311
Capitolul 10
Formula ne va da o arie cu semn. Semnul ariei unui poligon convex
va fi pozitiv dac punctele sunt dispuse n sens trigonometric i negativ dac
sunt dispuse n sens invers trigonometric. Dac nu ne intereseaz semnul
atunci putem pur i simplu s calculm modulul acestei funcii.
Funcie Arie(P, N), unde P este un vector de puncte care reprezint
un poligon, iar N numrul punctelor din vector va returna aria poligonului
definit de punctele din P. Aceast funcie poate fi scris n pseudocod astfel:
total = 0
pentru fiecare i de la 1 la N execut
o total += P[i].x*P[i % N + 1].y P[i].y*P[i % N + 1].x
returneaz total / 2
Am folosit expresia i % N + 1 pentru a obine valoarea i + 1 pentru
orice i < N i valoarea 1 atunci cand i == N. Astfel evitm tratarea ultimului
determinant ca i un caz particular. Folosirea operatorului modulo poate s
ncetineasc ns un algoritm care apeleaz des aceast funcie, aa c n
unele cazuri este preferabil adunarea ultimului determinant la sfrit.
Aceast funcie va funciona corect pentru orice poligon (convex sau
concav), mai puin pentru poligoanele care au laturi ce se taie reciproc, cum
ar fi de exemplu urmtorul poligon:
312
1
2
+1
2
313
Capitolul 10
Capitolul 10
parcursul algoritmului, minim va conine cel mai mic element din secvena
secv.
O analiz intuitiv a algoritmului este simpl: de fiecare dat cnd
dm de un element mai mic dect cel presupus a fi minimul global, revizuim
presupunerea fcut, considernd acest nou element ca fiind minimul global.
Dup parcurgerea tuturor elementelor, vom avea evident adevratul minim
global.
Algoritmul lui Jarvis are un raionament aproape identic. Fie P1
punctul ales care face sigur parte din nfurtoarea convex. Vom
presupune c segmentul [P1PnewPct] este o muchie a nfurtorii convexe.
Iniial vom considera newPct = 2. Parcurgem acum toate celelalte puncte
date. Dac exist un punct Pq, astfel nct Orientare(P1, Pq, PnewPct) > 0
(sau mai mic ca zero, nu are importan atta timp ct suntem consisteni n
alegere) atunci segmentul [P1Pq] are mai multe anse s aib restul
punctelor ntr-o singur parte dect segmentul [P1PnewPct]. Acest lucru se va
clarifica imediat. Vom seta newPct = q i vom continua algoritmul, cutnd
(fr a reporni cutarea!) un alt q astfel nct Orientare(P 1, Pq, PnewPct) > 0
(atenie, newPct este acuma egal cu vechiul q).
La finalul acestui pas, PnewPct va face sigur parte din nfurtoarea
convex. Mai mult, segmentul [P1PnewPct] va avea toate punctele date ntr-o
singur parte.
Se reia algoritmul de la urmtorul punct de pe nfurtoarea
convex, adic PnewPct. Acesta va juca acum rolul lui P1. Se continu pn
cnd se ajunge din nou la punctul de nceput. Deoarece se execut O(N) pai
pentru fiecare punct de pe nfurtoarea convex deducem c timpul de
execuie a ntregului algoritm este O(Nh).
Funcia Jarvis(P) care determin vrfurile nfurtorii convexe a
setului de puncte P poate fi implementat astfel:
adaug n CH cel mai din stnga punct din P, iar n caz de
egalitate cel mai de sus.
pentru fiecare punct startPct din CH execut
o nextPct = un punct din P diferit de startPct
o pentru fiecare punct q din P execut
dac Orientare(startPct, q, nextPct) > 0 execut
nextPct = q
o dac nextPct != CH[1] execut
adaug nextPct n CH
CH reprezint punctele de pe nfurtoarea convex a setului P.
316
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;
}
void Citire(vector<Punct> &P, int &N)
{
cin >> N;
double x, y;
for ( int i = 1; i <= N; ++i )
{
cin >> x >> y;
P.push_back(Punct(x, y));
}
}
318
Exerciii:
a) n ce ordine se afieaz punctele de pe nfurtoare?
b) Cum se poate modifica algoritmul astfel nct punctele de pe
nfurtoare s fie afiate n alt ordine?
c) Dai exemplu de un set de puncte pe care algoritmul efectueaz
un numr nefavorabil de pai.
319
Capitolul 10
Algoritmul de complexitate O(Nlog N) este algoritmul lui
Graham (sau scanarea Graham). Acesta este un algoritm foarte eficient pe
orice set de puncte, presupunnd doar o sortare i o parcurgere liniar a
punctelor.
Funcia Graham(P) care determin punctele ce reprezint vrfuri ale
nfurtorii convexe a setului de puncte P poate fi scris n pseudocod n
felul urmtor:
Interschimb P[1] cu un punct care sigur este vrf al
nfurtorii.
Sorteaz P[2, N] cresctor dup panta dreptei format de fiecare
dintre aceste puncte cu punctul P[1].
P[0] = P[N]
nrH = 2, va reprezenta numrul de vrfuri ale nfurtorii.
Pentru fiecare i de la 3 la N execut
o Ct timp nrH > 1 i
Orientare(P[nrH 1], P[nrH], P[i]) < 0 execut
nrH = nrH 1
o nrH = nrH + 1
o interschimb P[nrH] cu P[i]
P[1, nrH] reprezint mulimea punctelor care sunt vrfurii ale
nfurtorii convexe a setului de puncte P.
Se poate observa c algoritmul folosete o stiv n care construiete
soluia (practic se folosete vectorul dat). La fiecare pas se verific dac
ultimele dou puncte din stiv, mpreun cu cel de-al i-lea punct, sunt sau nu
valide. Invariantul pstrat de algoritm este ca toate punctele s aib semnul
pozitiv, aa c vor fi scoase din stiv punctele care nu ndeplinesc aceast
condiie. La sfritul algoritmului, n stiv vor rmne doar punctele valide.
Scanarea Graham este practic o optimizare a algoritmului lui Jarvis,
optimizare facilitat de sortarea punctelor dup panta dreptei pe care o
formeaz cu punctul care sigur face parte din nfurtoare.
Deoarece fiecare punct intr n stiv o singur dat i iese din stiv
cel mult o dat, deducem c asupra fiecrui punct se efectueaz O(N)
operaii de cost constant. Complexitatea algoritmului este deci O(Nlog N)
datorit sortrii.
Algoritmul ncepe cu cel mai din stnga punct i construiete
muchiile n sensul acelor de ceasornic. Muchiile albastre din figura
urmtoare reprezint muchii alese la un moment dat de ctre algoritm, dar
care au fost determinate apoi ca fiind greit alese, fornd algoritmul s
320
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]);
}
return vector<Punct>(P.begin(), P.begin() + nrH);
}
322
Liste nlnuite
11. Liste
nlnuite
Acest capitol prezint noiunile elementare despre liste nlnuite
(simplu nlnuite, dublu nlnuite i circulare). Vor fi prezentate noiuni
teoretice i detalii de implementare.
Listele nlnuite 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
inserri mai eficiente, mai ales la sfritul i nceputul acestora (acestea se
pot face n timp constant.
Aadar, alegerea dintre liste i tablouri trebuie fcut n funcie de
natura problemei pe care vrem s o rezolvm. Vom prezenta n acest capitol
i alte avantaje i dezavantaje.
Dei listele nlnuite exist deja implementate n cadrul librriei
S.T.L. (containerul list), considerm c este important pentru orice
programator s cunosc modul de implementare manual al acestora, ntruct
implementarea manual ofer mai mult control asupra acestora. Acest
control va fi necesar n capitolele urmtoare, care au de a face cu grafuri i
cu structuri avansate de date, structuri care uneori se pot implementa mai
simplu i mai eficient printr-o implementare manual a listelor nlnuite
care stau la baza acestora.
Recomandm aadar parcurgerea i nelegerea acestui capitol
nainte de a trece mai departe la capitolele urmtoare.
323
Capitolul 11
CUPRINS
11.1. Noiuni introductive ............................................................................. 325
11.2. Tipul abstract de date list simplu nlnuit..................................... 327
11.3. Aplicaii ale listelor nlnuite ............................................................. 339
11.4. Tipul abstract de date list dublu nlnuit ...................................... 343
11.5. Dancing Links ........................................................................................ 354
324
Liste nlnuite
Capitolul 11
struct nod
{
T informatie;
nod *link_;
};
Liste nlnuite
Cu alte cuvinte, dac este necesar folosirea n program a unui tip de
date nlnuit, fr necesitatea modificrilor structurale de adugare sau
tergere, se recomand utilizarea vectorilor statici. Dac se cer modificri
structurale minime i se pune accentul pe modificri i vizualizri ale
elementelor (satistici, ordonri...), atunci se recomand utilizarea vectorilor
dinamici (vector din S.T.L. este implementat ca un vector dinamic). n
schimb, dac se cere folosirea unui tip de date nlnuit bazat pe modificri
structurale, atunci este necesar i util folosirea unei liste.
327
Capitolul 11
328
Liste nlnuite
i
New->link_ = NULL;
329
Capitolul 11
nod *add_end(nod *Old)
{
// construcia nodului de adugat
nod *New = new nod;
New->link_ = NULL;
cin >> New->info;
// gsirea adresei ultimului nod
nod *Temp;
for ( Temp = Old; Temp->link_ != NULL; Temp = Temp->link_ );
// legtura
Temp->link_ = New;
// valoarea de returnat
return Old;
}
eroare!
fr erori!
330
Liste nlnuite
//gsirea adresei ultimului nod
nod *Temp;
for ( Temp = Old; Temp->link_ != NULL; Temp = Temp->link_ );
//legtura
Temp->link_ = New;
//valoarea de returnat
return Old;
}
}
(1)
(2)
331
Capitolul 11
(1)
(2)
Creeaz structura din figura de mai jos, n care se pierd toate datele
de la nodul 3 ncolo.
332
Liste nlnuite
Pentru funcionarea corect a funciei n orice caz, avem nevoie de
tratarea unor cazuri particulare: n primul rnd, dac lista nu are niciun nod,
se adaug pur i simplu nodul citit n list. n al doilea rnd, observm c
parcurgerea menionat mai sus nu funcioneaz corect dac primul nod al
listei are deja o valoare mai mare dect a nodului care trebuie adugat,
deoarece verificarea ncepe abia de la al doilea nod. De aceea, vom pune o
condiie ca dac primul nod are o valoare mai mare dect a noului nod, noul
nod va fi adugat la nceputul listei. Restul condiiilor sunt clare:
nod *add_mid(nod *Old)
{
if ( Old == NULL ) Old = add_beg(Old); // adaugarea primului nod
else
{
// nodul care trebuie adaugat
nod *New = new nod;
cin >> New->info;
// 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
Liste nlnuite
nod *del_beg(nod *Old)
{
if ( Old != NULL )
{
nod *ToDel = Old;
Old = Old->link_;
delete ToDel;
}
return Old;
}
335
Capitolul 11
nod *del_end(nod *Old)
{
if ( Old == NULL )
return NULL;
else
{
if ( Old->link_ == NULL )
{
nod *ToDel = Old;
Old = NULL;
delete ToDel;
}
else
{
nod *Temp;
for ( Temp = Old; Temp->link_->link_; Temp = Temp->link_ );
nod *ToDel = Temp->link_;
Temp->link_ = NULL;
delete ToDel;
}
}
return Old;
}
336
Liste nlnuite
Vom folosi condiia Temp->link_->info == key pentru a verifica
dac urmtorul element este cel care trebuie ters. Dac da, acest element se
va terge n felul urmtor: construim o variabil pointer ToDel care va lua
adresa elementului ce trebuie ters:
nod *ToDel = Temp->link_;
(1)
Trecem peste nodul pointat de Temp link_ prin
Temp->link_ = Temp->link_->link_;
(2)
i eliberm memoria lui ToDel prin
(3)
delete ToDel;
Fig. 11.2.9. tergerea unui nod din interiorul unei liste nlnuite
n acest caz avem tratat condiia Temp->link_->info == key, dar
dac primul element este chiar cel care trebuie ters? ]n implementarea
iniial nu avem tratat acest caz. Codul prezentat mai jos trateaz ns i
cazul acesta:
nod *del_mid(nod *Old, int key)
{
if ( Old->info == key )
Old = del_beg(Old);
else
{
for ( nod *Temp = Old; Temp->link_; Temp = Temp->link_ )
if ( Temp->link_->info == key )
{
nod *ToDel = Temp->link_;
Temp->link_ = Temp->link_->link_;
delete ToDel; break;
}
}
return Old;
}
337
Capitolul 11
Liste nlnuite
a) Stiva
Stiva este un tip particular de list n care adugarea i tergerea
nodurilor se realizeaz ntr-un singur capt (numit uzual vrf). n cazul
definiiei noastre, tipul de date stiv permite fie (add_beg i del_beg), fie
(add_end i del_end), crend astfel un mecanism F.I.L.O. (First In Last
Out) / L.I.F.O. (Last In First Out, folosit mai des). n multe dintre
problemele care necesit un tip de date de tip stiv este necesar
introducerea unei variabile ce reprezint dimensiunea maxim a stivei,
mpreun cu o subrutin cu ajutorul creia s se poat verifica dac o stiv
este sau nu plin (sau goal).
b) Coada
Pentru acest tip particular de list, adugarea se realizeaz la un
capt n schimb ce tergerea unui nod se realizeaz la captul opus. n cazul
definiiei noastre, tipul coad va permite sau (add_beg i del_end) sau
(add_end i del_beg), crendu-se un mecanism F.I.F.O. (First In First Out,
folosit mai des) / L.I.L.O. (Last In Last Out). La fel ca i n cazul stivei, de
multe ori este necesar introducerea unei variabile ce reprezint lungimea
339
Capitolul 11
maxim a cozii i a unei funcii cu ajutorul creia s putem verifica dac
structura este plin sau nu.
Liste nlnuite
aceasta, el va ncepe o numrtoare ncepnd de la cavalerul cu numrul de
ordine 1. Iniial, el numr pn la 1, oprindu-se pe cavalerul imediat
urmtor, adic 2, care este eliminat (sigur nu va fi trimis n misiune i se
ridic de la mas). Dup aceea, el numr pn la 2, oprindu-se pe al doilea
cavaler dup cel eliminat anterior, acesta fiind cel cu numrul de ordine 4,
care este i el eliminat. Dup aceea numr pn la 3, oprindu-se pe al
treilea cavaler dup ultimul eliminat. Regele se oprete atunci cnd mai
rmne un singur cavaler neeliminat, cavaler care va fi trimis n misiune.
Fiierul de ieire cavaleri.out va conine, n ordinea eliminrii lor,
numerele de ordine ale cavalerilor eliminai.
Exemplu:
cavaleri.in cavaleri.out
6
24135
Problema poate fi rezolvat folosind o list circular n felul
urmtor: vom ine o variabil nr care va reprezenta numrul de cavaleri
eliminai deja. Cnd nr = N 1, algoritmul se ncheie. Eliminarea efectiv
este simplu de realizat: ne vom deplasa la fiecare pas de attea ori de ct
este necesar pentru a gsi urmtorul cavaler care trebuie eliminat i vom
terge nodul asociat acestuia din list.
Trebuie avut grij ca ultimul nod al listei s fie legat de primul,
pentru a putea efectua deplasrile n mod natural.
#include <fstream>
using namespace std;
struct nod
{
int info;
nod *link_;
};
nod *Temp;
for ( Temp = Old;
Temp->link_ != NULL;
Temp = Temp->link_ );
Temp->link_ = New;
}
return Old;
return New;
}
341
Capitolul 11
void rezolvare(nod *LISTA, int N)
{
int nr = 0;
ofstream out("cavaleri.out");
for ( int i = 1; i <= N; ++i )
LISTA = add_end(LISTA, i);
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_ este cavalerul care
// trebuie eliminat, adica sters
nod *Tmp = LISTA->link_;
out << Tmp->info << ' ';
LISTA->link_ = LISTA->link_->link_;
delete Tmp;
++nr;
}
out.close();
}
int main()
{
int N;
ifstream in("cavaleri.in");
in >> N;
in.close();
nod *LISTA = NULL; // necesar pentru a evita anumite erori
rezolvare(LISTA, N);
return 0;
}
342
Liste nlnuite
343
Capitolul 11
n primul caz (clasic), se face referire la lista dublu nlnuit
printr-un singur pointer la primul nod:
nod *LISTA;
Iar apoi, pentru a ajunge la adresa ultimului element de pe direcia
link_, se va executa un ciclu repetitiv:
for ( nod *Temp = LISTA; Temp->link_; Temp = Temp->link_);
Liste nlnuite
(1)
(2)
Fig. 11.4.4. Adugarea unui nod la nceputul unei liste dublu nlnuite
S nu uitm c adresa primului element se schimb, deci va trebui s
returnm noua adres sau s transmitem primul nod al listei prin referin.
nod *add_beg(nod *Old)
{
nod *New = new nod;
cin >> New->info;
New->_link = NULL;
New->link_ = Old;
Old->_link = New;
Old = New;
return Old;
}
345
Capitolul 11
Mai trebuie s amintim cazul n care Old este NULL, astfel
neexistnd Old->_link; fapt ce poate genera erori. Modul tratare al acestui
caz este foarte simplu: se nlocuiete n funcia anterioar linia
Old->_link = New;
cu:
if ( Old != NULL ) Old->_link = New;
(1)
(2)
Fig. 11.4.5. Adugarea unui nod la sfritul unei liste dublu nlnuite
nod *add_end(nod *Old)
{
nod *New = new nod;
cin >> New->info;
New->link_ = NULL;
nod *Temp;
for ( Temp = Old; Temp->link_ != NULL; Temp = Temp->link_ );
Temp->link_ = New;
New->_link = Temp;
return Old;
}
346
Liste nlnuite
Mai trebuie verificat condiia de existen a lui Temp->link_: dac
lista Old este NULL, atunci adugarea la sfrit se schimb n add_beg:
if ( Old == NULL )
Old = add_beg(Old);
else { codul anterior }
Fig. 11.4.6. Adugarea unui nod n interiorul unei liste dublu nlnuite
Implementarea, respectiv condiiile de existen a acestui mod de
adugare, se pot urmri n aplicaia asociat acestui subcapitol.
347
Capitolul 11
(1)
(2)
(3)
respectiv
ToDel, pentru a elibera memoria ultimului nod
(2)
Liste nlnuite
Dup care:
(3)
(4)
(2)
349
(3)
Capitolul 11
(4)
Liste nlnuite
#include <iostream>
using namespace std;
struct nod
{
int info;
nod *link_;
nod *_link;
};
void add_beg(nod *&Old, int info)
{
nod *New = new nod;
New->info = info;
New->_link = NULL;
New->link_ = Old;
if ( Old != NULL )
Old->_link = New;
Old = New;
}
void add_end(nod *&Old, int info)
{
// nu mai este necesar sa ne deplasam
// pe ultimul nod, deoarece functia
// va fi intotdeauna apelata cu ultimul nod
// ca parametru.
nod *New = new nod;
New->info = info;
New->link_ = NULL;
Old->link_ = New;
New->_link = Old;
Old = New;
}
351
Capitolul 11
void add_mid(nod *&Old, int info)
{
if ( Old == NULL ) // adaugarea primului nod
add_beg(Old, info);
else
{
// daca valoarea este mai mica sau egala,
// cu a primului nod, se adauga la inceput
if ( info <= Old->info )
add_beg(Old, info);
else
{
// caut unde trebuie adaugat noul nod
nod *Temp = Old;
for ( ; Temp->link_ != NULL; Temp = Temp->link_ )
if ( info <= Temp->link_->info )
break; // am gasit pozitia pe care trebuie adaugat
if ( Temp->link_ == NULL ) // adaugare la sfarsit
add_end(Temp, info);
else // adaugare undeva in interior
{
nod *New = new nod;
New->info = info;
New->link_ = Temp->link_;
New->_link = Temp;
Temp->link_->_link = New;
Temp->link_ = New;
}
}
}
}
void del_beg(nod *&Old)
{
nod *ToDel = Old;
Old = Old->link_;
if ( Old != NULL ) // altfel nu exista Old->_link
Old->_link = NULL;
delete ToDel;
}
352
Liste nlnuite
void del_mid(nod *&Old, int info)
{
if ( Old->info == info )
del_beg(Old);
else
{
nod *Temp = Old;
for ( ; Temp->link_ != NULL; Temp = Temp->link_ )
if ( Temp->link_->info == info )
break;
if ( Temp->link_ != NULL )
{
nod *ToDel = Temp->link_;
Temp->link_ = Temp->link_->link_;
if ( Temp->link_ != NULL )
Temp->link_->_link = Temp;
delete ToDel;
}
}
}
void view(nod *L)
{
for ( ; L; L = L->link_ )
cout << L->info << ' ';
cout << endl;
}
int main()
{
// atentie la initializarea cu NULL!
nod *LISTA = NULL;
int N, M, x;
cin >> N >> M;
for ( int i = 0; i < N; ++i )
{
cin >> x;
add_mid(LISTA, x);
}
view(LISTA);
for ( int i = 0; i < M; ++i )
{
cin >> x;
del_mid(LISTA, x);
view(LISTA);
}
return 0;
}
353
Capitolul 11
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 relaiilor existente ntre elementele unui set suport. Elementele
din setul suport se numesc noduri, iar relaiile existente ntre acestea se
numesc muchii sau arce (termen folosit uneori n cazul grafurilor orientate).
n cele ce urmeaz ne propunem s prezentm n detaliu principalele
structuri de date folosite n teoria grafurilor i algoritmii cei mai des folosii
pentru rezolvarea problemelor cu grafuri. Datorit faptului c avem de a
face cu un domeniu foarte vast, care este i ntr-o continu dezvoltare, nu
putem dect s tindem spre a fi exhaustivi, deci materialul ce urmeaz
trebuie luat ca o trecere detaliat prin acest domeniu i nicidecum ca o
prezentare complet a acestuia.
Problemele de grafuri i algoritmii aplicabili acestora sunt foarte
numeroi. Temele abordate n acest capitol sunt ns suficiente pentru ca
cititorul s poat, o dat cu nelegerea acestora, aborda de unul singur
aproape orice problem din acest domeniu.
Vom prezenta, acolo unde este cazul, mai muli algoritmi de
rezolvare a unei probleme, sau mai multe posibiliti de implementare a unui
anumit algoritm. Considerm c lucrrile care se limiteaz la prezentarea
unei singure metode de rezolvare a unei probleme fac un mare deserviciu
cititorilor, deoarece metoda optim de rezolvare depinde aproape
ntotdeauna de mai muli factori, iar alegerea soluiei folosite trebuie s in
cont de avantajele i dezavantajele unei metode relativ la situaia n care ne
aflm.
Toate funciile i structurile de date folosite n acest capitol au fost
prezentate n cadrul seciunii Introducere n S.T.L., seciune pe care
cititorul ar trebui s o parcurg nainte de a ncepe aceste capitol.
355
Capitolul 12
CUPRINS
12.1. Noiuni teoretice .................................................................................. 357
12.2. Reprezentarea grafurilor n memorie ................................................. 360
12.3. Probleme introductive ......................................................................... 364
12.4. Parcurgerea n adncime ..................................................................... 369
12.5. Parcurgerea n lime........................................................................... 380
12.6. Componente tare conexe .................................................................... 388
12.7. Determinarea nodurilor critice ........................................................... 391
12.8. Drum i ciclu eulerian .......................................................................... 394
12.9. Drum i ciclu hamiltonian .................................................................... 399
12.10. Drumuri de cost minim n grafuri ponderate ................................... 404
12.11. Reele de transport ............................................................................ 423
12.12. Arbore parial de cost minim ............................................................ 438
12.13. Concluzii .............................................................................................. 445
356
Teoria grafurilor
357
Capitolul 12
12. Un graf complet este un graf fr bucle care are muchie ntre
oricare dou noduri. Numrul de muchii al unui graf neorientat
complet este egal cu:
( 1)
2
Numrul de muchii al unui graf orientat complet este egal cu:
( 1)
.
Numrul de grafuri neorientate cu N noduri este dat de formula:
(1)
2
Teoria grafurilor
24. Se numete component conex a grafului neorientat G un
subgraf conex de ordin maxim a lui G.
25. Un graf orientat G se numete tare conex dac oricum am alege
dou noduri i i j ale sale, exist drumt de la i la j i de la j la i.
26. Se numete component tare conex a grafului orientat G un
subgraf tare conex de ordin maxim a lui G.
27. n cazul grafurilor orientate, transpusa unui graf G este graful GT
construit astfel: pentru fiecare arc (i, j) din G, n GT vom avea
doar arcul (j, i). Practic, graful transpus este format prin
inversarea sensului fiecrui arc al grafului original.
28. Suma gradelor nodurilor unui graf neorientat este egal cu 2|E|.
29. Un arbore este un graf neorientat n care exist un singur drum
ntre oricare dou noduri. Un arbore cu N noduri are N 1
muchii. Uneori, arborii pot fi i grafuri orientate.
30. O pdure este un graf a crui componente conexe sunt arbori.
31. Se numete nivelul k al unui arbore o mulime format din toate
nodurile aflate la distana k fa de rdcin.
32. nlimea unui arbore este dat de numrul de niveluri existente
n arbore.
33. Se numete predecesor (sau tat) al unui nod i dintr-un arbore
acel nod j care este adiacent cu i i pe un nivel cu 1 mai mic
dect al lui i. Nodul i se numete fiu al lui j.
34. Se numete strmo al unui nod i dintr-un arbore acel nod j care
se afl pe un nivel mai mic dect cel al lui i. Nodul i se va numi
descendent al nodului j.
35. Se numete rdcin a unui arbore nodul care nu are predecesor.
36. Se numete nod terminal (sau frunz) acel nod al unui arbore
care nu are fii.
37. Spunem c un graf se numete rar dac numrul de muchii este
relativ mic (graful este mai aproape de un arbore dect de un graf
complet) i dens dac numrul de muchii este relativ mare
(graful se apropie de un graf complet).
359
Capitolul 12
1
2
3
4
5
6
1
0
1
0
1
0
0
Matrice de adiacen
2
3
4
0
1
1
0
0
0
0
0
0
0
0
0
0
0
1
0
1
1
5
0
1
0
0
0
1
6
0
1
1
0
1
0
Liste de adiacen
G[1] = {2, 4}
G[2] = {1, 5}
G[3] = {6}
G[4] = {1}
G[5] = {2, 6}
G[6] = {5, 2, 3}
Teoria grafurilor
inexistente. Listele de adiacen nu au acest dezavantaj, deoarece fiecare
list conine doar acele noduri adiacente nodului asociat listei respective.
Pentru a reprezenta un graf cu N noduri i M muchii, matricea de adiacen
va folosi O(N2) memorie, pe cnd listele de adiacen vor folosi doar
O(N + M) memorie. Pentru grafuri rare sunt mai eficiente listele de
adiacen, iar pentru grafuri dese matricile de adiacen.
Un dezavantaj al listelor fa de matricea de adiacen este timpul
necesar determinrii adiacenei a dou noduri. Pentru a verifica dac dou
noduri i i j sunt adiacente folosind matricea de adiacen, este suficient s
verificm valoarea elementului A[i][j]. Pentru a verifica acelai lucru
folosind liste, trebuie s parcurgem ntreaga list asociat nodului i sau
nodului j, lucru care, n cel mai ru caz, se efectueaz n timp O(N).
Pe grafuri rare, majoritatea algoritmilor care lucreaz cu grafuri se
execut mult mai rapid dac grafurile sunt reinute ca liste de adiacen. Pe
grafuri dense, este mai convenabil folosirea matricei de adiacen, care este
i mai eficient.
Exist i alte metode de a reprezenta grafurile. De exemplu, putem
folosi vectorii de tai n cazul arborilor. Dac vrem s reprezentm un
arbore, l putem reprezent reinnd pentru fiecare nod al su care este tatl
acestuia, folosind un vector T, unde T[i] = j se citete j este tatl lui i.
T[i] = 0 dac i este rdcina arborelui. De exemplu, urmtorul arbore:
Capitolul 12
O alt metod de reprezentare a grafurilor este folosirea listelor de
muchii. O list de muchii este un vector simplu care reine muchiile
grafului. Aceast poate fi implementat construind o structur care reine
doi ntregi care definesc o muchi sau folosind containerul S.T.L. pair. i
aceast structur de date are unele avantaje exploatate de unii algoritmi.
Aceste metode pot fi extinse i la grafuri ponderate: matricea de
adiacen devine matrice de ntregi care n loc de 1 i 0 reine costul muchiei
respective, respectiv o valoare care semnific inexistena muchiei; listele de
adiacen mai rein o valoare mpreun cu fiecare nod, care reprezint costul
de la nodul asociat listei curente la nodul aflat la poziia respectiv etc.
n continuare vom prezenta nite modele de implementare a
metodelor discutate de reprezentare a grafurilor. Implementrile propuse fac
uz de liste nlnuite i de containerul S.T.L. vector. Este recomandat ca
cititorul s fie familiarizat cu listele nlnuite i cu noiunile de baz S.T.L.
n restul capitolului, implementarea listelor de adiacen se va face
folosind exclusiv containerul vector, fiind cel puin la fel de eficient, mai
uor de folosit i mai flexibil.
a) Declaraii
Pentru a folosi liste nlnuite avem nevoie de o structur care ne
permite folosirea acestora. Pentru a folosi vectori, tot ce trebuie s facem
este s includem fiierul antet <vector> i s declarm un vector S.T.L. de
vectori clasici.
Declaraie liste nlnuite
struct graf
{
int nod;
graf *link_;
};
graf *G[maxn]; // vector clasic de liste inalntuite
b) Iniializri
n cazul listelor nlnuite, este necesar s iniializm fiecare list cu
valoarea NULL nainte de aplicarea altor operaii. Acest lucru este necesar
pentru a ti unde se termin o list.
graf *G[maxn];
for ( int i = 1; i < maxn; ++i ) G[i] = NULL;
362
Teoria grafurilor
363
Capitolul 12
364
Teoria grafurilor
Cu matrice de adiacen
Cu vectori
#include <fstream>
using namespace std;
const int maxn = 101;
#include <fstream>
#include <vector>
using namespace std;
const int maxn = 101;
int main()
int main()
{
{
int N;
int N, M, x, y;
bool G[maxn][maxn];
vector<int> G[maxn];
ifstream in("graf.in");
ifstream in("graf.in");
ofstream out("graf.out");
in >> N >> M;
for ( int i = 1; i <= M; ++i )
in >> N;
{
for ( int i = 1; i <= N; ++i )
in >> x >> y;
{
G[x].push_back(y);
int grad = 0;
G[y].push_back(x);
for ( int j = 1;
}
j <= N;
grad += G[i][j++] )
ofstream out("graf.out");
in >> G[i][j];
for ( int i = 1; i <= N; ++i )
out << grad << '\n';
out << G[i].size() << '\n';
}
in.close(); out.close();
in.close(); out.close();
return 0;
return 0;
}
}
365
Capitolul 12
Cu matrice de adiacen
#include <fstream>
Cu vectori
#include <fstream>
#include <vector>
ifstream in("graf.in");
in >> N >> M;
for ( int i = 1; i <= M; ++i )
{
in >> x >> y;
G[x].push_back(y);
}
in.close();
ofstream out("graf.out");
out << neor;
out.close();
bool neor = 1;
for ( int i = 1; i <= N; ++i )
for ( int j = 0; j < G[i].size(); ++j )
if ( !cauta(G[ G[i][j] ], i) )
neor = 0;
ofstream out("graf.out");
out << neor;
out.close();
return 0;
}
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
Pentru o mai bun familiarizare cu structurile de date folosite n
lucrul cu grafuri i cu noiunile de baz a grafurilor, propunem urmtoarele
probleme. Cititorul este ncurajat s exploreze, pe ct posibil, mai mult de o
singur metod de rezolvare.
a) Se d un graf oarecare. S se determine dac acesta conine
bucle.
b) Se d un graf neorientat. S se determine dac acesta este
complet. Algoritmul se schimb sau nu pentru grafuri orientate?
c) Scriei un program care construiete o matrice de adiacen din
liste de adiacen i invers.
d) Scriei un program care construiete un vector de tai dintr-o
matrice de adiacen, iar apoi din liste de adiacen.
e) Scriei un program care implementeaz o matrice de adiacen
folosind operaii pe bii.
368
Teoria grafurilor
f) Se d un graf neorientat i o secven de noduri. S se determine
dac secvena reprezint un drum elementar, un drum
neelementar, un ciclu elementar, un ciclu neelementar sau
niciuna dintre aceste variante.
g) Care este numrul maxim de componente conexe a unui graf cu
2010 noduri i 100 de muchii?
h) Scriei un program care implementeaz listele de adiacen
folosind vectori clasici i alocare dinamic.
i) Scriei un program care genereaz graful transpus al unui graf
orientat dat prin list de muchii.
j) Rezolvai problemele propuse folosind containerul S.T.L. list.
k) Scriei un program care determin numrul de arbori existeni
ntr-o pdure reprezentat prin vector de tai.
Capitolul 12
Algoritmul n pseudocod folosete o funcie DFS(G, nod, V), unde
G reprezint graful reprezentat prin liste de adiacen, nod reprezint nodul
curent al parcurgerii, iar V reprezint un vector boolean n care V[i] = true
dac nodul i a fost deja parcurs i false n caz contrar. Funcia poate fi
implementat astfel:
Dac V[nod] == true se iese din funcie
V[nod] = true
Prelucreaz nodul nod
Pentru fiecare vecin i al nodului nod, apeleaz recursiv
DFS(G, i, V)
n cazul arborilor reprezentai ca grafuri orientate nu este necesar
vectorul V, deoarece nu exist posibilitatea s vizitm un nod deja vizitat,
pentru c nu avem muchie de la un nod la tatl su. n cazul grafurilor
orientate avem ns nevoie de un vector care s ne spun dac am vizitat
deja un anumit nod.
Parcurgerea n adncime este un algoritm fundamental n teoria
grafurilor, ntruct st la baza altor algoritmi i structuri de date mai
avansate. Este un algoritm uor de implementat care poate fi folosit pentru a
rezolva elegant o multitudine de probleme. De exemplu, folosind
parcurgerea n adncime putem determina distana dintre dou noduri,
nlimea arborelui, nivelul la care se afl un nod, dac exist sau nu cicluri,
ce cicluri exist i altele.
Timpul de execuie al parcurgerii n adncime pe un graf cu N
noduri i M muchii este O(N + M) n cazul reprezentrii prin liste de
adiacen i O(N2) n cazul reprezentrii prin matrice de adiacen.
n continuare vom prezenta o implementare recursiv a parcurgerii
n adncime, cteva particularizri a acestei parcurgeri, dou probleme
rezolvate i o implementare iterativ.
Programul urmtor citete din fiierul dfs.in un graf dat prin lista de
muchii i afieaz n fiierul dfs.out nodurile grafului n ordinea n care au
fost parcurse de ctre algoritm, pornind de la nodul 1.
370
Teoria grafurilor
Exemplu:
dfs.in dfs.out
45
1324
13
23
31
34
14
Exist mai multe rspunsuri corecte, n funcie de ordinea nodurilor
n listele de adiacen.
#include <fstream>
#include <vector>
using namespace std;
const int maxn = 101;
int main()
{
int N, M;
bool V[maxn];
vector<int> G[maxn];
371
citire(G, N, M);
for ( int i = 1; i <= N; ++i )
V[i] = false;
ofstream out("dfs.out");
DFS(G, 1, V, out);
out.close();
return 0;
}
Capitolul 12
n cazul arborilor exist dou tipuri de parcurgeri n adncime:
1. Parcurgerea n preordine, care este dat de algoritmul discutat i
prezentat anterior. Aceast parcurgere este cea mai natural,
prelucrnd nodurile dup principiul: prelucreaz nodul curent
nainte de a prelucra vreun subarbore.
2. Parcurgerea n postordine este dat de prelucrarea nodului
curent dup ce toate apelurile recursive s-au ncheiat. Se aplic
aadar principiul prelucreaz nodul curent dup prelucrarea
tuturor subarborilor. Dac se d un arbore i se cere parcurgerea
sa n postordine, se poate folosi funcia urmtoare:
void DFS(vector<int> G[], int nod, bool V[], ofstream &out)
{
if ( V[nod] )
return;
V[nod] = true;
for ( int i = 0; i < G[nod].size(); ++i )
DFS(G, G[nod][i], V, out);
out << nod << ' ';
}
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 stng i nainte de
apelul recursiv pentru subarborele drept. O astfel de implementare este
lsat ca exerciiu pentru cititor.
Aceste trei parcurgeri au aplicaii n algoritmica arborilor de expresii
i n determinarea componentelor tare conexe a unui graf.
Cele dou desene de mai jos reprezint ordinea prelucrrii nodurilor
n cadrul parcurgerii unui arbore binar n postordine (stnga), respectiv n
ordine (dreapta).
372
Teoria grafurilor
373
Capitolul 12
374
Teoria grafurilor
bool DFS(vector<int> G[], int nod, int pred[])
{
for ( int i = 0; i < G[nod].size(); ++i )
if ( pred[ G[nod][i] ] != nod )
{
if ( pred[ G[nod][i] ] != -1 )
return true;
else
{
pred[ G[nod][i] ] = nod;
return DFS(G, G[nod][i], pred);
}
}
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;
}
Exerciii:
a) Modificai algoritmul n aa fel nct s i afieze un ciclu n
cazul existenei unuia.
b) Cum s-ar putea rezolva problema pentru grafuri orientate?
c) Scriei un program care afieaz toate ciclurile existente ntr-un
graf.
375
Capitolul 12
376
Teoria grafurilor
377
Capitolul 12
void init(int D[], int N)
{
for ( int i = 1; i <= N; ++i )
D[i] = -1;
}
int main()
{
int N;
int D[maxn];
vector<int> G[maxn];
citire(G, N);
init(D, N);
DFS(G, 1, 0, D);
int X = 1;
for ( int i = 2; i <= N; ++i )
if ( D[i] > D[X] )
X = i;
init(D, N);
DFS(G, X, 0, D);
Exerciii:
a) Rezolvai aceeai problem pe un graf ponderat (fiecare muchie
are asociat un anumit cost).
b) Modificai programul n aa fel nct s afieze i nodurile
drumului.
c) Implementare iterativ
Pot exista cazuri n care, din considerente de timp de execuie sau de
limitri ale stivei, nu ne permitem s folosim parcurgerea n adncime
implementat recursiv. Putem ns implementa iterativ aceast parcurgere
simulnd stiva folosind un vector clasic. Implementarea este dificil, mai
378
Teoria grafurilor
ales dac folosim vectori pentru reinerea listelor de adiacen sau liste
nlnuite pe care nu dorim s le distrugem n timpul prelucrrii. Funcia
prezentat este doar orientativ; cititorul este sftuit s o studieze i s o
mbunteasc. n practic, astfel de implementri sunt foarte rar ntlnite
sau necesare.
void DFSi(vector<int> G[], int nod, bool V[], ofstream &out)
{
// poz[i] = pozitia de la care trebuie continuata
// iterarea nodurilor din lista de adiacenta a lui i
// st = stiva in care se vor retine nodurile
int poz[maxn], st[maxn];
// un mod mai simplu de a seta toate valorile pe 0
memset(poz, 0, maxn*sizeof(int));
int k = 1; // pozitia in stiva
st[k] = nod;
cout << nod << ' ';
while ( k ) // cat timp stiva nu e goala
{
int nod = st[k]; // extrage un nod din stiva
V[nod] = true;
// parcurge fiii nodului extras
bool depus = false;
for (; poz[nod] < G[nod].size(); ++poz[nod] )
if ( !V[ G[nod][ poz[nod] ] ] )
{
depus = true;
cout << G[nod][ poz[nod] ] << ' ';
st[++k] = G[nod][ poz[nod] ];
break;
}
if ( !depus )
--k;
}
}
Exerciii:
a) Folosii parcurgerea n adncime pentru a determina
componentele conexe ale unui graf.
b) Implementai varianta iterativ a parcurgerii n adncime
folosind liste nlnuite.
379
Capitolul 12
c) Implementai varianta iterativ folosind liste nlnuite
implementate manual. Codul scris se reduce cu mult. Explicai
de ce.
d) Implementai parcurgerea DF pe un graf reprezentat prin matrice
de adiacen.
e) Scriei variante iterative ale parcurgerii n adncime care parcurg
un arbore binar n preordine, ordine i postordine.
f) Scriei un program care determin, pentru mai multe perechi de
noduri, dac acestea se afl n aceeai component conex.
g) Aceeai cerin ca la f), dar pentru aceeai component tare
conex.
380
Teoria grafurilor
Acest desen ascunde o proprietate important a parcurgerii n lime:
parcurgerea n lime determin distana minim de la nodul surs la toate
celelalte noduri accesibile ale grafului. Trebuie menionat totui c acest
lucru are loc numai atunci cnd toate muchiile au acelai cost (adic graful
nu este ponderat).
Timpul de execuie al parcurgerii n lime pentru un graf cu N
noduri i M muchii este O(N + M) n cazul n care graful este reinut prin
liste de adiacen i O(N2) n cazul n care graful este reinut prin matricea
de adiacen.
Cititorii care au parcurs seciunea dedicat programrii dinamice
sunt deja familiari cu acest algoritm: acesta nu este altceva dect o
generalizare a ceea ce atunci am numit algoritmul lui Lee. Algoritmul lui
Lee este folosit pentru a determina distane minime ntr-o matrice n care
avem doar elemente accesibile i elemente neaccesibile, matrice n care ne
putem deplasa dintr-o celul n toate celulele nvecinate la stnga, dreapta,
sus, jos i eventual pe diagonale. Din punct de vedere conceptual, putem
transforma uor matricea ntr-un graf neorientat n care un nod este
caracterizat prin linia i coloana pe care s-a situat n matrice. Aadar,
algoritmul lui Lee este de fapt o parcurgere n lime.
Deoarece am prezentat deja algoritmul, nu vom insista foarte mult
asupra modului de funcionare i asupra detaliilor de implementare. Singura
diferen este c aici vom folosi containerul S.T.L. queue pentru a
implementa coada FIFO (First In First Out). Recomandm citirea seciunii
Introducere n S.T.L. cititorilor nefamiliarizai cu acest container.
Vom prezenta dou aplicaii ale parcurgerii n lime:
a) Determinarea proprietii de graf bipartit.
b) Sortarea topologic
n primul rnd, prezentm o funcie care doar afieaz nodurile unui
graf, ntr-o ordine dat de parcurgerea sa n lime, n fiierul bfs.out.
Programul ntreg are aceeai structur cu programele deja prezentate.
381
Capitolul 12
void BFS(vector<int> G[], int N)
{
queue<int> C;
bool V[maxn];
ofstream out("bfs.out");
memset(V, 0, maxn*sizeof(bool)); V[1] = true;
C.push(1);
while ( !C.empty() )
{
int nod = C.front();
out << nod << ' ';
for ( int i = 0; i < G[nod].size(); ++i )
if ( !V[ G[nod][i] ] )
{
C.push( G[nod][i] );
V[ G[nod][i] ] = true;
}
C.pop();
}
out.close();
}
382
Teoria grafurilor
Capitolul 12
verde reprezentnd nodul pe care algoritmul a ncercat s-l coloreze cu dou
culori distincte. Fiecare nod are asociat un numr care reprezint o posibil
ordine de parcurgere.
384
Teoria grafurilor
bool bipartit(vector<int> G[], int N)
{
queue<int> C;
int cul[maxn];
cul[1] = 1;
for ( int i = 2; i <= N; ++i )
cul[i] = 0;
C.push(1);
while ( !C.empty() )
{
int nod = C.front();
for ( int i = 0; i < G[nod].size(); ++i )
if ( !cul[ G[nod][i] ] ) // nu este colorat, il coloram
// in culoarea opusa nodului *nod*
{
cul[ G[nod][i] ] = 3 - cul[nod];
C.push( G[nod][i] );
}
else if ( cul[ G[nod][i] ] == cul[nod] ) // trebuie schimbata
// culoarea
return false;
C.pop();
}
return true;
}
Exerciii:
a) Implementai algoritmul prezentat folosind parcurgerea n
adncime.
b) Considerm funciile f definite pe mulimea numerelor naturale
cu valori tot n mulimea numerelor naturale. Dai exemple de
funcii pentru care graful format din muchiile (x, f(x)) este
bipartit i de funcii pentru care acelai graf nu este bipartit, cnd
x parcurge pe rnd numerele naturale.
385
Capitolul 12
b) Sortarea topologic
Considerm o mulime de activiti {A1, A2, ..., AN} i o mulime de
relaii (Ap, Aq) care semnific faptul c activitatea Ap trebuie desfurat
neaprat naintea activitii Aq. Se cere gsirea unei ordini de desfurare a
activitilor.
Rezolvarea problemei presupune modelarea acesteia ca o problem
de grafuri. Astfel, fiecare activitatea va reprezenta un nod i fiecare relaie
(Ap, Aq) va reprezenta o muchie orientat de la Ap la Aq. Graful rezultat va
fi evident un graf orientat aciclic (n caz contrar, am avea dou activiti
care depind reciproc una de cealalt, deci nu ar exista soluie). Rezolvarea
problemei se reduce la gsirea unei ordini a nodurilor n care fiecare nod i
apare dup apariia tuturor nodurilor care au muchie nspre i.
Figura de mai jos este un graf care reprezint un posibil set de date
de intrare, mpreun cu o posibil sortare topologic a sa:
Teoria grafurilor
Sortarea topologic are aplicaii n probleme de planificare a
activitilor. De exemplu, algoritmul poate fi folositor pentru a stabili
prioriti i a stabili sarcini n cadrul unui proiect complex.
Funcia urmtoare citete un graf orientat din fiierul bfs.in i
afieaz o sortare topologic a sa n fiierul bfs.out.
void topo(vector<int> G[], int N, ofstream &out)
{
int gr[maxn]; // gr[i] = gradul interior al lui i
memset(gr, 0, maxn*sizeof(int));
for ( int i = 1; i <= N; ++i )
for ( int j = 0; j < G[i].size(); ++j )
++gr[ G[i][j] ];
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 << ' ';
for ( int i = 0; i < G[nod].size(); ++i )
{
--gr[ G[nod][i] ];
if ( gr[ G[nod][i] ] == 0 )
C.push( G[nod][i] );
}
C.pop();
}
}
387
Capitolul 12
Teoria grafurilor
Dac exist drum doar de la x la y sau doar de la y la x, graful se numete
conex sau slab conex.
n rezolvarea problemei vom folosi algoritmul lui Kosaraju, care
pornete de la urmtoarea idee pentru a determina componentele tare conexe
ale lui G:
Se construiete graful transpus GT .
Ct timp exist noduri n G execut
o Alege un nod nod din G
o Eticheteaz toate nodurile din G accesibile din nod
(inclusiv nod) cu +.
o Eticheteaz toate nodurile din GT accesibile din nod
(inclusiv nod) cu .
o Nodurile etichetate att cu + ct i cu reprezint o
component tare conex. Acestea se elimin din cele
dou grafuri.
Aceast metod poart numele de algoritmul plus-minus.
Complexitatea acestuia, n cazuri favorabile este O(N + M) pe un graf cu N
noduri i M muchii. Exist ns grafuri pe care algoritmul are un timp de
execuie de O(N2). Exerciiu: gsii un astfel de caz.
Algoritmul lui Kosaraju este o optimizare a algoritmului
plus-minus, optimizare care face algoritmul s aib timpul de execuie de
O(N + M) pe toate cazurile. Aceast optimizare const n reinerea
nodurilor grafului G n ordinea dat de parcurgerea n postordine a grafului.
Se prelucreaz apoi nodurile din GT n ordinea invers n care acestea apar
n parcurgerea n postordine a lui G. Se eticheteaz nodurile accesibile
dintr-un nod fixat cu k, care iniial este 0 i care se incrementeaz dup
fiecare etichetare. Nodurile marcate cu 1 vor reprezenta prima component
tare conex, cele cu 2 a doua, ..., cele cu k a k-a component tare conex.
Programul urmtor afieaz direct componentele tare conexe ale
unui graf citit, fr a efectua propru-zis etichetarea menionat.
389
Capitolul 12
#include <fstream>
#include <vector>
int x, y;
for ( int i = 1; i <= M; ++i )
{
in >> x >> y;
G[x].push_back(y);
GT[y].push_back(x);
}
}
int main()
{
int N, M;
vector<int> G[maxn], GT[maxn];
bool V[maxn];
in.close();
}
void DFS_G(vector<int> G[], int nod,
bool V[],
vector<int> &postord)
{
if ( V[nod] )
return;
V[nod] = true;
vector<int> postord;
for ( int i = 1; i <= N; ++i )
DFS_G(G, i, V, postord);
ofstream out("ctc.out");
for ( int i = 1; i <= N; ++i )
if ( V[i] )
{
DFS_GT(GT, i, V, out);
out << '\n';
}
out.close();
return 0;
}
390
Teoria grafurilor
391
Capitolul 12
392
Teoria grafurilor
Figura urmtoare prezint arborele DFS anterior completat cu
valorile D (rou) i minim (albastru).
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] ]);
if ( minim[ G[nod][i] ] >= D[nod] )
critic = 1;
}
else
minim[nod] = min(minim[nod], D[ G[nod][i] ]);
// nodul 1 e critic doar daca are cel putin 2 fii
if ( (nod == 1 && nrf > 1) || (nod != 1 && critic) )
out << nod << ' ';
}
Exerciii:
a) Problema se poate rezolva i mai intuitiv, dar mai puin eficient.
Care ar fi un algoritm naiv de rezolvare?
b) Modificai programul prezentat astfel nct s afieze muchiile
critice, adic acele muchii a cror nlturare ar deconecta graful.
c) Modificai programul prezentat astfel nct s afieze
componentele biconexe ale grafului.
Teoria grafurilor
395
Capitolul 12
Pentru fiecare muchie (nod, i) din G execut
o Elimin muchia (nod, i)
o Apeleaz recursiv euler(G, i, st)
Adaug-l pe nod la sfritul lui st.
La finalul algoritmului, vectorul st, citit invers, ne va da un drum sau
un ciclu eulerian al lui G. Din punct de vedere al implementrii
algoritmului, tergerea nodului i din lista de adiacen a lui nod se va face
atribuind poziiei lui i valoarea -1. O valoare de -1 va indica faptul c acea
poziie nu reprezint un nod existent n lista de adiacen curent.
Atenie! Deoarece implementarea prezentat lucreaz cu grafuri
neorientate, pentru a terge o muchie (nod, i), trebuie ters att i din lista lui
nod ct i nod din lista lui i!
Desenele urmtoare prezint modul de execuie al algoritmului
(ordinea efecturii apelurilor recursive) pe un graf neorientat care admite
doar un drum eulerian. Cu rou sunt marcate nodurile i muchiile care fac
parte dintr-un ciclu nedescoperit nc, iar cu albastru nodurile i muchiile
care fac parte dintr-un ciclu deja descoperit.
396
Teoria grafurilor
Capitolul 12
acestui algoritm este aadar O(M) pentru un graf cu M muchii. n final,
st = {8, 7, 4, 6, 5, 4, 3, 2, 4, 1}. Citit invers, obinem urmtorul drum
eulerian: 1 4 2 3 4 5 6 4 7 8.
#include <fstream>
#include <vector>
using namespace std;
const int maxn = 101;
int main()
{
int N, M;
vector<int> G[maxn], st;
398
citire(G, N, M);
euler(G, 1, st);
ofstream out("euler.out");
for ( int i = st.size() - 1;
i >= 0;
--i )
out << st[i] << ' ';
out.close();
return 0;
}
Teoria grafurilor
Capitolul 12
4. Teorema Bondy-Chvtal (1972): un graf G cu N noduri este
hamiltonian dac i numai dac cl(G) este hamiltonian. Prin
cl(G) nelegem nchiderea lui G, adic graful obinut prin
adugarea de muchii ntre oricare dou noduri neadiacente a lui
G a cror sum a gradelor este cel puin N.
Folosind a patra teorem, putem determina eficient existena unui
ciclu hamiltonian, dar gsirea efectiv a unui astfel de ciclu rmne o
problem dificil.
n cele ce urmeaz vom aborda o problema mai interesant din punct
de vedere algoritmic i anume problema comis voiajorului. Practic, n
aceast problem avem ca date de intrare un graf neorientat conex i
ponderat, adic fiecare muchie are ataat un cost (sau o distan). Se cere
un drum (uneori un ciclu) hamiltonian de cost minim. Nici aceast problem
nu are soluii deterministe eficiente, dar vom prezenta dou abordri
probabiliste care sunt mult mai rapide dect abordrile determineste i care
furnizeaz rspunsuri foarte apropiate de optimul global.
De exemplu, dac atribuim costuri muchiilor grafului din exemplul
anterior, avem urmtorul graf ponderat:
Teoria grafurilor
apeluri .a.m.d. Complexitatea algoritmului n cel mai ru caz este aadar
O(N!).
Deoarece complexitatea algoritmului este O(N!) nu ne vom permite
s lucrm cu grafuri cu mai mult de ~10 noduri. Aadar, pentru o
implementare mai simpl vom folosi matrici de adiacen pentru
reprezentarea grafului. Considernd 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 asemntoare cu o parcurgere n
adncime. Va trebui doar s adugm civa parametri suplimentari funciei
de parcurgere. Fie hamilton(G, nod, nr, c, cmin, V, st, sol) o funcie care
construiete n sol un drum hamiltonian de cost minim cmin. Variabila nod
reprezint nodul curent, nr reprezint numrul de noduri deja parcurse, st
reprezint drumul curent iar c reprezint costul drumului curent. Aceast
funcie poate fi implementat n felul urmtor:
Dac c > cmin sau V[nod] == true se iese din funcie
Adaug i n st
Dac nr == N i c < cmin execut
o Actualizeaz sol i cmin
o Ieire din funcie
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
Rezolvarea folosete metoda backtracking pentru parcurgerea tuturor
drumurilor. Datorit optimizrii facute n prima linie a programului, aceea
de a nu continua pe traseul curent dac costul acestuia este deja mai mare
dect minimul gsit pn acum, este posibil ca pe grafuri generate aleator
algoritmul s funcioneze mai bine dect ar indica notaia asimptotic.
Totui, n practic, aceast metod este ineficient pentru un numr mare de
noduri (mii, zeci de mii) i vom ncerca s gsim soluii mai eficiente.
Prezentm totui o implementare a acestei metode.
Datele de intrare se citesc din fiierul hamilton.in, care conine pe
prima linie numerele N M x, iar pe urmtoarele M linii o list de muchii
ponderate care descriu un graf semi-hamiltonian. n fiierul de ieire
hamilton.out se va afia costul minim al unui drum hamiltonian i un drum
care are acest cost.
401
Capitolul 12
#include <fstream>
#include <vector>
using namespace std;
const int maxn = 101;
int main()
{
int G[maxn][maxn], N, M, x;
bool V[maxn];
citire(G, N, M, x);
402
ofstream out("hamilton.out");
out << cmin << '\n';
for ( int i = 1; i <= N; ++i )
out << sol[i] << ' ';
out.close();
return 0;
}
Teoria grafurilor
Algoritmii folosii pentru a rezolva instane ale problemei cu un
numr foarte mare de noduri sunt algoritmi probabiliti. n cele ce urmeaz
vom prezenta un algoritm genetic care rezolv problema i un algoritm
aleator mai eficient i care se comport totodat foarte bine n practic.
Pentru algoritmul genetic, avem nevoie de:
1. O metod de codificare a unui drum hamiltonian. Este clar c
vom reine pur i simplu un vector cu N noduri care va
reprezenta un astfel de drum.
2. Unul sau mai muli operatori genetici care se aplic
cromozomilor n cadrul trecerii de la o generaie la alta.
3. O funcie de adecvare.
Operatorii genetici care pot fi folosii sunt:
Operatorul de mutaie: se aleg dou poziii i i j dintr-un
cromozom i se interschimb nodurile reinute n acele poziii.
Operatorul de inversiune: se aleg dou poziii i i j dintr-un
cromozom i se nlocuiete secvena [i, j] a acestui cromozom cu
inversul acesteia.
Operaia de recombinare trebuie tratat puin diferit, deoarece avem
de-a face cu permutri i nu putem s concatenm pur i simplu dou
secvene disjuncte a doi cromozomi diferii fr a strica validitatea
cromozomului rezultat. Vom aplica aadar urmtorul algoritm pentru
recombinare:
Fie C1 i C2 cei doi cromozomi din care vrem s obinem un
cromozom pentru generaia viitoare.
Se copiaz primele k gene (elemente) din C1 n cromozomul
rezultat, unde k este un numr aleator. Din C2 se copiaz toate
genele care nu se afl deja n cromozomul rezultat.
Funcia de adecvare va fi evident costul traseului codificat de ctre
un cromozom.
Algoritmul aleator este mai uor de implementat i presupune
mbuntirea unui drum ales aleator cu ajutorul efecturii unor schimbri
aleatoare.
Mai exact:
Se genereaz aleator un drum valid sol (o permutare a primelor
N numere naturale care ncepe cu x)
Se execut de k ori (cu ct k este mai mare, cu att este mai mare
probabilitatea s gsim un optim global)
403
Capitolul 12
o Efectueaz dou interschimbri aleatore n sol.
o Dac noul vector sol codific o soluie de cost mai mic,
se pstreaz interschimbrile, altfel se anuleaz.
Deoarece lucrm cu permutri i nu avem garania faptului c graful
citit este graf complet, vom aduga muchii de cost infinit (un numr foarte
mare) grafului pn ce acesta devine complet. Probabilitatea ca muchiile de
cost infinit adugate s fac parte dintr-o soluie furnizat de oricare
algoritm este foarte mic.
Implementrile acestor algoritmi sunt similare cu altele prezentate
deja, aa c le lsm ca exerciiu pentru cititor.
404
Teoria grafurilor
Capitolul 12
Unde 0 semnific inexistena unei muchii ntre cele dou noduri. n
cadrul implementarii, valorile de 0 care nu se afl pe diagonala principal
vor trebui nlocuite cu infinit.
Complexitatea algoritmului Roy-Floyd este O(N3 ), ceea ce nu l face
aplicabil pe grafuri cu mai mult de cteva sute de noduri. O proprietate
important a acestui algoritm este faptul c poate detecta dac exist cicluri
de cost negativ ntr-un graf. Un ciclu de cost negativ este un drum de la i la
i a crui cost scade cu fiecare parcurgere. De exemplu, graful urmtor
conine un astfel de ciclu:
406
Teoria grafurilor
#include <fstream>
using namespace std;
const int maxn = 101;
const int inf = 1 << 29;
void citire(int G[maxn][maxn],
int &N)
{
ifstream in("rf.in");
in >> N;
for ( int i = 1; i <= N; ++i )
for ( int j = 1; j <= N; ++j )
{
in >> G[i][j];
if ( i != j && !G[i][j] )
G[i][j] = inf;
}
in.close();
}
}
Capitolul 12
void reconst(int G[maxn][maxn], int N, int x, int y)
{
for ( int k = 1; k <= N; ++k )
if ( G[x][k] + G[k][y] == G[x][y] && k != x && k != y )
{
reconst(G, N, x, k);
reconst(G, N, k, y);
return; // ne intereseaza un singur drum
}
// daca s-a ajuns aici, (x, y) e muchie in graful initial
cout << x << ' ' << y << '\n';
}
Exerciii:
a) Ce se ntmpl dac folosim const int inf = 1 << 30; ?
b) Modificai algoritmul astfel nct s determine dac un graf este
conex (sau tare conex).
c) Memorai, pentru fiecare pereche (i, j) din cadrul algoritmului de
calculare a costurilor minime, nodul k intermediar ales. Scriei o
funcie de reconstituire care folosete aceste informaii pentru a
gsi mai eficient drumurile.
d) Modificai algoritmul de reconstituire astfel nct s afieze
nodurile unui drum n loc de muchiile unui drum.
408
Teoria grafurilor
cnd au fost alese toate nodurile. Actualizarea se face verificnd dac
distana pn la min plus costul muchiei de la min la i este mai mic dect
distana pn la i.
Figurile urmtoare prezint modul de execuie al algoritmului pe un
graf oarecare. Cu rou apar distanele minime calculate de ctre algoritm de
la nodul 1 la toate celelalte noduri mpreun cu nodurile i muchiile deja
parcurse, iar cu verde nodul min i vecinii si.
Capitolul 12
n implementarea clasic folosim un vector D cu semnificaia
D[i] = distana minim de la nodul 1 la nodul i, un vector V cu semnificaia
V[i] = true dac nodul i a fost deja extras ca minim i false n caz contrar i
un vector P cu semnificaia P[i] = ultimul nod care a mbuntit distana
pn la i. Vectorul P va fi folosit pentru a reconstitui soluia. Programul
urmtor citete un graf dat prin lista de muchii i afieaz un drum de cost
minim de la nodul 1 la nodul N.
#include <fstream>
#include <vector>
#include <utility>
using namespace std;
const int maxn = 101;
const int inf = 1 << 29;
int x, y, c;
for ( int i = 1; i <= M; ++i )
{
in >> x >> y >> c;
G[x].push_back(make_pair(y,c));
G[y].push_back(make_pair(x,c));
}
in.close();
}
void drum(int N, int P[maxn],
ofstream &out)
{
if ( !N )
return;
}
}
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;
}
Capitolul 12
Heap-ul va fi un min-heap ordonat dup distanele din vectorul
D.
Ct timp heap-ul este nevid execut
o Se salveaz n min valoarea din rdcina heap-ului (adic
H[1], care reprezint nodul pn la care distana de la
nodul surs este minim i care nu a mai fost selectat
pn acuma) i se terge rdcina. Rdcina se terge
interschimbnd H[1] cu H[k], sczndu-l pe k cu 1 i
aplicnd procedura Downheap lui H[1], unde k
reprezint numrul de elemente din heap.
o Se ncearc relaxarea tuturor vecinilor lui min, ca i n
cadrul implementrii clasice. Dac un vecin i i
mbuntete distana minim D[i], avem dou cazuri:
Dac i se afl deja n heap (poz[i] != -1), este
posibil ca i s trebuiasc s urce n heap. Pentru a
urca un element n heap, vom folosi o funcie
numit Upheap care interschimb un nod x
transmis ca parametru (practic, se transmite
poziia lui x n heap) cu tatl su (tatl
elementului H[x] este H[x / 2]) atta timp ct are
loc inegalitatea:
D[ H[x] ] < D[ H[ x / 2] ].
Atenie la implementare! va trebui actualizat i
vectorul poz dup ce se efectueaz o
interschimbare!
Dac i nu se afl n heap (poz[i] == -1), atunci
trebuie inserat. Se incrementeaz k, se adaug i n
H[k], poz[i] devine k i se apeleaz procedura
Upheap cu poziia nodului nou inserat.
Noul algoritm este mult mai eficient atunci cnd 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
puin implementarea este faptul c avem nevoie de o modalitate de a ordona
coada de prioriti dup un criteriu dat de noi, deoarece n aceasta vom
reine etichetele nodurilor, iar ordonarea vrem s se fac dup distanele
minime pn 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 relaii de ordine n funcie de prima component (avem nevoie de
412
Teoria grafurilor
greater<pair> pentru priority_queue). Aadar, putem declara un
priority_queue care reine etichetele nodurilor ordonate cresctor dup
distanele pn la nodurile reinute! Implementarea funciei Dijkstra este
foarte simpl:
void Dijkstra(vector<PER> G[], int N, int D[], int P[])
{
for ( int i = 1; i <= N; ++i )
D[i] = inf, P[i] = 0;
D[1] = 0;
priority_queue<PER, vector<PER>, greater<PER> > Q;
Q.push( make_pair(D[1], 1) );
while ( !Q.empty() )
{
PER tmp = Q.top();
Q.pop();
int min = tmp.second;
if ( tmp.first != D[min] )
continue;
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;
Q.push( make_pair(D[ i->first ], i->first) );
}
}
}
Capitolul 12
semnificativ asupra performanei algoritmului, deoarece verificm dac
perechea extras la pasul curent are distana care trebuie. De multe ori se
prefer aadar aceast implementare.
Dou probleme importante n determinarea drumurilor de cost
minim sunt date de existena muchiilor de cost negativ i a ciclurilor de cost
negativ. Dei acestea nu apar n majoritatea problemelor, exist probleme
modelabile cu grafuri n care apar muchii de cost negativ sau chiar cicluri de
cost negativ. n continuare vom analiza comportamentul algoritmului lui
Dijkstra pe grafuri cu muchii sau cicluri de cost negativ.
Vom considera urmtorul graf, orientat de data aceasta:
Teoria grafurilor
i se mbuntete la un anumit pas, atunci vom permite ca acel nod s fie
selectat ca minim chiar dac a mai fost deja selectat (adic vom seta
V[i] = false).
n cadrul implementrii clasice, este necesar n primul rnd
modificarea secvenei urmtoare:
for ( j = G[min].begin(); j != G[min].end(); ++j )
if ( D[min] + j->second < D[ j->first ] )
{
D[ j->first ] = D[min] + j->second;
P[ j->first ] = min;
V[ j->first ] = false;
}
Capitolul 12
relaxm distanele minime pn la fiecare vecin al nodului selectat.
Algoritmul Bellman Ford nu se bazeaz pe selectarea unui minim, ci pe
relaxarea tuturor distanelor ntr-o ordine oarecare de N 1 ori (unde N
este numrul de noduri ale grafului). Aadar, se parcurg toate muchiile (x, y)
de N 1 ori i se verific dac putem mbunti distana pn la y folosind
muchia (x, y), adic dac D[x] + C[x][y] < D[y], unde D[i] reprezint
distana minim pn la nodul i, iar C[i][j] reprezint costul arcului sau
muchiei (i, j).
Parcurgerea tuturor muchiilor de N 1 ori permite distanelor
minime s se propage n tot graful, deoarece, n absena ciclurilor de cost
negativ, un drum de cost minim poate vizita un nod cel mult o singur dat.
Spre deosebire de algoritmul lui Dijkstra, un algoritm de tip greedy care
profit de anumite particulariti ale problemei (n acest caz lipsa arcelor de
cost negativ), algoritmul Bellman Ford funcioneaz i pe cazul general.
Figura urmtoare prezint modul de execuie al algoritmului pe un
graf orientat. Pe fiecare desen sunt trecute cu verde iteraia curent
(1 i < N), iar cu rou muchia curent i nodurile incidente acesteia,
mpreun cu distanele minime pn la fiecare nod. Deoarece ordinea
parcurgerii muchiilor este aleatoare, pot exista mai multe soluii.
Teoria grafurilor
Exist mai multe metode de a implementa algoritmul. Metoda
clasic presupune reinerea grafului prin liste de muchii. S presupune c
graful este reinut n lista de muchii E i c tripletul (E[i].x, E[i].y, E[i].c)
este format din nodul surs al muchiei i, nodul destinaie al muchiei i,
respectiv costul muchiei i. Atunci algoritmul poate fi implementat n felul
urmtor:
Pentru fiecare i de la 1 la N 1 execut
o Pentru fiecare j de la 1 la M (numrul de muchii) execut
Dac D[ E[j].x ] + E[j].c < D[ E[j].y ] execut
D[ E[j].y ] = D[ E[j].x ] + E[j].c
P[ E[j].y ] = E[j].x
Dac exist un j, 1 j M, pentru care condiia de mai sus se
verific, atunci exist un ciclu de cost negativ n graf.
La finalul execuiei, vectorul D va conine distanele minime de la
nodul surs la toate celelalte noduri, iar P va fi vectorul de predecesori.
Trebuie fcute aceleai iniializri ca i pentru algoritmul lui Dijkstra.
Complexitatea acestui algoritm este O(NM).
Exist o implementare care, n practic, se dovedete a fi mult mai
eficient dect implementarea corespunztoare pseudocodului de mai sus.
Aceast implementare este foarte similar cu o parcurgere n lime, singura
deosebire fiind c un nod poate fi introdus n coad de N 1 ori. Aadar,
complexitatea teoretic rmne O(NM), dar algoritmul se comport mai
bine n practic. Vom prezenta modul de funcionare al algoritmului pe
exemplul anterior, presupunnd c vom folosi coada Q. Prima dat se
insereaz n coad nodul surs. Am marcat cu albastru nodul extras din
coad la pasul curent i cu rou nodurile introduse n coad la pasul curent
i valorile actualizate. La fiecare pas, se ncearc mbuntirea distanei
minime pn la vecinii nodului extras trecnd prin nodul extras. Un nod nu
va fi introdus n coad dac se afl deja acolo, deoarece, de data aceasta,
natura algoritmului nu necesit acest lucru.
Pasul 1
i 1 2
3
4
Q 1
D 0 inf inf inf
P 0
417
Capitolul 12
i
Q
D
P
i
Q
D
P
Pasul 2
1 2 3 4
1 2 3
0 4 2 inf
0 1 1
Pasul 4
1 2 3
1 2 3
0 4 1
0 1 2
i
Q
D
P
4
4
5
3
i
Q
D
P
Pasul 3
1 2 3 4
1 2 3
0 4 1 inf
0 1 2
Pasul 5
1 2 3
1 2 3
0 4 1
0 1 2
4
4
5
3
Teoria grafurilor
n literatura de specialitate, algoritmul prezentat apare i sub
denumirile de algoritmul Bellman Ford Moore (mai ales n cadrul
implementrii ce folosete o coad) i algoritmul Bellman Kalaba.
Deoarece ideea care st la baza fiecrei implementri este aceeai, am ales
folosirea denumirii algoritmului clasic pentru fiecare implementare.
n implementarea urmtoare am presupus un format al fiierelor
identic cu cel de la algoritmul lui Dijkstra, cerinele fiind identice i ele.
Prezentm aadar doar funcia de rezolvare efectiv a problemei.
void BellmanFord(vector<PER> G[], int N, int D[], int P[])
{
// V[i] = true daca i se afla in coada si false altfel
bool V[maxn];
for ( int i = 0; i <= N; ++i )
D[i] = inf, P[i] = 0, V[i] = false;
D[1] = 0;
V[1] = true;
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 gleat la pasul
respectiv. n practic, se trece i peste gleile goale.
Tabelul 12.10.7. Modul de execuie al algoritmului lui Dial
0 1 2 3 4 5 6 7 8 9 10
19
1
2
3
1
2 3 3
...
1
2 3 3
4
1
2 3 3
4
1
2 3 3
4
Complexitatea algoritmului este O(Nmaxc + M) ca timp i
O(Nmaxc) ca memorie auxiliar. n practic ns algoritmul ruleaz foarte
rapid, deoarece multe din cele Nmaxc cozi vor fi goale.
Memoria auxiliar folosit de algoritm poate fi mbuntit
observnd c nu este necesar s reinem Nmaxc cozi, fiind suficiente
maxc + 1. Mai mult, vom reine un contor cnt care va reprezenta numrul
de elemente din toate cele maxc + 1 cozi i vom opri algoritmul atunci cnd
cnt devine 0. n cadrul acestui algoritm, nu vom mai insera un nod j n
coada Q[ D[j] ] ci n coada Q[ D[j] % (maxc + 1) ]. Pe graful anterior,
modul de execuie al algoritmului este urmtorul:
Tabelul 12.10.8. Modul de execuie al algoritmului lui Dial optimizat
0 1 2 3 4 5
1
2
3
1
2 3 3
1
4 2 3 3
1
4 2 3 3
Se observ c nodul 4, care are D[4] = 8, este inserat n coada Q[2],
deoarece 8 % 6 = 2.
Astfel, memoria auxiliar folosit de ctre algoritm este O(maxc).
Algoritmul se bazeaz pe observaia c, folosind aceast metod, dac la
pasul curent am analizat un nod din coada Q[k], atunci la sfritul pasului
curent cozile Q[k + 1], ..., Q[Cmax], Q[0], ..., Q[k 1] vor conine noduri
cu distane din ce n ce mai mari, deci se pstreaz parcurgerea nodurilor n
ordinea cresctoare a distanelor minime.
421
Capitolul 12
Faptul c vom opri algoritmul atunci cnd toate cozile sunt goale va
face algoritmul mult mai rapid n practic, datorit faptului c nu vor exista
aproape niciodat un numr mare de distane minime distincte.
Implementarea prezentat conine toate optimizrile discutate. Acest
algoritm este indicat a fi folosit n cazurile n care costurile muchiilor sunt
mici sau distanele minime se repet des. Datorit faptului c folosim
indexarea dup distane, este clar c algoritmul lui Dial nu va funciona
corect n cazul existenei distanelor negative. n cazul general, rmne
aadar preferabil algoritmul Bellman Ford sau algoritmul lui Dijkstra
implementat cu heap-uri.
Menionm c n cadrul implementrii prezentate am presupus c
lungimea maxim a unui arc este 1000. Mai mult, am folosit un vector de
1024 de cozi pentru ca operaia modulo s se poat efectua mai eficient cu
ajutorul operaiilor pe bii. Implementarea conine doar funcia relevant.
const int maxc = 1023;
void Dial(vector<PER> G[], int N, int D[], int P[])
{
for ( int i = 0; i <= N; ++i ) D[i] = inf, P[i] = 0;
D[1] = 0;
queue<int> Q[maxc + 1];
Q[0].push(1);
int cnt = 1;
for ( int cst = 0; cnt; ++cst )
for ( ; !Q[cst & maxc].empty(); Q[cst & maxc].pop() )
{
int j = Q[cst & maxc].front();
--cnt;
if ( D[j] != cst )
continue;
vector<PER>::iterator i;
for ( i = G[j].begin(); i != G[j].end(); ++i )
if ( D[j] + i->second < D[ i->first ] )
{
D[ i->first ] = D[j] + i->second;
P[ i->first ] = j;
Q[ D[ i->first ] & maxc ].push( i->first ); // modulo (maxc+1)
++cnt;
}
}
}
422
Teoria grafurilor
423
Capitolul 12
n cele ce urmeaz vom prezenta algoritmi de determinare a fluxului
maxim n grafuri ct i probleme care au la baz aceti algoritmi. Mai exact,
vom aborda urmtoarele teme:
a) Algoritmul de flux maxim Edmonds Karp
b) Fluxul maxim de cost minim
c) Cuplajul maximal n graf bipartit
Teoria grafurilor
425
Capitolul 12
Aadar, cu o simpl scdere ne putem asigura c algoritmul va
funciona pe orice graf. Prezentm mai nti funciile relevante unei prime
implementri clasice. Am presupus c graful este dat prin list de muchii,
reinut ca graf neorientat in G ca pn acum i c C i F sunt matricele
menionate anterior. Funcia Drum gsete drumuri de ameliorare, iar
funcia Flux gsete fluxul maxim n graful G.
bool Drum(vector<int> G[], int N, int C[maxn][maxn],
int F[maxn][maxn], int P[], bool V[])
{
for ( int i = 2; i <= N; ++i )
V[i] = false;
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(vector<int> G[], int N, int C[maxn][maxn])
{
int F[maxn][maxn], P[maxn];
bool V[maxn]; V[1] = true;
for ( int i = 1; i <= N; ++i )
for ( int j = 1; j <= N; ++j )
F[i][j] = 0;
P[1] = 0;
int flux_total = 0;
while ( Drum(G, N, C, F, P, V) )
{
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];
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;
}
Capitolul 12
cantiti de flux pe un anumit drum poate bloca celelalte
drumuri, aa c, dac min este 0, putem scpa de nc o
parcurgere a N noduri, deoarece nu are rost s trimitem cantitatea
0 de flux.
Prezentm doar secvenele de cod care se modific:
// ...
while ( !Q.empty() )
{
int nod = Q.front();
Q.pop();
if ( nod == N ) continue;
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
Capitolul 12
a calcula vectorul distanelor D. Dup ce avem calculat vectorul D, costul
fiecrui arc (x, y), de cost iniial c, va fi nlocuit cu valoarea
c + D[x] D[y]. Aceast valoare este pozitiv, aa cum vom arta n
continuare. S presupunem c c + D[x] D[y] < 0. Asta ar nsemna c
c + D[x] < D[y], contrazicndu-se astfel minimalitatea valorilor din D. Mai
mult, costurile difer printr-o constant, deci rezultatul nu va fi afectat de
aceast transformare. Putem acum aplica algoritmul lui Dijkstra n forma sa
clasic pentru determinarea drumurilor de ameliorare.
Implementarea prezentat conine doar funciile relevante. Am ales
algoritmul lui Dijkstra implementat cu priority_queue. Acest algoritm
funcioneaz i dac se face transformarea precizat mai sus i dac nu. Am
introdus o nou matrice numit CS care reine costurile arcelor. Funcia
Flux returneaz costul minim al unui flux maxim.
Restul implementrilor menionate sunt lsate ca exerciiu.
int Dijkstra(vector<int> G[], int N, int C[maxn][maxn],
int F[maxn][maxn], int CS[maxn][maxn], int P[], int D[])
{
for ( int i = 1; i <= N; ++i ) D[i] = inf;
D[1] = 0;
priority_queue<PER, vector<PER>, greater<PER> > Q;
Q.push( make_pair(D[1], 1) );
while ( !Q.empty() )
{
PER tmp = Q.top();
Q.pop();
int min = tmp.second;
if ( tmp.first != D[min] ) continue;
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;
Q.push( make_pair(D[*i], *i) );
}
}
return D[N];
}
430
Teoria grafurilor
int Flux(vector<int> G[], int N, int C[maxn][maxn], int CS[maxn][maxn])
{
int F[maxn][maxn], P[maxn], D[maxn];
for ( int i = 1; i <= N; ++i )
for ( int j = 1; j <= N; ++j )
F[i][j] = 0;
P[1] = 0;
int cst_min = 0, tmp_cst;
while ( true )
{
tmp_cst = Dijkstra(G, N, C, F, CS, P, D);
if ( tmp_cst == inf )
break;
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];
for ( int x = N; x != 1; x = P[x] )
{
F[ P[x] ][x] += min;
F[x][ P[x] ] -= min;
}
cst_min += tmp_cst * min;
}
return cst_min;
}
Capitolul 12
Putem modela problema cu ajutorul unui graf bipartit, n care
mulimile de noduri sunt st i dr, reprezentnd angajaii respectiv sarcinile
existente. De exemplu, graful urmtor reprezint un posibil set de date de
intrare. Am marcat cu rou muchiile unui cuplaj maximal.
Teoria grafurilor
considera nodul 0 ca fiind supersursa i nodul N + M + 1 ca fiind
superdestinaia. Aceste noduri sigur nu vor face parte din graful iniial,
deoarece acesta are N + M noduri, numerotate de la 1 la N i de la 1 la M.
Pentru rezolvarea problemei n care dou noduri distincte au aceeai
etichet, vom renumerota nodurile. Astfel, nodurile care reprezint angajaii
vor fi numerotate de la 1 la N, iar nodurile care reprezint sarcinile vor fi
numerotate de la N + 1 la N + M. Practic, cnd citim o muchie (x, y), vom
aduga muchia (x, N + y). Problema se reduce aadar la gsirea fluxului
maxim de la nodul 0 la nodul N + M + 1. Modificarea algoritmilor de flux
maxim prezentai pentru a funciona pe grafuri neorientate nu prezint
dificulti prea mari, aa c este lsat ca exerciiu pentru cititor.
Graful de mai jos reprezint transformarea grafului iniial conform
indicaiilor anterioare. Fiecare muchie are capacitatea 1, iar muchiile
saturate apar colorate. Evident, din cuplajul maximal fac parte doar muchiile
saturate neincidente cu supersursa sau superdestinaia.
Capitolul 12
Vom exemplifica algoritmul pe urmtorul graf:
434
Teoria grafurilor
Capitolul 12
#include <fstream>
#include <vector>
using namespace std;
const int maxn = 101;
vector<int>::iterator v;
for ( v = G[i].begin();
v != G[i].end(); ++v )
if ( !st[*v] )
{
st[*v] = i;
dr[i] = *v;
return true;
}
int x, y;
for ( int i = 1; i <= E; ++i )
{
in >> x >> y;
G[x].push_back(y);
}
for ( v = G[i].begin();
v != G[i].end(); ++v )
if ( Cuplare(G, st[*v], st, dr, V) )
{
st[*v] = i;
dr[i] = *v;
return true;
}
in.close();
}
return false;
}
436
Teoria grafurilor
void HopcroftKarp(vector<int> G[], int N)
{
int st[maxn], dr[maxn];
bool V[maxn], ok = true;
for ( int i = 1; i < maxn; ++i )
st[i] = dr[i] = 0;
do
{
ok = false;
for ( int i = 1; i <= N; ++i )
V[i] = false;
for ( int i = 1; i <= N; ++i )
if ( !dr[i] )
ok |= Cuplare(G, i, st, dr, V);
} 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;
}
Capitolul 12
438
Teoria grafurilor
439
Capitolul 12
1. Optimizarea reuniunii dup rang. Aceast optimizare const
n meninerea unui vector R cu semnificaia R[i] = rangul
(nlimea) arborelui cu rdcina n i. Acum, cnd reunim doi
arbori identificai prin rdcinile x i y, vom subordona arborele
cu nlime mai mic celui cu nlime mai mare, iar nlimea
ambilor arbori va rmne neschimbat. Dac nlimile celor doi
arbori sunt egale, atunci nu conteaz cum i subordonm, dar este
important s incrementm rangul pentru cel care i pstreaz
calitatea de arbore, deoarece nlimea noului arbore va crete cu
1.
2. Optimizarea comprimrii drumurilor. Pn acum, n cadrul
apelului Find(x), parcurgem arborele de la nodul x n sus pn
cnd ajungem la rdcin, trecnd prin mai multe noduri
intermediare, care nu ne intereseaz. Deoarece nodurile
intermediare nu prezint interes, putem s mai parcurgem o dat
arborele dinspre x spre rdcin i s unim toate nodurile
intermediare (inclusiv pe x) direct de rdcin. Dei acest lucru
afecteaz nlimea arborilor, nu vom actualiza vectorul R. Cu
aceast optimizare, la urmtoarea parcurgere a arborelui, vom
gsi rdcina unui nod ntr-un singur pas. Optimizarea aceasta se
poate implementa uor folosind recursivitate.
Programul urmtor afieaz costul i muchiile care formeaz
arborele parial de cost minim al unui graf citit.
#include <fstream>
#include <vector>
#include <algorithm>
using namespace std;
const int maxn = 101;
const int maxm = 201;
struct muchie { int x, y, c; };
void citire(muchie E[], int &N, int &M)
{
ifstream in("kruskal.in");
in >> N >> M;
for ( int i = 1; i <= M; ++i )
in >> E[i].x >> E[i].y >> E[i].c;
in.close();
}
440
Teoria grafurilor
bool operator<(const muchie &x,
const muchie &y)
{
return x.c < y.c;
}
return T[x];
}
void Merge(int x, int y,
int T[], int R[])
{
if ( R[x] == R[y] )
{
T[y] = x;
++R[x];
}
else if ( R[x] < R[y] )
T[x] = y;
else
T[y] = x;
}
Merge(Find(E[i].x, T),
Find(E[i].y, T), T, R);
}
ofstream out("kruskal.out");
out << cost << '\n';
for ( int i = 1; i < N; ++i )
out << APM[i].x << ' ' <<
APM[i].y << '\n';
out.close();
}
int main()
{
int N, M;
muchie E[maxm];
citire(E, N, M);
Kruskal(E, N, M);
return 0;
}
441
Capitolul 12
Teoria grafurilor
(min, vec[min]) n arbore i vec[min] devin 0, deoarece l-am adugat pe
min n arbore. Mai trebuie actualizate muchiile de cost minim care leag
restul nodurilor de noul arbore. Evident, singurele noduri care pot cauza
schimbri n vectorii D i vec sunt vecinii nodului min. Parcurgem aadar
vecinii nodului min, iar dac dm de un vecin j care nu face nc parte din
arbore i D[j] este mai mare dect costul muchiei (min, j) atunci actualizm
corespunztor D[j] i vec[j].
Deoarece la fiecare pas se determin o nou muchie a arborelui,
avem nevoie de N 1 pai. De aici rezult aadar complexitatea O(N2).
Implementarea de complexitate O(Mlog N) este foarte similar cu
algoritmul lui Dijkstra implementat cu ajutorul heap-urilor. Vom folosi un
heap ordonat dup costul muchiei care leag fiecare nod care nc nu se afl
n arbore de un nod din arbore. La fiecare introducere a unui nod n arbore
este necesar actualizarea vecinilor n heap. Lsm aceast implementare ca
un exerciiu pentru cititor. De obicei, n practic algoritmul lui Kruskal se
comport similar cu aceast implementare. Avantajul principal al
algoritmului lui Prim implementat cu heap-uri este acela c memoria
suplimentar folosit este doar O(N) i nu O(M) ca n cazul algoritmului lui
Kruskal.
Figura 12.12.2. reprezint modul de execuie al algoritmului pe un
graf oarecare. Am marcat cu verde nodurile i i muchiile care fac parte din
A.P.M (vec[i] = 0) i cu rou muchiile de cost minim care leag restul
nodurilor i de subarborele curent (vec[i]). Apare ngroat muchia de cost
minim care va fi aleas la pasul urmtor (D[i] minim).
Implementarea propus reine graful prin liste de adiacen.
Abordarea i structurile folosite se regsesc i n cadrul algoritmilor de
drumuri minime prezentai. Am folosit cu aceast ocazie i noiunea de
prototip al unei funciei. n aceste cazuri folosirea unui prototip nu are
dect un scop strict didactic i stilistic, permindu-ne s scriem
implementarea funciei prim dup funcia main.
443
Capitolul 12
444
Teoria grafurilor
void citire(vector<PER> G[],
int &N, int &M)
{
ifstream in("prim.in");
in >> N >> M;
int x, y, c;
for ( int i = 1; i <= M; ++i )
{
in >> x >> y >> c;
G[x].push_back(make_pair(y, c));
G[y].push_back(make_pair(x, c));
}
in.close();
}
int main()
{
int N, M;
vector<PER> G[maxn];
citire(G, N, M);
}
return cost;
return 0;
}
12.13. Concluzii
Am prezentat n acest capitol noiunile elementare care stau la baza
algoritmilor de grafuri. Cititorii experimentai poate c au observat lipsa
abordrii unor teme legate de arbori, cum ar fi determinarea L.C.A. sau cum
ar fi arborii binari de cutare. Aceste teme vor fi abordate n cadrul
capitolului Structuri avansate de date.
445
Capitolul 12
Cu ajutorul celor prezentate n acest capitol poate fi rezolvat o
gam larg de probleme. Propunem spre rezolvare urmtoarele probleme:
1. Dac nu avem de gnd sa prelucrm arborele parial de cost
minim, putem doar s-l afim muchie cu muchie pe msur ce l
determinm. Modificai algoritmii prezentai n acest scop.
2. Scriei programe care compar performanele algoritmilor
prezentai de-a lungul capitolului. Evident, comparai doar
algoritmii care rezolv aceeai problem.
3. Considerm toate numerele naturale de cel mult 4 cifre. Se dau T
perechi de numere prime de cel mult 4 cifre. S se ajung de la
primul numr prim la cel de-al doilea printr-un numr minim de
pai, tiind c un pas const din adugarea, tergerea sau
modificarea unei cifre.
4. Scriei un program care determin numrul de drumuri minime.
5. Scriei un program care determin dac se pot asocia costuri unui
graf astfel nct o matrice a drumurilor dat s fie valid.
6. Se d un graf neorientat ponderat. Scriei un program care
determin valoarea minim min astfel nct s existe un drum de
la nodul 1 la nodul N care s conin doar muchii cu ponderi cel
mult egale cu min.
7. Scriei un program care genereaz aleator grafuri conexe cu N
noduri i M muchii. Analog pentru grafuri bipartite.
8. Scriei un program care determin drumul elementar de cost
minim de la nodul 1 la nodul N i napoi.
9. Se d un graf ponderat. Se cere un drum de la nodul 1 la nodul
N. Se tie c unele noduri sunt blocate, adic nu se poate trece
prin ele. Ne intereseaz ca minimul distanelor dintre nodurile
drumului cerut i orice nod blocat s fie ct mai mare.
10. Se d o matrice ptratic de ordin N. S se determine drumul
minim de la (1, 1) la (N, N), tiind c ne putem deplasa doar n
sus, n jos, n stnga i n dreapta.
11. Se d un graf ponderat n care fiecare muchie are asociat o
culoare. S se determine un drum de cost minim n acest graf,
fr a se parcurge dou muchii de aceeai culoare una dup alta.
Alte exerciii ar fi ncorporarea algoritmilor prezentai ntr-o clas de
grafuri i folosirea clasei list n loc de vector. Avantajul clasei vector este
c putem accesa orice element rapid, pe cnd folosirea clasei list nu permite
dect parcurgerea secvenial a elementelor. Comparai performana
algoritmilor pe grafuri n cazul implementrii acestora cu ajutorul fiecrei
dintre cele dou clase.
446
13. Structuri
avansate de
date
Am prezentat pn acum o serie de algoritmi fundamentali nsoii de
aplicaii 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 cteva structuri de date care joac un rol foarte important n
algoritmic, n special n probleme de optimizare.
Practic, acest capitol prezint metode de a rspunde eficient la
interogri de genul se gsete un anumit obiect ntr-o colecie de obiecte
dat anterior?, ntrebri nsoite i de actualizri de tipul adug un nou
obiect coleciei date atnerior. Vom analiza cazurile favorabile, medii i
defavorabile a mai multor structuri de date i vom discuta situaiile n care
fiecare structur este preferabil celorlalte.
Acest capitol va folosi noiuni de grafuri, liste, operaii pe bii,
recursivitate, tehnici de programare, matematic i S.T.L., aa c
recomandm cu trie stpnirea tuturor capitolelor anterioare nainte de
parcurgerea acestui capitol final.
Pe majoritatea structurile de date ce urmeaz a fi prezentate ne
intereseaz urmtoarele trei operaii de baz i timpul de execuie al
acestora:
1. Inserarea unui element (Insert)
2. tergerea unui element (Remove)
3. Cutarea unui element (Search)
Vom considera un element ca fiind un ntreg pentru a nu complica
inutil exemplele.
447
Capitolul 13
CUPRINS
13.1. Skip lists (liste de salt) .......................................................................... 449
13.2. Tabele de dispersie (Hash tables) ....................................................... 455
13.3. Arbori de intervale problema L.C.A. ................................................ 464
13.4. Arbori indexai binar ............................................................................ 474
13.5. Arbori de prefixe (Trie) ........................................................................ 481
13.6. Arbori binari de cutare (Binary Search Trees) .................................. 488
13.7. Arbori binari de cutare cutare echilibrai ....................................... 504
13.8. Concluzii ................................................................................................ 514
448
1
1
1
4
4
4
4
4
6
6
8
8
8
8
10
10
NULL
NULL
NULL
NULL
NULL
Capitolul 13
Figura de mai jos prezint modul n care se caut valoarea 10:
L[4]
L[3]
L[2]
L[1]
L[0]
1
1
1
4
4
4
4
4
6
6
8
8
8
8
10
10
NULL
NULL
NULL
NULL
NULL
nivelul k cu probabilitatea
. Evident, un element sigur nu va face parte
+1
din nivelul k dac nu face parte deja din toate nivelele anterioare lui k.
Deoarece la fiecare nivel superior avem aproximativ jumtate dintre
nodurile de la nivelul imediat anterior, nlimea maxim a unei liste cu N
elemente va fi O(log N).
Pentru a stabili nivelul maxim al noului nod, vom genera un numr
aleator pe 32 de bii, iar nivelul elementului va fi dat de numrul de bii
consecutivi de valoare 1 de la sfritul reprezentrii n baza doi a numrului
aleator.
Pentru a insera efectiv noul element vom folosi algoritmul de cutare
mpreun cu algoritmul de inserare ntr-o list nlnuit, deoarece, pe
fiecare nivel, noul element va fi inserat naintea celui mai mic element mai
mare dect acesta.
Va trebui s avem grij s inserm noul element pe toate nivelele
stabilite anterior.
450
d) Detalii de implementare
Deoarece codul complet al tuturor operaiilor ar fi prea voluminos i
greu de neles la prima vedere, vom prezenta pe rnd i cu explicaii fiecare
structur i metod.
n primul rnd vom folosi o structur Node, care va reprezenta un
nod, caracterizat prin informaia reinut i legaturile acestuia.
n al doilea rnd vom folosi o structur List, care va conine
nceputul listei, pentru a putea evita nite cazuri particulare i a simplifica
implementarea. Aadar, structurile folosite sunt:
struct Node
{
int info;
Node **link_;
struct List
{
int H; // inaltimea curenta a listei
Node *Header;
List()
{
// maxH = 32, suficient pentru
// 2^32 elemente, deoarece maxim
// este O(log N)
Header = new Node(0, maxH);
H = 1;
}
};
Capitolul 13
lookup de 256 de ntregi unde lookup[i] = cu ci bii de valoare 1 se
termin numrul i. Valorile acestui tablou pot fi calculate folosind un
subprogram temporar, iar apoi copiate ntr-un vector constant la nceputul
programului:
const int lookup[256] = {0, 1, 0, 2, 0, 1, 0, ... };
452
453
Capitolul 13
O ultim operaie de care se poate s avem nevoie este afiarea
elementelor listei n ordine sortat. Acest lucru l putem face afind
elementele de pe nivelul 0:
for ( Node *tmp = L->Header->link_[0]; tmp; tmp = tmp->link_[0] )
cout << tmp->info << ' ';
f) mbuntiri
Putem transforma listele de salt ntr-o structur de date determinist
n modul urmtor: renunm la promovarea unui element la un nivel
superior pe baza unor criterii aleatore i impunem ca la nivelul 0 fiecare salt
s sar peste 0 elemente, la nivelul 1 fiecare salt s sar peste 1 element, iar
n general la nivelul k fiecare salt s sar peste 2k 1 elemente.
454
Capitolul 13
ct mai uniform. Dimensiunea (numrul de mulime sau liste) tabloului H va
fi egal cu valoarea maxim care poate fi returnat de funcia h, iar
reuniunea tuturor mulimilor va fi chiar irul dat. Aadar, memoria folosit
devine O(N + maxh), unde maxh este valoarea maxim care poate fi
returnat de h. Deoarece funcia de dispersie este aleas de obicei la
nceputul programului, putem considera c maxh este o constant, memoria
folosit fiind de fapt O(N).
De exemplu, fie irul 132, 5, 10, 4, 13, 17, 29, 31, 1, 2, 8, 7, 0, 11 i
funcia h(x) = x % 5. Atunci tabela de dispersie va arta n felul urmtor:
H[0]
H[1]
H[2]
H[3]
H[4]
5
31
132
13
4
10
1
17
8
29
0
11
2
NULL
NULL
NULL
NULL
NULL
d) Detalii de implementare
n primul rnd trebuie s stabilim dimensiunea tabelei de dispersie i
funcia pe care o vom folosi. Pentru majoritatea problemelor se folosete o
funcie simpl de genul h(x) = x % P, unde P este un numr prim sau
h(x) = x % 2k, pentru a putea calcula mai rapid operaia modulo folosind
operaii pe bii. Vom alege cea de-a doua variant din motive de eficien.
Am putea folosi i de data aceasta dou structuri: una care va reine
doar un membru val, care va reprezenta numrul reinut de un anumit
obiect, i un pointer ctre urmtorul element din list, reprezentnd practic o
list nlnuit i o a doua structur care va declara i iniializa mai multe
liste nlnuite, constituind tabela de dispersie. Recomandm cititorilor s
incorporeze i funciile de gestiune a tabelei ntr-o clas, att pentru aceast
structur de date ct i pentru celelalte din acest capitol. n acest fel, vei
putea refolosi foarte uor i intuitiv codul.
struct Node
{
int val;
Node *link_;
struct Hash
{
Node **link_;
Hash(int size)
{
link_ = new Node*[size];
for ( int i = 0; i < size; ++i )
link_[i] = NULL;
}
Node(int val)
{
this->val = val;
}
};
};
Putem folosi ns clasele list sau vector din S.T.L. pentru a obine o
implementare mai scurt. La grafuri am folosit vector deoarece nu aveam
457
Capitolul 13
de-a face cu tergeri i aveam n unele cazuri nevoie de acces aleator la
elemente. n cazul tabelelor de dispersie, clasa list se potrivete mai bine,
deoarece tergerea unui element se efectueaz oricum n O(N) i avem
nevoie de eliberarea memoriei, deoarece dac doar am marca elementele
terse cu o anumit valoare, nu am reduce efectiv din ncrctura tabelei.
Vom prezenta implementarea funciilor de baz cu ajutorul tipului
list. Lsm ca exerciiu pentru cititor implementarea cu ajutorul tipului
vector sau cu ajutorul unei liste nlnuite implementate manual.
Prezentm un program complet care implementeaz cele trei operaii
de baz, adaug 2010 numere aleatoare n tabel iar apoi afieaz rezultatul
a 2010 cutri n tabel. Programul este explicat prin comentarii.
#include <iostream>
#include <cstdlib>
#include <ctime>
#include <list>
using namespace std;
const int maxH = 1 << 20; // 2 la 20
int h(int x) // functia de dispersie
// e h(x) = x % (2 la 20)
{
return x & (maxH - 1);
}
458
f) mbuntiri i aplicaii
Putem extinde tabelele de disperse pentru numere raionale i pentru
iruri de caractere. O funcie de dispersie pentru numere reale poate fi
h(x) = [{Ax}P], unde:
0 < A < 1, preferndu-se (conform lui Knuth) =
0.618033989
{x} partea fracionar a lui x
459
51
2
Capitolul 13
[x] partea ntreag a lui x
P un numr natural oarecare, de obicei un numr prim sau o
putere a lui 2.
Funciile de dispersie pentru numere raionale au aplicaii n
probleme de geometrie computaional (de exemplu pentru cutarea rapid a
unui punct din plan).
Alt aplicaie este dat de tablouri asociative. Un tablou asociativ
este un tablou care poate fi indexat prin iruri de caractere. De exemplu,
dac vrem s implementm o agend telefonic, ar fi convenabil s putem
accesa rapid numrul de telefon al fiecrei persoane din agend tiind doar
numele acelei persoane. Astfel, numrul lui Ionescu Vlad ar putea fi accesat
(i setat) prin Numere[Ionescu Vlad].
Pentru a implementa o astfel de structur de date avem nevoie de o
funcie de dispersie care s asocieze un numr irului de caractere dat ca
parametru. Evident, o astfel de funcie va folosi valorile ASCII ale
caracterelor din ir.
O metod naiv de funcie de dispersie pentru iruri de caractere este
s adunm valorile ASCII a tuturor caracterelor din ir i s returnm suma
acestora modulo un numr prim sau putere a lui 2. Aceast funcie va genera
ns multe coliziuni. De obicei se folosete o funcie polinomal de genul:
h(C) = (C1Pk 1 + C2Pk 2 + ... + CkP0) % Q
Unde C este irul de caractere dat, iar P i Q sunt dou numere
prime sau Q o putere a lui 2. Q va da dimensiunea maxim a tabelei de
dispersie, iar P este de obicei un numr mic, n jur de 100. Alegerea
numerelor P i Q determin calitatea funciei.
Avnd acest model, implementarea unui tablou asociativ simplu
devine trivial. Trebuie doar construit o clas care suprancarc operatorul
[ ], care implementeaz funcia de dispersie dup modelul de mai sus i care
construiete tabela de dispersie dup modelul anterior. De data aceasta
listele vor reine iruri de caractere, sau string-uri.
Putem folosi proprietile operaiei modulo pentru a calcula eficient
funcia de dispersie n O(k). Prezentm un model de implementare:
460
Capitolul 13
compara ns doar rezultatele funciei de dispersie aplicate asupra
acestor dou iruri, transformnd astfel cel de-al doilea for ntr-o simpl
condiie. Dac ne intereseaz s nu obinem fals-pozitive, atunci n caz c
h(S1[i, i + M 1]) = h(S2), vom efectua o comparaie caracter cu caracter
ntre cele dou iruri pentru a ne asigura c egalitatea este adevrat. n caz
c cele dou valori sunt diferite, tim sigur c cele dou iruri sunt diferite.
Funcia de dispersie trebuie s poate fi calculat rapid, n O(1),
pentru toate subsecvenele de lungime M ale lui S1. Pentru acest lucru vom
calcula mai nti h(S2) i h(S1[0, M 1]) i vom ncepe prin a compara
aceste dou valori separat de partea principal a algoritmului. S vedem cum
putem obine h(S1[1, M]) tiind h(S1[0, M 1]):
h(S1[0, M 1]) = (S1[0]PM 1 + S1[1]PM 2 +...+ S1[M 1]P0 )%Q
h(S1[1, M]) = (S1[1]PM 1 + S1[2]PM 2 + ... + S1[M]P0) % Q
= ((h(S1 [0, M 1]) ((S1[0]PM 1) % Q) + Q)P +
+ S1[M]) % Q
Iar n general:
h(S1[i+1, i+M]) = ((h(S1[i , i+M1]) ((S1[i]PM 1) % Q) + Q)P +
+ S1[M]) % Q
Practic, aplicnd aceast formul se elimin termenul care nu mai
face parte din secvena curent, se nmulesc restul termenilor cu P,
restabilindu-se puterile, i se adun codul caracterului nou intrat n secven.
Astfel, am obinut funcia de dispersie aplicat noii secvene n O(1).
Acest algoritm poart numele de algoritmul Rabin Karp. n
practic se prefer folosirea algoritmului K.M.P. datorit faptului c
tabelele de dispersie fie nu ofer garanii asupra corectitudinii rezultatului
final, fie nu ofer garanii asupra timpului de execuie, care poate degenera
uor.
Se mai pot face diverse optimizri pentru a ne asigura c vom obine
ct mai puine fals-pozitive n cazul n care nu verificm potrivirile rezultate
din egalitatea valorilor returnate de funcia de dispersie. Putem de exemplu
s folosim mai multe funcii de dispersie i s considerm o potrivire doar
atunci cnd toate funciile de dispersie indic o potrivire. Pentru iruri
formate din litere ale alfabetului englez i dou funcii, probabilitatea de
apariie a unor fals-pozitive este suficient de mic pentru majoritatea
scopurilor.
462
463
Capitolul 13
Exerciii:
a) Modificai funcia de mai sus astfel nct s afieze toate poziiile
de potrivire.
b) Propunei algoritmi pentru testarea calitii unei funcii de
dispersie, att pentru numere naturale ct i pentru iruri de
caractere.
c) Se d un ir de N numere naturale aleatoare. Se cere a gsirea a
patru numere din ir a cror sum este S. Cum se poate rezolva
problema n O(N2) cu ajutorul tabelelor de dispersie?
d) Elaborai un test pe care soluia gsit pentru problema
anterioar s aib timpul de execuie O(N4).
Capitolul 13
dac nu, atunci nu are rost s verificm fiii nodului curent,
deoarece nu au nicio ans s fie inclui n [x, y].
dac da, atunci se verific dac intervalul asociat nodului curent
este inclus n [x, y]:
o dac da, atunci se actualizeaz eventual minimul
intervalului [x, y] (considerat iniial infinit) i nu se mai
verific niciun fiu (deoarece nodul curent acoper
intervalele fiilor).
o dac nu, atunci se efectueaz apeluri recursive pentru
ambii fii.
Procednd n acest fel obinem complexitatea O(log N), datorat
faptului c nlimea unui arbore de intervale este O(log N) i datorit
faptului c se vor alege intervale de lungime maxim a arborelui pentru
acoperirea intervalului [x, y] (pornind de sus n jos, primul interval gsit
care e inclus n [x, y] va fi folosit, iar subintervalele acestuia nu).
Figura de mai jos prezint un arbore de intervale pentru exemplul
dat. Cu rou apar minimele fiecrui interval reinut de arbore, iar cu verde
nodurile parcurse pentru aflarea minimului secvenei [4, 6]. ngroat apar
nodurile a cror intervale sunt incluse n [4, 6], restul nodurilor fiind noduri
intermediare. Cu portocaliu apar nodurile respinse deoarece intervalele
asociate nu intersecteaz intervalul cutat.
467
Capitolul 13
care intervalul curent nu conine un anumit element, deoarece ne
intereseaz toate intervalele n aceast prim faz.
Astfel obinem o structur de date eficient care ne ajut s
rspundem la ntrebri n timp O(log N) i s actualizm setul de date tot n
O(log N).
b) Detalii de implementare
Ne mai intereseaz modalitatea de memorare a unui arbore de
intervale. Vom folosi aceeai idee ca la heap-uri: dac A este arborele de
intervale, fiii unui nod k vor fi A[2k] respectiv A[2k + 1]. A va trebui s
fie de dimensiune cel puin 2N 1 (exist N + N / 2 + N / 4 + ... noduri).
Arborele nu este neaprat s fie complet ns, aa c va trebui s verificm
dac apelurile recursive se fac pentru un nod care chiar exist n arbore.
Pentru a evita aceste verificri putem declara tabloul A ca fiind de
dimensiune 2P 2N 1. Practic, vom completa arborele cu nite
pseudonoduri pn cnd acesta va deveni un arbore binar complet.
#include <fstream>
using namespace std;
const int maxN = 101;
const int maxArb = 1 << 8;
const int inf = 1 << 30;
void citire(int &N, int &T, int A[],
ifstream &in)
{
in >> N >> T;
for ( int i = 1; i <= N; ++i )
in >> A[i];
}
468
int main()
{
int N, T;
int A[maxN], Arb[maxArb];
ifstream in("RMQ2.in");
citire(N, T, A, in);
ofstream out("RMQ2.out");
build(Arb, A, 1, 1, N);
while ( T-- )
{
int op, x, y;
in >> op >> x >> y;
if ( op == 1 )
out<<query(Arb, 1, 1, N, x, y)
<< '\n';
else
update(Arb, 1, 1, N, x, y);
}
if ( st == dr )
{
Arb[nod] = y;
return;
}
in.close();
out.close();
return 0;
}
469
Capitolul 13
Tabelul 13.3.4. Performana orientativ a arborilor de intervale
Numr test
N
query
update Timp (secunde)
1
1 000
1 000
1 000
0.02
2
10 000
10 000
10 000
0.04
3
10 000
100 000
100 000
0.16
4
100 000
100 000
100 000
0.19
5
1 000 000 100 000
100 000
0.272
6
1 000 000 1 000 000
0
0.778
7
1 000 000
0
1 000 000
1.193
8
1 000 000 1 000 000 1 000 000
1.913
Se poate observa c arborii de intervale sunt foarte eficieni att n
teorie ct i n practic.
Capitolul 13
Pentru a rezolva problema vom folosi parcurgerea eulerian a
arborelui dat. Parcurgerea eulerian a unui arbore este o parcurgere n
adncime care adaug fiecare nod parcurs ntr-o list Euler, eventual de mai
multe ori. Mai exact:
Dac nodul curent este o frunz, atunci este adugat parcurgerii.
Dac nodul curent are fii, acesta este adugat la nceputul
parcurgerii euleriene a fiilor si, la sfritul acestei parcurgeri i
ntre fiecare parcurgere eulerien a fiilor.
Mai mult, pentru fiecare nod i se va calcula i H[i] = adncimea
nodului i n arbore i Poz[i] = poziia primei apariii a nodului i n Euler.
Pentru arborele de mai sus avem:
Tabelul 13.3.6. Parcurgerea eulerian a unui arbore
i Euler[i] H[i] Poz[i]
1
0
1
1
2
1
2
2
1
1
4
3
3
1
12
4
5
2
5
5
3
2
7
6
6
2
13
7
9
2
15
8
6
3
8
9
3
10
1
11
4
12
7
13
4
14
8
15
4
16
1
17
Avnd aceti trei vectori calculai putem reduce problema la o
interogare de minim pe interval. Se observ c fiecare nod i este n interiorul
intervalelor formate din dou apariii consecutive a strmoilor nodului i n
parcurgerea eulerian. De exemplu, nodul 5 se afl n intervalul format din
dou apariii consecutive ale lui 3 n parcurgerea eulerian i din dou
apariii consecutive ale lui 1. Asta nseamn c nodurile 3 i 1 sunt strmoi
472
Capitolul 13
d)
e)
f)
g)
sume.out
83
22
1 -3 8 7 9 1 3 4 20
126
2 4 -4
135
474
475
Capitolul 13
Tabelul 13.4.1. Construirea unui arbore indexat binar
i
i2
A[i] S[i]
neles
Explicaie
1 0001 1
1 Sum[1, 1] [1 20 + 1, 1] = [1, 1]
2 0010 -3
-2 Sum[1, 2] [2 21 + 1, 2] = [1, 2]
3 0011 8
8 Sum[3, 3] [3 20 + 1, 3] = [3, 3]
4 0100 7
13 Sum[1, 4] [4 22 + 1, 4] = [1, 4]
5 0101 9
9 Sum[5, 5] [5 20 + 1, 5] = [5, 5]
6 0110 1
10 Sum[5, 6] [6 21 + 1, 6] = [5, 6]
7 0111 3
3 Sum[7, 7] [7 20 + 1, 7] = [7, 7]
8 1000 4
30 Sum[1, 8] [8 23 + 1, 8] = [1, 8]
Se pot observa cteva lucruri din acest tabel: n primul rnd
S[i] = A[i] pentru orice i impar. Acest lucru se datoreaz faptului c bitul cel
mai puin semnificativ al oricrui numr impar este 1, deci nu exist zerouri
terminale. n al doilea rnd, orice poziie putere a lui doi reprezint, n
arbore, suma tuturor elementelor pn la acea poziie.
Se mai poate observa c putem afla, pe baza tabelului, suma oricrei
subsecvene care ncepe pe prima poziie. Acest lucru este suficient pentru a
rspunde la o interogare, deoarece putem folosi urmtoarea formul de
calcul: Sum[x, y] = Sum[1, y] Sum[1, x 1].
Algoritmul de calculare a valorii Sum[1, i] se bazeaz pe ideea c
scznd din i pe 2k atta timp ct i este mai mare ca 0, vom obine la fiecare
pas o poziie care reprezint suma unei subsecvene disjuncte, dar adiacente
cu subsecvena anterioar. Adunnd fiecare S[i], vom obine suma cerut.
Algoritmul de implementare al funciei Suma(1, i) este urmtorul:
rez = 0
Ct timp i mai mare dect 0 execut
o rez = rez + S[i]
o i = i 2k, unde k = numrul zerourilor terminale ale lui i
Returneaz rez
De exemplu, pentru a calcula Suma(1, 7) vom aduna valorile S[7],
S[6] i S[4]. Se poate observa din tabelul de mai sus c adunarea acestor
valori va furniza rspunsul corect.
Pentru a actualiza arborele indexat binar, algoritmul este aproape
identic. Pentru a implementa funcia de actualizare Actual(i, v), care adun
elementului i valoarea v, va trebui s cretem valoarea fiecrui element al
arborelui a crui subsecven asociat l conine pe i. Astfel, vom aduna la
476
b) Detalii de implementare
O implementare naiv ar parcurge fiecare bit al lui i pentru a
determina valoarea k, sau cel puin va parcurge bii atta timp ct acetia
sunt 0:
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;
}
Capitolul 13
Operaia i & (i 1) are ca efect setarea celui mai puin
semnificativ bit de valoare 1 al lui i pe valoarea 0.
De exemplu:
i
1011000 &
i 1
1010111
i & (i 1)
1010000
Operaia i ^ i are ca efect setarea tuturor biilor lui i pe valoarea
0.
De exemplu: 10110 ^ 10110 = 00000.
n plus: 1 ^ 0 = 0 ^ 1 = 1.
Ne intereseaz o secven de operaii care s seteze toi biii unei
valori pe 0, n afar de cel mai puin semnificativ bit de valoare 1. Folosind
propoziiile de mai sus, obinem formula de calcul 2k = i ^ (i & (i 1)),
unde k are semnificaia sa de pn acum. De exemplu:
i
i1
i & (i 1)
1011000 &
1010111
1010000
i ^ (i & (i 1))
0001000
478
int main()
{
int N, T, A[maxN], S[maxN];
ifstream in("sume.in");
citire(N, T, A, S, in);
ofstream out("sume.out");
while ( T-- )
{
int op, x, y;
in >> op >> x >> y;
if ( op == 1 )
out << Suma(y, S) - Suma(x-1, S)
<< '\n';
else
Actual(N, x, y, S);
}
int Suma(int i, int S[]) // query
{
int rez = 0;
for ( ; i > 0; i -= i ^ (i & (i - 1)) )
rez += S[i];
}
in.close();
out.close();
return 0;
return rez;
}
// update
void Actual(int N, int i, int v, int S[])
{
for ( ; i <= N; i += i ^ (i & (i - 1)) )
S[i] += v;
}
Capitolul 13
d) Extinderi
Putem extinde ideea prezentat pentru a funciona i n cazul
bidimensional, adic n cazul n care actualizrile i interogrile se
efectueaz asupra unei matrici. Presupunem c se d o matrice cu N linii i
M coloane. O interogare presupune aflarea sumei submatricii cu colul
stnga-sus n elementul (x, y) i colul dreapta-jos n elementul (p, q). O
actualizare presupune adunarea unei valori elementului (x, y). Putem
implementa algoritmi care au timpul de execuie O(NM) pentru una dintre
operaii i O(1) pentru cealalt operaie. Acetia sunt similari cu algoritmii
naivi de rezolvare a problemei unidimensionale i nu vom insista asupra lor.
Pentru a obine timpul de execuie O((log N)(log M)) pentru fiecare
operaie este necesar s folosim un arbore de arbori indexai binar. Vom
folosi o matrice S, unde S[i][j] semnific suma submatricii cu colul
stnga-sus n elementul (i 2k + 1, j 2l + 1) i colul dreapta-jos n
elementul (i, j), unde k reprezint numrul de zerouri terminale ale lui i, iar
l numrul de zerouri terminale ale lui j.
Aceast matrice va fi gestionat exact dup modelul unidimensional,
480
Capitolul 13
Din aceast figur putem observa deja caracteristicile i proprietile
de baz ale acestei structuri de date.
n primul rnd, un trie este un arbore.
Nodul rdcin este un nod special, care nu are afecteaz coninutul
structurii de date, ci doar uureaz reprezentarea (i implementarea) acestei
structuri.
Fiecare frunz are eticheta \0, care semnific sfritul unui cuvnt.
Verificarea existenei unui cuvnt n dicionar se face ncepnd
parcurgerea arborelui de la rdcin i mergnd succesiv pe fiii etichetai cu
litera de pe poziia corespunztoare a cuvntului cutat. Dac la un moment
dat am ajuns pe caracterul \0, care semnific sfritul unui cuvnt, atunci
cuvntul cutat exist n trie. Dac am ajuns n situaia n care nu exist
niciun fiu al nodului curent care s fie etichetat cu litera de pe poziia
curent a cuvntului cutat, atunci cuvntul cutat nu se afl n trie. Operaia
de cutare se execut n timp O(L), unde L este lungimea cuvntului cutat.
Inserarea unui cuvnt se face n mod similar cu verificarea
existenei unui cuvnt, i are acelai timp de execuie. Singura diferen este
c, atunci cnd nodul curent nu are un fiu etichetat cu litera corespunztoare
poziiei curente a cuvntului care trebuie inserat, un astfel de fiu este creat.
Se procedeaz n acest fel pn cnd au fost create noduri (dac a fost cazul)
pentru toate literele cuvntului. La final, se adaug un nod cu eticheta \0,
semnificnd sfritul cuvntului.
n cele ce urmeaz ne propunem s scriem un program care citete
din fiierul trie.in N operaii de forma op cuv, unde:
op = 0 nseamn adugarea cuvntului cuv n dicionar.
op = 1 nseamn afiarea numrului de apariii a cuvntului cuv
n dicionar.
op = 2 nseamn tergerea unei apariii a cuvntului cuv din
dicionar. Nu se va afia nimic. Exist posibilitatea ca
argumentul acestei operaii (cuvntul care trebuie ters) s nu
existe n dicionar.
Vom explica pe larg modul de funcionare al fiecrei operaii,
precum i implementarea fiecreia.
b) Detalii de implementare
n primul rnd s vedem cum vom reine acest arbore. Fiind vorba de
un arbore n care fiecare nod poate avea un numr relativ mare de fii
482
Capitolul 13
procedeaz n acest fel pn cnd cuv va indica sfritul cuvntului, adic \0
(terminatorul de ir). n acest moment se actualizeaz numrul de apariii
(cmpul rasp) al acestui nod terminal i funcia se termin.
Implementarea este urmtoarea:
void Insert(node *rad, const char *cuv)
{
if ( *cuv == '\0' ) // daca am ajuns la terminatorul de sir (deci si la
// nodul terminal)
{
++rad->rasp; // incrementeaza numarul de aparitii al cuvantului cuv
return;
}
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
}
Insert(rad->next[val], cuv + 1); // apel recursiv pentru litera urmatoare
}
484
485
Capitolul 13
bool Del(nod *radInit, nod *rad, const char *cuv)
{
int val = *cuv - 'a';
if ( *cuv == '\0' ) // am ajuns la un nod final, scade numarul de aparitii
--rad->rasp;
else if ( Del(radInit, rad->next[val], cuv + 1) ) // daca putem sterge fiul
{
rad->next[val] = 0; // marcam fiul respectiv ca fiind sters
--rad->nrf; // scadem numarul de fii ai nodului curent
}
if ( rad->nrf == 0 && rad->rasp == 0 && rad != radInit )
{
delete rad; // sterge nodul curent daca sunt indeplinite cele 3 conditii
return true; // am putut sterge efectiv nodul curent
}
return false; // nu s-a putut sterge efectiv nodul curent
}
486
c) Aplicaii
n primul rnd, un trie poate fi folosit ca o alternativ la tabelele de
dispersie. n cel mai ru caz, verificarea existenei unui cuvnt ntr-un tabel
de dispersie are timpul de execuie O(NL), unde N este numrul de cuvinte,
iar L este lungimea cuvntului cutat. Acest caz are loc atunci cnd toate
cuvintele ajung pe aceeai poziie n tabel, iar cuvntul cutat ajunge la
sfritul listei asociate poziiei respective. Cutarea unui cuvnt ntr-un trie
se efectueaz ntotdeauna n timp O(L). n plus, implementarea unui
dicionar cu ajutorul tabelelor de dispersie este mai dificil.
O alt aplicaie important a unui trie este posibilitatea de a sorta
lexicografic cuvintele inserate n acesta ntr-un mod eficient i elegant.
Putem efectua aceast sortare parcurgnd arborele n adncime i innd la
fiecare pas o stiv cu etichetele nodurilor parcurse. Dac avem grij s
efectum apelurile recursive n mod cresctor al etichetelor asociate fiilor
(prima dat pentru fiul etichetat cu a, apoi pentru cel cu b dac exist
etc.), atunci este suficient s afim stiva o dat ajuni pe un nod terminal i
vom obine cuvintele n ordine lexicografic.
Dac ignorm costul afirii (o considerm o operaie care se
execut n O(1) cu alte cuvinte), atunci complexitatea acestui algoritm de
sortare este O(C), unde C reprezint numrul total de noduri din trie, adic
suma caracterelor tuturor cuvintelor din trie.
487
Capitolul 13
y x xor y
0
1
1
1
1
0
0
0
Capitolul 13
Se poate observa uor c acest arbore respect toate condiiile
menionate mai sus.
n continuare vom prezenta pe rnd fiecare operaie amintit
anterior, iar la sfrit vom prezenta codul unui program complet care rezolv
o problem cu ajutorul unui astfel de arbore.
490
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
identificm nodul care trebuie ters i s-l eliminm din arbore.
492
Capitolul 13
proprietile arborilor binari de cutare. Acest lucru se poate demonstra uor
prin reducere la absurd.
Ne punem aadar problema determinrii unui nod a crui valoare se
ncadreaz n unul dintre cele dou cazuri de mai sus. Pentru acest lucru,
vom reaminti algoritmul de parcurgere n ordine a unui arbore,
specializat pentru arbori binari:
Fie InOrdine(T) o funcie care afieaz parcurgerea n ordine a
arborelui cu rdcina n T. Aceast funcie poate fi implementat astfel:
Dac T este nenul execut
o apeleaz recursiv InOrdine(T.stnga)
o afieaz T.valoare
o apeleaz recursiv InOrdine(T.dreapta)
Datorit structurii arborilor binari de cutare, aceast parcurgere are
prorietatea de a afia valorile inserate ntr-un arbore n ordine cresctoare.
De exemplu, pentru arborele binar de cutare dat ca exemplu, funcia va
afia: 1 3 4 6 8 9 10 11.
Pornind de la acest algoritm de afiare n ordine cresctoare a
valorilor din arbore, problema iniial se reduce la a gsi fie valoarea care
precede o valoare fixat n cadrul acestei parcurgeri, fie valoarea care
urmeaz dup o valoare fixat n cadrui acestei parcurgeri. Altfel spus, va
trebuie s gsim fie predecesorul unui nod n parcurgerea n ordine, fie
succesorul unui nod n parcurgerea n ordine.
Predecesorul unui nod T n parcurgerea n ordine este cel mai din
dreapta nod al subarborelui stng al lui T. De exemplu, figura de mai jos
prezint modul de aflare al predecesorului nodului cu valoarea 9 n
parcurgerea n ordine:
Capitolul 13
Sau:
d) Cazuri defavorabile
Am afirmat la nceput c operaiile de cutare, inserare i tergere
ntr-un arbore binar de cutare au complexitatea O(log N) pe cazuri
favorabile. Un caz favorabil este dat de un arbore a crui nlime este O(log
N), iar un caz defavorabil de un arbore a crui nlime este O(N). Fiecare
operaie prezentat are proprietatea de a lucra la fiecare pas cu un singur
element dintr-un singur nivel al arborelui, deci dac arborele are nlimea
O(log N), atunci i aceste operaii se vor executa n aceeai complexitate.
Dac nlimea arborelui este mai apropiat de N ns, atunci fiecare
operaie va avea o complexitate liniar.
De exemplu, privii cum se construiete un arbore binar de cutare
pentru valorile 1 2 3 4 5:
496
e) Detalii de implementare
Vom prezenta pe rnd implementarea fiecrei funcii pentru care am
furnizat pn acum doar pseudocod. n primul rnd, pentru a reine un
arbore binar de cutare avem nevoie de o structur nod care va reprezenta
un nod al arborelui. Aceasta va conine trei cmpuri.
struct nod
{
int val; // valoarea aferenta nodului curent
nod *st, *dr; // pointeri la subarborele stang respectiv drept
nod(int v) : val(v) // val se initializeaza cu v, iar st si dr vor fi nuli initial
{
st = dr = NULL;
}
};
497
Capitolul 13
Avnd aceast structur, operaiile de inserare i de cutare nu
prezint mari probleme la implementare. Funcia de inserare poate fi
implementat astfel:
void Insert(int x, nod *&T) // pointerul trebuie transmis prin referinta,
// deoarece se va modifica
{
if ( T == NULL )
T = new nod(x);
else if ( T->val < x )
Insert(x, T->dr);
else
Insert(x, T->st);
}
498
499
Capitolul 13
Pentru simplitate, implementarea prezentat nlocuiete ntotdeauna,
n cadrul cazului III, nodul care trebuie ters cu predecesorul su din
parcurgerea n ordine. Aa cum am spus mai devreme ns, acest lucru nu
este indicat deoarece poate contribui la debalansarea arborelui.
Recomandm cititorilor s implementeze o variant care alege aleator ntre
predecesorul i succesorul nodului pe care vrem s-l tergem.
Menionm c pentru folosirea acestor funcii, trebuie declarat i
iniializat cu NULL o variabil de tip nod * prin instruciunea:
nod *T = NULL;
f) Ali algoritmi
Am prezentat pn acum algoritmii de baz afereni acestei structuri
de date. Vom prezenta n continuare dou probleme importante care se pot
rezolva cu ajutorul arborilor binari de cutare i anume:
1. identificarea celei mai mici valori din arbore n timp mediu
O(log N).
2. identificarea celei de-a k-a cea mai mic valoare din arbore n
timp mediu O(log N).
1. Identificarea celei mai mici valori n timp mediu O(log N)
O prim idee de rezolvare a problemei ar fi s parcurgem arborele n
ordine i s returnm primul element din cadrul acestei parcurgeri. De fapt,
putem astfel s rezolvm ambele probleme, doar c timpul de execuie va fi
O(N).
Putem ns s oprim parcurgerea n ordine imediat dup ce aceasta a
furnizat prima valoare, deoarece tim sigur c aceasta este valoarea minim.
Vom arta n continuare c n acest fel se execut un numr de pai
proporional cu nlimea arborelui, adic O(log N) pe cazul mediu. S
considerm urmtorul arbore:
Se poate observa uor c funcia de parcurgere n ordine se
autoapeleaz avnd ca parametru fiul stnd al nodului curent. Acest lucru se
face pn cnd se ajunge pe un nod nul. La revenire din recursivitate se va
afia valoarea nodului curent. Aadar, prima valoare afiat de ctre
algoritmul de parcurgere n ordine este cea mai din stnga valoare a
arborelui, sau, altfel spus, nodul pe care se ajunge pornind din rdcin i
mergnd la fiecare pas pe fiul stng al nodului curent, dac acesta exist.
500
Putem afla cea mai mare valoare din arbore aflnd care este cel mai
din dreapta nod al arborelui. Acest lucru este corect deoarece algoritmul de
parcurgere n ordine furnizeaz ultimul rezultat umplnd stiva cu apeluri
recursive pentru fiul drept al nodului curent.
int Maxim(nod *T)
{
while ( T->dr != NULL )
T = T->dr;
return T->val;
}
Capitolul 13
Altfel
o returneaz kMinim(T.st, k)
Raionamentul care ne conduce la acest algoritm este urmtorul:
dac ne aflm la un nod T i acesta are nr fii n subarborele su stng (n
acest subarbore intr i T), adic nr noduri mai mici sau egale cu valoarea
lui T, iar k este egal cu nr, atunci evident al k-lea cel mai mic element din
arbore este T.
Dac n schimb nr este mai mic dect k, atunci tim c al k-lea cel
mai mic element este mai mare dect valoarea lui T i se afl undeva n
subarborele drept al lui T. Putem aadar s facem abstracie de subarborele
stng al lui T i s reducem problema la gsirea celui x-lea cel mai mic
element din subarborele drept, unde x = k nr.
Altfel este clar c nr < k, deci elementul cutat se afl undeva n
subarborele stng al nodului curent. Putem aadar s reducem problema la
gsirea celui de-al k-lea cel mai mic element din subarborele stng.
De exemplu, s considerm urmtorul arbore binar de cutare, n
care am marcat valorile nr pentru fiecare nod:
Inserri
Cutri
tergeri
1 000
1 000
1 000
10 000
10 000
10 000
100 000
0
0
100 000
100 000
0
100 000
100 000
100 000
1 000 000
0
0
1 000 000 1 000 000
0
1 000 000 1 000 000 1 000 000
Timp (s)
0.020
0.030
0.065
0.103
0.140
0.758
1.600
2.434
Fa de
liste de salt
mai bine
mai bine
mai bine
mai bine
mai bine
mai bine
mai bine
mai bine
Capitolul 13
c) Scriei un program care afieaz parcurgerea n preordine i n
postordine a unui arbore binar de cutare.
d) Scriei un program care determin ci arbori binari de cutare
distinci din punct de vedere structural exist avnd ca elemente
numere distincte din mulimea {1, 2, ..., N}. De exemplu, pentru
N = 4 rspunsul este 14, pentru N = 5 este 42, iar pentru N = 6
este 132.
e) Scriei un program care determin dac un arbore binar dat ca
date de intrare este sau nu arbore binar de cutare. Gsii un
algoritm eficient.
f) Scriei un program care determin numrul de noduri dintr-un
arbore binar de cutare cu valori mai mici dect o valoare dat
(nu este obligatoriu ca valoarea dat s se regseasc n arbore).
504
505
Capitolul 13
Ne vom referi n continuare la arborele din partea stng, rotaia spre
stnga explicndu-se analog. S presupunem c nodul cu valoarea 7 din
figur nu respect proprietatea de heap, adic prioritatea nodului cu valoarea
7 (b) este mai mare dect prioritatea nodului cu valoarea 9 (a). Este clar c
rotind nodul cu valoarea 7 spre stnga se restituie proprietatea de heap a
arborelui, deoare n arborele din dreapta nodul 7 va fi tatl nodului 9, iar
b > a.
Vom arta n continuare c o rotaie spre dreapta pstreaz
propritatea de arbore binar de cutare.
n arborele stng avem urmtoarele inegaliti (fiecare identificator
va descrie valoarea rdcinii subarborelui respectiv):
A< 7<B
9<C
A, 7, B < 9
Combinnd aceste relaii obinem inegalitile: A < 7 < B < 9 < C.
n arborele drept avem urmtoarele inegaliti:
A< 7
B<9<C
7 < 9, B, C
Combinnd aceste relaii obinem inegalitile: A < 7 < B < 9 < C.
Aadar, deoarece am obinut acelai ir de inegaliti ntre noduri
att nainte ct i dup rotaie, am demonstrat pstrarea invariantului
arborilor binari de cutare dup efectuarea unei rotaii spre dreapta.
Demonstraia n sens invers este identic.
Vom prezenta n continuare pseudocod pentru operaiile necesare n
lucrul cu treap-uri.
a) Echilibrarea arborelui
Echilibrarea arborelui este necesar atunci cnd 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
rotaie a acelui nod spre dreapta sau spre stnga, dup caz:
dac prioritatea fiului stng al lui T este mai mare dect
prioritatea lui T, atunci se efectueaz o rotaie spre dreapta a
fiului stng.
dac prioritatea fiului drept al lui T este mai mare dect
prioritatea lui T, atunci se efectueaz o rotaie spre stnga a fiului
drept.
506
507
Capitolul 13
508
Capitolul 13
o Dac T.stnga i T.dreapta sunt nuli execut
terge T
o Altfel dac T.stnga e nul sau T.dreapta e nul execut
Dac T.stnga e nul execut RotDr(T)
Altfel execut RotSt(T)
o Altfel execut
Dac T.st.prioritate > T.dr.prioritate execut
RotSt(T)
Altfel execut RotDr(T)
o Apeleaz recursiv Remove(x, T)
e) Detalii de implementare
Structura asociat nodurilor unui treap este asemntoare cu cea de
la arbori binari de cutare, singura diferen fiind c mai avem un cmp ce
reprezint prioritatea:
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
if ( T->val < x )
Insert(x, T->dr);
else if ( T->val > x )
Insert(x, T->st);
Echilibrare(T);
}
Remove(x, T);
}
}
511
Capitolul 13
Se poate observa c, spre deosebire de implementarea funciei de
inserare din cadrul arborilor binari de cutare, implementarea prezentat aici
nu permite inserarea unei valori care exist deja n treap. Acest lucru se
datoreaz faptului c algoritmul de tergere ar intra ntr-un ciclu infinit dac
ar exista valori duplicate.
n cazul arborilor binari de cutare nu este obligatorie impunerea
unicitii elementelor, dar acest lucru este oricum recomandat. Pentru a
suporta valori duplicate, cea mai bun soluie este adugarea unui cmp
nrVal nodurilor, care s indice de cte ori apare valoarea respectiv. Astfel,
algoritmii de gestionare nu necesit dect modificri minime.
Funcia de parcurgere n ordine a unui treap se poate implementa
exact ca la arbori binari de cutare, deoarece aceasta nu are nevoie dect de
valorile nodurilor, nu i de prioritile acestora.
Algoritmii de determinare a minimului i de determinare a celui deal k cel mai mic element sunt, la rndul lor, similari n implementare.
Algoritmul de determinare al minimului este identic, iar cel de determinare a
celui de-al k cel mai mic element necesit doar modificarea modului de
calcul a valorii nr. Va trebui s fim ateni s actualizm aceast valoare
dup fiecare rotaie efectuat.
Inserri
Cutri
tergeri
1 000
1 000
1 000
10 000
10 000
10 000
100 000
0
0
100 000
100 000
0
100 000
100 000
100 000
1 000 000
0
0
1 000 000 1 000 000
0
1 000 000 1 000 000 1 000 000
512
Timp (s)
0.022
0.033
0.108
0.144
0.191
1.669
2.823
3.978
Fa de
liste de salt
mai bine
mai bine
mai bine
mai bine
mai bine
mai bine
mai bine
mai bine
Fa de
BST
mai ru
mai ru
mai ru
mai ru
mai ru
mai ru
mai ru
mai ru
513
Capitolul 13
13.8. Concluzii
Sperm c acest ultim capitol, ct i ntreaga lucrare, v-au fost i v
vor fi n continuare folositoare n studiul algoritmilor. Cititorii care au
parcurs temeinic materialul pus la dispoziie n aceast carte ar trebui s aib
deja o nelegere clar a noiunilor algoritmice elementare i a metodelor de
rezolvare a problemelor aferente acestui domeniu.
Cititorii care simt c nu i-au nsuit n totalitate toate temele
abordate nu trebuie s-i fac griji. Aceast carte poate fi folosit i ca o
referin asupra algoritmilor i a implementrilor acestora n limbajul C++.
Mai mult, unele capitole nici nu sunt scrise cu gndul de a putea fi nelese
ntr-un timp foarte scurt de ctre nceptori acest lucru ar fi imposibil de
realizat fr a pierde din rigoare.
n ncheiere, dorim tuturor cititorilor perseveren n studii i succes
n orice demersuri ntreprinse!
Profitm de aceste ultime rnduri pentru a v aduce la cunotin
publicarea, n viitorul apropiat, a unei cri intitulate Tehnici de
programare aplicate, care se va axa exclusiv pe rezolvarea unor
probleme date la concursuri naionale, olimpiade i site-uri de evaluare
online.
Sperm s ne rmnei fideli n continuare!
Autorii
514
Bibliografie
BIBLIOGRAFIE
1. Adrian Alexandrescu Programarea modern n C++. Programare
generic i modele de proiectare aplicate, Teora, Bucureti, 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. Bla Bollobs, 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): 464497, 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. 540545, 1989.
11. Constantin Popescu, Dan Noje, Ioan Mang, Horea Oros, Programarea
n limbajul C, Editura Universitii 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, Jrg 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
18. G. H. Gonnet, Handbook of Algorithms and Data Structures, AddisonWesley, 1984.
19. Harry R. Lewis, Christos H. Papadimitriou, Elements of the Theory of
Computation, Prentice-Hall, 1981.
20. Ivan Niven, Herbert S. Zuckerman, An Introduction to the Theory of
Numbers, John Wiley & Sons, fourth edition, 1980.
21. J. A. Bondy, U. S. R. Murty, Graph Theory with Applications, American
Elsevier, 1976.
22. J. B. Kruskal, On the shortest spanning subtree of a graph and the
traveling salesman problem, Proceedings of the American Mathematical
Society, 7:48-50, 1956.
23. J. W. J. Williams, Algorithm 232 (heapsort), Communications of the
ACM, 7:347-348, 1964.
24. Jack Edmonds, Richard M. Karp, Theoretical improvements in the
algorithmic efficiency for network flow problems, Journal of the ACM,
19:248-264, 1972.
25. John D. Dixon, Factorization and primality tests, The American
Mathematical Monthly, 91(6):333-352, 1984.
26. John E. Hopcroft, Jeffrey D. Ullman, Set merging algorithms, SIAM
Journal on Computing, 2(4):294-303, 1973.
27. John E. Hopcroft, Richard M. Karp, An n5/2 algorithm for maximum
matchings in bipartite graphs, SIAM Journal on Computing, 2(4):225231, 1973.
28. John E. Hopcroft, Robert E. Tarjan, Efficient algorithms for graph
manipulation, Communications of the ACM, 16(6):372-378, 1973.
29. John H. Holland, Adaptation in Natural and Artificial Systems,
University of Michigan Press, Ann Arbor, 1975.
30. Jon L. Bentley, Programming Pearls, Addison-Wesley, 1986.
31. Jon L. Bentley, Writing Efficient Programs, Prentice-Hall, 1982.
32. Jon L. Bentley, Writing Efficient Programs, Prentice-Hall, 1982.
33. Kendall A. Atkinson, An introduction to Numerical Analysis (2nd ed.),
John Wiley & Sons, New York, 1989.
34. Knuth D. E. Arta programrii calculatoarelor vol.2, Algoritmi
seminumerici, Editura Teora, Bucureti, 2000.
35. Knuth D. E. Arta programrii calculatoarelor, vol.1, Algoritmi
fundamentali, Editura Teora, Bucureti, 1999.
36. Knuth D. E. Arta programrii calculatoarelor, vol.3, Sortare i cutare,
Editura Teora, Bucureti, 2001.
37. Kurt Mehlhorn, Graph Algorithms and NP-Completeness, volumul 2 al
Data Structures and Algorithms, Springer-Verlag, 1984.
516
Bibliografie
38. Kurt Mehlhorn, Sorting and Searching, volumul 1 al Data Structures
and Algorithms, Springer-Verlag, 1984.
39. Leonard M. Adleman, Carl Pomerance, Robert S. Rumely, On
distinguishing prime numbers from composite numbers, Annals of
Mathematics, 117: 173-206, 1983.
40. Lestor R. Ford, Jr., D. R. Fulkerson, Flows in Networks, Princeton
University Press, 1962.
41. Liviu Negrescu, Limbajul C++, editura Albastr, Cluj Napoca, 1999.
42. Louis Monier, Evaluation and comparison of two efficient probabilistic
primality testing algorithms, Theoretical Computer Science, 12(1): 97108, 1980.
43. Manuel Blum, Robert W. Floyd, Vaughan Pratt, Ronald L. Rivest,
Robert E. Tarjan, Time bounds for selection, Journal of Computer and
System Sciences, 7(4):448-461, 1973.
44. Michael O. Rabin, Probabilistic algorithm for testing primality. Journal
of Number Theory, 12:128-138, 1980.
45. Mihai Oltean, Proiectarea i implementarea algoritmilor, Computer
Libris Agora, 1999.
46. Mihai Scoraru, Arbori indexai binar, revista Ginfo nr. 13/1, ianuarie,
2003.
47. Mircea D. Popvici, Mircea I. Popvici C++ Tehnologia orientat spre
obiecte, Aplicaii Editura Teora, Bucureti 2000.
48. P. van Emde Boas, Preserving order in a forest in less than logarithmic
time, n Proceedings of the 16th Annual Symposium on Foundations of
Computer Science, paginile 75-84, IEEE Computer Society, 1975.
49. R. A. Jarvis, On the identification of the convex hull of a finite set of
points in the plane, Information Processing Letters, 2:18-21, 1973.
50. R. C. Prim, Shortest connection networks and some generalizations, Bell
System Technical Journal, 36:1389-1401, 1957.
51. R. L. Graham, An efficient algorithm for determining the convex hull of
a finite planar set, Information Processing Letters, 1:132-133, 1972.
52. Richard Bellman, Dynamic Programming, Princeton University Press,
1957.
53. Richard M. Karp, Michael O. Rabin, Efficient randomized patternmatching algorithms, Technical Report TR-31-81, Aiken Computation
Laboratory, Harvard University, 1981.
54. Robert E. Tarjan, Data Structures and Network Algorithms, Society for
Industrial and Applied Mathematics, 1983.
55. Robert E. Tarjan, Depth first search and linear graph algorithms, SIAM
Journal on Computing, 1(2):146-160, 1972.
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