Sunteți pe pagina 1din 265

Daniel Danciu

George Mardale

ARTA PROGRAMARII N JAVA


vol II - Algoritmi si structuri de
date

Cuprins
9 Analiza ecientei algoritmilor
9.1 Ce este analiza algoritmilor? . . . . . . . . . . . . . . . . . .
9.2 Notatia asimptotica . . . . . . . . . . . . . . . . . . . . . . .
9.2.1 O notatie pentru ordinul de marime al timpului de exe
cutie al unui algoritm . . . . . . . . . . . . . . . . . .
9.3 Tehnici de analiza algoritmilor . . . . . . . . . . . . . . . . .
9.3.1 Sortarea prin selectie . . . . . . . . . . . . . . . . . .
9.3.2 Sortarea prin insertie . . . . . . . . . . . . . . . . . .
9.3.3 Turnurile din Hanoi . . . . . . . . . . . . . . . . . . .
9.4 Analiza algoritmilor recursivi . . . . . . . . . . . . . . . . . .
9.4.1 Metoda iteratiei . . . . . . . . . . . . . . . . . . . . .
9.4.2 Inductia constructiva . . . . . . . . . . . . . . . . . .
9.4.3 Recurente liniare omogene . . . . . . . . . . . . . . .
9.4.4 Recurente liniare neomogene . . . . . . . . . . . . . .
9.4.5 Schimbarea variabilei . . . . . . . . . . . . . . . . . .
9.5 Implementarea algoritmilor . . . . . . . . . . . . . . . . . . .
10 Structuri de date
10.1 Cum implementam structurile de date?
10.2 Stive . . . . . . . . . . . . . . . . . .
10.3 Cozi . . . . . . . . . . . . . . . . . .
10.4 Liste nlantuite . . . . . . . . . . . .
10.5 Arbori . . . . . . . . . . . . . . . . .
10.5.1 Notiuni generale . . . . . . .
10.5.2 Arbori binari . . . . . . . . .
10.5.3 Arbori binari de cautare . . .
10.6 Tabele de repartizare . . . . . . . . .
10.6.1 Tratarea coliziunilor . . . . .
3

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

15
. 16
. 18
.
.
.
.
.
.
.
.
.
.
.
.

19
21
21
23
24
25
25
26
26
28
31
33

.
.
.
.
.
.
.
.
.
.

40
43
45
51
55
64
64
65
69
83
90

CUPRINS

10.7 Cozi de prioritate . . . . . . . . . . . . . . . . . . . . . . . . . 92


10.7.1 Ansamble . . . . . . . . . . . . . . . . . . . . . . . . . 94
11 Metoda Backtracking
11.1 Prezentare generala . . . . . . . . . . . . . . . . .
11.2 Prezentarea metodei . . . . . . . . . . . . . . . . .
11.2.1 Atribuie si avanseaza . . . . . . . . . . . .
11.2.2 ncercare esuata . . . . . . . . . . . . . . .
11.2.3 Revenire . . . . . . . . . . . . . . . . . .
11.2.4 Revenire dupa construirea unei solutii . . .
11.3 Implementarea metodei backtracking . . . . . . . .
11.4 Probleme clasice rezolvabile prin backtracking . .
11.4.1 Problema generarii permutarilor . . . . . .
11.4.2 Generarea aranjamentelor si a combinarilor
11.4.3 Problema damelor . . . . . . . . . . . . .
11.4.4 Problema colorarii hartilor . . . . . . . . .
12 Divide et impera
12.1 Introducere n recursivitate . . . . . . . . . .
12.1.1 Functii recursive . . . . . . . . . . .
12.1.2 Recursivitatea nu nseamna recurenta
12.2 Prezentarea metodei Divide et Impera . . . .
12.3 Cautare binara . . . . . . . . . . . . . . . . .
12.4 Sortarea prin interclasare (MergeSort) . . . .
12.5 Sortarea rapida (QuickSort) . . . . . . . . . .
12.6 Expresii aritmetice . . . . . . . . . . . . . .

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

108
. 109
. 110
. 114
. 114
. 115
. 115
. 117
. 119
. 119
. 124
. 126
. 127

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

137
. 138
. 138
. 143
. 145
. 146
. 149
. 152
. 157

13 Algoritmi Greedy
13.1 Problema spectacolelor (selectarea activitatilor) .
13.1.1 Demonstrarea corectitudinii algoritmului
13.1.2 Solutia problemei spectacolelor . . . . .
13.2 Elemente ale strategiei Greedy . . . . . . . . . .
13.2.1 Proprietatea de alegere Greedy . . . . . .
13.2.2 Substructura optima . . . . . . . . . . .
13.3 Minimizarea timpului mediu de asteptare . . . .
13.4 Interclasarea optima a mai multor siruri ordonate

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

167
. 168
. 169
. 171
. 173
. 176
. 176
. 177
. 180

.
.
.
.
.
.
.
.

CUPRINS

14 Programare dinamica
14.1 Istoric si descriere . . . . . . . . . . . . . . . . . . . . . .
14.2 Primii pasi n programarea dinamica . . . . . . . . . . . .
14.2.1 Probleme de recurenta matematica tratate dinamic
14.3 Fundamentare teoretica . . . . . . . . . . . . . . . . . . .
14.4 Principiul optimalitatii . . . . . . . . . . . . . . . . . . .
14.4.1 Metoda nainte si metoda napoi . . . . . . . .
14.4.2 Determinarea efectiva a solutiei optime . . . . . .
14.5 nmultirea unui sir de matrice . . . . . . . . . . . . . . . .
14.6 Subsir comun de lungime maxima . . . . . . . . . . . . .
14.7 Distanta Levensthein . . . . . . . . . . . . . . . . . . . .

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

186
. 187
. 188
.188
. 198
. 201
. 204
. 205
. 208
. 215
. 220

15 Metoda Branch & bound


15.1 Prezentare generala . . . . . . . . .
15.1.1 Fundamente teoretice . . . .
15.2 Un exemplu: Puzzle cu 15 elemente
15.2.1 Enuntul problemei . . . . .
15.2.2 Rezolvarea problemei . . .

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

255
. 255
. 255
. 256
. 256
. 256

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

16 Metode de elaborare a algoritmilor (sinteza)


16.1 Backtracking . . . . . . . . . . . . . . .
16.2 Divide et impera . . . . . . . . . . . . . .
16.3 Greedy . . . . . . . . . . . . . . . . . . .
16.4 Programare dinamica . . . . . . . . . . .
16.5 Branch & bound . . . . . . . . . . . . . .

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

236
236
237
240
240
241

Introducere
Nu calculati capacitatea unui pod
numarnd persoanele care
traverseaza acum rul not.
Auzita la o prezentare

Oricine a folosit cel putin o data Internetulsau a citit o revista de specialitate


n domeniul informaticii, a auzit cu siguranta cuvntul Java. Java reprezinta
un limbaj de programare, creat de compania Sun Microsystems n anul 1995.
Initial, Java a fostgnditpentru a mbunataticontinutulpaginilorweb prinadaugarea unui continut dinamic: animatie, multimedia etc. n momentul lansarii
sale, Java a revolutionat Internetul, deoarece era prima tehnologie care oferea
un astfel de continut. Ulterior au aparut si alte tehnologii asemanatoare (cum
ar Microsoft ActiveX sau Macromedia Shockwave1), dar Java si-a pastrat
importanta deosebita pe care a dobndit-o n rndul programatorilor, n primul
rnd datorita facilitatilor pe care le oferea. ncepndcu anul1998, cnd a aparut
versiunea 2 a limbajului (engl. Java 2 Platform), Java a fost extins, acoperind si
alte directii de dezvoltare: programarea aplicatiilor enterprise (aplicatii de tip
server), precum si a celor adresate dispozitivelor cu resurse limitate, cum ar
telefoane mobile, pager-e sau PDA-uri2. Toate acestea au reprezentat facilitati
noi adaugate limbajului, care a pastrat nsa si posibilitatile de a crea aplicatii
standard, de tipul aplicatiilor n linie de comanda sau aplicatii bazate pe GUI3.
1Cei care utilizeaza mai des Internetul sunt probabil obisnuiti cu controale ActiveX sau cu ani
matii ash n cadrul paginilor web.
2PDA = Personal Digital Assistant (mici dispozitive de calcul, de dimensiuni putin mai mari
dect ale unui telefon mobil, capabile sa ofere facilitati de agenda, dar si sa ruleze aplicatii ntr-un
mod relativ asemanator cu cel al unui PC). La momentul actual exista mai multe tipuri de PDA-uri:
palm-uri, pocketPC-uri, etc.
3GUI = Graphical User Interface, interfata graca de comunicare cu utilizatorul, cum sunt n
general aplicatiile disponibile pe sistemul de operare Microsoft Windows.

INTRODUCERE
Lansarea versiunii 2 a limbajului Java a fost o dovad a succesului imens de
care s-au bucurat versiunile anterioare ale limbajului, dar i a dezvoltrii limba
jului n sine, a evoluiei sale ascendente din punct de vedere al facilitilor pe
care le ofer, ct i al performanelor pe care le realizeaz.
Cum este organizat aceast carte?
Avnd n vedere popularitatea extraordinar de care se bucur limbajul Java
n cadrul programatorilor din ntreaga lume, am considerat util scrierea unei
lucrri n limba romn care s fie accesibil celor care doresc s nvee sau s
aprofundeze acest limbaj. Ideea care a stat la baza realizrii acestei cri a fost
aceea de a prezenta nu numai limbajul Java n sine, ci i modul n care se imple
menteaz algoritmii i structurile de date fundamentale n Java, elemente care
sunt indispensabile oricrui programator cu pretenii. Aadar, cartea nu este
destinat numai celor care doresc s acumuleze noiuni despre limbajul Java n
sine, ci i celor care intenioneaz s i aprofundeze i rafineze cunotinele
despre algoritmi i s i dezvolte un stil de programare elegant. Ca o con
secin, am structurat cartea n dou volume: prima volum (disponibil separat)
este orientat spre prezentarea principalelor caracteristici ale limbajului Java, n
timp ce volumul al doilea (cel de fa) constituie o abordare a algoritmilor din
perspectiva limbajului Java. Finalul primului volum cuprinde un grup de cinci
anexe, care prezint mai amnunit anumite informaii cu caracter mai special,
deosebit de utile pentru cei care ajung s programeze n Java. Am ales aceast
strategie deoarece a dobndi cunotine despre limbajul Java, fr a avea o imag
ine clar despre algoritmi, nu reprezint un progres real pentru un programator.
Iar scopul nostru este acela de a v oferi posibilitatea s devenii un programator
pentru care limbajul Java i algoritmii s nu mai constituie o necunoscut.
Cele dou volume cuprind numeroase soluii Java complete ale problemelor
prezentate. Mai este necesar o remarc: deseori am optat, att n redactarea co
dului surs ct i n prezentarea teoretic a limbajului sau a algoritmilor, pentru
pstrarea terminologiei n limba englez n defavoarea limbii romne. Am luat
aceast decizie, innd cont de faptul c muli termeni s-au consacrat n acest
format, iar o eventual traducere a lor le-ar fi denaturat nelesul.
Primul volum al crii cuprinde opt capitole:
Capitolul 1 reprezint o prezentare de ansamblu a tehnologiei Java. Capi
tolul debuteaz cu istoria limbajului, ncepnd cu prima versiune i pn la cea
curent. n continuare, sunt nfiate cteva detalii despre implementrile ex
istente ale limbajului. Implementarea Java a firmei Sun este tratat separat, n
detaliu, fiind i cea pe care s-au testat aplicaiile realizate pe parcursul crii.
Capitolul 2 este cel care d startul prezentrii propriu-zise a limbajului Java,
8

INTRODUCERE

ncepnd cu crearea si executarea unui program simplu. Sunt prezentate apoi


tipurile primitive de date, constantele, declararea si initializarea variabilelor,
operatorii de baza, conversiile, instructiunile standard si metodele.
Capitolul 3 este destinat referintelor si obiectelor. Sunt prezentate n de
taliu notiunile de referinta, obiect, siruri de caractere (String-uri) si de siruri de
elemente cu dimensiuni variabile.
Capitolul 4 continua prezentarea nceputa n capitolul anterior, prezentnd
modul n care se pot deni propriile clase n Java si cum se implementeaza
conceptele fundamentale ale programarii orientate pe obiecte.
pe obiecte:
Capitolul
mostenirea.
5 prezinta Sunt
n detaliu
prezentate
un principiu
de asemenea
esentialnotiuni
al programariiorientate
adiacente cum ar
cea de polimorsm, interfata, clasa interioara, identicare a tipurilor de date
n faza de executie (RTTI = Runtime Type Identication).
Capitolul 6 este dedicat n ntregime modului de tratare a exceptiilor n
Java. Se prezintatipurile de exceptiiexistente n limbajulJava, cum se pot deni
propriile tipuri de exceptii, cum se pot prinde si trata exceptiile aruncate de o
aplicatie. Finalul capitoluluieste rezervatuneiscurte liste de sugestii referitoare
la utilizarea ecienta a exceptiilor.
Capitolul 7 prezinta sistemul de intrare-iesire (I/O) oferit de limbajul Java.
Pe lnga operatiile standard realizate pe uxurile de date (stream) si siere
secventiale, este prezentata si notiunea de colectie de resurse (engl. resource
bundles).
Capitolul 8 este rezervat problemei delicate a relor de executie (threaduri). Pornind cu informatii simple despre rele de executie, se continua cu
accesul concurent la resurse, sincronizare, monitoare, coordonarea relor de
executie, cititorul dobndind n nal o imagine completa asupra sistemului de
lucru pe mai multe re de executie, asa cum este el att de elegant realizat n
Java.
Cele opt capitole ale primului volum sunt urmate de un grup de anexe, care
contin multe informatii utile programatorilor Java.
Anexa A constituie o lista cu editoarele si mediile integrate pentru dez
voltarea aplicatiilor Java, precum si un mic tutorial de realizare si executare a
unei aplicatii Java simple. Tot aici este prezentata si ant, o unealta de foarte
mare ajutor n compilarea si rularea aplicatiilor de dimensiuni mai mari.
Anexa B este dedicata conventiilor de scriere a programelor. Sunt prezen
tate principalele reguli de scriere a unor programe lizibile, conforme cu stan
dardul stabilit de Sun Microsystems. Ultima parte a anexei este dedicata unei
unelte foarte utile n documentarea aplicatiilor Java: javadoc.
Anexa C detaliaza ideea de pachete, oferind informatii despre pachete Java
9

INTRODUCERE

predenite si despre posibilitatea programatorului de a deni propriile sale pa


chete. Un accent deosebit se pune si pe prezentarea arhivelor jar.
Anexa D prezinta modul n care se pot realiza aplicatii internationalizate,
prin care textele care apar ntr-o aplicatie sunt traduse dintr-o limba n alta, cu
unefortminim de implementare. De asemenea, este prezentat si rolulcolectiilor
de resurse (engl. resource bundles) n internationalizarea aplicatiilor.
Anexa E reprezinta o lista de resurse disponibile programatorului Java,
pornind de la site-ul Sun Microsystems si pna la tutoriale, carti, reviste online,
liste de discutii disponibile pe Internet. Cu ajutorul acestora, un programator
Java poate sa si dezvolte aptitudinile de programare si sa acumuleze mai multe
cunostinte despre limbajul Java.
Volumul de fata, al doilea al cartii, este destinat prezentarii algoritmilor.
Independenta algoritmilor relativ la un anumit limbaj de programare, face ca
majoritatea programelor din aceasta parte sa e realizate si n pseudocod, punc
tnd totusi pentru ecare n parte specicul implementarii n Java.
Capitolul 9 constituie primul capitol al celui de-al doilea volum si prezinta
modalitatea prin care se poate realiza analiza ecientei unui algoritm. Notatia
asimptotica, tehnicile de analiza a algoritmilor, algoritmii recursivi constituie
principala directie pe care se axeaza acest capitol.
Capitolul 10 reprezinta o incursiune n cadrul structurilor de date utilizate
cel mai frecvent n conceperea algoritmilor: stive, cozi, liste nlantuite, arbori
binaride cautare, tabele de repartizare si cozi de prioritate. Fiecare dintre aceste
structuri beneciaza de o prezentare n detaliu, nsotita de o implementare Java
n care se pune accentpe separarea interfeteistructuriide date de implementarea
acesteia.
Capitolul 11 constituie startul unei suite de capitole dedicate metodelor
fundamentale de elaborare a algoritmilor. Primul capitol din aceasta suita este
rezervatcelei mai elementare metode: backtracking. Dupa o analiza amanuntita
a caracteristicilor acestei metode (cum ar cei patru pasi standard n imple
mentarea metodei: atribuie si avanseaza, ncercare esuata, revenire, revenire
dupa construirea unei solutii), sunt prezentate cteva exemple de probleme cla
sice care admit rezolvare prin metoda backtracking: generarea permutarilor, a
aranjamentelor sia combinarilor,problema damelor,problemacolorariihartilor.
Capitolul 12 prezinta o alta metoda de elaborare a algoritmilor: divide et
impera. Prima parte a capitolului este rezervata prezentarii unor notiuni intro
ductive despre recursivitate si recurenta, absolut necesare ntelegerii modului n
Analog
propriucare
zisa afunctioneaza
metodei esteaceasta
nsotitametoda.
de cteva
exemple
capitolului
de probleme
11, prezentarea
clasice rezolvabile
prin aceasta metoda: cautarea binara, sortarea prin interclasare (mergesort),
10

INTRODUCERE

sortarea rapida (quicksort), trecerea expresiilor aritmetice n forma poloneza


postxata.
Capitolul 13 prezinta cea de-a treia metoda de elaborare a algoritmilor:
metoda Greedy. Capitolul pastreaza aceeasi structura ca si cele precedente:
sunt prezentate mai nti elementele introductive ale metodei, urmate apoi de
cteva exemple clasice de probleme rezolvabile prin aceasta metoda: problema
spectacolelor, minimizarea timpului de asteptare, interclasarea optima a mai
multor siruri ordonate.
Capitolul 14 este rezervat unei metode speciale de elaborare a algoritmilor:
programarea dinamica, ce reprezinta probabil cea mai complexa metoda de
elaborare a algoritmilor, punnd deseori n dicultate si programatorii experi
mentati. Totusi avem credinta ca modul simplu si clar n care sunt prezentate
notiunile sa spulbere mitul care nconjoara aceasta metoda. Capitolul debuteaza
cu o fundamentareteoretica a principalelorconcepte ntlnite n cadrul metodei.
Apoi, se continua cu rezolvarea unor probleme de programare dinamica: n
multirea unui sir de matrice, subsirul comun de lungime maxima, distanta Levensthein etc.
Capitolul 15 reprezinta ultimul capitol din seria celor dedicate metodelor
de elaborare a algoritmilor. Metoda branch and bound este cea abordata n
cadrul acestui capitol, prin intermediul unui exemplu clasic de problema: jocul
de puzzle cu 15 elemente.
Capitolul 16 reprezinta o sinteza a metodelor de elaborare a algoritmilor
care au fost tratate de-a lungul volumului al doilea, prezentnd aspecte comune
si diferente ntre metode, precum si aria de aplicabilitate a ecarei metode n
parte.
Esenta acestui volum o reprezinta metodele fundamentale de elaborare a
algoritmilor mpreuna cu structurile de date cele mai uzuale, precum si modul
n care acestea se particularizeaza pentru a implementate n limbajul Java.
Primul capitol al acestei parti introduce notiuni esentiale referitoare la analiza ecientei algoritmilor, notiuni care vor ulterior folosite pentru a evalua
precum de
tati.
ecienta
Urmatorul
diferiteloroperatiipe
capitolul introduce
structuride
succesiv
date,
structurile
si adate
algoritmilorprezencele mai uzuale,
ncepnd cu listele si ncheind cu structuri ceva mai complexe cum ar arborii
sau cozile de prioritate.
Restul capitolelor (de la capitolul 11 pna la capitolul 15) prezinta pe rnd
metodele fundamentale de elaborare a algoritimilor care reprezinta niste tehnici
cu caracter general prin care se poate rezolva o anumita clasa larga de proble
me. Aceste metode nu sunt legate de un limbaj de programare anume, si din
acest motiv multi autori prefera sa le trateze la modul general, descriind re
11

INTRODUCERE

zolvarea problemelor n pseudocod. n aceasta lucrare am urmarit sa prezentam


metodele de elaborare a algoritmilor la modul general, precum si specicul dat
de implementarea rezolvarilor n limbajul Java.
Metodele de elaborare a algoritmilor prezentate n aceasta parte sunt:
Backtracking: se aplica problemelor a caror solutie se poate scrie sub
forma de vector. Aceasta metoda construieste vectorul solutie compo
nenta cu componenta, cu eventuale reveniri asupra componentelor ante
rioare;
Greedy: principiul acestei metode este asemanator cu cel de la back
tracking, cu diferenta ca selectarea urmatorului pas se face pe baza unui
criteriu local fara a se reveni asupra pasilor anteriori;
Divide et impera (dezbina si cucereste): aceasta metoda mparte pro
blema originala n doua sau mai multe subprobleme; subproblemele sunt
mpartite la rndul lor n sub-subprobleme si asa mai departe pna cnd
se ajunge la subprobleme de dimensiune mica, a caror rezolvare este tri
viala. Se construieste apoi solutia problemei originale prin combinarea
solutiilor subproblemelor. Evident, nu orice problema poate rezolvata
n acest mod;
Programare dinamica: aplicabila doar problemelor de optimizare (sau
care pot reformulate ca probleme de optimizare) care respecta asanumitul principiu al optimalitatii;
Branch & bound: termen care ar putea tradus cu aproximatie prin
"mparte si evalueaza". Este o varianta a tehnicii backtracking, n care
alegerea urmatoruluipas nu se face la ntmplare, ci ntr-o anumita ordine
data de o evaluare locala a sanselor ca acel pas sa conduca la o solutie.
Metodele de elaborare a algoritmilorau fost concepute ca niste tehnicicu carac
ter generalaplicabile uneiclase foarte largide probleme de programare. Majori
tatea problemelor de programarepot abordate cu una sau mai multe din aceste
metode de elaborare a algoritmilor si, astfel, programatorulnu este ntotdeauna
nevoit sa conceapa cte o metoda ad-hoc pentru ecare problema pe care o are
de rezolvat. Programatorul trebuie sa ncadreze problema pe care o are de rezolvat n una dintre aceste metode de elaborare, dupa care particularizeaza acea
metoda pentru problema concreta si alege structurile de date adecvate.
n linii mari, putem spune ca rezolvarea unei probleme de programare pre
supune urmatorii pasi:
12

INTRODUCERE

1. Identicarea metodei de elaborare si a structurilor de date potrivite;


2. Particularizarea acestei tehnici pentru problema concreta.
n acest punct, tehnicile de programare se diferentiaza: pentru unele metode
(cum ar backtracking) trecerea de la forma generala la forma concreta este
aproapealgoritmica, neind necesarunmare efortde adaptare, n timpce pentru
altele (cum ar programarea dinamica) trecerea necesita un efort considerabil,
dublat de ingeniozitate si o profunda stapnire a metodei.

Cui se adreseaza aceasta carte?


Lucrarea de fata nu se adreseaza ncepatorilor, ci persoanelor care stap
nesc deja, chiar si partial, un limbaj de programare. Cititorii care au cunostinte
de programare n C si o minima experienta de programare orientata pe obiecte
vor gasi lucrarea ca ind foarte usor de parcurs. Nu sunt prezentate notiuni
elementare specice limbajelor de programare cum ar functii, parametri, in
structiuni etc. Nu se presupune cunoasterea unor elemente legate de progra
marea orientata pe obiecte, desi existenta lor poate facilita ntelegerea notiu
nilor prezentate. De asemenea, cartea este foarte utila si celor care doresc sa
aprofundeze studiul algoritmilor si modul n care anumite probleme clasice de
programare pot implementate n Java.

Pe Internet
Pentru comoditatea cititorilor, am decis sa punem la dispozitia lor codul
sursa complet al tuturor programelorprezentate pe parcursulcelor doua volume
ale lucrarii n cadrul unei pagini web conceputa special pentru interactiunea cu
cititorii. De asemenea, pagina web a cartii va gazdui un forum unde cititorii
vor putea oferi sugestii n vederea mbunatatirii continutului lucrarii, vor putea
schimba opinii n legatura cu diversele aspecte prezentate, adresa ntrebari autorilor etc. Adresele la care veti gasi aceste informatii sunt:
http://www.albastra.ro/carti/v178/
http://www.danciu.ro/apj/

Multumiri
n ncheiere, dorim sa adresam multumiri colegilor si prietenilor nostri care
ne-au acordat ajutorul n realizarea acestei lucrari: Vlad Petric (care a avut o
13

INTRODUCERE
contribuie esenial la structurarea capitolului 14), Alexandru Blu (autor al
anexei A), Iulia Tatomir (a parcurs i comentat cu mult rbdare de mai multe
ori ntreaga carte) i Rul Furnic (a parcurs i comentat capitolele mai delicate
ale lucrrii).

14

9.Analiza ecientei algoritmilor

Nu va faceti griji pentru


problemele pe care vi le pune
matematica. Va asigur ca ale mele
sunt cu mult mai mari.
Albert Einstein

n prima parte a cartii am examinat cum putem folosi programarea orientata


pe obiecte pentru proiectarea si implementarea programelor profesionale. Au
fost prezentate trasaturile fundamentale ale limbajului Java, precum si facilitati
mai putin cunoscute, dar foarte utile, cum ar mecansimul de reectare (Reection API) sau colectiile de resurse. Totusi, aceasta reprezinta doar jumatate
din problema.
Calculatorul este folosit de obicei pentru a prelucra cantitati mari de infor
siguriexecutam
matie.
trebuieAtunci
sa m cnd
ca vom obtine
un program
rezultatul
cu date
ntr-un
de intrare
timp rezonabil.
de dimensiunimari,
Acest lu
cru este aproape ntotdeauna independent de limbajul de programare folosit, ba
chiar si de metodologia aplicata (cum ar programare orientata pe obiecte, sau
programare procedurala).
Un algoritm este un set bine precizat de instructiuni pe care calculatorul le
va executa pentru a rezolva o problema. Odata ce am gasit un algoritm pentru
o anumita problema si am determinat ca algoritmul este corect, pasul urmator
este de a determina cantitatea de resurse, cum ar timpul si cantitatea de me
morie, pe care algoritmul le cere. Acest pas este numit analiza algoritmului. Un
algoritm care are nevoie de ctiva gigabytes de memorie nu este bun de nimic
pe calculatoarele existente la ora actuala, chiar daca el este corect.
n acest capitol vom vedea:
Cum putem estima timpul cerut de un algoritm (altfel spus, determinarea
complexitatii algoritmului);
15

9.1. CE ESTE ANALIZA ALGORITMILOR?


Tehnici pentru reducerea drastic a timpului de execuie al unui algoritm;
Un cadru matematic care descrie la un mod mai riguros timpul de execuie
al algoritmilor.

9.1

Ce este analiza algoritmilor?

Cantitatea de timp pe care orice algoritm o cere pentru execuie depinde aproape ntotdeauna de cantitatea de date de intrare pe care o proceseaz. Este
de ateptat c sortarea a 10.000 de elemente s necesite mai mult timp dect
sortarea a 10 elemente. Timpul de execuie al unui algoritm este astfel o funcie
de dimensiunea datelor de intrare. Valoarea exact a acestei funcii depinde
de mai muli factori, cum ar fi viteza calculatorului pe care ruleaz progra
mul, calitatea compilatorului i, nu de puine ori, calitatea programului. Pentru
un program dat, care ruleaz pe un anumit calculator, putem reprezenta grafic
timpul de execuie. n Figura 9.1 am realizat un astfel de grafic pentru patru
programe. Curbele reprezint patru funcii care sunt foarte des ntlnite n analiza algoritmilor: liniar, n log n, ptratic i cubic. Dimensiunea datelor
de intrare variaz de la 1 la 100 de elemente, iar timpii de execuie de la 0 la 5
milisecunde. O privire rapid asupra graficelor din Figura 9.1 i Figura 9.2 ne
lmurete c ordinea preferinelor pentru timpii de execuie este liniar, n log n,
ptratic i cubic.
S lum ca exemplu descrcarea (download-area) unui fiier de pe Internet.
S presupunem c la nceput apare o ntrziere de dou secunde (pentru a stabili
conexiunea), dup care descrcarea se va face la 1.6 KB/sec. n aceast situ
aie, dac fiierul de adus are N kilobaii, timpul de descrcare a fiierului este
descris de formula T ( N ) = N/1.6 + 2. Aceasta este o funcie liniar. Se
poate calcula uor c descrcarea unui fiier de 80K va dura aproximativ 52 de
secunde, n timp ce descrcarea unui fiier de dou ori mai mare (160K) va dura
102 secunde, deci cam de dou ori mai mult. Aceast proprietate, n care tim
pul este practic direct proporional cu cantitatea de date de intrare, este specific
unui algoritm liniar, i constituie adeseori o situaie ideal. Aa cum se vede
din grafice, unele curbe neliniare pot conduce la timpi de execuie foarte mari.
n acest capitol vom analiza urmtoarele probleme: cu ct este mai bun o
curb n comparaie cu o alt curb, cum putem calcula pe care curb se situeaz
un anumit algoritm sau cum putem proiecta algoritmi care s nu se situeze pe
curbele nefavorabile.
O funcie cubic este o funcie al crei termen dominant este N3, nmulit
cu o constant. De exemplu, 107V3 + N2 + 40N + 80 este o funcie cubic.
16

9.1. CE ESTE ANALIZA ALGORITMILOR?

Figura 9.1: Timpi de executie pentru date de dimensiune mica

Figura 9.2: Timpi de executie pentru date de intrare moderate

17

9.2. NOTAIA ASIMPTOTIC


Similar, o funcie ptratic are termenul dominant N2 nmulit cu o constant,
iar o funcie liniar are un termen dominant care este N nmulit cu o constant.
Oricare dintre cele trei funcii prezentate mai sus poate fi mai mic dect
cealalt ntr-un punct dat. Acesta este motivul pentru care nu ne intereseaz
valorile efective ale timpilor de execuie, ci rata lor de cretere. Acest lucru este
justificabil prin trei argumente. n primul rnd, pentru funciile cubice, cum
ar fi cea prezentat n Figura 9.2, atunci cnd N are valoarea 1000, valoarea
funciei cubice este aproape complet determinat de valoarea termenului cubic.
Funcial0A^3+A^2-|-40Af-|-80arevaloareal0.001.040.080pentruAf = 1000,
din care 10.000.000.000 se datoreaz termenului 107V3. Dac am fi folosit doar
termenul cubic pentru a estima valoarea funciei, ar fi rezultat o eroare de aprox
imativ 0.01%. Pentru un N suficient de mare, valoarea funciei este determinat
aproape complet de termenul ei dominant (semnificaia termenului suficient de
mare depinde de funcia n cauz).
Un al doilea motiv pentru care msurm doar rata de cretere a funciilor
este c valoarea exact a constantei multiplicative pentru termenul dominant
difer de la un calculator la altul. De exemplu, calitatea compilatorului poate s
influeneze destul de mult valoarea constantei. In al treilea rnd, valorile mici
pentru N sunt de obicei nesemnificative. Din Figura 9.1 se observ c pentru
N = 10, toi algoritmii se ncheie n mai puin de 3 ms. Diferena dintre cel
mai bun i cel mai slab algoritm este mai mic dect un clipit de ochi.
Pentru a reprezenta rata de cretere a unui algoritm se folosete aa-numita
notaie asimptotic (engl. "Big-Oh notation"). De exemplu, rata de cretere
pentru un algoritm ptratic este notat cu 0(7V2). Notaia asimptotic ne per
mite s stabilim o ordine parial ntre funcii prin compararea termenului lor
dominant.
Vom dezvolta n acest capitol aparatul matematic necesar pentru analiza
eficienei algoritmilor, urmrind ca aceast incursiune matematic s nu fie ex
cesiv de formal. Vom arta apoi, pe baz de exemple, cum poate fi analizat un
algoritm. O atenie special o vom acorda tehnicilor de analiz a algoritmilor
recursivi.

9.2

Notaia asimptotic

Notaia asimptotic are rolul de a estima timpul de calcul necesar unui al


goritm pentru a furniza rezultatul, funcie de dimensiunea datelor de intrare.
18

9.2. NOTAIA ASIMPTOTIC


9.2.1

O notaie pentru ordinul de mrime al timpului de ex


ecuie al unui algoritm

Fie N mulimea numerelor naturale, ? mulimea numerelor reale. Fie / :


N [0, oo) o funcie arbitrar. Definim mulimea de funcii:
O(f) = {t : N - [0, oo) | 3c > 0, 3n0 G N, astfel
no avem t(ri) < c * f (n)}

incat

n >

Cu alte cuvinte, 0(f) (se citete "ordinul luif") este mulimea tuturor funci
ilor t mrginite superior de un multiplu real pozitiv al lui /, pentru valori sufi
cient de mari ale argumentului n. Vom conveni s spunem c t este n ordinul
lui / (sau, echivalent, t este n 0(f), sau t G 0(/) ) chiar i atunci cnd t(n)
este negativ sau nedefinit pentru anumite valori n < no In mod similar, vom
vorbi despre ordinul lui / chiar i atunci cnd valoarea f(n) este negativ sau
nedefinit pentru un numr finit de valori ale lui n; n acest caz, vom alege no
suficient de mare, astfel nct pentru n > no acest lucru s nu mai apar. De
exemplu, vom vorbi despre ordinul lui n/log n , chiar dac pentru n=0 i n=l
funcia nu este definit. In loc de t 6 O(f), uneori este mai convenabil s
folosim notaia t(n) G 0(/(n)), subnelegnd aici c t(n) if(n) sunt funcii.
Fie un algoritm dat i fie o funcie t : N [0, oo), astfel nct o anumit
implementare a algoritmului s necesite cel mult t(n) uniti de timp pentru a
rezolva un caz de mrime n, unde n G N. Principiul invarianei1 ne asigur
atunci c orice implementare a algoritmului necesit un timp n ordinul lui t.
Cu alte cuvinte, acest algoritm necesit un timp n ordinul lui / pentru orice
funcie / : N [0, oo) pentru care t G O(f). n particular avem relaia:
t G 0(t) . Vom cuta, n general, s gsim cea mai simpl funcie/ astfel nct
teO(f).
Exemplu: Fie funcia t(n) = 3n2 9n + 13. Pentru n suficient de mare, vom
avea relaia t(n) < 4n2. n consecin, lund c = 4, putem spune c t{n) G
0(n2). La fel de bine puteam s spunem c t(n) G 0(13n2 \[2n + 12.5),
dar pe noi ne intereseaz s gsim o expresie ct mai simpl. Este adevrat
i relaia t(n) G 0(n4) dar, aa cum vom vedea mai trziu, suntem interesai
de a mrgini ct mai strns ordinul de mrime al algoritmului, pentru a putea
obiectiva ct mai bine durata sa de execuie.
Proprietile de baz ale lui O(f) sunt date ca exerciii (1 - 5) i ar fi reco
mandabil s le studiai nainte de a trece mai departe.
1 Acest principiu afirm c dou implementri diferite ale aceluiai algoritm nu difer, ca efi
cien, dect cel mult printr-o constant multiplicativ.
19

9.2. NOTAIA ASIMPTOTIC


Notaia asimptotic definete o relaie de ordine parial ntre funcii i, prin
urmare, ntre eficiena relativ a diferiilor algoritmi care rezolv o anumit
problem. Vom da n continuare o interpretare algebric a notaiei asimptotice.
Pentru oricare dou funcii /, g : N 5ft* definim urmtoarea relaie binar:
/ < g dac O(f) C O(g). Relaia "< " este o relaie de ordine parial
(reflexiv, tranzitiv, antisimetric) n mulimea funciilor definite pe N i cu
valori n [0, oo) (exerciiul 4). Definim i o relaie de echivalen: f = g
dac 0(f)=0(g). Prin aceast relaie obinem clase de echivalen, o clas de
echivalen cuprinznd toate funciile care difer ntre ele printr-o constant
multiplicativ. De exemplu, Ign = Inni avem o clas de echivalen a funci
ilor logaritmice, pe care o notm generic cu Oflog n) . Notnd cu O(l) clasa de
echivalen a algoritmilor cu timpul mrginit superior de o constant (cum ar
fi interschimbarea a dou numere, sau maximul a trei elemente), ierarhia celor
mai cunoscute clase de echivalen este:
0(1) C 0(logn) C O(n) C 0(nlogn) C 0(n2) C 0(n3) C 0(2")
Aceast ierarhie corespunde unei clasificri a algoritmilor dup un criteriu
al performanei. Pentru o problem dat, dorim mereu s obinem un algoritm
corespunztor unei clase aflate ct mai "de jos" (cu timp de execuie ct mai
mic). Astfel, se consider a fi o mare realizare dac n locul unui algoritm
exponenial gsim un algoritm polinomial.
Exerciiul 5 ne d o metod de simplificare a calculelor n care apare notaia
asimptotic. De exemplu:
n3 + 4n2 + 2n + 7 G 0(n3 + (4n2 +2n + 7)) =
0{max(n3,4n2 +2n + 7)) = 0(n3)
Ultima egalitate este adevrat chiar dac max(n3,4n2 + 2n + 7) ^ n3
pentru 0 < n < 4, deoarece notaia asimptotic se aplic doar pentru n suficient
de mare. De asemenea,
n3 - 3n2 - n - 8 G 0(^ +
- 3n2 - n - 8)) =
0{max{^, ^ - 3n2 - n - 8)) = O(^) = 0(n3)
chiar dac pentru 0 < n < 6 polinomul este negativ. Exerciiul 6 trateaz cazul
unui polinom oarecare.
Notaia O(f) este folosit pentru a limita superior timpul necesar unui algo
ritm, msurnd eficiena (complexitatea computaional) a algoritmului respec
tiv. Uneori este interesant s estimm i o limit inferioar a acestui timp. In
acest scop, definim mulimea:
20

9.3. TEHNICI DE ANALIZA ALGORITMILOR


fi(/) = {t : N - [0, oo) | 3c > 0, 3n0 N, astfel
no avem t(ri) > c * f (n)}

incat

n>

Exist o anumit dualitate ntre notaiile 0(f) i fi(/): pentru dou funcii
oarecare /, g : N [0, oo), avem:
/ G 0(g) dac i numai dac g G

O estimare foarte precis a timpului de execuie se obine atunci cnd timpul


de execuie al unui algoritm este limitat att inferior ct i superior de cte un
multiplu real pozitiv al aceleai funcii. n acest scop, introducem notaia:
(/) = OU) n n(/)
numit ordinul exact al lui f. Pentru a compara ordinele a dou funcii,
notaia 0 nu este ns mai puternic dect notaia O, n sensul c 0(f)=0(g)
este echivalent cu 0(/) = (<?)
Exist situaii n care timpul de execuie al unui algoritm depinde simultan
de mai muli parametri. Aceste situaii sunt tipice pentru anumii algoritmi care
opereaz cu grafuri i la care timpul depinde att de numrul de vrfuri ct
i de numrul de muchii. Notaia asimptotic se generalizeaz n mod natural
i pentru funcii cu mai multe variabile. Astfel, pentru o funcie arbitrar / :
N x N -> [0, oo) definim

N,

0(f) = {t:NxN->[0,oo)|3c>0, 3n0, m0 G


astfel incat V m>mo,V n>no avem t(m,n) <
c* f(m,n)}.

Similar se obin i celelalte generalizri.

9.3

Tehnici de analiza algoritmilor

Nu exist o metod standard pentru analiza eficienei unui algoritm. Este


mai curnd o chestiune de raionament, intuiie i experien. Vom arta pe
baz de exemple cum se poate efectua o astfel de analiz.
9.3.1

Sortarea prin selecie

Considerm algoritmul de sortare prin selecia minimului, reprodus mai jos:


21

9.3. TEHNICI DE ANALIZA ALGORITMILOR


pentrui = l,n 1
//se calculeaz poziia minimului lui a(i), a(i + 1), . . . , a{n)
PozMin < i //iniializm minimul cu indicele primului element
pentru j = i + 1, n
dac a(j) < a(PozMin) atunci
PozMin = j
sfrit dac
sfrit pentru //dup j
//se aeaz minimul pe poziia i
aux <- a(i)
a(i) <- a(PozMin)
a(PozMin) aux
sfrit pentru //dup i
Timpul necesar pentru o singur execuie a ciclului pentru dup variabila j
poate fi mrginit superior de o constant a. n total, pentru un i fixat, innd
cont de faptul c se realizeaz n-i iteraii, acest ciclu necesit un timp de cel
mult b + a(n i) uniti, unde b este o constant reprezentnd timpul necesar
pentru iniializarea buclei. O singur execuie a buclei exterioare are loc n cel
mult c + b + a(n i) uniti de timp, unde c este o alt constant. innd cont
de faptul c bucla dup j se realizeaz de n-1 ori, timpul total de execuie al
algoritmului este cel mult:
d + EIL-!1 (c + b + a(n- i))
uniti de timp, d fiind din nou o constant. Simplificm aceast expresie i
obinem |n2 + (b + c |)n + (d c b), de unde deducem c algoritmul
necesit un timp n 0{n2). O analiz similar asupra limitei inferioare arat
c timpul este de fapt n @(n2). Nu este necesar s considerm cazul cel mai
nefavorabil sau cazul mediu deoarece timpul de execuie al sortrii prin selecie
este independent de ordonarea prealabil a elementelor de sortat.
In acest prim exemplu am analizat toate detaliile. De obicei ns, detalii
cum ar fi timpul necesar iniializrii ciclurilor nu se vor considera explicit,
deoarece ele nu afecteaz ordinul de complexitate al algoritmului. Pentru cele
mai multe situaii, este suficient s alegem o anumit instruciune din algoritm
ca barometru i s numrm de cte ori se execut aceast instruciune. n cazul
nostru, putem alege ca barometru testul
a[j] < a[PozMin]
din bucla interioar. Este uor de observat c acest test se execut de n^n~1^
ori.
22

9.3. TEHNICI DE ANALIZA ALGORITMILOR


9.3.2

Sortarea prin inserie

Timpul pentru algoritmul de sortare prin inserie este dependent de ordo


narea prealabil a elementelor de sortat. Algoritmul este implementat n cadrul
primului volum, la capitolul Motenire, seciunea Extinderea clasei Shape. Analiza algoritmului se realizeaz pe baza implementrii prezentate n cadrul
acelei seciuni. Vom folosi comparaia
tmp . lessThan ( a [ j - 1])
din ciclul for ca barometru.
S presupunem c p este fixat i fie n = a.length lungimea irului. Cel
mai nefavorabil caz apare atunci cnd tmp < a[j 1] pentru fiecare j ntre p
i 1, algoritmul fcnd n aceast situaie p comparaii. Acest lucru se ntm
pl (pentru fiecare valoare a lui p de la 1 la n 1) atunci cnd tabloul a este
iniial ordonat descresctor. Numrul total de comparaii pentru cazul cel mai
nefavorabil este:
Er=-i.= Ifci) ee(rj2)
Vom estima acum timpul mediu necesar pentru un caz oarecare. Presupunem
c elementele tabloului a sunt distincte i c orice permutare a lor are aceeai
probabilitate de apariie. Atunci, dac 1 < k < p , probabilitatea ca a\p] s
fie cel de-al fe-lea cel mai mare element dintre elementele a[l], a[2], . . . , a\p]
este ^. Pentru un p fixat, condiia a\p] < a\p 1] este fals cu probabilitatea
deci probabilitatea ca s se execute comparaia "tmp < a[j 1]" o singur
dat nainte de ieirea din bucla while este K Comparaia "tmp < a[j 1]"
se execut de exact dou ori tot cu probabilitatea ^ etc. Probabilitatea ca s se
execute comparaia de exact p 1 ori este ^, deoarece aceasta se ntmpl att
cnd tmp < o[0] ct i cnd o[0] < tmp < a[l]\ Numrul mediu de compara
ii, pentru un p fixat, este n consecin, suma numrului de comparaii pentru
fiecare situaie, nmulit cu probabilitatea de apariie a acelei situaii:
Ci = li + 2i + ... + (i-2)i + (i-l)| = ii-i
Pentru a sorta n elemente avem nevoie de X^=2 c comparaii, ceea ce este
egal cu ^2 _ Hn e 0(n2). Prin Hn = J2"=1 i~x G <d(logn) am notat al
w-lea termen al seriei armonice.
Se observ c algoritmul de sortare prin inserare efectueaz pentru cazul
mediu de dou ori mai puine comparaii dect pentru cazul cel mai nefavorabil.
Totui, n ambele situaii, numrul comparaiilor este n @(n2).
23

9.3. TEHNICI DE ANALIZA ALGORITMILOR


Cu toate c algoritmul necesit un timp n fi(n2) att pentru cazul mediu
ct i pentru cel mai nefavorabil caz, pentru cazul cel mai favorabil (cnd iniial
tabloul este ordonat cresctor) timpul este n 0(n). De fapt, pentru cazul cel
mai favorabil, timpul este i n fi(n) (deci n @(n)).
9.3.3

Turnurile din Hanoi

Matematicianul francez Eduard Lucas a propus n 1883 o problem care a


devenit apoi celebr mai ales datorit faptului c a prezentat-o sub forma unei
legende. Se spune c Brahma (Zeul Creaiei la hindui) a fixat pe Pmnt trei
tije de diamant i pe una din ele a pus n ordine cresctoare 64 de discuri de
aur de dimensiuni diferite, astfel nct discul cel mai mare era jos. Brahma a
creat i o mnstire, iar sarcina clugrilor era s mute toate discurile pe o alt
tij. Singura operaiune permis era mutarea cte unui singur disc de pe o tij
pe alta, astfel nct niciodat s nu se pun un disc mai mare peste un disc mai
mic. Legenda spune c sfritul lumii se va petrece atunci cnd clugrii vor
svri lucrarea. Vom vedea c aceasta se dovedete a fi o previziune extrem de
optimist asupra sfritului lumii. Presupunnd c n fiecare secund se mut
un disc i se lucreaz fr ntrerupere, cele 64 de discuri nu pot fi mutate nici n
500 de miliarde de ani de la nceputul aciunii!
Pentru a rezolva problema, vom numerota cele trei tije cu 1, 2 i respectiv
3. Se observ c pentru a muta cele n discuri de pe tija cu numrul i pe tija
cu numrul j (i i j iau valori ntre 1 i 3) este necesar s transferm primele
n 1 discuri de pe tija i pe tija 6 i j (adic pe tija rmas liber), apoi s
transferm discul n de pe tija i pe tija j, iar apoi retransferm cele n 1 discuri
de pe tija 6 i j pe tija j. Cu alte cuvinte, reducem problema mutrii a n
discuri la problema mutrii a n 1 discuri. Urmtoarea metod Java descrie
acest algoritm recursiv.

1 public static void


2{
3
if (n > 0)
{
5
hanoi(n
6
System . out
7
hanoi(n
}
)

Listing9.1: Metoda hanoi


hanoi(int n, int i, int j)

1, i, 6 i j);
. println ( i + " >" +
1,6 i j,j);

j);

Pentru rezolvarea problemei iniiale, facem apelul


hanoi (64, 1,2);
24

9.4. ANALIZA ALGORITMILOR RECURSIVI


Considerm instruciunea pr intln ca barometru. Timpul necesar algorit
mului este exprimat prin urmtoare recuren:
1
2t(n - 1) + 1

dac
dac

n= 1
n> 1

Vom demonstra c t(n) = 2 1. Rezult c t 0(2").


Acest algoritm este optim n sensul c este imposibil s mutm discuri de
pe o tij pe alta cu mai puin de 2" 1 operaii. Pentru a muta 64 de discuri vor
fi n consecin necesare un numr astronomic de 264 operaii. Implementarea
n oricare limbaj de programare care admite exprimarea recursiv se poate face
aproape n mod direct.

9.4

Analiza algoritmilor recursivi

Am vzut n exemplul precedent ct de puternic i n acelai timp ct


de elegant este recursivitatea n elaborarea unui algoritm. Cel mai impor
tant ctig al exprimrii recursive este faptul c ea este natural i compact,
fr s ascund esena algoritmului prin detaliile de implementare. Pe de alt
parte, apelurile recursive trebuie folosite cu discernmnt, deoarece solicit i
ele resursele calculatorului (timp i memorie). Analiza unui algoritm recursiv
implic aproape ntotdeauna rezolvarea unui sistem de recurene. Vom vedea n
continuare cum pot fi rezolvate astfel de recurene. ncepem cu tehnica cea mai
simpl.

9.4.1

Metoda iteraiei

Cu puin experien i intuiie putem rezolva de multe ori astfel de re


curene prin metoda iteraiei: se execut primii pai, se intuiete forma genera
l, iar apoi se demonstreaz prin inducie matematic c forma este corect. S
considerm de exemplu recurena problemei turnurilor din Hanoi. Se observ
c pentru a muta n discuri este necesar s mutm n 1 discuri, apoi s mutm
un disc i n final din nou n 1 discuri. In consecin, pentru un anumit n > 1
obinem succesiv:
t(n) = 2t(n - 1) + 1 = 2H(n - 2) + 2 + 1 = . . . = 2""1f(l) + "~02 2i
Rezult c t(n) = 2" 1. Prin inducie matematic se demonstreaz acum
cu uurin c aceast form general este corect.
25

9.4. ANALIZA ALGORITMILOR RECURSIVI


9.4.2

Inducia constructiv

Inducia matematic este folosit de obicei ca tehnic de demonstrare a unei


aseriuni deja enunate. Vom vedea n aceast seciune c inducia matematic
poate fi utilizat cu succes i n descoperirea parial a enunului aseriunii. Aplicnd aceast tehnic, putem simultan s demonstrm o aseriune doar parial
specificat i s descoperim specificaiile care lipsesc i datorit crora aseri
unea este corect. Vom vedea c aceast tehnic a induciei constructive este
util pentru rezolvarea anumitor recurene care apar n contextul analizei algo
ritmilor, ncepem cu un exemplu.
Fie funcia / : N N definit prin recurena:
0
f(n -\)+n

dac
altfel

n= 1

S presupunem pentru moment c nu tim c f(n) = ^\- Avem


/() = E"=o 1 < "=o n = n2
i deci f(n) 0{n2). Aceasta ne sugereaz s formulm ipoteza induciei
specificate parial IISP(n) conform creia/ este de forma f(n) = an2 + bn +
c. Aceast ipotez este parial n sensul c a, b i c nu sunt nc cunoscute.
Tehnica induciei constructive const n a demonstra prin inducie matematic
aceast ipotez incomplet i a determina n acelai timp valorile constantelor
necunoscute a, b i c.
Presupunem c IlSP(n-l) este adevrat pentru un anumit n > 1. Atunci,
f(n-l) = a(n-l)2+b(n-l)+c = an2 +(l+b-2a)n+(a-b+c). Dac dorim
s artm c IISP(n) este adevrat, trebuie s artm c f(n) = an2 +bn + c.
Prin identificarea coeficienilor puterilor lui n, obinem ecuaiile 1 + b 2a = b
i o b + c = c, cu soluia a = b = \, c putnd fi oarecare. Avem acum
o ipotez "mai complet" (abuzul de limbaj este inevitabil), pe care o numim
tot IISP(n), f(n) = \ + + c. Am artat c dac IlSP(n-l) este adevrat
pentru un anumit n > 1, atunci este adevrat i IISP(n). Rmne s artm c
este adevrat i IISP(O). Trebuie s artm c /(O) = aO2 + bO + c. tim c
/(O) = 0, deci IISP(O) este adevrat pentru c = 0. In concluzie am demonstrat
c /(n) = ^ + pentru orice n.
9.4.3

Recurene liniare omogene

Exist din fericire i tehnici care pot fi folosite aproape automat pentru a re
zolva anumite clase de recurene. Vom ncepe prin a considera ecuaii recurente
liniare omogene, adic ecuaii de forma:
26

9.4. ANALIZA ALGORITMILOR RECURSIVI


a0tn + aitn-i + . . . + aktn-k = 0

(*)

unde ti sunt valorile pe care le cutm, iar coeficienii Oj sunt constante.


Conform intuiiei2, vom cuta soluii de forma:
tn X
unde x este o constant (deocamdat necunoscut). Dac nlocuim aceast
soluie n ecuaia ( * ) , obinem
a0xn + oix""1 + . . . + akxn~k = 0
Soluiile acestei ecuaii sunt fie soluia trivial a; = 0, care nu ne intereseaz,
fie soluiile ecuaiei:
a0xk + aixk~1 + .. . + ak =0
care se numete ecuaia caracteristic a recurenei liniare i omogene ( * ) .
Presupunnd deocamdat c cele k rdcini 7"i , 7"2 , , rk ale acestei ecuaii
caracteristice sunt distincte, se verific uor c orice combinaie liniar
tn = Yli=l cir?
este o soluie a recurenei ( * ) , unde constantele c, C2, . . . , ck sunt determinate
de condiiile iniiale. Se poate demonstra faptul c ( * ) are soluii numai de
aceast form.
S exemplificm prin recurena care definete irul lui Fibonacci
tn = tn-i + tn-2,n > 2
iar t0 = 0, ti = 1 . Putem s rescriem aceast recuren sub forma
tn ~ tn-1 ~ tn-2 = 0
care are ecuaia caracteristic
x2 - x - 1 = 0
cu rdcinile 7*1,2 = 1=F2V^- Soluia general are forma:
tn = cir? + c2r%
2De fapt, adevrul este c aici nu este vorba de intuiie, ci de experien.
27

9.4. ANALIZA ALGORITMILOR RECURSIVI


Impunnd condiiile iniiale, to = 0,t = 1, obinem
ci + c2 = 0, n = 0
Ci"i + c2r2 = l,n = 1
de unde determinm
Cl-2 = ^7E
Deci, i = -^{t + r" ) Observm c ri = </>, r2 = </>-1 i obinem:

' = 7f^" " (-0"")


care este cunoscuta relaie a lui Moivre, descoperit la nceputul secolului XVIII.
Nu prezint nici o dificultate s artm acum c timpul pentru calculul recursiv
al irului lui Fibonacci este n 0 ((/>").
Cum procedm ns atunci cnd rdcinile ecuaiei caracteristice nu sunt
distincte? Se poate arta c dac r este o rdcin de multiplicitate m a ecuaiei
caracteristice, atunci tn = rn ,tn = nrn,tn = n2rn , . . . ,tn = n-1}- sunt
soluii pentru ( * ) . Soluia general pentru o astfel de recuren este atunci
o combinaie liniar a acestor termeni i a termenilor provenii de la celelalte
rdcini ale ecuaiei caracteristice. Din nou, sunt de determinat exact k con
stante din condiiile iniiale.
Vom da din nou un exemplu. Fie recurena
tn = 5_i 8tn-2 + 4i_3
cu fo = 0, ti = 1, ti = 2. Ecuaia caracteristic are rdcinile 1 (de multiplici
tate 1) i 2 (de multiplicitate 2). Soluia general este:
tn = cil" + c22" + c3n2"
Din condiiile iniiale, obinem C\ = 2, c2 = 2, C3 = | .
9.4.4

Recurene liniare neomogene

Considerm acum recurene de urmtoarea form mai general


a0tn + aitn-i + ... aktn-k = bnp(n) (**)
28

9.4. ANALIZA ALGORITMILOR RECURSIVI


unde b este o constant, iar p(n) este un polinom n n de grad d. Ideea general
este ca prin manipulri convenabile s reducem un astfel de caz la o form
omogen.
De exemplu, o astfel de recuren poate fi:
tn ~

1 = 3"

In acest caz b = 3 i p(n) = 1 un polinom de grad 0. O simpl manipulare


ne permite s reducem acest exemplu la forma ( * ) . nmulind recurena cu 3,
obinem:
3t - 6t_i = 3"+1
nlocuind pe n cu n + 1 n recurena original, avem:

n final, scdem aceste dou ecuaii i obinem:


tn+i 5tn + 6i_i = 0
Am obinut o recuren omogen pe care o putem rezolva ca n seciunea
precedent. Ecuaia caracteristic este:
x2 - 5x + 6 = 0
adic (x - 2)(x - 3) = 0.
Intuitiv, observm c factorul (x 2) corespunde prii stngi a recurenei
originale, n timp ce factorul (x 3) a aprut ca rezultat al manipulrilor efec
tuate pentru a scpa de partea dreapt.
Iat un al doilea exemplu:
tn - 2t_1 = (n + 5)3"
Manipulrile necesare sunt puin mai complicate. Trebuie s:
1 . nmulim recurena cu 9;
2. nlocuim n recuren pe n cu n + 2;
3. nlocuim n recuren pe n cu n + 1 i s nmulim apoi cu -6.
Adunnd cele trei ecuaii obinute anterior avem:
29

9.4. ANALIZA ALGORITMILOR RECURSIVI


tn+2 ~ &tn+i + 21 18i_i 0
Am ajuns din nou la o ecuaie omogen. Ecuaia caracteristic corespunz
toare este
x3 - 8x2 + 21x - 18 = 0
adic (x - 2){x - 3)2.
nc o dat, observm c factorul (x 2) provine din partea stng a re
curenei originale, n timp ce factorul (x 3) 2 este rezultatul manipulrii.
Generaliznd acest procedeu, se poate arta c pentru a rezolva ( * * ) este
suficient s lum urmtoarea ecuaie caracteristic:
(aoxk + axxk-x + ...+ ak)(x - b)d+1 = 0
Odat ce s-a obinut aceast ecuaie, se procedeaz ca n cazul omogen.
Vom rezolva acum recurena corespunztoare problemei turnurilor din Hanoi
tn = 2tn-i + l,n > 1
iar to = 0. Rescriem recurena astfel
tn ~ 2tjj 1 = 1
care este de forma ( * * ) cu b = 1 i p(n) = 1, un polinom cu grad 0. Ecuaia
caracteristic este atunci (x l)(x 2), cu soluiile 1 i 2. Soluia general a
recurenei este:
tn =Cil"+C22"
Avem nevoie de dou condiii iniiale. tim c t0 = 0; pentru a gsi cea
de-a doua condiie calculm
h =2t0-|-l
Din condiiile iniiale, obinem tn = 2" 1.
Observaie: dac ne intereseaz doar ordinul lui tn, nu este necesar s cal
culm efectiv constantele n soluia general. Dac tim c tn = C\ 1" + C22",
rezult c tn G 0(2"). Din faptul c numrul de mutri a unor discuri nu poate
fi negativ sau constant (deoarece avem n mod evident tn > n), deducem c
C2 > 0. Avem atunci tn G fi(2") i deci tn G 0(2"). Putem obine chiar ceva
mai mult.
Substituind soluia general napoi n recurena originar, gsim
1 = tn - 2tn-l = Ci + C22" - 2(ci + C22""1) = -Ci
Indiferent de condiia iniial, C\ este -1.
30

9.4. ANALIZA ALGORITMILOR RECURSIVI


9.4.5

Schimbarea variabilei

Uneori putem rezolva recurene mai complicate printr-o schimbare de vari


abil, n exemplele care urmeaz, vom nota cu T(n) termenul general al re
curenei i cu tk termenul noii recurene obinute printr-o schimbare de vari
abil. Presupunem pentru nceput c n este o putere a lui 2.
Un prim exemplu este recurena
T(n) =4T(f) + n,n > 1
n care nlocuim pe n cu 2k , notm tk = T(2k) = T(n) i obinem:
tk = 4t*_i + 2k
Ecuaia caracteristic a acestei recurene liniare este (conform paragrafului
anterior):
(x-4)(x-2) =0
i deci tk = Ci4k + c<i2k . nlocuim pe k cu Ig n
T(n) = cin2 + c2n
Rezult c T(n) O (n2 \ n este o putere a lui 2).
Un al doilea exemplu l reprezint ecuaia
T(n) = 4T(%)+n2,n> 1
Procednd la fel, ajungem la recurena
tk = 4tk-i + 4k
cu ecuaia caracteristic
(x - 4)2 = 0
i soluia general tk = C\4k + Cik4k . Atunci,
T(n) = cin2 + cin2 Ign
i obinem c T(n) O (n2 logn \ n este o putere a lui 2).
n fine, s considerm i exemplul
31

9.4. ANALIZA ALGORITMILOR RECURSIVI


T{n) = 3T(f) +cn,n > 1
c fiind o constant. Obinem succesiv
T(2k) = 3T(2fc"1) + c2k
tk = 3tfe_i + c2k
cu ecuaia caracteristic
(x-3)(x-2) =0
tk = c13k+c22k
T(n) = d3l9n + c2n
i, deoarece
alg b frig a
obinem
T(n) = dnl93 +c2n
deci, T(n) G O (n's 3 | n este o putere a lui 2).
n toate aceste exemple am folosit notaia asimptotic condiionat. Pentru
a arta c rezultatele obinute sunt adevrate pentru orice n, este suficient s
adugm condiia ca T(n) s fie cresctoare pentru n > noPutem enuna acum o proprietate care este util ca reet pentru analiza algo
ritmilor cu recursiviti de forma celor din exemplele precedente. Proprietatea,
a crei demonstrare o lsm ca exerciiu, este foarte util la analiza algoritmilor
Divide et Impera.
Propoziie. Fie T : N > SR+ o funcie nedescresctoare
T(n) = aT(f ) +cnk,n>n0
unde rio>l,&>2ifc>0 sunt ntregi, a i c sunt numere reale pozitive, iar

no este o rputere a lui b. Atunci avem:


!Q(nk)
@(nklogn)
Q(nloga)
32

dac
dac
dac

a <bk
a = bk
a > bk

9.5. IMPLEMENTAREA ALGORITMILOR


9.5

Implementarea algoritmilor

Am considerat util prezena la sfritul capitolului a implementrilor Java


pentru problemele prezentate pe parcursul acestui capitol. Este cazul algorit
milor de sortare prin selecie, a celui de sortare prin inserie i a algoritmului
turnurilor din Hanoi.
Listing 9.2: Implementarea algoritmului de sortare prin selecia minimului
1 import j ava . io . * ;
2 import io . Reader ;
3
4 /* *
5 * Sortare prin selecia minimului .
6 */
7 public class SortareSelMin
f
9
io /** Ordonarea prin selecia minimului.*/
n public static void ordonare ( int [] a)
12 {
13
for (int i = 0; i < a.length 1; i ++)
1
15
int pozMin = i ;
16
17
for (int j = i + 1; j < a.length; j ++)
18
{
19
if ( a [ j ] < a [pozMin ] )
20
{
21
pozMin = j ;
22
}
}
24
25
int aux = a [ i ] ;
26
a [ i ] = a [pozMin ] ;
27
a [ pozMin ] = aux ;
28
}
29 }
30
31 /** Programul principal.*/
32 public static void main(String [] args )
33 {
34
//citirea elementelor irului
35
System . out . p ri n 1 1 n (" I ntro du c e ti elementele irului " +
36
" (pe aceeai linie, separate prin spaiu):");
37
int[] s = Reader. readlntArray ();
38
39
//ordonarea irului s
40
ordonare ( s ) ;
33

9.5. IMPLEMENTAREA ALGORITMILOR


41
42
43
44
46
48
49 }
50 }

//afiare rezultate
Sy stern . out . p ri n t (" S i rul ordonat este: ");
for (int i = 0; i < s.length; i++)
{
System . out . prin ( s [ i ] + " ");
)
System, out. println() ;

Listing 9.3: Implementarea algoritmului de sortare prin inserie


1 import java . io . * ;
2 import io . Reader ;
4 /* *
5 * Sortare prin inserie.
6 */
7 public elass S o rt a r el n s e rt i e
(
9
io /* * Ordonarea prin inserie . */
n public static void ordonare(int [] a)
i
13
for (int i = 1; i < a.length; i++)
{
15
int tmp = a [ i ] ;
16
int j = i ;
17
s
for (; (j > 0) && (tmp < a[j - 1]); j
{
20
a[j ] = a[j - 1 ] ;
}
22
23
a [ j ] = tmp ;
i

26
27 /** Programul principal.*/
28 public static void main (String[] args)
29 j
30
//citirea elementelor irului
31
System, out. println ("Introducei elementele irului" +
32
" (pe aceeai linie, separate prin spaiu):");
33
int [] s = Reader . readlntArray ();
34
35
//ordonarea irului s
36
ordonare ( s ) ;
37
34

9.5. IMPLEMENTAREA ALGORITMILOR


38
39
40
42
44
)
46 }

//afiare rezultate
System . out . p ri nt (" S i ru 1 ordonat este: ");
for ( int i = 0; i < s . length ; i ++)
{
Sy stern . out . p ri n t ( s [ i ] + " ");
}
System . out . p ri n tl n ( ) ;

Listing 9.4: Implementarea problemei turnurilor din Hanoi


1 import j ava . io . * ;
2 import io . Reader ;
3
4 /* *
5 * Turnurile din Hanoi.
6 */
7 public class Hanoi
f
9 /** Implementarea algoritmului "Turnurile din Hanoi".*/
10 public static void hanoi(int n, int i , int j)
" {
12
if (n > 0)
1
14
hanoi(n 1, i, 6 i j);
15
System. out. println(i + "
>" + j);
16
hanoi(n 1,6 i j,j);
}
.8 )
19
20 /* * Programul principal . */
21 public static void main ( String [] args )
22 {
23
//citirea numrului de discuri
24
System . out. prin ( " Introducei numrul de discuri: ");
25
26
int n = Reader. readlnt();
27
28
// apelul metodei hanoi
29
hanoi ( n , 1 , 2 ) ;
30 }
31 }

Rezumat
Capitolul de fa a realizat o scurt introducere n domeniul vast al analizei
algoritmilor. Ideea cea mai important care reiese de aici este c algoritmii
35

9.5. IMPLEMENTAREA ALGORITMILOR


utilizai afecteaz timpul de execuie al unui program mult mai drastic dect
artificiile de programare. Algoritmii care au un timp de lucru exponenial nu
sunt n general aplicabili pentru date de intrare de dimensiuni rezonabile, spre
deosebire de cei liniari sau ptratici al cror timp de execuie nu crete att de
drastic odat cu dimensiunea datelor de intrare.
Capitolul urmtor prezint cele mai importante structuri de date, mpreun
cu gradul lor de eficien i cu operaiile pe care le permit. De asemenea, sunt
prezentate diverse situaii n care pot fi folosite structurile de date. Restul capi
tolelor sunt dedicate prezentrii algoritmilor fundamentali.

Noiuni fundamentale
0(f): notaie utilizat pentru a determina termenul dominant al funciei f .
Cu ajutorul ei se limiteaz superior timpul de execuie al unui algoritm.
fi(/): notaie utilizat pentru a limita inferior timpul de execuie al unui
algoritm.
0(/): notaie utilizat pentru a arta c timpul de execuie al unui algoritm
este limitat att inferior ct i superior de cte un multiplu real al aceleai funcii
/algoritm liniar: algoritm care are timpul de execuie 0(n).
algoritm exponenial: algoritm al crui timp de execuie crete exponenial
n raport cu dimensiunea datelor de intrare (0(an)).
algoritm polinomial: algoritm al crui timp de execuie este mrginit su
perior de un polinom (0(nk)).
ecuaie caracteristic: ecuaie polinomial ataat recurenelor liniare, prin
a crei rezolvare se gsete soluia recurenei.
inducia constructiv: o variant a induciei matematice care permite de
monstrarea unei aseriuni parial sau incomplet enunate precum i descoperirea
specificaiilor care lipsesc din aseriunea respectiv.
metoda iteraiei: metod simpl de rezolvare a recurenelor liniare n care
se execut primii pai ai recurenei dup care se intuiete forma general care
se demonstreaz prin inducie.
recuren liniar: formul de recuren n care termenul al n-lea este ex
primat ca o combinaie liniar a termenilor precedeni.
schimbarea variabilei: tehnic utilizat pentru a reduce anumite formule
de recuren la o form liniar.
36

9.5. IMPLEMENTAREA ALGORITMILOR


Erori frecvente
1 . Pentru cicluri iterative imbricate, timpul total de execuie este produsul
dimensiunilor ciclurilor, n timp ce n cazul ciclurilor iterative consecu
tive, afirmaia nu este adevrat. De exemplu, pentru dou cicluri imbri
cate care se execut fiecare de la 1 la n2, timpul total de execuie este
0(n4).
2. Nu scriei expresii de tipul 0(2n2) sau 0(n2 + n). n cele mai multe
situaii, este necesar doar termenul dominant, fr constanta care l pre
cede, n consecin notaia utilizat pentru ambele cazuri de mai nainte
este 0(n2).
3. Pentru a exprima limita inferioar a complexitii unui algoritm, utilizai
notaia fi, nu O.
4. Baza n care se scrie logaritmul este irelevant pentru notaiile asimpto
tice. Astfel, O (lg n) este egal cu O (In n) deoarece ele difer doar printr-o
constant multiplicativ.

Exerciii
1 . Care din urmtoarele afirmaii sunt adevrate?
(a) n2 G 0(n3)
(b) n3 G 0(n2)
(c) 2"+1 G 0(2")
(d) (n+1)! G 0(n!)
(e) pentru orice funcie / : N -> R*, / 0(n) => [f2 G 0{n2)}
(f) pentru orice funcie / : N -> R*, / G 0(n) => [2/ G 0(2")]
2. Demonstrai c relaia " G O" este tranzitiv: dac / G 0(<?) i g G 0(/i),
atunci / G 0(h). Deducei de aici c dac g G 0(/i), atunci O (5) C
0(/i).
3. Gsii dou funcii /, g : N -> R*, astfel nct f 0(g) i g 0(f).
Soluie: f(n) = n,g(n) = n1+sinn.
37

9.5. IMPLEMENTAREA ALGORITMILOR


4. Pentru oricare dou funcii /, g : N R* definim urmtoarea relaie bi
nar: / < g dac O(f) C 0(g). Demonstrai c relaia "<" este o relaie
de ordine parial n mulimea funciilor definite pe N i cu valori n R* .
Indicaie: Trebuie artat c relaia este reflexiv, tranzitiv i antisimetric. inei cont de exerciiul 3.
5. Pentru oricare dou funcii f,g:N R*demonstrai c 0(f + g) =
0(max(f, g)) unde suma i maximul se iau punctual.
6. Fie f(n) = amnm + . . . + a\n + do un polinom de grad m, cu am > 0.
Artai c / e 0(nm).
7. Considerm afirmaia 0(n2) = 0(n3 + (n2 n3)) = 0(max(n3 ,n2
n3)) = 0(n3). Unde este eroarea?
8. Considerm afirmaia Yh=i i = 1 + 2 + . . . + n e 0(1 +2 + . . . +n) =
0(max(l + 2 + . . . + n)) = 0(n). Unde este eroarea?
9. Pentru oricare dou funcii /, g : N R* demonstrai c 0(/) + (<?) =
Q(f+g) = Q(max(f,g)) = max (@(/), @ (<?)), unde suma i maximul
se iau punctual.
10. Analizai eficiena urmtorilor algoritmi:
(a) pentru i=l,n
pentru 1=1,5
{operaie elementar}
(b) pentru i=l,n
pentru j=l,i+l
{operaie elementar}
(c) pentru i=l,n
pentru j=l,6
pentru k=l,n
{operaie elementar}
(d) pentru i=l,n
pentru j=l,i
pentru k=l,n
{operaie elementar}
1 1 . Construii un algoritm cu timpul n 0(n log n).
38

9.5. IMPLEMENTAREA ALGORITMILOR


12. Fie un algoritm:
pentru i=0,n
j<-i
ct timp jOO
j<-j div 2
Gsii ordinul exact al timpului de execuie.
13. Rezolvai urmtoarea recuren: tn 3i_i 4i_2 = 0,n > 2 cu
t0 =0,h = 1.
14. Care este timpul de execuie pentru un algoritm recursiv cu recurena:
tn = 2t_i + n?
Indicaie: Se ajunge la ecuaia caracteristic (x 2)(x l)2 = 0, /ar
soluia general este tn = C\ln +C2I" +C3nl". Rezult ctn 0(2").
Substituind soluia general n recuren, obinem c, indiferent de con
diia iniial, C2 = 2 C3 = 1. Atunci, toate soluiile interesante
ale recurenei trebuie s aib C\ > 0 i ele sunt toate n fi(2"), deci n
6(2").
15. S se calculeze secvena de sum maxim, format din termeni consecu
tivi, ai unui ir de numere.

39

10. Structuri de date

Nu poi obine ntotdeauna ceea ce


doreti, dar, dac ncerci, uneori
vei obine ceea ce ai nevoie.
Autor anonim
Dou programe care rezolv aceeai problem pot s arate complet diferit.
Unul poate fi extrem de lizibil, concis i uor de modificat pentru a se adapta
la rezolvarea unor probleme asemntoare, iar cellalt poate fi impenetrabil,
obscur, interminabil i dificil de modificat. Cele dou programe pot s difere
att de mult n ceea ce privete durata de execuie i necesarul de memorie nct,
pentru un anumit set de intrare, unul furnizeaz rspunsul dup 2 secunde, iar
cellalt dup cteva secole!
Experiena a demonstrat c aceste diferene sunt generate de structura pro
gramului i de structura datelor.
Programele sunt scrise pentru a rezolva probleme reale. Structurarea pro
gramului mparte problema i soluia ei n componente mai simple i mai uor
de neles. Informaia care trebuie procesat este reinut n structuri de date
(tablouri, nregistrri, liste, stive, arbori, fiiere etc). O structur de date gru
peaz datele. O structur de date aleas adecvat poate face operaiile simple i
eficiente, iar una aleas neadecvat poate face operaiile alambicate i ineficiente.
Structurile de date conin informaie asupra creia se opereaz n timpul ex
ecuiei unui program. Despre programe obinuim s spunem c proceseaz in
formaie, cnd, de fapt, ele proceseaz structuri de date. Astfel, nu mai pare sur
prinztor faptul c structura datelor i a programelor sunt extrem de importante
i c ele trebuie corelate corespunztor pentru a programa cu succes. Asimi
larea cunotinelor legate de structuri de date i algoritmi, alturi de stpnirea
conceptelor fundamentale ale programrii orientate pe obiecte, i permit pro
gramatorului s menin o supremaie regal asupra programelor, astfel nct
40

Figura 10.1: nchiderea datelor ntr-o cutie neagra. Datele pot accesate doar
prin invocarea unei operatii permise.

acestea sa ramna lizibile, usor de ntretinut si eciente, chiar si atunci cnd


cresc n dimensiune.
Att programele ct si datele au o anumita structurare si att structura pro
gramului ct si a datelor trebuie sa e adecvata problemei de rezolvat. Vom
urmari sa prezentam structurile de date nu ca un subiect teoretic izolat, ci ca pe
un instrument esential al procesului de rezolvare a problemelor care conduce la
crearea de programe eciente.
Structurile de date sunt importante, deoarece modul n care programatorul
alege sa reprezinte datele afecteaza n mod semnicativ claritatea, concizia,
viteza de executie si necesarul de memorie al programului. n acest capitol, ct
si n cele care urmeaza, vom prezenta cum sa folosim diferite structuri de date
pentru a crea programe corecte si eciente. Vom vedea ca operatiile care tre
buie realizate asupra datelor sunt cele care determina care este cea mai potrivita
structura de date care trebuie folosita.
Dezvoltarea de programe este dicila, mai ales atunci cnd este facuta fara
nici un fel de strategie. Progamarea orientata pe obiecte, prezentata n prima
parte a lucrarii, usureaza mult dezvoltarea programelor si conduce la programe
bine structurate. Ea consta n mpartirea atenta a unei probleme complexe n
componente mai simple a caror interfata este apoi cu usurinta combinata pentru
a rezolva problema initiala.
peconsta
saleAbstractizarea
esentiale, ignornd
datelor
ct senpoate
a trata
detaliile.
o colectie
Abstractizarea
de date extragndaspectele
datelor reduce
datele la o colectie si la operatiile care se pot realiza asupra acestei colectii.
Efectul este ca si cum colectia de date ar nchisa ntr-o cutie neagra (black
box) impenetrabila, singurul mod de a accesa datele ind invocarea uneia sau
mai multor operatii permise (Figura 10.1).
41

Modul n care datele sunt aezate n acea cutie neagr i modul n care ope
raiile se execut devin detalii irelevante1. Astfel de detalii determin eficiena
programului, dar nu i afecteaz structura logic.
Abstractizarea datelor trebuie privit mai degrab ca o facilitate care uureaz
efortul de programare, i nu ca o nou constrngere n privina stilului de pro
gramare. Pentru a nelege mai bine aceste noiuni, s vedem cum poate fi
definit simplificat o stiv ca tip abstract de date. Stiva este o colecie de date
omogene (de acelai tip) asupra creia se pot realiza urmtoarele operaii:
PUSH(X) - are ca efect depunerea lui X pe vrful stivei;
POP(X) - are ca efect ncrcarea valorii din vrful stivei n parametrul X
i eliminarea vrfului stivei.
Modul n care cele dou operaii (PUSH i POP) sunt implementate i modul
n care datele sunt reinute n stiv (static, nlnuit etc.) nu trebuie s transpar
utilizatorului; pe el pur i simplu nu trebuie s l intereseze acest lucru. Este
exact ca i curentul electric: atunci cnd acionm comutatorul de la veioz,
tim c becul se va aprinde; care este procesul prin care filamentul becului se
ncinge i emite lumin nu ne privete. Tot astfel, i listele, cozile, arborii binari
mpreun cu operaiile care se fac asupra lor pot fi privite ca tipuri abstracte de
date.
Unui tip abstract de date mulime putem s-i asociem operaii cum ar fi
reuniune, intersecie, dimensiune, complementar. ntr-o alt situaie, putem
avea nevoie numai de operatorii de reuniune i apartenen, care definesc un alt
tip abstract de date (TAD), care poate s aib o cu totul alt organizare intern,
deoarece, aa cum am spus, operaiile sunt cele care definesc TAD-ul.
n consecin, rolul acestui capitol este de a prezenta modul n care se con
struiesc aceste tipuri abstracte de date (sau structuri de date) pentru a le putea
utiliza apoi n crearea de programe robuste, lizibile i eficiente. Ideea de baz
este c implementarea operaiilor unui TAD se realizeaz o singur dat n
cadrul unei aplicaii, i orice alt parte a aplicaiei va utiliza structura de date
prin itermediul interfeei pe care aceasta o expune.
Dac din diverse motive anumite detalii de implementare trebuie schimbate,
acest lucru se va face uor prin modificarea rutinelor care realizeaz operai
ile din TAD. Aceste schimbri nu vor afecta n nici un fel restul programului,
deoarece interfaa se menine neschimbat. n exemplul nostru cu stiva, dac
decidem s trecem de la alocarea secvenial (static) a elementelor stivei la
1 Irelevante pentru cel care utilizeaz structura de date. Noi ne vom ocupa n acest capitol tocmai
de modul de construcie al acestei cutii negre, pentru a o putea utiliza apoi ori de cte ori este
necesar.
42

10.1. CUM IMPLEMENTAM STRUCTURILE DE DATE?

alocarea nlantuita (dinamica), implementarea operatiilor PUSH si POP va tre


bui n mod cert schimbata; aceasta schimbare nu va afecta n nici un fel restul
programului, care va utiliza operatiile PUSH si POP expuse de stiva fara a sesiza ca implementarea acestora este diferita.
Acest capitol prezinta sase dintre cele mai cunoscute structuri de date: stive,
cozi, liste nlantuite, arbori binari de cautare, tabele de repartizare (engl. hashtables) si cozi de prioritate. Scopuleste acela de a deniecare structurade date
si de a furniza o estimare intuitiva pentru complexitatea operatiilor de inserare,
stergere si acces. Implementarea operatiilor va facuta pentru ecare structura
de date separat.
n acest capitol vom vedea:
Descrierea structurilor de date uzuale, operatiile permise pe ele si timpii
lor de executie;
Pentru ecare structura de date, vom deni o interfata Java continnd
protocolul care trebuie implementat;
Programele complete pentru implementarea acestor structuri.
Scopul este acela de a arata ca specicarea, care descrie functionalitatea, este
independenta de implementare. Nu trebuie sa stim cum este implementat un
anumit lucru, atta timp ct stim ca este implementat.

10.1

Cum implementam structurile de date?

Am aratat deja ca structurile de date ne permitatingerea unuiscop important


programarea
n
orientata pe obiecte: reutilizarea componentelor. Asa cum vom
vedea mai trziu n acest capitol, structurile de date descrise sunt folosite n
multe situatii. Odata ce o structura de date a fost implementata, ea poate
folosita din nou si din nou n aplicatii de natura diversa2.
Aceasta abordare - separarea interfetei de implementare - este o parte fun
damentala a orientarii pe obiecte. Cel care foloseste structura de date nu trebuie
sa vada implementarea ei, ci doar operatiile admisibile. Aceasta tine de partea
de ascundere a informatiei din programarea orientata pe obiecte. O alta parte
importanta a programarii orientate pe obiecte este abstractizarea. Trebuie sa
proiectam cu grija structura de date, deoarece vom scrie programe care folosesc
aceste structuri de date fara sa aiba acces la implementarea lor. Aceasta va face
2De altfel, bibliotecile Java cuprind majoritatea structurilor de date uzuale prezentate n acest
capitol: Stack, Queue, Hashtable etc.

43

10.1. CUM IMPLEMENTM STRUCTURILE DE DATE?


Listing 10.1: Interfaa IMemoryCell pentru clasa MemoryCell
i/** Interfaa abstracta pentru o celula de memorie care
2 * stocheaz un obiect de tip arbitrar */
a public interface IMemoryCell
4{
5 /** ntoarce elementul stocat in cadrul celulei*/
6 Obj ect read ( ) ;
7 /** Scrie obiectul x in celula*/
8 void write(Object x);
)

n schimb ca interfaa s fie mai curat, mai flexibil, i, de obicei, mai uor de
implementat.
Toate structurile de date sunt uor de implementat dac nu ne punem proble
ma eficienei. Acest lucru permite s adugm componente "ieftine" n program
doar pentru depanare. Putem apoi nlocui aceste implementri "ieftine" cu im
plementri care au o performan (n timp i/sau n spaiu) mai bun i care sunt
adecvate pentru procesarea unei cantiti mai mari de informaie. Deoarece
interfeele sunt fixate, aceste nlocuiri nu necesit practic nici o modificare n
programele care folosesc aceste structuri de date.
Vom descrie structurile de date prin intermediul interfeelor. De exem
plu, stiva este precizat prin intermediul interfeei Stack. Clasa care imple
menteaz aceast interfa va implementa toate metodele specificate n Stack,
la care se mai pot aduga anumite funcionaliti.
Ca un exemplu, n Listing 10.1 este descris o interfa pentru clasa Memory
Cell, utilizat n primul volum, la capitolul Motenire, seciunea Implementa
rea de componente generice. Interfaa descrie funciile disponibile; clasa con
cret trebuie s defineasc aceste funcii. Implementarea interfeei este prezen
tat n Listing 10.2.
Este important de reinut faptul c structurile de date definite n acest capitol
stocheaz referine ctre elementele inserate, i nu copii ale elementelor. Am
ales aceast variant deoarece este bine ca n structura de date s fie plasate
obiecte nemodificabile (cum ar fi String, Integer, etc.) pentru ca un uti
lizator extern s nu poat s schimbe starea unui obiect care este nglobat ntr-o
structur de date.
Fiecare dintre structurile prezentate este implementat complet, mpreun
cu un program de test pentru a verifica corectitudinea implementrii structurilor
de date. Pentru o mai bun organizare, clasele utilizate n aceste programe sunt
mprite pe pachete, ceea ce nseamn c fiecare fiier surs este salvat ntr-un
44

10.2. STIVE

Listing 10.2: Implementarea clasei MemoryCell


Clasa concreta care implementeaza interfata MemCell
public class MemoryCell implements MemCell
3 {
/
4
/ Atribut care indica obiectul stocat
private Object storedValue ;
5
1/

public Object read()


{
return storedValue ;
}

7
8
9
10
11
12

public void write (Object x)

13

{
storedValue = x;

14

15
16

director corespunzator numelui pachetului. Pentru o mai buna ntelegere, con


sideram directorulde lucru c:\javawork(pentru utilizatoriiWindows). Pen
tru ca programele sa functioneze, acest director trebuie adaugat la variabila sis
tem CLASSPATH. n acest director se vor crea directoarele corespunzatoare pa
chetelor existente (n cazul nostru, este vorba de pachetele datastructures
pentru clasele specice structurilor de date si exceptions pentru clasele de
exceptii), precum si sierele sursa ale aplicatiilor care folosesc aceste struc
turi de date (clasele corespunzatoare acestor aplicatii de test nu sunt incluse n
pachete).
Unele structuri de date folosesc clase n comun cu alte structuri de date. Din
acest motiv, implementarile claselor respective nu sunt reluate separat pentru
ecare structura de date.

10.2

Stive

O stiva este o structura de date n care orice tip de acces este permis doar
asupra ultimului element inserat. Comportamentulunei stive este foarte asema
nator cu cel al unei gramezi de farfurii. Ultima farfurie adaugata va plasata
n vrf ind, n consecinta, usor de accesat, n timp ce farfuriile puse cu mai
mult timp n urma (aate sub alte farfurii) vor mai greu de accesat, putnd
periclita stabilitatea ntregii gramezi. Astfel, stiva este adecvata n situatiile n
care avem nevoie sa accesam doar elementul din vrf. Toate celelalte elemente
45

10.2. STIVE

Figura 10.2: Inserarea n stiv se face prin push ( ) , accesul prin top ( ) , iar
tergerea prin pop ( ) .
push
pop,top

Stiva

Listing 10.3: Clasa de excepii Underf lowException


i package exceptions ;
2
3 public elass UnderflowException extends Exception
4!
5 public UnderflowException ()
" !
7
super ( ) ;
" }
io public UnderflowException ( String msg)
!
12
super(msg);
,3 }
,4 }

sunt inaccesibile.
Cele trei operaii naturale de inserare, tergere i cutare, sunt denumite
n cazul unei stive, push, pop i top. Cele trei operaii sunt ilustrate n
Figura 10.2, iar o interfa Java pentru o stiv abstract este prezentat n List
ing 10.4. Interfaa declar i o metod topAndPop ( ) care combin dou
operaii, consultare i extragere. n cazul nostru, metodele pop ( ) , top ( ) i
topAndPop ( ) pot arunca o excepie UnderflowException n cazul n
care se ncearc accesarea unui element cnd stiva este goal. Aceast excepie
va trebui s fie prins pn la urm de o metod apelant. Clasa Underflow
Exception este definit n Listing 10.3, i ea este practic identic cu clasa
Exception. Important pentru noi este c difer tipul, ceea ce ne permite s
prindem doar aceast excepie cu o instruciune try-catch .
Listing 10.6 prezint un exemplu de utilizare a stivei n care se folosete o
clas S t a c kAr care reine elementele stivei ntr-un ir (array) . Clasa S t a c kAr
46

10.2. STIVE

Listing 10.4: Interfata pentru o stiva


1

package d a t a s t ruc t ur e s ;

import exceptions .;
Interfata pentru o stiva. Stiva expune metode pentru
manipularea ( adaugarea , stergerea , consultarea )
5
6
elementului din va rfu l ei/
7 public interface Stack
8 {
public void push(Object x);
9
3

4/

10

public void pop() throws UnderflowException ;

11
12

public Object top() throws UnderflowException;

13
14

public Object topAndPop() throws UnderflowException;

15
16

public boolean isEmpty();

17
18

public void makeEmpty();

19
20

este o modalitate de implementare a interfetei Stack. StackAr foloseste


un sir pentru a retine elementele din stiva. Mai exista si alte modalitati de
implementare a unei stive. Una dintre acestea se bazeaza pe liste si imple
mentarea ei este propusa n exercitiul 1. Listing 10.5 prezinta codul sursa al
clasei StackAr.
Analiznd programul de test din Listing 10.6 se observa ca stiva poate
folosita pentru a inversa ordinea elementelor. De remarcat un mic articiu
folosit aici: iesirea din ciclul for de la liniile 21-24 se realizeaza n momentul
n care stiva se goleste si metoda topAndPop() arunca UnderflowExcep
tion care este prinsa la linia 26. Am recurs la acest articiu pentru a ntelege
mecanismul exceptiilor. Folosirea acestui articiu n mod curent nu este reco
mandata, deoarece reduce lizibilitatea codului.
Fiecare operatie pe stiva trebuie sa ia o cantitate constanta de timp indife
rent de dimensiunea stivei, la fel cum accesarea farfuriei din vrful gramezii
este rapida, indiferent de numarul de farfurii din teanc. Accesul la un element
oarecare din stiva nu este ecient, de aceea el nici nu este permis de catre inter
fata noastra.
Stiva este deosebit de utila deoarece sunt multe aplicatii pentru care trebuie
sa accesam doar ultimul element inserat. Un exemplu ilustrativ este salvarea
parametrilor si variabilelor locale n cazul apelului unei alte subrutine.
47

10.2. STIVE
Implementarea interfeei Stack este realizat folosind irul de obiecte elements (linia 8 n clasa StackAr). De aici i numele de stiv bazat pe
iruri. Practic, elementele stivei sunt pstrate ntr-un ir de obiecte, pe care
se realizeaz operaiile expuse de interfa: top ( ) , pop ( ) , push ( ) etc.
Stiva are o capacitate iniial de elemente (DEFAULT_CAPACITY). Dac pe
msur ce se fac adugri n stiv, aceast capacitate este atins, atunci stiva
i mrete capacitatea cu un numr precizat de elemente (n cadrul metodei
increaseStackSize ( )). Operaia este ns ascuns privirilor utilizatoru
lui, pentru c stiva i mrete dimensiunea fr ca utilizatorul s precizeze sau
s aib cunotin de acest lucru. Nivelul de "umplere" al stivei este specifi
cat prin intermediul variabilei topPosition, care indic i poziia vrfului
stivei.
Listing 10.5: Implementarea unei structuri de tip stiv folosind iruri
i package d at a s t r uc t u re s ;
2
3 import exceptions .*;
4/** Implementarea unei stive folosind un sir */
5 public elass StackAr implements Stack
6!
7 /** Sir care retine elementele stivei*/
8 private Object [] elements ;
9 /** Dimensiunea implicita a stivei*/
10 private static final int DEFAULTCAPACITY = 10;
11 /** Poziia vrfului stivei */
12 private int topPosition;
13
14 /** Constructor care aloca memorie pentru elements si
s * iniializeaz vrful stivei */
i6 public StackAr ( )
" 1
is
elements = new Obj ect [DEFAULT_CAPACITY ] ;
19
topPosition = 1;
20 }
21
22 /* * Adaug elementul x in stiva . */
23 public void push(Object x)
24 j
25
if (topPosition == elements . length 1)
{
27
increaseStackSize ();
}
29
30
elements [++topPosition ] = x;
31 )
32
33 /* * Mrete dimensiunea ( capacitatea ) stivei cu
48

* DEFAULT_CAPACITY atunci cand stiva este plina . */


private void i ncre as e S tackS i ze ()
{
Object[] newStack = new Object [ elements . length +
DEFAULTCAPACITY ] ;
//copiem elementele in noua stiva
for ( int i = 0; i < elements .length ; i ++)
{
newStack[i] = elements [ i ] ;
)
//elements devine noua stiva
elements = newStack;
}
/** Extrage elementul din vrful stivei. */
public void pop() throws UnderflowException
{
if ( isEmpty ( ) )
{
throw new UnderflowException (" Stiva vida.");
)
topPosition

}
/** Returneaza elementul din vrful stivei
* (ultimul adugat).*/
public Object top() throws UnderflowException
{
if ( isEmpty ( ) )
{
throw new UnderflowException (" Stiva vida.");
)
return elements [ topPosition ] ;
}
/** Returneaza elementul din vrful stivei si
* il elimina apoi din stiva.*/
public Object topAndPop () throws UnderflowException
{
if ( isEmpty ( ) )
{
throw new UnderflowException (" Stiva vida.");
)
return elements [ topPosition

];

10.2. STIVE
84
85
86
B7
88
89
90
91
92
93
94
*
96 }

/* * Verifica daca stiva e vida . */


public boolean isEmpty ()
j
return topPosition == 1;
}
/** Elimina toate elementele din stiva. */
public void makeEmpty ( )
{
topPosition = 1;
}

Listing 10.6: Exemplu de utilizare a stivei. Programul va afia: Coninutul


stivei este 4 3 2 1 0
1 import d at a s t ru c t ure s . * ;
2 import exceptions .*;
3/** Clasa de test simpla pentru o stiva, care adaug 5 numere
4* dupa care le extrage in ordine inversa */
5 public elass TestStack
oI
7 public static void main ( String [ ] args)
* !
9
Stack s = new StackAr();
10
11
//introducem elemente in stiva
12
for ( int i = 0; i < 5; i++)
{
14
s.push(new Integer(i));
}
16
17
// scoatem elementele din stiva si le afi sam
s
System, out. prin (" Coninutul stivei este: ");
19
try
20
{
21
for ( ; ; )
22
{
23
System . out . prin (s . topAndPop ( ) + " " ) ;
i
}
26
catch ( UnderflowException ue)
{
28
}
29 }
30 }
50

10.3. COZI

Figura 10.3: Modelul unei cozi: adugarea la coad se face prin enqueue ( ) ,
accesul prin getFr ont ( ) , tergerea prin dequeue ( ) .
enqueue

dequeue, getFront

Queue

10.3

Cozi

O alt structur simpl de date este coada. n multe situaii este important
s avem acces i/sau s tergem ultimul element inserat. Dar, ntr-un numr la
fel de mare de situaii, acest lucru nu numai c nu mai este important, este chiar
nedorit. De exemplu, ntr-o reea de calculatoare care au acces la o singur im
primant este normal ca dac n coada de ateptare se afl mai multe documente
spre a fi tiprite, prioritatea s i fie acordat documentului cel mai vechi. Acest
lucru nu numai c este corect, dar este i necesar pentru a garanta c documen
tul nu ateapt la infinit. Astfel, pe sistemele mari este normal s se foloseasc
cozi de tiprire.
Operaiile fundamentale suportate de cozi sunt:
enqueue - inserarea unui element la captul cozii;
dequeue - tergerea primului element din coad;
getFront - accesul la primul element din coad.
Figura 10.3 ilustreaz operaiile pe o coad. Tradiional, metodele de
queue ( ) i getFront ( ) sunt combinate ntr-una singur. Prima metod
returneaz primul element, dup care l scoate din coad, n timp ce cea de-a
doua returneaz primul element fr a-1 scoate din coad.
Implementarea structurii de coad este asemntoare pn la un punct cu
cea a stivei. Coada pstreaz elementele ntr-un ir de obiecte cu o capacitate
iniial, iar dac prin adugri repetate aceast capacitate este atins, atunci ea
va fi mrit automat. Spre deosebire de stiv, coada are doi indici care indic
poziiile de nceput i de sfrit ale cozii.
Listing 10.7 ilustreaz interfaa pentru o coad, n timp ce Listing 10.8 pre
zint o implementare a interfeei anterioare, bazat pe iruri. Spre deosebire de
51

10.3. COZI
stiv, n care adugarea i extragerea unui element au loc la acelai capt, n
cazul cozii adugarea are loc la final, dar tergerea se face de la nceput. Astfel,
pentru a putea utiliza irul elements la ntreaga lui capacitate, elementele lui
sunt privite "circular", ca i cnd ultimul element ar fi legat de primul.
Listing 10.7: Interfa pentru coad
i package d at a s t r uc t u re s ;
2
3 import exceptions .*;
4/** Interfaa pentru o coada. Expune metode pentru adugarea
5* unui element , tergerea primului element , consultarea
6 * primului element */
7 public interface Queue
!
9 public void enqueue ( Obj ect x);
10
11 public Object getFront() throws UnderflowException ;
12
13 public Object dequeue() throws UnderflowException;
14
15 public boolean isEmpty();
16
17 public void makeEmpty ( ) ;
,8 }
Listing 10.8: Implementarea unei structuri de tip coad folosind iruri
1 package d at a s t r uc t u re s ;
2
3 import exceptions .*;
4/** Implementarea unei cozi folosind un tablou. */
5 public class QueueAr implements Queue
6!
7 /** Tablou care retine elementele din coada*/
8 private Object [] elements ;
9 /** Indicele primului element*/
10 private int front ;
11 /** Indicele ultimului element*/
12 private int back ;
13 /** Dimensiunea cozii */
14 private int currentSize ;
15 /** Numrul de elemente alocat iniial pentru coada*/
16 private final static int DEFAULT_CAPACITY = 10;
L7
s /* * C onstructo r care aloca memorie pentru elementele cozii
19 * si seteaz valorile atributelor*/
20 public QueueAr ( )
1
22
elements = new Object [DEFAULT_CAPACTTY ] ;
52

10.3. COZI
23
24
25
26
27
28
29
30
31
32
33
34
35
37
38
39
40
41
42
43
44
45
46
47
4s
49
50

52
53
54
55
56
57
59
60
61
62
63
64
65
66
67
68
69
70
71
^

makeEmpty ( ) ;
}
/* * ntoarce true daca este vida . */
public boolean isEmpty ()
{
return currentSize == 0;
}
/** Adaug elementul x in coada. */
public void enqueue(Object x)
{
if (currentSize == elements.length)
!
increaseQueueSize();
}
back = increment ( back ) ;
elements [ back ] = x;
currentSize ++;
}
/** Elimina toate elementele din coada. */
public void makeEmpty ()
{
currentSize = 0;
front = 0;
back = 1 ;
}
/** Extrage primul element din cadrul cozii*@throws Unde rflowException daca coada este goala*/
public Object dequeue() throws UnderflowException
{
if ( isEmpty ( ) )
!
throw new UnderflowException (" Coada vida");
}
currentSize ;
Object returnValue = elements [ front ] ;
front = increment ( front ) ;
return returnValue ;
}
/** ntoarce primul element din cadrul cozii
*@throws UnderflowException daca coada este goala . */
public Object getFront() throws UnderflowException
{
53

10.3. COZI
73

i f ( isEmpty ( ) )
{
throw new Underflo wException (" Coada vida.");
}

75
77
78
79
80
81
82
83
84
85
s6
ss
89
90
91
92
93
94
95
96
,7
98
99
IDO
oi
102
103
104
105
106
107
108
109
i io
u, }

return elements[front];
}
/* *
* Incrementeaz circular indicele din coada. */
*/
private int increment(int x)
{
if ( + + x == elements . length )
{
x = 0;
}
return x;
}
/** Incrementeaz dimensiunea cozii cu DEFAULT_CAPACITY atunci
* cand coada este plina . */
private void increaseQueueSize()
{
Object[] newQueue = new Object [ elements . length +
DEFAULTCAPACITY ] ;
for ( int i = 0; i < elements . length ; i ++)
{
newQueue [ i ] = elements [i];
front = increment ( front ) ;
}
elements = newQueue;
front = 0 ;
back = currentSize 1;
}

Exerciiul 3 propune o a doua modalitate de a implementa aceast interfa,


i anume cu ajutorul listelor, care vor fi prezentate mai trziu n cadrul acestui
capitol. Listing 10.9 prezint modul de utilizare a cozii. Deoarece operaiile pe
o coad sunt restricionate ntr-un mod asemntor cu operaiile pe o stiv, este
de ateptat ca i aceste operaii s fie implementate ntr-un timp constant. ntradevr, toate operaiile pe o coad pot fi implementate n timp constant, 0(1 ).

54

10.4. LISTE NLNUITE

Listing 10.9: Exemplu de utilizare a cozii. Programul va afia: Coninutul cozii


este: 0 12 3 4
1 import d a t a s t ruc t ur e s . * ;
2 import exceptions .*;
3/** Clasa simpla de test pentru o coada. Se adaug 5 elemente ,
4* dupa care elementele sunt extrase pe rand */
5 public class TestQueue
o{
7 public static void main(String [] args )
s {
9
Queue q = new QueueAr ( ) ;
io
11
//adugam elemente in coada
12
for ( int i = 0; i < 5; i ++)
!
14
q . enqueue (new Integer(i));
}
L6
17
//scoatem si afiam elementele din coada
s
System, out. prin (" Coninutul cozii este: ");
19
try
20
{
21
for ( ; ; )
22
{
23
System, out. prin (q.dequeue() + " ");
}
}
26
catch ( UnderflowException ue)
!
28
}
29 }
30 }

10.4

Liste nlnuite

ntr-o list nlnuit elementele sunt reinute discontinuu, spre deosebire


de iruri n care elementele sunt reinute n locaii continue. Acest lucru este
realizat prin stocarea fiecrui obiect ntr-un nod care conine obiectul i o refe
rin ctre urmtorul element n list, ca n Figura 10.4. n acest model se rein
referine att ctre primul (first) ct i ctre ultimul (last) element din list. Un
nod al unei liste este implementat n Listing 10.10.
Listing 10.10: Un nod al unei liste
55

10.4. LISTE NLNUITE

i package d at a s truc tu re s ;
! /* *
i * Un nod al listei nlnuite . Clasa este vizibila
i * doar in pachet ( packagefr i endly ), deoarece este
* pentru uzul intern al listei.
, */
' class ListNode
<!
) /* * Valoarea coninuta de nod. */
i Object element ;
/** Legtura ctre nodul urmtor.*/
'. ListNode next ;
i
>
>
i
20

public ListNode ( Object element)


I
this . element = element;
this . next = nuli ;
public ListNode ( Object element, ListNode next)
!
this . element = element ;
this . next = next ;

In orice moment, putem aduga n list un nou element x prin urmtoarele


operaii:
last.next = new ListNode(x); //creaza un nod cu coninutul x
last = last.next; //nodul creat anterior este ultimul in lista
In cazul unei liste nlnuite un element oarecare nu mai poate fi gsit cu
un singur acces. Aceasta este oarecum similar cu diferena ntre accesarea unei
melodii pe CD (un singur acces) i accesarea unei melodii pe caset (acces
secvenial). Dei din acest motiv listele pot s par mai puin atractive dect
irurile, exist totui cteva avantaje importante. n primul rnd, inserarea unui

Figura 10.4: O list simplu nlnuit.


last

first

0,2
56

a3

10.4. LISTE NLNUITE


element n mijlocul listei nu implic deplasarea tuturor elementelor de dup
punctul de inserare. Deplasarea datelor este foarte costisitoare (din punct de
vedere al timpului), iar listele nlnuite permit inserarea cu un numr constant
de instruciuni de atribuire.
Merit observat c dac permitem accesul doar lafirst, atunci obinem o
stiv, iar dac permitem inserri doar la last i accesri doar lafirst, obinem o
coad. Exerciiile 1 i 3 de la finalul acestui capitol propun exact acest lucru:
restricionarea operaiilor pe o list pentru a obine o stiv respectiv o coad.
n general, atunci cnd folosim o list, avem nevoie de mai multe operaii,
cum ar fi gsirea sau tergerea unui element oarecare din list. Trebuie s per
mitem i inserarea unui nou element n orice punct. Aceasta este deja mult mai
mult dect ne permite o stiv sau o coad.
Pentru a accesa un element n list, trebuie s obinem o referin ctre nodul
care i corespunde. Evident c oferirea unei referine ctre un element ncalc
principiul ascunderii informaiei. Trebuie s ne asigurm c orice acces la list
prin intermediul unei referine nu pericliteaz structura listei. Pentru a realiza
acest lucru, lista este definit n dou pri: o clas list i o clas iterator. Clasa
list va reine elementele propriu-zise ale listei n timp ce iteratorul va oferi o
modalitate de a parcurge i modifica elementele listei fr a periclita structura
ei. Listing 10.12 furnizeaz interfaa de baz pentru o list nlnuit, oferind
i metodele care descriu doar starea listei.
Listing 10.14 definete o clas iterator care este folosit pentru toate ope
raiile de accesare a listei. Pentru a vedea cum funcioneaz aceast clas, s
examinm secvena de cod clasic pentru afiarea tuturor elementelor din cadrul
unei structuri liniare. Dac lista ar fi stocat ntr-un ir, secvena de cod ar arta
astfel:
1
2
3
4
5

// parcurge irul a, afind fiecare element


for ( int index = 0; index < a.length; ++index)
{
System . out . p ri ntl n ( a [ index ]) ;
}
n Java elementar, codul pentru a itera o list este:

1
2
3
4
5

// parcurge lista theList de tip List, afind fiecare element


for (ListNode p = theList. first; p != nuli; p = p.next)
{
System . out . p ri n 1 1 n ( p . data ) ;
}

Pe de alt parte, trebuie avut n vedere faptul c anumite operaii n cadrul


listei pot eua (de exemplu tergerea unui element inexistent), motiv pentru
care este utilizat urmtoarea clasa ItemNotFoundException din Listing
57

10.4. LISTE NLNUITE


10.11.

Listing 10.11: Clasa de excepii ItemNotFoundException


i package exceptions ;
2
3/** Clasa de excepii care semnaleaz tentativa de a caut un
4 * element inexistent in cadrul unei structuri de date */
5 public elass ItemNotFoundException extends Exception
0(
7 public ItemNotFoundException ()
s !
9
super ( ) ;
,0 }
1I
12 public ItemNotFoundException ( String msg)
,3 {
14
super(msg);
15 )
,6 }
Listing 10.12: Interfa pentru o list abstract
1 package d at a s t r uc t u re s ;
2
3/** Interfaa pentru un tip abstract de lista inlantuita. */
4 public interface List
5!
6 /* * Testeaz daca lista e vida . */
7 boolean isEmpty ();
N
9 /* * terge toate elementele din lista . */
10 void makeEmpty ( ) ;
>' )
Listing 10.13: Implementarea unei liste nlnuite
1 package d at a s t r uc t u re s ;
2
3 /* *
4 * Lista simplu inlantuita.
5 * Accesarea elementelor se face cu iteratorul Linke dLi st Itr .
6 */
7 public class LinkedList implements List
8(
9 ListNode header ;
10
11
58

10.4. LISTE NLNUITE


12

public LinkedList()
header = new ListNode ( nuli ) ;
public boolean isEmpty ()
return header . next == nuli ;
public void makeEmpty()

24
header . next = nuli ;
25 }
26 }
Listing 10.14: Interfa pentru un iterator abstract de list
i package datastructures ;
2
3 import exceptions .*;
4
5 /* *
6 * Interfaa iterator pentru parcurgerea elementele- r unei
i * liste nlnuite abstracte ( simplu nlnuita , dublu
8 * nlnuita , circulara, etc.)
9 */
io public interface Listltr
" {
12 /** Insereaz un element la poziia curenta.*/
13 void insert(Object x) throws ItemNotFoundException;
14
15 /* *
16
* Seteaz poziia curenta pe elementul x daca
17
* il gsete in lista.
18
*/
19 boolean find(Object x);
20
21 /** terge elementul x din lista. */
22 void remove(Object x) throws ItemNotFoundException;
23
24 /** Verifica daca lista a fost parcursa in totalitate. */
25 boolean isInList ();
26
27 /** Obine elementul aflat pe poziia curenta . */
28 Object retrieve();
29
30 /** Seteaz poziia naintea primului element . */
31 void zeroth ( ) ;
59

10.4. LISTE NLNUITE


33
34
35
36
37
38 }

/** Seteaz p o zi t ia curenta pe primul element . * /


void f i r s t ( ) ;
/** Avanseaz in lista la urmtorul element . */
void advance ( ) ;

Pornind de la interfaa unei liste abstracte, codul pentru implementarea unei


liste nlnuite este dat n Listing 10.13, iar implementarea iteratorului unei liste
nlnuite este dat n Listing 10.15.
Mecanismul de iterare pe care l-ar folosi limbajul Java ar fi similar cu ur
mtoarea secven:
i //parcurge List, folosind abstractizarea si un iterator
2ListItr itr = new LinkedListItr(theList );
3
4 for (itr.first(); itr.isInList(); itr. advance ())
5(
6
System. out. println(itr . retrieve ());
'}
Iniializarea dinaintea ciclului for creeaz un iterator al listei. Testul de ter
minare a ciclului folosete metoda isInLi st ( ) definit pentru clasa Li nkedListltr. Metoda advance ( ) trece la urmtorul nod din cadrul listei. Putem
accesa elementul curent prin apelul metodei retrieve ( ) definit n LinkedL i s 1 1 1 r . Principiul general este c accesul fiind realizat prin intermediul cla
sei L i s 1 1 1 r , securitatea datelor este garantat. Putem avea mai muli iteratori
care s traverseze simultan o singur list.
Pentru a funciona corect, clasa Li st Itr trebuie s menin dou obiecte,
n primul rnd, are nevoie de o referin ctre nodul curent. In al doilea rnd
are nevoie de o referin ctre obiectul de tip List pe care l indic (aceast
referin este iniializat o singur dat n cadrul constructorului).
Listing 10.15: Implementarea iteratorului listei nlnuite
i package d at a s t r uc t u re s ;
2
3 import exceptions .*;
4
5 /* *
6* Iterator pentru lista nlnuita.
7 */
8 public elass Li nke dLi s 1 1 1 r implements Listltr
!
io /** Referina ctre lista care va fi iterata */
n protected LinkedList theList;
12 /* * Refe rin ta ctre nodul curent. */
13 protected ListNode current ;
60

10.4. LISTE NLNUITE


14
15
16
'7
s
19
20
21
22
23
24
25
26
27
28
29
30
31
32

34
35
36
37
38
39
40
41
43
44
45
46
47
48
49
50
si
*
53
54
55
57
59
6o
62

/* * Construiete un iterator pe lista list*/


public LinkedListItr(LinkedList list)
{
theList = list;
current = list. isEmpty () ? list.header : list.header.next;
}
/** Construiete cu parametru de tip List. Daca list
* nu refera o instana LinkedList , se arunca o excepie
* de tipul Clas s C astExc eption */
public LinkedListItr(List list) throws ClassCastException
[
this((LinkedList) list);
}
/** Reseteaza iteratorul care va indica poziia dinaintea
* primului element din lista*/
public void zeroth ()
{
current = t h eL i s t . header ;
}
/** Adaug obiectul x la poziia curenta in lista
* throws ItemNotFoundException daca lista este vida */
public void insert(Object x) throws ItemNotFoundException
{
if (current == nuli)
f
throw new ItemNotFoundException (" Eroare la inserare");
}
ListNode newNode = new ListNode(x, current . next ) ;
current. next = newNode ;
current = current . next ;
}
/** ntoarce true daca obiectul x se afla in lista */
public boolean find(Object x)
{
ListNode itr = t h eL i s t . header . next ;
while (itr != nuli && !itr. element. equals(x))
!
i t r = i t r . next ;
}
if (itr = = nuli)
!
return false ;
}
61

10.4. LISTE NLNUITE


64
65
66
67
68
69
70
71
72
73
74
75

current = itr;
return true ;
}
/** terge obiectul x din lista
* throws ItemNotFoundException daca x nu se afla in lista*/
public void remove (Object x) throws ItemNotFoundException
j
ListNode itr = t h e Li s t . header ;
while (itr.next != nuli && ! i t r . next . element, equals(x))
{
i t r = i t r . next ;
}

77
79
80
82
83
84
85
86
87
88
89
90

92
93
94
95
96
*
98
99
IDO
101
102
103
104
105
106
107
108
109
i io
ni
112
113
62

if (itr. next == nuli)


{
throw new ItemNotFoundException (" tergere euata");
}
itr.next = i t r . next . next ; //trecem peste nodul ters
current = t h eL i s t . header ;
}
/** ntoarce elementul de pe poziia curenta */
public Object retrieve ()
j
return (isInListQ ? current. element : nuli);
}
/** ntoarce true daca ne aflam in interiorul listei */
public boolean isInList ()
!
return (current != nuli) && (current != theL i st . header ) ;
}
/** Elementul curent va fi primul element din lista */
public void first ()
{
current = theList. header. next;
}
/** Avanseaz iteratorul pe urmtorul element*/
public void advance ()
{
if (current ! = nuli)
{
current = current. next;
}
}

10.4. LISTE NLNUITE


114 }
Iat i exemplul de utilizare a listei:

Listing 10.16: Exemplu de utilizare a listei. Programul va afia: Coninutul


listei: 4 3 2 1 0
1 import d a t a s t ruc t ur e s . * ;
2 import exceptions .*;
3/** Clasa simpla pentru testarea unei liste, care adaug 5 numere,
4* dupa care afieaz coninutul listei.*/
5 public class TestList
< {
7 public static void main(String [] args )
s {
9
List theList = new LinkedList () ;
10
Listltr itr = new LinkedListltr (theList );
11
12
//se insereaz elemente pe prima poziie
13
for ( int i = 0; i < 5; i ++)
{
s
try
16
{
17
itr.insert( new Integer(i));
18
}
19
catch(ItemNotFoundException e)
20
{
21
}
22
23
itr.zeroth(); //se trece la nceputul listei
}
25
26
System, out. prin (" Coninutul listei: ");
27
28
for ( itr . first (); itr.isInListQ; itr.advance())
29
{
30
System, out. prin ( itr. retrieve() + " ");
}
32 )
33 )
Dei discuia s-a axat pe liste simplu nlnuite, interfeele din Listing 10.12
i Listing 10.14 pot fi folosite pentru oricare tip de list, indiferent de imple
mentarea pe care o are la baz. Interfaa nu precizeaz faptul c este nevoie de
liste simplu nlnuite.
63

10.5. ARBORI

Figura 10.5: Un arbore generic

10.5

Arbori

10.5.1

Noiuni generale

Vom trece acum s studiem cele mai importante structuri neliniare care apar
n algoritmii pentru calculatoare: arborii. n general vorbind, structura arbores
cent implic o relaie de ramificare ntre noduri, foarte asemntoare celei n
tlnite la crengile unui arbore din natur.
Una dintre definiiile cele mai rspndite ale arborilor (nu neaprat binari)
este urmtoarea (dup D.E. Knuth):
Definiie: Un arbore (Figura 10.5) este o mulime finit T de unul sau mai
multe noduri, care are proprietile:
i) exist un nod special, numit rdcina arborelui;
ii) toate celelalte noduri din T sunt repartizate n mulimi T , Ti , . . . , Tm
disjuncte, fiecare mulime la rndul su fiind un arbore. Arborii T, Ti, . . . , Tm
se numesc subarborii rdcinii.
Se observ c definiia de mai sus este recursiv (recursivitatea este prezen
tat n capitolul 12): am definit un arbore pe baza unor arbori. Totui, privind cu
atenie definiia ne dm seama c nu se pune problema circularitii, deoarece
un arbore cu un singur nod este alctuit doar din rdcin, iar arborii cu n > 1
noduri sunt definii pe baza arborilor cu mai puin de n noduri. Exist i definiii
nerecursive ale arborilor3, dar definiia recursiv este mai adecvat, deoarece
vom vedea c recursivitatea pare s fie o trstur inerent operaiilor pe struc
turi arborescente. Caracterul recursiv al arborilor este de altfel prezent i n
natur, deoarece mugurii arborilor tineri cresc i se dezvolt n subarbori, care
la rndul lor fac muguri i aa mai departe.
Nodul rdcin al fiecrui subarbore se numete fiul (sau copilul) rdcinii,
iar rdcina este tatl (sau printele) fiecrui nod rdcin din subarbori.
3De exemplu, n teoria grafurilor, un arbore este definit ca un graf conex i fr cicluri.
64

10.5. ARBORI

Figura 10.6: Un arbore oarecare.

Din definiia recursiv reiese c un arbore este o colecie de n noduri, dintre


care unul este rdcina, i n 1 muchii. Faptul c exist nl muchii se deduce
din observaia simpl c fiecare muchie leag un nod de printele su i fiecare
nod, n afar de rdcin, are exact un printe.
In arborele din Figura 10.6, rdcina este 1. Nodul 5 l are ca printe pe 1
i are fii 12 , 13 i 1 4. Nodurile care nu au fii se numesc frunze. Frunzele din
arborele de mai sus sunt 8,9,12,13, . . .,21. Nodurile cu acelai printe
se numesc frai. In mod asemntor se definesc relaiile nepot, bunic etc.
Un drum de la un nod ri\ la un nod 71& este o secven de noduri ri\ , rii , , Uk
astfel nct rii este tatl lui rij+i pentru i = 1, k 1 . Lungimea unui drum este
dat de numrul de muchii ale drumului, adic k 1. Pentru oricare nod rii,
nivelul (adncimea) nodului este lungimea unicului drum de la rdcin la rij.
Astfel, nivelul rdcinii este 0. In arborele din Figura 10.6, nivelul nodului 2
este 1, iar al nodului 15 este 3. Adncimea unui arbore este definit ca fiind
maximul adncimilor nodurilor. In exemplul nostru, adncimea arborelui este
egal cu adncimea nodului 15 , care este 3.
10.5.2

Arbori binari

Definiie: Un arbore binar este un arbore n care orice nod are cel mult doi
fii.
Figura 10.7 arat c un arbore binar const ntr-o rdcin i doi subarbori
T8 i T((, oricare din ei putnd s fie vid (s lipseasc).
O proprietate deosebit de important a arborilor binari este c adncimea
medie a unui arbore binar cu n noduri este considerabil mai mic dect n. Se
poate arta c adncimea medie a unui arbore binar este proporional cu Jn,
iar adncimea medie a unui caz particular de arbore binar, arborele binar de
65

10.5. ARBORI

Figura 10.7: Arbore binar generic

Figura 10.8: Exemple de arbori binari (a) arbore binar degenerat (b) arbore
binar nedegenerat
NIVEL

cutare (seciunea 10.5.3), este proporional cu logn . Din pcate, n cazurile


degenerate, adncimea unui arbore poate fi chiar n 1, dup cum se vede i n
Figura 10.8(a).
nlimea (adncimea) unui arbore binar este foarte important, deoarece
multe operaii definite asupra arborilor (tergere de nod, inserare de nod etc.)
sunt proporionale cu nlimea arborelui. Din acest motiv, s-a recurs la diferite
metode pentru a menine adncimea unui arbore proporional cu log n n orice
situaie (arbori AVL, arbori bicolori etc).
Parcurgerea arborilor binari
Dup cum reiese i din Figura 10.7, putem defini un arbore binar ca fiind
o mulime finit de noduri care este fie vid, fie este format dintr-o rdcin i
doi arbori binari. Aceast definiie sugereaz o metod natural de reprezentare
a arborilor binari: fiecare nod va fi descris de trei componente: element informaia util a nodului, left - o referin ctre fiul din stnga i right 66

10.5. ARBORI

Figura 10.9: Reprezentarea nlnuit a arborilor din Figura 10.8

0 referin ctre fiul din dreapta. Dac un nod nu are fiu n stnga atunci left
este nuli, analog, dac nu are fiu n dreapta, atunci right este nuli. Putem
astfel defini clasa BinaryNode, care reine un nod al unui arbore binar:
1
*
3
4
5
6
7

public class BinaryNode


{
Object element ;
BinaryNode left ;
BinaryNode right ;
...
//constructori si alte metode
)

Informaia util din cadrul nodului este reinut de atributul element, care
este de tip Ob j e ct , deci poate referi o instan de orice tip. Figura 10.9 prezin
t modul n care se reprezint nlnuit arborii binari din Figura 10.8. Variabila
root din Figura 10.9 este o referin care indic rdcina arborelui.
Exist mai muli algoritmi pentru manevrarea structurilor arborescente, i o
idee care apare de foarte multe ori este noiunea de parcurgere, de "deplasare"
prin arbore. Parcurgerea unui arbore este de fapt o metod de examinare sis
tematic a nodurilor arborelui n aa fel nct fiecare nod s fie vizitat o singur
67

10.5. ARBORI
dat. Parcurgerea complet a unui arbore ne ofer o aranjare linear a nodurilor,
i operarea multor algoritmi este simplificat dac tim care este urmtorul nod
la care ne vom deplasa ntr-o astfel de secven, pornind de la un nod dat.
Exist trei moduri principale n care un arbore binar poate fi parcurs: nodurile
se pot vizita n preordine, inordine i n postordine. Vom descrie aceste trei
metode recursiv, ca i definiia arborelui.
Dac un arbore binar este vid (nu are nici un nod) parcurgerea lui nu pre
supune nici o operaie; n caz contrar parcurgerea comport trei etape, descrise
n tabelul urmtor:
Preordine
Inordine
Postordine
Se viziteaz rdcina
Se parcurge subarborele stng Se parcurge subarborele stng
Se parcurge subarborele stng
Se viziteaz rdcina
Se parcurge subarborele drept
Se viziteaz rdcina
Se parcurge subarborele drept Se parcurge subarborele drept
Pentru arborele din Figura 10.8 gsim c nodurile n preordine sunt:
ABDHIECFG
deoarece mai nti se viziteaz rdcina A, apoi subarborele stng (B D H I
E) i apoi subarborele drept (C F G).
Similar, parcurgerea n inordine are ca rezultat irul:
HDIBEAFCG
iar parcurgerea n postordine:
HIDEBFGCA
Numele de preordine, inordine i postordine deriv, bineneles de la pozi
ia relativ a rdcinii fa de subarbori. O posibil metod de a parcurge un
arbore n preordine este dat n Listing 10.17. Metoda primete ca parametru o
referin ctre rdcina subarborelui care este vizitat. Evident c la primul apel,
parametrul este chiar rdcina arborelui (referina r oot din Figura 10.9). Vom
vedea cum se integreaz parcurgerea unui arbore n cadrul unei clase complete
n paragraful 10.5.3.
Listing 10.17: Metod generic pentru parcurgerea n preordine a unui arbore
binar
i public void preord ( BinaryNode t)
^(
3 if( t != nuli )
4 (
5
process( t . element ) ;
6
preord ( t.left ) ;
7
preord ( t.right ) ;
8 )
>)
68

10.5. ARBORI
Celelalte dou parcurgeri se realizeaz absolut analog, prin simpla reordonare a instruciunilor de parcurgere.

10.5.3

Arbori binari de cutare

Una dintre cele mai importante aplicaii ale arborilor este utilizarea acestora
n probleme de cutare. Proprietatea care face ca un arbore binar s devin un
arbore binar de cutare este: pentru oricare nod X al arborelui toate nodurile
din subarborele stng sunt mai mici dect X, i toate nodurile din subarborele
drept sunt mai mari dect X. Aadar, fa de arborii binari obinuii, arborii
de cutare mai adaug o relaie de ordine ntre elemente. Pentru a putea fi
comparate, nodurile nu var mai conine obiecte de tip Ob j ect, ca n paragraful
10.5.2, ci instane ale clasei Comparable. Arborele binar de cutare este
o structur de date n care, pe lng cutare rapid, putem aduga sau terge
eficient elemente. Figura 10.10 ilustreaz operaiile de baz permise asupra
unui arbore binar de cutare.

Figura 10.10: Modelul pentru arborele binar de cutare,

insert

find,remove

Arbore binar de cutare

De exemplu, arborii din Figura 10.8 nu sunt arbori binari de cutare, deoa
rece, n ambele cazuri, n stnga nodului A se afl nodul B, care are valoare mai
mare dect A (dac considerm ordinea alfabetic).
S studiem cu atenie arborii din Figura 10.11. La prima vedere, ambii
par s fie arbori binari de cutare; examinnd totui mai minuios arborele din
dreapta, constatm c nodul 7, dei este mai mare dect rdcina, 6, se afl n
stnga ei, ceea ce contravine regulii arborilor de cutare.
69

10.5. ARBORI

Figura 10.1 1 : Doi arbori binari. Doar arborele din stnga este i arbore binar de
cutare.

Principalele operaii care se realizeaz asupra arborilor binari de cutare


sunt:
Cutarea unui nod n arbore;
Gsirea minimului i maximului;
Inserarea unui nou nod;
tergerea unui nou nod;
Vidarea arborelui (tergerea tuturor nodurilor).
O posibil interfa care descrie aceste operaii este prezentat n Listing 10.19.
Setul de operaii permise este acum extins fa de stiv i coad pentru a
permite gsirea unui element arbitrar, alturi de inserare i tergere. Metoda
f ind ( ) ntoarce o referin ctre un obiect care este egal (n sensul metodei
compareTo ( ) din interfaa Comparable) cu elementul cutat. Dac nu
exist nici un element egal cu cel cutat, f ind ( ) arunc o excepie ItemNotFoundException, prezentat n seciunea anterioar. Aceasta este o
decizie de proiectare. O alt posibilitate ar fi fost s ntoarcem nuli n cazul n
care elementul cutat nu este gsit. Diferena ntre cele dou abordri con
st n faptul c metoda noastr l oblig pe programator s trateze explicit
situaia n care cutarea nu are succes. In cealalt situaie, dac am fi n
tors nuli, i nu s-ar fi fcut verificrile necesare, programul ar fi generat un
70

10.5. ARBORI

Listing 10.18: Clasa de exceptie DuplicateItemException


1

package exceptions ;

Clasa de exceptii care semnaleaza adaugarea unui element deja


existent intro structura de date
/
5 public class DuplicateItemException
extends Exception
6 {
public DuplicateItemException ()
7
8
{
super ( ) ;
9
10
}
3/
4

11

public DuplicateItemException ( String msg)


{
super(msg);

12
13
14

15
16

NullPointerException la prima tentativa de a folosi referinta. Din punct


de vedere al ecientei, versiunea cu exceptii ar putea ceva mai lenta, dar este
putin probabil ca rezultatul sa e observabil, cu exceptia situatiei n care codul
ar foarte des executat n cadrul unor situatii n care viteza este critica.
n mod similar, inserarea unui element care deja este n arbore este sem
nalata printr-o exceptie DuplicateItemException (vezi Listing 10.18).
Exista si alte posibile alternative. Una dintre ele consta n a permite noii valori
sa suprascrie valoarea stocata.
Vom da ca exemplu un arbore de cautare care retine stringuri. Acest lucru
este posibil deoarece String implementeaza interfata Comparable. Im
plementarea arborelui binar n conformitate cu interfata SearchTree este
prezentata n Listing 10.21. Pentru a implementa nodurile arborelui binar se
utilizeaza clasa BinaryNode, prezentata n Listing 10.20.
Implementareaarboreluibinarde cautare este mult maicomplexadectpentru stiva sau coada si foloseste recursivitatea (care va prezentata n capitolul
12). Nu va ngrijorati daca nu ntelegeti acum toate detaliile de implementare;
puteti trece mai departe fara nici o problema si reveni mai trziu, dupa ce ati
parcurs si nteles capitolul 12.
Toate elementele din arborele binar ne sunt accesibile prin intermediul ra
dacinii sale, care este retinuta n atributul root. Sa descriem acum pe scurt
modul de functionare al metodelor din clasa BinarySearchTree.
71

10.5. ARBORI

Listing 10.19: Interfaa pentru un arbore binar de cutare


i package d at a s t r uc tu re s ;
2
3 import exceptions .*;
4
5 /* *
6* Interfaa pentru arbori binari de cutare . Expune metode
7 * pentru adugarea si tergerea unui element oarecare si
8 * pentru adugarea si tergerea elementului minim si maxim.
9 */
io public interface SearchTree
>' !
12 void i n s e rt ( Comparable x) throws DuplicateltemException ;
13 void remove ( Comparable x) throws ItemNotFoundException ;
14 void removeMin() throws ItemNotFoundException;
15 Comparable find ( Comparable x) throws ItemNotFoundException;
16 Comparable findMin() throws ItemNotFoundException;
17 Comparable findMax() throws ItemNotFoundException;
s boolean isEmpty();
19 void makeEmpty();
2(1 }

Cutarea unui nod n arbore: Aa cum le spune i numele, arborii binari


de cutare au fost concepui pentru a se putea cuta informaiile rapid i uor.
S presupunem c n arborele de cutare din Figura 10.11 (cel din stnga),
dorim s gsim nodul care are cheia 4. Pentru aceasta comparm valoarea 4
cu rdcina. Deoarece 4<6, nodul cutat se afl n subarborele stng, care are
rdcina 2. Comparm 4 cu 2. Deoarece 4>2, nodul cutat se afl n subarborele
drept. Comparm pe 4 cu fiul din dreapta al lui 2, constatm c valorile sunt
egale i ne oprim. Dac n loc de cheia 4 am fi cutat cheia 5, atunci am fi ajuns
n subarborele drept al lui 4, care este vid, deci nodul 5 nu se gsete n arbore.
Cutarea unui nod se realizeaz cu metoda f i nd ( ) , implementat n liniile
75-95 din Listing 10.21. Aceast metod pornete cu cutarea de la rdcina ar
borelui i coboar fie ctre stnga fie ctre dreapta (funcie de relaia dintre ele
mentul cutat i rdcina subarborelui) pn cnd elementul cutat este gsit sau s-a "ieit" din arbore, caz n care se arunc o ItemNotFoundException ( )

Gsirea elementului minim i a celui maxim: Gsirea elementului minim


(gsirea maximului se face analog) const pur i simplu n a cobor n arbore pe
partea stng (liniile 105-108) pn se ajunge la frunz. Elementul din frunz
este chiar minimul.
72

10.5. ARBORI

Listing 10.20: Clasa care modeleaz un nod al unui arbore binar


i package d at a s tructure s ;
2
3 /* *
4* Reprezint un nod in arbore. Este pentru uz intern.
5 */
6 class BinaryNode
?{
8 Comparable element ;
9 BinaryNode left;
10 BinaryNode right;
11
12
13 BinaryNode ( Comparable e)
14 {
15
element = e ;
16
1 e ft = nuli ;
n
ri ght = nuli ;
18 }
19
20 BinaryNode ( Comparable e , BinaryNode lt , BinaryNode rt )
21 {
22
element = e ;
23
1 e ft = 1 1 ;
24
right = r t ;
25 }
26 }

Adugarea unui nod n arbore: Adugarea unui nod n arbore se realizeaz


folosind metoa insert (Comparable) din liniile 20-24, care la rndul ei
deleg problema ctre insert ( Compar able , BinaryNode ) de la liniile
129-150. Probabil c v ntrebai de ce a fost nevoie s scindm operaia de
adugare n dou metode? Metoda propriu-zis de tergere are i un parametru
de tip BinaryNode, care la nceput este chiar root, rdcina arborelui. Uti
lizatorul obinuit nu are acces la acest atribut, de aceea el poate apela doar
metoda insert (Comparable), care fiind o metod din cadrul clasei, va
putea apela la linia 23 insert (x, root). Aceast tehnic este folosit pen
tru toate metodele existente n clasa BinarySearchTree.
Metoda de adugare a unui nod n arbore este conceptual destul de simpl.
Pentru a insera x n arborele cu rdcina t, ne "afundm" n interiorul arborelui
exact ca i n cazul metodei f ind ( ) . Dac valoarea x este gsit n arbore,
atunci aruncm o DuplicateltemException. n caz contrar inserm un
nod cu cheia x, n ultimul punct din calea care a fost parcurs. Figura 10.12
73

10.5. ARBORI

Figura 10.12: Arborele binar de cutare nainte i dup inserarea nodului 5.

ne arat ce se petrece. Pentru a insera valoarea 5, traversm arborele ca i cnd


am realiza procesul de cutare. Cnd ajungem la nodul cu cheia 4, trebuie s
mergem ctre dreapta, dar nu exist subarbore drept, deci 5 nu este n arbore i
acesta este locul n care trebuie inserat.
Ca orice metod recursiv, insert ( ) ncepe la linia 132 cu condiia de
terminare:
if ( t == nuli )
care este adevrat n momentul n care subarborele curent este vid. In acest
caz se creeaz pur i simplu un nou nod n care se plaseaz obiectul x i se actualizez rdcina subarborelui cu nodul curent. In cazul n care subarborele n
care se insereaz x nu este vid, comparm x cu valoarea din rdcina arborelui:
x . compareTo ( t . element )
Dac valoarea din x este mai mic, atunci x se va insera n subarborele
stng:
t . left = insert (x , t . left ) ;
altfel, dac este mai mare se va insera n subarborele drept:
t.right = insert(x, t.right) ;
iar dac valorile sunt egale nseamn c nodul respectiv exista deja n arbore i
se arunc o DuplicateltemException.
Metoda insert ( ) conine un aspect de finee: cum se leag noul nod
inserat (5 n exemplul nostru) de tatl su (4)? In metoda insert ( ) nu pare
s existe o secven de cod care s semnaleze c fiul din dreapta al lui 4 nu
mai este nuli, ci este noul nod inserat. Rspunsul se afl n modul n care se
74

10.5. ARBORI
realizez apelul recursiv al metodei insert ( ) : subarborelui stng (sau drept)
curent i se atribuie rezultatul inserrii nodului x curent. La un moment dat,
metoda va crea un nou nod, iar adresa acelui nod va fi ntoars ctre nodul
printe.
tergerea elementului minim i a celui maxim: tergerea elementului minim
const n gsirea lui, dup care se "ridic" subarborele drept n locul lui. Metoda
removeMin ( ) de la liniile 152-170 realizeaz exact acest lucru. Observai c
aici, pentru variaie, ciclul while de la liniile 105-108 ale metodei f indMin ( )
a fost nlocuit cu un apel recursiv. "Ridicarea" subarborelui drept n locul ele
mentului ters se face simplu, prin atribuirea de la linia 166:
t = t . right ;
tergerea elementului maxim se realizeaz absolut analog.
tergerea unui nod din arbore: Operaia de tergere a unui nod este cea
mai delicat operaie pe arborele binar de cutare, deoarece pe lng eliminarea
nodului mai implic i operaii suplimentare pentru a pstra structura de arbore
binar. n cazul operaiei de tergere, dup ce am gsit nodul care trebuie ters
trebuie s considerm mai multe posibiliti:
Dac nodul care trebuie ters este o frunz, el poate fi ters imediat prin
nlocuirea legturii printelui su cu nuli;
Dac nodul care trebuie ters are doar un singur fiu (indiferent c este
stng sau drept), tergerea lui se face prin ajustarea legturii printelui
su pentru a-1 "ocoli" (Figura 10.13);
Cazul complicat apare atunci cnd nodul care trebuie ters are doi fii.
Strategia general n acest caz este de a nlocui cheia nodului care tre
buie ters cu cea mai mic cheie din subarborele su drept (care este
uor de gsit) dup care nodul cu aceast cheie se terge folosind metoda
removeMin ( ) descris anterior (Figura 10.14).
tergerea unui nod din arbore se realizeaz folosind metoda remove ( Com
par able ) din liniile 28-32, care la rndul ei deleg problema ctre remove( Compar able, BinaryNode) de la liniile 172-199. Ca orice metod recursiv, remove ( ) ncepe la linia 175 cu condiia de terminare:
if ( t == nuli )
75

10.5. ARBORI

Figura 10.13: tergerea unui nod (4) cu un singur fiu. Legtura 2-3 "ocolete"
nodul 4.

76

10.5. ARBORI
care este adevrat cnd subarborele curent este vid. n aceast situaie elemen
tul x nu este gsit i se arunc o ItemNotFoundException.
Dac subarborele nu este vid, se compar elementul care trebuie ters cu r
dcina i, dac sunt diferite, se coboar fie pe subarborele drept fie pe cel stng.
In cazul n care elementul care trebuie ters a fost gsit (adic x . compareTo ( t . element) este 0) trebuie s eliminm nodul respectiv din arbore avnd
ns mare grij s nu pierdem structura de arbore binar de cutare. Dac nodul
curent are descendeni att n stnga ct i n dreapta, adic
t.left != nuli && t . right != nuli
este true, vom folosi artificiul descris anterior: vom aduce n locul nodului
ters, cel mai mic nod din subarborele drept (care este evident mai mare dect
toate nodurile din subarborele stng, deci se va pstra structura de arbore de
cutare):
t . element = findMin ( t . ri ght ). element ;
dup care vom apela removeMin ( ) pentru a terge elementul minim din acest
subarbore:
t. right = removeMin ( t . ri ght )
Dac nodul curent nu are descendeni n ambele pri, operaia de tergere
este mult mai simpl: pur i simplu se "urc" subarborele nevid n locul nodului
care a fost ters (altfel spus, nodul ters este "ocolit"):
t = (t.left != nuli)? t.left : t. right ;
Metodele isEmpty ( ) i makeEmpty ( ) sunt banale i nu le vom detalia
aici.
Listing 10.21: Implementarea arborelui binar de cutare
i package datastructures ;
2
3 import exceptions .*;
4/** Arbore binar de cutare alocat nlnuit . Toate
5 * cutrile se fac pe baza metodei compareTo ( ) . Expune
6* metode pentru inserare, tergere, cutare etc. */
7 public class BinarySearchTree implements SearchTree
>{
9 /** Referina ctre rdcina arborelui*/
10 protected BinaryNode root ;
11
12 /* * Constructor care iniializeaz rdcina cu nuli */
13 public BinarySearchTree ()
77

10.5. ARBORI

14

{
root = null ;

15
16

17
18
19
20
21
22
23
24

/ Adauga elementul x in arbore


@throws DuplicateItemException daca elementul deja exista./
public void insert( Comparable x )
throws DuplicateItemException
{
root = insert(x, root);
}

25
26
27
28
29
30
31
32
33

/ Sterge elementul x din arbore.


@throws ItemNotFoundException daca elementul nu se afla
in arbore . /
public void remove ( Comparable x )
throws ItemNotFoundException
{
root = remove(x , root );
}

34
35
36
37
38
39
40

Sterge elementul minim din arbore


@throws ItemNotFoundException daca arborele este vid/
public void removeMin () throws ItemNotFoundException
{
root = removeMin ( root ) ;
}
/

41
42
43
44
45
46
47

Intoarce elementul minim din arbore.


@throws ItemNotFoundException daca arborele este vid/
public Comparable findMin() throws ItemNotFoundException
{
return findMin ( root ). element ;
}

48

51

Intoarce elementul maxim din arbore.


@throws ItemNotFoundException daca arborele este vid/
public Comparable findMax () throws ItemNotFoundException

52

49
50

return findMax ( root ). element ;

53
54

55
56
57
58
59
60
61
62
63

78

Cauta elementul x in arbore.


@throws ItemNotFoundException daca x nu este gasit./
public Comparable find (Comparable x)
throws ItemNotFoundException
{
return find (x , root). element;
}

10.5. ARBORI
/** ntoarce true daca arborele este vid */
public boolean isEmpty ()
{
return root == nuli ;
}
/**Videaza arborele, setnd rdcina la nuli */
public void makeEmpty ()
{
root = nuli ;
protected BinaryNode find ( Comparable x, BinaryNode t)
throws ItemNotFoundException
{
while (t != nuli)
!
f ( x . compareTo ( t . element ) < 0)
t = t . left ;
Ise if ( x . compareTo ( t . element ) > 0)
t = t . ri ght ;
1se
return t; //element gsit

throw new ItemNotFoundException (" Element negasit.");


}
protected BinaryNode findMin ( BinaryNode t)
throws ItemNotFoundException
{
if (t == nuli)
{
throw new ItemNotFoundException (" Element negasit");
while ( t . left != nuli )
{
t = t . left ;
return t ;

79

10.5. ARBORI
protected BinaryNode findMax ( BinaryNode t)
throws ItemNotFoundException
{
if ( t == nuli)
{
throw new ItemNotFoundException (" Element negasit");
}
while (t.right != nuli)
{
t = t . ri ght ;
}
return t ;
protected BinaryNode i n s e rt ( Comparable x, BinaryNode t)
throws DuplicateltemException
{
if ( t == nuli)
t = new BinaryNode (x , nuli, nuli);
else if ( x . compareTo ( t . element ) < 0)
t . left = insert (x , t . left );
else if ( x . compareTo ( t . element ) > 0)
t.right = insert(x, t.right);
else
throw new DuplicateltemException (" Elementul exista deja");
return t ;
protected BinaryNode removeMin ( BinaryNode t)
throws ItemNotFoundException
{
if ( t == nuli)
{
throw new ItemNotFoundException (" Element negasit");
if ( t . left != nuli )
{
t.left = removeMin ( t . 1 e ft ) ;
80

10.5. ARBORI

else
t = t . ri ght
return t ;
protected BinaryNode remove ( Comparable x, BinaryNode t)
throws ItemNotFoundException
if (t == nuli)
throw new ItemNotFoundException (" Element negasit");
f ( x . compareTo ( t . element ) < 0)
t.left = remove(x, t.left);
lse if ( x . compareTo ( t . element ) > 0)
t.right = remove(x, t.right);
lse if (t.left != nuli && t . ri ght != nuli)
t . element = findMin ( t . ri ght ). element ;
t.right = removeMin ( t . ri ght ) ;
1se
t = (t.left != nuli) ? t.left : t.right;
return t ;
2(12 }
Listing 10.22 prezint modul n care arborele binar de cutare poate fi
folosit pentru obiecte de tip String.
Interfaa SearchTree mai are dou metode suplimentare: una pentru a
gsi cel mai mic element i una pentru a gsi cel mai mare element. Se poate
arta c transpirnd un pic mai mult se poate gsi foarte eficient i cel mai mic
al k-lea element, pentru oricare k trimis ca parametru.
S vedem care sunt timpii de execuie pentru operaiile pe un arbore bi
nar de cutare. Este normal s sperm c timpii de execuie pentru f ind ( ) ,
81

10.5. ARBORI
insert ( ) i remove ( ) s fie logaritmici, deoarece aceasta este valoarea
pe care o vom obine pentru cutarea binar (paragraful 12.3). Din nefericire,
pentru cea mai simpl implementare a arborelui binar de cutare, acest lucru nu
este adevrat. Timpul mediu de execuie este ntr-adevr logaritmic, dar n cazul
cel mai nefavorabil (cnd arborele este "dezechilibrat") timpul de execuie este
0 ( n ) , caz care apare destul de frecvent. Totui, prin aplicarea anumitor trucuri
de algoritmic se pot obine anumite structuri mai complexe (arbori bicolori)
care au ntr-adevr un cost logaritmic pentru fiecare operaie.
Listing 10.22: Model de program care utilizeaz arbori de cutare. Programul
va afia: Gsit Cristi;Alina nu a fost gsit
1 import d at a s t ru c t ure s . * ;
2 import exceptions .*;
3/** Clasa simpla de test pentru arborii binari de cutare.*/
4 public elass TestSearchTree
5!
6 public static void main ( String [ ] args)
' 1
b
SearchTree t = new Binary SearchTree ( ) ;
10
11
12

String result = nuli ;


try
t.insert("Cristi");
catch ( DuplicateltemException e)

rv
result = (String) t . find ( " Cri s ti " ) ;
System . out . prin (" Gsit " + result + "; ");
atch ( ItemNotFoundException e)
System . out . prin (" C ri s ti nu a fost gsit; ");
ry
result = (String) t . find (" Alina ") ;
System . out . prin (" Gsit " + result + " ;");
}
catch ( ItemNotFoundException e)
82

10.6. TABELE DE REPARTIZARE


38
{
39
System, out. prin (" Alina nu a fost gsita;");
40
}
4, }
42 )
Ce putem spune despre operaiile f indMin ( ) i f indMax ( ) ? In mod
cert, aceste operaii necesit un timp constant n cazul cutrii binare, deoarece
implic doar accesarea unui element indiciat. n cazul unui arbore binar de
cutare aceste operaii iau acelai timp ca o cutare obinuit, adic 0 (log
n ) n cazul mediu i 0 ( n ) n cazul cel mai nefavorabil.

10.6

Tabele de repartizare

Exist foarte multe aplicaii care necesit o cutare dinamic bazat doar
pe un nume. O aplicaie clasic este tabela de simboluri a unui compilator. Pe
msur ce compileaz programul, compilatorul trebuie s rein numele (mpre
un cu tipul, durata de via, locaia de memorie) tuturor identificatorilor care
au fost declarai. Atunci cnd vede un identificator n afara unei instruciuni de
declarare, compilatorul verific s vad dac acesta a fost declarat. Dac a fost,
compilatorul verific informaia adecvat din tabela de simboluri.
Avnd n vedere faptul c arborele binar de cutare permite acces logaritmic
la obiecte cu denumiri oarecare, de ce am avea nevoie de o alt structur de date?
Rspunsul este c arborele binar de cutare poate s dea un timp de execuie
liniar pentru accesul unui element, iar pentru a ne asigura de cost logaritmic
este nevoie de algoritmi mult mai sofisticai.
Tabela de repartizare (engl. hashtable) este o structur de date care evit
timpul de execuie liniar, ba mai mult, suport aceste operaii n timp (mediu)
constant. Astfel, timpul de acces la un element din tabel nu depinde de numrul
de elemente care sunt n tabel. n acelai timp, tabela de repartizare nu folosete
neaprat alocarea nlnuit (ca arborele binar). Aceasta face ca tabela de repar
tizare s fie rapid n practic. Un alt avantaj fa de arborele binar de cutare
este c elementele stocate n tabela de repartizare nu trebuie s implementeze
interfaa Compar able. Acum probabil c v ntrebai: bine, dar dac tabela de
repartizare este att de eficient, de ce se mai folosec arbori binari? Rspunsul
este c arborii binari, dei au operaii de inserare i acces care se execut n timp
logaritmic dispun de operaii pe care tabele de repartizare nu le suport. Astfel,
nu este posibil s parcurgem eficient elementele dintr-o tabel de repartizare.
Practic, tabelele de repartizare ofer un suport eficient pentru numai trei ope
raii: adgarea unui element, tergerea unui element i cutarea unui element.
Parcurgerea elementelor, calculul minimului sau maximului nu sunt suportate.
83

10.6. TABELE DE REPARTIZARE

Figura 10.15: Modelul pentru tabela de repartizare: orice element etichetat


poate adaugat sau sters n timp practic constant.

insert

nd, remove

Tabela de repartizare

Listing 10.23: Interfata pentru tabele de repartizare


1

package d at a s t r uc t u re s ;

import exceptions .;
Interfata expusa de o tabela de repartizare. Ofera

operatii de adaugare , stergere, cautare si golire.


5
/
6 public interface HashTable
7 {
8
void i n s e rt ( Hashable x);
9
void remove ( Hashable x) throws ItemNotFoundException ;
10
Hashable find ( Hashable x) throws ItemNotFoundException;
11
void makeEmpty();
12 }
3

4/

Operatiile permise asupra unei tabele de repartizare sunt date n Figura


10.15, iar o interfata este prezentata n Listing 10.23. n acest caz, insera
rea unui element care este duplicat nu genereaza o exceptie, deoarece elemen
tul va nlocuit cu noua valoare. Aceasta este o alternativa la metoda pe
care am aplicat-o n cazul arborilor binari de cautare. Tabela de repartizare
functioneaza doar pentru elemente care implementeaza interfata Hashable4.
Interfata Hashable (Listing 10.24) expune o functie de repartizare, care aso
ciaza obiectului de tip Hashable un numar ntreg.
Fiecare dintre elementele continute de o tabela de repartizare va asociat
cu un numar, pentru a permite accesul n timp constant la elementul respectiv.
Acest numar este calculat de metoda hash() expusa de interfata Hashable.
Vom vedea mai trziu cum se face accesul la element pe baza acestui numar
4n limba engleza, tabelele de repartizare se numesc "Hashtable", de aceea un element care poate
repartizat se numeste "Hashable".

84

10.6. TABELE DE REPARTIZARE


care este calculat ntr-un timp constant.
Elementele dintr-o tabel de repartizare trebuie s redefineasc i metoda equals ( ) . Listing 10.25 arat cum clasa MyStr ing implementeaz interfaa
Hashable prin implementarea metodelor hash ( ) i equals ( ) .
Listing 10.24: Interfaa Hashable
i package datastructures ;
2/** Descrie un element care poate fi stocat intro
3 * tabela de repartizare */
4 public interface Hashable
5{
6 /** Asociaz un numr ntreg instanei Hashable */
7 int hash(int tableSize);
}
Listing 10.25: Clasa MyStr ing
1 import datastructures.*;
2 import exceptions .*;
3
4/** Exemplu de clasa care implementeaz interfaa Hashable , deci
5 * instanele ei pot fi stocate intro tabela de repartizare. */
6 public class MyString implements Hashable
'{
8 private String value ;
11

public MyString ( Strin g value)


this . value = value ;
public String toString ()
return value ;
public boolean equals ( Object c)
return value . equals ( ((MyString) c). value);
public int hash(int tableSize)

2s
return QuadraticProbingTable . hash ( value , tableSize);
29 }
31) }
Pentru a calcula numrul asociat unui obiect de tip MyString n cadrul
85

10.6. TABELE DE REPARTIZARE

tabelei de repartizare, metoda hash() apeleaza la rndul ei o metoda statica a


clasei QuadraticProbingTable, care va denita mai trziu (vezi Listing 10.28).
Listing 10.26: Un element al tabelei de repartizare
1

package d at a s t r uc t u re s ;

Clasa care descrie un element dintro tabela de repartizare.


Elementul este descris de o in stanta Hashable si o valoare
booleana care indica daca elementul este activ .
/
class HashEntry
{
Hashable element;
boolean isActive ;

3/
4
5
6
7
8
9

10

public HashEntry ( Hashable e)


{
element = e;
isActive = true;
}

11
12
13
14
15
16

public HashEntry (Hashable e , boolean i )


{
element = e;
isActive = i ;
}

17
18
19
20
21
22

Interfata HashTable expune operatiile pe care le suporta structura de


date: inserare, stergere, cautare si eliminarea tuturor elementelor. Unele din
tre aceste operatii pot arunca exceptia ItemNotFoundException, care a
fost prezentata n sectiunea 10.4.
Sa vedem acum cum se implementeaza tabela de repartizare. Vom deni
mai nti un element al tabelei de repartizare, reprezentat de clasa HashEntry
din Listing 10.26. HashEntry modeleaza un element din tabela de reparti
zare. Practic, tabela de repartizare va contine elemente de tipul HashEntry.
Pentru ecare element din tabela este necesar sa specicam valoarea lui (atribu
tul element, de tip Hashable) si daca elementul este activ (atributul is
Active). Trebuie mentionat ca stergerea unui element din tabela de reparti
zare nu implica eliminarea lui zica din structura, ci doar "dezactivarea" lui,
prin setarea atributului isActive la false (paragraful 10.6.1).
Implementarea propriu-zisa a tabelei de repartizare consta din urmatoarele
doua clase: ProbingHashTable si QuadraticProbingTable.
Listing 10.27: Clasa ProbingHashTable
86

10.6. TABELE DE REPARTIZARE

i package datastructures ;
2
3 import exceptions .*;
4/** Implementeaz o tabela cu repartizare nchisa, in care
5 * elementele tabelei de repartizare se rein intrun tablou */
6 public abstract class ProbingHashTable
7 implements HashTable
>{
9 protected HashEntry [] elements ;
10 private int currentSize;
11 private static final int DEFAULT_TABLE_SIZE = 11;
12
13 /** Calculeaz poziia unui element in cadrul tabelei*/
14 protected abstract int findPos ( Hashable x);
15
16 /** Aloca memorie si intializeaza elementele tabloului */
17 public ProbingHashTable ()
18 {
19
elements = new HashEntry [DEFAULT_TABLE_SIZE] ;
20
makeEmpty ( ) ;
2, }
22
23 /** Golete tabela de repartizare, si elibereaz toate
24
* referinele din tabloul elements */
25 public void makeEmpty ()
26 [
27
currentSize = 0;
28
29
for ( int i =0; i < elements . length ; i ++)
30
{
31
elements [i] = nuli;
}
33 }
34
35 /** ntoarce elementul cu cheia x.hash()
36
* throws ItemNotF oundException daca elementul nu e gsit */
37 public Hashable find (Hashable x)
38 throws ItemNotFoundException
39 {
40
int currentPos = findPos(x);
41
42
assertFound(currentPos, " Element negasit");
43
44
return elements [currentPos]. element;
45 }
46
47 /** Verifica daca elementul de pe poziia currentPos
48
* exista si este activ.
49
* throws ItemNotF oundException in caz contrar */
87

10.6. TABELE DE REPARTIZARE


50
51
*
53
54
56
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
Bi
82
83
84
85
86
87
as
89
90
91
92
93
94
95
96
97
98
99
88

private void assertFound ( int currentPos , String message)


throws ItemNotFoundException
!
if ( elements [ currentPos ] == nuli I
elements [currentPos ]. isActive == false)
{
throw new ItemNotFoundException ( message ) ;
)
}
/** terge elementul x din tabela de repartizare.
* throws ItemNotFoundException daca x nu e gsit. */
public void remove(Hashable x)
throws ItemNotFoundException
{
int currentPos = findPos (x);
assertFound(currentPos , "Element negasit");
elements [currentPos ]. isActive = false ;
}
/** Adaug elementul x tabelei de repartizare. Daca
* se atinge gradul de incarcare maxima adims se
* dubleaz numrul de elemente din elements */
public void insert(Hashable x)
j
int currentPos = findPos (x);
elements [currentPos] = new HashEntry (x , true ) ;
if ( + +currentSize < elements. length / 2)
{
return ;
}
//dimensiunea tabelei este prea mica
//si pot aprea conflicte, deci vom dubla numrul de elemente
HashEntry [] oldElements = elements ;
//creaza un tabel de dimensiune dubla
elements = new HashEntry [oldElements . length ] ;
currentSize = 0;
//copie vechiul tabel in cel nou
for(int i = 0; i < oldElements. length; i ++)
{
if (oldElements[i] ! = nuli && oldElements [ i ]. isActive )
{
insert (oldElements [i]. element);

10.6. TABELE DE REPARTIZARE

100

101

102
103

Asociaza un numar unic unei chei de tip String


cu valoarea intre 0 si tableSize 1. /
public static int hash(String key , int tableSize)
{
int hashVal = 0 ;
/

104
105
106
107
108
109

for (int i =0; i <key.length(); i ++)


{
hashVal = 37
hashVal + key . charAt ( i ) ;
}

110
111
112
113
114

hashVal %= tableSize;

115
116

if ( hashVal < 0)
{
hashVal += tableSize ;
}

117
118
119
120
121

return hashVal ;

122

123
124

ProbingHashTable este clasa cea mai importanta n implementarea


unei tabele de repartizare. Ea implementeaza interfata HashTable si de
neste toate operatiile expuse de aceasta. Elementele din tabela de repartizare
HashEntry
Structura
sunt retinute
maintr-un
specica
sir de
deasemenea
obiecte desitip
numarul
de elemente
(atributul
care sunt
elements).
stocate la
momentul curent (atributul currentSize).
Inserarea unui element n tabela de repartizare presupune asocierea unui
numar unic respectivului element, prin intermediul metodei hash(). Acest
numar devine indicele elementului n sirul elements, motiv pentru care acce
sul n timp constant este asigurat. Practic, prin inserarea elementelor n tabela
de repartizare, sirul elements, care retine aceste elemente, va populat ntrun mod discontinuu (elementele nu se vor aa pe pozitii succesive, pentru ca
pozitia lor depinde de valoarea lor). Cnd se va face cautarea elementului, se va
realiza operatia de hash() prin care se obtine numarul asociat, si se va accesa
direct elementul de pe pozitia calculata. Evident, accesul este n timp constant.
Metoda hash() calculeaza numarul asociat unui element pe baza unui al
goritm simplu. Se realizeaza o suma de produse pe baza codului Unicode al
ecarui caracter din cheia elementului care va inserat n tabela, iar n nal
se considera restul mpartirii acestei valori la dimensiunea tabelei, pentru ca
89

10.6. TABELE DE REPARTIZARE

Listing 10.28: Clasa Quadr aticProbingTable


i package d at a s t r uc t u re s ;
2/** Tabela cu repartizare patratica nchisa . Implementeaz metoda
3 * abstracta findPos () motenit de la P rob in gH as h table */
4 public elass QuadraticProbingTable extends ProbingHashTable
5!
6 protected int findPos ( Hashable x)
> !

int collisionNum = 0;
9
int currentPos = x . hash ( array . length ) ;
io
11
while (array[currentPos] != nuli &&
12
!array[currentPos ]. element . equals (x ) )
{
14
collisionNum + + ;
15
currentPos += 2 * collisionNum 1;
16
17
if (currentPos >= array . length )
{
19
currentPos = array . length ;
20
}
}
22
23
return currentPos ;
24
* i
26 }

numrul asociat elementului trebuie s fie mai mic dect dimensiunea tabelei
(numrul nu este altceva dect un indice al irului element s).

10.6.1

Tratarea coliziunilor

Este important s observm faptul c metoda hash ( ) nu ne asigur de


faptul c obiecte distince vor avea repartizri distincte. De fapt, exist anse
considerabile ca pentru dou elemente diferite, metoda hash ( ) s returneze
acelai numr. Aceasta este o situaie inacceptabil, pentru c nu putem avea
dou elemente care s fie pe aceeai poziie n irul element s. Din acest
motiv, avem nevoie de o strategie care s elimine coliziunile care pot s apar.
Strategia va determina dac numrul asociat de metoda hash ( ) este deja ocu
pat i va oferi un alt numr care nu a fost alocat anterior. Aceast strategie va fi
utilizat att n operaia de adugare, ct i n cea de cutare a elementului.
90

10.6. TABELE DE REPARTIZARE


Exist mai multe strategii disponibile, care se bazeaz pe cutarea unui
spaiu neocupat n irul elements. De aici provine i numele clasei ProbingHashTable. Cu alte cuvinte, n caz de coliziune, se probeaz ele
mentele din irul elements, pn se descoper unul neocupat.
Pentru a oferi suport pentru implementarea oricrei strategii, am decis ca
ProbingHashTable s declare abstract metoda care trateaz situaiile de
coliziune (este vorba despre metoda f indPos ( ) ), motiv pentru care i clasa
devine abstract, urmnd ca fiecare strategie s defineasc aceast metod ntr-o
clas derivat separat.
Cea mai simpl strategie pentru evitarea coliziunii este probarea liniar, cu
alte cuvinte cutarea secvenial (element cu element) n ir, pn se gsete
o celul goal, care s fie alocat elementului n cauz. Performana acestei
strategii nu este ridicat, mai ales n situaia n care avem o tabel de dimensiuni
mari.
O strategie mai performant este cea ptratic, n care cutarea nu se face
liniar (i, i + 1, i + 2, i + 3 etc), ca n exemplul anterior, ci n "salturi"
mai mari de tipul i , i + l2,i + 22,i + 32 etc. Acesta este motivul pentru
care clasa care definete metoda f indPos ( ) poart numele de QuadraticProbingTable (quadratic == ptratic). Deoarece n cazul unui conflict, ele
mentul nu va fi pus pe poziia dat de funcia de repartizare ci pe locaia liber
gsit prin probare, cutarea unui element va trebui fcut tot prin probare,
deci tot utiliznd metoda f indPos ( ) , apelat la linia 40 n cadrul metodei
f ind ( ) din clasa ProbingHashTable. Astfel, cutarea se va opri fie cnd
elementul este gsit, fie cnd s-a ajuns la o locaie goal n elements. O alt
observaie important este c tergerea unui element nu va putea fi realizat prin
setarea poziiei care i corespunde n elements la nuli, deoarece aceasta va
mpiedica gsirea elementelor cu aceeai valoare a funciei de repartizare care
au fost aezate prin probare dup elementul ters. Acesta este motivul pen
tru care tergerea unui element se face prin setarea atributului isActive la
false.
Listing 10.29 prezint un exemplu de utilizare al tabelei de repartizare.
Exist foarte multe modaliti de a implementa tabelele de repartizare. Noi
am prezentat aici doar una singur, numit repartizare ptratic nchis. Pentru
0 detalii referitoare la acest subiect recomandm excelenta lucrare Data Structures (vezi [Tomescu]).
Listing 10.29: Exemplu de program care folosete tabele de repartizare; pro
gramul va afia: Gsit Marcel
1 import d a t a s t ruc t ur e s . * ;
2 import exceptions .*;
91

10.7. COZI DE PRIORITATE


3/** Clasa simpla de test pentru tabelele de repartizare. */
4 public elass TestHashTable
5(
6 public static void main ( String [ ] args)
' !
b
HashTable h = new QuadraticProbin gTable ( ) ;
10
11
12
13

MyString result = nuli;


h.insert(new My String (" Marcel ")) ;

result = (MyString) h.find(new MyString (" Marcel ")) ;


System . out . println (" Gsit " + result);
atch ( ItemNotFoundException e)
System . out . println ( "Marcel nu a fost gsit");

10.7

Cozi de prioritate

S pornim de la o problem concret extrem de simpl: ordinea de tiprire


a documentelor la o imprimant partajat, n cadrul unei reele, de un numr
mare de utilizatori. n mod obinuit documentele trimise unei imprimante sunt
aezate, ntr-o coad simpl, dar aceasta nu este ntotdeauna cea mai bun vari
ant. De exemplu, un document poate s fie deosebit de important, deci el
ar trebui executat imediat ce imprimanta este disponibil. De asemenea, dac
imprimanta a terminat de tiprit un document, iar n coad se afl cteva do
cumente avnd 1-2 pagini i un document avnd 100 de pagini, ar fi normal
ca documentul lung s fie tiprit ultimul, chiar dac nu este ultimul document
trimis.
Analog, n cazul unui sistem multiutilizator, sistemul de operare trebuie s
decid la un moment dat care proces, dintre cele existente, trebuie s fie exe
cutat, n general, un proces poate s se execute doar o perioad de timp fixat.
i aici este normal ca procesele care au nevoie de un timp foarte mic s aib
prioritate.
Dac vom atribui fiecrei sarcini cte un numr, atunci numrul mai mic
(pagini tiprite, resurse folosite) va indica o prioritate mai mare. Astfel, vom
dori s accesm cel mai mic element dintr-o colecie de elemente i s l tergem
92

10.7. COZI DE PRIORITATE

Figura 10.16: Modelul pentru coada de prioritate. Doar elementul minim este
accesibil.

insert

ndMin, deleteMin

Coada de prioritate

Listing 10.30: Interfata PriorityQueue


1

package datastructures ;

import exceptions .;
/C
Interfata care descrie operatiile expuse de o coada
5
de prioritati.
6
7
/
8 public interface PriorityQu eue
9 {
/ Adauga elementul x in coada de prioritati/
10
11
void insert(Comparable x) ;
/ Sterge si intoarce elementul minim.
/
12
Comparable deleteMin () throws UnderflowException ;
13
14
/ Intoarce elementul minim din coada/
Comparable findMin () throws UnderflowException ;
15
16
/ Goleste coada de prioritati/
void makeEmpty ( ) ;
17
18
/ Intoarce true daca coada este vida/
boolean isEmpty () ;
19
3
4

20
21

din cadrul colectiei. Acestea sunt operatiile findMin si deleteMin. Struc


tura de date care ofera o implementare foarte ecienta a acestor operatii se
numeste coada de prioritate. Figura 10.16 ilustreaza operatiile fundamentale
pentru coada de prioritate.
Interfata unei cozi de prioritate este descrisa n Listing 10.30.
Clasa de exceptii UnderflowException a fost prezentata n sectiunea
10.2 si are rolul de a semnala situatia n care s-a ncercataccesarea unui element
93

10.7. COZI DE PRIORITATE

Figura 10.17: Un arbore binar complet. Se observ c toate nodurile au exact


doi fii, mai puin nodurile de pe ultimul nivel (D, E, F i G) pentru care frunzele
(H, I i J) sunt completate de la stnga la dreapta.

Figura 10.18: Reprezentarea arborelui din figura 10.17 utiliznd un tablou.


Nodurile arborelui sunt introduse n tablou n ordinea natural a citirii lor de
sus n jos i de la stnga la dreapta. Prima locaie rmne necompletat.
J I
1
I
10 1) \2 13

ntr-o coada de prioriti goal.

10.7.1

Ansamble

Implementarea cozii de prioriti o prezentm sub forma unui ansamblu


(engl. heap, tradus n unele lucrri prin termeni amuzani cum ar fi movil
sau grmad). Ansamblele sunt arbori binari complei. Cu alte cuvinte, fiecare
nod are cte doi fii, mai puin pe ultimul nivel, unde se poate ntmpla ca un
nod s nu aib fii, dar n aceast situaie, nici un nod aflat la dreapta sa nu va
avea fii.
O observaie important pe care o putem face este c dat fiind faptul c
un arbore binar complet este att de regulat, elementele sale pot fi cu uurin
reprezentate ntr-un ir, fr a fi necesar s utilizm alocarea nlnuit din
paragraful 10.5.3! Tabloul din Figura 10.17 corespunde arborelui din Figura
10.18.
94

10.7. COZI DE PRIORITATE


Problema care se pune acum este: cum gsim n reprezentarea sub form de
ir a arborelui care sunt fii i care este printele unui anumit nod? Privind cu
atenie cele dou figuri se observ cu uurin c ntotdeauna fii unui nod care
este pe poziia i n tablou se vor afla pe poziiile 2*ii2*i + l.Pe poziia 2 * i
se afl fiul stng, iar pe poziia 2 * i + 1 se afl fiul drept. n consecin, printele
unui nod aflat de poziia i se va afla pe poziia i / 2 (mprire ntreag). Astfel,
nu numai c nu este nevoie de alocare nlnuit pentru a reprezenta un ansam
blu, ci, mai mult, operaiile de traversare vor fi extrem de rapide (vezi capitolul
2 din primul volum, subcapitolul Operatori la nivel de bit)\ Singura proble
m cu aceast implementare este c avem nevoie de o aproximare a numrului
maxim de elemente din ansamblu pentru a stabili dimensiunea irului n care
vor fi memorate elementele.
Pe lng proprietatea structural dat la nceputul acestui paragraf, un ansam
blu mai are i o proprietate de ordonare care permite execuia rapid a operai
ilor caracteristice: inserare i extragerea celui mai mare (sau a celui mai mic)
element. Deoarece dorim s gsim rapid cel mai mare (mic) element, are sens
s aezm cel mai mare (mic) element n rdcin. Dac considerm c i subarborii stng i drept trebuie s fie i ei ansamble, atunci fiecare nod trebuie s
fie mai mare (mic) dect descendenii si.
Aplicnd acest demers logic, ajungem la proprietatea de ordonare a ansamblelor: valoarea oricrui nod este cel puin la fel de mare ca valoarea fiilor si. In
Figura 10.19, arborele din stnga este un min-ansamblu, n timp ce arborele din
dreapta nu este un ansamblu. Pentru ca arborele ansamblul s respecte aceast
proprietate, indiferent de operaiile care se realizeaz asupra lui, trebuie ca la operaiile de inserare (metoda insert ( ) ) i tergere (metoda deleteMin ( ) )
s se realizeze o reordonare a nodurilor arborelui, astfel nct arborele modifi
cat s respecte i el proprietatea de ordine. Aceasta nseamn propagarea ele
mentelor nspre i dinspre rdcin, operaie realizat de metoda f ixHeap ( ) ,
care filtreaz toate elementele irului (metoda percolateDown()).
Adugarea unui element ntr-un ansamblu: Pentru a fixa ideile, vom con
veni ca n continuare prin ansamblu vom nelege un min-ansamblu. Inserarea
unui element x n ansamblu va decurge astfel: vom crea un nod necompletat
n urmtoarea locaie disponibil (cci altfel arborele nu ar fi complet). Dac x
poate fi plasat n noul nod fr a nclca proprietatea de ordonare, atunci sun
tem gata. Altfel, fie y printele noului nod; deoarece x ncalc proprietatea de
ordonare, avem evident x<y. Vom "mpinge" pe y n jos, n locul noului nod in
serat, urcnd astfel nodul necompletat n sus n arbore. Continum acest proces
de "mpingere" pn cnd noul element x poate fi plasat n nodul necompletat.
Figura 10.20 arat c pentru a insera valoarea 14 se creeaz un nod n urm
95

10.7. COZI DE PRIORITATE

Figura 10.19: Doi arbori binari complei. Numai arborele din stnga este un
ansamblu (un min-ansamblu). In arborele din dreapta nodul 40 are ca fiu nodul
34, ceea ce ncalc proprietatea de ordonare.

Figura 10.20: Inserarea nodului 14 n ansamblu: se creeaz o nou locaie, apoi


aceast locaie este "mpins" n sus pn ce nodul 14 poate fi aezat n ea. In
partea de jos este prezentat reprezentarea sub form de ir utilizat de metoda
insert ( )

toarea locaie disponibil. Aezarea lui 14 n noul nod ar nclca proprietatea de


ansamblu, de aceea 29 "alunec" n jos n locul noului nod. Paii sunt continuai
pn cnd locul corespunztor pentru 14 este gsit. Denumirea n limba englez
pentru aceast modalitate de inserare este "percolate up" (filtrare n sus).
Metoda insert ( ) din liniile 32-51 (Listing 10.31) realizeaz inserarea
obiectului x de tip Comparable n ansamblu. In liniile 35-39 se verific
dac tabloul element s chiar formeaz un ansamblu; dac nu, pur i simplu se
adaug x la finalul tabloului i metoda se ncheie. Dac elements respect
proprietatea de ordine, atunci se verific dac mai este loc n tablou apelnd
checkSize () (linia 41) care dubleaz numrul de elemente dac elements
s-a umplut. Apoi se aplic filtrarea n sus (liniile 43-50), pn cnd x ajunge la
locul lui n ansamblu.
Timpul necesar pentru a insera un element n ansamblu poate ajunge pn
la O (log n) dac elementul care trebuie inserat este chiar noul minim, deoarece
96

10.7. COZI DE PRIORITATE

Figura 10.21 : Crearea unei guri n locul rdcinii prin extragerea valorii mini
me. Din acest moment se caut locul potrivit pentru a insera ultimul nod, 29; se
vor urca pe rnd nodurile 14, 23, 25, dup care se aeaz 29 n locul lsat liber
de 25

va trebui filtrat n sus pn ajunge la rdcin (iar nlimea ansamblului este


log n). In mod surprinztor, s-a artat c n medie sunt necesare 2.6 comparaii
pentru a realiza o inserare, deci n medie un element urc 1.6 niveluri. Aadar,
complexitatea medie a metodei insert ( ) este 0(1).

tergerea unui element dintr-un ansamblu: Extragerea elementului minim


dintr-un ansamblu este realizat foarte asemntor cu inserarea. Gsirea mini
mului este uoar (acesta se afl pe poziia 1 n element s), partea mai dificil
este extragerea lui. Atunci cnd minimul este extras, se creeaz o "gaur" n
locul rdcinii. Deoarece ansamblul are acum cu un element mai puin, rezult
c ultimul element, x, trebuie s urce undeva n sus n ansamblu. Dac x poate fi
plasat n locul "gurii" din rdcin, atunci am terminat. Acest lucru este totui
improbabil, de aceea "mpingem" gaura n jos n locul fiului mai mic, care urc
n locul gurii. Repetm acest pas pn cnd x poate fi plasat n gaur.
Figura 10.21 arat un ansamblu nainte de tergerea rdcinii. Dup ce
nodul 13 a fost extras, trebuie s plasm nodul 31 undeva n ansamblu; 31 nu
poate fi plasat n gaura creat, deoarece ar nclca proprietatea de ordonare.
Astfel, vom plasa nodul 14 n locul rdcinii, i nodul gol alunec un nivel n
jos. Repetm acest pas din nou, punnd pe 19 n gaur i avansnd gaura i mai
adnc n arbore. Vom pune apoi pe 26 n gaur i crem o gaur n ultimul nivel.
In sfrit, vom putea pune nodul 3 1 n gaur. Aceast strategie este cunoscut
sub numele de "filtrare n jos" (percolate down).
tergerea unui element din ansamblu este realizat de deleteMin ( )
(liniile 145-154). La linia 147 se apeleaz metoda f indMin ( ) care ntoarce
elementul minim, sau arunc o Underf lowException dac ansamblul este
vid (excepia este aruncat mai departe de deleteMin ( ) ). n linia 149 este
97

10.7. COZI DE PRIORITATE


aezat n locul rdcinii (elementul minim) elementul de pe ultima poziie. Se
apeleaz apoi metoda per colateDown ( ) pentru filtra noua rdcin i a o
aduce pe poziia adecvat n ansamblu.
Filtrarea n jos (per colateDown ( ) , liniile 1 13-140) a fost definit sepa
rat (spre deosebire de filtrarea n sus, care a fost inclus n metoda i n s e r t ( ) )
deoarece ea va fi folosit n paragraful urmtor i la refacerea ansamblului dup
adugarea de elemente folosind append ( ) . Metoda per colateDown ( )
primete ca parametru poziia pe care se afl rdcina (1 n cazul nostru). Ea
presupune c att subarborele stng ct i cel drept sunt ansamble i doar rd
cina ncalc proprietatea de ordonare. La linia 1 16 se salveaz rdcina ntr-o
variabil temporar. Ciclul while de la liniile 118-137 realizeaz filtrarea
propriu-zis. In liniile 120-126 se calculeaz care este fiul mai mic al nodului i
(care poate fi 2*i sau 2*i + l). Dac rdcina nu poate fi aezat n locul fiului
mai mic (linia 128) atunci fiul mai mic urc n locul rdcinii (linia 130), alt
fel ciclul while este ntrerupt i se plaseaz rdcina pe poziia i (linia 139).
Ciclul while se execut pn cnd fie ultimul element (salvat n tmp) poate
fi aezat n locul nodului curent, fie nodul curent nu mai are fii (i deci putem
aduga rdcina n locul lsat liber de nodul curent).
Complexitatea n timp a algoritmului deleteMin ( ) este proporional
cu adncimea arborelui, adic O(logn). De obicei, gaura din rdcin este
cobort pn n partea de jos a ansamblului, deoarece ultimul element este
mare. Avnd n vedere faptul c adncimea arborelui este logn, rezult c
numrul mediu de execuii ale corpului ciclului while este O (log n).
Construirea unui ansamblu n timp liniar: Adugarea metodei append ( )
la clasa BinaryHeap poate prea nejustificat. De ce s oferim o metod care
stric structura de ansamblu? Rspunsul este c extragerea elementului minim
(care folosete de fapt proprietatea de ordonare) dintr-un ansamblu nu este rea
lizat ntotdeauna imediat ce am inserat un element. n multe situaii practice,
se insereaz un numr nedeterminat de elemente i abia dup aceea se extrage
elementul minim. Am vzut n paragrafele anterioare c tergerea i inserarea
n ansamblu se fac n cazul cel mai nefavorabil n O (log n). Astfel, inserarea a
n elemente ntr-un ansamblu iniial gol s-ar face (n cazul cel mai nefavorabil)
n log 1 + log 2 + . . . + log n = log n! 0(n log n) . Desigur c innd cont de
faptul c metoda ins ert ( ) are totui o complexitate medie de 0(1), inserarea
celor n elemente se face ntr-o complexitate medie de 0(n). Vom demonstra c
folosind n apeluri ale metodei append ( ) urmat de f ixHeap() putem face
acelai lucru n 0(n), chiar i n cazul cel mai nefavorabil. Este uor de vzut c
metoda append ( ) are complexitate 0(1), deci n apeluri ale ei vor fi realizate
n 0(n). Vom arta imediat c i metoda f ixHeap ( ) are tot complexitate
98

10.7. COZI DE PRIORITATE


0(n), deci construirea ansamblului s-a realizat n 0(n).
nainte de a analiza complexitatea n timp a metodei f ixHeap ( ) , s ve
dem cum funcioneaz ea de fapt. Metoda f ixHeap ( ) va rearanja elementele
tabloului elements, astfel nct ele s formeze un ansamblu, f ixHeap ( )
privete tabloul elements ca pe un arbore binar complet i opereaz de la
frunze ctre rdcin. S presupunem c tabloul elements conine n obiecte.
Subarborii de rdcin n/2 + 1, n/2 + 2,. ..,n formeaz ansamble cu un singur
nod, deci nu este necesar s fie modificai. Aadar operarea ncepe descresctor,
de la subarborele de rdcin n / 2 pn la cel de rdcin 1 , dup cum se vede i
din ciclul for de la linia 103. Subarborele de rdcin n/2 poate fi transformat
n ansamblu prin aplicarea algoritmului de filtrare n jos, deoarece att subar
borele stng ct i cel drept sunt deja ansamble. Analog se procedeaz pentru
subarborele de rdcin n/2-1 pn se ajunge la subarborele de rdcin 1,
care este chiar ntreg ansamblul.
S analizm acum complexitatea n timp a metodei f ixHeap ( ) . In cazul
cel mai nefavorabil, fiecare apel al metodei per colateDown ( ) are complex
itatea h(i), unde h{i) este nlimea subarborelui de rdcin i. Complexitatea
metodei f ixHeap ( ) este aadar proporional cu suma nlimilor nodurilor
din ansamblu, care este N H 1 G 0(n) n cazul unui ansamblu perfect de
nlime H (avnd 2H+1 - 1 noduri).
Iat i codul complet al clasei Binar yHeap:

Listing 10.31: Implementarea unei cozi de prioriti cu ajutorul unui ansamblu


i package datastructures ;
2
3 import exceptions .*;
4
5/** implementarea unei cozi de prioriti folosind un
6 * ansamblu
I */
8 public class BinaryHeap implements PriorityQueue
"i
10 /** Numrul de elemente din ansamblu . */
II private int currentSize ;
12
13 /** true daca elements formeaz un ansamblu, false altfel */
14 private boolean orderOK ;
15
16 /** Elementele din ansamblu . * /
17 private Comparable [] elements ;
18
99

10.7. COZI DE PRIORITATE


19
20
21
22
23
24

26
27
28
29
30
31
32
33
34
35
37
38
40
4i
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
63
64
65
66
67
68
100

/** Capacitatea implicita a ansamblului */


private static final int DEFAULT_CAPACITY = 11;
/** Contruieste un ansamblu, alocnd elemente pentru element s
* si umplnd poziia 0 cu un element foarte mic */
public BinaryHeap( Comp arab le ne glnf )
{
currentSize = 0;
orderOK = true ;
elements = new Comparable [DEFAULT.CAPAQTY ] ;
elements [0] = neglnf ;
}
/** Adaug elementul x in ansamblu */
public void i n s e rt ( Comparable x)
j
if (! orderOK)
{
append(x);
return ;
}
checkSize ( ) ;
int hole=++currentSize;
for ( ; x . compareTo (elements[hole / 2]) < 0; hole /= 2 )
{
elements[hole] = elements[hole / 2];
}
elements[hole] = x;
)
/** Adaug un element ignornd proprietatea de ordonare */
public void append( Comparable x)
!
checkSize ();
elements [++ currentSize ] = x;
if ( x . compareTo ( elements [ c urre nt S i z e / 2]) < 0)
{
orderOK = false;
)
}
/** Verifica daca irul elements este plin, si in caz
* afirmativ ii dubleaz dimensiunea. */
private void checkSize ()
{

10.7. COZI DE PRIORITATE


69

if (currentSize == elements.length 1)
!
Comparable [ ] oldelements = elements ;

71
72
73
74
75

elements = new Comparable [ c u rrent S i z e * 2];


for ( int i = 0; i < oldelements . length ; i ++)
!
elements [ i ] = oldelements [ i ] ;
}

77
}
80
XI
82
83
84
85
B6
ss
89
90
91
92
93
94
95
96
,7
98
99
oo
101
102
103
104
105
106
107
108
109
1 II)
ui
112
113
4
115
116
1 17
118

}
/** ntoarce elementul minim din ansamblu.
* throws Unse rflowException Daca ansamblul este gol */
public Comparable findMin () throws UnderflowException
{
if ( isEmpty ( ) )
!
throw new UnderflowException ( "Heap vid " ) ;
}
if ( ! orderOK)
{
fixHeap ( ) ;
}
return elements [ 1 ] ;
}
/** Transforma irul elements intrun ansamblu filtrnd
* toate nodurile care nu sunt frunze pana ajunge la rdcina */
private void fixHeap ()
{
for (int i = currentSize / 2; i > 0; i )
{
percolateDown ( i ) ;
}
orderOK = true ;
}
/** Filtreaz elementul de pe poziia hole asezandul in
* sub ar b o rele cu rdcina hole la locul cuvenit * /
private void percolateDown (int hole)
{
int child ;
Comparable tmp = elements [ hole ] ;
while(hole * 2 <= currentSize)
101

10.7. COZI DE PRIORITATE

child

hole * 2 ;

f (child != currentSize &&


elements [ child + 1 ]. compareTo ( element s [ chi 1 d ] ) < 0)
child ++;
f ( element s [ c hi ld ]. compareTo ( tmp ) < 0)
elements [ hole ] = elements [ child ] ;
eIse
break ;
hole = child
elements [ hole ] = tmp;
)
/** terge elementul minim ( aflat pe poziia 1!) si
* apeleaz algoritmul de filtrare pentru a reface ansamblul .
* @throws Unde rflowException daca ansamblul este vid. */
public Comp arab le deleteMin() throws UnderflowException
{
Comparable minltem = findMin();
elements [1] = elements [ c u rrent S i ze

];

percolateDown (1);
return minltem ;
/** true daca ansamblul este vid */
public boolean isEmpty()
{
return currentSize == 0;
/** Golete ansamblul setnd dimensiunea la 0 */
public void makeEmpty()
{
currentSize = 0;

102

10.7. COZI DE PRIORITATE


Pentru a testa ansamblul, am creat urmtorul exemplu simplu:
Listing 10.32: Exemplu de utilizare a unei cozi de prioriti. Programul va afia:
Coninutul ansamblului: 0 12 3 4
1 import d a t a s t ruc t ur e s . * ;
2 import exceptions .*;
3
4/** Clasa de test simpla pentru cozi de prioritate. */
5 public class TestPriorityQueue
< {
7 public static void main(String [] args )
s {
9
PriorityQueue pq = new BinaryHeap (
10
new Integer ( Integer .MESLVALUE) ) ;
11
12
pq . i n s ert (new Integer (4))
13
pq . i n s ert (new Integer (2))
14
pq . i n s ert (new Integer ( 1 ) )
15
pq . i n s ert (new Integer (3))
16
pq . i n s ert (new Integer (0))
17
s
System . out . prin (" Coninutul ansamblului : ");
19
try
20
{
21
for ( ; ; )
22
{
23
System . out . p ri n t ( pq . deleteMin ( ) + " ");
24
}
}
26
catch ( UnderflowException e)
1
28
}
29 }
30 }

Rezumat
Am prezentat pe scurt n cadrul acestui capitol cele mai importante structuri
de date utilizate de programatori n practic. Fiecare structur de date ofer o
interfa clar cu toate operaiile pe care le permite, implementarea ei putnd
fi fcut n mai multe moduri. Stiva i coada sunt structuri de date elementare,
care ofer operaii de adugare respectiv tergere la un singur capt n timp
constant. Listele nlnuite permit operaii de adugare i tergere arbitrare i
au avantajul fa de iruri c inserarea unui element nu implic deplasarea ele
mentelor aflate la dreapta. Arborii binari de cutare permit inserarea, ctuarea
103

10.7. COZI DE PRIORITATE


i tergerea n timp logaritmic. Ei ofer de asemeni o modalitate de a parcurge
n ordine elementele coninute. Tabelele de repartizare permit o implementare
extrem de eficient a operaiilor de inserare i regsire. n sfrit, cozile de pri
oritate permit operaii asupra elementului minim (sau maxim) din cadrul struc
turii n timp logaritmic. Ele au avantajul fa de arborii binari (care permit i
ei acelai lucru) c sunt mai simplu de implementat i nu implic reinerea de
legturi cte fii din stnga i dreapta. Structurile de date reprezinte o disciplin
de sine stttoare, iar noi nu am putut aici dect s oferim o introducere n acest
domeniu. Cei care doresc s afle mai multe sunt invitai s parcurg [Tomescu]
i [Cormen].
ncepnd cu urmtorul capitol, vor fi prezentate metodele fundamentale de
elaborare a algoritmilor. Prima dintre ele este metoda cutrii cu revenire (backtracking).
Noiuni fundamentale
ansamblu binar: o variant de arbore binar complet n care un nod este fie
mai mic fie mai mare dect fii si.
arbore: structur de date format dintr-o mulime de noduri i muchii (fo
losite pentru a lega nodurile ntre ele) astfel nct nu exist noduri care nu sunt
legate direct sau indirect i nu se pot realiza circuite.
arbore binar: arbore n care fiecare nod are cel mult doi fii.
arbore binar de cutare: variant de arbore binar care suport eficient
operaii de adugare, tergere i cutare. Fiul din stnga este mai mic dect
printele, iar fiul din dreapta este mai mare.
clas iterator: tip de clas care permite manipularea unei liste.
coad: structur de date care permite accesul doar la cel mai vechi element
adugat.
coad de prioriti: structur de date care permite accesul doar la elemen
tul cel mai mic.
frunz: ntr-un arbore, reprezint un nod fr fii.
funcie hash: funcie care calculeaz pe baza unui obiect primit, un ntreg
care va fi asociat cu obiectul respectiv. Doar obiectele care implementeaz in
terfaa Hashable pot fi folosite de aceast funcie.
stiv: structur de date care permite accesul doar la cel mai recent element
inserat.
structur de date: o reprezentare a unor date i a operaiilor pe acele date.
Prin intermediul structurilor de date se poate obine reutilizarea componentelor.
tabel de repartizare (hashtable): structur de date care permite adugri,
tergeri i cutri ntr-un timp mediu constant.
104

10.7. COZI DE PRIORITATE


Erori frecvente
1 . Se consider eroare, ncercarea de a accesa sau terge elemente n cazul
n care stiva, coada sau coada de prioriti sunt vide.
2. O coad de prioriti nu are nici o legtur cu o coad. Este doar o coin
ciden de nume.
3. Funcia hash ( ) pentru o tabel de repartizare returneazo valoare de tip
int. Este posibil ca n timpul calculelor intermediare s se produc de
piri, aadar este necesar s se verifice ca rezultatul operatorului modulo
s fie pozitiv.
4. Performana unei tabele de repartizare se degradeaz semnificativ pe m
sur ce factorul de ncrcare se apropie de 1 .0. Mrii dimensiunea tabelei
de ndat ce factorul de ncrcare a atins valoarea 0.5.
Exerciii
Pe scurt
1. Artai care sunt rezultatele urmtoarei secvene de operaii: adaug (6) ,
adaug(12), adaug(2), adaug(7), terge () , ter
ge ( ) n cazul n care operaiile adaug ( ) i terge ( ) corespund
operaiilor de baz pentru:
(a) stiv;
(b) coad;
(c) arbore binar de cutare;
(d) coad de prioriti.
2. Desenai arborii binari de cutare de nlime 2,3,4,5 i 6 pentru mulimea
de chei {1,5,8,9, 12, 17,29}!
3. Scriei ordinea nodurilor rezultat pentru parcurgerea n preordine, inordine i postordine a arborilor de cutare de la exerciiul precedent.
4. S presupunem c avem numerele de la 1 la 1000 ntr-un arbore binar
de cutare i c dorim s cutm numrul 363. Care dintre urmtoarele
secvene nu poate constitui o secven de noduri examinate?
(a) 2, 252, 401, 398, 330, 344, 397, 363;
105

10.7. COZI DE PRIORITATE


(b) 924, 220, 911, 244, 898, 258, 362, 363;
(c) 925,202,911,240,912,245,363;
(d) 2, 399, 387, 219, 266, 382, 381, 278, 363;
(e) 935, 278, 347, 621, 299, 392, 358, 363.
5. Desenai ansamblul care rezult n urma inserrii elementelor 47, 28, 19,
6, 15, 12, 3, 5, 9, 14 ntr-un ansamblu iniial vid!
Teorie
1. S presupunem c am implementa coada din paragraful 10.3 fr a aeza
elementele circular. Gsii o secven de operaii, care repetat la infinit
conduce la mrirea nedefinit a tabloului element s, chiar dac n coad
sunt doar cteva elemente.
2. S presupunem c dorim o structur de date care s suporte doar urm
toarele operaii: adugare, gsirea maximului i tergerea maximului. Ct
de eficient pot fi implementate aceste operaii?
Indicaie: Se utilizeaz un ansamblu n care rdcina este mai mare dect
fii.
3. Exist o posibilitate de a implementa urmtoarele operaii n timp logaritmic n cadrul aceleiai structuri de date: gsire minim i maxim, tergere
minim i maxim, adugare?
Indicaie: Se utilizeaz un arbore binar de cutare.
4. Exist posibilitatea de a implementa urmtoarele operaii n timp con
stant: push, pop i gsirea minimului?
Indicaie: Se observ c nu se cere i tergerea minimului. Putem astfel
utiliza dou stive, una care reine elementele structurii de date, iar alta
care reine valorile minime.
5. De ce n cazul metodei f indMin ( ) din clasa BinarySear chTree
nu a fost nevoie s salvm valoarea parametrului t, care este modificat
de ctre metod?
6. Profesorul tietot are impresia c a descoperit o proprietate remarcabil
a arborilor de cutare. S presupunem c o cutare pentru cheia k ntrun arbore de cutarese termin ntr-o frunz. Considerm urmtoarele
mulimi: A: nodurile din stnga drumului parcurs pn la k, B: mulimea
nodurilor aflate chiar pe acest drum i C, mulimea cheilor din dreapta
106

10.7. COZI DE PRIORITATE


drumului. Profesorul tietot susine c toate cheile din A sunt mai mici
dect cele din B, care sunt la rndul lor mai mici dect cele din C. Dai
cel mai mic contraexemplu care s spulbere afirmaia lui tietot!
7. Care este numrul minim i numrul maxim de elemente ntr-un ansam
blu cu nlimea hl
8. Artai c un ansamblu cu n elemente are nlimea [log2 n\ !
9. Unde poate s se afle cel mai mare element ntr-un min-ansamblu?
10. Un tablou cu elementele ordonate cresctor este un ansamblu?
11. Secvena 47, 28, 19,6, 15, 12,3,5,9, 14 este un ansamblu?
n practic
1 . n paragraful 10.2 am vzut cum se poate implementa o stiv folosind un
ir pentru a reine elementele. Implementai acum stiva folosind clasa
LinkedList din paragraful 10.4 i un iterator special, care permite
doar operaiile specifice stivei.
2. n paragraful 10.3 am vzut cum se poate implementa o coad folosind un
ir pentru a reine elementele. Implementai acum coada folosind clasa
LinkedList din paragraful 10.4 i un iterator special, care permite
doar operaiile specifice cozii.
3. Scriei o metod care afieaz elementele dintr-o list n ordine invers.
Pentru aceasta definii un iterator care parcurge lista i depune fiecare
element ntr-o stiv. Dup ce lista a fost parcurs se vor scoate elementele
din stiv pn cnd aceasta se golete.
Proiecte de programare
1. n paragraful 10.6 am vzut cum se implementeaz o tabel de reparti
zare utiliznd un tablou pentru a reine elementele. n cazul unui conflict
(dou elemente care sunt repartizate n acelai loc) utilizam o funcie de
repartizare ptratic pentru a gsi o poziie liber n care s inserm noul
element. Exist i o alt posibilitate de a gestiona conflictele: fiecare
element al tabloului elements este de fapt o list nlnuit. Astfel,
conflictele se vor rezolva aeznd elementele succesiv n cadrul listei n
lnuite. Scriei o clas ChainedHashtable care implementeaz in
terfaa Hashtable folosind metoda descris anterior.
107

11.Metoda Backtracking

Ab uno disce omnes (Dupa unul i


poti judeca pe toti).
Vergiliu, Eneida

Prima metoda de elaborare a algoritmilor pe care o vom prezenta este back


tracking. Aceasta metoda este utilizabila pentru un cadru larg de probleme, iar
modul ei de aplicare este aproape algoritmic. Dintre toate metodele de elabo
rare prezentate, backtracking este considerata a cea mai elementara, deoarece
ea se reduce la o parcurgere exhaustiva inteligentaa spatiuluide cautare si nece
sita cele mai putine adaptari ale formei generale pentru a aplicata n probleme
concrete.
Discutia asupra acestei metode ncepe cu o prezentare a cadrului n care ea
poate utilizata si a principiilor esentiale care stau la baza ei. Vom da apoi
schema generala de rezolvare a problemelor backtracking, urmata de cteva
exemple clasice n care aceasta metoda se aplica.
n cadrul acestui capitol vom prezenta:
Care sunt problemele carora li se poate aplica metoda backtracking;
Ce sunt conditiile interne si conditiile de continuare, si cum inuenteaza
acestea ecienta unui algoritm backtracking;
Care sunt cei patru pasi care se aplica starii initiale a problemein vederea
obtinerii solutiilor;
Care este schema generala de rezolvare a problemelor backtracking si
cum se particularizeaza ea pentru diverse probleme concrete;
Exemple clasice de probleme care admit rezolvare prin aceasta metoda.
108

11.1. PREZENTARE GENERAL


11.1

Prezentare general

n practic apar adeseori situaii n care rezolvarea unei probleme se reduce


n esen la determinarea unor vectori de forma:
x = {xi,x2, ,xn)
unde:
fiecare component Xi aparine unei mulimi finite Vf,
componentele vectorului x respect anumite relaii, numite condiii in
terne, astfel nct x este o soluie a problemei dac i numai dac aceste
condiii sunt satisfcute de componentele x , Xi , . . . , xn ale vectorului.
Produsul cartezian Vi x V2 x . . . x Vn se numete spaiul soluiilor posibile.
Elementele acestui produs cartezian care respect condiiile interne se numesc
soluii ale problemei.
Exemplul 1: Fie dou mulimi de litere Vi = {A, B, C} i V2 = {M, N}.
Se cere s se determine acele perechi {x\,X2) avnd proprietatea c dac X\
este A sau B, atunci 2 nu poate fi N.
Rezolvarea problemei de mai sus conduce la perechile:
(A,M),(B,M),(C,M),(C,N)
deoarece din cele ase soluii posibile, doar acestea ndeplinesc condiiile puse
n enunul problemei.
Exemplul 2: Se d mulimea cu elementele {A, B,C, D}. Se cere s se
genereze toate permutrile elementelor acestei mulimi. Se cer aadar cvadrupletele de forma x = (xi, 2, X3, 4) care respect condiiile
Xi ^ Xj pentru i ^ j;
Xi aparine mulimii V = Vi = {A, B, C, D}, i = 1, 2, 3, 4.
Exist mai muli vectori ce respect aceste condiii: {^4, B, C, D}, {B, A, C, D},
{B, C, D, A}, {B, D, A, C} etc. Mai exact, numrul de permutri ale elemen
telor unei mulimi cu 4 elemente este 4! =24.
O modalitate de rezolvare a problemei ar fi s se genereze toate cele 44 =
256 elemente ale produsului cartezian V x V2 x V3 x V4 (reprezentnd soluiile
109

11.2. PREZENTAREA METODEI


posibile) i s se aleag dintre ele cele 24 care respect condiiile interne. S
observm ns c dac n loc de 4 elemente mulimea noastr ar avea 7 elemente,
vor exista 77 = 823.543 variante posibile, dintre care doar 7! = 5040 (adic
doar 0.6%) vor respecta condiiile interne. Aceasta nseamn c 99.4% dintre
variante sunt generate inutil.
Din cele artate mai sus reiese c are sens s ne punem problema de a gsi
algoritmi mai eficieni, care s evite generarea brutal a ntregului spaiu de
soluii pentru a selecta apoi soluiile care respect condiiile interne. Metoda
backtracking este o metod foarte important de elaborare a algoritmilor pentru
problemele de genul celor descrise mai sus. Dei algoritmii de tip backtracking
au i ei, n general, complexitate exponenial, sunt totui net superiori unui
algoritm care genereaz toate soluiile posibile.

11.2

Prezentarea metodei

Aa cum am vzut n paragraful anterior, metoda backtracking urmrete


s evite generarea tuturor soluiilor posibile, reducnd astfel drastic timpul de
calcul.
S vedem acum care este modalitatea prin care backtracking genereaz
soluiile, evitnd parcurgerea exhaustiv a spaiului de cutare. Componen
tele vectorului x primesc valori n ordinea cresctoare a indicilor (vom nota
aceste valori cuii,^,---!^011 scopul de a face diferena ntre o component
care nu are o valoare atribuit, Xk, i o component care are atribuit o valoa
re, Vk). Aceasta nseamn c lui Xk nu i se atribuie o valoare dect dup ce
x,X2, , Xk-i au primit valori. Mai mult dect att, valoarea t>k atribuit
lui Xk va trebui astfel aleas nct v, V2, , Vk s respecte anumite condiii,
numite condiii de continuare, care sunt deduse de ctre programator pe baza
condiiilor interne. Astfel, dac n Exemplul 2, prima component, X\ , a primit
valoarea V\ = A, este clar c lui X2 nu i se va mai putea atribui aceeai valoare
(elementele unei permutri trebuie s fie diferite).
Nendeplinirea condiiilor de continuare exprim faptul c oricum am alege
valorile pentru componentele Xk+i , , xn, nu vom obine nici o soluie (deci
condiiile de continuare sunt strict necesare pentru obinerea unei soluii). Prin
urmare, se va trece la atribuirea unei valori lui Xk, doar dac condiiile de con
tinuare pentru componentele x , X2 , , Xk (avnd valorile V\ , V2 , , Vk ) sunt
ndeplinite. n cazul nendeplinirii condiiilor de continuare, se alege o nou va
loare pentru Xk sau, n cazul n care mulimea valorilor posibile, Vk, a fost
epuizat, se ncearc s se fac o nou alegere pentru componenta precedent,
Xk-i , a vectorului, micornd pe k cu o unitate. Aceast revenire la componenta
110

11.2. PREZENTAREA METODEI


precedent d numele metodei, exprimnd faptul c dac nu putem avansa, ur
mrim (engl. track = "a urmri") napoi (engl. back = "napoi") secvena curent
din soluie.
Trebuie observat faptul c respectarea condiiilor de continuare de ctre va
lorile Vi , Vi , , Vk nu reprezint nicidecum o garanie a faptului c vom obine
o soluie continund cutarea cu aceste valori. Deci condiiile de continuare sunt
condiii necesare pentru ca Vi, V2, ,Vk s conduc la o soluie, dar nu sunt
neaprat condiii suficiente.
Alegerea condiiilor de continuare este foarte important, o alegere bun
ducnd la o reducere substanial a numrului de calcule. n cazul ideal, aceste
condiii ar trebui s fie nu numai necesare, ci chiar suficiente pentru obinerea
unei soluii, ceea ce ar garanta c toate secvenele V\ , V2 , . . . , Vk generate vor
conduce n final la o soluie. n practic acest lucru nu este adeseori posibil, de
aceea se urmrete gsirea unor condiii de continuare care s fie ct mai "dure",
adic s elimine din start ct mai multe soluii neviabile. De obicei, condiiile de
continuare reprezint restricia condiiilor interne la primele k componente ale
vectorului. Evident, condiiile de continuare n cazul k=n sunt chiar condiiile
interne.
De exemplu, o condiie de continuare n cazul problemei permutrilor din
Exemplul 2 ar fi:
Vk ^vi,yii = \,k-\
Prin metoda backtracking, orice vector este construit progresiv, ncepnd cu
prima component i mergnd ctre ultima, cu eventuale reveniri asupra valo
rilor atribuite anterior. Reamintim c X\ , X2, , xn primesc valori n mulimile
Vi, V2, , Vn. Prin atribuiri sau ncercri de atribuiri euate din cauza nerespectrii condiiilor de continuare, anumite valori sunt consumate. Pentru o
component oarecare Xk vom nota prin Ck mulimea valorilor consumate la
momentul curent. Evident, Ck C V* .
O descriere complet a strii n care se afl un algoritm backtracking la un
moment dat se poate face prin precizarea urmtoarelor elemente:
1. componenta din vector la care s-a ajuns n procesul de cutare, avnd
valoarea k;
2. valorile V\ , V2, , Vk-i care au fost atribuite primelor k lcomponente
ale vectorului x;
3. mulimile de valori C\ , C2, , Ck care au fost consumate pentru com
ponentele v, V2, , Vk-i i pentru componenta curent, Xk111

11.2. PREZENTAREA METODEI


Aceast descriere poate fi sintetizat ntr-un tabel numit configuraie, avnd
urmtoarea form:

Semnificaia unei astfel de configuraii este urmtoarea:


1 . n ncercarea de a construi un vector soluie a problemei, componentelor
X\,X2, , Xk-i li s-au atribuit valorile v, V2, , vk-i\
2. aceste valori satisfac condiiile de continuare;
3. urmeaz s se atribuie o valoare componentei xk\ deoarece valorile con
sumate pn n prezent sunt cele din mulimea Ck, componenta xk va
putea primi o valoare vk din Vk Ck ;
4. valorile consumate pentru componentele x , X2 , , xk sunt cele din mul
imile C\ , C2, , Ck , cu precizarea c valorile curente V\ , V2 , , Vk-i
sunt consumate, deci apar n mulimile C\ , C2, -, Cfc_i ;
5. pentru componentele Xk+i , , xn nu s-a ncercat nici o atribuire, deci
nu s-a consumat nici o valoare i, prin urmare, Ck+i , , Cn sunt vide;
6. pn n acest moment au fost construite eventualele soluii de forma:
(ci, . . .) cu ci Ci - {vi};
(vi,c2, . . .) cu c2 C2 - {v2};
(vi,v2, ,vk-2,Ck-i, ) cucu G Ck-i - {vk-i};
(vi,v2,---,vk-i,ck,...) cucfc Ck;
Aceast ultim afirmaie este mai dificil de neles i recomandm reluarea ei
dup lecturarea exemplului de mai jos.
n construirea permutrilor mulimii cu elementele {A,B,C,D} din Ex
emplul 2, pentru k = 4, configuraia
C

are, conform celor artate mai nainte, urmtoarea semnificaie:


1 . componentele X\ , x2 , X3 au primit respectiv valorile C, A,B;
112

11.2. PREZENTAREA METODEI


2. tripletul C, A, B satisface condiiile de continuare;
3. urmeaz s se atribuie o valoare componentei 4. Componenta 4 ia
valori din mulimea V4 C4, adic una din valorile {C, D};
4. Ci = {A, B, C}, C2 = {A}, C3 = {A, B}, C4 = {A, B};
5.

+ 1 = 5 > n, deci acest subpunct nu are obiect n aceast situaie;

6. pn n acest moment au fost deja construite soluiile de forma (n ordinea


n care au fost descrise la punctul 6 de mai sus):
(A,.. .) adic (A,B,C,D), (A,B,D,C), (A,C,B,D), (A,C,D,B),
(A,D,B,C), (A,D,C,B) i
(B, . . .) adic (B,A,C,D), (B,A,D,C), (B,C,A,D), (B,C,D,A),
(B,D,A,C), (B,D,C,A);
soluii de aceast form nu exist, deoarece C2 {i>2 } = 4>\
soluii de forma (C, A,A,...): nu exist soluii de aceast form;
soluii de forma (C,A,B,A), (C,A,B,B): nu exist soluii de aceast
form.
Metoda backtracking ncepe a fi aplicat n situaia n care nu s-a fcut nici o
atribuire asupra componentelor lui x, deci nu s-a consumat nici o valoare, i se
ncearc atribuirea unei valori primei componente. Acest lucru este specificat
prin configuraia iniial, a crei form este:
X\ 3 3 xn
(j),...,<j)
n care toate mulimile Ck sunt vide.
Un alt caz special este cel al configuraiilor soluie, avnd forma:
Vi,...,Vn
Ci , . . . , Cn
cu semnificaia c vectorul (t>i , . . . , vn) este soluie a problemei. Astfel, pentru
Exemplul 2, configuraia:
A
B
C
D
{A}{A,B}{A,B,C}{A,B,C,D}
113

11.2. PREZENTAREA METODEI


are semnificaia c vectorul (A,B,C,D) constituie o soluie a problemei.
Metoda backtracking const n a porni de la configuraia iniial i a-i aplica
acesteia una dintre cele patru tipuri de transformri prezentate n continuare,
pn ce nu se mai poate aplica nici o transformare. Fiecare transformare se
aplic n anumite condiii bine precizate: la un moment dat doar o singur
transformare poate fi aplicat. Presupunem c ne aflm n configuraia descris
anterior, n care s-au atribuit valori primelor k 1 componente. Transformrile
care pot fi aplicate unei configuraii sunt: atribuie i avanseaz, ncercare eu
at, revenire, revenire dup construirea unei soluii. Le vom prezenta pe fiecare
pe rnd.
11.2.1

Atribuie i avanseaz

Acest tip de modificare are loc atunci cnd mai exist valori neconsumate
pentru componenta curent, Xk, (deci Ck C Vk), iar valoarea aleas, Vk, are
proprietatea c (v, . . . ,Vk) respect condiiile de continuare. In acest caz va
loarea t>k se atribuie lui Xk i se adaug mulimii Ck, dup care se avanseaz
la componenta urmtoare, Xk+i- Aceast modificare a configuraiei poate fi
reprezentat n felul urmtor:
Vk
,Vk-l

-,Vk-l,
vk
..,Ck-i,CkU{vk}

Xk j Xk+A i

Xk+A,
<j>,.

De exemplu, la generarea permutrilor, avem urmtoarea schimbare de con


figuraie pornind de la starea iniial:
A
A
{A}

Xi X2 X3 xa

11.2.2

#2 X3 Xa
<f> <t> <t>

ncercare euat

Acest tip de modificare are loc atunci cnd, ca i n cazul anterior, mai ex
ist valori neconsumate pentru componenta curent, Xk , dar valoarea Vk aleas
nu respect condiiile de continuare. n acest caz, Vk este adugat mulimii Ck
(deci este consumat), dar nu se avanseaz la componenta urmtoare. Modifi
carea este notat prin:
Vk
114 Ck-i

Xk i Xk+1 1
Ck, 4>,--

,Vk-l
, Ck-i

Xk i
Xk+A
Ck U {vk},(f>,

11.2. PREZENTAREA METODEI

n exemplul nostru cu generarea permutrilor, urmtoarea transformare este:

A
A
{A}

11.2.3

2 XS X4
<f> <t> <t>

A
{A}

2 XS X4
{A}(f> (f)

Revenire

Acest tip de transformare apare atunci cnd toate valorile pentru compo
nenta Xk au fost consumate (Ck = Vk), deci nu mai avem alte posibiliti de a
da valori acestei componente. n acest caz se revine la componenta precedent,
Xk-i , ncercndu-se atribuirea unei noi valori acestei componente. Este impor
tant de remarcat faptul c revenirea la Xk-i implic faptul c pentru Xk se vor
ncerca din nou toate variantele posibile, deci mulimea Ck trebuie din nou s
fie vid. Transformarea este notat prin:

-,Vk-l

Xk i Xfc--i ,
Ck, 4>,-

Xk 1 3 Xk 3
Ck-1, <t>,-

,Vk-2

O situaie de revenire, n exemplul cu generarea permutrilor este dat de


configuraia:

3
1
2
{1,2,3}{1}{1,2}

11.2.4

X4
{1,2,3,4}

3
1
{1.2,3}{1}

x3 x4
{1,2}^

Revenire dup construirea unei soluii

Acest tip de transformare se realizeaz atunci cnd toate componentele vec


torului au primit valori care satisfac condiiile interne, adic a fost gsit o
soluie. n aceast situaie se revine din nou la cazul n care ultima component,
xn, urmeaz s primeasc o valoare.
Transformarea se noteaz astfel:

,v

sol

; Un_l

Xn
115

11.2. PREZENTAREA METODEI

n exemplul nostru cu generarea permutrilor, revenirea dup gsirea primei


soluii este dat de diagrama:

12
3
4
{1}{1,2}{1,2,3}{1,2,3,4}

1
2
3
{1}{1, 2} {1,2, 3}

sol

X4
{1,2,3,4}

Revenirea dup construirea unei soluii poate fi considerat ca fiind un caz


particular al revenirii, dac adugm vectorului soluie x o component supli
mentar
care nu poate lua nici o valoare (V+i = 4>).
O problem important este cea a ncheierii procesului de cutare a solui
ilor, sau, cu alte cuvinte, ne putem pune ntrebarea: transformrile succesive
aplicate configuraiei iniiale se ncheie vreodat sau continu la nesfrit?
Evident c pentru ca metoda backtracking s constituie un algoritm trebuie s
respecte proprietile unui algoritm, printre care se afl i proprietatea de finitudine. Demonstrarea finitudinii algoritmilor de tip backtracking se bazeaz pe
urmtoarea observaie simpl: prin transformrile succesive de configuraie nu
este posibil ca o configuraie s se repete, iar numrul de elemente al produsului
cartezian V x V2 x . . . x Vn este finit. Prin urmare, la un moment dat se va
ajunge la configuraia:

Xi
Vi

x2
(f>

...
...

xr,
(f>

numit configuraie final. n configuraia de mai sus ar trebui s aib loc o


revenire (deoarece toate valorile pentru prima component au fost consumate),
adic o deplasare a barei verticale la stnga. Acest lucru este imposibil, i al
goritmul se ncheie deoarece nici una din cele patru transformri nu poate fi
aplicat. n practic, aceast ncercare de a deplasa bara de pe prima poziie
(k = 1) pe o poziie anterioar (k = 0) este utilizat pe post de condiie de
terminare a algoritmului.
nainte de a trece la implementarea efectiv a metodei backtracking n pseudocod, s generm diagramele de configuraie pentru Exemplul 1:
116

1 1 .3. IMPLEMENTAREA METODEI BACKTRACKING


f\ xi x2 \ A /

M
A M
{A}{M}

^2

sol I

A
{A}

X2
{M}

M
A
{A}

X2
{M,N}

B
{A,B}

Xi X2
{A}<fi

B M
{A,B}{M}

x2

N
B
{A,B}

x2
{M}

C
{A,B,C}

X2 \

B
{A,B}

x2
{M,N}

Xi X2
{A,B}cf>

M
* J

C
N
{A,B,C}{M,N}
11.3

N
C
M
/
{A,B,C}
{M}
*l

sol

C
{A,B,C}

sol l

X2
{M,N}

C
{A,B,C}

x2
{M}

Xi
X2
{A,B,C}cf>

Implementarea metodei backtracking

Procesul de obinere a soluiilor prin metoda backtracking este uor de pro


gramat, deoarece la fiecare pas se modific foarte puine componente (indicele
k, reprezentnd poziia barei, componenta Xk i mulimea
Algoritmul
(scris n pseudocod) de mai jos construiete configuraia iniial, dup care
aplic una dintre cele patru transformri descrise n paragraful anterior, pn
cnd se ajunge la configuraia final (adic pn cnd se ncearc o revenire de
pe prima poziie):
Iniializeaz (citete) mulimile de valori, V\,...,Vn
k <r- 1 / /se construiete configuraia iniial
pentru i = 1, n
Ci ^<j>
//acum ncepe efectiv aplicarea celor 4 transformri, funcie de caz
ct timp > 0 / /k = 0 nseamn terminarea cutrii
dack = n + 1 atunci / /configuraia este tip soluie
reine soluia Vi, . . . ,vn
k
k 1 // revenire dup soluie
altfel dacCk ^ Vk atunci / /mal exist valori neconsumate
117

1 1 .3. IMPLEMENTAREA METODEI BACKTRACKING


alege o valoare Vk din Ck Vjt
Cfc = Ck U Vk I /valoarea Vk este consumat
dacvi, ,Vk respect condiiile de continuare atunci
%k
Vu', IIatribuie i
k
k + 1; / /avanseaz
altfel I /ncercare euat, nu fac nimic
altfel 1 1revenire
Ck <- 4>,k <- k - 1
sfrit ct timp

Algoritmul de mai sus funcioneaz pentru cazul cel mai general, dar este
destul de dificil de programat din cauza lucrului cu mulimile Ck i Vk Din
fericire, adeseori n practic mulimile Vk au forma
Vk = {1,2,. ..,*}
deci fiecare mulime Vk poate fi reprezentat foarte simplu prin numrul su
de elemente, Sk- Pentru a simplifica i mai mult lucrurile, n cadrul procesului
de construire a unei soluii vom alege valorile pentru fiecare component Xk n
ordine cresctoare, pornind de la 1 i pn la Sk- In aceast situaie, mulimea
de valori consumate Ck va fi de forma {1, 2, . . . , Vk} i, drept consecin, va
putea fi reprezentat doar prin valoarea t>k
Consideraiile de mai sus permit nlocuirea algoritmului anterior, bazat pe
mulimi, cu un algoritm simplificat care lucreaz numai cu numere.
Dac n Exemplul 1 vom conveni s reprezentm pe A, B, C prin valorile
1, 2, 3, iar pe M i N prin 1 i 2, configuraiile care se succed n procesul de
cutare pot fi reprezentate simplificat astfel:

(|

1
l 2 ) _^ ( 1

1
2 ) _^ ( 1
etc

|)^(1

X2 )

Particularizarea algoritmului pseudocod prezentat anterior se concretizeaz


n urmtoarea metod Java:
Listing 11.1: Metoda backtracking
1 public void backtracking ()
2{
3
int k = 0;
i
while (k >= 0)
{
118

1 1 .4. PROBLEME CLASICE REZOLVABILE PRIN BACKTRACKING


6

if (k == n) // am gsit o soluie
!
retSol();// afi sam s o lut ia
k
; //revenire dupa gsirea unei soluii

8
9
ii

else
!

13

if (x[k] < s[k]) //mai sunt valori neconsumate


{
x[k]+ + ; //se ia urmtoarea valoare
if (continuare(k)) //respecta cond . de cont?
!
k + + ; //avanseaz
}
}
else
{
x[k
] = 0;// revenire
}

15
ie
s
20
21
22
23
}
}
27 }

Metoda backtr acking ( ) folosete nc dou metode:


metoda ret Sol ( ) , care, aa cum sugereaz i numele ei, reine soluia,
constnd n valorile vectorului x. Cel mai adesea aceast metod realizea
z o simpl afiare a soluiei i, eventual, o comparare cu soluiile gsite
anterior;
metoda continuare (k) verific dac valorile primelor k componente
ale vectorului x satisfac condiiile de continuare; n cazul afirmativ este
returnat valoarea true, iar n caz contrar este returnat valoarea false.

11.4

Probleme clasice rezolvabile prin backtracking

11.4.1

Problema generrii permutrilor

Se d mulimea A cu elementele {a, 2, ,


toate permutrile elementelor acestei mulimi.

S se genereze

Se observ c aceast problem este o simpl generalizare a Exemplului 2


din acest capitol. Mai mult dect att, problema poate fi redus la a gene
ra permutrile mulimii de indici {1, 2, . . . , n}. In aceast situaie vom avea
119

1 1 .4. PROBLEME CLASICE REZOLVABILE PRIN BACKTRACKING

i
^
3
5
7
io
"

Listing 1 1 .2: Funcia de continuare pentru problema permutrilor


public boolean continuare ( int k)
(
for (int i = 0; i < k; ++i)
{
if (X[k] == X[i ])
{
return false ;
}
}
return true ;
)

Vi = Vi = = Vn = {1, 2, . . . , n}, deci putem aplica varianta simplificat a


metodei backtracking.
Condiiile interne pe care trebuie s le respecte un vector soluie sunt:
Xi ^ Xj pentru Vi, j = 1, n,

i ^ j.

Condiiile de continuare pentru componenta numrul k sunt o simpl res


tricie a condiiilor interne:
Xi 7^ Xu pentru Vi = 1, k 1.
Codul pentru funcia de continuare este prin urmare foarte simplu, dup cum
reiese i din Listing 11.2.
Metoda backtracking pentru generarea permutrilor se obine din metoda
backtracking pentru cazul general, nlocuind numrul de elemente al mulimi
lor T4(notat cu
prin valoarea n. Soluia complet a generrii permutrilor
este prezentat n Listing 11.4. Pentru a citi mulimea de elemente care vor
fi permutate am folosit clasa ajuttoare Re ader, prezentat n volumul nti,
capitolul 4, paragraful Variabila sistem CLASSPATH. Reamintim nc o dat
codul surs al acestei clase.
Listing 11.3: Clasa Reader
1 package io ;
2 //clasa va trebui salvata intrun director cu numele "io"
3 //directorul in care se afla "io " va trebui adugat
4 //in CLASSPATH
5 import java . io . * ;
6 import j ava . u t i 1 . S trin gTokeni zer ;
7 public class Reader
{
120

1 1 .4. PROBLEME CLASICE REZOLVABILE PRIN BACKTRACKING


9
11
12
13
is
i7
19
20
21
22
23
24
26
28
29
30
31
33
34
36
37
3s
39
40
41
42
43
44
45
46
49
si
52
53
54
55
56
5s

public static String readString()


!
BufferedReader in = new BufferedReader (
new InputStreamReader ( System . in )) ;
try
!
return in . readLine ( ) ;
}
catch ( IOException e)
(
//ignore
}
return null ;
}
public static int readlnt()
!
return I n t e g e r . p ars el n t ( read S t rin g ( ) ) ;
}
public static double readDouble ()
{
return Double .parseDouble(readString ());
}
public static char readChar ()
!
BufferedReader in = new BufferedReader (
new InputStreamReader ( System . in )) ;
try
{
return ( char ) in . read () ;
}
catch ( IOException e)
{
// ignore
}
return ' \0 ' ;
}
public static int [] readlntArray ()
!
String s = readS t rin g ( ) ;
StringTokenizer st = new StringTokenizer ( s ) ;
//aloca memorie pentru sir
int[] a = new int [ st . countTokens ( ) ] ;
for (int i = 0; i < a.length;++i)
!
a[i] = I n t e ge r . p ar s el n t ( s t . nextToken ( ) ) ;
121

1 1 .4. PROBLEME CLASICE REZOLVABILE PRIN BACKTRACKING


}
60
6i

return a;
}

63
64 }
Prin rularea programului urmtor se vor genera permutrile mulimii intro
duse de la tastatur.
Listing 11.4: Rezolvarea problemei permutrilor
i import io . Reader ;
2
3 /* *
4* Program care genereaz permutrile unei mulimi
5* introduse de la tastatura .
6 */
7 public class Permutri
8!
9 /** Testeaz daca elementul adugat exista deja.*/
10 public static boolean continuare(int[] x, int k)
>' !
12
for (int i = 0; i < k; ++i)
{
if (x[k] == x[i ])
{
i6
return f al s e ;
}
}
19
20
return true ;
22
23 /** Construiete un string care conine soluia curenta.*/
24 public static void retSol(int[] s , int[] x, int nrSol)
* !
26
System . out . p ri n t (" Permutarea " + nrSol + ": " );
27
for (int i = 0; i < s.length; i++)
28
{
29
System . out . prin ( s [x [ i ] 1] + " ");
30
}
31
System . out . println ( ) ;
32 }
33
34 /* *
35 * Backtracking standard pentru determinarea
36 * permutrilor mulimii .
37 */
38 public static void backtracking (int [] s)
39 (
122

1 1 .4. PROBLEME CLASICE REZOLVABILE PRIN BACKTRACKING


40
41
42
43
44
45
46

in t k = 0 ;
//aloca memorie pentru irul de indici
int[] x = new int [ s . length ] ;
int nrSol = 0;
//iniializeaz x
for (int i = 0; i < x. length ; i ++)
!
x[i ] = 0;
}

4s
50
51
52

//procesul de backtracking
while (k >= 0)
{
if ( k = = x . length) //am gsit o soluie
{
retSol(s,x, + +nrSol) ; // afieaz soluia
k
; // revenire dupa ce o soluie a fost gsita

54
56
57
59
60
61

el se
{
if (x[k] < x. length) //valori neconsumate?
!
//se ia urmtoarea valoare neconsumata
x [k ] ++ ;

63
64
65
66
67
68
69
70

//respecta valoarea aleasa


//condiia de continuare?
if (continuare(x, k ) )
{
k + + ; //avanseaz
}
}
e1se
!
x[k
}

73
75

] = 0; //revenire

}
}
79

si
82
B3
84
85
86
87

/** Programul principal.*/


public static void main ( String [] args )
{
//citirea elementelor mulimii
System . out . p ri n 1 1 n (" I ntro duc e ti elementele mulimii " +
"(pe o linie, separate prin spaiu):");
int[] s = Reader.readlntArrayO;

89

//generarea permutrilor mulimii


123

1 1 .4. PROBLEME CLASICE REZOLVABILE PRIN BACKTRACKING


90
backtrackin g ( s ) ;
91 }
92 }

11.4.2

Generarea aranjamentelor i a combinrilor

Vom vedea acum ct de simplu se poate adapta algoritmul de generare a


permutrilor unei mulimi pentru a genera aranjamentele i combinrile acelei
mulimi. Pentru a simplifica lucrurile, vom presupune c mulimea A este for
mat din primele n numere naturale, adic A = {1,2, . . . ,n}.
Reamintim faptul c prin aranjamente de n luate cte m (n > m), notate
A se neleg toate mulimile ordonate cu m elemente formate din elemente ale
mulimii A, cu alte cuvinte toi vectorii de forma:
x = (xi, ... ,xm), unde Xi {1,2, ... ,n},

Xi^Xj,

Vi,j = l,m

Se observ c, din punct de vedere al reprezentrii formale, singura diferen


dintre aranjamente i permutri este c aranjamentele au lungime m n loc de
n. De altfel, pentru m=n aranjamentele i permutrile coincid.

Exemplu:

Aranjamentele de 3 luate cte 2 (A\) sunt:


(1,2), (1,3), (2,1), (2,3), (3,1), (3,2).

Condiiile interne i, n consecin, condiiile de continuare sunt identice


cu cele de la generarea permutrilor. Prin urmare i funcia de continuare este
identic cu cea de la permutri. Unde este totui diferena? Avnd n vedere
c lungimea vectorului este m i nu n, condiia de gsire a unei soluii trebuie
adaptat. Prin urmare, n metoda backtracking ( ) linia:
if (k == n)
va fi nlocuit cu:
if (k == m)
Desigur, aceeai modificare este necesar i n metoda ret Sol ( ) , n care
secvena
for (int i =0; i <n;++i)
se va nlocui cu
for (int i = 0; i < m; ++i)
124

1 1 .4. PROBLEME CLASICE REZOLVABILE PRIN BACKTRACKING

1
2
3
5
7
9
"

Listing 1 1 .5: Funcia de continuare pentru combinri


public boolean continuare ( int k)
{
if (k > 0 && x[k] <= x[k - 1])
!
return false ;
)
else
!
return true ;
}
}

S vedem acum modalitatea de generare a combinrilor. Reamintim c prin


combinri de n luate cte m (notat C) se neleg toate submulimile cu m
elemente ale mulimii A = {1,2, . . . ,n}.
Exemplu:

Combinrile de 3 luate cte 2 (Cf ) sunt:


(1,2), (1,3), (2,3).

Diferena ntre combinri i aranjamente este dat de faptul c, n cazul


combinrilor, ordinea n care apar componentele nu conteaz (combinarea (1 ,2)
este aceeai cu combinarea (2,1) etc). Din acest motiv am optat n exemplul de
mai sus s aranjm componentele unei combinri n ordine cresctoare). Prin
urmare, combinrile unei mulimi cu n elemente luate cte m sunt definite de
vectorii:
x = (xi, . . . ,xm), unde X\ < x2 < < xm.
Condiia de continuare n cazul combinrilor va fi pur i simplu:
Xk > Xk-i pentru k > 1.
Metodele backtr acking ( ) i retSol ( ) sunt, n cazul combinrilor,
identice cu cele de la aranjamente. Diferena apare la funcia de continuare,
descris n Listing 11.5. Listing 11.6 prezint varianta mai elegant a aceleiai
funcii.
Problema 3 de la finalul capitolului propune o variant mai eficient de ge
nerare a combinrilor n care funcia de continuare este complet eliminat.
125

1 1 .4. PROBLEME CLASICE REZOLVABILE PRIN BACKTRACKING

i
^
3
4

Listing 1 1 .6: Funcia de continuare pentru combinri (varianta elegant)


public boolean continuare ( int k)
(
return k == 0 II x[k] > x[k - 1];
}

Figura 11.1: Soluie de aezare a damelor pe tabla de ah


X
X
X
X

11.4.3

Problema damelor

S se aeze n dame pe o tabl de ah de dimensiune nxn astfel


nct damele s nufie pe aceeai linie, aceeai coloan sau aceeai
diagonal (damele s nu se atace ntre ele).
Reamintim c n jocul de ah, o dam "atac" poziiile aflate pe aceeai linie
sau coloan i pe diagonal. O posibil aezare a damelor pe o tabl de ah de
dimensiuni 4x4 este dat n Figura 11.1.
S vedem cum putem reformula problema damelor pentru a o aduce la o
problem de tip backtracking . Se observ cu uurin c pe o linie a tablei de
ah se poate afla o singur dam, prin urmare putem conveni c prima dam se
va aeza pe prima linie, a doua dam pe a doua linie etc. Rezult c pentru a
cunoate poziia damei numrul k este suficient s tim coloana pe care aceasta
se gsete. O soluie a problemei se poate astfel reprezenta printr-un vector
x= (x1,x2,...,xn),

xk {1, 2, . . . , n},

unde Xk reprezint coloana pe care se gsete dama numrul k.


Cu aceast notaie, vectorul soluie corespunztor exemplului din Figura
11.1 este: (2,4,1,3).
S vedem acum care este condiia ca dou dame distincte, k i i, s se atace:
n mod cert damele nu pot fi pe aceeai linie;
damele sunt pe aceeai coloan dac Xk = Xf,
126

1 1 .4. PROBLEME CLASICE REZOLVABILE PRIN BACKTRACKING

1
2
3
5
7
io

Listing 1 1 .7: Funcia de continuare pentru problema damelor


public boolean continuare ( int k)
{
for (int i =0; i <k;++i)
!
if (x[i] == x[k] II k-i == Math. abs (x[k]-x[ i ]))
!
return false ;
)
}
return true ;
)

damele sunt pe aceeai diagonal dac se afl pe colurile unui ptrat,


adic lungimea (\xk Xi\) este egal cu limea (k i):
\Xk

%i\ = \k

^|

Condiia de continuare este ca dama curent, k, s nu atace nici una dintre


damele care deja sunt aezate pe tabl, adic:
Xk 7^ %i i \xk Xi\ ^ \k i\ pentru Vi = 1, k 1.
Transpus n Java, funcia de continuare are forma din Listing 11.7.
Modificrile care trebuie aduse metodelor retSol i backtracking
sunt minime i le lsm ca exerciiu.
Observaie: Problema damelor este primul exemplu de problem n care
condiiile de continuare sunt necesare, dar nu sunt suficiente. De exemplu (pen
tru n=4), la nceput, algoritmul va aeza prima dam pe prima coloan, a doua
dam pe a treia coloan, iar cea de-a treia dam nu va putea fi aezat pe nici o
poziie, fiind necesar o revenire.

11.4.4

Problema colorrii hrilor

Se d o hart ca cea din figura de mai jos, n care sunt reprezen


tate schematic 6 ri, dintre care unele au granie comune. Pre
supunnd c dispunem doar de trei culori (rou, galben, verde),
se cere s se determine toate variantele de colorare a hrii ast
fel nct oricare dou ri vecine (care au frontier comun) s fie
colorate diferit.
127

1 1 .4. PROBLEME CLASICE REZOLVABILE PRIN BACKTRACKING

Figura 1 1 .2: Matricea de vecinti corespunztoare hrii din partea stng


0 110 0 1
10 1110
110 10 1
0 110 11
0 10 10 1
10 1110

Pentru a memora relaia de vecintate ntre dou ri vom utiliza o matrice


de dimensiuni 6x6, numit vecin, definit prin:
vecin[i,j]

true
false

dac rile Tj i Tj sunt vecine


altfel

Figura 11.2 reprezint matricea de vecinti pentru harta cu 6 ri n care


s-a fcut convenia c 1 reprezint true i 0 reprezint/a/se.
Problema se poate generaliza uor i la o hart cu n ri care trebuie colorat
cu m culori. Vom utiliza pentru uurarea expunerii harta cu 6 ri de mai sus.
In aceast problem, un vector soluie x = (x, X, . . . , xn) reprezint o
variant de colorare a hrii, avnd semnificaia c ara numrul i va fi colorat
cu culoarea xi . n exemplul nostru, xi poate fi 1 ,2 sau 3 , corespunznd respectiv
culorilor rou, galben, verde.
Condiia de continuare este ca ara creia urmrim s i atribuim o culoare,
s aib o culoare distinct de rile cu care are grani. Cu alte cuvinte, trebuie
s avem Xi ^ Xk dac A[i, k] = 1, Vi = 1, k 1.
Funcia de continuare care descrie condiia de mai sus este dat n Listing
11.8.
Pentru o mai bun nelegere a mecanismului metodei backtracking aplicat
la problema colorrii hrilor, putem s ne imaginm c dispunem de 6 cutii
identice Vi , V2, , Ve, fiecare dintre cutii coninnd trei creioane colorate no
tate cu r - rou, g - galben, v - verde. Fiecare cutie Vju conine creioanele care
pot fi utilizate pentru colorarea rii T\. .
O vizualizare a procesului de cutare a soluiilor poate fi obinut dac aran
jm cele 6 cutii n ordine (fiecrei ri i asociem o cutie) i punem un semn
naintea cutiei din care urmeaz s se aleag un creion (marcajul corespunde
barei verticale de la configuraii); iniial acest semn este n stnga primei cutii.
Atunci cnd se alege un creion dintr-o cutie corespunztoare unei ri el va fi
aezat fie pe ara respectiv, dac nu exist o ar vecin cu aceeai culoare,
128

1 1 .4. PROBLEME CLASICE REZOLVABILE PRIN BACKTRACKING

1
2
3
5
7
io
"

Listing 11.8: Funcia de continuare pentru problema colorrii hrilor


public boolean continuare ( int k)
{
for (int i =0; i <k;++i)
!
if (x[i] == x[k] && vecin [k][i] == 1)
!
return false ;
)
}
return true ;
}

fie lng cutie n caz contrar. Astfel, mulimile Ci de valori consumate la un


moment dat sunt alctuite din creioanele de lng cutia Vi i de pe ara Tj. Cu
aceste precizri, cele 4 modificri de configuraie au urmtoarele semnificaii
concrete:
atribuie i avanseaz: se aeaz creionul ales pe ara corespunztoare i
se trece la cutia urmtoare;
ncercare euat: creionul ales este aezat lng cutia din care a fost scos;
revenire: creioanele corespunztoare rii curente sunt repuse n totalitate
la loc n cutie i se trece la cutia precedent;
revenire dup gsirea unei soluii: semnul este adus la stnga ultimei
cutii.
Procesul se ncheie n momentul n care toate creioanele ajung din nou n cutiile
n care se aflau iniial.

Rezumat
In acest capitol am prezentat metoda bactracking, care se reduce n esen
la parcurgerea exhaustiv a spaiului de cutare, n care se elimin cu grij
configuraiile care nu pot conduce la o soluie. Metoda backtracking se aplic
oricrei probleme a crei soluie se poate scrie sub form de ir, cu fiecare element al irului lund valori n cadrul unei mulimi finite. Elementul estenial
care determin eficiena cutrii este reprezentat de condiiile de continuare care
129

1 1 .4. PROBLEME CLASICE REZOLVABILE PRIN BACKTRACKING


sunt utilizate pentru a reduce dimensiunile spaiului de cutare. n modelul teo
retic al acestei metode, se pornete de la o configuraie iniial, creia i se aplic
succesiv anumite transformri pn n momentul n care se ajunge la configu
raia final. n practic, rezolvarea unei probleme prin aceast metod se reduce
cel mai adesea la scrierea funciei de continuare (care este o implementare a
condiiilor de continuare) pentru problema concret creia i se aplic.
Noiuni fundamentale
backtracking: metod de elaborare a algoritmilor care const n constru
irea soluiei component cu component, cu eventuale reveniri asupra compo
nentelor anterioare.
condiie intern: condiia pentru ca un ir s reprezinte o soluie a proble
mei.
condiie de continuare: condiie necesar pentru ca un ir parial s poat
conduce la o soluie.
configuraie final: configuraie creia nu i se mai poate aplica nici una
dintre cele patru transformri i care indic ncheierea procesului de cutare.
configuraie iniial: configuraia de la care se pornete n procesul de
cutare al soluiilor.
diagram de stare: diagram care sintetizeaz starea n care se afl proce
sul de cutare la un anumit moment.
soluie: un ir care respect condiiile interne.
Erori frecvente
1 . Exist adeseori situaii n care soluia unei probleme nu se poate scrie di
rect sub form de vector, ci trebuie fcute anumite convenii pentru a o re
duce la un vector. Aadar, backtracking se poate aplica oricrei probleme
a crei soluie se poate reduce sau transforma ntr-un vector. Muli pro
gramatori nceptori renun la a ncerca s aplice aceast metod dac
soluia ei nu este n mod evident structurat sub form de vector, fr
a-i pune problema de a transforma soluia ntr-o astfel de form (vezi
problema damelor din paragraful 1 1 .4.3, n care soluia se reduce de la o
matrice (tabla de ah) la un ir de numere).
2. Dei metoda backtracking se poate aplica oricrei probleme a crei soluie
se poate scrie sub form de ir, aceasta nu nseamn c backtracking este
i metoda cea mai eficient de a o rezolva. De exemplu, i n cazul pro
blemei sortrii soluia se scrie sub form de ir, deci se poate aplica back
tracking. Totui aplicarea lui backtracking n aceast situaie ar nsemna
130

1 1 .4. PROBLEME CLASICE REZOLVABILE PRIN BACKTRACKING


o generare optimizat a permutrilor elementelor acelui ir, ceea ce ar
conduce la o soluie exponenial, deci inutilizabil. O s prezentm n
capitolul urmtor metode eficiente de ordonare a unui ir.
3. Adeseori funcia de continuare nu este optim aleas, n aa fel nct s
elimine ct mai multe configuraii nefezabile.
4. Se confund adeseori dimensiunea spaiului de cutare (notat de noi cu
n) cu dimensiunea individual a fiecrei mulimi Vk (notat de noi su
s[k]). Este adevrat ca la multe probleme (de exemplu, permutri) aces
tea sunt egale, dar la altele (cum ar fi colorarea hrilor, combinri, aran
jamente), ele difer.
Exerciii
Teorie
1 . Enumerai problemele prezentate n cadrul acestui capitol, pentru care
condiiile de continuare sunt necesare, dar nu sunt suficiente.
2. Luai o tabl de ah obinuit i ncercai s simulai modul n care se
genereaz soluia problemei damelor pentru n = 8.
3. Gsii toate soluiile de colorare cu trei culori a hrii din Figura 11.2.
n practic
1 . S se afieze toate modurile n care n persoane pot fi aezate la o mas
rotund precum i numrul acestora.
Indicaie: Exist dou posibiliti de rezolvare:
(a) Se vor genera toate permutrile posibile, prin metoda backtracking,
i se vor contoriza. Se va afia apoi numrul lor. Va trebui ns s
inei seama de faptul c unele dispuneri sunt identice din cauza
mesei circulare;
(b) Mult mai elegant, folosind o observaie simpl. Astfel, cu n obiecte
se pot forma n\ permutri. Cum, n cazul dispunerii lor circu
lare 1,2, ... ,n, respectiv 2, 3, . . . , n, 1; . . .; n, 1, 2, . . . , n 1 sunt
identice, rezult c din n astfel de permutri trebuie considerat
doar cea care ncepe cu 1. Numrul de permutri va fi aadar
= ("-!)!
131

1 1 .4. PROBLEME CLASICE REZOLVABILE PRIN BACKTRACKING


2. Idem problema 1 , cu precizarea c anumite persoane nu se agreeaz, deci
nu pot fi aezate una lng cealalt. La intrare se mai furnizeaz o matrice
simetric A, cu urmtoarea semnificaie:
1

dac nu se agreaz
altfel

3. S se modifice algoritmul de generare a combinrilor prezentat n para


graful 1 1 .4.2 astfel nct funcia de continuare s nu mai fie necesar.
Indicaie: Pentru fiecare component x[k] se pornete cu valoarea x[k
1] + 1.
4. Se dau n mulimi A, A%, . . . , An. S se afieze produsul lor cartezian.
Indicaie: Generarea produsului cartezian nseamn de fapt generarea
ntregului spaiu de soluii, adic un backtracking n care funcia de con
tinuare lipsete.
5. Se d o mulime A = {1,2, ... ,n}. S se afieze toate submulimile
acestei mulimi.
Indicaie: Se genereaz toi vectorii caracteristici de lungime n. Prin
vector caracteristic se nelege un vector care are doar valorile 1 sau 0
pentru fiecare element cu semnificaia:

dac i aparine submulimii


altfel
Exist i o soluie care genereaz vectorii caracteristici de lungime n prin
adunarea n baza 2. Iniial vectorul este nul, corespunztor mulimii vide,
iar apoi prin adunri repetate se genereaz toate submulimile. Atenie,
numrul total de submulimi este 2n!
6. O firm dispune de n angajai, dintre care p sunt femei. Firma trebuie s
formeze o delegaie de m persoane, dintre care k sunt femei. S se afieze
toate delegaiile care se pot forma.
Indicaie: Pentru a forma o delegaie de k femei din p disponibile avem
la dispoziie C variante. Delegaia de m persoane poate fi completat
132

1 1 .4. PROBLEME CLASICE REZOLVABILE PRIN BACKTRACKING


cu oricare din variantele de C_pfc de alegere a brbailor din delegaie.
Aadar numrul total de variante este
* CSpGenerarea efectiv se bazeaz pe un vector caracteristic cu semnificaia:

X\l\ ^ i\
{i

dac persoana e femeie


j+ r j
altfel

Funcia de continuare va numra femeile din delegaie i nu va lsa ca


numrul lor s-l depeasc pe k.
7. Se consider mulimea A = {1,2, ... ,n}. S se furnizeze toate partiiile
acestei mulimi. (O partiie a unei mulimi este o scriere a mulimii ca
reuniune de submulimi disjuncte).
Indicaie: Vom genera partiia sub forma unui vector cu n componente
n care x[i] = k are semnificaia c elementul i aparine submulimii
k a partiiei considerate. Ca exemplu, pentru n = 4 putem avea, la
un moment dat, vectorul x = (1, 2, 1, 2) ceea ce corespunde partiiei:
A = {1, 3} U {2, 4}. Arfi de remarcat c vectorul caracteristic poate lua
valori care vor avea aceeai interpretare, ca de exemplu x = (2, 1, 2, 1)
ceea ce corespunde partiiei: A = {2, 4} U {1, 3}. Dar reuniunea e co
mutativ i partiia astfel obinut e identic cu cea anterioar. Pentru a
evita acest lucru vom impune cafiecare component a vectorului s poat
avea cel mult valoarea k, unde k este indicele elementului. Semnificaia
arfi c elementul cu indicele 1 va putea face parte doar din submulimea
1, cel cu indicele 2 doar din submulimile 1 i 2 etc. O alt restricie arfi
aceea c un element nu poate lua o valoarea mai mare ca max + 1, unde
max este valoare maxim a elementelor de rang inferior. Acest lucru se
justific prin faptul c x = (1, 1, 3, 1) nu ar avea nici o semnificaie.
8. Un comis-voiajor trebuie s viziteze un numr n de orae pornind din
oraul numrul 1 . El trebuie s viziteze fiecare ora o singur dat, dup
care s se ntoarc n oraul 1 . Cunoscnd legturile existente ntre orae,
se cere s se gseasc toate rutele posibile pe care le poate efectua comisvoiajorul.
Indicaie: Se va crea o matrice de adiacen (cunoscut din teoria grafurilor), care este o matrice simetric:
133

1 1 .4. PROBLEME CLASICE REZOLVABILE PRIN BACKTRACKING

1
0

dac exist legtur ntre oraele i i j


altfel

Funcia de continuare va testa dac la elementul actual se poate ajunge


din anteriorul, n vectorul x. Ca observaie trebuie spus c pentru a
obine soluiile distincte trebuiefcut un artificiu asemntor cu cel de la
problema anterioar.
9. Idem problema anterioar, cu precizarea c pentru fiecare drum ntre dou
orae se cunoate distana care trebuie parcurs. Se cere s se gseasc
ruta de lungime minim.
Indicaie: In momentul reinerii soluiei se va calcula lungimea drumului
parcurs. Se va compara aceast lungime cu lungimea anterioar con
siderat minim i se va reine valoarea actual minim mpreun cu
drumul parcurs.
Aceast problem este celebr prin faptul c este un exemplu pentru im
posibilitatea aflrii soluiei exacte n timp polinomial. Datorit complex
itii mari a metodei s-au gsit metode mai puin complexe (metodele
euristice) dar care dau o soluie cu o marj de aproximare.
10. Presupunem c avem de pltit o sum s i avem la dispoziie un numr ne
limitat de bancnote i monede de valoare V\ , 1/2, . . . , vn. S se furnizeze
toate variantele de plat a sumei utiliznd numai aceste monezi.

1 1 . Idem problema anterioar, cu precizarea c trebuie s pltim suma res


pectiv cu un numr ct mai mic de monede i bancnote.
Indicaie: Fa de rezolvarea problemei anterioare se poate face, spre
exemplu, o modificare care s compare, n momentul gsirii unei soluii,
numrul de monede cu cel gsit la soluiile anterioare.
12. Idem problema anterioar pentru cazul n care dispunem doar de un numr
rt\ , 712 , . . , nn de monede de valoare V\ , Vi , . . . , vn .
Indicaie: Deosebirile fa de problemele anterioare sunt:
* n vectorul soluie vom reine numrul de monede sau bacnote folosite,
nu i valoarea lor. Astfel, fiecrui nivel i corespunde o anumit valoare;
134

1 1 .4. PROBLEME CLASICE REZOLVABILE PRIN BACKTRACKING


* vom avea grij ca fiecare component a vectorului soluie s nu de
peasc numrul de monede existent din valoarea care i corespunde;
* sumele se vor calcula prin cumularea produselor dintre valoare i nu
mrul de valorifolosite.
1 3 . Fiind dat un numr natural n, s se genereze toate partiiile sale. O partiie
a unui numr reprezint scrierea sa ca sum de numere naturale nenule.

14. Fiind dat un numr natural n, s se genereze toate descompunerile sale ca


sum de numere prime.
Indicaie: Fa de problema anterioar se poate verifica, la continuare,
dac numrul ales este prim.
15. O fotografie alb-negru este reprezentat sub forma unei matrice cu ele
mente 0 sau 1 . n fotografie sunt reprezentate unul sau mai multe obiecte.
Poriunile corespunztoare obiectelor au valoarea 1 n matrice. Se cere s
se determine dac fotografia reprezint unul sau mai multe obiecte.
Exemplu: Matricea de mai jos reprezint dou obiecte:

/ 0 1
10
0 0

1 0 \
0 0
11

V 1

0 )

16. Un teren dreptunghiular este mprit n m x n parcele reprezentate sub


forma unei matrice A cu m linii i n coloane. Fiecare element al matricei
este un numr real care reprezint nlimea parcelei respective. Pe una
dintre parcele se afl plasat o bil. Se cere s se furnizeze toate posibi
litile prin care bila poate s prseasc terenul, cunoscut fiind faptul c
bila se poate rostogoli numai pe parcele nvecinate a cror nlime este
strict inferioar nlimii parcelei pe care bila se afl.
Indicaie: Aceasta i problema anterioar sunt cazuri tipice de backtracking n plan. Ideea rezolvrii const n ncercarea de a ajunge la o
poziie vecin respectnd condiiile problemei. Modalitatea de micare
este dat de cele 8 direcii cardinale N, NV, V, SV, S, SE, E, NE. Practic,
vectorul soluie va conine direcia n care s-a fcut deplasarea.
135

1 1 .4. PROBLEME CLASICE REZOLVABILE PRIN BACKTRACKING


17. Pe o tabla de ah de dimensiune 8 x 8 s se poziioneze n pioni dup
urmtoarele reguli:
(a) Pe fiecare linie se afl doi pioni;
(b) Pe fiecare coloan se afl cel mult doi pioni;
(c) Pe fiecare paralel la diagonala principal se afl cel mult doi pioni.

136

12.Divide et impera

Vom gasi o cale, iar daca nu exista,


vom crea una.
Hanibal

n acest capitol vom studia o alta metoda fundamentala de elaborare a al


goritmilor, numita divide et impera. Ca si backtracking, divide et impera se
mai
bazeazape
multe) unprincipiuextrem
subprobleme de dimensiuni
de simplu:
mai
descompunemprobleman
mici, rezolvam subproblemele,
doua(sau
iar
solutia pentru problema initiala se obtine combinnd solutiile subproblemelor
n care a fost descompusa. Rezolvarea subproblemelor se face n acelasi mod
cu problema initiala. Procedeul se reia pna cnd subproblemele devin att de
simple nct admit o rezolvare imediata.
nca din descrierea globala a acestei tehnici s-au strecurat elemente de recursivitate. Pentru a putea ntelege mai bine aceasta metoda de elaborare a al
goritmilor care este eminamente recursiva, vom prezenta pentru nceput cteva
elemente fundamentale referitoare la recursivitate. Continuam apoi cu prezen
tarea generala a metodei, urmata de rezolvarea anumitor probleme de Divide
et Impera deosebit de importante: cautare binara, sortarea prin interclasare,
sortarea rapida si evaluarea expresiilor aritmetice.
n cadrul acestui capitol vom prezenta:
Ce este recursivitatea si care este mecanismul prin care ea functioneaza;
Cnd se poate aplica metoda divide et impera si care este forma ei gene
rala;
Cum se aplica aceasta metoda pentru a rezolva ecient problema or
donarii.
137

12.1. INTRODUCERE N RECURSIVITATE

12.1

Introducere n recursivitate

n acest paragrafvom reaminticteva elemente esentiale referitoarela recur


sivitate. Cei care stapnesc deja acest mecanism, pot trece direct la prezentarea
metodei Divide et Impera din paragraful urmator.
Avnd n vedere faptul ca recursivitatea este un mecanism de programare
general, care nu tine doar de limbajul Java, prezentarea facuta va folosi si
limbajul pseudocod la descrierea algoritmilor recursivi, pentru a nu ncarca
prezentarea cu detalii de implementare.

12.1.1 Functii recursive


Recursivitatea este un concept care deriva n mod direct din notiunea de
recurenta matematica. Recursivitatea este un instrument elegant si puternic pe
care programatorii l au la dispozitie pentru a descrie algoritmii. Este interesant
de retinut faptul ca programatorii obisnuiau sa utilizeze recursivitatea pentru a
descrie algoritmii, cu mult nainte ca limbajele de programare sa suporte imple
mentarea directa a acestui concept.
Din punct de vedere informatic, o subrutina (procedura1 sau functie) recursiva este o subrutina care se autoapeleaza.
Sa luam ca exemplu functia factorial, a carei denitie matematica recurenta
este:
w(Ef

kw(E

'

pentru
pentru

h


Din exemplul de mai sus se observa ca factorialul este denit functie de el


nsusi, dar pentru o valoare a parametrului mai mica cu o unitate. Iata acum
care este implementarea recursiva a factorialului, folosind o functie algoritmica
(stnga) si implementarea Java (dreapta):
functie fact(n)
public static long fact(int n)
daca n = 0 atunci
fact
if (n == 0)
altfel
return 1;
fact
n*fact(n-1)
else
return
return n*fact(n-1);

1n algoritmica, prin procedura se ntelege o functie care nu returneaza nici o valuare (de exem
plu, n Java o metoda care returneaza void)

138

12.1. INTRODUCERE N RECURSIVITATE


Se observ c funcia de mai sus nu este dect o "traducere" aproape di
rect a formulei matematice anterioare. Trebuie s remarcm c, aa cum vom
vedea n continuarea acestui paragraf, la baza funcionrii acestor funcii st un
mecanism foarte precis, care nu este att de trivial cum ar prea la prima vedere.
S lum ca al doilea exemplu, calculul celebrului ir al lui Fibonacci, care
este definit recurent astfel:
ib(n 2)
/i6(n) = {f<"-1) + '

pentru
pentru

n> 1
n = 0, 1

Implementarea n pseudocod, respectiv Java, a calculului irului lui Fi


bonacci este:
funcie fib(n)
dac n=0 sau n=l atunci
fib <- n
altfel
fib <r- fib(n-l)+fib(n-2)
return

public static long fib(int n)


{
if (n==0||n==l)
return n;
else
return fib(n-l)+fib(n-2);
}

Se observ c n ambele exemple am nceput cu aa numita condiie de ter


minare:
dac n = 0 sau n = 1 atunci
fib^n
care corespunde cazului n care nu se mai fac apeluri recursive. O funcie recursiv care nu are condiie de terminare va genera apeluri recursive interminabile,
care se soldeaz n Java cu o eroare de tipul StackOverf lowError {de
pire de stiv, deoarece aa cum vom vedea, fiecare apel recursiv presupune
salvarea anumitor date pe stiv, iar stiva are o dimensiune finit). Condiia de
terminare ne asigur de faptul c atunci cnd parametrul funciei devine su
ficient de mic, nu se mai realizeaz apeluri recursive i funcia este calculat
direct.
Ideea fundamental care st la baza nelegerii profunde a mecanismului
recursivitii este aceea c n esen, un apel recursiv nu difer cu nimic de un
apel de funcie obinuit. Pentru a veni n sprijinul acestei afirmaii trebuie s
studiem mai n amnunime ce se petrece n cazul unui apel de funcie.
Se cunoate faptul c n situaia n care compilatorul ntlnete un apel de
funcie, acesta pred controlul execuiei funciei respective, dup care se revine
139

12.1. INTRODUCERE N RECURIVITATE


la urmtoarea instruciune de dup apel. ntrebrile care apar n mod firesc sunt:
de unde tie compilatorul unde s se ntoarc la terminarea funciei? De unde
tie care au fost valorile variabilelor nainte de a se preda controlul funciei?
Rspunsul este simplu: nainte de a realiza un apel de funcie, compilatorul
salveaz complet starea programului (linia de la care s-a realizat apelul, va
lorile variabilelor locale, valorile parametrilor de apel) pe stiv, urmnd ca la
revenirea din subrutin s rencarce de pe stiv starea care a fost nainte de apel.
Pentru exemplificare s considerm urmtoarea procedur (nerecursiv) care
afieaz o linie a unei matrice. Att linia care trebuie afiat, ct i matricea sunt
transmise ca parametru:

procedur AfisLin(a: tmatrice; n, lin: integer)


pentru i = 1, n
scrie aflin, i]
return

Procedura AfisLin este apelat de procedura AfisMat descris mai jos, care
afieaz, linie cu linie, o ntreag matrice pe care o primete ca parametru:

procedur AfisMatja: tmatrice; n: integer)


pentru i = 1, n
AfisLin(a, n, i)
return

S presupunem c procedura AfisMat este apelat ntr-un program astfel:

AfisMatfa, 5)

pentru a afia o matrice de dimensiuni 5x5.


In momentul n care compilatorul ntlnete acest apel, el salveaz pe stiv
linia de la care s-a fcut apelul (s spunem 2181), valoarea matricei a i alte
variabile locale declarate n program:
140

12.1. INTRODUCERE N RECURSIVITATE

2181; AfisMat(a,n);...
Controlul va fi apoi preluat de ctre procedura AfisMat, care intr n ciclul
pentru cu apelul: AfisLin(a,n,i) aflat, s zicem, la linia 2198.
In acest moment controlul va fi preluat de ctre procedura AfisLin, dar nu
nainte de a aduga la vrful stivei linia de la care s-a fcut apelul, valorile
parametrilor i a variabilei locale i:

2198; AfisLin(n, a,i);i= 1; . . .


2181; AfisMat(n, a);...
Procedura AfisLin va tipri prima linie a matricei, dup care execuia ei se
ncheie. n acest moment compilatorul consult vrful stivei pentru a vedea
unde trebuie s revin i care au fost valorile parametrilor i variabilelor locale
nainte de apel. Variabila i devine 2 i din nou se apeleaz procedura AfisLin,
etc.
Remarcm aici faptul c att procedura AfisMat ct i procedura AfisLin
utilizeaz o variabil local numit i. Nu poate exista nici o confuzie ntre cele
dou variabile, deoarece n momentul execuiei lui AfisLin, valoarea variabilei i
din AfisMat este salvat pe stiv.
S vedem acum evoluia stivei program n cazul calculului recursiv al lui
fact(5). Presupunem c la linia 2145 are loc apelul recursiv: fact < n *
fact(n 1).
Pentru a realiza nmulirea respectiv, trebuie ca nti s se calculeze/acffra1 ). Cum n are valoarea 5, pe stiv se va depune fact(4). Abia dup ce valoarea
lui fact(4) va fi calculat se poate calcula valoarea lui fact(5). Calculul lui
fact(4) implic ns calculul lui fact(3), care implic la rndul lui calculul
lui fact(2), fact(l), fact(0). Calculul lui fact(0) se realizeaz prin atribuire
direct, fr nici un apel recursiv:
141

12.1. INTRODUCERE N RECURIVITATE


dac n=0 atunci
fact <- 1
n acest moment, stiva programului conine toate apelurile recursive realizate
pn acum:

2145;/act(0);
2145;/act(l);
2145;/act(2);
2145;/act(3);
2145;/act(4);
xxxx; fact(5);
fact(l) fiind calculat, se poate reveni la calculul nmulirii 2*fact(l) = 2,
apoi, fact(2) fiind calculat se revine la calculul nmulirii 3* fact(2)=6 etc,
pn se calculeaz 5*/<zct(4)=120 i se revine n programul apelant.
S vedem acum modul n care se realizeaz calculul recursiv al irului lui
Fibonacci. Vom vedea c timpul de calcul al acestei recurene este incomparabil
mai mare fa de calculul factorialului. S presupunem c funcia/;^ se apeleaz
cu parametrul n = 3. n aceast situaie, se depune pe stiv apelul fib(3)
mpreun cu linia de unde s-a realizat apelul (de exemplu, 2160). n linia 2160
a procedurii are loc apelul recursiv: fib < fib(n 1) + fib(n 2) care n
cazul nostru, n fiind 3, presupune calcularea sumei fib(2) + fib{\). Aceast
sum nu poate fi calculat nainte de a-1 calcula pe fib(2). Calculul lui fib(2)
presupune calcularea sumei fib(l) + fib{0). fib{\) i fib(0) se calculeaz
direct la urmtorul apel recursiv, dup care se calculeaz suma lor, rezultnd
c fib(2) = 2. Abia acum se revine la suma fib(2) + fib{\) i se calculeaz
fib(l), dup care se revine i se calculeaz fib(3).
Modul de calcul al lui fib(n) recursiv se poate reprezenta foarte sugestiv
arborescent. Rdcina arborelui este fib(n), iar cei doi fii sunt apelurile recur
sive pe care fib(n) le genereaz, i anume fib(n 1) i fib(n 2). Apoi se
reprezint apelurile recursive generate de fib(n 2) ca n Figura 12.1.
Din Figura 12.1 se observ c anumite valori ale irului lui Fibonacci se
calculeaz (inutil) de mai multe ori. fib(n) i fib(n 1) se calculeaz o dat,
fib(n 2) se calculeaz de dou ori, fib(n 3) de 3 ori etc. Aceasta explic
de ce n capitolul 9 am obinut o complexitate exponenial pentru varianta recursiv de calcul a irului lui Fibonacci.
142

12.1. INTRODUCERE N RECURSIVITATE

Figura 12.1: Reprezentarea arborescent a apelurilor recursive realizate pentru


a calcula irul lui Fibonacci. Se observ c Fib ( n-3 ) este calculat de 3 ori.

12.1.2

Recursivitatea nu nseamn recuren

Implementarea recursiv a funciilor recurente este uor de neles, datorit


mecanismului simplu de transpunere a recurenei ntr-o funcie recursiv. To
tui, recursivitatea nu se limiteaz doar la implementarea recurenelor matema
tice. Putem defini la fel de bine i operaii recursive. O operaie recursiv este
definit funcie de ea nsi, dar pentru o problem de dimensiune cu o unitate
mai mic. De exemplu, operaia de inversare a n caractere se poate defini recursiv astfel: se extrage primul caracter din ir, apoi se inverseaz cele n 1
caractere rmase dup care se adaug la final caracterul extras. Acest principiu
l aplicm n exemplul care urmeaz.

Exemplu: S se scrie o funcie care citete o secven de caractere pn cnd


ntlnete caracterul
dup care afieaz caracterele n ordine invers.
Rezolvarea acestei probleme se poate formula recursiv astfel: inversarea
caracterelor unui ir de n elemente implic inversarea caracterelor rmase dup
citirea primului caracter, i scrierea n final a primului caracter:
Inv(n) = Citeste(a) + Inv(n 1) + Scrie(a)
n consecin, metoda de inversare va avea forma (parametrul n a fost eliminat,
el fiind dat n formul doar pentru claritate):
143

12.1. INTRODUCERE N RECURIVITATE


funcie inversare
citete a
dac a <>' .' atunci inversare
scrie a
return

public static void inversareQ


{
char a;
a=Reader.readChar() ;
if (a!=V)
{
inversare();
}
System,out.print(a);

}
Reamintim c Reader este o clas ajuttoare pentru citirea de date de la
tastatur i a fost definit n cadrul primului volum, fiind reluat i n cadrul
acestui volum, la paragraful 1 1 .4. 1 .
Este important de notat c pentru ca metoda s funcioneze corect, variabila
a trebuie declarat ca variabil local; astfel, toate valorile citite vor fi salvate pe
stiv, de unde vor fi extrase succesiv (n ordinea invers citirii) dup ntlnirea
caracterului
Exemplu: Transformarea unui numr din baza 10 ntr-o baz b, mai mic
dect 10.
S ne reamintim algoritmul clasic de trecere din baza 10 n baza b. Numrul
se mparte la b i se reine restul. Ctul se mparte din nou la b i se reine restul
i se continu acest procedeu pn cnd ctul devine mai mic dect b. Rezultatul
se obine prin scrierea n ordine invers a resturilor obinute.
Formularea recursiv a acestei rezolvri pentru trecerea unui numr n n
baza b este:
pentru n > b
pentru n < b

funcie transform(n:integer)
rest = n mod b
dac n > b atunci transform(ndivb)
scrie rest
return

int rest=n%b;
if(n >= b)
{
transform(n/b);
}
System,out.print(rest);
'
}

144

12.2. PREZENTAREA METODEI DIVIDE ET IMPERA


De remarcat c n aceast funcie variabila rest trebuie s fie declarat local,
pentru a fi salvat pe stiv, n timp ce variabila b este bine s fie declarat global,
deoarece valoarea ei nu se modific, salvarea ei pe stiv ocupnd spaiu inutil.
Odat ce am neles mecanismul recursivitii, suntem pregtii pentru a
nelege cea de-a doua metod de elaborare a algoritmilor, Divide et Impera.

12.2

Prezentarea metodei Divide et Impera

Divide et Impera este o metod special prin care se pot aborda anumite ca
tegorii de probleme. Ca i celelalte metode de elaborare a algoritmilor, Divide et
Impera se bazeaz pe un principiu extrem se simplu: se descompune problema
iniial n dou (sau mai multe) subprobleme de dimensiune mai mic, dup
care soluia problemei iniiale se obine combinnd soluiile subproblemelor n
care a fost descompus. Procedeul de descompunere se repet pn cnd, dup
descompuneri succesive, se ajunge la probleme de dimensiune mic, pentru care
exist rezolvare direct.
Evident, nu orice gen de problem se preteaz la a fi abordat cu Divide et
Impera. Din descrierea de mai sus reiese c o problem abordabil cu aceast
metod trebuie s aib dou proprieti:
1 . S se poat descompune n subprobleme;
2. Soluia problemei iniiale s se poat construi simplu pe baza soluiei
subproblemelor.
Modul n care metoda a fost descris, conduce n mod natural la o implementare
recursiv, avnd n vedere faptul c i subproblemele se rezolv n acelai mod
cu problema iniial. Iat care este forma general a unei funcii Divide et Im
pera:

funcie DivImp(P: Problem)


dac Simplu (P) atunci
RezolvDirect (P) ;
altfel
Descompune (P , PI, P2) ;
Divlmp (PI ) ;
Divlmp (P2) ;
Combin (PI, P2);
return
145

12.3. CUTARE BINAR


n consecin, putem spune c abordarea Divide et Impera implic trei pai la
fiecare nivel de recursivitate:
1 . Divide problema n dou subprobleme;
2. Impera (Stpnete, Cucerete) cele dou subprobleme prin rezolvarea
acestora n mod recursiv;
3 . Combin soluiile celor dou subprobleme n soluia final pentru proble
ma iniial.

12.3

Cutare binar

Cutarea binar este o metod eficient de regsire a unor valori ntr-o


secven ordonat. Cutarea binar este cel mai simplu exemplu de problem
Divide et Impera, deoarece n cazul ei se rezolv doar una din cele dou sub
probleme, deci faza de recombinare a soluiilor nu mai este necesar. Enunul
problemei de cutare binar este:
Se d un vector cu n componente (ntregi), ordonate cresctor i un numr
ntreg oarecare p. S se decid dac acest numr se gsete n vectorul dat, i
n caz afirmativ s se furnizeze indicele poziiei pe care se gsete.
O rezolvare imediat a problemei presupune parcurgerea secvenial a vec
torului dat, pn cnd p este gsit, sau am ajuns la sfritul vectorului. Aceast
rezolvare ns nu folosete faptul c vectorul este sortat.
Cutarea binar procedeaz n felul urmtor: se compar p cu elementul
din mijlocul vectorului, dac p este egal cu acel element, cutarea s-a ncheiat.
Dac este mai mic, se caut doar n prima jumtate, iar dac este mai mare, se
caut doar n a doua jumtate.
Se observ c n aceast situaie problema nu se descompune n dou sub
probleme care se rezolv, dup care se construiete soluia, ci se reduce la una
sau la alta din subprobleme. Cei trei pai ai lui Divide et Impera sunt n aceast
situaie:
1 . Divide: mparte irul de n elemente n care se realizeaz cutarea n dou
iruri cu n/2 elemente;
2. Stpnete: Caut ntr-una dintre cele dou jumti, funcie de valoarea
elementului din mijloc;
3. Combin: Nu exist.
146

12.3. CUTARE BINAR


Metoda binarySearch() din Listing 12.1 realizeaz cutarea elementului e 1 n
irul s, ntre indicii low i high.
Listing 12.1: Implementarea cutrii binare. Metoda va returna poziia pe care
se gsete numrul el n irul s sau -l dac el nu este gsit.
1 public static int binarySearch ( int [ ] s, int el, int low,
2
int high )
if (low <= high) //condiie de oprire
!
int middle = (low + high) / 2;
if ( el == s [middle ])
{
return middle :
else
(
f ( el < s [middle ])
return binarySearch ( s , el, low, middle 1);
1se
return binary S earch ( s , el, middle + 1, high);

return
}
Poziia pe care se gsete elementul el n irul s este obinut prin apelul:
poz = binarySearch ( s , el, 0, s.length 1)
Listing 12.2 prezint o clas simpl care utilizeaz metoda cutrii binare
pentru a gsi un numr ntr-un ir citit de la tastatur:
Listing 12.2: Rezolvarea problemei cutrii binare
1 import j ava . io . * ;
2 import io . Reader ;
3
4 /* *
5 * Program ce verifica daca un element introdus
6 * de la tastatura se gsete in cadrul unui sir ordonat.
7 */
8 public class BinarySearch
{
10 /** Metoda de cutare a elementului el in irul s . */
11 public static int search(int[] s, int el, int low,
147

12.3. CUTARE BINAR


12
'3
i4

int high )
{
if ( low <= high )
{
int mid = (low + high) / 2;

i6
17
s

if ( el == s [mid ] )
{
//element gsit
return mid ;
}
e1se
{
if ( el < s [mid])
{
//caut in prima jumtate a subsirului
return search (s , el, low , mid 1 ) ;
}
e1se
{
//caut in a doua jumtate a subsirului
return search ( s , el , mid + 1 , high ) ;
}
}

20
21
22
23
25
26
27
28
29
30
31
32
33
34
}
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
59
60
61
148

return 1 ;
}
/** Programul principal.*/
public static void main (String[] args)
j
//citirea elementelor irului
System. out.println(" Introducei elementele irului in " +
"ordine cresctoare (pe aceeai linie):");
int [] s = Reader . readlntArray ();
//citirea elementului cutat
System, out. prin (" Introducei elementul cutat: ");
int el = Reader . readlnt ();
//cutarea elementului
int poz = search (s, el, 0, s.length 1);
//afiarea rezultatului cutrii
i f ( poz > 1 )
{
System . out . println (" Elementul " + el +
" a fost gsit in sir pe poziia " +
poz ) ;

12.4. SORTAREA PRIN INTERCLASARE (MERGESORT)


}
e1se
{
System . out . p ri n 1 1 n (" E lementul " + el +
"nu a fost gsit in sir");
}

63
64
65
66
68 }
69 }

12.4

Sortarea prin interclasare (MergeSort)

Sortarea prin interclasare este, alturi de sortarea rapid (QuickSort) i


sortarea cu ansamble (HeapSort), una dintre metodele eficiente de ordonare a
elementelor unui ir. Enunul problemei este urmtorul:
S se ordoneze cresctor un ir cu n componente ntregi.
Principiul de rezolvare const n a mpri irul care trebuie ordonat n dou
pri egale i a ordona fiecare jumtate, dup care se interclaseaz cele dou
jumti. Descompunerea n dou jumti se realizeaz pn cnd se ajunge la
iruri cu un singur element, care nu mai necesit sortare. Algoritmul de sortare
prin interclasare urmeaz ndeaproape conceptul Divide et Impera. Pe scurt,
modul lui de operare este urmtorul:
1 . Divide: mparte irul de n elemente care urmeaz a fi sortat n dou iruri
cu n/2 elemente;
2. Stpnete: Sorteaz recursiv cele dou subiruri utiliznd sortarea prin
interclasare;
3. Combin: Interclaseaz subirurile sortate pentru a obine rezultatul final.
Metoda mergeSort ( ) din Listing 12.3 implementeaz algoritmul de sortare
prin interclasare. Apelul iniial al funciei este:
mergeSort(s, 0, s . length 1);
Listing 12.3: Metod care ordoneaz irul s folosind sortarea prin interclasare
i public static void mergeSort ( int [ ] s, int low , int high)
^{
3
i f ( low < high)
!
5
int mid = (low + high) / 2;
6
mergeSort (s, low, mid);
7
mergeSort (s, mid + 1, high);
149

12.4. SORTAREA PRIN INTERCLASARE (MERGESORT)

intercls(s, low , mid , high);


}

,o }
Metoda de interclasare n acest caz este analoag cu metoda de interclasare
obinuit a dou iruri, diferena constnd n faptul c acum se interclaseaz
dou jumti ale aceluiai ir, iar rezultatul se va depune n final tot n irul
interclasat. Listing 12.4 prezint implementarea complet a sortrii prin inter
clasare, aplicat pe un ir care este preluat de la tastatur.
Listing 12.4: Soluia algoritmului de sortare Mergesort
1 import java . io . * ;
2 import io . Reader ;
4 /* *
5 * Program ce ordoneaz un sir de numere ntregi
6 * folosind metoda MergeSort .
7 */
8 public elass MergeSort
!
10 /** Metoda de interclasare a celor 2 subsiruri.*/
11 public static void intercls(int low, int mid,
12
int high , int [ ] s )
'3 {
14
int i = low;
15
int j = mid + 1 ;
16
17
int[] inter = new int[high + 1];
s
int k = low;
19
20
// int e r cla s ar e a elementelor
21
while (( i <= mid) && (j <= high ))
22
{
if (s[i] <= s[j])
{
25
inter[k + + ] = s[i + + ] ;
)
27
eIse
28
{
29
inter[k + + ] = s[j+ + ];
}
)
32
33
//au mai rmas elemente din primul subsir?
34
for ( int 1 = i ; 1 <= mid ; 1++)
{
36
inter[k + + ] = s[l];
}
38
150

12.4. SORTAREA PRIN INTERCLASARE (MERGESORT)


39
40
4,
42

//au mai rmas elemente din cel de al doilea subsir?


for ( int 1 = j; 1 <= high; 1 ++)
{
inter[k + + ] = s[l];
}

44
45
46
48
49
50
51
52
53
54
55
57
5s
59
60
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

//copierea elementelor din irul inter in irul s


for ( i = low ; i <= high ; i ++)
!
s[i]= inter [i];
}
)
/* * Metoda de sortare . */
public static void mergeSort(int[] s, int low, int high)
{
if (low < high) //subsir u l are cel puin 2 elemente
!
int mid = (low + high) / 2;
mergeSort ( s , low, mid);
mergeSort ( s , mid + 1, high);
i n t e r c 1 s ( low , mid, high, s);
}
}
/* * Programul principal . */
public static void main(String [] args )
{
//citirea elementelor irului
System, out . p rintln ( " Introducei elementele irului " +
"(pe aceeai linie):");
int[] s = Reader . readlnt Array ( ) ;

8o

//sortarea elementelor irului prin metoda "MergeSort"


mergeSort (s, 0, s.length 1);
//afiarea rezultatului sortrii
System. out. println(
"irul ordonat prin metoda MergeSort este:");
for (int i = 0; i < s.length ; i ++)
!
Sy stem . out . p ri n t ( s [ i ] + " ");
}

82 }
83 )

151

12.5. SORTAREA RAPID (QUICKSORT)


12.5

Sortarea rapid (QuickSort)

Sortarea rapid este, aa cum i spune i numele, cea mai rapid metod
de sortare prin comparaii cunoscut n prezent. Exist foarte multe variante
ale acestei metode, o parte dintre ele avnd doar rolul de a micora timpul de
execuie n cazul cel mai nefavorabil. Vom prezenta aici varianta clasic, despre
care vei remarca cu surprindere c este neateptat de simpl. Enunul proble
mei este identic cu cel de la sortarea prin interclasare, i anume:
S se ordoneze cresctor un ir de numere ntregi.
Metoda de sortare rapid prezentat n acest paragraf este, dintr-un anumit
punct de vedere, complementara metodei Mergesort. Diferena dintre cele dou
metode este dat de faptul c, n timp ce la Mergesort mai nti vectorul se
mprea n dou pri dup care se sorta fiecare parte i apoi se interclasau cele
dou jumti, la Quicksort mprirea se face n aa fel nct cele dou iruri
s nu mai necesite a fi interclasate dup sortare, adic primul ir s conin
doar elemente mai mici (nu neaprat ordonate) dect elementele celui de-al
doilea ir. Rezult de aici c n cazul lui Quicksort, etapa de recombinare este
trivial, deoarece problema este astfel mprit n subprobleme nct s nu mai
fie necesar interclasarea irurilor. Etapele lui Divide et Impera pot fi descrise
n aceast situaie astfel:
1 . Divide: mparte irul de n elemente care urmeaz a fi sortat n dou iruri,
astfel nct elementele din primul ir s fie mai mici dect elementele din
al doilea ir;
2. Stpnete: Sorteaz recursiv cele dou subiruri utiliznd sortarea rapid;
3. Combin: irul sortat este obinut din concatenarea celor dou subiruri
sortate.
Funcia care realizeaz mprirea n subprobleme (astfel nct elementele primu
lui ir s fie mai mici dect elementele celui de-al doilea) se datoreaz lui C. A.
Hoare, care a gsit o metod de a realiza aceast mprire (numit partiionare)
n timp liniar.
Metoda de partiionare rearanjeaz elementele tabloului n funcie de primul
element, numit pivot, astfel nct elementele mai mici dect primul element sunt
trecute n stnga lui, iar elementele mai mari dect primul element sunt trecute
n dreapta lui. De exemplu, dac avem vectorul:
o = (7,8,5,2,3),
152

12.5. SORTAREA RAPID (QUICKSORT)


atunci procedura de partiionare va muta elementele 5, 2 i 3 n stnga lui 7,
iar 8 va fi n dreapta lui. Cum se realizeaz acest lucru? irul este parcurs
simultan de doi indici: primul indice, low, pleac de la primul element i este
incrementat succesiv, iar al doilea indice, high, pornete de la ultimul element
i este decrementat succesiv. n situaia n care a[Zou>] este mai mare dect
a[high], elementele se interschimb. Partiionarea este ncheiat n momentul
n care cei doi indici se ntlnesc (devin egali) undeva n interiorul irului. La
fiecare pas al algoritmului, fie se incrementeaz low, fie se decrementeaz high;
ntotdeauna unul dintre cei doi indici, low sau high, este poziionat pe pivot.
Atunci cnd low indic pivotul, se decrementeaz high, iar atunci cnd high
indic pivotul se incrementeaz low. Iat cum funcioneaz partiionarea pe
exemplul de mai sus. La nceput, low indic primul element, iar high indic
ultimul element:
o = (7,8,5,2,3)
t
t
low
high
Deoarece a[Zcw] > a[high] elementele 7 i 3 se vor interschimb. Dup
interschimbare, pivotul va fi indicat de high, deci low va fi incrementat:
a =(3, 8, 5, 2, 7)
t
t
low high
Din nou avem <z[Zcw] > a[high], elementele 8 i 7 se vor interschimb.
Dup interschimbare, pivotul va fi indicat de low, deci high va fi decrementat:
a = (3,7,5,2,8)
t t
low high
Din nou avem <z[Zcw] > a[high], elementele 7 i 2 se vor interschimb.
Dup interschimbare, pivotul va fi indicat de high, deci low va fi incrementat:
a = (3,2,5,7,8)
tt
low high
De data aceasta avem of/ow] <= a[high], deci low va fi incrementat din
nou, fr a se realiza interschimbri.
153

12.5. SORTAREA RAPID (QUICKSORT)


Listing 12.5: Metoda de partiionare. Elementele mai mici dect pivotul,
a [ 1 o w ] , vor fi aezate n stnga lui, iar elementele mai mari n dreapta. Metoda
va returna poziia pe care se afl pivotul.
1 public static int p a r t i t i o n ( in t low , int high)
2{
3
//variabila care ne spune daca high indica pivotul
4
boolean pozPivot = false ;
5
while (low < high) //indicii nu sau suprapus
{
7
if (a[low] > a[high])
{
9
int ers chimb a ( a , low, high);
10
//celalalt indice indica acum pivotul
11
pozPivot = ! pozPivot ;
}
13
pozPivot ? low++ : high ;
}
15
16
return low; //se re turn ea za poziia pivotului

In acest moment low i high s-au suprapus (au devenit egale), deci partiionarea s-a ncheiat. Pivotul este pe poziia a 4-a, care este de fapt i poziia
luifinal n irul sortat.
Metoda part it ion ( ) din Listing 12.5 primete ca parametri limitele
inferioar, respectiv superioar ale irului care se partiioneaz i returneaz
poziia pe care se afl pivotul n finalul partiionrii. Poziia pivotului este im
portant deoarece ne d locul n care irul va fi desprit n dou subiruri.
Dou observaii importante merit fcute referitor la metodapartition ( ) :
1. Variabila pozPivot poate lua valoarea false dac pivotul este indi
cat de low, sau true dac pivotul este indicat de high. Atribuirea
pozPivot =\pozPivot are ca efect schimbarea strii acestei variabile
din false n true sau invers;
2. Metoda se folosete n mod inteligent de transmiterea prin valoare a para
metrilor, deoarece modific variabilele low i high bazndu-sepe faptul
c aceast modificare nu va afecta valorile lui low si high din metoda
quickSort ( ) .
Metoda de ordonare propriu-zis din Listing 12.6 respect structura Divide et
Impera obinuit, doar c funcia de recombinare a soluiilor nu mai este nece
sar, deoarece am realizat partiionarea nainte de apel.
154

12.5. SORTAREA RAPID (QUICKSORT)

1
2
3
5
6
7

Listing 12.6: Metoda de ordonare quickSort


public static void quickSort ( int low,int high)
{
if ( low < high) //subsirul mai are cel puin 2 elemente
\
int mid = p a rt i t i o n ar e ( low , high); // p a rtiti on e a za
qui ckS ort ( low , mid 1); //sorteaz prima jumtate
qui ckS ort ( mid + 1, high); //sorteaz a doua jumtate
}
)

Programul din Listing 12.7 realizeaz ordonarea folosind Quicksort a unui


ir citit de la tastatur.

Listing 12.7: Soluia problemei de ordonare prin metoda quicksort


1 import j ava . io . * :
2 import io . Reader ;
3
4 /* *
5 * Program ce realizeaz sortarea unui sir de
6 * numere intregi prin metoda quicksort.
7 */
8 public class Quicksort
'{
10 /** Metoda de partitionare a irului 5.*/
11 public static int partitionare(int[] s , int low, int high)
^- {
13
int pozPivot = 0:
14
int aux :
15
16
while (low < high)
1
is
if ( s [low ] > s [high ])
1
20
// interschimbarea elementelor s [ low ] si sfhigh]
21
aux = s [ low ] ;
22
s [ low ] = s [ high ] :
23
s [ high ] = aux :
24
25
pozPivot = 1 pozPivot ;
}
27
28
if (pozPivot == 0)
155

12.5. SORTAREA RAPID (QUICKSORT)


{
30

high ;
}
eIse
{
low++;
)

32
34
}
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

return low ;
}
/** Metoda de sortare a irului s . */
public static void sort(int[] s, int low, int high)
j
if (low < high) //subsirul are cel puin 2 elemente
{
int mid = p art i t i o n ar e ( s , low, high);
sort(s, low, mid 1);
sort(s, mid + 1, high);
}
}
/** Programul principal.*/
public static void main (String[] args)
j
//citirea elementelor irului
System . out . p ri n 1 1 n ( " I n t r o du c e t i elementele irului " +
"(pe aceeai linie):");
int [] s = Reader . readlntArray ();

68
69
70 }
VI }

156

//sortarea elementelor irului prin metoda " quicksort "


sort(s, 0, s . length 1);
//afiarea rezultatului sortrii
System . out . println (
"irul ordonat prin metoda quicksort este:");
for (int i = 0; i < s. length; i++)
{
System . out . prin ( s [ i ] + " " ) ;
}

12.6. EXPRESII ARITMETICE


12.6

Expresii aritmetice

Se d o expresie aritmetic n care operanzii sunt simbolizai prin litere mici


(de la a la z), iar operatorii sunt '+',
'/' i '*' cu semnificaia cunoscut.
Se cere s se scrie un program care transform expresia n form polonez
postfixat.
Reamintim faptul c forma polonez postfixat (Lukasiewicz) este obinut
prin scrierea operatorului dup cei doi operanzi, i nu ntre ei. Aceast form
are avantajul c nu necesit paranteze pentru a schimba prioritatea operatorilor.
Ea este utilizat adeseori n informatic pentru a evalua expresii. Iat cteva
exemple:
1. a+b se scrie ab+
2. a'*(b+c) se scrie abc+*
3. (a+b)*(c+d) se scrie ab+cd+*
Unul dintre cei mai simpli algoritmi de a trece o expresie n form polonez
const n a cuta care este operatorul din expresie cu prioritatea cea mai mic,
i de a aeza acest operator la sfritul expresiei, urmnd ca prima parte a formei
poloneze s fie format din transformarea expresiei din stnga operatorului, iar
a doua parte a formei poloneze s fie format din transformarea expresiei din
dreapta operatorului.
Cele dou subexpresii urmeaz a se trata n mod analog, pn cnd se ajunge
la o subexpresie de lungime 1 , care va fi obligatoriu un operand i care nu mai
necesit transformare.
Schematic, dac avem expresia:
E = ElopE2
unde El i E2 sunt subexpresii, iar op este operatorul cu prioritatea cea mai
mic (deci operatorul unde expresia se poate "rupe" n dou), atunci forma
polonez a lui E, notat Pol(E), se obine astfel:
Pol(E)=Pol(El ) Pol(E2) op.
Expresia de mai sus exprim faptul c forma polonez postfixat a lui E se
obine prin scrierea n form polonez postfixat a celor dou subexpresii, ur
mate de operatorul care le separ. Expresia de mai sus este o expresie recursiv
specific tehnicii Divide et Impera. Etapele sunt n aceast situaie:
157

12.6. EXPRESII ARITMETICE


1. Divide: mparte expresia aritmetic n dou subexpresii legate printr-un
operator de prioritate minim;
2. Stpnete: Transform recursiv n form polonez cele dou subexpre
sii;
3. Combin: Scrie cele dou subexpresii n form polonez urmate de ope
ratorul care le leag.
Soluia problemei expresiilor aritmetice este dat n Listing 12.8. Transfor
marea propriu-zis este realizat de metoda polonez ( ) de la liniile 39-86.
polonez ( ) primete 3 parametri, care descriu subirul care trebuie trecut n
form polonez: x, care reprezint expresia E la care se adaug low i high,
care delimiteaz subirul din E care va fi procesat.
Ca orice metod recursiv, polonez ( ) ncepe la linia 43 cu condiia de
terminare:
if(low == high)
care corespunde cazului n care irul are un singur caracter (care trebuie n mod
obligatoriu s fie un operand) i a crui transformare n form polonez este
banal.
Ciclul while de la liniile 49-54 realizeaz un lucru foarte important: elimin eventualele paranteze inutile care nconjoar expresia, pentru a putea
gsi ulterior uor operatorul de unde se "rupe" expresia n dou. De exemplu,
expresia (a + b) * (c + d) va fi rupt ntr-o prim etap n (o + b) i (c + d).
Ambele expresii rezultate sunt nconjurate de paranteze care sunt acum inutile,
i care ar mpiedica gsirea operatorului cu prioritate minim (care trebuie s fie
n afara oricrei paranteze). Eliminarea parantezelor exterioare se face utilznd
metoda parant ( ) de la liniile 14-37, care ntoarce true dac parantezele
exterioare pot fi eliminate i false n caz contrar.
Ciclul for de la liniile 56-82 conine paii de divide i combin ai metodei.
Se ncearc mai nti gsirea unui operatori aditiv (+ sau ) i apoi a unui ope
rator multiplicativ (* sau /) care s se afle n afara oricrei paranteze. Dac un
astfel de operator este gsit (liniile 73-75), expresia este "rupt" n acel loc, iar
metoda ntoarce transformarea n form polonez a primei jumti, concatenat
cu transformarea celei de-a doua jumti urmate de operatorul unde s-a realizat
mprirea. Cutarea operatorului de prioritate minim se face de la dreapta la
stnga, i nu de la stnga la dreapta cum era de ateptat, pentru a transorma
corect expresii de forma a b c sau a/b/c (de exemplu, dac am transforma a
b c de la stnga la dreapta, am obine forma poloneza a bc, care se traduce
158

12.6. EXPRESII ARITMETICE


prin a (b c), ceea ce este incorect; transformarea corect, obinut prin
parcurgerea de la dreapta la stnga este
abc). Dac se ajunge la instruciunea
return de la linia 85, nseamn c nu s-a gsit nici un operator de la care
expresia s se rup n dou, deci expresia este incorect.
Listing 12.8: Rezolvarea problemei expresiilor aritmetice
1 import j ava . io . * ;
2 import io . Reader ;
3
4 /* *
5 * Program care transforma o expresie aritmetica
6 * in forma poloneza postfixata.
7 */
8 public class Expresii
"{
10 /* *
11
* Verifica daca expresia mai este corecta
12
* dupa eliminarea parantezelor exterioare.
13
*/
14 public static boolean parant(String x, int low , int high )
15 {
16
int nrp = 0; //numrul de paranteze deschise si neinchise
17
s
for ( int i = low + 1 ; i < high ; i ++)
i
20
if ( x . charAt ( i ) == ' ( ' )
{
22
nrp++;
if ( x . charAt ( i ) == ' ) ' )
{
nrp ;
if (nrp < 0) // sa inchis o paranteza fara pereche
{
//deci nu putem elimina paranetezele exterioare
return false ;

36
3V
38
39
40
41
42
43

return true ; //parantezele exterioare pot fi eliminate


}
/** Trece o expresie in forma poloneza postfixata.*/
public static String polonez(String x, int low ,
int high , char [] [] op)
{
if (low == high)
159

12.6. EXPRESII ARITMETICE


44
45
46
47
48
49
50

{
return x.valueOf(x. charAt ( low ) ) ;
}
//elimina parantezele exterioare inutile
while ( x . charAt ( low ) == '(' && x . charAt ( hi gh ) == ')'
&& parant (x , low , high ))
{
low++;
high ;
)
//caut locul unde irul poate fi "rupt" in doua
for (int i = 0; i < op.length; i ++)
{
int nrp = 0;

52
53
55
se
58
59
6o

for ( int j = high ; j >= low ; j


{
if (x . charAt (j ) == ' ( ' )
{
nrp++;
}

62
64
66
67
68
69

if ( x . charAt (j ) == ' ) ' )


{
nrp ;
i
//daca ne aflam in afara parantezelor si am
//gsii un operator cu prioritatea adecvata
if ( nrp == 0 &&
(x . charAt (j ) == op [ i ] [ 0] I
x.charAt(j) == op [ i ] [ 1 ] ) )
{
return polonez(x, low, j 1, op) +
polonez(x, j + 1, high, op) +
x . valueOf (x . charAt ( j )) ;
}

71
72
73
74
75
77
78
79
80
82
83
84
85
86
87
88
89
90
91
92
93
160

)
}
//daca sa ajuns aici, irul nu a putut fi "rupt" in
//doua, deci expresia este incorecta
return "irul este incorect" ;
}
/** Programul principal.*/
public static void main (String[] args)
{
//matricea celor 4 operatori standard
char [][] op = { {' + ', ' ' } , { ' * ' , ' / * } };

12.6. EXPRESII ARITMETICE

''5
96
91

// citirea expresiei aritmetice


System . out . prin (" Introducei expresia aritmetica : " );
String x = Reader.readString();
if (x.length() == 0) return;

100
System . out . println ( "Forma poloneza postfixata a
101
"expresiei aritmetice este : " +
io:
polonez(x, 0, x.length() 1, op));
103 }
104 }

Rezumat
La nceputul acestui capitol am prezentat elemente eseniale referitoare la
recursivitate. Am vzut care este mecanismul care st la baza acestei tehnici
i faptul c un apel recursiv nu difer n esen cu nimic de un apel obinuit.
Iat care sunt regulile de baz ale recursivitii, pe care este bine s le reinei i
aplicai ntotdeauna:
1. Condiia de terminare: cel puin o instan a problemei trebuie ntot
deauna s se poat rezolva fr a utiliza recursivitatea;
2. Progresul: orice apel recursiv trebuie s progreseze ctre condiia de terminare;
3. Crede i nu cerceta: ntotdeauna trebuie s presupunei c apelul recursiv
funcioneaz (fr a v pune problema cum);
4. Suprapunere de apeluri: evitai s executai acelai lucru de dou ori,
prin rezolvarea aceleiai instane a problemei n apeluri recursive distince
(cum am fcut la irul lui Fibonacci).
Recursivitatea are multe aplicaii concrete, cteva dintre ele fiind prezentate
chiar n cadrul acestui capitol.
Introducerea elementelor principale ale recursivitii a fost urmat apoi de
prezentarea metodei divide et impera, precum i a celor trei etape fundamen
tale care o caracterizeaz: divide, cucerete, combin. Cutarea binar este
o metod de a gsi rapid un element n cadrul unui ir ordonat. Quicksort
i Mergesort sunt metode deosebit de eficiente de a ordona un ir. Problema
expresiilor aritmetice presupune trecerea unei expresii din forma standard n
forma polonez postfixat.
161

12.6. EXPRESII ARITMETICE


Noiuni fundamentale
cutare binar: metod de cutare eficient a unui element n cadrul unui
ir ordonat.
condiie de terminare: condiie care indic sfritul recursiei prin rezolvarea
direct a unei instane a problemei.
Mergesort: algoritm de sortare eficient n care irul se mparte n dou
jumti, dup care se ordoneaz fiecare jumtate i se combin rezultatul.
metod recursiv: o metod care se autoapeleaz direct sau indirect.
Quicksort: algoritm de sortare eficient n care irul se partiioneazn dou,
dup care se ordoneaz fiecare partiie.
suprapunere de apeluri: situaie n care un algoritm recursiv realizeaz
inutil aceleai apeluri de mai multe ori, scznd astfel drastic eficiena rezolvrii.

Erori frecvente
1. Cea mai frecvent eroare la nceptori este de a uita s stabileasc o
condiie de terminare pentru apelurile recursive.
2. Fii ateni ca fiecare apel recursiv s constituie un pas ctre condiia de
terminare, altfel recursia este incorect.
3. Trebuie evitat suprapunerea apelurilor recursive, deoarece ele tind s ge
nereze algoritmi de complexitate exponenial.
4. Complexitatea algoritmilor recursivi trebuie calculat folosind formule
de recuren. Nu putei presupune c un apel recursiv are o complexitate
n timp liniar.
5. In cazul unui apel recursiv, doar variabilele locale i parametri actuali se
salveaz pe stiv. Nu v bazai pe faptul c variabilele definite n afara
funciei sunt salvate la apelul recursiv.
6. Pe de alt parte, evitai s declarai variabile locale sau parametri formali
care nu sunt necesari pentru metoda recursiv, pentru a nu umple stiva cu
informaii inutile.
7. Dei multe probleme admit descompunerea n dou sau mai multe subprobleme, nu ntotdeauna soluia problemei iniiale se poate obine pe
baza soluiei subproblemelor.
162

12.6. EXPRESII ARITMETICE


Exerciii
Teorie
1. Reprezentai evoluia stivei pentru funciile recursive din acest capitol.
2. Calculai de cte ori se recalculeaz valoarea Fk n cazul calcului recursiv
al valorii Fn a irului lui Fibonacci, prezentat n paragraful 12.1 .1 .
n practic
1. S se calculeze recursiv i iterativ cel mai mare divizor comun a dou
numere dup formulele (Euclid):
cmmdc(b, a mod b)
cmmdc{a, b) = | ^

pentru
altfel

cmmdc(b, \a b\)
cmmdc{a, b) = | ^

a mod 6^0

pentru
altfel

a^b

2. S se calculeze recursiv i iterativfuncia lui Ackermann, dat de formula:

[ nil
Ack(m,n) = < Ack(m 1, 1)
^ Ack{m - 1, Ack(m, n - 1))

pentru
pentru
altfel

m=0
n=0

3. S se calculeze combinrile dup formula de recuren din triunghiul lui


Pascal:

/~<k

/~<k

i r~<k 1

Calculai apoi combinrile dup formula clasic:

k\{n-k)\

Ce constatai? Cum explicai ceea ce ai constatat?


163

12.6. EXPRESII ARITMETICE


4. S se calculeze recursiv i iterativfuncia Manna-Pnueli, dat de formula:

FM = { f(f\
x + 2))

pentru
altfel

x > 12

5. S se scrie o funcie care calculeaz recursiv suma cifrelor unui numr


dup formula:
q( \ f n md 10 + S(n div 10)
^ '
[0

pentru
pentru

n>0
n=0

6. Se consider dou iruri definite recurent dup formulele:


an = a"-1+6"-1 i 5n = ^/an-ibn-i, cua0 = a i 60 = b.
S se scrie un program recursiv care calculeaz aceste iruri.
7. (Partiiile unui numr) Un numr natural n se poate descompune ca sum
descresctoare de numere naturale. De exemplu, pentru numrul 4 avem
descompunerile 2+1+1 sau 3+1. Prin P(n,k) se noteaz numrul de
posibiliti de a-1 descompune pe n ca sum (descresctoare) de k nu
mere. De exemplu, P(4, 2) = 2 (4 = 3 + 1, 4 = 2 + 2). Numerele
P(n, k) verific relaia de recuren:

P(n + k,k)= P{n, 1) + P{n, 2) + . . . + P(n, k)


cuP(n,l) = P(n,n) = 1.
S se calculeze numrul total de descompuneri ale numrului n.
8. S se modifice metodapolonez ( ) din Listing 12.8 astfel nct s poat
transforma corect i expresii n care operanzii au lungimi mai mari de un
caracter. De exemplu (max + 1) * (min + 2) etc.
9. S se scrie o funcie care calculeaz maximul elementelor unui ir uti
liznd tehnica Divide et Impera.
Indicaie: Se mparte irul n dou jumti egale, se calculeaz recursiv
maximul celor dou jumti i se alege numrul mai mare.
10. (Turnurile din Hanoi) Se dau trei tije simbolizate prin literele A, B i
C. Pe tija A se afl n discuri de diametre diferite aezate descresctor
n ordinea diametrelor, cu diametrul maxim la baz. Se cere s se mute
discurile pe tija B respectnd urmtoarele reguli:
164

12.6. EXPRESII ARITMETICE


(a) la fiecare pas se mut un singur disc;
(b) nu este permis aezarea unui disc cu diametru mai mare peste un
disc cu diametrul mai mic.
Indicaie: Formularea recursiv a soluiei este: se mut primele n-1 dis
curi de pe tija A pe tija C folosind ca tij intermediar tija B; se mut
discul rmas pe A pe tija B; se mut discurile de pe tija C pe tija B
folosind ca tij intermediar tija A. Parcurgerea celor trei etape permite
definirea recursiv a irului H(n,a,b,c) astfel:
H(n, a, b, c) =
b),ab,H(n l,c,b,a)

dac
dac

n= 1
n> 1

Exemplu: Pentru n = 2 avem:


H(2, a, b, c) = H(l, a, c, b),ab, H(l, c, b, a) = ac, ab, cb
1 1 . Scriei un program n care calculatorul s ghiceasc ct se poate de re
pede un numr natural la care v-ai gndit. Numrul este cuprins ntre 1
i 32.000. Atunci cnd calculatorul propune un numr i se va rspunde
prin 1, dac numrul este prea mare, 2 dac numrul este prea mic i 0
dac numrul a fost ghicit.
Indicaie: Problema folosete metoda cutrii binare prezentat n acest
capitol.
12. (Problema tieturilor) Se d o bucat dreptunghiular de tabl de dimen
siune l x h, avnd pe suprafaa ei n guri de coordonate numere ntregi
(colul din stnga jos al tablei este considerat centrul sistemului de co
ordonate). S se determine care este bucata de arie maxim fr guri
care poate fi decupat din suprafaa original. Sunt permise doar tieturi
orizontale sau verticale.
Indicaie: Se caut n bucata curent prima gaur. Dac o astfel de
gaur exist, atunci problema se mparte n alte patru subprobleme de
acelai tip. Dac suprafaa nu are nici o gaur, atunci se compar
suprafaa ei cu suprafeele fr gaur obinute pn la acel moment.
Dac suprafaa este mai mare, atunci se rein coordonatele ei.
165

12.6. EXPRESII ARITMETICE


Coordonatele gurilor sunt date n doi vectori xv i yv. Coordonatele
dreptunghiurilor care aparpe parcursul problemei sunt reinute prin colul
stnga jos (x,y), lungime i lime (l,h).
Pentru a se afla n interioul unui dreptunghi o gaur trebuie s ndepli
neasc simultan condiiile:
(a) xv (i) > x;
(b) xv (i) < x + l;
(c) yv(i) > y;
(d) yv(i) <y + h.
Tietura vertical prin aceast gaur determin dou dreptunghiuri:
(a) x,y,xv(i)-x,h;
(b) xv(i),y,l+x-xv(i),h.
Tietura orizontal prin aceast gaur determin alte dou dreptunghi
uri:
(a) x,yl,yv(i)-y;
(b) x,yv(i),l,h+y-yv(i).

166

13.Algoritmi Greedy

Am constatat ca cu ct muncesc
mai mult, cu att am mai mult
noroc.
Thomas Jeferson

Algoritmiiaplicatiproblemelorde optimizare(ncare se urmareste obtinerea


minimului sau maximului unei functii obiectiv) sunt, n general, compusi dintro secventa de pasi, la ecare pas existnd mai multe alegeri posibile. Pentru
multe problemede optimizare, utilizareametodeiprogramariidinamice (prezen
tata n capitolul 14) n vederea determinarii celei mai bune solutii se dovedeste
a o strategie prea complicata. Un algoritm Greedy va alege la ecare mo
ment solutia care pare a cea mai buna la momentul respectiv. Este vorba deci
despre o alegere optima, facuta local, cu speranta ca ea va conduce la un op
tim global. Acest capitol trateaza probleme de optimizare care pot rezolvate
cu ajutorul algoritmilor Greedy. Algoritmii Greedy conduc n multe cazuri la
solutii optime, dar nu ntotdeauna... n sectiunea 13.1 vom prezenta mai nti o
problema simpla dar netriviala, problema selectarii activitatilor, a carei solutie
poate calculata n mod ecient cu ajutorul unei metode de tip Greedy. Mai
departe, n sectiunea 13.2 se recapituleaza cteva elemente de baza ale metodei
Greedy. Capitolul se ncheie cu prezentarea ctorva probleme specice.
Metoda Greedy este destul de puternica si este aplicata cu succes unui spec
tru larg de probleme. Lucrarile despre teoria grafurilor contin mai multi algo
ritmi care pot priviti ca aplicatii ale metodei Greedy, cum ar algoritmii de
determinare
Dijkstra pentru
a arboreluipartial
determinarea celor
de cost
maiminim
scurte(Kruskal,
drumuri pornind
Prim) sau
dintr-un
algoritmullui
vrf.
n cadrul acestui capitol vom vedea:
Care sunt elementele care caracterizeaza o strategie greedy;
167

13.1. PROBLEMA SPECTACOLELOR (SELECTAREA ACTIVITILOR)


Ce este substructura optim (principiul optimalitii) i principiul alegerii
greedy;
Cum se poate demonstra corectitudinea unui algoritm.

13.1

Problema spectacolelor (selectarea activiti


lor)

Primul exemplu pe care l vom considera este o problem de repartizare a


unei resurse (o sal de spectacol) mai multor activiti care concureaz pentru
a obine resursa respectiv (diferite spectacole care au loc n sala respectiv).
Vom vedea c un algoritm de tip Greedy reprezint o metod simpl i elegant
pentru programarea unui numr maxim de spectacole care nu se suprapun (nu
mite activiti compatibile reciproc).
S presupunem c dispunem de o mulime S = 1,2,. ..,n de n activiti (spec
tacole) care doresc sfoloseasc o aceeai resurs (sala de spectacole). Aceast
resurs poatefifolosit de o singur activitate la un moment dat. Fiecare activi
tate i are un timp de start Si i un timp de terminare ti, unde Si < f,. Dac este
selectat activitatea i, ea se desfoar pe durata intervalului [Sj,tj). Dou
activiti sunt compatibile dac duratele lor de desfurare sunt disjuncte. Pro
blema spectacolelor (selectrii activitilor) const n selectarea unei mulimi
maximale de activiti compatibile ntre ele.
Un algoritm Greedy pentru aceast problem este descris de urmtoarea
funcie, prezentat n pseudocod. Vom presupune c spectacolele (adic datele
de intrare) sunt ordonate cresctor dup timpul de terminare:
ti <ti <,...,< tn.
In cazul n care activitile nu sunt ordonate astfel, ordonarea poate fi fcut
n timpul 0(n * logri) (folosind Mergesort sau Quicksort prezentate n capi
tolul anterior). Algoritmul de mai jos presupune c datele de intrare s i t sunt
reprezentate ca iruri.

funcie SELECT-SPECTACOLE-GREEDY (s, t)


//SS = mulimea spectacolelor selectate
SS^{1}
//uss = indicele Ultimului Spectacol
//Selectat
uss < 1
168

13.1. PROBLEMA SPECTACOLELOR (SELECTAREA ACTIVITILOR)


pentru sc = 2,n //sc este spectacolul curent
dac ssc > tU88 atunci
//spectacolul curent ncepe
//dup ce uss s-a terminat , deci
//se adaug sc la spect . selectate
SS <- SS U {sc}
//ultimul spectacol selectat devine sc
uss <- SC
return SS
n mulimea SS se introduc spectacolele care au fost selectate. Variabila uss
identific ultimul spectacol introdus n SS. Deoarece activitile sunt con
siderate n ordinea cresctoare a timpilor lor de terminare, tuss va reprezenta
ntotdeauna timpul maxim de terminare a oricrei activiti din SS. Aceasta
nseamn c:
tuss = max{tk\k SS}
La nceptul algoritmului, mulimea spectacolelor selectate, SS, se iniiali
zeaz cu activitatea 1 (deoarece aceast activitate se termin cel mai repede),
iar variabila uss (ultimul spectacol selectat) ia ca valoare aceast activitate. In
continuare, n ciclul pentru se consider pe rnd fiecare activitate, sc. Aceasta se
adaug mulimii SS dac este compatibil cu celelalte activiti deja selectate.
Pentru a vedea dac specacolul curent, sc, este compatibil cu toate celelalte
activiti existente la momentul curent n SS, este suficient ca momentul de
start ssc s nu fie mai devreme dect momentul de terminare tuss al activi
tii cel mai recent adugate mulimii SS. Dac specacolul curent, sc, este
compatibil, atunci el este adugat mulimii SS, iar variabila uss este actualiza
t. Funcia SELECT-SPECTACOLE-GREEDY este foarte eficient. Ea poate
planifica o mulime S de n activiti n 0(n), presupunnd c activitile au
fost deja ordonate dup timpul lor de terminare. Activitatea aleas de SELECTSPECTACOLE-GREEDY este ntotdeauna cea cu primul timp de terminare care
poate fi planificat fr suprapuneri.

13.1.1

Demonstrarea corectitudinii algoritmului

Pn n acest moment, nu ne-am pus problema demonstrrii corectitudinii


algoritmilor prezentai. Totui, trebuie s menionm c exist o ntreag ra
mur a algoritmicii care se ocup exclusiv de demonstrarea corectitudinii al
goritmilor, n general, demonstrarea corectitudinii unui algoritm este un proces
169

13.1. PROBLEMA SPECTACOLELOR (SELECTAREA ACTIVITILOR)


destul de laborios, i, n majoritatea aplicaiilor practice concrete, este mai adec
vat testarea comportamentului pe mulimi de date reprezentative dect demon
strarea riguroas a corectitudinii. Exist totui situaii n care trebuie s ne asi
gurm c o anumit secven critic din cadrul unui program face exact ceea ce
intenionm, indiferent de setul de date care este primit la intrare. De exemplu,
pentru un sistem de operare, este esenial ca algoritmul de planificare al pro
ceselor s funcioneze corect, indiferent de numrul i natura proceselor care
trebuie planificate. Oricte seturi de date am alege pentru a testa algoritmul,
rmne posibilitatea ca s fi scpat din vedere o anumit configuraie pentru
care algoritmul ar funciona greit. Astfel, singura posibilitate de a ne convinge
c algoritmul va funciona corect indiferent de setul de date de la intrare este s
demonstrm riguros corectitudinea lui.
Pentru algoritmul de fa am ales s i demonstrm corectitudinea, deoarece
pe de o parte ea este ilustrativ pentru o ntreag clas de probleme, iar pe de
alt parte demonstraia nu este dificil.
Teorema 13.1.1 Algoritmul SELECT-SPECTACOLE-GREEDYfurnizeaz so
luia optim (numr maxim de spectacole) pentru problema selectrii activit
ilor.
Demonstraie: Fie S = {1,2, ... ,n} mulimea activitilor care trebuie pla
nificate, ordonate cresctor dup timpul de terminare. n consecin, activitatea
1 se termin cel mai devreme. Vom arta c exist o soluie optim care ncepe
cu activitatea 1 .
S presupunem c avem o soluie A C S optim pentru o instan a pro
blemei. Pentru simplitate, presupunem c activitile din A sunt ordonate dup
timpul de terminare. Dac primul spectacol din A este chiar 1, atunci demon
straia este ncheiat. Dac primul spectacol din A nu este 1 , atunci nlocuim
primul spectacol cu spectacolul 1 , obinnd evident o soluie corect, deoarece
spectacolul 1 se va termina mai devreme dect primul spectacol din A. Am ar
tat astfel c exist o soluie optim pentru S care ncepe cu activitatea 1 .
Mai mult, odat ce este fcut alegerea activitii 1, problema se reduce
la determinarea soluiei optime pentru activitile din S care sunt compatibile
cu activitatea 1. Fie S' = {i G S\si > t\] mulimea activitilor care ncep
dup ce 1 se termin. Rezult c dac A este o soluie optim pentru S, atunci
A' = A {1} este o soluie optim pentru 5". Dac nu ar fi aa, atunci ar exista o
soluie optim B' pentru 5" care s aib mai multe activiti dect A'. Adugnd
activitatea 1 la B', vom obine o soluie pentru S cu mai multe activiti dect
soluia A, ceea ce este absurd.
Astfel, prin inducie dup numrul de alegeri fcute se poate arta c alegnd
primul spectacol compatibil la fiecare pas, se obine o soluie optim.
170

13.1. PROBLEMA SPECTACOLELOR (SELECTAREA ACTIVITILOR)


13.1.2

Soluia problemei spectacolelor

Soluia problemei spectacolelor este dat n Listing 13.1. Metoda selectSpectacole ( ) de la liniile 12-33 este transpunerea n Java a funciei SELECTSPECTACOLE-GREEDY. selectSpectacole ( ) primete ca parametri do
u iruri reprezentnd timpul de nceput respectiv timpul de sfrit al fiecrui
spectacol i ntoarce un vector cu specacolele care au fost planficate, n ordinea
cresctoare a timpului de ncepere. Ordonarea cresctoare a spectacolelor este
realizat de metoda ordonare ( ) de la liniile 35-70, care, pentru simplitate,
folosete metoda bulelor. De remarcat faptul c metoda ordonare ( ) trebuie
s interschimbe i valorile din irul s, pentru a menine consistena cu irul t.
Listing 13.1: Soluia problemei spectacolelor
1 import java. util.*;
2 import j ava . io . * ;
3 import io . Reader ;
4
5 /* *
6 * Problema spectacolelor ( selectarea activitilor prin
i * metoda Greedy )
8 */
9 public class Spectacole
,o {
ii /** Selectarea spectacolelor. */
12 public static Vector selectSpectacole(int[] s, int[] t)
{
u
Vector sol = new Vector () ;
15
16
if (s.length == 0) return sol;
17
s
//primul spectacol face parte din soluie
19
sol.addElement (new Integer (0));
20
21
int j = 0;
22
23
for (int i = 1; i < s.length ; i ++)
!
if (s[i] >= t[j])
!
27
sol.addElement( new Integer(i ));
28
j = i;
29
}
30
}
31
32
return sol ;
)
34
35 /** Ordonarea timpilor de terminare a spectacolelor. */
171

13.1. PROBLEMA SPECTACOLELOR (SELECTAREA ACTIVITILOR)


36

38
39
40
41
42
43
44
45
46
47
48
49
50

public static void ordonare ( int [ ] s, int[] t)


!
//ordonare prin " bubble sort"
//daca inte rschimbam valorile din sir
//atunci k este 1, altfel k este 0
int k;
int aux ;
d0
{
// la inc eput nu sunt schimbri in iruri
k = 0;
for (int i = 0; i < t . length 1; i ++)
{
if (t[i] > t[i + 1]J
{
// inter schimb am valorile din s
aux = s [ i ] ;
s[i] = s[i+l];
s [ i + 1 ] = aux ;

54
55
56
57
5S
59
60
61
62
63
64
65
66

// inter schimb am valorile din


aux = t [ i ] ;
t[i] = t[i + 1];
t [ i + 1 ] = aux ;
//au fost fcute schimbri in iruri
k = 1;
}
}

68
69
70
71
72
73
74
75
76
77
79
80
si
82
83
84
85
172

}
while ( k = = 1 ) ;
}
/** Programul principal. */
public static void main (String[] args)
j
//citirea numrului de spectacole
Sy stem . out . p ri n t ( " I n t r o duc e t i numrul de spectacole: ");
int n = Reader . readlnt ( ) ;
//citirea timpilor de ncepere si terminare ai spectacolelor
Sy stem . out . p ri n 1 1 n ( " I n t r o du c e t i timpii de incepere si " +
"terminare ai spectacolelor:");
i n t | ] s = new i n t [ n ] ;
i n t | ] t = new i n t [ n ] ;
for(int i =0; i < s. length; i ++)

13.2. ELEMENTE ALE STRATEGIEI GREEDY


86
87
88
89
90

{
System . out . p ri n t (" s [ "
s[i] = Reader . re adl n t
System . out . p ri n t (" t [ "
t[i] = Reader . re adl nt

+ i + "] = ");
() ;
+ i + "] = ");
() ;

}
92
93
94
95
96
97
98
99
oo
101
102
103
104
105
106
107
10K
109 }
1 LO }

13.2

//ordonarea cresctoare a spectacolelor in funcie de


//timpul de terminare a spectacolelor
ordonare ( s , t ) ;
//se selecteaz prin metoda greedy spectacolele
Vector sol = selectSpectacole(s, t);
//afiarea rezultatelor
System, out. println(" Organizarea spectacolelor:");
for (int i =0; i < sol.sizeQ; i ++)
{
int id = ((Integer) s ol . elementAt ( i )). int Value () ;
System. out. println(s[id] + " " + t[id]);
}

Elemente ale strategiei Greedy

Un algoritm Greedy determin o soluie optim a unei probleme n urma


unei succesiuni de alegeri. La fiecare moment de decizie din algoritm este
aleas opiunea care pare a fi cea mai potrivit. Aceast strategie euristic nu
produce ntotdeauna soluia optim, dar exist i cazuri cnd aceasta este obi
nut, cum ar fi n cazul problemei selectrii activitilor. n acest paragraf vom
prezenta cteva proprieti generale ale metodei Greedy.
Cum se poate decide dac un algoritm Greedy poate rezolva o problem par
ticular de optimizare? n general nu exist o modalitate de a stabili acest lucru,
dar exist anumite caracteristici pe care le au majoritatea problemelor care se
rezolv prin tehnici Greedy: proprietatea de alegere Greedy i substructura
optim.
n cazul general o problem de tip Greedy, are urmtoarele componente:
o mulime de candidai (lucrri de planificat, vrfuri ale grafului etc);
o funcie care verific dac o anumit mulime de candidai constituie o
soluie posibil (nu neaprat optim) a problemei;
173

13.2. ELEMENTE ALE STRATEGIEI GREEDY


o funcie care verific dac o mulime de candidai este fezabil, adic
dac este posibil s completm aceast mulime astfel nct s obinem o
soluie posibil (nu neaprat optim) a problemei (verific dac planifi
carea este format din activiti care nu se suprapun, etc);
ofuncie de selecie care indic la orice moment care este cel mai promi
tor dintre candidaii nc nefolosii (se alege spectacolul compatibil care
se termin cel mai repede);
o funcie obiectiv care d valoarea unei soluii (numrul de lucrri plani
ficate, timpul necesar executrii tuturor lucrrilor ntr-o anumit ordine,
lungimea drumului pe care l-am gsit, etc); aceasta este funcia pe care
urmrim s o optimizm (minimizm/maximizm).
Pentru a rezolva o problem de optimizare cu Greedy, cutm o soluie posibil
care s optimizeze valoarea funciei obiectiv. Un algoritm Greedy construiete
soluia pas cu pas. Iniial, mulimea candidailor selectai este vid. La fiecare
pas, ncercm s adugm acestei mulimi cel mai promitor candidat, conform
funciei de selecie. Dac, dup o astfel de adugare, mulimea de candidai se
lectai nu mai este fezabil, eliminm ultimul candidat adugat, iar acesta nu va
mai fi niciodat considerat. Dac, dup adugare, mulimea de candidai selec
tai este fezabil, ultimul candidat adugat va rmne de acum ncolo n ea. De
fiecare dat cnd lrgim mulimea candidailor selectai, verificm dac aceast
mulime nu constituie o soluie posibil a problemei noastre. Dac algoritmul
Greedy funcioneaz corect, prima soluie gsit va fi totodat o soluie optim
a problemei. Soluia optim nu este n mod necesar unic: se poate ca funcia
obiectiv s aib aceeai valoare optim pentru mai multe soluii posibile. De
scrierea n pseudocod a unui algoritm Greedy general este:

funcie greedy (C) // C este mulimea candidailor


S<tp // S este mulimea n care construim soluia
ct timp not soluie (S) i C^<f>
x<r- un element din C care maximizeaz select (x)
C <^C-{x}
dac fezabil(S U {x}) atunci S < S U {x}
dac soluie (S) atunci
ret urn S
altfel
return "nu exist soluie"
174

13.2. ELEMENTE ALE STRATEGIEI GREEDY


Este de neles acum de ce un astfel de algoritm se numete "lacom" (am putea
s-1 numim i "nechibzuit"). La fiecare pas, greedy() alege cel mai bun candi
dat la momentul respectiv, fr s-i pese de viitor i fr s se rzgndeasc.
Dac un candidat este inclus n soluie, el rmne acolo; dac un candidat este
exclus din soluie, el nu va mai fi niciodat reconsiderat. Asemenea unui n
treprinztor care urmrete ctigul imediat n dauna celui de perspectiv, un
algoritm Greedy acioneaz simplist. Totui, ca i n afaceri, o astfel de metod
poate da rezultate foarte bune tocmai datorit simplitii ei. Funcia de selectare
este n general derivat, ca i funcia de continuare de la metoda backtracking,
din funcia obiectiv. S identificm acum elementele strategiei Greedy pentru
urmtoarea problem:
S se scrie un algoritm care este capabil s dea o anumit sum, reprezen
tnd restul unui client, folosind un numr ct mai mic de monezi, avnd valorile
de 1,5 i 25 de uniti.
Elementele strategiei Greedy sunt:
Candidaii: mulimea iniial de monezi de 1, 5 i 25 uniti, n care
presupunem c din fiecare tip de moned avem o cantitate nelimitat;
O soluie posibil: valoarea total a unei astfel de mulimi de monezi
selectate trebuie s fie exact valoarea pe care trebuie s o dm ca rest;
O mulime fezabil: valoarea total a unei astfel de mulimi de monezi
selectate nu este mai mare dect valoarea pe care trebuie s o dm ca rest;
Funcia de selecie: se alege cea mai mare moned din mulimea de can
didai rmas;
Funcia obiectiv: numrul de monezi folosite n soluie; se dorete mini
mizarea acestui numr.
Se poate demonstra c algoritmul Greedy va gsi n acest caz mereu soluia
optim - restul cu un numr minim de monezi. Pe de alt parte, presupunnd
c exist i monezi de 12 uniti sau c unele din tipurile de monezi lipsesc din
mulimea iniial de candidai, se pot gsi contraexemple pentru care algoritmul
nu gsete soluia optim, sau nu gsete nici o soluie cu toate c exist una.
Evident, soluia optim se poate gsi i ncercnd toate combinrile posibile
de monezi (folosind backtracking), dar soluia ar avea n acest caz o complexi
tate exponenial, n timp ce complexitatea algoritmului Greedy este liniar.
Un algoritm Greedy nu duce deci ntotdeauna la soluia optim sau la o
soluie. Este doar un principiu general, urmnd ca pentru fiecare caz n parte s
determinm dac obinem sau nu soluia optim.
175

13.2. ELEMENTE ALE STRATEGIEI GREEDY


S vedem acum care sunt cele dou caracteristici eseniale pe care trebuie
s le respecte o problem pentru a putea fi abordat folosind Greedy.

13.2.1

Proprietatea de alegere Greedy

Prima caracteristic a unei probleme de tip Greedy este proprietatea alegerii


Greedy, adic se poate ajunge la o soluie optim global, realiznd alegeri
(Greedy) optime local. In procesul de construcie a unei soluii din cadrul unui
algoritm Greedy se realizeaz o alegere care pare a fi cea mai bun la momentul
respectiv. Alegerea realizat de un algoritm Greedy poate depinde de alegerile
fcute pn n acel moment, dar nu ia n calcul niciodat alegerile care pot fi
fcute ulterior.
Desigur, trebuie s demonstrm c o alegere Greedy la fiecare pas conduce
la o soluie optim global, dar aceasta este o problem mai delicat. De obicei,
demonstraia examineaz o soluie optim global. Apoi se arat c soluia poate
fi modificat astfel nct la fiecare pas este realizat o alegere Greedy, iar aceast
alegere reduce problema la una similar dar de dimensiuni mai reduse. Se aplic
apoi principiul induciei matematice pentru a arta c o alegere Greedy poate fi
utilizat la fiecare pas. Faptul c o alegere Greedy conduce la o problem de di
mensiuni mai mici reduce demonstraia corectitudinii la demonstrarea faptului
c o soluie optim trebuie s evidenieze o substructur optim.

13.2.2

Substructur optim

O problem evideniaz o substructur optim dac o soluie optim a pro


blemei conine soluii optime ale subproblemelor. Aceast proprietate este
cheia pentru aplicarea programrii dinamice sau a unui algoritm Greedy. Ca
exemplu al unei structuri optime, s ne reamintim demonstraia corectitudinii
algoritmului pentru problema selectrii spectacolelor, unde se arat c dac
o soluie optim A a problemei selectrii activitilor ncepe cu activitatea 1 ,
atunci mulimea activitilor A' = A {1} este o soluie optim pentru proble
ma selectrii activitilor S' = {i S\si > t\}.
Cu proprietatea de substructur optim ne vom ntlni n capitolul urmtor,
n care vom prezenta cum se pot rezolva problemele care o respect (fr a
respecta i proprietatea de alegere Greedy) folosind metoda programrii dina
mice.
176

13.3. MINIMIZAREA TIMPULUI MEDIU DE ATEPTARE


13.3

Minimizarea timpului mediu de ateptare

O singur staie de servire (procesor, pomp de benzin etc) trebuie s satis


fac cererile a n clieni. Timpul de servire necesarfiecrui client este cunoscut
n prealabil: pentru clientul i este necesar un timp ti,i = l,n. Dorim s
minimizm timpul total de ateptare:
T = X^iLi(timPul de ateptare pentru clientul i)
Ceea ce este acelai lucru cu a minimiza timpul mediu de ateptare, care
T.
este
n
De exemplu, dac avem trei clieni cu t\ = 5, ti = 10, = 3, sunt posibile
ase ordini de servire:
Ordine
Timpul (T)
1 2 3 5+(5+10)+(5+10+3)
1 3 2 5+(5+3)+(5+3+10)
2 1 3 10+(10+5)+(10+5+3)
2 3 1 10+(10+3)+(10+3+5)
3 1 2 3+(3+5)+(3+5+10) optim
3 2 1 3+(3+10)+(3+10+5)
n primul caz, clientul 1 este servit primul, clientul 2 ateapt pn este
servit clientul 1 i apoi este servit, clientul 3 ateapt pn sunt servii clienii
1, 2 i apoi este servit. Timpul total de ateptare a celor trei clieni este 38.
Timpul total minim de ateptare este obinut n al cincilea caz i are valoarea
29.
Algoritmul Greedy este foarte simplu - la fiecare pas se selecteaz clientul
cu timpul minim de servire din mulimea de clieni rmas. Vom demonstra
c acest algoritm este optim. Fie I =
in) o permutare oarecare a
ntregilor {1,2,..., n}. Dac servirea are loc n ordinea /, avem:
T(I) = th + (th +ti2) + (th +ti2+U3)+... = nth +(n- l)ti2 + ...=
ELi (" - * + l)Uh
Presupunem acum c / este astfel ales nct putem gsi doi ntregi a < b cu

deci exist un client (6) care necesit un timp mai lung de deservire, i care este
servit nainte (de o).
Interschimbm pe ia cu n /; cu alte cuvinte, clientul care a fost servit al
fc-lea va fi servit acum al a-lea i invers. Obinem o nou ordine de servire
care este de preferat deoarece
177

13.3. MINIMIZAREA TIMPULUI MEDIU DE ATEPTARE


T(J') = (n - a + \)tib +(n-b + \)tia + E=i,*#,6 (" " * +
T(J) - T(J') = (n - o + l)(tia - tij + (n - 6 + l)(t4i - tia) =
(6-o)(tia - iij > 0
Aplicnd succesiv pasul de mai sus se obine o permutare optim, de tipul
J = (ji , h , , jn) pentru care avem:
tjl

Prin metoda Greedy, selectnd permanent clientul cu timpul cel mai mic de
deservire, obinem deci ntotdeauna planificarea optim a clienilor. Problema
poate fi generalizat i pentru un sistem cu mai multe staii de servire.
Implementarea algoritmului se reduce la o banal ordonare a clienilor cresc
tor dup timpul de deservire i este prezentat n Listing 13.2.
Listing 13.2: Soluia problemei minimizrii timpului de ateptare
1 import java . io . * ;
2 import io . Reader ;
4 /* *
5 * Program pentru minimizarea timpului de ateptare al unui
6 * client pentru a fi deservit de o staie (metoda Greedy).
7 */
8 public elass MinimTimp
!
io /** Ordonarea timpilor de ateptare ai clienilor. */
n public static void ordonare(int[] t)
i
13
//ordonare prin " bubble sort"
14
15
//daca interschimbam valorile din sir
16
//atunci k este 1, altfel k este 0
17
int k;
s
int aux ;
19
20
do
{
22
//la inceput nu sunt schimbri in iruri
23
k = 0;
24
25
for (int i = 0; i < t . length 1; i ++)
{
if (t[i] > t[i + 1])
28
{
29
// inter schimbam valorile din t
30
aux = t [ i ] ;
178

13.3. MINIMIZAREA TIMPULUI MEDIU DE ATEPTARE


ai
32
33
34
35

t[i ] = t[i + 1];


t [ i + 1 ] = aux ;
//au fost fcute schimbri in sir
k = 1;
}
}

38
39
40
41
42
43
44
45
46
47
48
49

}
while ( k = = 1 ) ;
}
/** Calculeaz, timpul total minim de ateptare . */
public static int calculTimpMinim(int[] t)
{
int s = 0;
System, out . prin In ( " Ordinea de deservire este:");
for (int i = 1; i <= t.length; i ++)
!
s += (t.length i + 1) * t[i 1];

51
52
53
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
" 1

Sy stern . out . p ri n t ( t [ i 1] + " ");


}
Sy stem . out . p r i n 1 1 n ( ) ;
return s ;
}
/** Programul principal.*/
public static void main(String [] args )
{
//citirea timpului de ateptare al fiecrui client
System . out . p ri n tl n (" Timpii de ateptare pentru " +
"clieni (pe aceeai linie):");
int[] t = Reader . readlnt Array ( ) ;
//ordonarea timpilor de ateptare ai clienilor
ordonare ( t ) ;
Sy stem . out . p ri ntl n (
"Timpul total de ateptare are valoarea minima = " +
calculTimpMinim ( t ) ) ;
}

179

13.4. INTERCLASAREA OPTIM A MAI MULTOR IRURI ORDONATE


13.4

Interclasarea optim a mai multor iruri or


donate

S presupunem c avem dou iruri Si i S2, de lungime m i n, ordonate


cresctor i c dorim s obinem prin interclasarea lor un ir ordonat cresctor,
S, care conine exact elementele din cele dou iruri. Dac interclasarea are loc
prin plasarea elementelor din Si i S2 n noul ir, S, atunci numrul deplasrilor
este m + n.
Generaliznd, s considerm acum n iruri S, S2, . . . S, fiecare ir Si,i =
1, n, fiind format din qi elemente ordonate cresctor (vom numi qi lungimea lui
Si). Ne propunem s obinem irul S ordonat cresctor, coninnd exact ele
mentele din cele n iruri. Vom realiza acest lucru prin interclasri succesive de
cte dou iruri. Problema interclasrii optime a mai multor iruri ordonate con
st n determinarea ordinii optime n care trebuie efectuate aceste interclasri,
astfel nct numrul total de operaii (deplasri) s fie ct mai mic. Exemplul
de mai jos ne arat c problema astfel formulat nu este banal, adic nu este
indiferent n ce ordine se fac interclasrile.
Exemplu: Fie irurile Si,S2,Ss de lungimi qi = 30,^2 = 20,93 = 10.
Dac interclasm pe Si cu S2, iar rezultatul l interclasm cu S3, numrul total
al deplasrilor este (30 + 20) + (50 + 10) = 110. Dac interclasm pe S3 cu
52, iar rezultatul l interclasm cu Si, numrul total al deplasrilor este (10 +
20) + (30 + 30) = 90, deci cu 20 de operaii mai puin.
Atam fiecrei strategii de interclasare cte un arbore binar n care valoarea
fiecrui vrf este dat de lungimea irului pe care l reprezint. Dac irurile
Si , S2, . . . , S6 au lungimile qi = 30, q2 = 10, q3 = 20, q = 30, q5 = 50, q6 =
10, dou astfel de strategii de interclasare sunt reprezentate prin arborii din
Figura 13.1.
Observm c fiecare arbore are 6 vrfuri terminale, corespunznd celor 6
iruri iniiale i 5 vrfuri neterminale, corespunznd celor 5 interclasri care
definesc strategia respectiv. Numerotm vrfurile n felul urmtor: vrful ter
minal i (i G 1, 2, 3, 4, 5, 6), va corespunde irului Si, iar vrfurile neterminale se
numeroteaz de la 7 la 1 1 n ordinea obinerii interelsrilor respective (Figura
13.2).
Strategia Greedy apare n Figura 13.1 (arborele din partea dreapt) i con
st n a interclasa mereu cele mai scurte dou iruri disponibile la momentul
respectiv.
Pentru a interclasa irurile Si , S2 , . . . , S, de lungimi qi , q2 , . . . , qn, obinem
pentru fiecare strategie cte un arbore binar cu n vrfuri terminale numerotate
180

13.4. INTERCLASAREA OPTIMA A MAI MULTOR SIRURI ORDONATE

Figura 13.1: Reprezentarea strategiilor de interclasare.


150

140

150

10
90

130

10
40

110

50

30

20

30
10

30

30

20
20

80

60

10

50

de la 1 la si
vrfuri neterminale numerotate de la d la
.
Denim pentru un arbore oarecare A de acest tip lungimea externa ponderata:
f

unde
este adncimea vrfului i. Este usor de observat ca numarul total de
deplasari de elemente pentru strategia corespunzatoare lui A este chiar .
Solutia optima a problemei noastre este atunci arborele (strategia) pentru care
lungimea externa ponderata este minima.

Teorema 13.4.1 Prin metoda Greedy, n care se interclaseaza la ecare pas


cele doua siruri de lungime minima, se obtine sirul s cu un numar minim de
operatii.
Demonstratie: Demonstram prin inductie. Pentru G} , proprietatea este
vericata. Presupunem ca proprietatea este adevarata pentru
siruri. Fie A
181

13.4. INTERCLASAREA OPTIM A MAI MULTOR IRURI ORDONATE

arborele strategiei Greedy de interclasare a n iruri de lungime q\ < q2 < . . . <


qn. Fie B un arbore cu lungimea extern ponderat minim, corespunztor unei
strategii optime de interclasare a celor n iruri. n arborele A apare subarborele:

qi + 92

Qi

12

reprezentnd prima interclasare fcut conform strategiei Greedy. n arborele


B, fie un vrf neterminal de adncime maxim. Cei doi fii ai acestui vrf sunt
atunci dou vrfuri terminale qj i q\~. Fie B' arborele obinut din B schim
bnd ntre ele vrfurile qi i qj, respectiv q2 i qu- Evident, L(B') < L(B).
Deoarece B are lungimea extern ponderat minim, rezult c L(B) = L(B').
Eliminnd din B' vrfurile q\ i q2, obinem un arbore B" cu n 1 vrfuri
182

13.4. INTERCLASAREA OPTIM A MAI MULTOR IRURI ORDONATE


terminale qi + q2,q3, ,qn- Arborele B' are lungimea extern ponderat mi
nim i i(-B') = L(B) + (qi + q2). Rezult c i B are lungimea extern
ponderat minim. Atunci, conform ipotezei induciei, avem L(B) = L(A')
unde A' este arborele strategiei Greedy de interclasare a irurilor de lungime
Qi + 92; 93; j Qn- Cum A se obine din A' atand la vrful qi + q-i fiii q\ i
q2, iar B' se obine n acelai mod din B", rezult c L() = L(B') = L(B) .
Proprietatea este deci adevrat pentru orice n.
Deoarece algoritmul alege la fiecare pas irurile disponibile de lungime mi
nim, structura de date adecvat pentru a reine irurile este coada de priori
ti, prezentat n paragraful 10.7. Implementarea algoritmului este simpl i o
lsm ca exerciiu.

Rezumat
n acest capitol am prezentat metoda Greedy, care se poate aplica probleme
lor care respect substructura optim i principiul alegerii Greedy. n cazul aces
tei metode, soluia se construiete succesiv, la fiecare pas realiznd o alegere
optim local, fr a reveni asupra deciziilor anterioare. Soluia construit astfel,
va fi o soluie optim a problemei de rezolvat. Am vzut modul n care se poate
demonstra corectitudinea unui algoritm utiliznd un procedeu general, care se
poate aplica la muli algoritmi de acest tip.

Noiuni fundamentale
funcie de selecie: funcie care indic cel mai promitor dintre candidaii
nc nefolosii la un moment dat.
proprietatea de alegere Greedy: se poate ajunge la o soluie optim global,
realiznd alegeri (Greedy) optime local.
problem de optimizare: problem n care se cere minimizarea sau maxi
mizarea unei funcii obiectiv.
substructura optim (principiul optimalitii): o problem evideniaz o
substructur optim dac o soluie optim a problemei conine soluii optime
ale subproblemelor.

Erori frecvente
1 . Nu ncercai s aplicai aceast metod orbete, fr a verifica dac pro
blema respect substructura optim i proprietatea de alegere Greedy.
183

13.4. INTERCLASAREA OPTIM A MAI MULTOR IRURI ORDONATE


2. Faptul c ai gsit un algoritm Greedy care funcioneaz corect pe une
le exemple, nu nseamn c metoda va da soluia optim pentru orice
date de intrare. Pentru a fi siguri de aceasta, demonstrai corectitudinea
algoritmului.
Exerciii
Teorie
1 . Calculai complexitatea rezolvrii problemei spectacolelor din paragraful
13.1,Listing 13.1.
2. Gsii o modalitate de a reduce complexitatea rezolvrii problemei ante
rioare la 0(n log n).
3. n rezolvarea problemei interelsrii optime din paragraful 13.4 am reco
mandat utilizarea unei cozi de prioriti. Care este complexitatea algorit
mului n acest caz? Dar dac n loc de o coad de prioriti folosim un
arbore binar de cutare?
n practic
1 . Rezolvarea problemei spectacolelor din paragraful 13.1 folosete metoda
bulelor pentru a ordona spectacolele n ordinea cresctoare a timpului de
terminare. nlocuii metoda bulelor cu o ordonare mai eficient.
2. Rezolvai problema interclasrii optimale din paragraful 13.4 folosind
(a) O list nlnuit;
(b) O coad de prioriti;
(c) Un arbore binar de cutare.
3. {Problema rucsacului) Avem la dispoziie un rucsac de capacitate M i
n obiecte diferite (cte unul din fiecare) cu costurile a i greutatea gi.
Scriei un algoritm care aeaz aceste obiecte n rucsac astfel nct costul
total s fie maxim. Suma greutilor obiectelor din rucsac nu poate depi
capacitatea rucsacului. Dac un obiect nu ncape n rucsac, se poate lua
doar o parte (fraciune) din el.
Indicaie: Este uor de intuit c pentru obinerea unui profit (cost) total
maxim trebuie alese obiecte de greutate mic i cost mare. Demonstraia
riguroas a afirmaiei anterioare v-o propunem spre rezolvare.
184

13.4. INTERCLASAREA OPTIM A MAI MULTOR IRURI ORDONATE


4. (Problema discret a rucsacului) Acelai enun ca la problema prece
dent, cu diferena c dintr-un obiect nu se poate pune o fraciune (un
obiect fie se pune ntreg, fie nu se pune). Artai c algoritmul Greedy de
la problema precedent nu furnizeaz ntotdeauna soluie optim n acest
caz. Gsii un algoritm care furnizeaz ntotdeauna soluie optim! Ce
complexitate are acest algoritm?
Indicaie: Soluia optim pentru aceast problem poate fi determinat
doarprin backtracking sau programare dinamic (prezentat n capitolul
urmtor). De exemplu, pentru c=(5,3,3), g=(3,2,2) i M=4, prin apli
carea algoritmului de la problema precedent am selecta doar obiectul
1 (obiectul 2 nu ar ncape ntreg n rucsac deci nu ar putea fi selectat) i
am obine costul 5, n timp ce dac am selecta obiectele 2 i 3 am obine
costul 6.
5. Gsii o soluie Greedy pentru problema comis-voiajorului propus la
capitolul 1 1 . Este aceast soluie optim?
Indicaie: Conform strategiei greedy, vom construi ciclul pas cu pas, adugnd la fiecare iteraie cea mai scurt muchie disponibil cu urm
toarele proprieti:
nu formeaz un ciclu cu muchiile deja selectate (exceptnd pentru
ultima muchie aleas, care completeaz ciclul);
nu exist nc dou muchii deja selectate, astfel nct cele trei muchii
s fie incidente n acelai vrf.
Acest algoritm nu numai c nu furnizeaz soluia optim, dar n multe
situaii nici mcar nu gsete o soluie.

185

14. Programare dinamic

Muli oameni prefer s tolereze o


problem pe care nu o pot rezolva,
dect o soluie pe care nu o pot
nelege.
Woolsey and Swanson
In capitolul anterior am vzut cum putem aplica metoda Greedy problemelor
de optimizare care respect principiul optimalitii (substructur optim) i prin
cipiul alegerii Greedy. Programarea dinamic este o alt metod de elaborare
a algoritmilor care se aplic problemelor de optimizare care respect principiul
optimalitii (fr a respecta principiul alegerii Greedy). In acest capitol vom
introduce gradat noiunile i tehnicile specifice acestei metode de elaborare a al
goritmilor, considerat de muli ca avnd un grad de dificultate mai mare dect
celelalte metode1. Dar nu v ngrijiorai! Materialul din acest capitol poate
fi cu uurin parcurs de ctre oricine, datorit structurrii gradate a noiunilor
prezentate.
Pe parcursul acestui capitol vom vedea:
Cum se trateaz dinamic problemele de recuren matematic, n vederea
unei rezolvri eficiente;
Ce este principiul optimalitii;
Cum se aplic principiul optimalitii pentru a descompune problema de
programare dinamic n subprobleme;
Cum se creeaz relaiile de recuren care rezolv problema de progra
mare dinamic;
1 De altfel, majoritatea problemelor spinoase alese pentru concursurile naionale i internaionale
de informatic se rezolv prin aceast metod.
186

14.1. ISTORIC SI DESCRIERE

Cum se poate reface solutia problemeipe baza calculelor efectuate pentru


obtinerea valorii optime;
Cteva probleme clasice de programare dinamica.

14.1

Istoric si descriere

Programarea dinamica provine din domeniul cercetarilor operationale, unde


constituie un domeniude sine statator. Cuvntulprogramare din numele acestei
metode nu are nimic de a face cu scrierea de programe pentru calculator. Mate
maticienii foloseau acest cuvnt pentru a descrie un set de reguli pe care oricine
le poate urmari pentru a rezolva un anumit tip de problema. Programarea di
namica a fost introdusa n anii 1950 de catre matematicianul Richard Bellman,
care a descris modalitatea prin care se rezolva problemele n care trebuie luata o
decizie optimala la ecare pas. n cei mai bine de 50 de ani de la aparitia acestei
metode utilizarea si aplicarea programarii dinamice au crescut enorm.
Programarea dinamica descompune de obiceiproblema de optimizare origi
nala ncusubprobleme
chiar
cele de dimensiune
si alege cele
minima.
mai bune
Solutia
solutii
optima
pentru
a problemelor
subproblemencepnd
de dimen
siune mai mare este obtinuta pe baza solutiilor optime a problemelor de di
mensiune mai mica cu ajutorul unei formule de recurenta care face legatura
ntre solutii. Pna n acest punct descrierea facuta nu difera cu nimic de cea
a tehnicii Divide et Impera. Ceea ce face ca programarea dinamica sa e mai
speciala este faptul ca formula de recurenta este folosita pentru a elimina toate
solutiile subproblemelor care nu pot genera solutia optima. Mai mult, n cazul
programarii dinamice se retin solutiile subproblemelor care pot utilizate n
calculul solutiei optime.
Similaritati exista si cu metoda Greedy. Asa cum vom vedea, si problemele
de programare dinamica trebuie sa respecte substructura optima, numita n
aceasta situatie principiul optimalitatii. Diferenta consta n faptul ca n cazul
programarii dinamice nu se face o alegere Greedy, mergnd pe optimul local,
ci se iau n considerare toate solutiile optimale ale subproblemelor cu un ordin
de marime mai mic. Exista multe probleme care admit o rezolvare att prin
metoda Greedy, ct si prin programare dinamica (problema rucsacului, proble
ma comis-voiajorului). n aceste situatii, solutia Greedy va avea o complexitate
n timp mai mica, dar solutiile gasite nu vor ntotdeauna cele optime, n timp
ce solutiile date prin programare dinamica for avea o complexitate n timp ceva
mai mare, dar vor furniza solutia optima.
Dicultatile care apar n utilizarea metodei programarii dinamice au facut
ca aceasta metoda sa e considerata de multi ca ind prea complexa si n con
187

14.2. PRIMII PAI N PROGRAMAREA DINAMIC


secin s evite aprofundarea i utilizarea ei. Scopul acestui capitol este tocmai
acela de a forma treptat capacitatea de a analiza i rezolva cu mult uurin
orice gen de problem de programare dinamic.

14.2

Primii pai n programarea dinamic

Acest paragraf este consacrat tratrii unor probleme de recuren matema


tic. Nu putem afirma c problemele de recuren matematic se ncadreaz
strict n categoria problemelor de programare dinamic, deoarece ele nu sunt
probleme de optimizare. Totui, modul n care vom rezolva aici recurenele
introduce deja un concept esenial pentru rezolvarea problemelor de programare
dinamic: tabelul2. Tot n acest paragraf vom analiza i care sunt avantajele
utilizrii tabelelor pentru a reine soluiile subproblemelor n dauna soluiilor
recursive.
14.2.1

Probleme de recuren matematic tratate dinamic

irul lui Fibonacci


Un exemplu clasic, adesea ntlnit n lucrrile care trateaz recursivitatea,
prezentat i n paragraful 12.1.1 (pagina 138) este calculul irului lui Fibonacci,
definit astfel:
Fn = Fn-i + Fn-2', F0 = 0; Fi = 1
Metoda recursiv care calculeaz Fn este descris n Listing 14.1. Aa cum
am vzut deja n paragraful 9.4.3 (pagina 26), funcia de mai sus recalculeaz
din nou i din nou termenii irului, rezultnd astfel un timp de lucru exponenial
(0((f)n)). Figura 14.1 pune n eviden modul n care se recalculeaz din nou
i din nou aceleai valori din cauza apelurilor recursive suprapuse.

1
2
3
4
5
f
7
8

Listing 14.1 : Metoda recursiv de calcul a irului lui Fibonacci


public static final int fibonacciRec ( int n)
{
if (n == 0 II n == 1)
{
return n;
)
else
(

2Este interesant de remarcat faptul c recurgerea la tabele a dat i numele metodei. Astfel,
n cercetrile operaionale (domeniul din care provine aceast metod) termenul "programare" se
refer la un set de reguli care se aplic pe un tabel i nu la scrierea unui program pentru calculator.
188

14.2. PRIMII PAI N PROGRAMAREA DINAMIC

Figura 14.1: Reprezentarea arborescent a apelurilor recursive realizate pentru


a calcula irul lui Fibonacci. Se observ c Fib ( n-3 ) este calculat de 3 ori.

return fib onacciRec ( n 1) + fibonacciRec (n 2);


,o }
" }
Putem evita cu uurin recalcularea termenilor irului, calculnd elementele
irului succesiv, de la F la Fn i introducndu-le ntr-un tabel (cu o singur
linie) pe msur ce sunt calculate (Listing 14.2). Aceast idee, banal n apa
ren, reduce complexitatea algoritmului de la O (</>") la 0(n) i introduce un
concept fundamental pentru rezolvarea problemelor de programare dinamic:
tabelul n care se rein soluiile subproblemelor pentru a evita recalcularea lor.

i
*
3
4
5

Listing 14.2: Metoda iterativ de calcul a irului lui Fibonacci


public static final int f i b o n ac c ii t e r ( in t n)
{
int[] fib = (n >= 2) ? new int[n + l] : new int[2];
fib [0] = 0;
fib [ 1 ] = 1 ;

7
x
9
,o
ii
,2 )

for (int i = 2; i <= n; ++i)


{
fib [i ] = fib [ i - 1] + fib [i -2];
)
return fib [n ] ;

Listing 14.3: Exemplu simplu de utilizare a metodei fibonaccilter pentru


a calcula termenul de ordin n din cadrul irului lui Fibonacci
189

14.2. PRIMII PAI N PROGRAMAREA DINAMIC

1 import java . io . * ;
2 import io . Reader ;
4 public elass Fibonacci
5!
6 public static int f i b o n ac c ii t e r ( in t n)
> 1
8
// . . .
" )
10
ii public static void main ( String args [ ] )
<2 !
13
System . out . p ri n t (" n = ");
14
int n = Reader . readlnt O;
15
16
i f ( n < 1 ) return ;
17
s
System . out . p ri n 1 1 n (" Termenul " + n + " din irul" +
19
" Fibonacci este : " + fi b o n a c c i 1 1 e r ( n 1));
20 }
21 }
Clasa Fibonacci din Listing 14.3 citete un numr ntreg n de la tastatur
i calculeaz valoarea irului lui Fibonacci la poziia n (altfel spus, termenul de
ordin n al irului) folosind metoda f ibonaccilter ( ) .
Calculul combinrilor
n cadrul calculului irului lui Fibonacci din paragraful anterior am uti
lizat un tabel unidimensional pentru a reine valorile irului. In exemplul care
urmeaz vom prezenta o problem n rezolvarea creia este necesar utilizarea
unui tablou bidimensional (utilizarea tablourilor bidimensionale este mai frec
vent n rezolvarea problemelor de programare dinamic).
Este cunoscut faptul c prin combinri de n luate cte k (notate C(n, k)) se
nelege numrul de submulimi care conin k elemente ale unei mulimi cu n
elemente. Formula matematic pentru combinri de n luate cte k este:
n'
C{n>k) = w^w.
Nu este ns avantajos ca numerele C(n, k) s fie calculate direct, folosind
formula de mai sus. Formula de recuren C(n, k) = C(n l,k) + C(n
l,k 1) este mult mai rapid, deoarece utilizeaz numai adunri i elimin
operaiile de mprire i nmulire care sunt mai costisitoare n timp. Metoda
190

14.2. PRIMII PAI N PROGRAMAREA DINAMIC

1
2
3
4
5
<>
7
*
9
,o
"

Listing 14.4: Calculul recursiv al combinrilor


public static int combRec(int n, int k)
{
if (n == k I I k == 0)
{
return 1 ;
}
eIse
{
return combRec(n 1, k) + combRecn 1, k 1);
)
}

Figura 14.2: Reprezentarea arborelui rezulat n urma apelurilor recursive gene


rate de recurena pentru calculul combinrilor (se observ recalcularea inutil a
valorilor C(n - 3, k - 1) i C(n - 3, k - 2)).

combRec ( ) din Listing 14.4 reprezint implementarea recursiv direct a for


mulei de recuren anterioare.
Aceast metod, dei este foarte elegant i compact, sufer de aceleai
recalculri inutile ale anumitor valori ca i algoritmul recursiv pentru calculul
irului lui Fibonacci din Listing 14.1. Recalculrile inutile sunt puse n eviden
de Figura 14.2, n care am reprezentat primele 3 nivele din arborele generat prin
calculul recursiv al combinrilor.
Notnd cu tnk numrul de operaii efectuate pentru a calcula C(n, k) folo
sind metoda de mai sus, obinem relaia de recuren:
tnk tnlk ~t~ tn lfc 1?

tnO tnn 1-

Iternd recurena de mai sus se obine cu uurin faptul c timpul de calcul


191

14.2. PRIMII PAI N PROGRAMAREA DINAMIC

Figura 14.3: Tabelul utilizat pentru calculul iterativ al combinrilor. Se observ


c acest tabel nu este altceva dect triunghiul lui Pascal.
C(0,0)
C(1,0)
C(2,0)
C(3,0)
C(4,0)

1
2
3
4
5
6
7
9
"
12
,3

C(l,l)
C(2,l)
C(3,l)
C(4,l)

C(2,2)
C(3,2)
C(4,2)

C(3,3)
C(4,3)

C(4,4)

Listing 14.5: Calculul iterativ al combinrilor


public static int comblter ( int n, int k)
{
int[][] comb = new int[n + l][n + 1];
for (int i = 0; i <= n; ++i)
!
comb [ i ] [ 0 ] = comb [i][i] = 1;
for (int j = 1; j < i;++j)
{
comb [ i ] [ j ] = comb [ i 1 ] [ j ] + comb [ i 1 ] [ j 1 ] ;
)
)
return comb[n][k];
}

pentru tnk este n 0(2").


Pentru a evita repetarea inutil a calculelor, vom folosi din nou un tabel n
care n celula (i, j) se va pstra valoarea lui C(i,j) (indicele i trebuie s fie
mai mare sau egal cu j, deci tabelul nostru va avea elemente doar sub diagonala
principal). Examinnd Figura 14.3 se constat c tabelul construit de noi nu
este altceva dect triunghiul lui Pascal.
Metoda comblter ( ) din Listing 14.5 realizeaz calculul iterativ al com
binrilor folosind un tabel (triunghiul lui Pascal). Complexitatea acestei metode
este 0(n2), deci ea este net superioar ca i eficien metodei combRec ( ) din
Listing 14.4.
In cazul irului lui Fibonacci am evaluat termenii ncepnd cu cei de grad
inferior i terminnd cu cei de grad superior. n cazul combinrilor am calculat
treptat mai nti combinri de un element, apoi combinri de dou elemente
etc, deoarece C(n, k) depinde direct de C(n 1, k) i C(n 1, k 1), deci
de dou valori aflate pe linia anterioar n tabelul reprezentnd triunghiul lui
Pascal. O idee important care reiese de aici cu uurin este urmtoarea:
192

14.2. PRIMII PASI N PROGRAMAREA DINAMICA

ntotdeauna n cazul unei probleme de programare dinamica este


necesar ca n momentul n care se rezolva o problema, toate subproblemele ei sa fost deja rezolvate.
Strategia de rezolvare pentru problemele de programare dinamicadecurgeastfel
normal din aceasta observatie:
se rezolva mai nti problemele de dimensiune mica pentru care solutia
este evidenta (n cazul nostru acestea sunt
Ct
, respectiv
X"f3 );
pe baza suproblemelor de dimensiune inferioara se rezolva subproblema
cu un ordin de dimensiune mai mare (n cazul nostru, pe baza lui P si
#
se calculeaza , respectiv pe baza lui
#" ,
'
x
se calculeaza h( ).
Clasa CombinariIt din Listing 14.6 reprezinta un exemplu de simplu de
utilizare a metodei combIter() pentru a calcula combinarile a doua numere
citite de la tastatura.
Problema parantezelor
Cele doua probleme prezentate anterior sunt relativ simple, deoarece recurenta care trebuie rezolvata apare explicit n enunt. Att pentru sirul lui Fibonacci, ct si pentru calculul combinarilor, ni se furnizeaza explicit relatia de
recurenta pe care trebuie sa o rezolvam. Problema parantezelor este un exemplu de problema n care recurenta care trebuie rezolvata nu apare explicit
n enunt. Ramne n sarcina noastra sa construim relatia de recurenta care rezolva aceasta problema. Din acest punct de vedere, problema parantezelor se
apropie mai mult de problemele reale de programare dinamica, deoarece nici
acolo recurenta care trebuie rezolvata nu este data explicit. Mai mult, am putea
chiar arma ca principala dicultate n cazul problemelor de programare dina
mica consta, mai ales la ncepatori, n formularea corecta a relatiei de recurenta
care rezolva problema. Asadar, dicultatea principala n cazul problemelor de
programare dinamica este construirea recurentei, scrierea programului pentru
rezolvarea propriu-zisa a recurentei ind n cele mai multe cazuri banala.
Enuntul problemei parantezelor este simplu:
Dndu-seun numarnatural g , se cere sa se determine numarul
de siruri avnd v paranteze care se nchid corect.
193

14.2. PRIMII PAI N PROGRAMAREA DINAMIC


Listing 14.6: Soluia complet a problemei combinrilor
1 import java . io . * ;
2 import io . Reader ;
4 public elass Combinarilt
5(
6 public static int combinarilter ( int n, int k)
' !
8
// . . .
11
10
1 1 public static void main ( String ar.?s [])
12 {
13
System . out . p ri n t (" n = ");
14
int n = Reader . readlnt () ;
15
System . out . p ri n t ( "k = ");
16
int k = Reader . readlnt () ;
17
18
if (n < 1 1 1 k < 0 1 1 k > n)
19
{
20
System . out . println ( "n trebu ie sa fie > =
21
System . out . println ( "k trebu ie sa fi e > =
22
" mai mic dect n" ) ;
23
return ;
24
}
25
26
System . out . p ri n 1 1 n ( "Comb (" + n + " , " +
27
combinarilter (n , k));
1i
29 }

De exemplu, pentru n = 2, irurile nchise corect sunt ()() i (()), deci rspun
sul este 2.
Soluia (celebr) a acestei probleme este reprezentat de aa-numitele nu
mere catalane (vezi [Cormen], pag. 261). Totui, datorit scopului acestui para
graf, vom da o alt soluie a problemei, bazat pe o relaie simpl de recuren.
Este cert c orice ir de paranteze nchise corect ncepe cu o parantez des
chis "(" S considerm acum paranteza care nchide aceast prim parantez.
Ceea ce se afl ntre aceste dou paranteze este tot un ir de paranteze care se
nchid corect; la fel i pentru irul care se afl n dreapta lor. Deci, un ir S de
paranteze care se nchid corect se poate scrie ca (Sl)S2, unde ST i S2 sunt
alte iruri de paranteze care se nchid corect, posibil vide.
Lungimea maxim a lui ST este 2 n 2 (atins cnd S2 este vid), iar cea
minim este 0 (atins cnd S2 are lungimea 2 n 2). S notm cu P(n)
194

14.2. PRIMII PAI N PROGRAMAREA DINAMIC


numrul de iruri avnd 2n paranteze care se nchid corect. Se observ cu
uurin c paranteza care nchide prima parantez poate fi pe oricare dintre
poziiile 2, 4, 6, ... , 2n. Astfel, dac paranteza este pe poziia 2, atunci irul
51 este vid, iar irul S2 va avea lungimea 2n 2 = 2{n 1), deci vor exista
P(0)P(n 1) iruri de paranteze care se nchid corect. Analog, dac paranteza
este pe poziia 4, irul SI va avea lungimea 2, iar irul S2 va avea lungimea
2n 4 = 2(n 2), deci vom avea P(l)P(n 2) iruri care se nchid corect.
Obinem astfel recurena:
J
1
= l Efe=o P{k)P{n -k-l)

pentru n = 0
pentru n > 1

Implementarea recurenei de mai sus este simpl i este prezentat n Listing 14.7.
Listing 14.7: Soluia problemei parantezelor
1 import j ava . io . * ;
2 import io . Reader ;
3
4 public class Paranteze
5{
6 public static int p ar an t e z el t e r ( in t n)
' {
s
int[] p = new int[n + 1];
p[0] = 1;
]()
ii
for (int i = 1; i <= n; i ++)
!
13
for ( int k = 0 ; k <= i - 1 ; k++)
!
15
p[i]+=p[k]*p[i k 1];
}
}
18
19
return p [n ] ;
20 }
21
22 public static void main(String args [ ] )
* {
24
System . out . prin ( "n = ");
25
int n = Reader . re adl n t ( ) ;
26
27
if ( n < 0) return ;
28
29
Sy stern . out . p ri n 1 1 n (" Numrul de iruri cu " + 2 * n +
30
" paranteze care se inchid corect este : " +
31
p ar an t ez el t er ( n ) ) ;
195

14.2. PRIMII PAI N PROGRAMAREA DINAMIC


32 }
33 }

Descopunerea unui numr natural ca sume distincte


Problema descompunerii unui numr natural n sume distincte este un alt
exemplu n care recurena care furnizeaz soluia trebuie construit de ctre
programator. Enunul problemei este:
Dndu-se un numr natural n, se cere s se determine numrul
de moduri distincte3 n care acesta se poate scrie ca sum de nu
mere naturale nenule.
De exemplu, numrul n = 5 se poate scrie ca:
1+1+1+1+1
1+1+1+2
1 + 1+3
1+2 + 2
1+4
2+3
5
Astfel, pentru n = 5 am obinut 7 descompuneri (5 se consider aici ca fiind
sum; se poate modifica problema excluznd aceast posibilitate, dar rezolvarea
se pstreaz, din numrul total de descompuneri trebuind doar s fie sczut 1).
Pentru a obine recurena pentru aceast problem, vom rezolva mai nti un
caz particular:
Dndu-se un numr natural n, s se determine numrul de
moduri distincte n care acesta poate fi descompus n sum de m
numere naturale.
S notm cu S(n, m) numrul de moduri distincte n care n se poate scrie ca
sum de m termeni. Evident, soluia problemei originale va fi
n
i=l
Problema original se reduce aadar la a calcula S(n, i) i = 1, 2, . . . n.
Se observ cu uurin c S(i, 1) = S(i,i) = 1 (acestea sunt condiiile
iniiale). De asemeni, S(i, i + k) = 0 pentru oricare k numr natural pozitiv.
^Adunarea fiind comutativ, vom considera c descumpunerile 2 + 3^/3 + 2 sunt echivalente.
196

14.2. PRIMII PAI N PROGRAMAREA DINAMIC

i
^
3
4
5
*
7
8
io

16
17
18
19
20
21
22

Listing 14.8: Metoda de calcul a numrului de descompuneri


public static int descompuneNumar ( int n )
{_int s = new int[n][n] ; //alocam memorie pentru tabelul s
// construim tabelul s
for ( int i=0; i<n; ++i )
{
s [ i ] [ 0 ] = s [ i ] [ i ] = 1 ; //condiiile iniiale
for ( int j=l; j<i; ++i J
!
for ( int k = 0; k<j ; + + k )
!
s[i][j] += S[i-j][k] ;
)
}
I
int nrDescompuneri = 0 ; //numrul de descompuneri ale lui n
for ( int i=0; i<n; ++i )
{
nrDescompuneri += s[n l][i] ;
}
return nrDescompuneri ;
}

Putem determina o relaie de recuren pentru S(i, j) n cazul general (i >


j) fcnd urmtoarea observaie simpl:
Fiecare dintre cei j termeni care nsumai dau i sunt mai mari sau
egali cu 1. Dac scdem valoarea 1 din fiecare dintre termeni, suma
se va micora cu j, iar termenii care fuseser 1 devin 0, deci dispar.
innd cont de aceast observaie i de faptul c numrul de termeni care sunt
mai mari strict dect 1 este de minim 1 i maxim j, putem scrie urmtoarea
relaie de recuren:
i
= ^2S(i-j,k)
k=l
Metoda descompuneNumar ( ) din Listing 14.8 rezolv recurena de mai
sus folosind trei cicluri for imbricate.
Analiznd structura ciclurilor imbricate din metoda de mai sus, reiese c
aceasta are o complexitate n 0(n3). Putem ns reduce complexitatea la 0{n2)
197

14.3. FUNDAMENTARE TEORETIC


folosind urmtorul artificiu de calcul:
j+i
3
S(i + 1, j + 1) = 5(i - j, k) =
s(i ~ 3, k) + S(i - j, j + l) =
k=l
k=l
= S(i,j) + S(i-

Astfel, termenii S(i,j) sunt calculai n 0(1) i nu n 0(n), cum erau calculai
nainte. Modificarea algoritmului pentru noua formul este simpl i o lsm ca
exerciiu.
n concluzie, dei problemele prezentate n aceast seciune nu sunt pro
bleme de programare dinamc propriu-zise, deoarece nu sunt de optimizare,
ele constituie un excelent exerciiu pentru nelegerea principiilor programrii
dinamice, i mai ales pentru deprinderea abilitii de a gsi relaii de recuren
adecvate pentru exprimarea soluiei.

14.3

Fundamentare teoretic

n paragraful anterior am fcut o scurt trecere n revist a unor probleme


simple a cror rezolvare const n determinarea i calcularea unor recurene ma
tematice. Am accentuat tratarea iterativ n contrast cu cea recursiv a relaiilor
de recuren, subliniind n acelai timp i modul n care problemele se suprapun
n cazul rezolvrii recursive. Totui, aceste probleme nu pun n eviden dect
un anumit aspect al programrii dinamice, fr a aparine propriu-zis acestui
domeniu. n acest capitol vom urmri s vedem care sunt caracteristicile eseniale ale problemelor de programare dinamic pe baza unui exemplu clasic:
problema triunghiului de numere.
n cazul acestei probleme se d un triunghi de numere Zij, j <= i avnd
forma:

Figura 14.4: Triunghi de numere de dimensiune n


Zll
Z21 Z22
Znl Zn2 Znn
Pentru n = 4, un posibil triunghi de numere este:
198

14.3. FUNDAMENTARE TEORETIC


2
6 9
3 4 5
9 5 7 6
Un drum n acest triunghi este o secven de n elemente ale triunghiului (n
este numrul de linii) care ncepe ntotdeauna cu elementul de pe prima linie,
Z\i, (2 n exemplul nostru) i coboar linie cu linie pn ajunge pe ultima linie
a triunghiului. Din elementul Zij se poate merge fie n Zj+i j, fie n Zj+i
Elementele ngroate din exemplul urmtor reprezint un posibil drum:
2
6 9
3 4 5
9 5 7 6
Problema const n a determina un drum n triunghi pentru care suma ele
mentelor s fie maxim. n exemplul nostru, un drum de sum maxim este:
2
6 9
3 4 5
9 5 7 6
avnd valoarea 2 + 9 + 5 + 7 = 23.
nainte de a ne avnta s rezolvm o problem folosind programarea di
namic, trebuie s ne convingem c nu putem folosi i strategii mai simple
(cum ar fi Greedy) pentru a obine soluia optim. O strategie Greedy n cazul
problemei triunghiului ar porni din vrful triungiului i ar cobor la fiecare pas
prin elementul mai mare. Aplicnd aceast strategie pe exemplul anterior, am
porni din 2, apoi am cobor n 9 (deoarece 9 > 6), apoi am continua cu 5
respectiv 7. Am obinut astfel drumul 2, 9, 5, 7, care este chiar drumul optim!
Am putea fi tentai s credem c putem aplica strategia Greedy. S considerm
ns exemplul urmtor:
2
4 3
6 5 9
Soluia obinut prin strategia Greedy este 2, 4, 6, avnd lungimea total 12,
n timp ce soluia optim este 2, 3, 9, avnd lungimea total 14. Rezult aadar
c strategia Greedy nu genereaz ntotdeauna soluia optim.
Pentru a rezolva o problem printr-un algoritm de programare dinamic, o
prim etap esenial const n a descompune (structura) problema n subprobleme asemntoare ca structur cu problema iniial.
Structurarea pe subprobleme asemntoare se face n cazul problemei tri
unghiului pe baza urmtoarei observaii: fiecare element din triunghi poate fi
199

14.3. FUNDAMENTARE TEORETICA

considerat vrful unui sub-triunghi. De exemplu elementul


din triunghiul de mai sus este vrful sub-triunghiului

iar elementul de pe pozitia

de pe pozitia

este vrful sub-triunghiului

Odata determinate subproblemele, ncepem prin a le rezolva subproblemele


banale (de dimensiune minima). Astfel, vom ncepe prin a determina un drum
de suma maxima pentru subproblemele generate de elementele din ultima linie
a triunghiului. Acest lucru este banal, ntruct triunghiurile generate de catre
elementele de pe ultima linie au un singur element, deci drumul maxim este dat
chiar de elementele respective:
Avnd rezolvate subproblemele generate de elementele de pe ultima linie,
vom trece sa rezolvam subproblemele generate de elementele de pe penultima
linie (). Pentruecare elementde pe penultima linie avem doua posibilitati:
e se merge pe elementul de pe linia aat chiar dedesubt, e pe elementul de
pe linia aat n dreapta. Evident, vom alege varianta (subproblema) pentru
care drumul corespunzator este mai mare. De exemplu, pentru elementul 3,
(3 + 9 = 12) dect pe traseul
vom prefera sa mergem pe traseul r
(3 + 5 = 8). Astfel, valoarea drumului optim pentru subproblemele generate de
elementele de pe penultima linie este:

Analog procedam si pentru elementele aate pe a antepenultima linie (a


doua n cazul nostru) linie. Si n acest caz putem merge e pe elementul aat
dedesubt e pe elementul aat n dreapta-jos. Vom alege, desigur, elementul
a carui subproblema are valoarea mai mare. De exemplu, din elementul 6 am
putea cobore prin3, e prin 4. ntructsubproblemageneratade 3 are drumul
maxim de valoare 12, iar subproblema generata de 4 are drumul maxim de
pentru
pe antepenultima
valoaresubproblemele
11, vom alege drumulcare
generate de elementele
trece prin 3.deAstfel,
valoarea drumuluioptim
linie este:

n nal, pentru elementul din vrful triunghiului, vom alege sa mergem pe


traseul din dreapta, deoarece subproblema generata de elementul din dreapta
200

14.4. PRINCIPIUL OPTIMALITII


jos (9) are valoare mai mare. Obinem astfel valoarea drumului optim pentru
elementul din vrf (care este de fapt valoarea optim pentru problema original),
ca fiind 2 + 21 = 23:
23
18 21
12 11 12
9
8
7 6

14.4

Principiul optimalitii

Desigur c structurarea n subprobleme ar fi fost lipsit de sens dac nu near fi permis s rezolvm subproblemele corespunztoare unui nivel bazndu-ne
pe soluiile subproblemelor deja rezolvate. Am anticipat deci faptul c soluia
unei subprobleme se determin pe baza soluiei subproblemelor de dimensiune
mai mic, deja rezolvate. n aceast situaie, vom spune c soluia subproblemei
x provine din soluiile subproblemelor y\ , yi , . . . yn .
Problemele de programare dinamic sunt n general probleme de optimizare,
iar problema triunghiului nu face nici ea excepie. Din acest motiv, este necesar
ca i subproblemele n care am descompus problema original s fie tot de opti
mizare, n fine, cea mai important caracteristic a problemelor de programare
dinamic este urmtoarea:
Dac soluia subproblemei x provine din soluiile subproblemelor
Vi > 2/2 ; Vn i soluia subproblemei x este optimal, atunci i solui
ile subproblemelor yi , y2 , yn sunt optimale.
Aceast proprietate poart numele de principiul optimalitii i constituie piatra
de temelie a programrii dinamice.
n general, dup ce am realizat o structurare a problemei iniiale n subpro
bleme, trebuie verificat principiul optimalitii. Aceast verificare se face cel
mai adesea prin reducere la absurd.
Observaie: Dificultatea nu const n a verifica principiul optimalitii, ci n
a gsi o structurare a problemei n subprobleme care s verifice acest principiu.
S demonstrm acum faptul c problema triunghiului de numere respect
principiul optimalitii. O soluie a unei subprobleme const ntr-un traseu
optimal care pornete din vrful triunghiului corespunztor subproblemei i
ajunge la baza triunghiului. Notm acest traseu cu a^, cifc+i, . . . , an, unde a&
este un element de pe linia k, dk+i este un element de pe linia k + 1, iar an
este un element de pe ultima linie. Pentru a se respecta principiul optimali
tii trebuie ca i subtraseul Ofc+i , . . . ,an s fie optimal pentru sub-triunghiul
201

14.4. PRINCIPIUL OPTIMALITII


generat de elementul Ofc+i. Verificarea se face uor prin reducere la absurd.
Dac admitem c ar exista un alt traseu care s porneasc din Ofc+i, notat
cu Ofc+i, 6^+2, , bn, care s fie mai bun dect traseul o^+i, . . . , an, atunci
traseul , dk+i , bk+2 , bn ar fi mai bun dect , dk+i , ...,a, ceea ce con
trazice ipoteza c o^, cifc+i, . . . ,an este optim. Rezult deci c structurarea n
subprobleme realizat de noi respect principiul optimalitii.
Nu ne rmne acum dect s scriem formula de recuren care descrie cum
se obine soluia unei subprobleme pe baza subproblemelor de dimensiune mai
mic deja rezolvate. S notm cu lij valoarea traseului optim pentru subproblema generat de elementul Zij, aflat pe linia i i coloana j (i > j). Este uor de
observat c lni = zni Vi = 1 . . . n, deoarece drumul maxim pentru elementele
de pe ultima linie este reprezentat chiar de elementul respectiv. Am vzut c 1^
se obine fie din
j fie din
j+i la care se adaug valoarea elementului
Zif.
hj = Zij + max{lj+1

Vi = 1 . . .n - 1, j = 1 . . .i.

Valoarea lui In reprezint chiar soluia pentru problema original.


Calcul recurenei de mai sus este realizat de metoda drumMaximTr iunghi ( ) din Listing 14.9.

Reciproca principiului optimalitii nu este adevrat


O alt observaie important este aceea c, n general, reciproca principiului
optimalitii nu este adevrat. Cu alte cuvinte, combinnd dou sau mai multe
sub-soluii optime nu se obine neaprat tot o soluie optim. S presupunem
c dorim s aflm drumul de lungime minim dintre dou localiti, s zicem
Braov i Constana. Conform principiului optimalitii, dac avem un drum
optim, x, ntre Braov i Constana care trece prin Ploieti, atunci subdrumul
lui x de la Ploieti la Constana este de asemenea optim. Totui, dac avem
un drum optim ntre Braov i Suceava i un alt drum optim ntre Suceava i
Constana, prin combinarea acestor dou drumuri vom obine un traseu BraovSuceava-Constana care este departe de a fi optim. Rezult deci c reciproca
principiului optimalitii nu este adevrat n acest caz.
Este important s nelegem c principiul optimalitii nu ne asigur c prin
combinarea soluiilor optime se obine tot o soluie optim. n schimb, el ne asi
gur c n cutarea unei soluii optime nu trebuie s lum n considerare dect
subsoluiile optime.
202

14.4. PRINCIPIUL OPTIMALITII


Listing 14.9: Rezolvarea recurenei pentru problema triunghiului
1 /* *
2 * Calculeaz formula de recurenta pentru matricea L.
3 * @ return Lungimea drumului maximal pentru triunghi
4 * care se presupun a fi membri ai clasei care conine metoda.
5 */
6 public static int drumMaximTriunghi ()
'{
8 //iniializeaz ultima linie din L
9 for (int i = 0; i < z.length; ++i)
,0 {
ii
l[z.length - l][i] = z[z.length - l][i];
'2 )
L3
14 // calculeaz elementele din l linie cu linie
15 for (int i = z.length 2; i>=0;
i)
16 {
17
for (int j = 0; j <= i; ++j)
(
l[i][j] = z[i][j] + Math.max(l[i+l][j ], 1 [ i + 1 ] [ j + 1 ] ) ;
20
}
2, }
22
23 return 1 [0] [0] ;
24 }

O alt descompunere n subprobleme


Este interesant de remarcat faptul c problema triunghiului de numere ad
mite i o alt descompunere n subprobleme care verific i ea principiul opti
malitii, i care conduce la o alt soluie a problemei. In descompunerea n sub
probleme pe care am dat-o anterior am notat prin lij lungimea drumului maxim
care ncepe cu elementul Zij. De data aceasta, vom nota cu rriij lungimea dru
mului maxim care se ncheie cu elementul z%j . Considernd acelai triunghi de
numere:
2
6 9
3 4 5
9 5 7 6
subproblema corespunztoare elementului 4 din linia a treia este dat de "tie
tura" n sus generat de acest element:
203

14.4. PRINCIPIUL OPTIMALITII


2
6 9
3 4
unde drumul maxim care se termin cu 4 a fost ngroat. Verificarea principiului
optimalitii se face foarte uor i de aceast dat: este clar c un drum de
lungime maxim este format din sub-drumuri care sunt tot de lungime maxim.
Odat determinate subproblemele, ncepem din nou prin a le rezolva pe
cele triviale. Astfel, vom determina mai nti un drum de sum maxim pentru
elementul din prima linie a triunghiului. ntruct triunghiul generat de ctre
elementul din prima linie are un singur element, drumul optim este dat chiar de
elementul respectiv.
Avnd rezolvate subproblemele generate de elementele de pe prima linie,
vom trece s rezolvm subproblemele generate de elementele de pe umtoarele
linii, obinnd triunghiul:
2
8 11
11 15 16
20 20 23 22
Pentru fiecare element al triunghiului (mai puin cele din prima i ultima co
loan a fiecrei linii) avem dou posibiliti: fie se ajunge la el de pe elementul
aflat imediat deasupra, fie de pe elementul aflat pe diagonala stnga-sus. Evi
dent, vom alege traseul (subproblema) pentru care drumul corespunztor este
mai mare. De exemplu, pentru elementul 4, vom prefera s mergem pe traseul
... 9 4 (avnd lungimea 11 +4 = 15) dect pe traseul ... 6 4 (avnd
lungimea 8 + 4 = 12).
Soluia subproblemei iniiale este dat de subproblema corespunztoare elementului de pe ultima linie, a crei valoare este maxim. Rezult deci c
lungimea drumului maxim este 23, aadar am regsit valoarea obinut aplicnd
cealalt descompunere n subprobleme. Formulele de recuren pentru calculul
elementelor rriij sunt:
rriij = Zij + max(mi_i

m^j)

mu = zn,

unde facem convenia c mo i = ?Tij+i j = 0.

14.4.1

Metoda "nainte" i metoda "napoi"

Aa cum probabil ai remarcat deja, cele dou descompuneri n subproble


me pe care le-am descris pentru problema triunghiului de numere sunt comple
mentare:
204

14.4. PRINCIPIUL OPTIMALITII


n prima variant se consider sub-drumuri care ncep cu un anumit ele
ment, n cea de-a doua variant se consider sub-drumuri care se termin
cu un anumit element;
n prima variant se construiete drumul optim pornind de la baza tri
unghiului, n timp ce n cea de-a doua variant se construiete drumul
optim pornind de la vrful triunghiului;
n prima variant recurena merge "nainte" de la elementul i la elementul
i + 1, n cea de-a doua variant recurena merge "napoi", de la elementul
i la elementul i 1 .
Unii autori clasific metodele de rezolvare a problemelor de programare dina
mic n trei clase:
1 . Metoda "nainte", n care ne folosim de faptul c optimalitatea subsoluiei
<Zfc, cifc+i, . . -dn implic optimalitatea subsoluiei dfc+i, . . . , an;
2. Metoda "napoi", n care ne folosim de faptul c optimalitatea subsoluiei
di , . . . dk-i , dk implic optimalitatea subsoluiei a, . . .aki',
3. Metoda "mixt", n care ne folosim de faptul c optimalitatea soluiei de
tipul ai , . . . , dk , an implic optimalitatea att a subsoluiei Oi , . . . dk-i
ct i a sub-soluiei dk+i ,...,a.
n prima descompunere n subprobleme am aplicat metoda "nainte", iar n a
doua descompunere n subprobleme am aplicat metoda "napoi". Vom prezenta
n paragraful 14.5 i un exemplu de problem (parantezarea unui ir de matrice)
n care se aplic metoda "mixt".
14.4.2

Determinarea efectiv a soluiei optime

Dei rezolvarea recurenelor asociate unei probleme de programare dina


mic ne conduce la aflarea valorii optime cutate, ea nu furnizeaz i soluia
efectiv pentru care se obine acea valoare optim. De exemplu, n cazul pro
blemei triunghiului de numere am calculat care este valoarea drumului optimal3
dar nu am gsit efectiv care este drumul pentru care se obine valoarea optim.
Vestea bun este c aflarea soluiei optime a problemei este n general sim
pl i se bazeaz de cele mai multe ori pe reconstituirea "traseului" prin care
s-a obinut valoarea soluiei optime. n cazul problemei triunghiului de numere
putem afla cu uurin care este drumul pentru care se obine soluia optim,
205

14.4. PRINCIPIUL OPTIMALITII


reconstituind traseul prin care s-a obinut valoarea optim. Pentru exemplul
nostru, triunghiul construit prin rezolvarea relaiilor de recuren era:
23
18 21
12 11 12
9
5
7 6
Se observ cu uurin c valoarea 23 din vrful triunghiului a fost obinut
din elementul 21 aflat n dreapta jos (prin adugarea valorii 2 din vrful triunghi
ului original), valoarea 21 a fost obinut din elementul 12 aflat la dreapta, iar
12 din 7.
Metoda af iseazaDrumMaxim ( ) din Listing 14.10 calculeaz drumul
maxim pentru problema triunghiului. La linia 33 se iniializeaz variabila s cu
valoarea drumului maxim, aflat n vrful triungiului. Drumul propriu-zis este
construit n ciclul while din liniile 38-53, n care se parcurge matricea 1 de
sus n jos, linie cu linie. La fiecare iteraie, variabila s conine valoarea ctre
care trebuie s coborm. Astfel, dac s este egal cu 1 [ i + 1 ] [j + 1] (linia 46)
se coboar direct n 1 [ i + 1 ] [j + 1], altfel se coboar n 1 [ i + 1 ] [ j + 2 ] .
Listing 14.10: Soluia complet a problemei drumului maxim
1 import java . io . * ;
2 import io . Reader ;
4 public elass DrumulMaxim
5!
6 public static int drumMaximTriunghi ( int [ ] [ ] z)
' 1
s
i n t [ ] [ ] 1 = new int [z. length ][z. length ];
io
12
14
15
17
20

24
25
26
27
206

for (int i = 0; i < z. length; i++)


{
l[z. length - l][i] = z[z. length - l][i];
}
for (int i = z. length 2; i >=0; i
{
for (int j = 0; j <= i; j ++)
{
l[i][j] = z[i][j] +
Math . max( 1 [ i + 1 ] [ j ] ,
l[i + l][j + 1]);
}
}
afiseazaDrumMaxim ( z , 1);
return 1 [0] [0] ;

14.4. PRINCIPIUL OPTIMALITII


28
29
30

32
33
34

}
public
{
int s
int i
int j

36
37
38
39
41)
4i
42
43
44

static void afiseazaDrumMaxim ( int [ ] [ ] z, int [ ] [ ] 1)


= 1 [0] [0] ;
= 0;
= 0;

System . out . prin ( "Drumul maxim: ");


while ( s ! = 0 )
{
System . out . p ri n t ( z [ i ][ j ] + " ");
s -= z [ i ] [ j ] ;
if ( i + 1 < z . length )
{
if (S == l[i + l][j + 1])
{
+;
}

46
47
48
49
50

i++;
}

53
54
55
56

58
59
60
61
62
63
64
65
66
67
68
69
70

}
Sy stem . out . p r i n 1 1 n ( ) ;
}
public static void main(String args [ ] )
{
System, out. prin (" Dimensiune triunghi: ");
int n = Reader . re adl n t ( ) ;
int[][] z = new int[n][n];
System . out . p ri n 1 1 n (" E lementele triunghiului:");
for (int i = 0; i < n; i ++)
{
for ( int j = 0 ; j <= i ; j ++)
{
System . out . p ri n t (" z [ " + i + "][" + j
+"]=");
z[i][j] = Reader . readlnt () ;
}

}
73
74
System, out. println(" Suma maxima = " + drumMaximTriunghi ( z ) ) ;
}
76 }
207

14.5. NMULIREA UNUI IR DE MATRICE


14.5

nmulirea unui ir de matrice

Urmtorul exemplu de problem de programare dinamic pe care l vom


studia este problema nmulirii optimale a unui ir de matrice. n cazul acestei
probleme se d o secven A, A% . . . , An de matrice care trebuie nmulite. Cu
alte cuvinte, se dorete calcularea produsului
AxA2...An,
unde Ai are dimensiunile di-i x di. Avnd n vedere faptul c nmulirea matricelor este asociativ, calculul produsului de mai sus se poate face n mai
multe moduri, n funcie de ordinea n care decidem s realizm operaiile de
nmulire. De exemplu, pentru n = 3 exist dou moduri n care se poate cal
cula produsul:

sau
(A^As.
Unii cititori i pot pune n mod legitim ntrebarea: innd cont de faptul c
nmulirea matricelor este asociativ, oricum am pune parantezele, rezultatul
final al calcului va fi acelai. Aadar ce sens are s ne preocupm de ordinea
de realizare a operaiilor de nmulire? Rspunsul este c numrul de operaii
elementare de nmulire este diferit, funcie de modul n care alegem s punem
parantezele. De exemplu, pentru n = 3, s presupunem c cele 3 matrice au
respectiv dimensiunile (10, 50), (50, 20) i (20, 1). Numrul de operaii necesar
pentru a nmuli dou matrice de dimensiune (m,n) i (n,p) este m n p,
dup cum reiese clar din algoritmul din Listing 14.11 (considerm ca barometru
operaia de nmulire scalar a[i][fc] *
Dac vom calcula produsul celor
3 matrice dup primul mod (Ai{A2A3)), vom face 50 x 20 x 1 = 1000 de
operaii pentru a calcula produsul ^42^3, plus nc 10 x 50 x 1 = 500 de operaii
pentru a nmuli rezultatul cu A. Aadar, n total vom realiza 1000 + 500 =
1500 de nmuliri scalare. Dac vom calcula produsul celor 3 matrice n al
doilea mod ((A1A2)A3), vom face 10 x 50 x 20 = 10000 de operaii pentru a
calcula produsul AXA2 plus nc 10 x 20 x 1 = 400 operaii pentru a calcula
produsul rezultatului cu A3. Vom face aadar un total de 10400 de operaii ceea
ce nseamn cam de 7 ori mai mult dect n varianta precedent. n concluzie,
are sens s ne punem problema de a gsi cea mai eficient metod de a realiza
aceast nmulire.
O soluie imediat a problemei ar fi s se gseasc toate modurile posibile
de parantezare a irului de matrice i s se aleag cea pentru care numrul de
208

14.5. NMULIREA UNUI IR DE MATRICE


operaii este minim. Totui, numrul de parantezri pentru un ir de lungime n
este (vezi [Cormen], p. 261)
C2 G 0(4"/n3/2)
ceea ce exclude din start posibilitatea cutrii exhaustive datorit numrului ex
ponenial de variante.

Listing 14.11: nmulirea a dou matrice de dimensiune m-n respectiv n-p. Se


observ cu uurin c numrul de operaii de nmulire realizate este m*n*p
1 /* *
2 * Calculeaz produsul a doua matrice de numere ntregi.
3 * @ return O matrice de numere ntregi care reprezint produsul
4 * matricelor date ca parametru .
5 */
6 public static int[][] produs ( int [][ ] a, int[][] b)
'{
s int[][] produs = new in t [ a . length ] [ b [ 0] . length ] ;
9 for (int i = 0; i < a. length; ++ i )
,o {
n
for (int j = 0; j < b [ 0 ]. length ;++ j )
{
13
produs [ i ] [ j ] = 0 ;
14
for (int k = 0; k < b. length; + + k)
1
i6
produs[i][j] += a [ i ] [k]*b [k] [ j ] ;
)
}
i 1
20
21 return produs ;
12 }

Caracterizarea substructurii optime


O parantezare optim a produsului A\A2 . . . An mparte secvena ntre Ak
i Af.+i, unde k este un numr natural n intervalul l..n 1. Aceasta nseamn
c mai nti se calculeaz A\ . . . A/., apoi Af.+i . . . An i n final se nmulesc
cele dou rezultate pentru a obine produsul AiA2 . . . An. Numrul total de
nmuliri realizate este egal cu numrul de nmuliri necesare pentru calculul lui
Ai . . . Af. plus numrul de nmuliri necesare pentru a calcula Af.+i . . . An la
care se adaug numrul de operaii necesar pentru a nmuli cele dou rezultate.
209

14.5. NMULIREA UNUI IR DE MATRICE


Aceast modalitate de mprire ne duce cu gndul la a considera subprobleme de forma Pij = AiAi+ . . . Aj, cu 1 < i < j < n. Observaia esenial
n cazul acestei probleme este c dac parantezarea optim pentru a calcula
Ai . . . Aj mparte produsul pe poziia k,
Pij = (Ai . . . Ak)(Ak+1 ...Aj)
atunci parantezarea subirului Ai . . . Ak din cadrul parantezrii optime a irului
Ai . . . Aj este o parantezare optim pentru irul Ai . . . Ak (dac nu ar fi op
tim, atunci nlocuirea respectivei parantezri cu cea optim ar produce o alt
parantezare pentru Ai . . . Aj al crei cost ar fi mai mic dect cel optim, ceea
ce este absurd). O observaie similar este valabil i pentru parantezarea lui
Ak+i Aj din cadrul parantezrii optime a lui Ai . . . Aj, care trebuie s fie
optim pentru irul Ak+i Aj. Am artat aadar c o soluie optim pentru
problema noastr este compus din subsoluii optime ale subproblemelor, ceea
ce ne permite s aplicm programarea dinamic.
Obinerea formulelor de recuren
Odat gsit o descompunere n subprobleme care respect principiul optimalitii, putem trece la definirea valorii unei soluii optime n funcie de solui
ile optime ale subproblemelor. Fie rriij numrul minim de nmuliri necesare
pentru a calcula Pij = Ai . . .Aj. Scopul final este s calculm m\n. Dac
i = j, atunci irul const ntr-o singur matrice, deci nu avem nevoie de nici o
nmulire scalar. Vom avea astfel
fn%% = 0, Vi = l..n.
Pentru a calcula rriij n cazul general (i < j) ne vom folosi de substructura
optimal definit n paragraful anterior. S presupunem c o parantezare optim
a irului Ai . . . Aj mparte produsul ntre Ak i Ak+i cu i < k < j. Am artat
deja c n aceast situaie numrul de operaii necesar pentru a calcula Py este
egal cu numrul de operaii necesare pentru a calcula Pa- plus numrul de ope
raii necesare pentru a calcula Pk+ij plus costul nmulirii celor dou matrice
rezultat. Avnd n vedere faptul c Pa- are dimensiunea (<2,_i, dk), iar Pk+ij
are dimensiunea (dk,dj), evaluarea produsului PikPk+ij necesit di-id^dj
operaii. Obinem astfel numrul de operaii necesare pentru a calcula rriij:
rriij = rriik + mu+ij + du-idudj.
Ecuaia de mai sus este valabil n situaia n care cunoatem poziia k n care se
descompune produsul. Cum noi nu tim care este aceast poziie, vom ncerca
210

14.5. NMULIREA UNUI IR DE MATRICE


toate variantele posibile pentru k i o vom alege pe cea pentru care se obine va
loarea minim. Din fericire, exist doar j i valori posibile pentru k, i anume
k = i,i + l, . . . j 1. n consecin, formula recurent pentru definirea costului
minim a parentezrii produsului Ai . . . Aj devine
(14.1)

Rezolvarea recurenei i calculul costului optimal


n acest moment este uor s scriem un algoritm care s calculeze recurena
pentru rriij. O prim alternativ, destul de tentant, este aceea de a imple
menta calculul recurenei direct, folosind un algoritm recursiv. O analiz simi
lar cu cea de la calculul recursiv al irului lui Fibonacci (paragraful 14.2.1),
ne convinge c aceast modalitate de calcul genereaz o complexitate expo
nenial, cu nimic mai bun dect ncercarea tuturor parantezrilor posibile.
n locul calculului recursiv al lui m,j vom folosi din nou un tabel n care
vom reine valorile intermediare ale subproblemelor. Ca de obicei, calculul
se va face bottom-up, adic se rezolv mai nti subproblemele de dimensiune
mic, urmate apoi succesiv de subprobleme de dimensiune mai mare, pn cnd
se ajunge la problema iniial. Metoda sirMatrice ( ) din Listing 14.12
implementeaz aceast strategie.
sirMatrice ( ) calculeaz matricea m mergnd pe probleme de dimen
siune din ce n ce mai mare. La prima iteraie (l = 0) algoritmul calculeaz
m[i][i + 1] (costul irurilor de lungime 2) pentru i = 0 . . .n 2 (indicii sunt
deplasai fa de notaia anterioar deoarece n Java indexarea tablourilor ncepe
de la 0). La a doua trecere prin ciclu (l = 1) se calculeaz m[i][i + 2] pentru
i = 0, 1, . . . , n 3 (costul irurilor de lungime 3) etc. Aadar, elementele ma
tricei m sunt calculate pe diagonal, mergnd paralel cu diagonala principal,
pn cnd se ajunge n colul din dreapta sus al matricei. Figura 14.5 ilus
treaz aceast procedur pentru un ir de n = 6 matrice, avnd dimensiunile
(20 x 15), (15 x 30), (30 x 5), (5 x 25), (25 x 10), (10 x 35).
Construirea efectiv a unei soluii optime
i n cazul acestei probleme, rezolvarea recurenei implic gsirea valorii
optime, fr a da ns i parantezarea efectiv pentru care s-a obinut valoarea
optim. Ca de obicei, calculul soluiei optime se face simplu, folosindu-ne de
rezolvarea recurenei. Pentru a putea gsi care este soluia optim, este suficient
ca pentru fiecare produs Ai . . . Aj s tim care este poziia k la care se mparte
211

14.5. NMULIREA UNUI IR DE MATRICE


Listing 14.12: Calculul recurenei pentru nmulirea irului de matrice folosind
un tabel m n care se rein valorile subproblemelor
1 /* *
2 * Calculeaz formula de recurenta pentru matricea m.
3 * Ne folosim de faptul ca elementele matricei l sunt
4 * iniializate implicit cu 0 .
5 * @ return Costul optim pentru nmulirea matricelor a cror
6 * dimensiune este reinuta in irul d care se considera a
7 * fi membru al clasei care conine metoda.
8 */
9 public static int sirMatrice()
,0 {
11 int n = d.length 1;
12 int [ ] [ ] m = new int [n ] [ n ] ;
13 for (int 1 =0; 1 < n 1; ++ 1 ) // / este lungimea subsirului
,4 j
s
for (int i=0;i<n 1 l;++i)
{
17
j = i + 1 + 1;
s
m[i][j] = Integer .MAXVALUE;
19
int temp ;
20
21
for ( int k = i ; k < j ; + + k)
22
{
23
temp =m[i][k] +m[k + l][j] + d [ i ] * d [k+1 ] * d [ j + 1 ] ;
24
if ( temp < m[ i ] [ j ] )
{
26
m[ i ] [ j ] = temp ;
}
28
}
29
}
30 }
31
32 return m[0][n 1];
33 }

n dou subproduse. Aceasta implic s construim o matrice poZij care reine


poziia n care irul Ai . . . Aj este mprit n subproduse. Calculul lui poZij se
face simplu, adugnd atribuirea
poz [i] [ j ] = k;
dup linia 26 din metoda sirMatrice ( ) (Listing 14.12). Avnd la dispozi
ie irul poz, gsirea soluiei optime se face simplu folosind metoda recursiv
sirMatr iceSolutie ( ) din Listing 14.13.
212

14.5. NMULIREA UNUI IR DE MATRICE

Figura 14.5: Tabelul m rezultat n urma rezolvrii recurenei pentru problema


nmulirii irului de matrice, pentru un ir de 6 matrice avnd dimensiunile (20 x
15), (15 x 30), (30 x 5), (5 x 25), (25 x 10), (10 x 35)
0

9000
0

3750
2250
0

6250
5625
3750
0

6000
7250
2750
1250
0

13000
12500
13250
10500
8750
0

Listing 14.13: Construirea efectiv a soluiei optime pentru problema nmulirii


irului de matrice
1 /* *
2 * Metoda care determina efectiv produsul irului de
3 * matrice in mod optim bazanduse pe matricea calculata
4 * de metoda sir Matrice.
5 * i si j reprezint limitele intre care se calculeaz produsul
6 * @ return String ce reprezint modul de parantezare al matricelor
i */
spublic static String sirMatriceSolutie(int[][] poz, int i, int j)
{
io if ( i < j ) //irul conine cel puin doua matrice
ii {
12
String sl = s i rM a t ri c e S o lu t i e ( poz , i, poz[i][j]);
13
String s2 = s i rM a t ri ce S olu t i e ( poz , poz[i][j] + l, j);
14
15
return "(" + sl + "*" + s2 + ")" ;
16
,7 }
is else //irul conine o sin gura matrice
19 {
20
return "A" + Strin g . valueOf ( i ) ;
21 }
22 }

Clasa Matrice din Listing 14.14 afieaz parantezarea optim i numrul


de nmuliri minim necesar pentru un ir de matrice ale crui dimensiuni sunt
citite de la tastatur.
Listing 14.14: Soluia complet a problemei irului de matrice
i import j ava . io . * ;
213

14.5. NMULIREA UNUI IR DE MATRICE


2 import io . Reader ;
4 public class Matrice
5{
6 public static int s i rM at ri c e ( in t [ ] d)
' !
b
int n = d. length 1;
9
i n t [ ] [ ] m = new i n t [ n ] [ n ] ;
10
int[][] poz = new int[n][n];
11
i2
for ( int 1 = 0; 1 < n - 1 ; 1++)
{
i4
for (int i = 0; i<n 1 1; i ++)
{
i6
int j = i + 1 + 1 ;
,7
m[i][j] = Integer .MAXVALUE;
is
int temp ;
19
for ( int k = i ; k < j ; k++)
20
{
21
temp =m[i][k] +m[k + l][j] +
22
d[i ] * d[k + 1] * d[j + 1];
23
24
if (temp < m[i][j])
!
26
m[ i ] [ j ] = temp ;
27
poz[i ][j ] = k;
28
}
29
}
}
)
32
33
34
Sy stem . out . p ri n t (" Ordinea inmultirii matricilor: " +
35
s i rM a t ri ce S o lu t i e ( poz , 0, n 1));
36
Sy stem . out . p ri n 1 1 n ( ) ;
37
38
return m[0][n 1];
39 }
40
41 public static String s i rM a t ri c e S olu t i e ( in t [ ] [ ] poz,
42
int i, int j)
43 j
44
if ( i < j )
{
46
String sl = s i rM at ri c e S o lu t i e ( poz , i, poz[i][j]);
47
String s2 = s i rM at ri c e S o lu t i e ( poz , poz[i][j] + 1, j);
48
49
return "(" + sl + " * " + s2 + ")";
)
si
else
214

14.6. SUBIR COMUN DE LUNGIME MAXIM


!
53

return "A" + Strin g . valueOf ( i ) ;


}

56
57
58
59
60
61
62
63
64
65
66
67
6s
69
70
71

)
public static void main ( String [] args )
{
Sy stern . out . p ri nt (" Numrul de matrice: ");
int n = Reader . re adl n t ( ) ;
if ( n < 1 ) return ;
int[] d = new int[n + 1];
System . out . p ri n 1 1 n (" S i rul dimensiunilor: ");
for (int i =0; i < n + 1; i ++)
{
System . out . p rin t (" d [ " + i + "]=");
d[i] = Reader . readl nt () ;
}

73
74
Sy stem . out . p ri n 1 1 n (" Numrul minim de operaii necesar " +
75
"inmultirii matricilor: " + s i rM at ri c e ( d ) ) ;
76 }
"I

14.6

Subir comun de lungime maxim

Urmtoarea problem clasic de programare dinamic pe care o vom ana


liza este problema subirului comun de lungime maxim. Un subir al unui ir
x = X\Xi . . . xn se obine prin eliminarea din irul iniial x a unor componente
Xi , Xi2 , . . . , Xik ; componentele care se elimin nu trebuie s fie neaprat con
secutive. De exemplu, irul aeg este un subir al lui abcdefgh. Astfel, dndu-se
dou iruri, x i y, spunem c irul z este un subir comun pentru x i y dac z
este un subir att pentru x ct i pentru y.
n problema subirului comun de lungime maxim se dau dou iruri x =
X1X2 xm i y = y\yi yn arbitrare i se cere s se gseasc un subir
comun al lor z = Z\Zi . . . zp (evident, p <= min(m, n)) care s aib lungimea
maxim.
Exemplu: Pentru irurile dinamic i inadecvat subirul comun de lungime
maxim este inac, deoarece nu exist nici un subir comun de lungime mai
mare dect 4. Este important de observat c subirul comun de lungime max
im nu este neaprat unic. Astfel, pentru irurile abeiro i bueor un subir
215

14.6. SUBIR COMUN DE LUNGIME MAXIM


comun de lungime maxim este ber, iar altul este beo.
O abordare brutal a acestei probleme const n generarea tuturor subirurilor lui x i alegerea celui mai lung care este n acelai timp i sub ir al lui y.
Desigur, aceast soluie nu este deloc eficient, avnd n vedere faptul c exist
2 subiruri ale lui x, deci timpul necesar pentru rezolvarea problemei ar fi
exponenial.
Caracterizarea substructurii optime (verificarea principiului
optimalitii)
Din fericire, aceast problem se preteaz la o structurare pe subprobleme
care verific principiul optimalitii. Totui, n acest caz, structurarea n sub
probleme care s verifice principiul optimalitii nu este evident.
Subproblemele se definesc n mod natural pe baza unor perechi de "prefixe"
ale celor dou iruri de intrare. Mai exact, dndu-se un ir x = X\X2 . . . xm,
un prefix al su este irul X\X2 Xi, cu i = 0, 1, ... m. Astfel, pentru fiecare
pereche de prefixe, x = x . . . Xi, y = y . . . yj ale irurilor iniiale vom calcula
lungimea subirului maximal comun, ajungnd n final s rezolvm i problema
noastr.
Pentru a verifica principiul optimalitii vom enuna trei propoziii sim
ple, n care plecm de la ipoteza c zz2 . . . zp este un subir maximal pentru
xix2 ...xmi yiy2 ...yn1. Dac xm = yn, atunci Z\Z2 zv-\ este un subir comun maximal
pentru XiX2 ..xm-i i yiy2 .yn-i\
2. Dac xm ^ yn i zp 7^ xm, atunci Z\Z2 . . . zp este un subir comun
maximal pentru xx2 . . . xm-\ i y\y2 . . . yn\
3. Dac xm ^ yn i zp ^ yn, atunci Z\Z2 . . . zp este un subir comun
maximal pentru xx2 . . . xm i yiy2 yn-iDemonstraia celor trei afirmaii de mai sus se face n mod banal prin redu
cere la absurd i o lsm ca exerciiu.
Caracterizarea din propoziiile anterioare arat c un subir comun maximal
pentru dou iruri conine un subir comun maximal pentru prefixele celor dou
iruri, deci problema are proprietatea de substructur optim. Tot propoziiile
de mai sus furnizeaz i formula de recuren care rezolv problema.
Vom nota cu
lungimea subirului comun maximal pentru prefixele
xi...Xiiyi. ..yj.
Ca i condiii iniiale avem:
J[0,j]=0, Vj=0,l,...n
Z[i,0] = 0, Vi = 0, l,...m
216

14.6. SUBIR COMUN DE LUNGIME MAXIM


deoarece atunci cnd unul dintre iruri este vid (are lungime 0), subirul comun
maximal va fi ntotdeauna vid.
Din cele trei propoziii rezult n mod clar faptul c avnd un subir comun
maximal pentru dou iruri oarecare xx2 . . . xm i y\y2 yn> acesta se va
regsi n subirul comun maximal pentru una din variantele
xix2 xm-i i yiy2 y-i dac xm = yn,
sau
xix2 xm-i i yiy2 y ori XiX2 xm i yiy2 . . . yn-i dac xm ^
/nDe aici reiese c pentru a calcula l[m, n] sunt necesare valorile l[m l,n
1], l[m 1, n] i l[m, n 1]. Totui, cele trei propoziii nu dau indicaii exacte
despre formula de recuren dect ntr-un singur caz, i anume cand xm yn
(noi nu tim ce valoare are zp atunci cnd construim soluia, deci nu putem
distinge ntre variantele 2 i 3). n situaia n care xm ^ yn putem alege fie
l[m 1, n], fie l[m, n l]. Dintre aceste valori o vom alege evident pe cea mai
mare, deoarece ea ndeplinete proprietatea de optimalitate (se poate arta acest
lucru uor, prin reducere la absurd). Astfel, vom alege
l[m, n] = max{l[m 1, n], l[m, n 1]} xm ^ yn
Aadar, formula de recuren complet este:
!0
l[i 1, j 1] + 1
max(/[i 1, j],l[i, j 1])

ptri = 0 sau j = 0,
ptri = l-.m,j = L.nsixi = Xj,
ptri = l..m,j = L.nsixi ^Xj.

Calculul recurenei de mai sus se face simplu, folosind metoda subsirComunMaximal ( ) din Listing 14.15.

Listing 14.15: Rezolvarea recurenei pentru subirul comun de lungime maxim


1 /* *
2 * Calculeaz formula de recurenta pentru matricea l.
3 * Ne folosim de faptul ca elementele matricei l sunt
4 * iniializate implicit cu 0.
5 * @ return Lungimea subisrului comun maximal pentru x si y
6 * care se presupun a fi membri ai clasei care conine metoda.
7 */
8 public static int subsirComunMaximal ()
{
io intm=x.length(); //x este String
n int n = y.lengthQ; //y este String
217

14.6. SUBIR COMUN DE LUNGIME MAXIM


12
'3
i4
i6

20
22
23

for ( int i = 0 ; i <


{
for (int j = 0; j
{
if (x.charAt(i)
{
1 [i+l][j +1] =
}
eIse
{
1 [ i +1] [j + 1 ] =
}
'
)
)

m; ++i)
< n; + + j)
== y.charAt(j))
1 [i ][j ] + 1 ;

Math . max ( 1 [ i ] [ j + 1 ] , 1 [ i + 1 ] [ j ] ) ;

*
26
27 return 1 [m] [ n ] ;
29 }

Am rezolvat astfel problema determinrii lungimii subirului comun maxi


mal, dar, ca de obicei, nu am gsit subirul efectiv care are lungimea maxim.
Determinarea subirului efectiv se poate realiza uor pe baza matricei l calcu
lat n metoda subsirComunMaximal ( ) din Listing 14.15, urmrind care
a fost "traseul" care a condus la subirul de lungime maxim (dat de l[m, n]).
Am evideniat deja faptul c
poate proveni doar dintr-unul din elementele
l[i 1, j 1], l[i
sau l[i,j 1]. ncepem cu elementul l[m, n] al matri
cei; acesta provine din l[m 1, n 1] dac xm = yn sau din maximul dintre
l[m l,n] i l[m, n 1] n caz contrar. Mergem astfel pas cu pas pn cnd
ajungem la /[O, 0]. Din drumul de la l[m, n] la /[O, 0] construit anterior reinem
doar elementele
pentru care Xi = yj. Afind n ordine invers aceste
elemente, obinem chiar irul cerut (Listing 14.16).

Listing 14.16: Determinarea efectiv a subirului comun maximal


1 /* *
2 * Determina care este subirul comun maximal pe baza
3 * matricei l si a irurilor x si y ( care se presupun a fi
4 * membri ai clasei.
5 * @ return Un String care reprezint subirul comun maximal.
6 */
7 public static String determinaSubsir ( int i, int j)
*{
9 if ( i != 0 && j != 0)
,o {
ii
if (x.charAt(i - 1) == y.charAt(j - 1))
218

14.6. SUBIR COMUN DE LUNGIME MAXIM


(
13
15

19
20
21
22
23
26
27
28
29
30
31 }

return determinaSubsir ( i 1, j 1) + x . charAt ( i 1);


}
else
!
ir (i[i][j] == i[i-i][j])
(
return determinaSubsir ( i 1, j);
}
else
{
return determinaSubsir ( i , j 1);
}
}
}
else
{
return " " ;
}

Clasa Subs ir Comun din Listing 14.17 citete dou stringuri de la tas
tatur, dup care afieaz subirul lor comun de lungime maxim.
Listing 14.17: Soluia complet a problemei subirului comun maximal
1 import j ava . io . * ;
2 import io . Reader ;
3
4 public class SubsirComun
5 {
6 private static String x;
7 private static String y;
8 private static int[][] 1;
9
10 public static int subsirComunMaximal ()
11 {
12
// . . .
13
System . out . p ri n 1 1 n (" S ub irul comun maximal: " +
14
determinaSubsir (m, n));
15
16
return 1 [m] [ n ] ;
,7 }
19
20
21
22
23
24
25

public static String determinaSubsir (int i, int j)


{
// . . .
}
public static void main(String args [ ] )
{
219

14.7. DISTANA LEVENSTHEIN


26
27

Sy stem . out . p ri n t ( " x = ");


x = Reader . re ad S t ri n g ( ) ;

29
Sy stem . out . p ri n t (" y = ");
30
y = Reader . re ad S tri n g () ;
31
32
Sy stem . out . p ri n t In (" Lungimea subsirului comun maximal: "
33
+ subsirComunMaximal ());
34 }
35 )

14.7

Distana Levensthein

O problem foarte asemntoare ca structur cu problema subirului comun


de lungime maxim este calculul distanei Levensthein ntre dou iruri de ca
ractere. S presupunem c avem dou iruri de caractere, x = X\Xi . . . xm i
y = 2/i 2/2 Un- Operaiile permise asupra unui ir de caractere sunt:
tergerea unui caracter oarecare din ir;
inserarea unui caracter pe o poziie oarecare din ir;
nlocuirea unui caracter oarecare cu un altul.
Se cere s se determine numrul minim de operaii prin care irul x se poate
transforma n irul y. Acest numr se numete distana Levensthein dintre x i
y i reprezint o evaluare mult mai obiectiv a similaritii a dou iruri dect
distana Hamming (care pur i simplu numr poziiile pe care cele dou iruri
difer).
Exemplu: Pentru a transforma irul algoritm n aborigen sunt necesare urm
toarele operaii:
algoritm[l <-> b] > abgoritm[sterge g] > aboritm[t <-> g] >
> aborigm[insereaza e] > aborigem[m <-> n] > aborigen
Este uor de vzut c ntotdeauna exist o secven de astfel de operaii
care s transforme un ir ntr-altul, deci nu se pune problema de fezabilitate.
Structurarea n subprobleme este foarte asemntoare cu cea de la problema
subirului comun de lungime maxim din seciunea precedent. Astfel, pentru
oricare prefix
= X\ . . . Xi al lui x i oricare prefix Yj = /i Uj al lui y
ne vom propune s determinm secvena cea mai scurt de operaii pentru a-1
transforma pe Xi n Yj .
220

14.7. DISTANA LEVENSTHEIN


S artm mai nti c descompunerea n subprobleme dat de noi respect
principiul optimalitii. Pentru aceasta, va trebui s gsim o modalitate de a
construi soluia optim a unei probleme pe baza soluiilor optime ale probleme
lor de dimensiune mai mic. S considerm dou prefixe
i Yj ale irurilor
iniiale, iar s = S1S2 . . . sp secvena optim de operaii pentru a-1 transforma pe
primul n al doilea. Putem face acum urmtoarele observaii:
Dac Xi = yj, atunci s = s . . . sp este o secven optimal de transfor
mri i pentru Xi_\, Yj-\\
Dac Xi ^ yj i ultima transformare din s este o operaie de tergere,
atunci Si . . . sp-i este o secven optimal de transformri pentru

Dac Xi ^ yj i ultima transformare din s este o operaie de inserare,


atunci Si . . . sp-i este o secven optimal de transformri pentru Xi i
Yj-i;
Dac Xi ^ yj i ultima transformare din s este o operaie de nlocuire,
atunci Si . . . sp-i este o secven optimal de transformri pentru JTj_i

Cele patru observaii de mai sus ne indic substructura optim a problemei


noastre. Determinarea secvenei optimale de transformri pentru cele dou
iruri Xi i Yj se reduce la determinarea secvenei pentru una dintre perechile
{Xi.^Yj^), (Xi_uYj) sau (Xj.Fj-i).
Ca de obicei, vom calcula mai nti care este numrul de operaii pentru
a-1 tranforma pe x n y, urmnd ca pe baza tabelului obinut s determinm
efectiv care este secvena optimal de transformri. S notm cu dij distana
Levensthein dintre Xi i Yj. Pentru i = 0, vom avea nevoie de j operaii de
inserare pentru a ajunge la Yj (Xo este irul vid), deci doj = j; similar avem
di0 = i.
S gsim acum relaia de recuren pentru cazul general. Conform primei
observaii, dac Xi = yj, vom avea dij = di-\j-\. Dac Xi ^ yj atunci dij
va fi, funcie de care dintre cele trei operaii s-a aplicat, egal cu di-ij-i + 1,
di-ij + 1, sau dij-i + 1. Vom alege desigur cea mai mic valoare. Astfel,
formula de recuren pentru dij este:
j
i
dij = <
1 +m.m{di-1j,dij-1,di-1j_1}

pentru i
pentru j
pentru Xi
pentruxi

=0
=0
= yj
^ yj
221

14.7. DISTANA LEVENSTHEIN

Listing 14.18: Rezolvarea recurenei pentru distana Levensthein


1 /* *
2 * Calculeaz formula de recurenta pentru matricea d.
3 * @ return distanta Levensthein dintre x si y
4 * care se presupun a fi membri ai clasei care conine metoda.
5 */
6 public static int distantaLevensthein()
'(
8 int m = x . length () ;
9 int n = y . length ( ) ;
10 d = new int[m+ l][n + 1];
11
12 for (int i = 0; i <=m;++i)
d[i][0] = i;
for (int j = 0; j <= n; ++j)
d[0][j ] = 0;
for (int i = 0 ; i < m; ++i)
for (int j = 0; j < n; ++j)
{
if (x.charAt(i) == y.charAt(j))
{
d[i + l][j + 1] = d[i][j ];
}
eIse
{
d[i+l][j+l] = 1 + min(d[i ][j +1], d[i+l][j], d[i][j]);
)
}
)
return d [m] [ n ] ;

Rezolvarea recurenei de mai sus este foarte simpl, i este realizat de


metoda distantaLevensthein ( ) din Listing 14.18.
Pentru a reconstitui secvena efectiv de operaii prin care x se transform
n y vom folosi o parcurgere a matricei d asemntoare cu cea de la problema
subirului comun de lungime maxim. Pornim de pe poziia (rn,n). Dac
xm = yn, atunci trecem pe poziia (m 1, n 1) fr nici o operaie. Dac
222

14.7. DISTANA LEVENSTHEIN

Figura 14.6: Elementele matricei d pentru irurile algoritm i aborigen. Ele


mentele ngroate reprezint traseul corespunztor transformrii optime dat la
nceptul paragrafului.

A
a
1
g
o
r
i
t
m

A
0
1
2
3
4
5
6
7
8

a
1
0
1
2
3
4
5
6
7

b
2
1
1
2
3
4
5
6
7

o
3
2
2
2
2
3
4
5
6

r
4
3
3
3
3
2
3
4
5

i
5
4
4
4
4
3
2
3
4

g
6
5
5
4
5
4
3
3
4

e
7
6
6
5
6
5
4
4
4

n
8
7
7
6
7
6
5
5
5

2 7^
atunci alegem poziia nvecinat de valoare minim. Dac valoarea
minim s-a obinut pentru (m l,n 1), atunci ultima operaie a fost de
nlocuire; dac valoarea minim s-a obinut pentru elementul de pe poziia (m
\,ri), atunci ultima operaie a fost de inserare, iar dac valoarea minim s-a
obinut pentru (m, n 1), ultima operaie a fost de tergere.
Figura 14.6 prezint matricea d pentru irurile algoritm i aborigen, iar
elementele ngroate corespund soluiei date la nceputul paragrafului. Algo
ritmul de reconstituire este asemntor cu cel de la problema subirului comun
de lungime maxim din paragraful precedent i este redat n liniile 61-98 din
Listing 14.19.
Listing 14.19: Soluia complet a problemei distanei Levensthein
1 import j ava . io . * ;
2 import io . Reader ;
3
4 public class Levensthein
5 {
6 private static String x;
7 private static String y;
8 private static int [][] d ;
9
10 public static int di s t an t aL e ven s t hein ( )
11 {
12
int m = x . length ( ) ;
13
int n = y . length ( ) ;
14
d = new int[m+ l][n + 1];
15
223

14.7. DISTANA LEVENSTHEIN


16
17
is
19
20
21
22

for (int i =0; i <=m; i ++)


{
d[i ][0] = i ;
}
for (int j = 0; j <= n; j ++)
{
d[0][j ] = j ;
}

24
25
26
27
28
29
30

for (int i =0; i <m;


{
for ( int j = 0 ; j < n ; j ++)
{
if (x.charAt(i) == y.charAt(j))
{
d[i+l][j+l] = d[i][j ];
}
else
{
d[i+l][j +1] = 1 + min(d[i ][j +1],
d[i+l][j],
d[i ][j ]);
}
}
}

33
34
36
39
40
42
43
44
45
46
47
48
49
50
si
52
53

System . out . p ri n 1 1 n (" Op e r at i i 1 e de transformare: ");


determinaSecventa (m, n);
return d [m] [ n ] ;
}
private static int min (int a, int b, int c)
{
int m = Math.min(a, b);
i f ( c < m)
{
m = c;
}

55
57
ss
59
60
61
62
63
64
65
224

return m;
}
public static void determinaSecventa(int i, int j)
j
if ( i != 0 && j != 0)
{
if ( x . charAt ( i 1 ) == y . charAt ( j 1))

14.7. DISTANA LEVENSTHEIN


66
67
68
69

{
determinaSecventa ( i 1 , j 1);
}
else
!
int m = min ( d [ i 1] [ j 1 ] ,
d[i-l][j],
d [ i ] [ j 1 ]):

71
74

if (m == d[i-l][j -1])
!
System, out. println( " inlocuire: " +
x . charAt ( i 1 ) + " cu " +
y . charAt ( j 1 ));
determinaSecventa ( i 1 , j 1);
}
else if (m == d[i-l][j ])
!
System . out . println (" tergere: " +
x . charAt ( i 1 ) +
" de pe poziia " + ( i - 1));
determinaSecventa ( i 1 , j);
}
else
{
System . out . println (" inserare : " +
y . charAt ( j 1 ) +
" pe p o z i t i a " + i ) ;
determinaSecventa ( i , j 1);
}

77
7s
79
80
82
84
85
86
87
88
89
90
91
92
93
94
96

}
}

98
99
100
101
102
103
104
105
106
107
108
109
III) }

}
public static void main(String args[])
{
System . out . prin ( "x = ");
x = Reader . readString ( ) ;
System . out . prin ( "y = ");
y = Reader . readString () ;
System . out . p ri ntl n (" Numrul minim de operaii: "
+ di s t ant aLe ven s thein ( ) ) ;
}

225

14.7. DISTANA LEVENSTHEIN


Rezumat
Noiuni fundamentale
programare dinamic: metod de elaborare a algoritmilor care se aplic
problemelor de optimizare n care soluia optim se construiete pe baza soluiei
optime a subproblemelor.
problem de optimizare: problem n care se cere minimizarea sau maxi
mizarea unei funcii obiectiv (de exemplu, n cazul nmulirii irului de matrice,
funcia obiectiv este numrul de nmuliri scalare necesare pentru a calcula pro
dusul).
principiul optimalitii: este numit adeseori i substructur optim. n esen, acest principiu afirm c soluia optim a unei probleme de optimizare
este construit din subsoluii optime ale subproblemelor.
soluie optim: o soluie pentru care se atinge valoarea optim a funciei
obiectiv n cazul unei probleme de optimizare. Soluia optim nu este neaprat
unic.
subproblem: problem similar cu cea original, dar de dimensiune mai
mic. n cazul programrii dinamice subproblemele se aleg astfel nct s res
pecte principiul optimalitii.

Erori frecvente
1. Se confund principiul optimalitii cu reciproca lui, creznd c prin com
binarea a dou subsoluii optime se obine tot o soluie optim.
2. Se aplic metoda programrii dinamice pentru subprobleme care nu res
pect principiul optimalitii.
3. Nu trebuie s ne avntm s rezolvm toate problemele care respect
principiul optimalitii folosind programarea dinamic. Trebuie verificat
nainte c nu putem aplica strategii mai simple, cum ar fi Greedy.
4. Suprapunerea apelurilor recursive trebuie evitat, deoarece exist posibi
litatea de a genera algoritmi exponeniali.
5. Calculul complexitii n timp a algoritmilor recursivi se face pe baza
unei formule recurente. Nu v bazai pe faptul c un apel recursiv are
timp liniar.
226

14.7. DISTANA LEVENSTHEIN


Exerciii
Pe scurt
1 . Crui tip de probleme se poate aplica metoda programrii dinamice?
2. Care este enunul principiului optimalitii?
3. Este reciproca principiului optimalitii adevrat?
4. Care este principala dificultate n rezolvarea problemelor de programare
dinamic?
5. Construii matricea 1 i evideniai drumul de lungime maxim pentru
triunghiul:
4
2
9
11 2 1
4
8 9 6
7 10 1 12 4
6. Gsii un exemplu pentru care problema triunghiului din paragraful 14.3
admite mai multe soluii optime. Construii matricea 1 att pentru metoda
nainte ct i pentru metoda napoi.
7. Gsii o parantezare optim a produsului unui ir de matrice de dimensi
uni (4,12,5,9,2,60,8).
8. Determinai cel mai lung subir comun pentru (1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0)
i(0,0,l,l,0,l,l,0,l,0,0,l,0,l).
9. Determinai distana Levensthein ntre irurile recursivitate i acuitate.
Teorie
1 . Demonstrai c o parantezare corect a unui ir de n matrice are exact
n 1 perechi de paranteze.
2. Care este modalitatea cea mai eficient de determinare a parantezrii op
time a unui ir de matrice: enumerarea tuturor parantezrilor posibile ale
produsului i calculul numrului de operaii pentru fiecare parantezare,
sau rezolvarea recursiv a formulei de recuren 14.1? Justificai rspun
sul.
227

14.7. DISTANA LEVENSTHEIN


Probleme
1. Toate problemele de programare dinamic prezentate n cadrul acestui
capitol (problema triungiului, parantezarea unui ir de matrice, subir co
mun de lungime maxim etc.) admit n unele situaii mai multe soluii
optime. Totui, algoritmii prezentai de noi determin o singur soluie
optim pentru fiecare problem. Modificai metodele de determinare a
soluiei optime astfel nct acestea s afieze toate soluiile optime ale
problemei.
Indicaie
Toi algoritmii de determinare a soluiei optime se bazeaz pe parcurgerea
ulterioar a matricei soluie reconstruind traseul pe care soluia optim a
fost obinut. De exemplu, n cazul problemei triungiului, se pornete din
vrful matricei 1 i se merge n jos sau dreapta-jos, funcie de care ele
ment este mai mare. Totui, dac cele dou elemente sunt egale, nseamn
c soluia optim se poate construi mergnd pe ambele variante. Re
zolvarea corect este aadar s se parcurg recursiv ambele variante i s
se afieze soluiile corespunztoare fiecreia. Este important de observat
c situaia n care elementele sunt egale poate s apar de mai multe ori
n cadrul reconstruirii soluiei. Practic, de cte ori apare aceast situaie,
attea soluii optime admite problema.
2. Problema bancomatului.
Se pune problema construirii unui bancomat care s fie capabil s furni
zeze o anumit sum clienilor bncii. Pentru aceasta, aparatul dispune
de un numr k de bancnote. Dei fiecare dintre aceste bancnote exist
n cantitate nelimitat, se dorete returnarea restului cu un numr minim
de bancnote. Se cere ca dndu-se o sum S i k numere reprezentnd
valorile bancnotelor (i>i,i>2, -Vn), s se determine numrul minim de
bancnote cu care se poate plti suma S, n cazul n care acest lucru este
posibil. S se determine i bancnotele cu care suma este pltit.
Observaie
Problema nu este trivial, ntruct bancnotele pot avea orice valoare natu
ral. De exemplu, nu se poate plti suma 25 cu bancnote avnd valoarea
7 i 13). De asemenea, strategia Greedy de a folosi monede de valoare
maxim nu d ntotdeauna soluia optim (de exemplu, achitarea sumei
20 cu monede de valoare 6, 5, 4, 1 ar genera soluia (6, 6, 6, 1, 1), care nu
este optim).
228

14.7. DISTANA LEVENSTHEIN


Indicaie
Notm cu bi numrul minim de bancnote cu care poate fi pltit suma i.
bs ne va da chiar soluia problemei (numrul minim de bancnote necesare
pentru a plti suma S). Prin convenie, bi va fi infinit dac suma i nu
poate fi pltit cu bancnotele date. Se iniializeaz elementele vectorului
& cu valoarea infinit (Integer .MAXINT n cazul nostru). Pentru fiecare
bancnot cu valoarea Vk se pune bVk = 1. Apoi vom determina bi, cu for
mula de recuren

bi

min {bi-Vh} + l i = l,2,...s


i-vk>0

care exprim faptul c suma i se poate obine adugnd moneda


la
monedele necesare pentru a obine suma i v^. Reconstituirea soluiei
se face reinnd pentru fiecare i indicele k pentru care s-a obinut valoarea
minim.
3. Company party.
Preedintele unei companii dorete s organizeze o petrecere cu angajaii
firmei. Acetia se afl ntr-o structur ierarhic de subordonare care este
reprezentat sub forma unui arbore a crui rdcin este, desigur, chiar
preedintele. Pentru ca toat lumea s se simt bine, nu trebuie s fie
invitate dou persoane care s se afle n relaie direct de ef-subaltern.
Mai mult dect att, fiecare angajat are un atribut calculat de serviciul
personal - rata de sociabilitate. Se dorete ca suma ratelor de sociabili
tate ale persoanelor invitate s fie maxim, asigurnd astfel o petrecere
mai mult dect reuit. Ca o condiie suplimentar, preedintele trebuie
s mearg la propria-i petrecere...
Indicaie
Se consider fiecare subarbore al ierarhiei date, calculnd valoarea op
tim n cazul n care vrful subarborelui este invitat i n cazul n care
nu este invitat. Dac vrful arborelui este invitat la petrecere, atunci sub
alternii si direci nu vor fi invitai. Dac vrful nu este invitat, atunci
nu exist nici o restricie pentru subalternii direci (care pot sau nu s fie
invitai).
4. Problema paragrafrii.
Se dau n cuvinte de lungime (numr de caractere) Z1; l2, , ln. Trebuie
s se aeze aceste cuvinte n ordine ntr-un paragraf n care lungimea
229

14.7. DISTANA LEVENSTHEIN


unei linii este l. Fiecare linie din paragraf trebuie s nceap cu un cu
vnt. Distana optim (numrul de spaii) dintre dou cuvinte este dat de
un numr d. Distana minim dintre dou cuvinte este dat de numrul
dmin- Toate cele n cuvinte au lungime mai mic dect l. Pentru fiecare
linie se calculeaz un cost care se obine prin nsumarea abaterilor de la
distana optim dintre cuvinte la care se adun numrul de spaii libere
rmase dup ultimul cuvnt pn la finalul liniei. Costul ultimei linii este
0 chiar dac aceasta este incomplet. Se definete costul paragrafrii ca
fiind suma costurilor liniilor care formeaz textul. Se cere s se furnizeze
o aezare a celor n cuvinte care s fie de cost minim.
Exemplu
Se dau 4 cuvinte de lungime respectiv 1,2,2,6, lungimea liniei fiind 8. Se
consider distana optim d = 2, iar distana minim dmin = 1. Paragrafarea optim este (c reprezint un caracter din cuvnt, iar s reprezint un
spaiu):
cssccscc
ccccccss
avnd costul 1 .
Indicaie
Vom considera ca subproblem de ordin i aranjarea n paragraf a cuvin
telor i, i + 1, . . . , n. Se observ uor c dac o soluie optim conine o
linie al crei nceput este cuvntul i, atunci subsoluia este optim pentru
subproblem de ordin i. Vom nota cu Cj costul paragrafrii cuvintelor
i,i + 1, . . . ,n. Evident, c = 0, deoarece costul ultimei linii este 0.
Formula de recuren pentru irul c este
0, pentru i = n
i-i {dij + Cj+i}, altfel
unde Ojj este costul obinut prin aezarea cuvintelor i , i + 1 , . . . j pe o linie
(n situaia n care cuvintele nu pot fi aezate pe linie, costul se consider
a fi infinit). Ojj se afl uor calculnd diferena dintre spaiile disponibile
(l X^i=i W i cele care ar trebui s fie pentru un cost 0 ((j i) dopt):

230

14.7. DISTANA LEVENSTHEIN

Aflarea unei paragrafri optime se face reinnd cuvintele la care se trece


pe linie nou i distribuind uniform spaiile avute la dispoziie n cadrul
cuvintelor din aceeai linie.

5. Mere, pere.
Se consider n camere distincte, situate succesiv una dup cealalt astfel
nct din camera numrul i se poate trece doar n camera numrul i + 1
(i = 1,2,. ..,n 1 ) . In fiecare camer se afl un anumit numr de mere i
de pere. O persoan avnd la dispoziie un rucsac suficient de ncptor,
iniial gol, pornete din camera 1, trece prin camerele 2, 3, . . . , n i iese.
La intrarea n fiecare camer persoana trebuie s descarce rucsacul i s
ncarce fie toate merele, fie toate perele din camera respectiv, dup care
trece n urmtoarea camer. Se presupune c pentru fiecare fruct trans
portat dintr-o camer ntr-alta persoana consum cte o calorie. S se pre
cizeze ce fructe trebuie s ncarce persoana respectiv n fiecare camer
astfel nct dup parcurgerea celor n camere s consume un numr minim
de calorii i s se precizeze acest numr.
Indicaie
Strategia Greedy de a alege la fiecare pas cantitatea de fructe mai mic nu
conduce ntotdeauna la soluia optim (gsii un contraexemplu!). Prin
cipiul optimalitii se formuleaz astfel: presupunem c avem o soluie
optim pentru subproblema generat de camerele i, i + 1, . . . , n; atunci
subsoluia acesteia pornind din camera i+1 este optim pentru subproble
ma generat de camerele i + l,i + 2, . . . ,nn care se pleac cu fructele
alese n camera i + 1 n cadrul soluiei optime. Cu alte cuvinte, dac
n camera i + 1 s-au ales mere, atunci subsoluia respectiv este optim
pentru subproblema generat de camerele i+l,i+2, . . . ,nn care se pre
supune c se pornete cu mere (dac se pornete cu pere se poate obine
o soluie mai bun).
Vom nota cu vrii numrul merelor i cu pi numrul perelor din camera
i, i = 1,2, ... ,n. De asemenea, vom nota cu crrii costul optim (numrul
minim de calorii) obinut cnd se pleac cu mere din camera i i cu cpi
costul optim obinut cnd se pleac cu pere din camera i. Evident, soluia
problemei este dat de min(cpi, crai). Formula de recuren este dat de
observaia c dac plecm cu un anumit tip de fruct din camera i, putem
231

14.7. DISTANA LEVENSTHEIN


s schimbm acel tip de fruct n oricare dintre camerele i + 1, i + 2, . . . , n:

(
_ J

mn, i = n
mi + cpi+1
+ (r^ + mi+i) + cpi+2
rai + (rai + rai+i) + . . . + (rai + . . . mn-i) + cpn
mi + (mi + rai+i) + . . . + (rai + . . . ra)

Analog se obine formula de recuren i pentru pere.

6. Mere3 pere - problema generalizat.


Generalizai problema anterioar pentru cazul n care n fiecare camer
se gsesc t tipuri de fructe.

7. Compararea secvenelor de ADN.


Se numete secven de ADN un ir format din caracterele A,C,T i G
(corespunztoare elementelor fundamentale ale codului genetic: adenin,
citozin, timin i guanin). O problem de o deosebit importan n
genetic este compararea a dou secvene de ADN (de exemplu, pentru
stabilirea paternitii). Compararea a dou secvene de ADN nu se face
ns direct, existnd posibilitatea alinierii celor dou iruri prin inserarea
de caractere '-' (dup aliniere cele dou iruri au aceeai dimensiune). De
exemplu, dac avem secvenele TGCGAT i TAGCAG, o posibil alinere
este:
T - GCGAT
TAGC - AG

Se acord 3 puncte dac dou caractere care se afl unul sub cellalt sunt
egale i se acord o penalizare de un punct dac cele dou caractere sunt
diferite. Pentru exemplul de mai sus se acord 4*3-3*1=9 puncte.
Fiind date dou iruri ADN, A i B (nefiind obligatoriu ca acestea s
aib aceeai dimensiune), se cere s se determine o aliniere pentru care
punctajul acordat este maxim. (Pentru exemplul precedent, alinierea dat
este optim).
232

14.7. DISTANA LEVENSTHEIN


Indicaie
Aceast problem seamn ntructva cu problema determinrii subirului comun de lungime maxim (paragraful 14.6). O prim idee de re
zolvare ar fi s determinm subirul comun de lungime maxim pentru
cele dou iruri i apoi s potrivim elementele comune din cele dou iruri
unele sub altele, folosindu-ne de '-' att sus ct i jos. Aceast soluie ar
fi corect dac nu ar exista penalizri pentru nepotriviri. Cu toate acestea,
structura de optimalitate este asemntoare cu cea a problemei subirului comun de lungime maxim, diferind doar relaia de recuren. i aici
vom considera prefixe ale celor dou iruri, iar pentru fiecare pereche
de prefixe vom determina alinierea optimal. Prin aliniere se nelege o
reprezentare a ieirii cerute de problem n care se insereaz caractere '-'
pentru ca irurile s aib aceeai lungime.
Fie Ski scorul maxim ce se poate obine prin alinierea prefixelor A . . . Ak
i Bi . . . Bi. Dac Ak = Bi, atunci ski va fi Sk-u-i + 3, deoarece se
acord 3 puncte pentru dou caractere identice aezate unul sub altul.
Dac cele dou caractere nu sunt egale, atunci putem obine Ski fie din
Sk-ii, fie din Ski-i sau din Sk-ii-i corespunztor respectiv situaiei n
care adugm un '-' la irul A, un '-' la irul B sau cte un caracter diferit
de '-' la ambele iruri. Pentru fiecare dintre aceste trei cazuri va tre
bui acordat o penalizare pentru nepotrivirea caracterelor. Desigur, vom
alege varianta al crei cost este maxim.
Formula de recuren pentru matricea s

_ I
Skl ~ I

k, pentrul = 0
l, pentru k = 0
Sk-ii-i +3, pentru Ak = Bt
max{sk-ii,ski-i,sk-u-i} - 1, pentruAkBx

Reconstituirea soluiei se face uor parcurgnd matricea s de la smn pn


la Soo- Dac ski provine din Skiii, atunci este vorba de o aezare unul
sub altul a dou caractere ADN (deci nu '-'), momentan nefiind impor
tant dac Ak este egal cu Bi sau nu. Dac ski provine din sk-u, atunci
trebuie inserat un '-' n irul B, altfel el trebuie inserat n A.

8. Problema vrjitorului.
Indiana Jones intr ntr-un labirint unde gsete un vrjitor. Acesta i
pune n fa n lzi, fiecare lad coninnd un anumit numr precizat (m,)
233

14.7. DISTANA LEVENSTHEIN


de monede de aur. Vrjitorul i cere s aleag nite lzi astfel nct suma
monedelor din acestea s fie divizibil cu n, altfel nu va putea pleca cu
ele. Evident, Indiana Jones dorete s ia un numr ct mai mare de mone
de de aur. Elaborai un algoritm care s-1 ajute pe Indiana Jones s plece
cu cantitatea maxim de monede, respectnd cerinele vrjitorului.
Indicaie
Trebuie s artm n primul rnd c problema noastr admite soluie,
cu alte cuvinte exist cel puin o combinaie de lzi pentru care suma
monezilor este divizibil cu n. Notm cu Si suma monezilor din lzile
1,2,--.*
i
Si = ^2mi
j=i
i cu Ti restul mpririi lui Si la n. Evident, va lua valori ntregi ntre 0
i n 1. Dac exist un i pentru care avem = 0, atunci rezult c se
divide cu n, deci lzile 1,2, ... ,i reprezint o soluie a problemei. Dac
nu exist nici un i pentru care = 0, atunci rezult c ia valori ntre
1 i n 1, deci vor exista cel puin dou componente i Tj din irul r
(care are n componente) care s fie egale. Aceasta nseamn c Sj Si
se divide cu n, deci lzile i + 1, . . . , j reprezint o soluie a problemei.
Am artat deci c problema are ntotdeauna mcar o soluie. Desigur,
aceast metod de alegere a lzilor nu garanteaz optimalitatea soluiei,
motiv pentru care vom folosi metoda programrii dinamice pentru a re
zolva problema.
Vom nota cu My suma maxim de monezi care se poate obine din
primele i lzi i care s dea restul j la mprirea cu n. Prin convenie,
Mij va fi infinit dac nu se poate obine o sum care s dea restul j la
mprirea cu n. Scopul nostru este de a-1 calcula pe Mo- Este uor de
observat c Mij este egal cu roi dac Toi se divide cu j, i infinit n caz
contrar. Totui, n cazul n care roi nu se divide cu n, vom considera
Mio ca fiind 0. n cazul general, pentru a calcula Mki ne vom folosi de
informaia determinat pentru primele k 1 lzi. Lada numrul k poate
sau nu s apar n soluia subproblemei Mm . Dac nu este luat, atunci
vom avea Mm = M^-n. Dac lada numrul k este luat, atunci M\~i va
fi egal cu
Mjp + mk
unde j este mai mic dect k, iar p+ vn^ trebuie s dea restul / la mprirea
la n. Formula de mai sus rezult din observaia c Mjp reprezint suma
234

14.7. DISTANA LEVENSTHEIN


maxim care se poate obine din primele j lzi care s fie divizibil cu p.
Dac acestei sume i se adaug m^, se obine o sum care prin mprirea
la n va da restul l, adic chiar M\~i (la acest pas se aplic principiul optimalitii). Dintre toate aceste variante o vom alege, desigur, pe cea mai
convenabil
kl
Mm = max{Mfc_ii, max{Mjp +rrik\j < ksil = p + rrik (modn)}}

Valoarea lui p din formula de mai sus poate fi calculat cu uurin dup
formula p = (l rrik) modn (atenie la cazul n care l rrik este negativ
- putei aduga pentru siguran un n la sum). Reconstituirea soluiei se
face reinnd pentru fiecare element M\~i o valoare p\~i care indic lada j
pentru care s-a obinut valoarea optim, sau este 0 dac M\~i = M^-u.
cu

235

15. Metoda Branch & bound

Trebuie s fii foarte atent dac nu


tii unde mergi, pentru c riti s
nu ajungi acolo.
Yogi Berra
Pe parcursul acestui capitol vom vedea:
n ce const metoda Branch & bound de elaborare a algoritmilor;
Care este legtura dintre Branch & bound i Backtracking;
Abordarea unei probleme clasice de Branch & bound, cunoscut sub nu
mele de jocul de puzzle cu 15 elemente.

15.1

Prezentare general

Metoda Branch & bound este asemntoare metodei Backtracking, n sensul


c ncearc s fac o cutare exhaustiv inteligent n spaiul soluiilor (numit
n acest context i spaiul strilor) problemei. Comparativ cu Branch & bound,
Backtracking acioneaz "orbete", n sensul c la fiecare pas se alege (la ntm
plare) o component n vectorul soluie care respect condiiile de continuare.
Singura condiie la Backtracking este aadar respectarea condiiilor de conti
nuare: toate posibilele configuraii care respect aceast condiie sunt privite
n mod egal i se alege arbitrar una dintre ele. Alegerea n cazul Branch &
bound este ceva mai rafinat: fiecrei configuraii viabile i se acord o pondere
care reprezint o estimare a valorii acelei configuraii. La fiecare pas vom alege
configuraia a crei estimare este optim. Ca i n cazul metodei Backtrack
ing, problemele rezolvabile prin aceast metod genereaz un spaiu al strilor,
236

15.1. PREZENTARE GENERAL


n care se gsesc toate configuraiile viabile posibile. Spaiul strilor poate fi
reprezentat n acest caz sub forma unui arbore, n care rdcina arborelui este
configuraia iniial a problemei, n timp ce configuraiile finale constituie, n
cazul n care exist, frunzeale arborelui. Este posibil ca pentru o anumit pro
blem s nu existe o soluie, adic s nu existe o secven de mutri posibile,
prin care s se ajung de la o configuraie iniial la o configuraie dorit (fi
nal). Asemnarea cu Backtracking este i acum evident. In cazul metodei
Backtracking spaiul strilor era reprezentat sub forma unei liste care ncepea cu
configuraia iniial i se ncheia cu configuraia final n care nu se mai putea
aplica nici una dintre cele patru transformri (atribuie i avanseaz, ncercare
euat, revenire, revenire dup gsirea unei soluii). Diferena esenial dintre
Backtracking i Branch&bound apare n cadrul pasului atribuie i avanseaz.
In cazul Backtracking alegeam o configuraie arbitrar care respecta condiiile
de continuare. n cazul Branch & bound considerm toate configuraiile pe care
putem avansa i o alegem pe cea care pare s fie mai promitoare.
Branch & bound gsete soluia optimal prin alegerea celei mai bune soluii
de moment. Dac soluia parial curent nu este mai potrivit dect cea mai
bun soluie parial disponibil la un moment dat, atunci ea este abandonat.
De exemplu, s presupunem c se dorete aflarea celui mai scurt drum de la
Braov la Constana i c cea mai scurt cale descoperit pn la un moment
dat este de 400 de kilometri. Apoi, s presupunem c vrem s considerm
posibilele rute care trec prin Galai. Dac cel mai scurt drum din Braov la
Galai are 380 de kilometri, iar distana dintre Galai i Constana este de 50 de
kilometri, atunci nu are nici un sens s cutm drumuri ctre Constana care trec
prin Galai, pentru c ele vor fi tot timpul mai lungi dect cea mai scurt cale
cunoscut pn n acel moment (380 + 50 > 400). De aceea, rutele din Braov
ctre Constana prin Galai nu vor mai fi explorate. Cu alte cuvinte, se ncearc
parcurgerea subarborilor din spaiul soluiilor care pot conduce la un rezultat
favorabil, evitndu-se subarborii despre care se afl c nu conduc la rezultate
optime.

15.1.1

Fundamente teoretice

Metoda Branch & bound utilizeaz cteva noiuni a cror nelegere este
strict necesar pentru a putea deprinde mecanismul de funcionare a metodei.
Unei probleme de tipul Branch & bound i se asociaz un arbore oarecare
(nu neaprat arbore binar), n care fiecare nod reprezint o configuraie. Con
figuraia iniial reprezint rdcina arborelui i conine datele de intrare ale
problemei. Prin efectuarea operaiilor (mutrilor) permise asupra configuraiei
iniiale se obin alte configuraii, care vor constitui nodurile aflate pe nivelul 2 al
237

15.1. PREZENTARE GENERAL


arborelui. Procedeul se repet, obinndu-se noi i noi configuraii, care conduc
la crearea unei structuri arborescente, care va conine soluia problemei (dac
aceasta exist).
Succesorul unui nod reprezint o configuraie la care se ajunge prin apli
carea nodului respectiv a uneia dintre mutrile permise de problem. In cele
mai multe probleme, un nod are mai muli succesori.
Expandarea unui nod constituie aciunea de determinare a tuturor succeso
rilor nodului respectiv. Nodul care este expandat este un nod de tip "tat", n
timp ce nodurile rezultate n urma expandrii sunt noduri de tip "fiu".
Nodul rspuns reprezint configuraia la care se dorete s se ajung (altfel
spus, configuraia final). Acesta este obinut prin aplicarea unei succesiuni de
mutri configuraiei iniiale. Exist i situaii n care nu exist o succesiune de
mutri prin care s se ajung la configuraia final. n acest caz, spunem c
problema nu are soluie.
Nodul terminal (frunz) este un nod care nu are succesori. Dac la un mo
ment dat nu se mai poate realiza nici o mutare pe configuraia curent sau s-a
ajuns la configuraia final, atunci ea nu va mai avea nici un succesor.
Exist dou tipuri de noduri n arborele asociat problemei Branch & bound:
noduri active i noduri expandate.
Nodurile active reprezint mulimea de noduri obinut prin expandarea
unui nod. Aceste noduri sunt denumite i noduri nc neexpandate.
Un nod expandat este nodul pe care se realizeaz operaia de expandare.
Practic, pe configuraia reprezentat de acest nod se aplic toate operaiile per
mise de problem, obinndu-se o mulime de noduri active. Alegerea nodului
expandat este o problem dificil, dei nodul expandat este ales doar dintre
elementele mulimii nodurilor active existente n acel moment. Primul nod ex
pandat este reprezentat de configuraia iniial. Urmtoarele noduri expandate
sunt alese pe baza unui procedeu care va fi prezentat mai trziu. Nodul expandat
este ntlnit n literatura de specialitate i sub numele de nod E (engl. E-node)
sau nod curent.
Nodurile active sunt stocate ntr-o list. Modul de reprezentare al listei no
durilor active ne ofer informaii despre modul de parcurgere al arborelui solui
ilor. Dac nodurile active sunt pstrate ntr-o stiv (structur LIFO, Last In First
Out), atunci arborele spaiului de stri va fi parcurs n adncime (engl. DFS =
Depth First Search), iar dac nodurile sunt pstrate ntr-o coad (structur FIFO,
First In First Out), atunci arborele spaiului de stri va fi parcurs n lime (engl.
BFS = Breadth First Search).
Nu exist un criteriu, care odat aplicat, s permit gsirea urmtorului nod
care trebuie expandat. Din acest motiv se apeleaz la un procedeu specific
inteligenei artificiale, prin care se asociaz fiecrui nod o funcie c {funcie
238

15.1. PREZENTARE GENERAL


de cost). Ca i funcia de continuare din cazul metodei Backtracking, aceasta
este o funcie de limitare, utilizat pentru evitarea generrii unor subarbori care
nu au cum s conduc la nodul rspuns. De aici i numele metodei: branch
nseamn ramur, despritur, n timp ce bound nseamn margine, limit,
grani. Alegerea acestei funcii este partea cea mai dificil a metodei, pe ea
bazndu-se i selectarea urmtorului nod expandat.
Pe scurt, un algoritm Branch & bound funcioneaz la modul urmtor: din
lista nodurilor active se va alege unul care devine nod expandat. Acest nod este
urmtorul nod care va fi prelucrat. Nu pot exista mai multe noduri expandate
n acelai timp. n momentul n care un nod devine nod expandat, se vor gene
ra (determina) toate nodurile descendente (noduri succesor), acestea devenind
noduri active. Ele se vor aduga listei nodurilor active existente. Odat generai
descendenii nodului expandat, va fi ales acela care este situat cel mai aproape
de configuraia final sau, mai corect spus, cel care are cele mai mari anse de a
se afla mai aproape de starea final. Pentru a afla care este acest nod, se definete
funcia de cost c pe mulimea nodurilor din arborele spaiului soluiilor. Aceast
funcie difer de la problem la problem i trebuie aleas n funcie de cerinele
problemei respective, dar descoperirea ei este un pas important spre rezolvarea
problemei. Odat ce am determinat funcia de cost, vom alege dintre nodurile
active pe cel cu costul minim (pentru care funcia ia valoarea minim), care
devine astfel nod expandat. Parcurgerea de acest tip poart numele de cutare
LC (cutare Least Cost = cutare dup costul minim). Altfel spus, atunci cnd
cutm o soluie n arborele generat de spaiul configuraiilor folosind funcia de
limitare, spunem c efectum o cutare LC a soluiei problemei, indiferent dac
arborele n sine este parcurs n adncime (DFS) sau n lime (BFS). Cutarea
LC nu nlocuiete cutrile n lime sau n adncime, ci doar le mbuntete.
Funcia de cost nu poate fi determinat n mod precis, deoarece determinarea ei
exact este echivalent cu rezolvarea problemei iniiale. Chiar dac nu putem
determina o funcie de cost infailibil, exist cteva sugestii care pot fi urmate
n definirea ei:

s poat fi calculat pentru un nod doar pe baza informaiilor deinute de


nodurile intermediare aflate pe drumul ctre rdcina arborelui;

s fie independent de generarea nodurilor active ale nodului expandat.


239

15.2. UN EXEMPLU: PUZZLE CU 15 ELEMENTE

Figura 15.1: Un posibil aranjament al jocului de puzzle cu 15 elemente.


1
5
9
13

2
6
10
14

3
7
15

4
8
11
12

Figura 15.2: Aranjamentul final al jocului de puzzle cu 15 elemente.


1
5
9
13

2
6
10
14

3
7
11
15

4
8
12

15.2

Un exemplu: Puzzle cu 15 elemente

15.2.1

Enunul problemei

Puzzle cu 15 elemente este un joc distractiv inventat de Sam Loyd n 1878,


care const ntr-un tabel cu 16 celule (csue), dintre care 15 celule sunt nu
merotate de la 1 la 15, iar o celul rmne liber (Figura 15.1).
Regulile acestui joc sunt simple. Iniial se d un aranjament de tipul celui
din Figura 15.1, iar scopul jocului este de a transforma acest aranjament prin
transformri (mutri) succesive n aranjamentul din Figura 15.2.
Aranjamentul iniial se mai numete configuraie iniial sau stare iniial,
iar cel final configuraie final sau stare final. Pentru a ajunge de la confi
guraia iniial la cea final este permis doar un singur tip de mutare: o celul
vecin cu celula liber poate s fac schimb de poziii cu aceasta. Astfel, s
considerm aranjamentul din Figura 15.1 ca fiind configuraia iniial. Atunci
exist patru mutri permise de regulamentul jocului: se poate muta celula liber
n locul uneia din celulele care conin valorile 3, 6, 8, 7, ca n Figura 15.3
Dup cum se poate observa, se consider a fi celule vecine ale celulei libere,
doar cele care au o latur comun cu celul respectiv (n cazul configuraiei
noastre iniiale este vorba de csuele cu valorile 3, 6, 7, 8). Csuele cu valorile
2, 4, 10, 12 nu sunt considerate celule vecine ale celulei libere. Evident, nu tot
timpul sunt posibile toate cele patru mutri. n situaia n care celula liber nu
240

15.2. UN EXEMPLU: PUZZLE CU 15 ELEMENTE

Figura 15.3: Mutrile permise pentru celula liber din tabela 15.1.

1
5
9
13
1
5
9
13

sus
2
6
3
10
7
14 15
JOS
2
3
7
6
10
14 15

4
8
11
12
4
8
11
12

dreapta
1
2
3
5
6
8
10
7
9
13 14 15
stnga
1
2
3
5
6
10
7
9
13 14 15

4
11
12
4
8
11
12

are patru celule vecine (de exemplu, dac se afl pe prima linie, sau pe prima
coloan) mutrile respective nu sunt posibile.
Asemntor, pe fiecare dintre cele patru configuraii obinute se pot realiza
alte mutri, generndu-se astfel un nou set de configuraii. Toate aceste confi
guraii poart numele de stri ale jocului. O stare poate fi obinut pornind de
la starea iniial creia i se aplic o secven de mutri permise. Spaiul strilor
unei configuraii iniiale reprezint mulimea tuturor strilor care pot fi obinute
pornind de la starea iniial.
Generarea configuraiilor continu pn cnd se ajunge la configuraia fi
nal. Uneori este posibil ca starea final s nu poat fi obinut pornind de la
starea iniial, caz n care problema noastr nu va avea soluie.
15.2.2

Rezolvarea problemei

Cea mai direct cale de a rezolva un astfel de puzzle este ca pornind de


la starea iniial s se genereze toate configuraiile intermediare pn cnd se
obine configuraia final, sau s-a epuizat ntreg spaiul de cutare. Este uor
de observat c numrul total al configuraiilor jocului este de 16! 20.9 *
IO12. Prin urmare, spaiul strilor pentru problema noastr este foarte mare i o
cutare exhaustiv este sortit eecului.
nainte de a ncerca s cutm configuraia final n cadrul spaiului strilor,
este util s determinm dac starea final poate fi obinut printr-o secven de
mutri din configuraia iniial. Pentru aceasta vom nota csuele de la 1 la 16.
Astfel, n configuraia iniial din Figura 15.1 celula 1 conine valoarea 1, celula
11 conine valoarea 7, celula 12 conine valoarea 11, etc. Pentru configuraia
241

15.2. UN EXEMPLU: PUZZLE CU 15 ELEMENTE

Figura 15.4: Valoarea variabilei x, funcie de poziia celulei libere n configu


raia iniial

final, cele dou valori sunt egale: celula 1 conine valoarea 1, celula 2 conine
valoarea 2 etc... Celulei libere i se atribuie valoarea 16.
Considerm funcia position(i) ca fiind numrul celulei care conine valoa
rea i. De exemplu, n configuraia iniial prezentat n Figura 15.1, avem:
position(3) = 3
position(7) = 11
position(ll) = 12 etc.
Pentru celula liber avem position(16) = 7, ceea ce nseamn c spaiul liber
se afl pe poziia 7 n configuraia iniial.
De asemenea, considerm funcia less(i) ca fiind numrul de celule j, cu
j < i, pentru care position(j) > position(i). Cu alte cuvinte, aceast funcie
calculeaz cte celule cu valori mai mici dect valoarea celulei curente se gsesc
dup ea n configuraia iniial. De exemplu, n Figura 15.1, pentru celula
care conine valoarea 8, exist o celul cu valoare mai mic aflat pe o poziie
mai mare, i anume, celula cu valorea 7. Aadar, less(8)=l. Analog, se pot
determina i celelalte valori: less(13)=l, less(5)=0, less(ll)=0, etc.
Ultima consideraie se refer la poziia spaiului liber n cadrul configuraiei
iniiale. Considerm o variabil x care are valoarea 1, dac spaiul liber se afl
n configuraia iniial pe una din poziiile marcate cu * n Figura 15.4, sau 0,
dac spaiul liber se afl pe una dintre poziiile nemarcate n aceeai figur.
Avnd aceste notaii, putem enuna urmtorul rezultat:
Teorema 15.2.1 Configuraia final poate fi obinut din configuraia iniial
dac i numai dac
x + ^2\6 less(i) este un numr par.
Demonstraia acestei teoreme se bazeaz pe observaii simple asupra con
figuraiilor posibile i o putei gsi pe multe situri care trateaz aceast problem
242

15.2. UN EXEMPLU: PUZZLE CU 15 ELEMENTE


(de exemplu http : / / kevingong . corn/Mat h/ SixteenPuz z le . html).
Putem folosi aceast teorem pentru a determina dac starea final se afl n
spaiul strilor asociat strii iniiale. Dac starea final poate fi obinut plecnd
de la starea iniial, atunci are sens s ncepem cutarea secvenei de mutri
permise care ne conduce la starea final.
Pentru a realiza aceast cutare este util s organizm spaiul strilor ntr-un
arbore. Rdcina arborelui este starea (configuraia) iniial. Copiii fiecrui nod
X al arborelui reprezint stri care pot fi obinute din starea X printr-o mutare
permis de regulamentul jocului. Pentru simplitatea notaiei, este mai convena
bil s ne gndim la o mutare ca fiind mai degrab o mutare a celulei libere dect
a unei alte celule. Prin urmare, celula liber poate fi mutat la fiecare mutare,
fie n sus, n jos, la stnga sau la dreapta, dac este posibil.
Iat cum ar arta arborele spaiului de stri dup cteva mutri n configu
raia iniial:
Dup cum se poate observa i din Figura 15.5, prin mutri succesive s-au
obinut diverse stri intermediare, care au creat o structur arborescent: starea
A a generat strile B, C, D i E prin mutri n sus, respectiv la dreapta, n jos
i la stnga. Cele patru stri nou create, la rndul lor, au generat alte stri (de
exemplu, starea D a generat strile J i K prin mutri la dreapta i n jos). n final
din starea J s-a generat starea M prin mutarea spaiului liber n jos, atingndu-se
starea final, situaie n care generarea unor noi mutri este stopat. Se poate
observa c au fost necesare trei mutri pentru a atinge starea final pornind de la
starea iniial. Figura anterioar nu este complet, n sensul c, pentru a pstra
simplitatea, am ales s nu prezentm toate mutrile care ar fi fost posibile. De
exemplu, configuraia D genereaz de fapt patru alte stri (prin mutri n sus, n
jos, la stnga i la dreapta ale spaiului liber), dintre care noi am prezentat doar
dou (J i K).
Dac am fi generat arborele strilor folosind una dintre cele dou posibili
ti, parcurgerea n lime (n care se genereaz toate strile obinute din strile
B, C, D, E, apoi toate strile pentru F, G, H etc.) sau cea n adncime (n care
se genereaz toate strile obinute din starea B, apoi toate strile obinute din
starea F etc), ne-am fi ndeprtat simitor de soluia problemei. Acest lucru este
pus n eviden foarte clare de Figura 15.5. Asta se ntmpl pentru c, folosind
parcurgerea spaiului de configuraii n lime sau n adncime, cutarea soluiei
este "oarb", adic nu ine cont de configuraia iniial i nici de cele interme
diare obinute, pentru a vedea care dintre ele este mai aproape de configuraia
final. Practic, procedeul ar genera aceeai secven de stri indiferent de starea
iniial a problemei. Cutarea n adncime corespunde n mod precis parcurgii
spaiului de cutare rezultat n urma aplicrii metodei Backtracking . Coborrea
n arbore se asociaz pasului atribuie i avanseaz, iar revenirea la nodul printe
243

15.2. UN EXEMPLU: PUZZLE CU 15 ELEMENTE

244

15.2. UN EXEMPLU: PUZZLE CU 15 ELEMENTE


se asociaz pasului de revenire.
Pentru a evita cutarea oarb a soluiei, am dori s utilizm o metod de
cutare "inteligent". Aceast metod inteligent ar trebui s caute nodul rspuns
(configuraia final) i s adapteze calea ctre acesta, n funcie de starea iniial
de la care pornim cutarea. O metod pentru evitarea cutrii brute este s aso
ciem o funcie de cost fiecrui nod din spaiul strilor, funcie care s scoat
cumva n eviden nodurile care sunt mai "promitoare", deci care au anse
mai mari s ne conduc la soluia problemei.
Aadar, fiecrui nod X din arborele strilor i asociem o funcie c, iar c(X)
reprezint costul nodului X. Funcia c o definim ca o sum de alte dou funcii
/ i g, unde/PQ o definim ca fiind lungimea drumului de la rdcina arborelui
de stri (altfel spus, de la configuraia iniial) pn la nodul X, iar g(X) este o
estimare a lungimii cii celei mai scurte de la nodul X la un nod rspuns aflat
n subarborele de rdcin X. O posibil alegere pentru g(X) este numrul de
celule nelibere care nu se afl pe poziia din configuraia final. Aceasta este
o alegere natural, din moment ce este clar c pentru a ajunge la configuraia
final trebuie realizate cel puin g(X) mutri pe starea X. Pentru o mai bun
nelegere a funciei de cost, iat cteva valori calculate pentru arborele de stri
din Figura 15.5: c(B) = 1+4 = 5 (f(B) = 1 pentru c nodul B se afl pe
nivelul 2 n arbore, deci la o distan de un (1) nivel fa de rdcin, iar g(B) =
4 pentru c sunt 4 valori care nu se afl n poziiile lor finale: 3, 7, 11, 12),
c(C) = 1 + 5 = 6, c(D) = 1+2 = 3 etc.
Folosind funcia c definit anterior, vom realiza o cutare LC a nodului
rspuns pe baza algoritmului descris n paragraful 15.1.1. Dup cum s-a putut
observa n cazul cutrilor nodului rspuns prin intermediul parcurgerii stan
dard n lime sau n adncime a arborelui de stri, selecia nodului expandat nu
s-a realizat innd cont de ansele de a ajunge la nodul rspuns ntr-un timp ct
mai redus. Cutarea nodului rspuns este "mbuntit" din punct de vedere al
vitezei prin utilizarea unei funcii inteligente de clasificare a nodurilor, aa cum
este cazul funciei de cost c. Urmtorul nod expandat va fi selectat, n algorit
mul pe care l propunem, nu la ntmplare (a se citi "indiferent de configuraia
reprezentat de nodul respectiv"), ci pe baza valorilor pe care posibilele noduri
expandabile le vor avea prin aplicarea funciei de cost.
Cutarea LC pornete de la configuraia iniial (Figura 15.1). Conform
algoritmului, acest nod este primul nod expandat (nodul A din Figura 15.5). Se
genereaz toi fiii nodului expandat, adic nodurile B, C, D i E. Aceste patru
noduri reprezint noduri active i vor fi adugate n lista nodurilor active. Din
lista nodurilor active existente, se va alege viitorul nod expandat. Pentru aceasta
se calculeaz c(B), c(C), c(-D), c(E), alegndu-se nodul pentru care funcia c
are valoarea minim. Cum c(B) = 5, c(C) = 6, c(D) = 3, c(E) =1+4 = 5,
245

15.2. UN EXEMPLU: PUZZLE CU 15 ELEMENTE


rezult c urmtorul nod expandat este nodul D. Nodul proaspt expandat este
eliminat din lista nodurilor active. n continuare, pentru acest nod se genereaz
toi fiii (nodurile J i K din Figura 15.5 sunt doar o parte dintre fiii nodului
expandat, la ceilali renunndu-se din lips de spaiu), care devin noduri active
i sunt adugai n lista nodurilor active (aceasta va conine nodurile B, C, E, J
i K). Pentru nodurile active existente se calculeaz din nou valorile funciei c,
obinndu-se valoarea minim pentru nodul J, care devine noul nod expandat,
fiind eliminat din lista nodurilor active. Procedeul se aplic ntr-un mod similar,
pn cnd se obine nodul rspuns, dac problema are soluie. n final, n cazul
n care problema are soluie, se obine secvena de mutri care a dus la obinerea
nodului rspuns. n cazul nostru, aceast secven este A, D, J, M.
Lipsa spaiului a fcut ca arborele de stri asociat configuraiei iniiale s nu
conin toate mutrile posibile.

n final, prezentm implementarea Java a problemei puzzle-ului cu 15 ele


mente:
1 import java. util.*;
2 import io . Reader ;
3
4 /* *
5 * Rezolvarea jocului de Puzzle cu 15 elemente .
6 */
7 public elass Puzzle
8(
9 //pstreaz valorile funciei f ptr. fiecare nod activ
10 public static Vector valoriF = new Vector ();
11
12 //distanta de la rdcina la un nod
13 public static int distanta = 0;
14
15 public static void main (String[] args)
,6 j
17
System . out . println (" Introducei configuraia iniiala " +
s
"(ptr. spaiul gol tastai valoarea 16): ");
20
21
22
23
24
25
27
28
246

int n = 4 ;
i n t [ ] [ ] x = new i n t [ n ] [ n ] ;
obtineConfiguratialnitiala(x);
if (existaSolutie(x))
{
cautaSolutie(x);
}

15.2. UN EXEMPLU: PUZZLE CU 15 ELEMENTE


29
30
ai
33
34
35
36
37
38
39
40
4,
42
44
46
47
48
49
50

52
53
55
57
58
59
60
61
62
63
64
65
66
67
68
69
70
73

77
78

e1se
{
System. out.println( "NU exista soluii !!!");
}
}
/** Citete configuraia iniiala a jocului. */
public static void obtineConfiguratiaInitiala(int[][] x)
{
for ( int i = 0; i < x.length ; i ++)
{
for (int j = 0; j < x.length ; j ++)
{
System . out . p ri n t (" x [ " + i +
"]["+ j + "]=");
x[i][j] = Reader . readlnt ( ) ;
}
}
}
/** Determina daca jocul are soluie . */
public static boolean existaSolutie(int[][] x)
{
int s = 0;
for (int i = 1 ; i <= 16; i ++)
!
s+=less(x, i);
}
s += determinaPozSpatiu (x ) ;
if ( s % 2 == 0) return true ;
else return false ;
)
/** Funcia po sition . * /
public static int position(int[][] x, int el)
{
for (int i = 0; i < x.length ; i ++)
{
for (int j = 0; j < x.length ; j ++)
j
if ( el == x[i ][j ])
!
return i * x.length + j + 1;
}
}
}
return 0;
247

15.2. UN EXEMPLU: PUZZLE CU 15 ELEMENTE


79
80
si
82
83
84

}
/** Funcia le s s . * /
public static int less(int[][] x , int i)
{
intnr=0;

86

for ( int j = 1 ; j < i ; j ++)


{
if (position(x, j) > position(x, i))
{
nr++;
)
}

88
89
90
92
93
94
,5
96
97
98
99
oo
101
102
103
104
105
106
107
ies
109
i io
112
114

1 19
120
121
122
123
124
125
126
127
128
248

return nr ;
}
/** Determina poziia spaiului pe grila de joc. */
public static int determinaPozSpatiu(int[][] x)
{
for (int i = 0; i < x.length; i++)
{
for (int j =0; j <x.length; j ++)
{
// 4 * 4 = 1 6 = spaiu liber
if (x[i][j]==x.length * x.length)
{
if (( i % 2 == 0 && j % 2 == 1) II
(i % 2 == 1 && j % 2 == 0))
{
return 1 ;
}
else
{
return 0;
}
}
)
}
return 0 ;
}
/** Caut soluia jocului pe baza configuraiei iniiale. */
public static void cautaSolutie(int[][] x)
{
//lista nodurilor active
Vector noduri Active = new Vector ();
//nodul expandat ( implicit este configuraia iniiala )

15.2. UN EXEMPLU: PUZZLE CU 15 ELEMENTE


129
130
131
132
133
134
135
136
137
i3s
139
140
141
142
143
144
145
146
147
i4s
149
150
151
152

int[][] nodExpandat = x;
//pstreaz succesiunea de mutri efectuate
Vector mutri = new Vector ();
for ( ; ; )
{
distanta++;
noduriActive = determinaNoduriActive ( nodExpandat ,
noduriActive ) ;
if ( noduriActive . size ( ) == 0) return ;
nodExpandat = determinaNodExpandat ( noduri Active ) ;
mutri . addElement ( nodExpandat ) ;
if ( gc ( nodExpandat ) == 0)
{
a fi s e a z a S o 1 u t i e ( mutri ) ;
return ;
}
int minPos = noduri Active . indexOf ( nodExpandat ) ;
noduriActive .removeElementAt(minPos);
val ori F . removeElement At ( minPos ) ;
}

154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
'74
176

}
/* *
* Determina nodurile active din mulimea crora se
* va alege nodul expandat .
*/
public static Vector determinaNoduriActive (int [] [] x,
Vector noduriActive)
{
//caut spaiul gol in interiorul configuraiei
int i 1 = 0 ;
int j 1 = 0 ;
for (int i = 0; i < x.length ; i ++)
{
for (int j = 0; j < x.length ; j ++)
{
if (x[i][j] == x.length * x.length)
{
i1 = i ;
jl = j ;
}
}
}

178
249

15.2. UN EXEMPLU: PUZZLE CU 15 ELEMENTE


179
180
isi
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
i97
198
199
200
201
202
203
204
205
206
207
208
209
210
2ii
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
250

if (il ! = O ) //mutare SUS posibila


{
int[][] xUp = new int [x. length ] [x . length ] ;
copie (x , xUp ) ;
int aux = xUp [ i 1 l][jl ];
xUp[il - l][jl] = xUp[il][jl];
xUp [ i 1 ] [ j 1 ] = aux ;
noduri Active. addElement (xUp ) ;
valoriF . addElement (new I n t e ge r ( d i s t an t a ) ) ;
}
if (il != x. length 1) //mutare JOS posibila
{
int |]|] xDown = new in t [ x . len gth ] [ x . len gth ] ;
copie (x, xDown);
int aux = xDown [il ] [ j 1 ] ;
xDown [ i 1 ] [ j 1 ] = xDown [il + 1 ] [ j 1 ] ;
xDown [il + 1 ] [ j 1 ] = aux ;
noduriActive. addElement (xDown) ;
valoriF . addElement (new I n t e ge r ( d i s t an t a ) ) ;
}
if (jl != 0) //mutare STNGA posibila
{
n t | Il I xLeft = new in t [ x . len gth ] [ x . len gth ] ;
copie (x , xLeft ) ;
int aux = xLeft [ i 1 ] [ j 1 1];
xLeft[il][jl - 1] = xLeft[il ][jl ];
xLeft[il][jl] = aux ;
noduriActive. addElement ( xLeft ) ;
valoriF . addElement (new I n t e ge r ( d i s t an t a ) ) ;
}
if (jl != x. length 1) //mutare DREAPTA posibila
{
n t | || | xRight = new in t [ x . len gth ] [ x . len gth ] ;
copie (x, xRight);
int aux = xRight[il][jl|;
xRight [il ][jl ] = xRight [il ][jl + 1|;

15.2. UN EXEMPLU: PUZZLE CU 15 ELEMENTE


229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278

xRight [ i 1 ] [ j 1 + 1] = aux ;
noduriActive. addElement ( xRight ) ;
valoriF . addElement (new Integer( distanta ));
}
return noduriActive ;
}
/* *
* Metoda utila de copiere a valorilor dintrun sir sursa
* intrun sir destinaie.
*/
public static void copie (int[][] src, int [] [] dest)
{
for (int i = 0; i < src . length ; i ++)
{
for (int j =0; j < src. length; j ++)
{
dest [i ][j ] = src [i ][j ];
}
}
}
/** Determina nodul expandat pe baza nodurilor active. */
public static int [ ] [ ] determinaNodExpandat (Vector noduriActive)
{
int minPos = 0 ;
int min = Integer .MAXVALUE;
for (int i = 0; i < noduriActive. size ( ) ; i ++)
{
int [ ] [ ] el = (int[][]) noduriActive. elementAt(i);
i f ( cc ( e 1 , i)< min )
{
min = cc ( el , i ) ;
minPos = i ;
}
}
return (int[][]) noduriActive. elementAt( minPos ) ;
}
/** Calculul funciei c. */
public static int cc(int[][] x , int j)
{
return gc(x) + f(j );
}
/** Calculul funciei g. */
251

15.2. UN EXEMPLU: PUZZLE CU 15 ELEMENTE


279
280
281
282
283
284
285
286
28?
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
31 I
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
252

public static int gc(int [][] x)


{
int nr = 0 ;
for (int i = 0; i < x.length; i++)
{
for (int j =0; j <x.length; j ++)
{
if (x[i][j] ! = i * x.length + j+ l&&
x[i ][j ] != 16)
{
nr++;
}
}
}
return nr ;
}
/** Calculul funciei f. */
public static int f(int j)
{
return ((Integer) valoriF.elementAt(j)).intValue();
}
/** Afiarea soluiei obinute. */
public static void afiseazaSolutie (Vector mutri)
{
System. out.println(" Mutrile efectuate: ");
for (int i = 0; i < mutari.size(); i ++)
{
int[][]m=(int[][]) mutri. elementAt ( i ) ;
for (int j = 0; j <m.length; j ++)
{
for ( int k = 0; k < m. length ; k++)
{
if (m[j][k] = = 16) //spaiu
{
System, out. prin ("
");
}
else if (m[j ][k] < 10)
{
System, out. prin (" " + m[ j ] [k ] ) ;
}
else
{
System, out. prin (" " + m[ j ] [k ] ) ;
}
}

15.2. UN EXEMPLU: PUZZLE CU 15 ELEMENTE


329
System. out.println();
330
}
331
System. out.println();
332
}
333 }
334 }

Rezumat
Capitolul de fa a prezentat metoda Branch & bound de elaborare a al
goritmilor. Asemntoare metodei Backtracking, aceast metod este mai rar
utilizat dect Backtracking-ul, de aceea a fost prezentat n mai puine detalii.
S-a insistat ns asupra mecanismului ei de funcionare, care se bazeaz pe o
funcie de cost asociat nodurilor din arborele de stri. Funcia de cost trebuie
aleas n funcie de problema care trebuie rezolvat. Problema puzzle-ului cu
15 elemente este un bun exemplu pentru aprofundarea cunotinelor de Branch
and bound.

Noiuni fundamentale
arbore de stri: arbore oarecare generat de aplicarea mutrilor permise de
problem unei configuraii iniiale.
funie de cost: funcie special care difer de la problem la problem i
este asociat unui nod din arborele de stri. Cu ajutorul ei se elimin subarborii
care nu duc la rezultatul dorit, obinndu-se o reducere semnificativ a timpului
de aflare a soluiei.
nod activ: nod care a fost obinut prin expandarea unui alt nod i care nu a
fost nc, la rndul lui, expandat.
nod expandat: nodul curent, pentru care au fost generai fiii (s-au realizat
mutrile permise de problem)
nod rspuns: configuraia final la care trebuie s se ajung prin efectuarea
mutrilor permise asupra configuraiei iniiale.

Exerciii
Teorie
1. Fie S = x +
less(i) pentru o configuraie oarecare a problemei
puzzle cu 15 elemente. Demonstrai c S mod 2 este invariant pentru
253

15.2. UN EXEMPLU: PUZZLE CU 15 ELEMENTE


orice stare obinut din starea iniial. Cu alte cuvinte, dac S este im
par, atunci S rmne impar pentru orice secven de mutri legal, iar
dac este par, atunci rmne par.
2. Folosind observaia de la exerciiul precedent, demonstrai teorema 15.2.1.
n practic
1 . Rezolvai problema discret a rucsacului folosind o abordare Branch and
bound. Reamintim c n problema discret a rucsacului se dau un numr
de n produse, fiecare cu greutatea Wi i costul pi, i un numr pozitiv M
ce reprezint capacitatea rucsacului. Se cere s se aleag o submulime
de produse astfel nct
WiXi < M i Y^i p%Xi este maxim, unde x
este un vector de dimensiune n ale crui elemente se afl n intervalul [0,
1]. Altfel spus, este posibil ca dintr-un produs s se aleag doar o anumit
parte i nu ntregul.
2. Rezolvai problema comis-voiajorului folosind metoda Branch and bound.
Reamintim c problema comis-voiajorului are urmtoarea definire: un
comis voiajor trebuie s treac prin toate oraele dintr-un jude, doar o
singur dat, astfel nct costul drumului realizat s fie minim.
3. Se d urmtoarea problem de optimizare:
max 3xi + x-i + X3 + 2x4 + 2x5 + 3x6
avnd restriciile:
Xi +x2
+Vi =
xi
+x3
+y2 =
x2
+x3
+V3 =
2
+X4
+V4 =
x3
+x5
+y5 =
X4
+x5
+y6 =
X4
+x6 +1/7 =
x5
+x6 +ys =
H G {0,1}, w > Oi = 1,2,..., 6
Observai c spaiul de cutare al problemei este finit (26). S se gseasc
soluia optim a problemei utiliznd mai nti o strategie de tip Backtracking, iar apoi o strategie tip Branch & bound.

254

16. Metode de elaborare a algoritmi


lor (sintez)

Punctul de convergen al artei i


tiinei este metoda.
Edward Bulwer-Lytton
Vom prezenta o scurt sintez a metodelor de elaborare a algoritmilor, care
poate fi utilizat ca o referin rapid n clasificarea problemelor de algoritmic.

16.1

Backtracking

Metoda backtracking se poate aplica problemelor a cror soluie se scrie


sub form de ir, cu fiecare component a irului aparinnd unei mulimi finite.
Soluia se construiete incremental, ncepnd cu prima component i mergnd
ctre ultima, cu eventuale reveniri la componentele anterioare. Fiecare compo
nent trebuie s respecte condiiile de continuare, care sunt derivate din condii
ile interne.
Complexitatea algoritmilor backtracking este n general exponenial. Cu
ct condiia de continuare este mai bine aleas (elimin ct mai multe soluii
pariale care nu pot conduce la o soluie), cu att soluia va fi mai eficient.

16.2

Divide et impera

Divide et impera se aplic problemelor care se pot descompune n subprobleme, astfel nct soluia problemei originale s se poat obine uor din solui
ile subproblemelor. Subproblemele se mpart la rndul lor n subprobleme pn
255

16.3. GREEDY
cnd se ajunge la subprobleme triviale, care admit soluie imediat. Exprimarea
rezolvrii este recursiv, ca i algoritmii care utilizeaz aceast metod.
Timpul de calcul este, n general, de forma nk log n. Cele mai eficiente
metode de sortare (Mergesort, Quicksort) se bazeaz pe aceast metod.

16.3

Greedy

Metoda Greedy se aplic problemelor de optimizare care respect principi


ul optimalitii (substructur optim) i principiul alegerii Greedy. Principiul
optimalitii ne asigur c o soluie optim cuprinde subsoluii optime. Prin
cipiul alegerii Greedy ne asigur c alegnd la fiecare pas optimul local, se
obine optimul global. Exist multe situaii n care metoda Greedy se aplic
problemelor care nu respect principiul alegerii Greedy (de exemplu, problema
discret a rucsacului), caz n care soluia dat de metoda Greeddy nu va mai fi
ntotdeauna cea optim.
Datorit simplitii strategiei, soluiile Greedy au cel mai adesea complexi
tate polinomial.

16.4

Programare dinamic

Programarea dinamic se aplic problemelor de optimizare care respect


principiul optimalitii. Reiese imediat c programarea dinamic se aplic unei
clase mai largi de probleme dect metoda Greedy. Problemele de programare
dinamic se rezolv prin construirea soluiei problemei pe baza soluiilor optime
ale subproblemelor. Din acest punct de vedere, programarea dinamic se afl
ntre Greedy, care ia n considerare o singur subproblem (cea optim local),
i Backtracking, care genereaz exhaustiv toate soluiile.
Complexitatea algoritmilor de programare dinamic este n general polino
mial (vezi toate problemele prezentate n capitolul dedicat metodei Greedy),
dar poate fi i exponenial (problema comis- voiajorului).
Abordarea rezolvrii este bottom-up, ceea ce nseamn c se rezolv mai
nti problemele de dimensiune mai mic, pe baza lor cele mai mari, pn cnd
se ajunge la soluia problemei iniiale.

16.5

Branch & bound

Branch & bound este o variant a metodei backtracking, n care alegerea


urmtorului element nu se face pur i simplu aleator (sau n ordine cresc toa
256

16.5. BRANCH&BOUND
re) ci se urmrete alegerea variantelor care sunt mai promitoare n vederea
obinerii unei soluii. Aceasta nseamn c fiecare posibil variant este evaluat
dup anumite criterii specifice fiecrei probleme n parte, alegndu-se la fiecare
pas varianta evaluat a fi optim.
Tabel sintez:
Tabelul urmtor prezint o sintez a informaiilor din acest paragraf.
Metoda
Condiii de aplicare
Timp de lu
Construire
cru (n gene
soluie
ral)
Backtracking
ir cu elemente lund exponenial
incremental
valori n mulimi finite
nk * logn
Divide et Im- Soluia se poate con
top-down
pera
strui pe baza soluiilor
subproblemelor
Greedy
Probleme de opti
polinomial
incremental
mizare care respect
principiul optimalitii
i al alegerii Greedy
Programare
Probleme de opti
polinomial
bottom-up
dinamic
mizare care respect
principiul optimalitii
Branch
& Probleme de opti
difer funcie parcurgere
bound
mizare dificile n de problem
least-cost
care aplicarea celor
lalte metode nu este
adecvat.

257

Bibliograe
[Andonie]

R. Andonie, I. Grbacea - Algoritmi si Calculabilitate, Computer


Press Agora, 1995

[Balanescu]

T.Balanescu - Metoda Backtracking, Gazeta de informatica, nr


2/1993, pag. 9-18

[Boian]

F.M. Boian - De la aritmetica la calculatoare, Presa Universitara


Clujeana, 1996, ISBN 973-97535-5-8

[Cormen]

T.H. Cormen, C.E. Leiserson, R.R. Rivest - Introducere n Algo


ritmi, Computer Press Agora, 2000, ISBN 973-97534-3-4

[Danciu]

D.Danciu, S.Dumitrescu - Algoritmica si Programare, Curs si


Probleme de Seminar, Reprograa Univ. Transilvania, 2002

[Eckel]

B. Eckel - Thinking in Java, http://www.mindview.net/Books/


TIJ/

[Horrowitz]

E. Horrowitz, S. Sahni - Fundamentals of Computer Algorithms,


Computer Science Press, ISBN 3-540-12035-1

[Norton]

P. Norton, W. Stanek - Peter Nortons Guide to Java Programming, Sams.net Publishing, 1996, ISBN 1-57521-088-6

[Roman]

Ed Roman - Mastering Enterprise JavaBeans, Wiley Computer


Publishing, 1999, ISBN 0-471-33229-1

[Sorin]

T.Sorin - Tehnici de programare,manualpentruclasa a X-a, L&S


Infomat, 1996

[Tomescu]

I. Tomescu - Data Structures, Bucharest University Press,


Bucharest, 1997
259

BIBLIOGRAFIE

[Vanderburg] G. Vanderburg - Tricks of the Java Programming Gurus,


Sams.net Publishing, 1996, ISBN 1-57521-102-5
[Weiss]

260

M.A. Weiss - Data Structures and Problem Solving Using Java,


Addison-Wesley, 1998, ISBN 0-201-54991-3

Index

n(/),2i
e(/),2i

condiii de continuare, 110, 130


condiii interne, 109, 110, 130
configuraie, 112, 237
configuraie final, 116, 130, 237
configuraie iniial, 113, 130, 237
configuraie soluie, 113
corectitudinea algoritmilor, 169
cost, 239

acces secvenial, 56
ActiveX, 7
alegere optim, 167
algoritm, 15
algoritm exponenial, 36
algoritm liniar, 36
algoritm polinomial, 36
algoritm recursiv, 25
apel recursiv, 139, 142
arbore, 104, 237
arbore binar, 104
arbore binar de cutare, 69
arbore de stri, 253
autoapelare, 138

dequeue, 5 1
DFS, 238
distan, 220
divide et impera, 12, 255
E-node, 238
enqueue, 51
enterprise, 7
expandare, 238
expresie aritmetic, 157

backtracking, 12, 108, 130, 255


BFS, 238
branch and bound, 12, 236, 256

fezabil, 174
FIFO, 238
forma polonez, 157
formule de recuren, 210
frunz, 104,237,238
funcie de selecie, 174, 183
funcie obiectiv, 174
funcie de cost, 239, 253

cutare LC, 239


cutare binar, 162
candidai, 173
celul, 240
cercetri operaionale, 1 87
coad, 5 1
coad de prioritate, 93
complexitate, 15
condiie de terminare, 139, 162

getFront, 5 1
greedy, 12, 256
261

INDEX
GUI, 7
hash, 104
hashtable, 83
inteligen artificial, 238
interclasare, 180
internet, 7
iterator, 57, 104
Java 2 Platform, 7
LC (Least Cost), 239
LIFO, 238
list nlnuit, 55
lungime extern ponderat, 181

pager, 7
partiionare, 152
PDA, 7
pivot, 152
principiul alegerii greedy, 168
principiul invarianei, 19
principiul optimalitii, 168, 183, 186,
226
probare liniar, 9 1
probare ptratic, 9 1
problem de optimizare, 183, 226
programare dinamic, 12, 226, 256
proprietatea de alegere greedy, 1 73,
183
quicksort, 152, 162

mergesort, 149, 162


metod recursiv, 162
mutri, 237
nod, 55, 238
nod nc neexpandat, 238
nod activ, 238, 253
nod curent, 238
nod E, 238
nod expandat, 238, 253
nod rspuns, 238, 253
nod terminal, 238
notaie asimptotic, 1 8
numere catalane, 194
O(f), 19
operaii recursive, 143
operand, 157
operator, 157
optim global, 167, 176
optim local, 176
optimalitate, 201
optimizare, 167, 186
ordonare, 149
262

rdcin, 237
Reader, 120
recuren, 138, 21 1
recuri vitate, 137
relaie de recuren, 221
relaii Moivre, 28
Shockwave, 7
soluie optim, 226
soluie optimal, 237
sortare, 137
spaiul soluiilor posibile, 109
spaiul strilor, 236
stiv, 45
stiva programului, 142
strategie euristic, 173
strategie greedy, 1 67
structur de date, 40
subproblem, 137, 187,226
substructur optim, 168, 173, 176,
183, 186, 216
succesor, 238
Sun Microsystems, 7

INDEX

suprapunere de apeluri, 162


tabel, 188, 189
tabela de repartizare, 83
teoria grafurilor, 167
triunghiul lui Pascal, 192
turnurile din Hanoi, 24
valori consumate, 111
web, 7

263

S-ar putea să vă placă și