Sunteți pe pagina 1din 320

Vlad Huţanu Tudor Sorin

INFORMATICĂ INTENSIV
(filiera teoretică, profilul real, specializarea
matematică-informatică, intensiv informatică)

Ciclul superior al liceului,


clasa a XI-a

Editura L&S Soft


Bucureşti
Copyright 2006-2016  L&S SOFT
Toate drepturile asupra acestei lucrǎri aparţin editurii L&S SOFT.
Reproducerea integralǎ sau parţialǎ a textului din aceastǎ carte este posibilǎ
doar cu acordul în scris al editurii L&S SOFT.

Manualul a fost aprobat prin Ordinul ministrului Educaţiei şi Cercetării


nr. 4446 din 19.06.2006 în urma evaluării calitative organizate de către
Consiliul Naţional pentru Evaluarea şi Difuzarea Manualelor şi este
realizat în conformitate cu programa analitică aprobată prin Ordin al
ministrului Educaţiei şi Cercetării nr. 3252 din 13.02.2006.

Referenţi ştiinţifici:

Prof. Dr. Victor Mitrana, Facultatea de Matematică, Universitatea Bucureşti


Prof. grad I Valiana Petrişor, Colegiul Naţional Bilingv George Coşbuc

Tiparul executat la S.C. LUMINATIPO s.r.l.


Str. Luigi Galvani nr. 20 bis, sector 2, Bucureşti

Descrierea CIP a Bibliotecii Naţionale a României


HUŢANU, VLAD
Informatică intensiv : manual pentru ciclul superior al
liceului : clasa a XI-a - (filiera teoretică, profilul real, specializarea
matematică-informatică, intensiv informatică) / Vlad Huţanu, Sorin
Tudor. - Bucureşti : Editura L & S Soft, 2006
ISBN (10) 973-88037-0-5; ISBN (13) 978-973-88037-0-1
I. Tudor, Sorin
004(075.35)

Editura L&S SOFT:


Adresa: Str. Stânjeneilor nr. 6, Sector 4, Bucureşti;
Telefon: 0722-573701; 0727.731.947;
E-mail: office@ls-infomat.ro
Web Site: www.ls-infomat.ro
3

Cuprins

Capitolul 1. Alocarea dinamică a memoriei……………………………… 7


1.1. Generalităţi ……………………………………………………………………………………… 7
1.2. Variabile de tip pointer…………………………….............................................................. 8
1.2.1. Variabile de tip pointer în Pascal.........................................................................8
1.2.2. Variabile de tip pointer în C++...........................................................................11
1.3. Alocarea dinamică a memoriei………………………………………………………………. 14
1.3.1. Alocarea dinamică în Pascal.............................................................................14
1.3.2. Alocarea dinamică în C++.................................................................................17
Probleme propuse………………………………………………………………………………….. 21
Răspunsuri …………………………………………………………………………………………. 22

Capitolul 2. Liste liniare …………………………………………………… 23


2.1. Definiţia listelor ……………………………………………………………………………….. 23
2.2. Liste liniare alocate simplu înlănţuit………………………………………………………… 24
2.2.1. Prezentare generală……………………………………………………………….. 24
2.2.2. Crearea şi afişarea listelor………………………………………………………… 24
2.2.3. Operaţii asupra unei liste liniare………………………………………………….. 28
2.2.4. Aplicaţii ale listelor liniare………………………………………………………….. 34
2.2.4.1. Sortarea prin inserţie....................................................................... 34
2.2.4.2. Sortarea topologică......................................................................... 36
2.2.4.3. Operaţii cu polinoame..................................................................... 41
2.3. Liste liniare alocate dublu înlănţuit………………………………………………………….. 50
2.3.1. Crearea unei liste liniare alocată dublu înlănţuit………………………………… 50
2.3.2. Adăugarea unei înregistrări la dreapta…..……………………………................ 51
2.3.3. Adăugarea unei înregistrări la stânga…..…………….………………………….. 51
2.3.4. Adăugarea unei înregistrări în interiorul listei……..…………………………….. 51
2.3.5. Ştergerea unei înregistrări din interiorul listei……..…………………………….. 52
2.3.6. Ştergerea unei înregistrări la stânga/dreapta listei……….…………………….. 53
2.3.7. Listarea de la stânga la dreapta listei………..……..…………………................ 53
2.3.8. Listarea de la dreapta la stânga listei ……..…………………………………….. 53
2.4. Stiva implementată ca listă liniară simplu înlănţuită………………………………………. 55
2.5. Coada implementată ca listă liniară simplu înlănţuită…………………………………….. 56
Probleme propuse………………………………………………………………………………….. 58
Răspunsuri la testele grilă………………………………………………………………………… 63

Capitolul 3. Metoda Divide et Impera…………………………………… 64


3.1. Prezentare generală………………………………………………………………………….. 64
3.2. Aplicaţii…………………………………………………………………………………………. 64
3.2.1. Valoarea maximă dintr-un vector…………………………………………………. 64
3.2.2. Sortarea prin interclasare………………..………………………………………… 66
3.2.3. Sortarea rapidă……………………………………………………………………… 68
3.2.4. Turnurile din Hanoi…………………………………………………………………. 71
3.2.5. Problema tăieturilor…………………………………………………………………. 72
4 Cuprins

3.3. Fractali…………………………………………………………………………………………. 75
3.3.1. Elemente de grafică………………………………………………………………… 75
3.3.1.1. Generalităţi (varianta Pascal).......................................................... 75
3.3.1.2. Generalităţi (varianta C++).............................................................. 77
3.3.1.3. Setarea culorilor şi procesul de desenare (Pascal şi C++)............ 78
3.3.2. Curba lui Koch pentru un triunghi echilateral..…………………………………... 80
3.3.3. Curba lui Koch pentru un pătrat..…………………………………………………. 83
3.3.4. Arborele…………………………..………………………………………………….. 85
Probleme propuse………………………………………………………………………………….. 87
Răspunsuri………………………………………………………………………………………….. 88

Capitolul 4. Metoda Backtracking………………………………………… 90


4.1. Prezentarea metodei…………………………………………………………………………. 90
4.1.1. Când se utilizează metoda backtracking ?....................................................... 90
4.1.2. Principiul care stă la baza metodei backtracking……………………………….. 90
4.1.3. O modalitate de implementare a metodei backtracking………………………………. 92
4.1.4. Problema celor n dame……………………………………………………………. 95
4.2. Mai puţine linii în programul sursă………………………………………………………….. 98
4.3. Cazul în care se cere o singură soluţie. Exemplificare: problema colorării hărţilor…... 101
4.4. Aplicaţii ale metodei backtracking în combinatorică……………………………………... 103
4.4.1. O generalizare utilă……………………………………………………………….. 103
4.4.2. Produs cartezian…………………………………………………………………... 104
4.4.3. Generarea tuturor submulţimilor unei mulţimi………………………………….. 106
4.4.4. Generarea combinărilor…………………………………………………………... 108
4.4.5. Generarea aranjamentelor……………………………………………………….. 110
4.4.6. Generarea tuturor partiţiilor mulţimii {1, 2, ..., n}…………………………………... 112
4.5. Alte tipuri de probleme care se rezolvă prin utilizarea metodei backtracking………….114
4.5.1. Generalităţi…………………………………………………………………………. 114
4.5.2. Generarea partiţiilor unui număr natural…………………………………………... 115
4.5.3. Plata unei sume cu bancnote de valori date……………………………………… 117
4.5.4. Problema labirintului………………………………………………………………… 119
4.5.5. Problema bilei…………………………………………………………………………122
4.5.6. Săritura calului……………………………………………………………………….. 124
Probleme propuse………………………………………………………………………………… 125
Indicaţii…………………………………………………………………………………………….. 128

Capitolul 5. Metoda Greedy ……………………………………………… 129


5.1. Generalităţi …………………………………………………………………………………... 129
5.2. Probleme pentru care metoda Greedy conduce la soluţia optimă……………………... 130
5.2.1. Suma maximă……………………………………………………………………… 130
5.2.2. Problema planificării spectacolelor……………………………………………… 131
5.2.3. Problema rucsacului (cazul continuu)……..……………………………………. 133
5.2.4. O problemă de maxim……..……………………………………………………… 135
5.3. Greedy euristic……………………................................................................................. 137
5.3.1. Plata unei sume într-un număr minim de bancnote.…………………………… 137
5.3.2. Săritura calului……………………………………………………………………….. 139
5.3.3. Problema comis-voiajorului………...………………………………………………..141
Probleme propuse………………………………………………………………………………… 142
Răspunsuri / Indicaţii…………………………………………………………………………...... 144
Manual de informatică pentru clasa a XI-a 5

Capitolul 6. Programare dinamică ……………………………………… 145


6.1. Generalităţi …………………………………………………………………………………... 145
6.2. Problema triunghiului………………………………………………………………………... 147
6.3. Subşir crescător de lungime maximă……………………………………………………... 151
6.4. O problemă cu sume……………………………………….............................................. 154
6.5. Problema rucsacului (cazul discret)……..………………………………………………… 156
6.6. Distanţa Levenshtein……..…………………………………………………………………. 161
6.7. Înmulţirea optimă a unui şir de matrice…………………………………………………… 167
6.8. Probleme cu ordinea lexicografică a permutărilor………………………………………. 174
6.9. Numărul partiţiilor unei mulţimi cu n elemente…………………………………………… 177
Probleme propuse………………………………………………………………………………… 180
Indicaţii…………………………………………………………………………………………….. 183

Capitolul 7. Grafuri neorientate ………………………………………… 186


7.1. Introducere …………………………………………………………………………………... 186
7.2. Definiţia grafului neorientat……………………………………………………………........ 187
7.3. Memorarea grafurilor……………………………………………………………................. 188
7.4. Graf complet…………………………………………………………….............................. 196
7.5. Graf parţial, subgraf…………………………………………………................................. 197
7.6. Parcurgerea grafurilor neorientate…………………………………................................. 198
7.6.1. Parcurgerea în lăţime (BF – Bredth First)....................................................... 199
7.6.2. Parcurgerea în adâncime (DF – Depth First)................................................. 201
7.6.3. Estimarea timpului necesar parcurgerii grafurilor........................................... 203
7.7. Lanţuri…………………………………........................................................................... 203
7.8. Graf conex……………………………………………………………................................ 207
7.9. Componente conexe………………………………………………................................... 208
7.10. Cicluri………………………………………………........................................................ 210
7.11. Ciclu eulerian, graf eulerian………..……………........................................................ 212
7.12. Grafuri bipartite………..……………........................................................................... 215
7.13. Grafuri hemiltoniene..……………............................................................................. 217
Probleme propuse………………………………………………………………………………… 221
Răspunsuri………………………………………………………………………………………… 227

Capitolul 8. Grafuri orientate …………………………………………… 230


8.1. Noţiunea de graf orientat……………………………………………………….................. 230
8.2. Memorarea grafurilor orientate……………………………………………....................... 233
8.3. Graf parţial, subgraf…………………………………………………................................. 238
8.4. Parcurgerea grafurilor. Drumuri. Circuite…………………………................................. 239
8.5. Graf complet şi graf turneu…………………………...................................................... 241
8.6. Graf tare conex. Componente tare conexe.................................................................. 243
8.7. Drumuri de cost minim................................................................................................. 246
8.7.1. Introducere..................................................................................................... 246
8.7.2. Algoritmul Roy-Floyd...................................................................................... 247
8.7.3. Utilizarea algoritmul Roy-Floyd pentru determinarea drumurilor de cost maxim.. 251
8.7.4. Algoritmul lui Dijkstra...................................................................................... 252
Probleme propuse………………………………………………………………………………… 256
Răspunsuri………………………………………………………………………………………… 260
6 Cuprins

Capitolul 9. Arbori ………………………………………………………… 261


9.1. Noţiunea de arbore…….……………………………………………………….................. 261
9.2. Noţiunea de arbore parţial…………………………………………………....................... 263
9.3. Mai mult despre cicluri…………………………………………………............................. 264
9.4. Arbori cu rădăcină……………………………………….................................................. 267
9.4.1. Noţiunea de arbore cu rădăcină..................................................................... 267
9.4.2. Memorarea arborilor cu rădăcină prin utilizarea referinţelor descendente..... 268
9.4.3. Memorarea arborilor cu rădăcină prin utilizarea referinţelor ascendente....... 268
9.4.4. înălţimea unui arbore cu rădăcină.................................................................. 270
9.5. Noţiunea de pădure……………………………….......................................................... 272
9.6. Arbori parţiali de cost minim…………………..….......................................................... 275
9.6.1. Algoritmul lui Kruskal...................................................................................... 276
9.6.2. Algoritmul lui Prim.......................................................................................... 280
9.7. Arbori binari…………………..…................................................................................... 285
9.7.1. Noţiunea de arbore binar. Proprietăţi............................................................. 285
9.7.2. Modalităţi de reprezentare a arborilor binari................................................... 287
9.7.3. Modalităţi de parcurgere a arborilor binari..................................................... 287
9.7.4. O aplicaţie a arborilot binari: forma poloneză a expresiilor............................ 291
9.7.5. Arbore binar complet...................................................................................... 297
9.7.6. MinHeap-uri şi MaxHeap-uri. Aplicaţii............................................................ 299
9.7.7. Arbori de căutare............................................................................................ 304
Probleme propuse………………………………………………………………………………… 310
Răspunsuri………………………………………………………………………………………… 315

Anexa 1. Aplicaţii practice ale grafurilor……………………………… 316


7

Capitolul 1

Alocarea dinamică a memoriei

1.1. Generalităţi

După cum ştiţi, fiecărui program i se alocă trei zone distincte în memoria
internă, zone în care se găsesc memorate variabilele programului. În acest capitol,
vom învăţa să alocăm variabile în Heap. De asemenea, vom învăţa să accesăm
conţinuturile variabilelor, atât cele din segmentul de date, cât şi cele din Heap,
pornind de la adresa lor din memorie.

segment de date

segment de stivă

Heap

Figura 1.1. Cele trei zone din memoria internă

Definiţia 1.1. Prin pointer înţelegem adresa unei variabile, iar printr-o
variabilă de tip pointer vom înţelege o variabilă care poate reţine adresele
altor variabile.

Definiţia 1.2. Prin alocarea dinamică a memoriei vom înţelege alocarea


unor variabile în Heap, alocare care se face în timpul executării
programului şi nu de la început, aşa cum am fost obişnuiţi până acum.
Mecanismul alocării dinamice a memoriei necesită utilizarea pointerilor şi
evident, a variabilelor de tip pointer.

Avantajele alocării dinamice sunt:

 Programul va utiliza atâta memorie cât are nevoie. Nu întotdeauna se poate


cunoaşte, de la început, de câtă memorie are nevoie, aceasta se decide în
timpul executării, în funcţie de datele de intrare. În plus, dacă o variabilă nu
mai este necesară, există posibilitatea să eliberăm memoria ocupată de
aceasta. O astfel de abordare conduce la micşorarea substanţială a
necesarului de memorie a unui program.
8 Capitolul 1. Alocarea dinamică a memoriei

 În varianta Borland a limbajelor Pascal şi C++, memoria din segmentul de


date nu este întotdeauna suficientă, ea este limitată la 64K. Apelând la
1
Heap, se măreşte memoria disponibilă.

 Anumite structuri de date, pe care le vom studia în amănunt, se


implementează cu uşurinţă în Heap. Un exemplu în acest sens sunt listele
liniare, dar acestea sunt prezentate în capitolul următor.

1.2. Variabile de tip pointer

1.2.1. Variabile de tip pointer în Pascal

Am învăţat faptul că memoria internă poate fi privită ca o succesiune de


octeţi. Pentru a-i distinge, aceştia sunt numerotaţi.

Definiţia 1.3. Numărul de ordine al unui octet se numeşte adresa lui.

Orice variabilă ocupă un număr de octeţi succesivi. De exemplu, o


variabilă de tip integer ocupă doi octeţi.

Definiţia 1.4. Adresa primului octet al variabilei se numeşte adresa


variabilei.

Observaţie. Nu trebuie confundată adresa unei variabile cu valoarea pe


care aceasta o memorează!

Memorarea adreselor variabilelor se face cu ajutorul variabilelor de tip pointer.

⇒ Variabilele de tip pointer se caracterizează prin faptul că valorile pe care le


pot memora sunt adrese ale altor variabile.

⇒ Ele nu pot fi citite, nu pot fi tipărite în mod direct şi, cu o excepţie, conţinutul
lor nu poate fi modificat în urma unor operaţii aritmetice (de exemplu, nu
putem incrementa o astfel de variabilă).

Limbajul Pascal face distincţie între natura adreselor care pot fi memorate.
Astfel, există adrese ale variabilelor de tip integer (formând un tip identificat prin
sintagma uzuală “pointer către variabile de tip integer”), adrese ale variabilelor
de tip real (”pointer către variabile de tip real”), adrese ale variabilelor de tip
string (”pointer către variabile de tip string”). Din acest motiv, tipul pointer
este variat.

1
În ultimele versiuni ale celor două limbaje, memoria din segmentul de date nu mai este
limitată, ci se poate folosi întreaga memorie disponibilă. Din acest punct de vedere, dacă se
folosesc aceste versiuni, avantajul "memoriei în plus" nu mai poate fi luat în considerare.
Manual de informatică pentru clasa a XI-a 9

⇒ Tipul unei astfel de variabile pointer se declară ca mai jos:

type nume=^tip.

1. Variabile de tip pointer către variabile de tip integer. Variabilele adr1,


adr2, pot reţine adrese ale variabilelor de tip întreg.
type adr_int=^integer;
var adr1,adr2:adr_int;
numar:integer;

2. Variabile de tip pointer către variabile de tip real. Variabila adresa,


poate reţine adrese ale variabilelor de tip real.
type adr_real=^real;
var adresa: adr_real;

3. Variabile de tip pointer către variabile de tip inreg. Tipul inreg este tip
record. Variabila adr_inr, poate reţine adrese ale variabilelor de
tipul inreg.
type inreg=record
nume:string[10];
prenume:string[10];
varsta:byte;
end;
type adr_inreg=^inreg;
var adr_inr:adr_inreg;

Observaţii

 Între variabilele de tip pointer sunt permise atribuiri doar în cazul în care au
acelaşi tip pointer (reţin adrese către acelaşi tip de variabile).
Exemplu:
adr1, adr2:^integer;
adr3: ^real;

Atribuirea adr1:=adr2 este corectă, iar atribuirea adr3:=adr2 nu este


corectă.

 Chiar şi în cazul în care avem tipurile identice, dar descrise diferit, atribuirea
nu este posibilă. Atribuirea din exemplul de mai jos este eronată.
Exemplu:
type adrese=^integer;
var adr1:adrese;
adr2:^integer;
...
adr2:=adr1;
10 Capitolul 1. Alocarea dinamică a memoriei

⇒ Pentru a obţine adresa unei variabile oarecare se foloseşte operatorul “@“


prefixat. Adresa va fi memorată de o variabilă pointer către tipul variabilei a
cărei adresă a fost returnată de operatorul “@“.

⇒ Pornind de la o variabilă de tip pointer care memorează adresa unei


variabile, cu ajutorul operatorului “^“, postfixat, se poate adresa conţinutul
variabilei a cărei adresă este memorată.

1. În programul următor, variabilei x, care este de tip integer, i se


atribuie valoarea 3. Variabilei a, de tip pointer către integer, i se atribuie
adresa lui x.

Pornind de la conţinutul variabilei a, se afişează conţinutul variabilei x:

type adrint=^integer;

var a:adrint;
x:integer;

begin
x:=3;
a:=@x;
writeln(a^);
end.

2. În programul următor, variabilei v, de tip Inreg, i se atribuie o valoare.


Variabilei adresa, de tip pointer către Inreg, i se atribuie adresa lui v.

Pornind de la conţinutul variabilei adresa, se afişează conţinutul variabilei v.

type adrInreg=^inreg;
Inreg=record
nume:string;
varsta:integer;
end;

var v:Inreg;
adresa:adrInreg;

begin
v.nume:='Popescu';
v.varsta:=17;
adresa:=@v;
writeln(adresa^.nume,' ', adresa^.varsta);
end.
Manual de informatică pentru clasa a XI-a 11

1.2.2. Variabile de tip pointer în C++

Am învăţat faptul că memoria internă poate fi privită ca o succesiune de


octeţi. Pentru a-i distinge, aceştia sunt numerotaţi.

Definiţia 1.3. Numărul de ordine al unui octet se numeşte adresa lui.

Orice variabilă ocupă un număr de octeţi succesivi. De exemplu, o


variabilă de tip int ocupă doi octeţi (în varianta Borland C++ 3.0).

Definiţia 1.4. Adresa primului octet al variabilei se numeşte adresa


variabilei.

Observaţii

 Nu trebuie confundată adresa unei variabile cu valoarea pe care aceasta o


memorează!

 Uneori, în loc de adresă a unei variabile vom folosi termenul pointer!

Memorarea adreselor variabilelor se face cu ajutorul variabilelor de tip pointer.

⇒ Variabilele de tip pointer se caracterizează prin faptul că valorile pe care le


pot memora sunt adrese ale altor variabile.

Limbajul C++ face distincţie între natura adreselor care pot fi memorate.
Astfel, există adrese ale variabilelor de tip int, adrese ale variabilelor de tip
float, adrese ale variabilelor de tip char, etc. Din acest motiv şi tipul variabilelor
de tip pointer este diferit.

⇒ Tipul unei variabile de tip pointer se declară ca mai jos:

tip *nume.

1. Variabile de tip pointer către variabile de tip int. Variabilele adr1 şi


adr2 pot reţine adrese ale variabilelor de tip int. Priviţi declaraţia de
mai jos:

int *adr1, *adr2;

2. Variabile de tip pointer către variabile de tip float. Variabila adresa,


poate reţine adrese ale variabilelor de tip float:

float* adresa;
12 Capitolul 1. Alocarea dinamică a memoriei

3. Variabile de tip pointer către variabile de tip elev, care la rândul lor
sunt de tip struct. Variabilele a şi b, pot reţine adrese ale variabilelor de
tipul elev.
struct elev
{ char nume[20], prenume[20];
float nota_mate, nota_info;
int varsta;
};

elev *a,*b;

Observaţii

 Caracterul “*“ poate fi aşezat în mai multe feluri, după cum se observă:
int* adr1; int * adr1; int *adr1;

 Pentru a declara mai multe variabile de acest tip, caracterul “*“ se trece de
fiecare dată:
int *adr1, *adr2, *adr3;

 O declaraţie de genul “int* adr1, adr2;“ are semnificaţia că adr1 este


de tip pointer către int, în vreme ce adr2 este de tip int.
Atenţie! Aici se greşeşte deseori...

⇒ Adresa unei variabile se obţine cu ajutorul operatorului de referenţiere “&“,


care trebuie să preceadă numele variabilei:
&Nume_variabila;

adr1=&numar; - variabilei adr1 i se atribuie adresa variabilei numar.

⇒ Fiind dată o variabilă de tip pointer către variabile de un anume tip, care
memorează o adresă a unei variabile de acel tip, pentru a obţine conţinutul
variabilei a cărei adresă este memorată, se utilizează operatorul unar “*“,
numit şi operator de dereferenţiere.

1. Variabila a este iniţializată cu 7, iar variabila adr este iniţializată cu


adresa lui a. Secvenţa afişează conţinutul variabilei a (7), pornind de la
adresa ei, reţinută de adr:

int a=7, *adr=&a;


cout<<*adr;

2. Variabila a de tip elev este iniţializată, iar variabila adra, de tip pointer
către variabile de tip elev este iniţializată cu adresa variabilei a. Secvenţa
următoare tipăreşte conţinutul variabilei a:
Manual de informatică pentru clasa a XI-a 13

...
struct elev
{
char nume[20], prenume[20];
};

...
elev a, *adra=&a;
strcpy(a.nume,"Bojian");
strcpy(a.prenume, "Andronache");
cout<<(*adra).nume<<" "<<(*adra).prenume<<endl;

Observaţi modul în care am obţinut conţinutul unui câmp al variabilei a,


pornind de la pointerul către a:
(*adra).nume.

De ce este nevoie de paranteze?

Operatorul “.“ - numit operator de selecţie, are prioritatea 1, deci maximă.

Operatorul “*“ - unar, numit şi operator de dereferenţiere, are prioritatea 2,


mai mică. Prin urmare, în absenţa parantezelor rotunde, se încearcă mai întâi
evaluarea expresiei “adra.nume“, expresie care n-are sens! Parantezele schimbă
ordinea de evaluare, se evaluează mai întâi “*adra“, expresie care are sens.

⇒ Pentru o astfel de selecţie, în loc să folosim trei operatori, se poate utiliza


unul singur, operatorul de selecţie indirectă: “->“. Acesta accesează un
câmp al unei structuri pornind de la un pointer (adresă) către acea structură.
El are prioritatea maximă - vezi tabelul operatorilor.
Tipărirea se poate face şi aşa:

cout<<adra->nume<<" "<<adra->prenume;

Între variabile de tip pointer sunt permise atribuiri doar în cazul în care au
acelaşi tip pointer (reţin adrese către acelaşi tip de variabile).
Exemplu:
int *adr1, *adr2;
float *adr3;
... // initializari

Atribuirea “adr1=adr2“ este corectă, iar atribuirea “adr3=adr2“ nu este


corectă.

În aceste condiţii, vă puteţi întreba: cum putem atribui conţinutul unei


variabile de tip pointer către tipul x, altei variabile de tip pointer către tipul y?
În definitiv, amândouă reţin o adresă... În acest caz, se utilizează operatorul
de conversie explicită. De această dată, pentru exemplul anterior, atribuirea:
“adr3=(float*)adr2“ este corectă.
14 Capitolul 1. Alocarea dinamică a memoriei

1.3. Alocarea dinamică a memoriei

1.3.1. Alocarea dinamică în Pascal

Anumite variabile pot fi alocate dinamic. Asta înseamnă că:

 Spaţiul necesar memorării este rezervat într-un segment special destinat


acestui scop, numit HEAP.

 Rezervarea spaţiului se face în timpul executării programului, atunci când se


execută o anumită procedură, scrisă special în acest scop.

 Atunci când variabila respectivă nu mai este utilă, spaţiul din memorie este
eliberat, pentru a fi rezervat, dacă este cazul, pentru alte variabile.

În Borland Pascal, pentru alocarea dinamică se utilizează următoarele


două proceduri:

- Procedura New alocă spaţiu în HEAP pentru o variabilă dinamică. După


1
alocare, adresa variabilei se găseşte în P .
procedure New(var P: Pointer)

- Procedura Dispose eliberează spaţiul rezervat pentru variabila a cărei


adresă este reţinută în P. După eliberare, conţinutul variabilei P este
nedefinit.
procedure Dispose(var P: Pointer)

Mecanismul alocării dinamice este următorul:


• Se declară o variabilă pointer, s-o numim P, care permite memorarea
unei adrese.
• Se alocă variabila dinamică prin procedura New, de parametru P. În
urma alocării, variabila P reţine adresa variabilei alocată dinamic.
• Orice acces la variabila alocată dinamic se face prin intermediul
variabilei P.

Adresa variabilei
Variabila alocată dinamic
alocată dinamic

P segment de date HEAP


Figura 1.2. Accesul la variabila alocată dinamic

1
Dacă nu există spaţiu în HEAP, P reţine nil (nici o valoare). În practică, întotdeauna se face testul
existenţei spaţiului. Din motive didactice, pentru a nu complica programele, în exemplele pe care le vom
da nu vom testa existenţa spaţiului în HEAP.
Manual de informatică pentru clasa a XI-a 15

⇒ Pentru a elibera spaţiul ocupat de o variabilă dinamică, a cărei adresă se


găseşte în P, se utilizează Dispose, de parametru P. După eliberarea
spaţiului rezervat pentru variabila dată, conţinutul variabilei P este nedefinit.

⇒ Fiind dată o variabilă de tip pointer către variabile de un anumit tip, pentru a
accesa conţinutul variabilei a cărei adresă este memorată, se utilizează
numele variabilei de tip pointer urmat de operatorul “^“.

1. Variabile de tip pointer către variabile de tip integer. Variabilele adr1


şi adr2 pot reţine adrese ale variabilelor de tip întreg.
type adr_int=^integer;
var adr1:adr_int;
{sau adr1:^integer prin renuntarea la prima linie}
...
new(adr1) {aloc spatiu in HEAP pentru o variabila de tip
intreg}
adr1^:=7; {variabila retine 7}
writeln(adr^) { tiparesc continutul acestei variabile (7)}
dispose(adr) {eliberez spatiul}

2. Variabile de tip pointer către variabile de tip real. Variabila adresa,


poate reţine adrese ale variabilelor de tip real.
type adr_real=^real;
var adresa: adr_real;
...
new(adresa);
adresa^:=7.65;
writeln(adresa^:4:2);

3. Variabile de tip pointer către variabile de tip inreg, care la rândul lor,
sunt de tip record. Variabila adr_inr, poate reţine adrese ale
variabilelor de tipul inreg.
type inreg=record
nume:string[10];
prenume:string[10];
varsta:byte;
end;
adr_inreg=^inreg;

var adr:adr_inreg;

begin
readln(adr^.nume);
readln(adr^.prenume);
readln(adr^.varsta);
writeln(adr^.nume);
writeln(adr^.prenume);
writeln(adr^.varsta);
...
16 Capitolul 1. Alocarea dinamică a memoriei

4. Programul următor citeşte şi afişează o matrice. Nou este faptul că


matricea este alocată în HEAP.

type matrice=array[1..10,1..10]of integer;


adr_mat=^matrice;
var adr:adr_mat;
m,n,i,j:integer;
begin
write('m=');readln(m);
write('n=');readln(n);
new(adr);
for i:=1 to m do
for j:=1 to n do
readln(adr^[i,j]);
for i:=1 to m do
begin
for j:=1 to n do write(adr^[i,j]:4);
writeln;
end;
end.

5. Este cunoscut faptul că funcţiile nu pot întoarce decât tipuri simple. Prin
urmare, o funcţie nu poate întoarce o matrice care este descrisă de un tip
structurat. Dar tipul pointer este simplu. Aceasta înseamnă că o funcţie
poate întoarce un pointer. În cazul în care avem un pointer către o matrice
(reţinută în HEAP) se poate spune, prin abuz de limbaj, că o funcţie
întoarce o matrice. Programul următor citeşte două matrice şi afişează
suma lor. Matricele sunt rezervate în HEAP.
type matrice=array[1..10,1..10]of integer;
adr_mat=^matrice;
var adr1,adr2,adr3:adr_mat;
m,n:integer;
function Cit_Mat(m,n:integer):adr_mat;
var i,j:integer;
adr:adr_mat;
begin
new(adr);
for i:=1 to m do
for j:=1 to n do readln(adr^[i,j]);
Cit_Mat:=adr;
end;
function Suma_Mat(adr1,adr2:adr_mat):adr_mat;
var i,j:integer;
adr:Adr_mat;
begin
new(adr);
for i:=1 to m do
for j:=1 to n do adr^[i,j]:=adr1^[i,j]+adr2^[i,j];
Suma_Mat:=adr;
end;
Manual de informatică pentru clasa a XI-a 17

procedure Tip_Mat(adr:adr_mat);
var i,j:integer;
begin
for i:=1 to m do
begin
for j:=1 to n do write(adr^[i,j]:4);
writeln;
end
end;
begin
write('m='); readln(m);
write('n='); readln(n);
adr1:=Cit_Mat(m,n); adr2:=Cit_Mat(m,n);
adr3:=Suma_Mat(adr1,adr2);
Tip_Mat(adr3);
end.

1.3.2. Alocarea dinamică în C++


În C++, pentru alocarea dinamică se utilizează următorii operatori:

⇒ Operatorul new alocă spaţiu în HEAP pentru o variabilă dinamică. După


alocare, adresa variabilei se atribuie lui P, unde P este o variabilă de tip
1
pointer către tip :
P=new tip.
Observaţii

 Numărul de octeţi alocaţi în HEAP este, evident, egal cu numărul de octeţi


ocupat de o variabilă de tipul respectiv.

 Durata de viaţă a unei variabile alocate în HEAP este până la eliberarea


spaţiului ocupat (cu delete) sau până la sfârşitul executării programului.

⇒ Operatorul delete eliberează spaţiul rezervat pentru variabila a cărei adresă


este reţinută în P. După eliberare, conţinutul variabilei P este nedefinit.
delete P.

1. Variabile de tip pointer către variabile de tip int. Variabila adr1 poate
reţine adrese ale variabilelor de tip int.
...
int* adr1;
adr1=new int; // aloc spatiu in HEAP pentru o var. de tip int
*adr1=7; //variabila alocata retine 7
cout<<*adr1; // tiparesc continutul variabilei
delete adr1; // eliberez spatiul

1
Dacă nu există spaţiu în HEAP, P reţine 0 (nici o valoare). În practică, întotdeauna se face testul
existenţei spaţiului. Din motive didactice, pentru a nu complica programele, în exemplele pe care le vom
da nu vom testa existenţa spaţiului în HEAP.
18 Capitolul 1. Alocarea dinamică a memoriei

2. Variabile de tip pointer către variabile de tip float. Variabila adresa,


poate reţine adrese ale variabilelor de tip float.
float* adresa; // sau float* adresa=new float;
adresa=new float;
*adresa=7.65;
cout<<*adresa;

3. Variabile de tip pointer către variabile de tip inreg, care la rândul lor,
sunt de tip struct. Variabila adr_inr, poate reţine adrese ale
variabilelor de tipul inreg.
#include <iostream.h>
struct inreg
{ char nume[20], prenume[20];
int varsta;
};
main()
{ inreg* adr;
adr=new inreg;
cin>>adr->nume>>adr->prenume>>adr->varsta;
cout<<adr->nume<<endl<<adr->prenume<<endl<<adr->varsta;
}

Mecanismul alocării dinamice


În continuare prezentăm mecanismul alocării dinamice a masivelor. Pentru
început, vom prezenta pe scurt legătura între pointeri şi masive.

⇒ Un tablou p-dimensional se declară astfel:


tip nume[n1] [n2]...[np]
Exemple
- float A[7][4][2]; - am declarat un tablou cu 3 dimensiuni, unde
tipul de bază este float.
- long b[9][7][8][5]; - am declarat un tablou cu 4 dimensiuni,
unde tipul de bază este long.

⇒ Numele tabloului p dimensional de mai sus este pointer constant către un


tablou p-1 dimensional de forma [n2]...[np], care are componentele de
bază de acelaşi tip cu cele ale tabloului. Exemplele de mai jos se referă la
tablourile anterior prezentate:
- A este pointer constant către tablouri cu [4][2] componente de tip
float;
- b este pointer constant către tablouri cu [7][8][5] componente de tip
long.
⇒ Un pointer către un tablou k dimensional cu [l1][l2]...[lk]
componente de un anumit tip se declară astfel:
tip (*nume)[l1] [l2]... [lk].
Manual de informatică pentru clasa a XI-a 19

Exemple:
- variabila de tip pointer, numită p, care poate reţine pe A, se declară:
float (*p)[4][2];
- variabila de tip pointer, numită q, care poate reţine pe b, se declară:
long (*q)[7][8][5];

De ce se folosesc parantezele rotunde? Operatorul de tip array ([]) are


cea mai mare prioritate (mai mare decât operatorul ”*”). Declaraţia:
float *p[7][8][5];

este interpretată ca masiv cu [7][8][5] componente de tip float*. În


concluzie, este masiv cu componente de tip pointer şi nu pointer către
masive. Atenţie! Aici se fac multe confuzii…

⇒ Întrucât numele unui masiv p dimensional este pointer către un masiv p-1
dimensional, pentru a aloca dinamic un masiv se va utiliza un pointer către
masive p-1 dimensionale (ultimele p-1 dimensiuni).

1. Alocăm în HEAP un vector cu 4 componente de tip int. Numele unui


astfel de vector are tipul int* (pointer către int). Prin urmare, variabila a
are tipul int*. După alocare, ea va conţine adresa primului element al
vectorului din HEAP. Din acest moment, pornind de la pointer (reţinut în a)
vectorul se adresează exact cum suntem obişnuiţi.
int *a =new int[4];
a[3]=2;

Observaţi modul în care a fost trecut tipul în dreapta operatorului new.


Practic, declararea tipului se face întocmai ca declaraţia masivului, însă
numele a fost eliminat.

2. Declarăm în HEAP o matrice (masiv bidimensional) cu 3 linii şi 5


coloane, cu elemente de tip double:
double (*a)[5]=new double [3][5];
a[1][2]=7.8;

⇒ În cazul masivelor, trebuie atenţie la eliberarea memoriei (cu delete).


Trebuie ţinut cont de tipul pointerului, aşa cum rezultă din exemplele
următoare.

1. Fie vectorul alocat în HEAP, int *a =new int[4];. Dacă încercăm


dezalocarea sa prin: delete a;, îi dezalocăm prima componentă, pentru
că pointerul este de tip int*.
Corect, dezalocarea se poate face prin: ”delete [4] a;” - eliberăm
spaţiul pentru toate componentele (în acest caz avem 4). Observaţi că
operatorul delete se poate utiliza şi ca mai sus.
20 Capitolul 1. Alocarea dinamică a memoriei

2. Fie matricea alocată în HEAP:


double (*a)[5]=new double [3][5];

Eliberarea spaţiului ocupat de ea se face prin ”delete [3] a;”.

 Aplicaţia 1.1. Programul următor citeşte şi afişează o matrice. Nou este faptul
că matricea este alocată în HEAP.

#include <iostream.h>
main()
{ int m,n,i,j,(*adr)[10];
adr=new int[10][10];
cout<<"m="; cin>>m;
cout<<"n="; cin>>n;
for(i=0;i<m;i++)
for (j=0;j<n;j++)
cin>>adr[i][j];
for(i=0;i<m;i++)
{ for (j=0;j<n;j++) cout<<adr[i][j]<<" ";
cout<<endl;
}
}

 Aplicaţia 1.2. Este cunoscut faptul că funcţiile nu pot întoarce masive. În


schimb, o funcţie poate întoarce un pointer către orice tip (void*). Unei variabile
de tipul void* i se poate atribui orice tip de pointer, dar atribuirea inversă se
poate face doar prin utilizarea operatorului de conversie explicită.

Programul următor citeşte două matrice şi afişează suma lor. Matricele sunt
alocate în HEAP.
#include <iostream.h>
void* Cit_Mat(int m, int n)
{ int i,j,(*adr1)[10]=new int[10][10];
for(i=0;i<m;i++)
for (j=0;j<n;j++) cin>>adr1[i][j];
return adr1;
}
void Tip_Mat( int m,int n,int(*adr1)[10])
{ int i,j;
for(i=0;i<m;i++)
{ for (j=0;j<n;j++) cout<<adr1[i][j]<<" ";
cout<<endl; }
}
void* Suma_Mat( int m, int n,int(*adr1)[10],int(*adr2)[10])
{ int i,j,(*adr)[10]=new int[10][10];
for (i=0;i<m;i++)
for (j=0;j<n;j++)
adr[i][j]=adr1[i][j]+adr2[i][j];
return adr;
}
Manual de informatică pentru clasa a XI-a 21

main()
{ int m,n,i,j,(*adr)[10],(*adr1)[10],(*adr2)[10];
cout<<"m="; cin>>m;
cout<<"n="; cin>>n;
adr1=(int(*)[10])Cit_Mat(m,n);
adr2=(int(*)[10])Cit_Mat(m,n);
adr=(int(*)[10])Suma_Mat(m,n,adr1,adr2);
Tip_Mat(m,n,adr);
}

Probleme propuse
1. O variabilă de tip pointer către tipul integer/int poate memora:

a) un număr întreg;
b) conţinutul unei variabile de tipul integer/int;
c) adresa unei variabile de tipul integer/int.

2. Fie declaraţiile următoare:

Varianta Pascal Varianta C++


type adrint=^integer; int *a1, *a2;
adrreal=^real; float *a3;
var a1,a2:adrint;
a3:adrreal;

Care dintre atribuirile de mai jos este corectă?


a) a1:=7.35; a) a1=7.35;
b) a3:=7.35; b) a3=7.35;
c) a2:=a3; c) a2=a3;
d) a1:=a2; d) a1=a2;

3. Ce înţelegeţi prin HEAP?

a) un segment din memorie;


b) tipul variabilelor care reţin adrese;
c) o variabilă de sistem în care se pot reţine date de orice tip.
4. Rolul procedurii new / operatorului new este:

a) de a crea o adresă;
b) de a aloca spaţiu pentru o variabilă la o adresă dată;
c) de a aloca spaţiu în HEAP pentru o variabilă de tip pointer;
d) de a aloca spaţiu în HEAP pentru o variabilă de un tip oarecare.
22 Capitolul 1. Alocarea dinamică a memoriei

5. Rolul procedurii dispose / operatorului delete este:

a) de a şterge conţinutul unei variabile;


b) de a şterge conţinutul unei variabile alocată în HEAP.
c) de a elibera spaţiul ocupat de o variabilă alocată în HEAP;
d) de a elibera spaţiul ocupat de o variabilă oarecare;
e) de a şterge variabila a cărei adresă se găseşte memorată în HEAP.

6. Ce se afişează în urma executării programului următor?

Varianta Pascal Varianta C++


type adrreal=^real; #include <iostream.h>
var a1,a2:adrreal; main()
begin { float *a1,*a2;
new(a1); a1^:=3; a1=new float; *a1=3;
new (a2); a2^:=4; a2:=a1; a2=new float; *a2=4; a2=a1;
writeln(a2^:3:0); cout<<*a2;
end. }

a) 4; b) 3; c) Eroare de sintaxă.

7. Ce se afişează în urma executării programului următor, dacă se citesc, în


această ordine, valorile 7 şi 8?

Varianta Pascal Varianta C++


type adrreal=^real; #include <iostream.h>
var a1,a2,man:adrreal; main()
begin { int *a1,*a2,*man;
new (a1); readln(a1^); a1=new int; cin>>*a1;
new (a2); readln(a2^); a2=new int; cin>>*a2;
man:=a2; man=a2;
a2:=a1; a2=a1;
a1:=man; a1=man;
writeln(a1^:1:0,' ',a2^:1:0); cout<<*a1<<" "<<*a2;
end. }

a) 7 7; b) 8 8; c) 7 8; d) 8 7.

8. Se citesc n, număr natural şi n numere reale care se memorează într-un vector


alocat în HEAP. Se cere să se afişeze media aritmetică a numerelor reţinute în vector.

9. Scrieţi un ansamblu de subprograme care implementează operaţiile cu matrice


(adunare, scădere şi înmulţire). Matricele vor fi alocate în HEAP.

Răspunsuri
1. c); 2. d); 3. a); 4. d); 5. c); 6. b); 7. d).
23

Capitolul 2
Liste liniare

2.1. Definiţia listelor

Definiţia 2.1. O listă liniară este o colecţie de n≥0 noduri, X1, X2, ..., Xn
aflate într-o relaţie de ordine. Astfel, X1 este primul nod al listei, X2 este
al doilea nod al listei, ..., Xn este ultimul nod. Operaţiile permise sunt:
- accesul la oricare nod al listei în scopul citirii sau modificării
informaţiei conţinute de acesta;
- adăugarea unui nod, indiferent de poziţia pe care o ocupă în listă;
- ştergerea unui nod, indiferent de poziţia pe care o ocupă în listă;
- schimbarea poziţiei unui nod în cadrul listei.

⇒ Un vector poate fi privit ca o listă liniară. Oricare din operaţiile de mai sus se
poate efectua şi pe un vector. Astfel, relaţia de ordine dintre elementele listei
este cea a componentelor vectorului. Accesul la un nod Xi este imediat
pentru că se accesează componenta i a vectorului. Adăugarea unui nod se
face mai greu, pentru că nodurile care îi urmează în listă trebuie deplasate
către dreapta, pentru a face loc noului nod. Ştergerea unui nod necesită, de
asemenea, efort de calcul, pentru că nodurile care urmează trebuie
deplasate către stânga, pentru a ocupa spaţiul lăsat liber de nodul şters. Tot
aşa, schimbarea poziţiei unui nod necesită efort de calcul. Puteţi arăta,
pentru această situaţie, în ce constă efortul de calcul?

⇒ Dacă o listă este memorată cu ajutorul unui vector, spunem că lista este
alocată secvenţial.

 Exerciţiu. Scrieţi un set de subprograme cu ajutorul cărora se poate lucra


cu uşurinţă cu o listă liniară alocată secvenţial. Pentru fiecare operaţie permisă
asupra listei veţi scrie un subprogram separat.

Din câte observaţi, majoritatea operaţiilor permise asupra unei liste liniare
alocate secvenţial necesită un efort mare de calcul. Din acest motiv, s-a
simţit nevoia unei alte modalităţi de alocare a unei liste liniare, şi anume
1
alocarea înlănţuită .
1
A nu se face confuzie între modul de alocare a unei structuri de date, în cazul de faţă liste
liniare şi structura propriu-zisă. Aceeaşi structură, în exemplu lista, poate fi alocată
secvenţial sau înlănţuit, dar structura, conform definiţiei ei, rămâne aceeaşi.
24 Capitolul 2. Liste liniare

2.2. Liste liniare alocate simplu înlănţuit

2.2.1. Prezentare generală

Definiţia 2.2. O listă liniară simplu înlănţuită este o structură de forma:

in1 adr2 in2 adr3 inn 0


•••
adr1 adr2 adrn

Semnificaţia notaţiilor folosite este următoarea:

 adr1, adr2, adr3, ..., adrn reprezintă adresele celor n înregistrări;

 in1, in2, ..., inn reprezintă informaţiile conţinute de noduri, de altă natură
decât cele de adresă;

 0 - are semnificaţia "nici o adresă" - elementul este ultimul în listă.

După cum observăm, fiecare nod, cu excepţia ultimului, reţine adresa


nodului următor.

1. Accesul la un nod al listei se face prin parcurgerea nodurilor care îl preced.


Aceasta necesită un efort de calcul.
2. Informaţiile de adresă sunt prezente în cadrul fiecărui nod, deci ocupă
memorie.
3. Avantajele alocării înlănţuite sunt date de faptul că operaţiile de adăugare
sau eliminare ale unui nod se fac rapid.

Rămâne de stabilit modul în care implementăm listele liniare alocate înlănţuit.


În linii mari, există două modalităţi: alocarea statică şi alocarea dinamică. Pe scurt,
în alocarea statică, atât datele propriu-zise asociate nodurilor (altele decât cele de
adresă) cât şi datele de adresă sunt memorate cu ajutorul vectorilor, iar aceştia sunt
alocaţi în segmentul de date. În cazul alocării dinamice, cea pe care o prezentăm în
acest capitol, toate datele sunt alocate în Heap 2.

2.2.2. Crearea şi afişarea listelor

În capitolul precedent am studiat alocarea dinamică. Acum o vom aplica.


Întreaga structură (lista) memorată în HEAP este gestionată printr-un singur pointer,
memorat în segmentul de date.

2
Un exemplu de alocare statică a listelor liniare simplu înlănţuite îl veţi întâlni atunci când
veţi studia teoria grafurilor.
Manual de informatică pentru clasa a XI-a 25

 În cazul listelor, prin acel pointer se poate accesa numai primul element al
listei. Apoi, pornind de la acesta se poate accesa al doilea element al listei,
ş.a.m.d.

 Ultimul element al listei va avea memorat în câmpul de adresă o valoare cu


semnificaţia de nici o adresă. Cu ajutorul acestei valori programele vor detecta
sfârşitul listei. În Pascal, această valoare este nil, iar în C++ ea este 0.

Crearea listelor

Iniţial, o variabilă v reţine nil / 0.

Presupunem că, la un moment dat, lista este cea de mai jos, iar v reţine
adresa primului element (adr1):

3 adr2 7 adr3 9 0

v
adr1 adr2 adrn

Dacă se citeşte un nou număr (de exemplu 4), atunci acesta se adaugă
într-o înregistrare aflată la începutul listei, în următoarele etape:

a) Se alocă spaţiu pentru noua înregistrare, se completează câmpul numeric, iar


adresa următoare este cea din v, deci a primului element al listei.

3 adr2 7 adr3 9 0

v
adr1 adr2 adrn
4 adr1

adrn+1

b) Variabila v va memora adresa noii înregistrări:

3 adr2 7 adr3 9 0

adr1 adr2 adrn


v
4 adr1

adrn+1

Programul este prezentat în continuare:


26 Capitolul 2. Liste liniare

Varianta Pascal Varianta C++


type Adresa=^Nod; #include <iostream.h>
Nod=record
struct Nod
info:integer;
{ int info;
adr_urm:Adresa;
Nod* adr_urm;
end;
};
var v:adresa;
nr:integer; Nod* v;
int nr;
procedure Adaug(var v:Adresa;
nr:Integer); void Adaug(Nod*& v, int nr)
var c:Adresa; { Nod* c=new Nod;
begin c->info=nr;
new(c); c->adr_urm=v;
c^.info:=nr; v=c;
c^.adr_urm:=v; }
v:=c;
void Tip(Nod* v)
end;
{ Nod* c=v;
procedure Tip(v:Adresa); while (c)
var c:Adresa; { cout<<c->info<<endl;
begin c=c->adr_urm;
c:=v; }
while c<>nil do }
begin
writeln(c^.info); main()
c:=c^.adr_urm { cout<<"numar=";cin>>nr;
end; while (nr)
end; { Adaug(v,nr);
cout<<"numar=";cin>>nr;
begin };
write('numar='); readln(nr); Tip(v);
while nr<>0 do }
begin
Adaug(v,nr);
write('numar='); readln(nr);
end;
Tip(v);
end.

În Pascal, nu este permis, ca în C++, să definim un tip care conţine declarări


ce se referă la el. Spre exemplu, declararea de mai jos
Nod = record
info:integer;
adr_urm = ^Nod;
end;

este greşită. Câmpul adr_urm face parte din tipul Nod şi este definit ca
pointer către acelaşi tip (Nod). Convenţia de limbaj este să se declare pe
rând cele două tipuri, ca în program.

Procedând după algoritm, lista va conţine informaţiile în ordinea inversă în


care au fost introduse. Acest fapt nu prezintă importanţă pentru majoritatea
aplicaţiilor.
Manual de informatică pentru clasa a XI-a 27

Un alt algoritm de creare a listei, recursiv, este prezentat mai jos. De


această dată lista cuprinde informaţiile în ordinea în care acestea au fost introduse:

Varianta Pascal Varianta C++


type Adresa=^Nod; #include <iostream.h>
Nod=record
struct Nod
info:integer;
{ int info;
adr_urm:Adresa;
end; Nod* adr_urm; };
var v:adresa; Nod* v;
function Adaug:Adresa; Nod* Adaug()
var c:adresa; { Nod* c;
nr:integer; int nr;
begin cout<<"numar "; cin>>nr;
write ('nr='); readln(nr); if (nr)
if nr<>0 then { c=new(Nod);
begin c->adr_urm=Adaug();
new(c); c->info=nr;
adaug:=c; return c;
adaug^.info:=nr; }
adaug^.adr_urm:=adaug else return 0;
end }
else adaug:=nil;
end; void Tip(Nod* v)
procedure Tip(v:Adresa); { Nod*c=v;
var c:Adresa; while (c)
begin { cout<<c->info<<endl;
c:=v; c=c->adr_urm;
while c<>nil do }
begin }
writeln(c^.info); main()
c:=c^.adr_urm { v=Adaug();
end; Tip(v);
end; }
begin
v:=Adaug;
Tip(v);
end.

Mai jos este prezentat un subprogram recursiv care tipăreşte informaţiile în


ordine inversă faţă de modul în care se găsesc în listă:

Varianta Pascal Varianta C++


procedure Tip_inv(v:adresa); void Tip_inv(Nod* v)
begin { if (v)
if v<>nil then { Tip_inv (v->adr_urm);
begin cout<<v->info<<endl;
Tip_inv(v^.adr_urm); }
writeln(v^.info); }
end;
end;
28 Capitolul 2. Liste liniare

 Problema 2.1. Fiind dată o listă liniară simplu înlănţuită, cu adresa de început v,
se cere să se inverseze legăturile din listă, adică dacă în lista iniţială, după nodul i
urmează nodul i+1, atunci, în noua listă, după nodul i+1, urmează nodul i.

 Rezolvare. Funcţia inv, rezolvă problema dată. Ea are doi parametri: adresa
primului element al listei (pred) si adresa următorului element din listă (curent).
Practic, la fiecare pas, partea de adresă a nodului referit de curent va reţine adresa
referită de pred (adică adresa nodului precedent). Funcţia returnează adresa primului
nod al liste inversate (adică a ultimului nod în cazul listei neinversate). Înainte de
apelul funcţiei, partea de adresă a primului nod listei va trebui să reţină nil/0.

Varianta Pascal Varianta C++


function inv(pred, curent: Nod* inv(Nod* pred,
adresa):adresa; Nod* curent)
var urm:adresa; { Nod* urm;
begin while (curent)
while curent<>nil do { urm=curent->adr_urm;
begin curent->adr_urm=pred;
urm:=curent^.adr_urm; pred=curent;
curent^.adr_urm:=pred; curent=urm;
pred:=curent; }
curent:=urm; return pred;
end; }
inv:=pred;
....
end;
Nod* curent=v->adr_urm;
...
v->adr_urm=0;
curent:=v^.adr_urm; v=inv(v,curent);
v^.adr_urm:=nil; Tip(v);
v:=inv(v,curent);
Tip(v);

 Exerciţiu. Modificaţi subprogramul astfel încât acesta să aibă un singur


parametru, adresa de început a listei.

2.2.3. Operaţii asupra unei liste liniare

În acest paragraf prezentăm principalele operaţii care se pot efectua cu o


listă liniară simplu înlănţuită.

În prezentarea operaţiilor, vom folosi de multe ori desene. Reţineţi: pentru


orice operaţie aveţi de efectuat, faceţi un mic desen. Vă ajută mult...

Orice listă va fi reţinută prin două informaţii de adresă: a primului nod (v) şi a
ultimului nod (sf). Precizăm faptul că, în general, numai prima informaţie este
indispensabilă. Pentru simplitate şi pentru rapiditatea executării vom reţine şi
adresa ultimului nod. Structura unui nod al listei este:
Manual de informatică pentru clasa a XI-a 29

Varianta Pascal Varianta C++


type Adresa=^Nod; struct Nod
Nod=record { int info;
info:integer; Nod* adr_urm;
adr_urm:Adresa; };
end;

A. Adăugarea unui nod

Fiind dată o listă liniară, se cere să se adauge la sfârşitul ei un nod, cu o


anumită informaţie, în exemplele noastre, un număr întreg. Se disting două cazuri:

a) lista este vidă - v reţine 0.

Să presupunem că vrem să adăugăm un nod cu informaţia 3. Se alocă în HEAP


nodul respectiv, adresa sa va fi în v, şi cum lista are un singur nod, adresa primului
nod este şi adresa ultimului, deci conţinutul lui v va coincide cu acela al lui sf.

3 0

v sf

b) lista este nevidă

Fie lista:

3 adr2 7 adr3 9 0

v sf

Se adaugă un nod cu informaţia 6. Iniţial se alocă spaţiu pentru nod.

3 7 9 0 6 0

v sf c

Câmpul de adresă al ultimului nod, cel care are adresa în sf, va reţine adresa
nodului nou creat, după care şi sf va reţine aceeaşi valoare.

3 7 9 6 0

v sf
30 Capitolul 2. Liste liniare

Dacă n-am fi utilizat variabila sf pentru a reţine adresa ultimului nod, ar fi


fost necesar să parcurgem întreaga listă, pornind de la v, pentru a obţine
adresa ultimului.

Varianta Pascal Varianta C++


procedure Adaugare(var v, void Adaugare(Nod*& v,
sf:Adresa;val:integer); Nod*& sf, int val)
var c:Adresa; { Nod* c;
begin if (v==0)
if v=nil then begin { v=new(Nod);
new(v); v->info=val;
v^.info:=val; v->adr_urm=0;
v^.adr_urm:=nil; sf=v;
sf:=v; }
end else
else begin { c=new(Nod);
New(c); sf->adr_urm=c;
sf^.adr_urm:=c; c->info=val;
c^.info:=val; c->adr_urm=0;
c^.adr_urm:=nil; sf=c;
sf:=c;
}
end
}
end;

B. Inserarea unui nod, după un altul, de informaţie dată

Fie lista din figura anterioară. Dorim să adăugăm după nodul cu informaţia 3,
un altul, cu informaţia 5.

Iniţial, se identifică nodul după care se face adăugarea. În cazul de faţă


acesta este primul. Se alocă spaţiu pentru noul nod. Se completează adresa şi
anume adresa nodului care urmează după cel de informaţie 3.

3 7 9 6 0

v sf

Apoi, câmpul de adresă al nodului cu informaţia 3 va reţine adresa nodului


nou creat:

3 7 6 0
9

v sf

5
Manual de informatică pentru clasa a XI-a 31

Un caz aparte apare atunci când nodul de informaţie val este ultimul în
listă. În acest caz sf va reţine adresa nodului nou creat pentru că acesta va
fi ultimul.

Varianta Pascal Varianta C++


procedure Inserare_dupa( void Inserare_dupa(Nod* v,
v:adresa;var sf:adresa; Nod*& sf, int val, int val1)
val,val1:integer); { Nod* c=v, *d;
while (c->info!=val)
var c,d:adresa;
c=c->adr_urm;
begin d=new Nod;
c:=v; d->info=val1;
while c^.info<>val do d->adr_urm=c->adr_urm;
c:=c^.adr_urm; c->adr_urm=d;
new(d); if (d->adr_urm==0) sf=d;
d^.info:=val1; }
d^.adr_urm:=c^.adr_urm;
c^.adr_urm:=d;
if d^.adr_urm=nil then sf:=d;
end;

C. Inserarea unui nod, înaintea altuia, de informaţie dată

Întrucât operaţia este asemănătoare cu precedenta, prezentăm numai


subprogramul care realizează operaţia respectivă:

Varianta Pascal Varianta C++


procedure Inserare_inainte(var void Inserare_inainte(Nod*& v,
v:adresa;val,val1:integer); int val, int val1)
var c,d:adresa; { Nod* c,*d;
begin if (v->info==val)
if v^.info=val { d=new Nod;
then d->info=val1;
begin d->adr_urm=v;
new(d); v=d;
d^.info:=val1; }
d^.adr_urm:=v; else
v:=d; { c=v;
end while (c->adr_urm->
else info!=val) c=c->adr_urm;
begin d=new Nod;
c:=v; d->info=val1;
while c^.adr_urm^.info<> d->adr_urm=c->adr_urm;
val do c:=c^.adr_urm; c->adr_urm=d;
new(d); }
d^.info:=val1; }
d^.adr_urm:=c^.adr_urm;
c^.adr_urm:=d;
end
end;
32 Capitolul 2. Liste liniare

D. Ştergerea unui nod de informaţie dată

Algoritmul este diferit în funcţie de poziţia în listă a nodului care va fi şters -


dacă este primul sau nu.

a) Nodul nu este primul. Pentru nodul care va fi şters, informaţia de adresă a


predecesorului va reţine adresa nodului succesor:

3 5 7 9 0

Memoria ocupată de nodul care urmează a fi şters este eliberată:

3 7 9 0

b) Nodul este primul. Fie lista:

v 3 5 7 9 0

Variabila v va reţine adresa celui de-al doilea nod:

v 3 5 7 9 0

Spaţiul ocupat de primul nod va fi eliberat:

v 5 7 9 0

Programul este următorul:

Varianta Pascal Varianta C++


procedure Sterg(var v, void Sterg(Nod*& v, Nod*& sf,
sf:adresa;val:integer); int val)
var c,man:adresa; { Nod* c, *man;
begin if (v->info==val)
if v^.info=val then { man=v;
begin v=v->adr_urm;
man:=v; v:=v^.adr_urm; }
end
Manual de informatică pentru clasa a XI-a 33

else else
begin { c=v;
c:=v; while (c->adr_urm->info
while c^.adr_urm^.info<>val !=val) c=c->adr_urm;
do c:=c^.adr_urm; man=c->adr_urm;
man:=c^.adr_urm; c->adr_urm=man->adr_urm;
c^.adr_urm:=man^.adr_urm; if (man==sf) sf=c;
if man=sf then sf:=c; }
end; delete man;
dispose(man); }
end;

Pentru a verifica modul de funcţionare a subprogramelor de mai sus, este


necesar să utilizăm un altul, care afişează lista liniară:

Varianta Pascal Varianta C++


procedure listare(v:Adresa); void Listare(Nod* v)
var c:Adresa; { Nod* c=v;
begin while (c)
c:=v; {
while c<>nil do cout<<c->info<<endl;
begin c=c->adr_urm;
write(c^.info,' '); }
c:=c^.adr_urm; cout<<endl;
end; }
writeln;
end;

Acum putem testa aplicaţia. Utilizaţi secvenţa următoare:

Varianta Pascal Varianta C++


var v,sf:Adresa; Nod* v,*sf;
i:integer; int i;
begin main()
for i:=1 to 10 do { for (i=1;i<=10;i++)
Adaugare(v,sf,i); Adaugare(v,sf,i);
listare(v); Listare(v);
inserare_dupa(v,sf,7,11); Inserare_dupa(v,sf,7,11);
inserare_dupa(v,sf,10,12); Inserare_dupa(v,sf,10,12);
inserare_dupa(v,sf,1,13); Inserare_dupa(v,sf,1,13);
listare(v); Listare(v);
inserare_inainte(v,13,14); Inserare_inainte(v,13,14);
inserare_inainte(v,1,15); Inserare_inainte(v,1,15);
listare(v); Listare(v);
sterg(v,sf,15); Sterg(v,sf,15);
sterg(v,sf,13); Sterg(v,sf,13);
sterg(v,sf,12); Sterg(v,sf,12);
listare(v); Listare(v);
end. }
34 Capitolul 2. Liste liniare

2.2.4. Aplicaţii ale listelor liniare

2.2.4.1. Sortarea prin inserţie

Se citesc de la tastatură n numere naturale. Se cere ca acestea să fie


sortate crescător prin utilizarea metodei de sortare prin inserţie.

Această metodă de sortare a fost studiată în clasa a IX-a. Ideea de bază a


metodei constă în a considera primele k valori sortate, urmând să inserăm
valoarea k+1 în şirul deja sortat. Prin utilizarea listelor liniare înlănţuite, inserţia
este mai simplă, întrucât nu necesită deplasarea componentelor, ca în cazul
vectorilor.

Pentru simplificarea algoritmului, lista va conţine valoarea maximă MaxInt


alocată deja în listă. În acest fel, algoritmul se simplifică pentru că se porneşte deja
de la o listă liniară nevidă. Evident, valoarea MaxInt nu va fi listată, atunci când se
tipăresc numerele sortate.

Problema se reduce la inserţia unui număr într-o listă deja sortată. Mai întâi
se alocă spaţiu în HEAP pentru o valoare, apoi aceasta este citită. Se disting două
cazuri:

1. Valoarea citită este mai mică decât prima valoare a listei. Aceasta înseamnă
că ea este cea mai mică din listă şi va fi introdusă prima în listă.

Fie lista următoare şi se citeşte 2:

v 3 7 9 MaxInt

Noua înregistrare va conţine adresa nodului 3, iar v conţine adresa noii înregistrări:

v 3 7 9 MaxInt

2. Valoarea citită nu este cea mai mică din listă. În mod sigur, nu este cea mai
mare, pentru că am introdus MaxInt în listă. Dacă nu a fost îndeplinită
condiţia de la cazul 1, înseamnă că valoarea nu este nici cea mai mică.
Aceasta înseamnă că ea va trebui introdusă în interiorul listei.
Manual de informatică pentru clasa a XI-a 35

Va trebui să identificăm prima valoare mai mare decât valoarea citită.


Întrucât, odată găsită adresa acestei valori, avem nevoie de adresa precedentă
(pentru a putea lega în listă noul nod) vom "merge" cu doi pointeri, c şi c1, unde
c1 reţine adresa înregistrării cu valoare mai mare decât înregistrarea citită, iar c
adresa înregistrării precedente.

Fie lista următoare şi se citeşte 8. Se identifică prima înregistrare care


reţine o valoare mai mare decât cea citită (în exemplu, 9).

v 3 7 9 MaxInt

8 c c1

Noua valoare se inserează în listă.

v 3 7 9 MaxInt

Varianta Pascal Varianta C++


type Adresa=^Nod; #include <iostream.h>
Nod=record
const MaxInt=32000;
info:integer;
adr_urm:Adresa; struct Nod
end; { int info;
Nod* adr_urm;
var n,i:integer; };
var v,adr,c,c1:Adresa;
int n,i;
begin
Nod *v,*adr,*c,*c1;
write('n=');
readln(n); main()
new(v); { cout<<"n=";
v^.info:=MaxInt; cin>>n;
v^.adr_urm:=nil; v=new Nod;
for i:=1 to n do v->info=MaxInt;
begin v->adr_urm=0;
new(adr); for (i=1;i<=n;i++)
write('numar='); { adr=new Nod;
readln(adr^.info); cout<<"numar=";
if adr^.info<=v^.info cin>>adr->info;
then {primul din lista} if (adr->info<=v->info)
begin // primul din lista
adr^.adr_urm:=v; { adr->adr_urm=v;
v:=adr; v=adr;
end }
36 Capitolul 2. Liste liniare

else{nu e primul din lista} else


begin // nu e primul din lista
c:=v; { c=v;
c1:=v^.adr_urm; c1=v->adr_urm;
while c1^.info<adr^.info while (c1->info<adr->info)
do { c=c->adr_urm;
begin c1=c1->adr_urm;
c:=c^.adr_urm; }
c1:=c1^.adr_urm; c->adr_urm=adr;
end; adr->adr_urm=c1;
c^.adr_urm:=adr; }
adr^.adr_urm:=c1; }
end; //tiparesc
end; c=v;
{tiparesc} while (c->info!=MaxInt)
c:=v; { cout<<c->info<<endl;
while c^.info<>MaxInt do c=c->adr_urm;
begin }
writeln(c^.info); }
c:=c^.adr_urm
end
end.

Procedeul generării unei valori mai mari sau mai mici decât toate cele
posibile este deseori folosit în programare. Realizaţi cât de mult a simplificat
algoritmul?

La fiecare adăugare în listă se face o parcurgere a acesteia, deci algoritmul


are complexitatea maximă O(n2).

2.2.4.2. Sortarea topologică

Presupunem că dorim sortarea numerelor 1, 2, ..., n, numere care se găsesc


într-o ordine oarecare, alta decât cea naturală. Pentru a afla relaţia în care se
găsesc numerele, introducem un număr finit de perechi (i,j). O astfel de pereche
ne exprimă faptul că, în relaţia de ordine considerată, i se află înaintea lui j.

Exemplul 1. Fie n=3 şi citim perechile (3,1) şi (3,2). Numărul 3 se află


înaintea lui 1 şi 3 se află înaintea lui 2. Apar două soluţii posibile: 3,1,2
şi 3,2,1, întrucât nu avem nici o informaţie asupra relaţiei dintre 1 şi 2.

De aici tragem concluzia că o astfel de problemă poate avea mai multe soluţii.

Exemplul 2. Fie n=3 şi citim (1,2), (2,3), (3,1). În acest caz nu


avem soluţie. Din primele două relaţii rezultă că ordinea ar fi 1,2,3, iar
relaţia a-3-a contrazice această ordine.

În concluzie, problema poate avea sau nu soluţie, iar dacă are, poate fi
unică sau nu.
Manual de informatică pentru clasa a XI-a 37

Algoritmul pe care îl prezentăm în continuare furnizează o singură soluţie


atunci când problema admite soluţii. În caz contrar, specifică faptul că problema nu
admite soluţie.

Vom exemplifica funcţionarea algoritmului pentru n=4 şi citind perechile


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

Pentru fiecare număr între 1 şi n trebuie să avem următoarele informaţii:


- numărul predecesorilor;
- lista succesorilor.

Pentru aceasta folosim doi vectori:


- contor, vector care reţine numărul predecesorilor fiecărui k, k∈{1..n};
- a, care reţine adresele de început ale listelor de succesori ai fiecărui element.

Pentru fiecare element există o listă simplu înlănţuită a succesorilor săi.

Iniţial, în dreptul fiecărui element din vectorii contor şi a se trece 0.

Citirea unei perechi (i,j) înseamnă efectuarea următoarelor operaţii:


• incrementarea variabilei contor(j) (j are un predecesor, şi anume i);

• adăugarea lui j la lista succesorilor lui i.

Pentru exemplul dat, se procedează în felul următor:

contor 0 0 0 0 Valorile iniţiale ale celor


doi vectori.
a 0 0 0 0

contor 0 0 0 1 am citit (3,4);


al3 - adresa listei 3;
a 0 0 al3 0

contor 0 1 0 1 am citit (4,2);

a 0 0 al3 al4

4 2
38 Capitolul 2. Liste liniare

contor 1 1 0 1 am citit (4,1);


a 0 0 al3 al4

4 2

contor 1 2 0 1 am citit (1,2);


a al1 0 al3 al4

2 4 2

contor 2 2 0 1 am citit (3,1);


a al1 0 al3 al4

2 4 2

1 1

În continuare se procedează astfel:

⇒ toate elementele care au 0 în câmpul contor se reţin într-un vector c;

⇒ pentru fiecare element al vectorului c se procedează astfel:


• se tipăreşte;
• se marchează cu -1 câmpul său de contor;
• pentru toţi succesorii săi (aflaţi în lista succesorilor) se scade 1 din câmpul
contor (este normal, întrucât aceştia au un predecesor mai puţin);

⇒ se reia algoritmul dacă nu este îndeplinită una din condiţiile următoare:


a) au fost tipărite toate elementele, caz în care algoritmul se încheie cu succes;
b) nu avem nici un element cu 0 în câmpul contor, caz în care relaţiile au fost
incoerente.
Manual de informatică pentru clasa a XI-a 39

Astfel:

contor 1 2 -1 0 Tipăresc 3, scad 1 din


predecesorii lui 4 şi 1,
a al1 0 al3 al4 marchez cu -1 contorul
lui 3.

2 4 2

1 1

contor 0 1 -1 -1 Tipăresc 4, scad 1 din


predecesorii lui 2 şi 1,
a al1 0 al3 al4 marchez cu -1 contorul
lui 4.

2 4 2

1 1

contor -1 0 -1 -1 Tipăresc 1, scad 1 din


predecesorii lui 2,
a al1 0 al3 al4 marchez cu -1 contorul
lui 1.

2 4 2

1 1

Algoritmul are multe aplicaţii, ca de exemplu:

- ordonarea unor activităţi, atunci când ele sunt condiţionate una de alta;

- ordonarea unor termeni care se cer explicaţi, pentru a-i putea explica prin
alţii deja prezentaţi.

Algoritmul are complexitatea O(n2). Odată sunt n extrageri şi la fiecare


extragere se parcurge lista succesorilor unui nod.
40 Capitolul 2. Liste liniare

Varianta Pascal Varianta C++


type ref=^inr; #include <iostream.h>
inr=record struct Nod
succ:integer; { int succ;
urm:ref Nod* urm;
end; };
vector=array [1..100] of int n,m,i,j,k,gasit;
integer; int contor[100],c[100];
vectad=array [1..100] of Nod* a[100];
ref; void adaug(int i,int j)
{ Nod *c, *d;
var n,m,i,j,k:integer;
contor[j]++;
contor,c:vector; c=a[i]; d=new Nod;
a:vectad; d->urm=0; d->succ=j;
gasit:boolean; if (c==0) a[i]=d;
else
procedure adaug(i,j:integer); { while (c->urm) c=c->urm;
var c,d:ref; c->urm=d; }
begin }
contor[j]:=contor[j]+1;
c:=a[i]; void actual(int i)
new(d); { Nod* c=a[i];
d^.urm:=nil; while (c)
d^.succ:=j; { contor[c->succ]--;
if c=nil c=c->urm; }
then a[i]:=d }
else
begin main()
while c^.urm<>nil do { cout<<"n="; cin>>n;
c:=c^.urm; for (i=1;i<=n;i++)
c^.urm:=d { contor[i]=0;
end a[i]=0;
end; }
while (i)
procedure actual(i:integer); { cout<<"Tastati i, j=";
var c:ref; cin>>i>>j;
begin if (i) adaug(i,j);
c:=a[i]; }
while c<>nil do m=n;
do
begin
{ k=1;
contor[c^.succ]:=
gasit=0;
contor[c^.succ]-1;
for (i=1;i<=n;i++)
c:=c^.urm if (contor[i]==0)
end { gasit=1;
end; m--;
c[k]=i;
begin k++;
write('n='); contor[i]=-1;
readln(n); }
for i:=1 to n do for (i=1;i<=k-1;i++)
begin { actual(c[i]);
contor[i]:=0; cout<<c[i]<<endl;
a[i]:=nil }
end; }
Manual de informatică pentru clasa a XI-a 41

while i<>0 do while (gasit && m);


begin if (m) cout<<"relatii
write('Tastati i,j='); contradictorii";
readln(i,j); else cout<<"Totul e OK";
if i<>0 then adaug(i,j) }
end;
m:=n;
repeat
k:=1;
gasit:=false;
for i:=1 to n do
if contor[i]=0
then
begin
gasit:=true;
m:=m-1;
c[k]:=i;
k:=k+1;
contor[i]:=-1
end;
for i:=1 to k-1 do
begin
actual(c[i]);
writeln(c[i]);
end;
until (not gasit) or (m=0);
if m=0
then writeln('totul e ok')
else writeln('relatii
contradictorii')
end.

2.2.4.3. Operaţii cu polinoame

În acest paragraf vom prezenta modul în care se pot programa operaţii precum
adunarea, scăderea, înmulţirea şi împărţirea polinoamelor cu coeficienţi reali.

Să observăm că, în acest caz, utilizarea listelor liniare simplu înlănţuite este
necesară şi, exemplul în sine, constituie un argument pentru utilizarea acestora.

De ce?

Să presupunem că un polinom va fi memorat cu ajutorul unui vector. Fiecare


coeficient va fi memorat de o componentă a vectorului. De exemplu, polinomul:
10×X3+6×X2+3×X+2, poate fi memorat cu ajutorul vectorului (10,6,3,2). Dar dacă
avem polinomul 3×X1456+1, cum procedăm? Este necesar să avem un vector cu
1457 de componente, dintre care numai prima şi ultima sunt diferite de 0. Este
eficient să folosim vectori? Evident, nu.

Atunci?
42 Capitolul 2. Liste liniare

 Pentru memorarea unui polinom (prin coeficienţii lui) vom utiliza o listă liniară
simplu înlănţuită. Fiecare nod al listei va reţine, în această ordine,
coeficientul şi gradul unui monom. Pentru simplificarea operaţiilor cu
polinoame, un polinom va fi reţinut în ordinea descrescătoare a gradelor. De
exemplu, pentru polinomul 3×X1456+ X100+2, vom avea:

3 1456 1 100 0 2

Mai jos, prezentăm structura unui nod al listei:

Varianta Pascal Varianta C++


type struct Nod
adrNod=^Nod; { float coef;
Nod=record int grad;
coef:real; Nod* adr_urm;
grad:integer; };
adr_urm:adrNod;
end;

1. Pentru a adăuga un nod listei liniare simplu înlănţuite, atunci când aceasta se
creează, vom utiliza subprogramul următor, adaug. Parametrii de intrare sunt
adresele de început şi de sfârşit ale listei. Desigur, se putea evita parametrul prin
care se transmite adresa de sfârşit a listei, dar, prin transmiterea acestui
parametru, adăugarea unui nod la sfârşitul listei se poate face cu mult mai repede,
pentru că nu mai este necesară parcurgerea întregii liste.

Varianta Pascal Varianta C++


procedure adaug(var v, void adaug(Nod*& v,
sf:adrNod; gr:integer;cf:real); Nod*& sf,int gr, float cf)
var c:adrNod; { Nod* c;
begin c=new Nod;
new(c); c->grad=gr;
c^.grad:=gr; c->coef=cf;
c^.coef:=cf; c->adr_urm=0;
c^.adr_urm:=nil; if (v==0) v=sf=c;
if v=nil else
then { sf->adr_urm=c;
begin sf=c;
v:=c; }
sf:=c; }
end
else
begin
sf^.adr_urm:=c;
sf:=c;
end
end;
Manual de informatică pentru clasa a XI-a 43

2. Pentru a crea lista asociată unui polinom, vom utiliza funcţia următoare, care
după ce creează lista, returnează adresa ei de început. Pentru a putea introduce
datele, se cere, de la început, numărul de "termeni" (monoame) ai polinomului.
Este foarte important ca monoamele să fie introduse în ordinea descrescătoare a
gradelor, pentru că funcţia nu realizează ordonarea acestora. Pentru a crea lista se
utilizează subprogramul prezentat anterior, adaug. Exerciţiu. Modificaţi funcţia
astfel încât datele de intrare să se găsească într-un fişier text. De asemenea, se
cere ca funcţia să sorteze monoamele în ordinea descrescătoare a gradului.

Varianta Pascal Varianta C++


function crePolinom(nr_termeni: Nod* crePolinom(int nr_termeni)
integer):adrNod; {
var gradul,i:integer; cout<<"Date polinom "<<endl;
coeficient:real; Nod* vf=0, *sf;
vf,sf:adrNod; int gradul;
begin float coeficient;
vf:=nil; for (int i=1;i<=nr_termeni;i++)
for i:=1 to nr_termeni do { cout<<"Grad=";
begin cin>>gradul;
write ('Grad='); cout<<"coeficientul=";
readln(gradul); cin>>coeficient;
write ('coeficientul='); adaug(vf,sf,gradul,
readln(coeficient); coeficient);
adaug(vf,sf,gradul, }
coeficient); return vf;
end; }
crePolinom:=vf;
end;

3. Uneori, în operaţiile care vor fi prezentate, intervin anumite polinoame, de care


este nevoie numai la un moment dat, după care devin inutile. Fiind polinoame, sunt
memorate tot sub forma unor liste liniare simplu înlănţuite. Care este problema?
După utilizarea lor, aceste liste trebuie şterse, pentru că ocupă în mod inutil
memoria. Pentru a şterge o listă, utilizăm subprogramul sterg. Să observăm că
ştergerea listei se face, prin parcurgerea ei, nod cu nod. Imediat ce un nod a fost
parcurs, el este şters.

Varianta Pascal Varianta C++


procedure sterg(v:adrNod); void sterg(Nod* v)
var c:adrNod; { Nod * c=v;
begin while (v)
c:=v; { v=v->adr_urm;
while v<>nil do delete c;
begin c=v;
v:=v^.adr_urm; }
dispose(c); }
c:=v;
end
end;
44 Capitolul 2. Liste liniare

4. După ce efectuăm o operaţie cu polinoame, este necesar ca rezultatul să fie


afişat. Pentru afişarea unui polinom vom utiliza subprogramul următor:

Varianta Pascal Varianta C++


procedure afis(v:adrNod); void afis(Nod* v)
var i:integer; { int i=0;
begin while (v)
i:=0; { if (i) cout<<"+";
while v<>nil do cout<<v->coef<<"x**"
begin <<v->grad;
if i<>0 v=v->adr_urm;
then write('+'); i++;
write(v^.coef:3:2,'x**', }
v^.grad); cout<<endl<<endl;
v:=v^.adr_urm; }
i:=i+1;
end;
writeln;
writeln;
end;

5. Adunarea polinoamelor. Pentru a aduna două polinoame se utilizează funcţia


adun. Cele două polinoame care se adună sunt sub forma unor liste liniare simplu
înlănţuite. Nu uitaţi, nodurile listelor conţin monoame, iar pentru fiecare monom se
cunoaşte gradul şi coeficientul său. Monoamele sunt aranjate în ordinea
descrescătoare a gradului. Funcţia returnează adresa de început a polinomului
sumă. Să presupunem că avem de adunat polinoamele:

X3+6×X2+3×X şi 2×X2-3×X+1.

Listele asociate celor două polinoame sunt prezentate mai jos:

1 3 6 2 3 1

c1

2 2 -3 1 1 0

c2

Algoritmul de adunare a polinoamelor seamănă foarte mult cu algoritmul de


interclasare a două şiruri ordonate. Avem două variabile de tip pointer, c1 şi c2.
Variabila c1 reţine adresa de început a primei liste, iar c2 adresa de început a
celeilalte liste.
Manual de informatică pentru clasa a XI-a 45

Avem mai multe situaţii:

a) dacă gradul monomului indicat de c1 este egal cu gradul monomului indicat


de c2 şi dacă suma coeficienţilor celor două monoame este diferită de 0, atunci se
adaugă la lista polinomului rezultat monomul care are gradul comun şi care are
drept coeficient suma celor doi coeficienţi. Indiferent de suma coeficienţilor,
avansează c1 şi c2;

b) dacă gradul monomului indicat de c1 este strict mai mare decât gradul
monomului indicat de c2, se adaugă la lista polinomului rezultat monomul indicat
de c1, avansează c1;

c) dacă gradul monomului indicat de c1 este strict mai mic decât gradul
monomului indicat de c2, se adaugă la lista polinomului rezultat monomul indicat
de c2, avansează c2.

Testele de mai sus, se repetă cât timp nu s-a parcurs integral nici o listă de
intrare. Dacă s-a ajuns la sfârşitul listei pointată de c1, atunci lista pointată de c2
este copiată, începând de la c2 până la sfârşit în lista rezultat. Dacă s-a ajuns la
sfârşitul listei pointată de c2, atunci lista pointată de c1 este copiată, începând de
la c2 până la sfârşit în lista rezultat.

Pentru X3+6×X2+3×X şi 2×X2-3×X+1, c1 pointează către nodul asociat


monomului X3, iar c2 pointează către nodul asociat monomului 2×X2.

- Gradul monomului reţinut de nodul pointat de c1 este mai mare decât gradul
monomului reţinut de nodul pointat de c2. Primul nod al listei polinomului
rezultat va fi X3. c1 va pointa către nodul care reţine monomul 6×X2.

- Gradele celor două monoame sunt egale şi suma coeficienţilor este diferită de
0. Al doilea nod al listei polinomului rezultat va fi 8×X2. c1 va pointa către
nodul care reţine monomul 3×X, iar c2 va pointa către nodul care reţine
monomul -3×X.

- Gradele celor două monoame sunt egale şi suma coeficienţilor este egală cu
0. Prin avansul lui c1, prima listă a fost parcursă integral.

- Al treilea nod al listei polinomului rezultat va reţine 1.

Varianta Pascal Varianta C++


function adun(v1, Nod* adun(Nod* v1,Nod* v2)
v2:adrNod):adrNod; {
var c1,c2,v,sf:adrNod; Nod* c1=v1;
begin Nod* c2=v2;
c1:=v1; c2:=v2; v:=nil; Nod* v=0, *sf;
46 Capitolul 2. Liste liniare

while (c1<>nil) and (c2<>nil) while (c1 && c2)


do if (c1->grad==c2->grad)
if c1^.grad=c2^.grad { if (c1->coef+c2->coef)
then adaug(v,sf,c1->grad,
begin c1->coef+c2->coef);
if c1^.coef+c2^.coef<>0 c1=c1->adr_urm;
then c2=c2->adr_urm;
adaug(v,sf,c1^.grad, }
c1^.coef+c2^.coef); else
c1:=c1^.adr_urm; if (c1->grad>c2->grad)
c2:=c2^.adr_urm; { adaug(v,sf,c1->grad,
end c1->coef);
else c1=c1->adr_urm;
if c1^.grad>c2^.grad }
then else
begin { adaug(v,sf,c2->grad,
adaug(v,sf,c1^.grad, c2->coef);
c1^.coef); c2=c2->adr_urm;
c1:=c1^.adr_urm; }
end if (c1)
else while (c1)
begin { adaug(v,sf,c1->grad,
adaug(v,sf,c2^.grad, c1->coef);
c2^.coef); c1=c1->adr_urm;
c2:=c2^.adr_urm; }
end; else
if c1<>nil while (c2)
then { adaug(v,sf,c2->grad,
while c1<>nil do c2->coef);
begin c2=c2->adr_urm;
adaug(v,sf,c1^.grad, }
c1^.coef); return v;
c1:=c1^.adr_urm; }
end
else
while c2<>nil do
begin
adaug(v,sf,c2^.grad,
c2^.coef);
c2:=c2^.adr_urm;
end;
adun:=v;
end;

6. Scăderea polinoamelor. Pentru această operaţie vom folosi o funcţie negativ,


care primeşte ca parametru de intrare adresa de început a unei liste liniare care
reţine un polinom şi returnează adresa de început a unei noi liste care reţine un
polinom în care coeficienţii sunt cei ai polinomului de intrare înmulţiţi cu -1.

Evident, scăderea se reduce la aplicarea funcţiei negativ pentru polinomul


descăzut şi adunarea rezultatului cu scăzătorul.
Manual de informatică pentru clasa a XI-a 47

Varianta Pascal Varianta C++


function negativ(v:adrNod) Nod* negativ(Nod* v)
:adrNod; { Nod *v1=0, *sf1;
var v1,sf1:adrNod; while (v)
begin { adaug(v1,sf1,v->grad,
v1:=nil; -(v->coef));
while v<> nil do v=v->adr_urm;
begin }
adaug(v1,sf1,v^.grad, return v1;
-v^.coef); }
v:=v^.adr_urm;
end;
negativ:=v1;
end;

7. Înmulţirea unui polinom cu un monom. Pentru a realiza această operaţie


utilizăm funcţia mulMonom. Funcţia primeşte ca parametri de intrare adresa de
început a listei care reţine un polinom, gradul monomului şi coeficientul său.
Funcţia construieşte un nou polinom, cel rezultat în urma înmulţirii polinomului dat
cu monomul şi returnează adresa de început a listei care îl reţine.

Varianta Pascal Varianta C++


function mulMonom(v:adrNod; Nod* mulMonom(Nod* v,int gr,
gr:integer;cf:real):adrNod; float cf)
var vf,sf:adrNod; { Nod* vf=0, *sf;
begin while (v)
vf:=nil; { adaug(vf,sf,v->grad+gr,
while v<> nil do v->coef*cf);
begin v=v->adr_urm;
adaug(vf,sf,v^.grad+gr, }
v^.coef*cf); return vf;
v:=v^.adr_urm; }
end;
mulMonom:=vf;
end;

8. Înmulţirea a două polinoame. Operaţia este realizată de funcţia mul. Ea are


ca parametri de intrare adresele de început ale listelor care reţin polinomul
deînmulţit şi polinomul înmulţitor şi returnează o nouă listă în care se găseşte
polinomul rezultat.

Produsul se obţine înmulţind, cu fiecare monom al polinomului înmulţitor,


polinomul deînmulţit şi adunarea fiecărui polinom astfel obţinut la rezultat.

Observaţi cum se şterg polinoamele intermediare, care nu mai sunt


necesare!
48 Capitolul 2. Liste liniare

Varianta Pascal Varianta C++


function mul(v1,v2:adrNod):adrNod; Nod* mul(Nod* v1, Nod* v2)
var v,vman,vman1:adrNod; { Nod* v=0;
begin while (v2)
v:=nil; { Nod *vman=0, *vman1;
while v2<>nil do vman=mulMonom(v1,
begin v2->grad,v2->coef);
vman:=mulMonom(v1,v2^.grad, vman1=v;
v2^.coef); v=adun(v,vman);
vman1:=v; sterg(vman);
v:=adun(v,vman); sterg(vman1);
sterg(vman); v2=v2->adr_urm;
sterg(vman1); }
v2:=v2^.adr_urm; return v;
end; }
mul:=v;
end;

9. Împărţirea polinoamelor. Operaţia este realizată de subprogramul divp.


Acesta primeşte ca parametri de intrare adresele de început ale listelor care reţin
polinomul deîmpărţit şi polinomul împărţitor. Subprogramul returnează adresele de
început ale listelor care reţin polinomul cât şi polinomul rest. Iniţial, restul va fi
deîmpărţitul (se construieşte lista care reţine deîmpărţitul). Apoi, cât timp gradul
restului este mai mare sau egal cu gradul împărţitorului, se află monomul cu care
se înmulţeşte câtul pentru a-l scădea din rest, se calculează produsul dintre
monomul astfel determinat şi cât şi se scade polinomul astfel rezultat din rest,
obţinându-se un nou rest. Şi aici, listele conţinând polinoame care nu mai sunt
necesare, se şterg.

Varianta Pascal Varianta C++


procedure divp(deim,imp:adrNod; void divp(Nod *deim, Nod *imp,
var cat,rest:adrNod); Nod*& cat, Nod*& rest)
var coef:real; { float coef;
gradul:integer; int gradul;
sfcat,sfrest,pol,neg, Nod* sfcat=0,*sfrest=0;
factor,sfactor:adrNod; cat=rest=0;
while (deim)
begin
{ adaug(rest,sfrest,
cat:=nil; rest:=nil;
deim->grad,deim->coef);
while deim<>nil do
deim=deim->adr_urm;
begin
}
adaug(rest,sfrest,
while (rest->grad>=imp->grad)
deim^.grad,deim^.coef);
{ coef= rest->coef/
deim:=deim^.adr_urm;
imp->coef;
end; gradul=rest->grad-
while rest^.grad>=imp^.grad do imp->grad;
begin adaug(cat,sfcat,
coef:=rest^.coef/imp^.coef; gradul,coef);
gradul:=rest^.grad-imp^.grad; Nod *factor=0, *sfactor=0;
adaug(cat,sfcat,gradul,coef); adaug(factor,sfactor,
factor:=nil; gradul,coef);
Manual de informatică pentru clasa a XI-a 49

adaug(factor,sfactor,
gradul,coef); Nod* pol=mul(imp,factor);
pol:=mul(imp,factor); sterg(factor);
sterg(factor); Nod* neg=negativ(pol);
neg:=negativ(pol); sterg(pol);
sterg(pol); rest=adun(rest,neg);
rest:=adun(rest,neg); sterg(neg);
sterg(neg); }
end; }
end;

Pentru a testa subprogramele prezentate, puteţi folosi programul următor,


care citeşte două polinoame şi calculează suma, diferenţa şi produsul lor. De
asemenea, se calculează şi câtul şi restul împărţirii celor două polinoame.

Varianta Pascal Varianta C++


type struct Nod
adrNod=^Nod; { float coef;
Nod=record int grad;
coef:real; Nod* adr_urm;
grad:integer; };
adr_urm:adrNod; // subprogramele prezentate
end; main()
var m,n:integer; { int m,n;
p1,p2,s,d,p,cat,rest:adrNod; cout<<"Nr termeni primul
polinom="; cin>>m;
{ subrogramele prezentate } Nod* p1=crePolinom(m);
begin cout<<"Nr termeni al doilea
write('Nr termeni primul polinom=";cin>>n;
polinom='); readln(m); Nod* p2=crePolinom(n);
p1:=crePolinom(m); // suma
write('Nr termeni al doilea Nod* s=adun(p1,p2);
polinom='); readln(n); cout<<"Suma este"<<endl;
p2:=crePolinom(n); afis(s);
{ suma } // diferenta
s:=adun(p1,p2);
Nod* d=adun(p1,negativ(p2));
writeln('Suma este');
cout<<"diferenta este"<<endl;
afis(s);
afis(d);
{ diferenta }
// produs
d:=adun(p1,negativ(p2));
writeln('diferenta este'); Nod* p=mul(p1,p2);
afis(d); cout<<"produsul este"<<endl;
{ produs } afis(p);
p:=mul(p1,p2); // cat
writeln('produsul este'); Nod* cat,*rest;
afis(p); divp(p1,p2,cat,rest);
{ cat } cout<<"catul este"<<endl;
divp(p1,p2,cat,rest); afis(cat);
writeln('catul este'); cout<<"rest este"<<endl;
afis(cat); if (rest==0) cout<<0;
writeln('rest este'); else afis(rest);
if rest=nil then writeln(0) }
else afis(rest);
end.
50 Capitolul 2. Liste liniare

2.3. Liste liniare alocate dublu înlănţuit

Definiţia 2.3. O listă alocată dublu înlănţuit este o structură de date de


forma:

0 in1 adr2 adr1 in2 adr3 ••• adrn-1 inn 0

adr1 adr2 adrn

Avantajul utilizării listei alocate dublu înlănţuit este dat de faptul că o astfel
de listă poate fi parcursă în ambele sensuri.

Operaţiile pe care le facem cu o listă dublu înlănţuită sunt următoarele:

1) creare;
2) adăugare la dreapta;
3) adăugare la stânga;
4) adăugare în interiorul listei;
5) ştergere din interiorul listei;
6) ştergere la stânga listei;
7) ştergere la dreapta listei;
8) listare de la stânga la dreapta;
9) listare de la dreapta la stânga.

2.3.1. Crearea unei liste liniare alocate dublu înlănţuit

O listă dublu înlănţuită se creează cu o singură înregistrare. Pentru a ajunge


la numărul de înregistrări dorit, utilizăm funcţii de adăugare la stânga sau la
dreapta. În programul de faţă acest lucru este realizat de funcţia creare. Această
funcţie realizează operaţiile următoare:

• citirea informaţiei numerice;


• alocarea de spaţiu pentru înregistrare;
• completarea înregistrării cu informaţia numerică;
• completarea adreselor de legătură la stânga şi la dreapta cu 0;
• variabilele tip referinţă b şi s vor căpăta valoarea adresei acestei prime
înregistrări (b semnifică adresa înregistrării cea mai din stânga, s adresa
ultimei înregistrări din dreapta).
Manual de informatică pentru clasa a XI-a 51

Creăm lista cu un singur element:

0 7 0

b s

2.3.2. Adăugarea unei înregistrări la dreapta

Această operaţie este realizată de funcţia Addr. Pentru adăugarea unei


înregistrări se realizează următorii paşi:

• citirea informaţiei numerice;


• alocarea spaţiului pentru înregistrare;
• completarea adresei la dreapta cu 0;
• completarea adresei din stânga cu adresa celei mai din dreapta înregistrări
(reţinute în variabila s);
• modificarea câmpului de adresă la dreapta a înregistrării din s cu adresa noii
înregistrări;
• s va lua valoarea noii înregistrări, deoarece aceasta va fi cea mai din dreapta.

Adăugăm la dreapta înregistrarea 3.

0 7 3 0

b s

2.3.3. Adăugarea unei înregistrări la stânga

 Această operaţie vă este propusă ca exerciţiu!


2.3.4. Adăugarea unei înregistrări în interiorul listei

Această operaţie este realizată de funcţia Includ:

• parcurge lista de la stânga la dreapta căutând înregistrarea cu informaţia


numerică m, în dreapta căreia urmează să introducem noua înregistrare;
• citeşte informaţia numerică;
• alocă spaţiu pentru noua înregistrare;
52 Capitolul 2. Liste liniare

• completează informaţia utilă;


• adresa stângă a noii înregistrări ia valoarea adresei înregistrării de informaţie
utilă m;
• adresa stângă a înregistrării care urma la acest moment înregistrării cu
informaţia numerică m capătă valoarea adresei noii înregistrări;
• adresa dreaptă a noii înregistrări ia valoarea adresei dreapta a înregistrării cu
informaţia utilă m;
• adresa dreaptă a înregistrării cu informaţia numerică m ia valoarea noii
înregistrări.

În lista următoare adăugăm, după înregistrarea 3, înregistrarea 5:

0 3 7 0

b s

0 5 0

0 3 7 0

b s

 Propunem ca exerciţiu realizarea unei funcţii de adăugare în interiorul listei a


unei înregistrări la stânga înregistrării cu informaţia numerică m.

2.3.5. Ştergerea unei înregistrări din interiorul listei

Această operaţie este realizată de funcţia Sterg. Operaţiile efectuate de


această funcţie sunt următoarele:

• se parcurge lista de la stânga la dreapta pentru a ne poziţiona pe înregistrarea


care urmează a fi ştearsă;
• câmpul de adresă dreapta al înregistrării care o precede pe aceasta va lua
valoarea câmpului de adresă dreapta al înregistrării care va fi şterse;
Manual de informatică pentru clasa a XI-a 53

• câmpul de adresă stânga al înregistrării care urmează înregistrării care va fi


şterse va lua valoarea câmpului de adresă stânga al înregistrării pe care o
ştergem;
• se eliberează spaţiul de memorie rezervat înregistrării care se şterge.

În lista de mai jos se şterge elementul cu informaţia numerică 5:

0 3 5 7 0

b s

0 3 5 7 0

b s

0 3 7 0

b s

2.3.6. Ştergerea unei înregistrări la stânga/dreapta listei

 Aceste două operaţii sunt propuse ca exerciţii!


2.3.7. Listarea de la stânga la dreapta listei

Această operaţie este realizată de funcţia Listare care realizează


următoarele operaţii:
• porneşte din stânga listei;
• atât timp cât nu s-a ajuns la capătul din dreapta al listei, se tipăreşte informaţia
numerică şi se trece la înregistrarea următoare.

2.3.8. Listarea de la dreapta la stânga listei

 Operaţia se propune ca exerciţiu!


54 Capitolul 2. Liste liniare

În continuare sunt prezentate subprogramele şi un exemplu de utilizare a lor:

Varianta Pascal Varianta C++


type ref=^inr; #include <iostream.h>
inr=record struct Nod
as:ref; { Nod *as, *ad;
nr:integer; int nr;
ad:ref };
end; Nod *b,*s,*c;
var b,s,c:ref; int n,m,i;
n,m,i:integer;
void Creare (Nod*& b, Nod*& s)
procedure creare(var b,s:ref); { cout<<"n="; cin>>n;
begin b=new Nod;
write('n='); readln(n); b->nr=n;
new(b); b^.nr:=n; b->as=b->ad=0;
b^.as:=nil; b^.ad:=nil; s=b;
s:=b }
end;
void Addr(Nod*& s)
procedure addr(var s:ref);
{ cout<<"n="; cin>>n;
var d:ref;
Nod* d=new Nod;
begin
write('n='); readln(n); d->nr=n;
new(d); d->as=s;
d^.nr:=n; d^.as:=s; d->ad=0;
d^.ad:=nil; s->ad=d;
s^.ad:=d; s:=d s=d;
end; }

procedure listare(b:ref); void Listare(Nod*& b)


var d:ref; { Nod* d=b;
begin while (d)
d:=b; { cout<<d->nr<<endl;
while d<>nil do d=d->ad; }
begin }
writeln(d^.nr);
void Includ(int m, Nod* b)
d:=d^.ad
{ Nod *d=b, *e;
end
end; while (d->nr!=m) d=d->ad;
cout<<"n="; cin>>n;
procedure includ e=new Nod;
(m:integer;b:ref); e->nr=n;
var d,e:ref; e->as=d;
begin d->ad->as=e;
d:=b; e->ad=d->ad;
while d^.nr<>m do d:=d^.ad; d->ad=e;
write('n='); readln(n); }
new(e);
e^.nr:=n; e^.as:=d; void Sterg(int m, Nod* b)
d^.ad^.as:=e; { Nod* d=b;
e^.ad:=d^.ad; while (d->nr!=m) d=d->ad;
d^.ad:=e d->as->ad=d->ad;
end; d->ad->as=d->as;
delete d;
}
Manual de informatică pentru clasa a XI-a 55

procedure sterg (m:integer; main()


b:ref); { cout<<"Creare lista cu o
var d:ref; singura inregistr. "<<endl;
begin Creare (b,s);
d:=b; cout<<"Cate inregistrari se
while d^.nr<>m do d:=d^.ad; adauga ?";cin>>m;
d^.as^.ad:=d^.ad; for (i=1;i<=m;i++) Addr(s);
d^.ad^.as:=d^.as; cout<<"Acum listez de la
dispose(d) stanga la dreapta"<<endl;
end; Listare(b);
begin cout<<"Includem la dreapta o
writeln('Creare lista cu o inregistrare "<<endl;
singura inregistrare'); cout<<"dupa care inregistrare
creare(b,s); se face includerea?"; cin>>m;
write('Cate inreg. se adauga? '); Includ (m,b);
readln(m); cout<<"Acum listez de la
for i:=1 to m do addr(s); stanga la dreapta"<<endl;
writeln('Acum listez de la Listare(b);
stanga la dreapta'); cout<<"Acum stergem o
listare(b); inregistrare din
writeln('Includem la dreapta o interior"<<endl;
inregistrare'); cout<<"Ce inregistrare se
write('Dupa care inregistrare sterge?";
se face includerea? ');
cin>>m;
readln(m);
Sterg(m,b);
includ(m,b);
writeln('Acum listez de la cout<<"Acum listez de la
stanga la dreapta'); stanga la dreapta"<<endl;
listare(b); Listare(b);
writeln('Acum stergem o }
inregistrare din interior');
write('Ce inreg. stergem? ');
readln(m);
sterg(m,b);
writeln('Acum listez de la
stanga la dreapta');
listare(b)
end.

2.4. Stiva implementată ca listă liniară simplu înlănţuită

Definiţia 2.4. Stiva este o listă pentru care singurele operaţii permise sunt:
• adăugarea unui element în stivă;
• eliminarea, consultarea sau modificarea ultimului element introdus în
stivă.

Stiva funcţionează pe principiul LIFO (Last In First Out) - "ultimul


intrat primul ieşit".
Analizaţi programul următor, care creează o stivă prin utilizarea unei liste
liniare simplu înlănţuite. Adăugarea unui element în stivă se face cu subprogramul
PUSH, iar eliminarea, cu subprogramul POP. Vârful stivei este reţinut de variabila v.
56 Capitolul 2. Liste liniare

Varianta Pascal Varianta C++


type Adresa=^Nod; #include <iostream.h>
Nod=record struct Nod
info:integer; { int info;
adr_inap:Adresa; Nod* adr_inap;
end; };
var v:Adresa; Nod* v;
n:integer; int n;
procedure Push(var v:Adresa; void Push (Nod*& v,int n)
n:integer); { Nod* c;
var c:Adresa; if (!v)
begin { v= new Nod;
if v=nil v->info=n;
then begin v->adr_inap=0;
new(v); }
v^.info:=n; else
v^.adr_inap:=nil; { c= new Nod;
end c->info=n;
else begin c->adr_inap=v;
new(c);c^.info:=n; v=c;
c^.adr_inap:=v; }
v:=c; }
end void Pop (Nod*& v)
end; { Nod* c;
procedure Pop (var v:Adresa); if (!v)
var c:Adresa; cout<<"stiva este vida";
begin else
if v=nil then { c=v;
writeln('stiva este vida') cout<<"am scos"
else begin << c->info<<endl;
c:=v; v=v->adr_inap;
writeln('am scos', c^.info); delete c;
v:=v^.adr_inap; }
dispose(c) }
end; main()
end; { Push(v,1); Push(v,2);
begin Push(v,3);
Push(v,1); Push(v,2); Pop(v); Pop(v);
Pop(v); Pop(v); Pop(v) Pop(v); Pop(v);
end. }

2.5. Coada implementată ca listă liniară simplu


înlănţuită

Definiţia 2.5. O coadă este o listă pentru care toate inserările sunt făcute
la unul din capete, iar toate ştergerile (consultările, modificările) la celălalt
capăt.

Coada funcţionează pe principiul FIFO (First In First Out) - "primul


intrat primul ieşit".
Manual de informatică pentru clasa a XI-a 57

Alocarea dinamică înlănţuită a cozii. O variabilă v va reţine adresa


elementului care urmează a fi scos (servit). O alta, numită sf, va reţine adresa
ultimului element introdus în coadă. Figura următoare prezintă o coadă în care
primul element care urmează a fi scos are adresa în v, iar ultimul introdus are
adresa în sf.

v sf

7 3 5 2

Varianta Pascal Varianta C++


type Adresa=^Nod; #include<iostream.h>
Nod=record struct Nod
info:integer; { int info;
adr_urm:Adresa; Nod* adr_urm;
end; };
var v,sf:Adresa;
n:integer; Nod* v,*sf;
int n;
procedure Pune(var v,
sf:Adresa;n:integer); void Pune(Nod*& v,Nod*& sf,int n)
var c:Adresa; { Nod* c;
begin if (!v)
if v=nil then begin { v=new Nod;
new(v); v->info=n;
v^.info:=n; v->adr_urm=0;
v^.adr_urm:=nil; sf=v;
sf:=v; }
end else
else begin { c=new Nod;
new(c); sf->adr_urm=c;
sf^.adr_urm:=c; c->info=n;
c^.info:=n; c->adr_urm=0;
c^.adr_urm:=nil; sf=c;
sf:=c }
end }
end;

procedure Scoate(var v, void Scoate(Nod*& v)


sf:Adresa); { Nod* c;
var c:Adresa; if (!v)
begin cout<<"coada este
if v=nil then vida"<<endl;
writeln('coada este vida') else
else { cout<<"Am scos "
begin <<v->info<<endl;
c:=v; c=v;
v:=v^.adr_urm; v=v->adr_urm;
dispose(c) delete c;
end; }
end; }
58 Capitolul 2. Liste liniare

{ subprogram de Listare a // subprogram de Listare a


elementelor aflate in coada } //elementelor aflate in coada
begin main()
Pune(v,sf,1); Pune(v,sf,2); { Pune(v,sf,1); Pune(v,sf,2);
Pune (v,sf,3); Listare (v); Pune(v,sf,3); Listare(v);
Scoate(v,sf); Listare(v); Scoate(v); Listare(v);
Scoate(v,sf); Listare(v); Scoate(v); Listare(v);
Scoate(v,sf); Listare(v); Scoate(v); Listare(v);
end. }

Probleme propuse
1. Asociaţi fiecărui tip de listă denumit în coloana din stânga desenul
corespunzător din coloana aflată în partea dreaptă a tabelului următor, scriind cifra
asociată fiecărei litere:

A. listă liniară simplu înlănţuită 1.

B. listă neliniară simplu înlănţuită 2.

C. listă liniară dublu înlănţuită 3.

D. listă neliniară dublu înlănţuită 4.

2. Care dintre nodurile listei următoare (identificate prin numere între 1 şi 4) este
primul element al listei?

1 2 3 4

a) 1; b) 4; c) 2; d) nu există un prim element.

3. Dacă o listă formată din două noduri (identificate prin numerele 1 şi 2) are
proprietatea că elementul următor nodului 2 este nodul 1 şi nu există un element
următor nodului 1, atunci spunem că lista:

a) este circulară; b) este liniară simplu înlănţuită;


c) nu este liniară; d) este liniară dublu înlănţuită.
Manual de informatică pentru clasa a XI-a 59

4. Ştiind că adresa de început a listei reprezentate în desenul următor este


memorată în variabila p şi că fiecare nod al listei reţine în câmpul inf numărul
scris în desen şi în câmpul adr_urm adresa elementului următor, stabiliţi ce
reprezintă expresia de mai jos.

Varianta Pascal Varianta C++


p^.adr_urm^.adr_urm^.nr p->adr_urm->adr_urm->nr

8 4 2 3

a) este o expresie incorectă;


b) valoarea memorată în nodul al treilea (valoarea 2);
c) adresa elementului al treilea (elementului ce memorează valoarea 2);
d) valoarea memorată în nodul al doilea (valoarea 4);
e) adresa elementului al doilea (elementului ce memorează valoarea 4).

5. Ştiind că adresa de început a listei reprezentate în desenul următor este


memorată în variabila p şi că fiecare nod al listei reţine în câmpul inf numărul
scris în desen şi în câmpul adr_urm adresa elementului următor, stabiliţi ce
reprezintă expresia:

Varianta Pascal Varianta C++


p^.adr_urm^.nr^.adr_urm p->adr_urm->nr->adr_urm

8 4 2 3

a) este o expresie incorectă;


b) valoarea memorată în nodul al doilea (valoarea 4);
c) adresa elementului al treilea (elementului ce memorează valoarea 2);
d) valoarea memorată în nodul al treilea (valoarea 2).

6. Ştiind că există o listă liniară simplu înlănţuită nevidă, fiecare nod reţinând în
câmpul ref adresa elementului următor al listei, şi ştiind că variabilele v şi s reţin
adresa primului şi respectiv adresa ultimului element al listei, explicaţi care este
efectul instrucţiunii:

Varianta Pascal Varianta C++


s^.ref=v s->ref=v

7. Ştiind că există o listă liniară simplu înlănţuită cu cel puţin două noduri, fiecare
nod reţinând în câmpul urm adresa elementului următor al listei, şi ştiind că
variabilele ini şi fin reţin adresa primului şi respectiv adresa ultimului element al
listei, explicaţi care este efectul instrucţiunii:
60 Capitolul 2. Liste liniare

Varianta Pascal Varianta C++


ini^.urm=fin ini->urm=fin

8. Scrieţi un subprogram care creează o listă liniară simplu înlănţuită în care,


fiecare nod, pe lângă informaţia de adresă, va conţine o variabilă de tip Struct
care reţine date referitoare la un elev. Funcţia va returna adresa primului nod al
listei. Datele se citesc de la tastatură.

 numele: 20 de caractere;
 prenumele: 20 de caractere;
 un vector de numere reale cu 3 componente care reţin notele elevului.

9. Scrieţi un subprogram care afişează pe monitor numele, prenumele şi media


generală a fiecărui elev din lista de mai sus. Funcţia va primi ca parametru de
intrare vârful listei.

10. Scrieţi o funcţie care returnează media generală a elevilor care se găsesc în listă.

11. Scrieţi un program care creează şi afişează o listă liniară simplu înlănţuită.
Fiecare nod al listei conţine, pe lângă informaţia de adresă, un număr natural mai
mic sau egal cu 100000. Numerele se găsesc, toate pe o linie, în ordine, separate
prinr-un spaţiu (blank) în fişierul text ”lista.in”.

12. Scrieţi un program care creează şi afişează două liste liniare simplu înlănţuite.
Prima listă va conţine, în ordinea citirii, numere pare, iar a doua va conţine, în
aceeaşi ordine, numere impare. Numerele se citesc din fişierul text ”numere.in”.
Ele se găsesc toate pe o linie şi sunt separate prin cel puţin un spaţiu. De exemplu,
dacă fişierul text conţine numerele: 0 2 5 9 8 3 6 7 1, atunci listele vor fi:

0 2 8 6

5 9 3 7 1

13. Scrieţi un subprogram care creează fişierul text ”liste.out” cu informaţiile


aflate în cele două liste de la problema anterioară. Prima linie va conţine numerele
din prima listă, a doua numerele din a doua listă. Exemplu: pentru listele din figura
de mai sus, fişierul va fi:

Linia 1 0 2 8 6
Linia 2 5 9 3 7 1

14. Scrieţi un subprogram care adaugă un nod la sfârşitul unei liste liniare simplu
înlănţuite. Fiecare nod al listei conţine, pe lângă informaţia de adresă, un număr
real. Subprogramul are ca parametri formali adresa primului element al listei şi
valoarea reală care se adaugă.
Manual de informatică pentru clasa a XI-a 61

15. Scrieţi o funcţie care adaugă un nod la începutul unei liste liniare simplu
înlănţuite. Fiecare nod al listei conţine, pe lângă informaţia de adresă, un număr
real. Funcţia are ca parametri formali adresa primului element al listei şi valoarea
reală care se adaugă. Ea returnează noua adresă de început a listei.
16. Se dă o listă liniară simplu înlănţuită ale cărei noduri reţin, pe lângă informaţiile
de adresă, numere naturale cu o singură cifră. Lista are cel puţin un nod şi cel mult
6 noduri. Se cere să se scrie o funcţie care calculează şi afişează valoarea
întreagă obţinută prin lipirea cifrelor memorate în listă în ordinea citirii. Funcţia va
primi ca parametru de intrare vârful listei. Exemplu: pentru lista de mai jos se
afişează valoarea 956:

0 9 5 6

17. Prin operaţia de concatenare a două liste liniare simplu înlănţuite se obţine o a
treia listă liniară simplu înlănţuită care conţine, în ordine, nodurile primei liste,
urmate de nodurile celei de-a doua liste. Să se scrie o funcţie care concatenează
două liste date prin adresele nodurilor de început. Funcţia va returna adresa
primului nod al noii liste.
18. Se citeşte un fişier text cifre.in care conţine, pe o unică linie, numai numere
naturale între 0 şi 9. Numerele nu sunt separate prin spaţii. Se cere să se formeze
o listă liniară simplu înlănţuită în care fiecare nod reţine o cifră. Exemplu: Pentru
01396 se obţine lista:

0 1 3 9 6

19. Să se scrie o funcţie care primeşte ca parametru de intrare adresa unei liste
liniare simplu înlănţuite şi are rolul de a inversa nodurile aflate pe prima şi ultima
poziţie. Funcţia va returna adresa primului nod al listei.

20. Scrieţi un subprogram care eliberează spaţiul ocupat de o listă liniară simplu
înlănţuită.
21. Scrieţi un subprogram care memorează un tablou bidimensional cu m linii şi n
coloane ca m liste liniare simplu înlănţuite, unde fiecare listă memorează, în ordine,
elementele unei linii.

Exemplu: pentru tabloul următor se obţin listele:

1 2 3 7 1 2 3 7
4 1 5 9

4 1 5 9

22. Se dă o listă liniară simplu înlănţuită în care fiecare nod reţine un caracter. Să
se scrie o funcţie care depistează dacă lista conţine caractere distincte sau nu. Ce
valoare trebuie să întoarcă o astfel de funcţie?
62 Capitolul 2. Liste liniare

23. Se dă o listă liniară simplu înlănţuită în care fiecare nod reţine o literă. Se cere
să se scrie o funcţie care depistează dacă cuvântul format prin alăturarea literelor
citite este sau nu palindrom (se obţine acelaşi rezultat dacă cuvântul se citeşte
direct sau invers). De exemplu, lista următoare conţine un cuvânt palindrom.

a b c b a

24. În cazul în care pentru o listă liniară simplu înlănţuită câmpul de adresă al
ultimului nod reţine adresa primului nod, se obţine o listă circulară:

Creaţi o listă circulară în care fiecare nod reţine un număr natural. De asemenea,
scrieţi subprograme de inserare şi ştergere a unui nod al listei create.
25. Se citeşte o permutare a numerelor 1, 2, ..., n. Se cere ca, prin utilizarea unei
liste circulare, să se afişeze toate permutările circulare ale acesteia.
Exemplu: Se citeşte 1 2 3. Se va afişa: 1 2 3, 2 3 1, 3 1 2.

26. Scrieţi un subprogram care transformă o listă liniară simplu înlănţuită în una
dublu înlănţuită.

27. După cum ştiţi, nu se poate lucra în mod direct cu numere naturale oricât de
mari. Din acest motiv, vom memora un număr natural ca o listă liniară simplu
înlănţuită. De exemplu, numărul 5610 se va memora sub forma de mai jos. Scrieţi
un subprogram care citeşte de la tastatură cifrele unui număr natural, începând cu
cifra cea mai semnificativă, şi-l memorează ca listă liniară simplu înlănţuită.

0 1 6 5

28. Scrieţi un subprogram recursiv care primeşte ca parametru de intrare adresa


unei liste liniare simplu înlănţuite care reţine un număr ca mai sus şi afişează
numărul. De exemplu, pentru lista de mai sus se afişează 5610.

29. Scrieţi o funcţie care adună două numere naturale memorate ca mai sus şi
returnează adresa de început a numărului sumă, memorat ca listă. Exemplu: din
primele liste rezultă a treia listă:

0 1 6 5

5 9 4 7 9

5 0 1 3 0 1
Manual de informatică pentru clasa a XI-a 63

30. Scrieţi o funcţie care calculează produsul dintre un număr natural memorat sub
formă de listă şi un altul, cu o singură cifră, transmis ca parametru. Funcţia
returnează adresa primului nod al listei care conţine rezultatul. Exemplu: numărul
de mai jos se înmulţeşte cu 3 şi rezultă:

0 1 6 5

0 3 8 6 1

31. Scrieţi o funcţie care înmulţeşte două numere naturale memorate sub formă de
liste şi returnează adresa de început a listei rezultat.
32. Lucrare în echipă. Scrieţi un ansamblu de subprograme (numim ansamblul
NUMERE_MARI) care să ne ajute să lucrăm cu numere întregi, oricât de mari,
memorate sub formă de liste. Utilizatorul poate efectua adunarea, scăderea,
înmulţirea şi împărţirea a două astfel de numere. De asemenea, vor exista funcţii
care să permită efectuarea comparaţiilor între două numere: mai mare, mai mic,
egal, mai mare sau egal, mai mic sau egal.
33. Prin utilizarea asamblului numit NUMERE_MARI, calculaţi maximul a n numere
întregi, citite de la tastatură.
34. Prin utilizarea asamblului numit NUMERE_MARI, sortaţi crecător n numere întregi,
citite de la tastatură.
35. Prin utilizarea asamblului numit NUMERE_MARI, calculaţi n!, unde n este citit
de la tastatură.
n
36. Prin utilizarea asamblului numit NUMERE_MARI, calculaţi: ∑ k!
k =1

37. Lucrare în colectiv. Se numeşte matrice rară o matrice în care majoritatea


elementelor sunt nule. O matrice rară va fi memorată cu ajutorul a două liste
liniare simplu înlănţuite, una conţinând valorile nenule, alta numărul de ordine al
lor, număr rezultat prin parcurgerea pe linii a matricei. Scrieţi un set de
subprograme cu ajutorul cărora să se poată efectua cu uşurinţă operaţii cu matrice
rare: adunare, scădere, înmulţire. Exemplu: matricea următoare se memorează
aşa cum se vede:

1.2 0 0 0 1.2 5 3
 
A= 0 5 0 0
 0 3 
 0 0 1 6 12

Răspunsuri la testele grilă


1. Se realizează asocierile: A-3, B-1, C-2, D-4; 2. a); 3. b) ; 4. b); 5. a).
64

Capitolul 3

Metoda DIVIDE ET IMPERA

3.1. Prezentare generală

DIVIDE ET IMPERA este o tehnică specială şi se bazează pe un principiu


extrem de simplu: descompunem problema în două sau mai multe subprobleme
(mai uşoare), care se rezolvă, iar soluţia pentru problema iniţială se obţine
combinând soluţiile problemelor în care a fost descompusă. Se presupune că
fiecare dintre problemele în care a fost descompusă problema iniţială, se poate
descompune în alte subprobleme, la fel cum a fost descompusă problema iniţială.
Procedeul se reia până când (în urma descompunerilor repetate) se ajunge la
probleme care admit rezolvare imediată.

Evident, nu toate problemele pot fi rezolvate prin utilizarea acestei tehnici.


Fără teama de a greşi, putem afirma că numărul lor este relativ mic, tocmai datorită
cerinţei ca problema să admită o descompunere repetată.

DIVIDE ET IMPERA este o tehnică ce admite o implementare recursivă. Am


învăţat principiul general prin care se elaborează algoritmii recursivi: ce se întâmplă
la un nivel, se întâmplă la orice nivel (având grijă să asigurăm condiţiile de
terminare). Tot aşa, se elaborează un algoritm prin DIVIDE ET IMPERA. La un anumit
nivel, avem două posibilităţi:

1) am ajuns la o problemă care admite o rezolvare imediată, caz în care se


rezolvă şi se revine din apel (condiţia de terminare);

2) nu am ajuns în situaţia de la punctul 1, caz în care descompunem problema în


două sau mai multe subprobleme, pentru fiecare din ele reapelăm funcţia,
combinăm rezultatele şi revenim din apel.

3.2. Aplicaţii

3.2.1. Valoarea maximă dintr-un vector

 Problema 3.1. Se citeşte un vector cu n componente, numere naturale. Se


cere să se tipărească valoarea maximă.

Problema de mai sus este binecunoscută. Cum o rezolvăm utilizând tehnica


DIVIDE ET IMPERA?
Manual de informatică pentru clasa a XI-a 65

 Rezolvare. Trebuie tipărită valoarea maximă dintre numerele reţinute în vector de


la i la j (iniţial, i=1 şi j=n).

Pentru aceasta, procedăm astfel:

• dacă i=j, valoarea maximă va fi v[i];

• contrar, vom împărţi vectorul în doi vectori (primul vector va conţine


componentele de la i la (i+j) div 2, al doilea va conţine componentele
de la (i+j) div 2 + 1 la j, rezolvăm subproblemele (aflăm maximul
pentru fiecare din ele) iar soluţia problemei va fi dată de valoarea maximă
dintre rezultatele celor două subprobleme.

Programul este următorul:

Varianta Pascal Varianta C++


var v:array[1..10] of #include <iostream.h>
integer; int v[10],n;
n,i:integer;
int max(int i,int j)
function max(i,j:integer) { int a,b;
:integer; if (i==j) return v[i];
var a,b:integer; else
begin { a=max(i,(i+j)/2);
if i=j b=max((i+j)/2+1,j);
then max:=v[i] if (a>b) return a;
else else return b;
begin }
a:=max(i,(i+j) div 2); }
b:=max((i+j) div 2+1,j);
if a>b main()
then max:=a { cout<<"n="; cin>>n;
else max:=b; for (int i=1;i<=n;i++)
end; { cout<<"v["<<i<<"]=";
end; cin>>v[i];
}
begin
cout<<"max="<<max(1,n);
write('n=');
}
readln(n);
for i:=1 to n do
begin
write('v[',i,']=');
readln(v[i])
end;
writeln('max=',max(1,n))
end.

Algoritmul prezentat este exclusiv didactic, în practică este preferat


algoritmul clasic.
66 Capitolul 3. Metoda DIVIDE ET IMPERA

3.2.2. Sortarea prin interclasare

 Problema 3.2. Se consideră vectorul a cu n componente numere întregi (sau


reale). Să se sorteze crescător, utilizând sortarea prin interclasare.

Interclasarea a doi vectori a fost studiată. Dacă dispunem de două şiruri de


valori, primul cu m elemente, al doilea cu n elemente, ambele sortate, atunci se
poate obţine un vector care conţine toate valorile sortate. Algoritmul de interclasare
este performant, pentru că efectuează cel mult m+n-1 comparări.

În cele ce urmează, vom utiliza algoritmul de interclasare în vederea sortării


unui vector prin interclasare.

 Rezolvare. Algoritmul de sortare prin interclasare se bazează pe următoarea


idee: pentru a sorta un vector cu n elemente îl împărţim în doi vectori care, odată
sortaţi, se interclasează.

Conform strategiei generale DIVIDE ET IMPERA, problema este descompusă în


alte două subprobleme de acelaşi tip şi, după rezolvarea lor, rezultatele se combină
(în particular se interclasează). Descompunerea unui vector în alţi doi vectori care
urmează a fi sortaţi are loc până când avem de sortat vectori de una sau
două componente.

În aplicaţie, funcţia sort sortează un vector cu maximum două elemente;


interc interclasează rezultatele; divimp implementează strategia generală a
metodei studiate.

Varianta Pascal Varianta C++


type vector=array [1..10] of #include <iostream.h>
integer;
int a[10],n;
var a:vector;
n,i:integer; void sort(int p,int q,
int a[10])
procedure sort(p,q:integer; {
var a:vector); int m;
if (a[p]>a[q])
var m:integer; { m=a[p];
begin a[p]=a[q];
if a[p]>a[q] a[q]=m;
then }
begin }
m:=a[p]; void interc(int p,int q,
a[p]:=a[q]; int m,int a[10])
a[q]:=m { int b[10],i,j,k;
end i=p; j=m+1; k=1;
end;
Manual de informatică pentru clasa a XI-a 67

procedure interc while (i<=m && j<=q)


(p,q,m:integer; var a:vector); if (a[i]<=a[j])
var b:vector; { b[k]=a[i];
i,j,k:integer; i=i+1;
k=k+1;
begin }
i:=p; j:=m+1; k:=1; else
while (i<=m) and (j<=q) do { b[k]=a[j];
if a[i]<=a[j] then j=j+1;
begin k=k+1;
b[k]:=a[i]; }
i:=i+1; k:=k+1 if (i<=m)
end for (j=i;j<=m;j++)
else
{ b[k]=a[j];
begin
k=k+1;
b[k]:=a[j];
j:=j+1; k:=k+1 }
end; else
if i<=m then for (i=j;j<=q;j++)
for j:=i to m do { b[k]=a[i];
begin k=k+1;
b[k]:=a[j]; k:=k+1 }
end k=1;
else for (i=p;i<=q;i++)
for i:=j to q do { a[i]=b[k];
begin k=k+1;
b[k]:=a[i]; k:=k+1 }
end; }
k:=1;
for i:=p to q do void divimp (int p,int q,
begin int a[10])
a[i]:=b[k]; k:=k+1 { int m;
end if ((q-p)<=1) sort(p,q,a);
end; else
{ m=(p+q)/2;
procedure divimp divimp(p,m,a);
(p,q:integer;var a:vector); divimp(m+1,q,a);
var m:integer;
interc(p,q,m,a);
begin
}
if (q-p)<=1 then sort(p,q,a)
else }
begin
m:=(p+q) div 2; main()
divimp(p,m,a); { int i;
divimp(m+1,q,a); cout<<"n="; cin>>n;
interc(p,q,m,a) for (i=1;i<=n;i++)
end { cout<<"a["<<i<<"]=";
end; cin>>a[i];
}
begin divimp(1,n,a);
write('n='); readln(n); for (i=1;i<=n;i++)
for i:=1 to n do cout<<a[i]<<" ";
begin }
write('a[',i,']=');
readln(a[i])
end;
divimp(1,n,a);
for i:=1 to n do writeln(a[i])
end.
68 Capitolul 3. Metoda DIVIDE ET IMPERA

În continuare, calculăm numărul aproximativ de comparări efectuat de


algoritm. Fie acesta T(n). Mai simplu, presupunem n=2k.

O problemă se descompune în alte două probleme, fiecare cu n/2


componente, după care urmează interclasarea lor, care necesită n/2+n/2=n
comparaţii:

0, n = 1;

T(n) =   n 
2T  + n, altfel.

  2
Avem:

T(n) = T(2k ) = 2(T(2k −1 ) + 2k −1 ) = 2T(2 k −1 ) + 2k = 2T(2 k −2 + 2k −1 ) + 2k =


2T(2 k −2 ) + 2k + 2k = ...2

k
+ +
2k +2
... = n+
k
n + ...
  + n = n ⋅ k = n ⋅ log2n

de k ori de k ori

3.2.3. Sortarea rapidă

 Problema 3.3. Fie vectorul a cu n componente numere întregi (sau reale). Se


cere ca vectorul să fie sortat crescător.

 Rezolvare. Este necesară o funcţie POZ care tratează o porţiune din vector,
cuprinsă între indicii daţi de li (limita inferioară) şi ls (limita superioară). Rolul
acestei funcţii este de a poziţiona prima componentă a[li] pe o poziţie k cuprinsă
între li şi ls, astfel încât toate componentele vectorului cuprinse între li şi k-1
să fie mai mici sau egale decât a[k] şi toate componentele vectorului cuprinse
între k+1 şi ls să fie mai mari sau egale decât a[k].

În această funcţie există două moduri de lucru:

a) i rămâne constant, j scade cu 1;


b) i creşte cu 1, j rămâne constant.

Funcţia este concepută astfel:


• iniţial, i va lua valoarea li, iar j va lua valoarea ls (elementul care iniţial
se află pe poziţia li se va găsi mereu pe o poziţie dată de i sau de j);
• se trece în modul de lucru a);
• atât timp cât i<j, se execută:
− dacă a[i] este strict mai mare decât a[j], atunci se inversează
cele două numere şi se schimbă modul de lucru;
− i şi j se modifică corespunzător modului de lucru în care se află
programul;
− k ia valoarea comună a lui i şi j.
Manual de informatică pentru clasa a XI-a 69

Pentru a=(6,9,3,1,2), li=1, ls=5; modul de lucru a):

• i=1, j=5;
• a[1]>a[5], deci se inversează elementele aflate pe poziţiile 1 şi 5,
deci a=(2,9,3,1,6) şi programul trece la modul de lucru b);
• i=2, j=5;
• a[2]>a[5], deci a=(2,6,3,1,9) şi se revine la modul de lucru a);
• i=2, j=4;
• a[2]>a[4], deci a=(2,1,3,6,9); se trece la modul de lucru b);
• i=3, j=4;
• funcţia se încheie, elementul aflat iniţial pe poziţia 1 se găseşte
acum pe poziţia 4, toate elementele din stânga lui fiind mai mici
decât el, totodată toate elementele din dreapta lui fiind mai mari
decât el (k=4).

Alternanţa modurilor de lucru se explică prin faptul că elementul care trebuie


poziţionat se compară cu un element aflat în dreapta sau în stânga lui, ceea ce
impune o modificare corespunzătoare a indicilor i şi j.

După aplicarea funcţiei POZ, este evident că elementul care se află iniţial în
poziţia li va ajunge pe o poziţie k şi va rămâne pe acea poziţie în cadrul
vectorului deja sortat, fapt care reprezintă esenţa algoritmului.

Funcţia QUICK are parametrii li şi ls (limita inferioară şi limita superioară).


În cadrul ei se utilizează metoda DIVIDE ET IMPERA, după cum urmează:

• se apelează POZ;
• se apelează QUICK pentru li şi k-1;
• se apelează QUICK pentru k+1 şi ls.

Varianta Pascal Varianta C++


type vector=array [1..100] of #include <iostream.h>
integer; int a[100],n,k;

var i,n,k:integer; void poz (int li,int ls,int&


a:vector; k,int a[100])
{ int i=li,j=ls,c,i1=0,j1=-1;
procedure poz (li,ls:integer; while (i<j)
var k:integer; { if (a[i]>a[j])
var a:vector); { c=a[j]; a[j]=a[i];
a[i]=c; c=i1;
var i,j,c,i1,j1:integer; i1=-j1; j1=-c;
begin }
i=i+i1;
i1:=0; j=j+j1;
j1:=-1; }
i:=li; k=i;
j:=ls; }
70 Capitolul 3. Metoda DIVIDE ET IMPERA

while i<j do void quick (int li,int ls)


begin { if (li<ls)
if a[i]>a[j] { poz(li,ls,k,a);
then quick(li,k-1);
begin quick(k+1,ls);
c:=a[j]; }
a[j]:=a[i]; }
a[i]:=c;
c:=i1; main()
i1:=-j1; { int i;
j1:=-c cout<<"n="; cin>>n;
end; for (i=1;i<=n;i++)
i:=i+i1; { cout<<"a["<<i<<"]=";
j:=j+j1 cin>>a[i];
end; }
k:=i quick(1,n);
end; for (i=1;i<=n;i++)
cout<<a[i]<<endl;
procedure quick(li,ls:integer);
}
begin
if li<ls
then
begin
poz(li,ls,k,a);
quick(li,k-1);
quick(k+1,ls)
end
end;
begin
write('n=');
readln(n);
for i:=1 to n do
begin
write('a[',i,']=');
readln(a[i])
end;
quick(1,n);
for i:=1 to n do
writeln(a[i])
end.

Reţineţi! Sortarea rapidă efectuează în medie n ⋅ log 2 n operaţii.

 Demonstraţia necesită cunoştinţe de matematică pe care nu le aveţi la nivelul


acestui an de studiu…
Manual de informatică pentru clasa a XI-a 71

3.2.4. Turnurile din Hanoi

 Problema 3.4. Se dau 3 tije simbolizate prin a, b, c.


Pe tija a se găsesc discuri de diametre diferite, aşezate
în ordine descrescătoare a diametrelor privite de jos în
sus. Se cere să se mute discurile de pe tija a pe tija b,
utilizând ca tijă intermediară tija c, respectând următoarele reguli:

• la fiecare pas se mută un singur disc;


• nu este permis să se aşeze un disc cu diametrul mai mare peste un disc
cu diametrul mai mic.

 Rezolvare.
Dacă n=1, se face mutarea ab, adică se mută discul de pe tija a pe tija b.

Dacă n=2, se fac mutările ac, ab, cb.

În cazul în care n>2, problema se complică. Notăm cu H(n,a,b,c) şirul


mutărilor celor n discuri de pe tija a pe tija b, utilizând ca tijă intermediară, tija c.

Conform strategiei DIVIDE ET IMPERA, încercăm să descompunem problema


în alte două subprobleme de acelaşi tip, urmând apoi combinarea soluţiilor. În acest
sens, observăm că mutarea celor n discuri de pe tija a pe tija b, utilizând ca tijă
intermediară tija c, este echivalentă cu:

− mutarea a n-1 discuri de pe tija a pe tija c, utilizând ca tijă intermediară tija b;


− mutarea discului rămas pe tija b;
− mutarea a n-1 discuri de pe tija c pe tija b, utilizând ca tijă intermediară tija a.

Parcurgerea celor trei etape permite definirea recursivă a şirului


H(n,a,b,c) astfel:

ab, n =1
H(n, a, b, c) = 
H(n − 1, a, c, b), ab, H(n − 1, c, b, a), n > 1

Priviţi următoarele exemple:

1) pentru n=2, avem: H(2,a,b,c)=H(1,a,c,b),ab,H(1,c,b,a)=ac,ab,cb;

2) pentru n=3, avem:


H(3,a,b,c)=H(2,a,c,b),ab,H(2,c,b,a)=H(1,a,b,c),ac,H(1,b,c,a),
ab,H(1,c,a,b),cb,H(1,a,b,c)=ab,ac,bc,ab,ca,cb,ab.
72 Capitolul 3. Metoda DIVIDE ET IMPERA

Varianta Pascal Varianta C++


var a,b,c:char; #include <iostream.h>
n:integer; char a,b,c;
procedure han (n:integer; int n;
a,b,c:char);
begin void han (int n,char a,
if n=1 then char b,char c)
writeln(a,b) { if (n==1) cout<<a<<b<<endl;
else else
begin { han(n-1,a,c,b);
han(n-1,a,c,b); cout<<a<<b<<endl;
writeln(a,b); han(n-1,c,b,a);
han(n-1,c,b,a) }
end }
end; main()
begin { cout<<"N="; cin>>n;
write('N='); readln(n); a='a'; b='b'; c='c';
a:='a'; b:='b'; c:='c'; han(n,a,b,c);
han(n,a,b,c) }
end.

3.2.5. Problema tăieturilor

 Problema 3.5. Se dă o bucată dreptunghiulară de tablă cu lungimea l şi


înălţimea h, având pe suprafaţa ei n găuri de coordonate numere întregi. Se cere
să se decupeze din ea o bucată de arie maximă care nu prezintă găuri. Sunt
permise numai tăieturi verticale şi orizontale.

 Rezolvare. Coordonatele găurilor sunt reţinute în doi vectori XV şi YV.


Dreptunghiul iniţial, precum şi dreptunghiurile care apar în procesul tăierii sunt
memorate în program prin coordonatele colţului din stânga-sus (X,Y), prin
lungime şi înălţime (L,H).

Pentru un dreptunghi (iniţial pornim cu toată bucata de tablă), verificăm dacă


avem sau nu o gaură în el (se caută practic prima din cele n găuri). În situaţia când
acesta prezintă o gaură, problema se descompune în alte patru probleme de
acelaşi tip. Dacă bucata nu prezintă găuri, se compară aria ei cu aria unei alte
bucăţi fără gaură, găsită în fazele precedente.

Menţionăm că dreptunghiul de arie maximă fără găuri este reţinut prin


aceiaşi parametri ca şi dreptunghiul cu găuri, în zonele XF, YF, LF, HF.

În concluzie, problema iniţială se descompune în alte patru probleme de


acelaşi tip, mai uşoare, întrucât fiecare nou dreptunghi are cel mult n-1 găuri, dacă
dreptunghiul iniţial avea n găuri. La această problemă compararea soluţiilor constă
în a reţine dreptunghiul cu aria maximă dintre cele fără găuri.
Manual de informatică pentru clasa a XI-a 73

Fie dreptunghiul cu o gaură:

h
• xv(i),yv(i)

x,y l
Pentru a se afla în interiorul dreptunghiului, gaura trebuie să îndeplinească
simultan condiţiile:
1) xv(i)>x;
2) xv(i)<x+l;
3) yv(i)>y;
4) yv(i)<y+h.

Dacă facem o tăietură verticală prin această gaură, obţinem două dreptunghiuri:
1) x, y, xv(i)-x, h;
2) xv(i), y, l+x-xv(i), h.

În urma unei tăieturi pe orizontală se obţin cele două dreptunghiuri:


1) x, y ,l ,yv(i)-y;
2) x, yv(i), l, h+y-yv(i).

Programul este următorul:

Varianta Pascal Varianta C++


type vect=array [1..9] of #include <iostream.h>
integer; int l,h,i,n,xf,yf,lf,
hf,xv[10],yv[10];
var l,h,i,n,xf,yf,
lf,hf:integer; void dimp(int x,int y,int l,
xv,yv:vect; int h, int& xf, int& yf,
int& lf,int& hf,
procedure dimp int xv[10],int yv[10])
(x,y,l,h:integer; { int gasit=0,i=1;
var xf,yf,lf,hf:integer; while (i<=n && !gasit)
var xv,yv:vect); if (xv[i]>x && xv[i]<l &&
var gasit:boolean; yv[i]>y && yv[i]<y+h)
gasit=1;
i:integer;
else i++;
begin
if (gasit)
i:=1;
{ dimp(x,y,xv[i]-x,h,xf,
gasit:=false;
yf,lf,hf,xv,yv);
while (i<=n) and (not gasit) dimp(xv[i],y,l+x-xv[i],
do h,xf,yf,lf,hf,xv,yv);
if (xv[i]>x) and (xv[i]<l) dimp(x,y,l,yv[i]-y,xf,
and (yv[i]>y) and yf,lf,hf,xv,yv);
(yv[i]<y+h) dimp(x,yv[i],l,h+y-yv[i],
then gasit:=true xf,yf,lf,hf,xv,yv);
else i:=i+1; }
74 Capitolul 3. Metoda DIVIDE ET IMPERA

if gasit else
then if (l*h>lf*hf)
begin { xf=x;
dimp(x,y,xv[i]-x, yf=y;
h,xf,yf,lf,hf,xv,yv); lf=l;
dimp(xv[i],y,l+x-xv[i], hf=h;
h,xf,yf,lf,hf,xv,yv); }
dimp(x,y,l,yv[i]-y, }
xf,yf,lf,hf,xv,yv);
dimp(x,yv[i],l,h+y-yv[i], main()
xf,yf,lf,hf,xv,yv) { cout<<"n="; cin>>n;
end for (int i=1;i<=n;i++)
else { cout<<"x["<<i<<"]=";
if (l*h)>(lf*hf) cin>>xv[i];
then cout<<"y["<<i<<"]=";
begin cin>>yv[i];
xf:=x; }
yf:=y; cout<<"l="; cin>>l;
lf:=l; cout<<"h="; cin>>h;
hf:=h dimp(0,0,l,h,xf,yf,lf,
end hf,xv,yv);
end; cout<<"x="<<xf<<" y="<<yf
<<" l="<<lf<<" h="<<hf;
begin }
write('n=');
readln(n);
for i:=1 to n do
begin
write('x[',i,']=');
readln(xv[i]);
write('y[',i,']=');
readln(yv[i])
end;
write('l=');
readln(l);
write('h=');
readln(h);
lf:=0;
hf:=0;
dimp(0,0,l,h,xf,yf,
lf,hf,xv,yv);
writeln('x=',xf,' y=',yf,'
l=',lf,' h=',hf)
end.
Manual de informatică pentru clasa a XI-a 75

3.3. Fractali

Fractalii au fost introduşi în anul 1975 prin lucrarea revoluţionară a


matematicianului francez Benoit Mandelbrot, “O teorie a seriilor fractale”, ce
reuneşte totodată diversele teorii dinaintea sa. El este cel care a inventat cuvântul
“fractal”, de provenienţă latină (“frângere” – a sparge în fragmente neregulate).

Noţiunea de fractal a apărut ca urmare a studiului vieţii reale, în care


informaţia genetică conţinută în nucleul unei celule se repetă la diferite scări.
Calculatorul permite ca o anumită figură (de exemplu, un segment) să se
transforme într-o alta, formată din mai multe figuri iniţiale (de exemplu, o linie
frântă) şi fiecare figură obţinută să se transforme în mod asemănător (aceste
transformări necesită foarte multe calcule).

Aceste forme geometrice au fost considerate în trecut haotice sau “aberaţii


geometrice”, iar multe dintre ele erau atât de complexe încât necesitau
calculatoare performante pentru a le vizualiza. Pe parcurs, domenii ştiinţifice ca
fizica, chimia, biologia sau meteorologia descopereau elemente asemănătoare cu
fractalii în viaţa reală. Fractalii au proprietăţi matematice extrem de interesante,
care de multe ori contrazic aparenţa, dar acestea depăşesc cu mult cunoştinţele de
matematică din liceu.

Înainte de a prezenta câteva exemple, trebuie cunoscute mai întâi noţiunile


de bază pentru a lucra în mod grafic.

3.3.1. Elemente de grafică

3.3.1.1. Generalităţi (varianta Pascal)

Limbajul Pascal conţine o serie de proceduri şi funcţii care permit realizarea


unor aplicaţii grafice. Acestea sunt reunite în unitatea GRAPH, ce se găseşte în
subcatalogul UNITS.

Pentru ca o imagine să poată apărea pe ecran, calculatorul utilizează placa


video, care diferă în funcţie de memoria video şi alţi parametri. Pentru a accesa o
placă video, trebuie să folosim anumite rutine speciale, specifice lor, numite
Driver-e. Limbajul Pascal deţine o colecţie de astfel de componente software şi în
funcţie de placa ce a fost detectată în sistem, se încarcă un driver sau altul. Aceste
fişiere au extensia “bgi”. Deoarece performanţele componentelor hardware au
depăşit cu mult capacităţile CGA sau EGA, ne vom referi în continuare doar la
driver-ul VGA (Video Graphics Array), dezvoltat de firma IBM. Driver-ul VGA poate
lucra în mai multe moduri, însă vom prefera modul standard de înaltă rezoluţie
”VGAHI” (constantă de tip întreg), ce poate afişa 640 x 480 puncte în 16 culori.
76 Capitolul 3. Metoda DIVIDE ET IMPERA

Selectarea driver-ului şi a modului de lucru se face prin utilizarea procedurii


initgraph. Aceasta are trei parametri: gdriver (de tip integer) care conţine
codul asociat driver-ului, gmode (de tip integer) care reţine modul de lucru şi o
variabilă de tip string, care arată calea către unitatea GRAPH. Forma generală a
acestei proceduri este

initgraph(gdriver,gmode,‘cale’);.

Primii doi parametri sunt transmişi prin referinţă.

Iniţializarea sistemului grafic se poate face în două feluri:

1) prin a solicita să se identifice automat placa grafică şi corespunzător ei să


se încarce un anumit driver şi să se selecteze modul de lucru – cel mai bun din
punct de vedere al performanţelor:
procedure initg;
begin
gdriver := detect;
initgraph(gdriver,gmode,’c:\tp\bgi’);
if graphresult<>0 then
begin
writeln(“Tentativa esuata!”);
halt
end
end;

Constanta detect are valoarea 0 şi se specifică procedurii identificarea


automată a driver-ului şi a modului de lucru.

Vom reţine această procedură pentru că o vom utiliza în exemplele ulterioare.

2) prin indicarea cu ajutorul primilor doi parametri a unui driver şi a unui mod
de lucru solicitate de programator (în acest caz, nu se poate executa programul pe
un calculator ce nu este dotat cu placa grafică specificată):
gdriver := VGA;
gmode := VGAHI;
initgraph(gdriver,gmode,’c:\tp\bgi’);
if graphresult<>0 then
begin
writeln(“Tentativa esuata!”);
halt
end;

Tentativa de iniţializare grafică poate eşua din diverse motive, cum ar fi: lipsa
unităţii GRAPH, calea indicată greşit, etc. Testarea se realizează cu funcţia întreagă
graphresult care returnează 0 în caz afirmativ şi o valoare diferită de 0, în
caz contrar.

Odată intraţi în modul grafic, nu se mai poate scrie pe monitor, ca până


acum (cu write sau writeln). Ieşirea din modul grafic se face prin
utilizarea procedurii closegraph.
Manual de informatică pentru clasa a XI-a 77

3.3.1.2. Generalităţi (varianta C++)

Limbajul C++ (în varianta Borland), conţine o serie de funcţii care permit
realizarea unor aplicaţii grafice. Acestea sunt reunite în fişierul GRAPHICS.H, ce se
găseşte în folderul INCLUDE.

Pentru ca o imagine să poată apărea pe ecran, calculatorul utilizează placa


video, care diferă în funcţie de memoria video şi alţi parametri. Pentru a accesa o
placă video, trebuie să folosim anumite rutine speciale, specifice lor, numite
Driver-e. Limbajul C++ deţine o colecţie de astfel de componente software şi în
funcţie de placa ce a fost detectată în sistem, se încarcă un driver sau altul. Aceste
fişiere au extensia “bgi”. Deoarece performanţele componentelor hardware au
depăşit cu mult capacităţile CGA sau EGA, ne vom referi în continuare doar la
driver-ul VGA (Video Graphics Array), dezvoltat de firma IBM. Driver-ul VGA
poate lucra în mai multe moduri, însă vom prefera modul standard de înaltă
rezoluţie ”VGAHI” (constantă de tip întreg), ce poate afişa 640 x 480 puncte în
16 culori. Fişierul ce conţine driver-ul utilizat este “EGAVGA.CGI”.

Selectarea driver-ului şi a modului de lucru se face prin utilizarea funcţiei


initgraph. Aceasta are trei parametri: gdriver (de tip integer) care conţine
codul asociat driver-ului, gmode (de tip integer) care reţine modul de lucru şi o
variabilă de tip string, care arată calea către unitatea GRAPH. Forma generală a
acestei funcţii este

initgraph(&gdriver,&gmode,"cale");.

Primii doi parametri sunt transmişi prin referinţă.

Iniţializarea sistemului grafic se poate face în două feluri:

1) prin a solicita să se identifice automat placa grafică şi corespunzător ei să


se încarce un anumit driver şi să se selecteze modul de lucru – cel mai bun din
punct de vedere al performanţelor:
void init()
{ gdriver = DETECT;
initgraph(&gdriver,&gmode,"E:\\BORLANDC\\BGI");
if (graphresult())
{ cout<<"Tentativa nereusita.";
cout<<"Apasa o tasta pentru a inchide...";
getch();
exit(1);
}
}

Constanta DETECT are valoarea 0 şi se specifică funcţiei identificarea


automată a driver-ului şi a modului de lucru.

Vom reţine funcţia init() pentru că o vom utiliza în exemplele ulterioare.


78 Capitolul 3. Metoda DIVIDE ET IMPERA

2) prin indicarea cu ajutorul primilor doi parametri a unui driver şi a unui mod
de lucru solicitate de programator (în acest caz, nu se poate executa programul pe
un calculator ce nu este dotat cu placa grafică specificată):

gdriver := VGA; gmode := VGAHI;


initgraph(&gdriver,&gmode,"E:\\BORLANDC\\BGI");
if (graphresult())
{ cout<<"Tentativa nereusita.";
cout<<"Apasa o tasta pentru a inchide...";
getch();
exit(1);
}

Tentativa de iniţializare grafică poate eşua din diverse motive, cum ar fi: lipsa
unităţii GRAPHICS, calea indicată greşit, etc. Testarea se realizează cu funcţia
întreagă graphresult() care returnează 0 în caz afirmativ şi o valoare diferită
de 0, în caz contrar.

Odată intraţi în modul grafic, nu se mai poate scrie pe monitor ca până acum
(de exemplu, cu cout). Ieşirea din modul grafic se face prin utilizarea
procedurii closegraph().

Atenţie! Pentru a putea scrie şi rula programe C++ ce utilizează modul grafic al
limbajului, trebuie bifată următoarea opţiune, din meniu:

Options / Linker / Libraries / Graphics library.

3.3.1.3. Setarea culorilor şi procesul de desenare (Pascal şi C++)

Cu siguranţă, placa video utilizată de dvs. are performanţe superioare


modului standard VGA, ce se regăseşte în driver-ul limbajului Pascal sau C++.
Pentru a generaliza însă, vom considera modul menţionat anterior, ce poate reda
16 culori, reprezentate pe 4 biţi.

Fiecare culoare de bază are atribuită o constantă de la 0 la 15, precum


urmează: 0 – black (negru); 1 – blue (albastru); 2 – green (verde); 3 – cyan
(turcoaz); 4 – red (roşu); 5 – magenta (violet); 6 – brown (maro); 7 – lightgrey
(gri deschis); 8 – darkgrey (gri închis); 9 – lightblue (albastru deschis); 10 –
lightgreen (verde deschis); 11 – lightcyan (turcoaz deschis); 12 –
lightred (roşu deschis); 13 – lightmagenta (violet deschis); 14 – yellow
(galben) şi 15 – white (alb).

Aceste culori sunt cele implicite. Pentru a utiliza mai multe culori (dar nu în
acelaşi timp), se poate schimba setul (paleta) de culori. Întrucât în acest
moment nu sunt necesare o multitudine de culori, nu vom prezenta în detaliu
acest aspect.
Manual de informatică pentru clasa a XI-a 79

 Pentru a seta culoarea de fundal, se utilizează procedura (în Pascal) sau


funcţia (în C++)
setbkcolor(culoare);.

Exemple: setbkcolor(6); sau setbkcolor(RED);.

 Selectarea culorii cu care se desenează se face cu ajutorul procedurii (în


Pascal) sau funcţiei (în C++)
setcolor(culoare);.

Exemplu: setcolor(15); sau setcolor(WHITE);.

Observaţii

 Schimbarea culorii nu afectează ce am desenat anterior, ci doar ce este scris


după apelul acestei rutine.
 În cazul limbajului C++, numele simbolic al culorii se scrie obligatoriu cu
majuscule.

Operaţia de desenare

Oricare ar fi modul de lucru ales, un punct se reprezintă printr-un pixel de


coordonate x (linia) şi y (coloana), ambele valori întregi. Punctul din stânga-sus are
coordonatele (0,0). Pentru a ne muta la poziţia (x,y), vom folosi procedura (în
Pascal) sau funcţia (în C++) moveto(x,y). Pentru a trasa o linie de la punctul
curent, determinat anterior, până la o nouă poziţie, vom utiliza procedura (în
Pascal) sau funcţia (în C++) lineto(x1,y1). Astfel, vom obţine o linie între
punctele de coordonate (x,y) şi (x1,y1).

Exemplu. Mai jos, este prezentat un program ce desenează o linie pe diagonala


principală a ecranului (de la colţul din stânga-sus la colţul din dreapta jos):

Varianta Pascal Varianta C++


... ...
begin main()
initg; { init();
setcolor(red); setcolor(RED);
moveto(0,0); moveto(0,0);
lineto(getmaxx,getmaxy); lineto(getmaxx(),getmaxy());
readln; getch();
end. }

De asemenea, două funcţii foarte utile sunt getmaxx şi getmaxy (în


Pascal) sau getmaxx() şi getmaxy() (în C++). Acestea întorc valoarea minimă
şi respectiv, maximă a coordonatelor de pe ecran. Astfel, cu ajutorul lor se poate
obţine o independenţă relativă a programelor faţă de modul grafic al sistemului.
80 Capitolul 3. Metoda DIVIDE ET IMPERA

3.3.2. Curba lui Koch pentru un triunghi echilateral

Se consideră un triunghi echilateral. Fiecare latură a sa se transformă aşa


cum se vede în figura următoare (se împarte în trei segmente congruente, se
elimină segmentul din mijloc şi se construieşte deasupra un triunghi echilateral):

Figura 3.1. Exemplu de transformare

Fiecare latură a acestui poligon se transformă din nou, după aceeaşi regulă.
Să se vizualizeze figura obţinută după ls paşi (număr citit de la tastatură).

Această curbă este cunoscută în literatura de specialitate ca fiind curba lui


Koch (Herge von Koch a fost matematician suedez şi a imaginat această
curbă în anul 1904).

Programul principal va apela, pentru fiecare segment care constituie o latură


a triunghiului, o procedură numită generator. Aceasta execută transformarea de
ls ori, având ca parametri de intrare coordonatele punctelor care constituie
extremităţile segmentului, numărul de transformări făcute (n) şi numărul de
transformări care trebuie efectuate (ls). Pentru a înţelege funcţionarea procedurii,
trebuie să avem un minimum de cunoştinţe specifice geometriei analitice (ce se
poate face fără matematică?).

Fie AB un segment de dreaptă, unde A este un punct de coordonate (x1,y1),


iar B are coordonatele (x2,y2). Două puncte P1 şi P2 împart segmentul într-un
anumit raport, notat cu k:
x1 − k ⋅ x2 y1 − k ⋅ y 2
xp = , yp = .
1− k 1− k

Demonstraţi singuri aceste formule!

Fie segmentul AB cu A(x1,y1) şi B(x2,y2). Considerăm punctele C şi D care


împart segmentul în trei segmente congruente.

Aflăm coordonata punctului D:


DA x1 + 2 ⋅ x 2 y1 + 2 ⋅ y 2
k= = −2; xp = , yp = .
DB 3 3

Problema constă în stabilirea coordonatelor vârfului noului triunghi


echilateral. Acestea se obţin dacă se roteşte punctul C în jurul punctului D cu
unghiul π / 3 . Rotaţia o efectuează procedura rotplan.
Manual de informatică pentru clasa a XI-a 81

Să prezentăm algoritmul care stă la baza procedurii generator:

• se porneşte de la segmentul AB;


• se determină coordonatele punctului care constituie vârful triunghiului
echilateral (să-l notăm cu V);
• în cazul în care segmentul nu a fost transformat de ls ori, se apelează
generator pentru segmentele AC, CV, VD şi DB;
• contrar, se apelează procedura care trasează linia frântă ACVDB.

În programul principal au fost alese punctele care determină triunghiul


echilateral iniţial – plasat în centrul ecranului – şi pentru fiecare segment ce
constituie o latură a acestuia s-a apelat procedura generator. Odată trasată
curba, se colorează interiorul acesteia.

Programul este următorul:

Varianta Pascal Varianta C++


uses graph,crt; #include "graphics.h"
var L,gdriver,gmode, #include <iostream.h>
ls:integer; #include <stdlib.h>
xmax,ymax:integer; #include <conio.h>
#include <math.h>
procedure initg; int gdriver,gmode,ls,L;
...
void init()
procedure rotplan(xc,yc,x1, { ... }
y1:integer; var x,y:integer;
unghi:real); void rotplan(int xc,int yc,
begin int x1, int y1,int &x,
x := round(xc+(x1-xc)* int &y,float unghi)
cos(unghi)-(y1-yc)* {x = ceil(xc+(x1-xc)*cos(unghi)-
sin(unghi)); (y1-yc)*sin(unghi));
y := round(yc+(x1-xc)* y = ceil(yc+(x1-xc)*sin(unghi)+
sin(unghi)+(y1-yc)* (y1-yc)*cos(unghi));
cos(unghi)) }
end; void desenez(int x1,int y1,
procedure desenez(x1,y1,x2, int x2,int y2,int x3,int y3)
y2,x3,y3:integer); { moveto(x1,y1);
begin lineto(div((2*x1+x2),3).quot,
moveto(x1,y1); div((2*y1+y2),3).quot);
lineto((2*x1+x2) div 3, lineto(x3,y3);
(2*y1+y2) div 3); lineto(div((x1+2*x2),3).quot,
lineto(x3,y3); div((y1+2*y2),3).quot);
lineto((x1+2*x2) div 3, lineto(x2,y2); }
(y1+2*y2) div 3); void generator(int x1,int y1,
lineto(x2,y2); int x2,int y2,int n, int ls)
end; { int x,y;
procedure rotplan(div((2*x1+x2),3).quot,
generator(x1,y1,x2,y2, div((2*y1+y2),3).quot,
n,ls:integer); div((x1+2*x2),3).quot,
var x,y:integer; div((y1+2*y2),3).quot,
x,y,M_PI/3);
82 Capitolul 3. Metoda DIVIDE ET IMPERA

begin if (n<ls)
rotplan((2*x1+x2) div 3, {generator(x1,y1,div((2*x1+x2),
(2*y1+y2) div 3,(x1+2*x2) div 3).quot,div((2*y1+y2),
3,(y1+2*y2) div 3,x,y,pi/3); 3).quot,n+1,ls);
if n<ls then generator(div((2*x1+x2),
begin 3).quot,div((2*y1+y2),
generator(x1,y1,(2*x1+x2) 3).quot,x,y,n+1,ls);
div 3,(2*y1+y2) div 3, generator(x,y,div((x1+2*x2),
n+1,ls); 3).quot,div((y1+2*y2),
generator((2*x1+x2) div 3, 3).quot,n+1,ls);
(2*y1+y2) div 3, generator(div((x1+2*x2),
x,y,n+1,ls); 3).quot,div((y1+2*y2),
generator(x,y,(x1+2*x2) div 3).quot,x2,y2,n+1,ls);
3,(y1+2*y2) div 3,n+1,ls); }
generator((x1+2*x2) div 3, else desenez(x1,y1,x2,y2,x,y);
(y1+2*y2) div 3, }
x2,y2,n+1,ls);
end main()
else desenez(x1,y1,x2,y2,x,y); { cout<<"ls= "; cin>>ls;
end; init();
setcolor(6);
begin L = getmaxx()-320;
write('ls= '); readln(ls); generator(160,getmaxy()-150,
initg; 160+L,getmaxy()-150,1,ls);
setcolor(red); generator(160+L,getmaxy()-
L:=getmaxx-320; 150,160+div(L,2).quot,
generator(160,getmaxy-150, getmaxy()-150-
160+L,getmaxy-150,1,ls); ceil(L*(sqrt(3)/2)),1,ls);
generator(160+L,getmaxy-150, generator(160+div(L,2).quot,
160+L div 2,getmaxy-150 – getmaxy()-150-
L*round(sqrt(3)/2),1,ls); ceil(L*(sqrt(3)/2)),160,
generator(160+L div 2,getmaxy- getmaxy()-150,1,ls);
150-L*round(sqrt(3)/2),160, setfillstyle(1,4);
getmaxy-150,1,ls); floodfill(div(getmaxx(),2)
setfillstyle(1,blue); .quot,div(getmaxx(),
floodfill(getmaxx div 2, 2).quot,6);
getmaxy div 2, red); getch();
readln closegraph();
end. }

Priviţi mai jos rezultatele obţinute pentru diferite valori ale lui ls:

ls = 2 ls = 3 ls = 4
Figura 3.2. Exemple de fractali formaţi cu ajutorul curbei lui Koch (triunghi echilateral)
Manual de informatică pentru clasa a XI-a 83

3.3.3. Curba lui Koch pentru un pătrat

Se consideră un pătrat. Fiecare latură a sa se transformă după cum se vede


în figura de mai jos:

Figura 3.3. Exemplu de transformare

Fiecare segment al liniei frânte astfel formate se transformă din nou după
aceeaşi regulă. Se cere să se vizualizeze curba după ls transformări (valoare
citită de la tastatură). Transformarea şi desenarea unui segment sunt realizate de
procedura desen. Aceasta are ca parametri de intrare coordonatele punctului care
determină segmentul, numărul de transformări efectuate (n) şi numărul de
transformări cerut (ls). Procedura conţine următorul algoritm:
• dacă nu a fost efectuat numărul de transformări necesar, se
calculează coordonatele punctelor care determină linia frântă obţinută
pornind de la segment şi pentru fiecare segment din această linie se
reapelează procedura desen;
• contrar, se desenează linia frântă obţinută.
În final, figura se colorează. Programul este prezentat în continuare:

Varianta Pascal Varianta C++


uses graph,crt; #include "graphics.h"
var gdriver,gmode,ls:integer; #include <iostream.h>
#include <stdlib.h>
procedure initg;
#include <conio.h>
...
#include <math.h>
procedure rotplan(...);
int gdriver,gmode,ls,L;
...
void init()
procedure desen(x1,y1,x2, { ... }
y2,n,ls:integer); void rotplan(...)
var x3,x4,x5,x6,x7,x8,xc, { ... }
y3,y4,y5,y6,y7,y8,yc:integer;
void desen(int x1,int y1,int
begin
x2,int y2,int n,int ls)
if n<=ls then
{ int x3,x4,x5,x6,x7,x8,
begin xc,y3, y4,y5,y6,y7,y8,yc;
x3:=(3*x1+x2) div 4; if (n<=ls)
y3:=(3*y1+y2) div 4; { x3=div(3*x1+x2,4).quot;
rotplan(x3,y3,x1,y1,x4,y4, y3=div(3*y1+y2, 4).quot;
-pi/2); rotplan(x3,y3,x1,y1,x4,y4,
xc:=(x1+x2) div 2; -M_PI/2);
84 Capitolul 3. Metoda DIVIDE ET IMPERA

yc:=(y1+y2) div 2; xc=div(x1+x2,2).quot;


rotplan(xc,yc,x3,y3,x5,y5, yc=div(y1+y2,2).quot;
-pi/2); rotplan(xc,yc,x3,y3,x5,y5,
rotplan(xc,yc,x3,y3, -M_PI/2);
x6,y6,pi/2); rotplan(xc,yc,x3,y3,x6,y6,
x8:=(x1+3*x2) div 4; M_PI/2);
y8:=(y1+3*y2) div 4; x8=div(x1+3*x2, 4).quot;
rotplan(x8,y8,xc,yc,x7,y7, y8=div(y1+3*y2,4).quot;
pi/2); rotplan(x8,y8,xc,yc,x7,y7,
desen(x1,y1,x3,y3,n+1,ls); M_PI/2);
desen(x3,y3,x4,y4,n+1,ls); desen(x1,y1,x3,y3,n+1,ls);
desen(x4,y4,x5,y5,n+1,ls); desen(x3,y3,x4,y4,n+1,ls);
desen(x5,y5,xc,yc,n+1,ls); desen(x4,y4,x5,y5,n+1,ls);
desen(xc,yc,x6,y6,n+1,ls); desen(x5,y5,xc,yc,n+1,ls);
desen(x6,y6,x7,y7,n+1,ls); desen(xc,yc,x6,y6,n+1,ls);
desen(x7,y7,x8,y8,n+1,ls); desen(x6,y6,x7,y7,n+1,ls);
desen(x8,y8,x2,y2,n+1,ls); desen(x7,y7,x8,y8,n+1,ls);
if n = ls then begin desen(x8,y8,x2,y2,n+1,ls);
moveto(x1,y1); if (n == ls)
lineto(x3,y3); { moveto(x1,y1);
lineto(x4,y4); lineto(x3,y3);
lineto(x5,y5); lineto(x4,y4);
lineto(x6,y6); lineto(x5,y5);
lineto(x7,y7); lineto(x6,y6);
lineto(x8,y8); lineto(x7,y7);
lineto(x2,y2); lineto(x8,y8);
end lineto(x2,y2); }
end }
end; }
main()
begin { cout<<"ls= "; cin>>ls;
write('ls= '); readln(ls); init(); setcolor(6);
initg; setcolor(red); desen(100,100,300,100,1,ls);
desen(100,100,300,100,1,ls); desen(300,100,300,300,1,ls);
desen(300,100,300,300,1,ls); desen(300,300,100,300,1,ls);
desen(300,300,100,300,1,ls); desen(100,300,100,100,1,ls);
desen(100,300,100,100,1,ls); setfillstyle(1,3);
setfillstyle(1,blue); floodfill(div(getmaxx(),2)
floodfill(getmaxx div 2, .quot,div(getmaxy(),2).quot,6);
getmaxy div 2, red); getch(); closegraph();
readln }
end.

Sunt prezentate mai jos imaginile obţinute în urma rulării programului, pentru
diferite valori ale lui ls:

ls=1 ls=2 ls=3


Figura 3.4. Exemple de fractali formaţi cu ajutorul curbei lui Koch (pătrat)
Manual de informatică pentru clasa a XI-a 85

3.3.4. Arborele
Se dă un segment AB. Cu ajutorul lui se construieşte un arbore, aşa cum se
vede în figura de mai jos:

Figura 3.5. Exemplu de transformare în cazul unui arbore

Lungimea fiecărei ramuri este o treime din lungimea iniţială a segmentului.


Fiecare latură se transformă în mod asemănător. Se cere să se vizualizeze figura
astfel rezultată, după ls transformări.

Pentru obţinerea ramurilor se procedează astfel:

• se consideră punctul situat pe dreapta determinată de segment şi


pentru care avem:
CA x1 − 3 ⋅ x 2 3 ⋅ x 2 − x1 y1 − 3 ⋅ y 2 3 ⋅ y 2 − y1
k= = 3; xc = = , yp = = .
CB 1− 3 2 1− 3 2

• se roteşte acest punct în jurul punctului B(x2,y2) cu un unghi de π / 4 ;


• se roteşte punctul în jurul lui B cu unghiul −π / 4 .

În urma acestor rotaţii se obţin coordonatele punctelor care, împreună cu


punctul B, determină segmentele ce costituie ramurile arborelui.
Procedura desenez are ca parametri de intrare coordonatele unui segment,
numărul de transformări efectuate (n) şi numărul de transformări care trebuie
efectuate (ls). În cazul în care nu s-au efectuat toate transformările, se trasează
segmentul (cu o culoare oarecare), se calculează coordonatele punctelor care
determină ramurile şi, pentru fiecare segment, se reapelează procedura.

Programul este prezentat mai jos:

Varianta Pascal Varianta C++


uses graph,crt; #include "graphics.h"
var gdriver,gmode,ls:integer; #include <iostream.h>
xmax,ymax:integer; #include <stdlib.h>
#include <conio.h>
procedure initg; #include <math.h>
... int gdriver,gmode,ls,L;
86 Capitolul 3. Metoda DIVIDE ET IMPERA

procedure rotplan(xc,yc,x1, void init()


y1:integer; var x,y:integer; { ... }
unghi:real); void rotplan(...)
... { ... }
procedure desenez(x1,y1,x2,y2, void desenez(int x1,int y1,
n,ls:integer); int x2,int y2,int n,int ls)
var x,y:integer; { int x,y;
begin if (n<=ls)
if n<=ls then { setcolor(1+random(15));
begin moveto(x1,y1);
setcolor(1+random(15)); lineto(x2,y2);
moveto(x1,y1); rotplan(x2,y2,div(3*x2-
lineto(x2,y2); x1,2).quot,div(3*y2-y1,2)
rotplan(x2,y2,(3*x2-x1) div .quot,x,y,M_PI/4);
2,(3*y2-y1) div 2,x,y,pi/4); desenez(x2,y2,x,y,n+1,ls);
desenez(x2,y2,x,y,n+1,ls); rotplan(x2,y2,div(3*x2-
rotplan(x2,y2,(3*x2-x1) div x1,2).quot,div(3*y2-y1,
2,(3*y2-y1) div 2,x,y,-pi/4); 2).quot,x,y,-M_PI/4);
desenez(x2,y2,x,y,n+1,ls); desenez(x2,y2,x,y,n+1,ls);
end }
end; }
main()
begin { randomize();
randomize; cout<<"ls= "; cin>>ls;
write('ls= '); readln(ls); init(); setcolor(6);
initg; desenez(div(getmaxx(),2)
setbkcolor(white); .quot,getmaxy(),
desenez(getmaxx div 2, div(getmaxx(),2).quot,
getmaxy, getmaxx div 2, getmaxy()-250,1,ls);
getmaxy-250,1,ls); getch();
readln closegraph();
end. }

Pentru diverse valori ale parametrului de intrare ls, vom obţine arborii:

ls = 3 ls = 5 ls = 7
Figura 3.6. Exemple de fractali de tip arbore
Manual de informatică pentru clasa a XI-a 87

Observaţii

 Exemplele grafice prezentate au fost generate pentru valori mici ale lui ls
deoarece la tipărire, detaliile sunt greu de observat peste o anumită limită.
 Generarea fractalilor reprezintă o aplicaţie a recursivităţii, tehnica aplicată
fiind DIVIDE ET IMPERA. Pentru valori mari ale lui ls, timpul de efectuare al
calculelor poate fi şi de ordinul zecilor de secunde, ceea ce poate fi
considerat un inconvenient major.

Probleme propuse
1. Se citeşte a≥1, număr real. Se cere să se scrie o funcţie care calculează ln(a)
cu 3 zecimale exacte. Nu este permisă utilizarea funcţiei logaritmice a limbajului.

2. Scrieţi o funcţie care calculează prin metoda DIVIDE ET IMPERA suma numerelor
reţinute dintr-un vector.

3. Referitor la problema anterioară: care este complexitatea algoritmului folosit?


Se va considera ca operaţie de bază adunarea.

4. Se citeşte un număr real x∈(-10000, 10000). Să se afişeze partea


fracţionară. Exemple: pentru x=1.23, se va afişa: 0.23; pentru x=-12.7, se va
afişa 0.7. Nu se vor folosi funcţii specializate ale limbajului.

5. Se ştie că ecuaţia x3+x-1=0 are o singură rădăcină reală în intervalul (0,1).


Scrieţi un program, care o afişează cu 4 zecimale exacte.

6. Problema selecţiei. Se consideră un vector cu n componente numere naturale


şi 1≤t≤n. Se cere să se determine al t-lea cel mai mic element. Imaginaţi o
rezolvare care utilizează funcţia Poz de la sortarea rapidă!

7. Se consideră un vector care reţine n numere naturale. Se cere să se determine


dacă există un element majoritar (adică un număr care se găseşte în mai mult de
[n / 2] + 1 elemente).
Victor Mitrana

8. Fiind dat x real, să se calculeze [ x ] cu patru zecimale exacte! Nu se vor folosi


3

funcţii specializate ale limbajului.

9. Se pleacă de la un pătrat a cărui suprafaţă se divide în 9 părţi egale prin


împărţirea fiecărei laturi în 3 părţi egale. Pătratul din mijloc se elimină. Cu pătratele
rămase se procedează la fel. Vizualizaţi figura după ls astfel de transformări
(Covorul lui Sierpinski).
88 Capitolul 3. Metoda DIVIDE ET IMPERA

Răspunsuri
1. ln(a)=x ⇔ a=ex ⇔ ex-a=0. Dacă notăm cu f(x)=ex-a, atunci trebuie
rezolvată ecuaţia f(x)=0. Avem f(0)=e0-a=1-a<0 şi f(a)=ea-a>0. De aici,
rezultă că f(x) are o rădăcină în intervalul (0,a). Cum f(x) este strict
crescătoare (ca diferenţă între funcţia strict crescătoare ex şi o constantă),
rădăcina este unică. Algoritmul pe care îl folosim se numeşte în matematică
“metoda înjumătăţirii intervalului”, dar, din punct de vedere informatic, corespunde
metodei DIVIDE ET IMPERA.
Fie li=0 şi ls=a, m=(a+b)/2. Dacă f(li)×f(m)<0, rădăcina se găseşte
în (li,m), altfel rădăcina este în [m,ls). Condiţia de terminare este ca
li − ls < 0.0001 , pentru că trebuie să avem 3 zecimale exacte.

Varianta Pascal Varianta C++


var a:real; #include <iostream.h>
function LogN(a,li,ls:double): #include <math.h>
double; double a;
begin double LogN(double a,double li,
if a=1 then LogN:=0 double ls)
else { if (a==1) return 0;
if abs(li-ls)<0.0001 else
then LogN:=(li+ls)/2 if (fabs(li-ls)<0.0001)
else return (li+ls)/2;
if (exp(li)-a)* else
(exp((li+ls)/2)-a)<0 if ((exp(li)-a)*
then (exp((li+ls)/2)-a)<0)
LogN:=LogN(a,li,(li+ls)/2) return LogN(a,li,
else (li+ls)/2);
LogN:=LogN(a,(li+ls)/2,ls) else return LogN(a,
(li+ls)/2,ls);
end;
}
begin main()
write ('a='); readln(a); { cout<<"a="; cin>>a;
writeln(' rezultat cout<<"rezultat calculat "
calculat:',LogN(a,0,a):3:3); <<LogN(a,0,a)<<endl;
writeln(' rezultat preluat ', cout<<"rezultat preluat "
ln(a):3:3); <<log(a)<<endl;
end. }

Practic, la fiecare pas se înjumătăţeşte intervalul în care se caută soluţia şi


aceasta corespunde strategiei generale DIVIDE ET IMPERA.
2. Programul este prezentat mai jos:

Varianta Pascal Varianta C++


type vector=array[1..9] of integer; #include <iostream.h>
var v:vector; int n,i,v[10];
n,i:integer;
Manual de informatică pentru clasa a XI-a 89

function int Suma(int li, int ls)


Suma(li,ls:integer):integer; { if (li==ls) return v[li];
begin else return
if li=ls then Suma:=v[li] Suma(li, (li+ls)/2)+
else Suma:=Suma(li,(li+ls) div Suma((li+ls)/2+1,ls);
2) + Suma((li+ls) div 2+1,ls); }
end;
main()
begin { cout<<"n="; cin>>n;
write('n='); readln(n); for (i=1;i<=n;i++)
for i:=1 to n do readln(v[i]); cin>>v[i];
writeln(suma(1,n)); cout<<Suma(1,n);
end. }

3. Fiecare problemă se descompune în alte două şi rezultatul se adună. Pentru


simplitate, consideraţi n=2k. În final, se obţine O(n). Puteţi scrie şi funcţia recursivă
care calculează T(n), dar, pentru a obţine rezultatul corect, luaţi n=2k:

0 n = 1;

T(n) =   n 
2T  + 1 altfel.
  2 

4. A calcula [x] se reduce la DIVIDE ET IMPERA. Partea fracţionară se obţine uşor,


dacă calculăm x − [x ] . 5. Vedeţi problema 1.
6. Funcţia Poz returnează poziţia k pe care se va găsi, după rularea ei, primul
element al vectorului. În plus, toate elementele de indice mai mic decât k sunt mai
mici sau egale decât A[k] şi toate elementele de indice mai mare decât k sunt mai
mari sau egale decât A[k]. Altfel spus: elementul A[1], care se află după rularea
funcţiei pe poziţia k, este al k-lea cel mai mic element din vectorul A. Atunci, în
cazul în care k=t, problema este rezolvată. Dacă t<k, elementul căutat are
indicele cuprins între li şi k-1 şi reluăm rularea funcţiei Poz între aceste limite, iar
dacă t>k, elementul căutat are indicele între k+1 şi ls şi reluăm rularea funcţiei
Poz între aceste limite. Datorită faptului că, la fiecare pas, se restrânge numărul
valorilor de căutare, se ajunge în situaţia în care t=k. Secvenţa este:

Varianta Pascal Varianta C++


li:=1; ls:=n; li=1; ls=n;
repeat do
poz(li,ls,k,a); { poz(li,ls,k,a);
if t<k then ls:=k-1; if (t<k) ls=k-1;
if t>k then li:=k+1; if (t>k) li=k+1;
until t=k; }while (t!=k);
writeln('Elementul cautat ', cout<<"elementul cautat "
a[t]); <<a[t];

7. După aplicarea algoritmului de la problema anterioară, elementul din mijloc


trebuie să fie majoritar.
90

Capitolul 4
Metoda BACKTRACKING

4.1. Prezentarea metodei

4.1.1. Când se utilizează metoda backtracking ?

Metoda backtracking se foloseşte în rezolvarea problemelor care îndeplinesc


simultan următoarele condiţii:

 soluţia lor poate fi pusă sub forma unui vector S=x1,x2,...,xn, cu


x1∈A1, x2∈A2, ..., xn∈An;
 mulţimile A1, A2, ..., An sunt mulţimi finite, iar elementele lor se consideră
că se află într-o relaţie de ordine bine stabilită;
 nu se dispune de o altă metodă de rezolvare, mai rapidă.

În continuare, este prezentat un exemplu de problemă care poate fi


rezolvat prin utilizarea tehnicii backtracking.

Generarea permutărilor. Se citeşte un număr natural n. Să se


123
genereze toate permutările mulţimii {1,2,...,n}. De exemplu, pentru 132
n=3, permutările mulţimii {1,2,3} sunt prezentate alăturat. 213
231
Pentru această problemă, A1=A2=A3={1,2,3}. Fie permutarea 213. Ea 312
este scrisă sub formă de vector, unde 2∈A1, 1∈A2 şi 3∈A3. 321

4.1.2. Principiul care stă la baza metodei backtracking

Principiul care stă la baza metodei backtracking va fi prezentat printr-un


exemplu, acela al generării permutărilor. Cum se poate rezolva această
problemă?
O primă soluţie ar fi să generăm toate elementele produsului cartezian:

{1,2,3}×{1,2,3}×{1,2,3}={11, 12, 13, 21, 22, 23, 31, 32, 33}


×{1,2,3}= {111, 112, 113, 121, 122, 123, 131, 132, 133, 211,
212, 213, 221, 222, 223, 231, 232, 233, 311, 312, 313, 321,
322, 323, 331, 332, 333}.
Manual de informatică pentru clasa a XI-a 91

Apoi, urmează să vedem care dintre elementele acestui produs cartezian


sunt permutări, adică să conţină numai numere distincte. Astfel, 111, 112…
nu sunt permutări, dar 123 este permutare, ş.a.m.d. Produsul cartezian are 27 de
elemente şi dintre ele, doar 6 sunt permutări. În general, produsul cartezian are
nn elemente, din care permutări sunt doar n!. Vă daţi seama că un astfel de
algoritm de generare a permutărilor este ineficient…

Întrebarea este dacă problema nu se poate rezolva eficient? Să observăm


că nu are rost să generăm un element al produsului cartezian pentru ca apoi, să ne
dăm seama că nu este permutare, deoarece nu este alcătuit din numere distincte.
De exemplu, dacă la un pas al algoritmului am generat 22, e clar că nu se poate
obţine o permutare, oricare ar fi numărul care urmează pe poziţia 3.

Principiul metodei

 Metoda backtracking are la bază un principiu simplu: dacă în procesul de


generare a unui vector soluţie S=x1x2,...,xn, pentru componenta k, atunci
când am generat deja x1x2,...,xk, constatăm că valoarea xk nu este bine
aleasă (păstrând-o nu se va ajunge la o soluţie), nu trecem componenta k+1
ci reluăm căutarea pentru altă valoare pentru componenta k, iar dacă
această valoare nu există, reluăm căutarea pentru componenta k-1.

Observaţi faptul că după ce am analizat posibilele valori pe care le poate lua


componenta k, avem două posibilităţi: ori trecem la componenta k+1 (facem
pasul înainte), ori mergem la componenta k-1 (facem pasul înapoi).

Trecem la exemplificarea algoritmului pentru generarea permutărilor, în


cazul în care n=3.

 Componenta 1 va memora numărul 1. Întrucât există


1
permutări care încep cu 1, trecem la elementul 2 - facem
pasul înainte.

 Componenta 2 va memora numărul 1. 1 1

 Nu există permutări care încep cu 1,1, motiv pentru care,


1 2
pentru aceeaşi componentă, vom reţine valoarea următoare,
adică 2. Întrucât există permutări care încep cu 1,2, vom trece la elementul
3 (înainte).

 Componenta 3 va memora numărul 1. 1 2 1

 Nu există permutări care sunt de forma 1,2,1, motiv pentru


1 2 2
care aceeaşi componentă va reţine numărul următor, 2.

 Nu există permutări care sunt de forma 1,2,2, motiv pentru


1 2 3
care aceeaşi componentă va memora numărul următor, adică
3. Am obţinut deja o primă soluţie şi o afişăm.
92 Capitolul 4. Metoda backtracking

 Pentru componenta 3, nu există o altă valoare pe care o


1 3 0
putem utiliza. Din acest motiv, vom trece la elementul 2,
(înapoi). Componenta 2 are deja memorată valoarea 2. Alegem valoarea
următoare, 3. Întrucât există permutări care încep cu 1,3, vom trece la
elementul următor, 3 (înainte).

 Prima valoare care poate fi memorată este 1. Întrucât nu


1 3 2
există permutări de forma 1,3,1 trecem la valoarea
următoare 2. Dar 1,3,2 este soluţie şi o afişăm.
...

 Algoritmul continuă până când se ajunge la componenta de indice 0. În acel


moment, au fost deja afişate toate permutările.

 Exerciţiu. Arătaţi cum funcţionează algoritmul până se ajunge la


componenta de indice 0.

4.1.3. O modalitate de implementare a metodei backtracking

Pentru uşurarea înţelegerii metodei, mai întâi vom prezenta un subprogram


general, aplicabil oricărei probleme. Subprogramul va apela alte subprograme care
au întotdeauna acelaşi nume şi parametri şi care, din punct de vedere al
metodei, realizează acelaşi lucru. Sarcina celui care face programul este să scrie
explicit, pentru fiecare problemă în parte, subprogramele apelate de acesta.

 Evident, o astfel de abordare conduce la programe cu multe instrucţiuni. Din


acest motiv, după înţelegerea metodei backtracking, vom renunţa la această
formă standardizată. Dar, principiul rămâne nemodificat.

Iată subprogramul care implementează metoda. El va fi apelat prin


back(1).

Varianta Pascal Varianta C++


procedure back(k:integer); void back(int k)
begin {
if solutie(k) if (solutie(k)) tipar();
then tipar else
else { init(k);
begin while(succesor(k))
init(k); if (valid(k)) back(k+1);
while succesor(k) do }
if valid(k) then back(k+1) }
end
end;
Manual de informatică pentru clasa a XI-a 93

Să-l analizăm! Subprogramul are parametrul k de tip întreg. Acest parametru


are semnificaţia de indice al componentei vectorului pentru care se caută o valoare
convenabilă. Algoritmul va porni cu componenta de indice 1. Din acest motiv,
subprogramul se va apela cu back(1). După cum observaţi, subprogramul
este recursiv.

 Iniţial se testează dacă s-a generat o soluţie. Pentru aceasta, se apelează


subprogramul solutie(k).

Pentru permutări, vom avea o soluţie când s-a generat o secvenţă alcătuită din n
numere distincte. Cum subprogramul este recursiv, acest fapt se întâmplă atunci
când s-a ajuns pe nivelul n+1.

 Dacă s-a obţinut o soluţie, aceasta se afişează. Pentru această operaţie se


va utiliza subprogramul tipar.

 În situaţia în care nu a fost obţinută o soluţie, se iniţializează nivelul k.


Iniţializarea se face cu valoarea aflată înaintea tuturor valorilor posibile. Se
va folosi subprogramul init.

Pentru permutări, iniţializarea se face cu 0.

 După iniţializare, se generează, pe rând, toate valorile mulţimii Ak. Pentru


aceasta se utilizează subprogramul succesor. Rolul său este de a atribui
componentei k valoarea următoare celei deja existente.

 Pentru fiecare valoare generată, se testează dacă aceasta


îndeplineşte anumite condiţii de continuare. Acest test este realizat
de subprogramul valid:

 în cazul în care condiţiile sunt îndeplinite, se trece la


componenta k+1, urmând ca generarea valorilor pe nivelul k să
continue atunci când se revine pe acest nivel;

 dacă condiţiile de continuare nu sunt îndeplinite, se


generează următoarea valoare pentru componenta k.

 După ce au fost generate toate valorile mulţimii Ak se trece, implicit, la


componenta k-1, iar algoritmul se încheie când k=0.

Pentru permutări, pe fiecare nivel, valorile posibile sunt Ak={1,2,...,n}.


Condiţia de continuare, în acest caz este ca valoarea aflată pe nivelul k să fie
distinctă în raport cu valorile aflate pe nivelurile inferioare.

Programul de generare a permutărilor este prezentat în continuare.


94 Capitolul 4. Metoda backtracking

Varianta Pascal Varianta C++


var n:integer; #include <iostream.h>
sol:array [1..10] of #include <iostream.h>
integer;
int n, sol[10];
procedure init(k:integer);
begin void init(int k)
sol[k]:=0 { sol[k]=0;
end; }
function succesor int succesor(int k)
(k:integer):boolean; { if (sol[k]<n)
begin { sol[k]++;
if sol[k]<n then return 1; }
begin else return 0;
sol[k]:=sol[k]+1; }
succesor:=true int valid(int k)
end { int i, ev=1;
else succesor:=false for (i=1;i<=k-1;i++)
end; if (sol[k]==sol[i]) ev=0;
function valid return ev;
(k:integer):boolean; }
var i:integer; int solutie(int k)
begin { return k==n+1;
valid:=true; }
for i:=1 to k-1 do
if sol[i]=sol[k] then void tipar()
valid:=false { for (int i=1;i<=n;i++)
end; cout<<sol[i];
cout<<endl;
function solutie }
(k:integer):boolean;
begin void back(int k)
solutie:=(k=n+1) { if (solutie(k)) tipar();
end; else
{ init(k);
procedure tipar; while(succesor(k))
var i:integer; if (valid(k)) back(k+1);
begin }
for i:=1 to n do }
write(sol[i]);
writeln main()
end; { cout<<"n="; cin>>n;
back(1);
procedure back(k:integer); }
begin
if solutie(k)
then tipar
else
begin
init(k);
while succesor(k) do
if valid(k) then
back(k+1)
end
end;
begin
write('n='); readln(n);
back(1)
end.
Manual de informatică pentru clasa a XI-a 95

4.1.4. Problema celor n dame

 Enunţ. Fiind dată o tablă de şah cu dimensiunea n×n, se cer toate


soluţiile de aranjare a n dame, astfel încât să nu se afle două dame pe
aceeaşi linie, coloană sau diagonală (damele să nu se atace reciproc).

De exemplu, dacă n=4, o soluţie este reprezentată în figura 4.1., a). Modul de
obţinere al soluţiei este prezentat în figurile următoare, de la b) la i):

a) b) c)

d) e) f)

g) h) i)

Figura 4.1. Exemplu pentru n=4


96 Capitolul 4. Metoda backtracking

Comentarii referitoare la figurile anterioare


b) Observăm că o damă trebuie să fie plasată singură pe linie. Poziţionăm prima
damă pe linia 1, coloana 1.
c) A doua damă nu poate fi aşezată decât în coloana 3.
d) Observăm că a treia damă nu poate fi plasată în linia 3. Încercăm atunci
plasarea celei de-a doua dame în coloana 4.
e) A treia damă nu poate fi plasată decât în coloana 2.
f) În această situaţie dama a patra nu mai poate fi aşezată. Încercând să
avansăm cu dama a treia, observăm că nu este posibil să o plasăm nici în coloana
a-3-a, nici în coloana a-4-a, deci o vom scoate de pe tablă. Dama a doua nu mai
poate avansa, deci şi ea este scoasă de pe tablă. Avansăm cu prima damă în
coloana a-2-a.
g) A doua damă nu poate fi aşezată decât în coloana a 4-a.
h) Dama a treia se aşează în prima coloană.
i) Acum este posibil să plasăm a patra damă în coloana 3 şi astfel am obţinut o
soluţie a problemei.

Algoritmul continuă în acest mod până când trebuie scoasă de pe tablă


prima damă.

 Pentru căutarea şi reprezentarea unei soluţii folosim un vector cu n componente,


numit sol (având în vedere că pe fiecare linie se găseşte o singură damă). Prin
sol[i] înţelegem coloana în care se găseşte dama de pe linia i.

Alăturat, puteţi observa modul în care este reprezentată


2 4 1 3
soluţia cu ajutorul vectorului sol.

 Două dame se găsesc pe aceeaşi diagonală dacă şi numai dacă este


îndeplinită condiţia:
|sol(i)-sol(j)|=|i-j|
(diferenţa, în modul, dintre linii şi coloane este aceeaşi).

Exemple:
a)
sol(1) = 1 i = 1
sol(3) = 3 j = 3
|sol(1) - sol(3)| = |1 - 3| = 2
|i - j| = |1 - 3| = 2

Figura 4.2.
Manual de informatică pentru clasa a XI-a 97

b)
sol(1) = 3 i = 1
sol(3) = 1 j = 3
|sol(i) - sol(j)| = |3 - 1| = 2
|i - j| = |1 - 3| = 2

Figura 4.3.

Întrucât două dame nu se pot găsi în aceeaşi coloană, rezultă că o soluţie


este sub formă de permutare. O primă idee ne conduce la generarea tuturor
permutărilor şi la extragerea soluţiilor pentru problemă (ca două dame să nu fie
plasate în aceeaşi diagonală). Dacă procedăm astfel, înseamnă că nu lucrăm
conform strategiei backtracking. Aceasta presupune ca imediat ce am găsit două
dame care se atacă, să reluăm căutarea în alte condiţii. Faţă de programul de
generare a permutărilor, programul de generare a tuturor soluţiilor problemei celor
n dame are o singură condiţie suplimentară, în subprogramul valid. Mai jos,
puteţi observa noua versiune a subprogramului valid. Dacă îl utilizaţi în locul
subprogramului cu acelaşi nume din programul de generare a permutărilor, veţi
obţine programul care rezolvă problema celor n dame.

Varianta Pascal Varianta C++


function valid(k:integer): int valid(int k)
boolean; {
var i:integer; for (int i=1;i<k;i++)
if (sol[k]==sol[i] ||
begin abs(sol[k]-sol[i])==abs(k-i))
valid:=true; return 0;
for i:=1 to k-1 do return 1;
if (sol[k]=sol[i]) or }
(abs(sol[k]-sol[i])=abs(k-i))
then
valid:=false
end;

Problema este un exemplu folosit în mai toate lucrările în care este


prezentată metoda backtracking.

În ceea ce ne priveşte, dincolo de un exemplu de backtracking, am avut


ocazia să vedem cât de mult seamănă rezolvările (prin această metodă) a
două probleme între care, aparent, nu există nici o legătură.

 Exerciţii
1. Desenaţi configuraţia tablei corespunzătoare vectorului sol=(3,1,4,2,5) şi
verificaţi dacă aceasta reprezintă o soluţie a problemei damelor.
98 Capitolul 4. Metoda backtracking

2. Explicaţi de ce configuraţia corespunzătoare vecorului sol=(3,1,3,4,5) nu


este o soluţie a problemei damelor. Care este poziţia din sol la care nu s-a făcut
alegerea corectă a unei valori valide?

3. Explicaţi de ce nu orice permutare a mulţimii {1,2, …, n} este o soluţie a


problemei damelor pe o tablă cu n linii şi n coloane.

4. Determinaţi, folosind metoda backtracking, o soluţie care se obţine pe o tablă cu


şase linii şi şase coloane, ştiind că dama plasată pe ultima linie trebuie să se afle
pe a doua coloană. Consideraţi că mai este util să completăm vectorul sol
pornind de la poziţia 1? Dacă pornim de la poziţia 1, ce adaptări trebuie făcute?

4.2. Mai puţine linii în programul sursă

Până în prezent, am rezolvat două probleme prin metoda backtracking:


generarea permutărilor şi problema celor n dame. În ambele cazuri, am utilizat
subprogramul standard.

Este întotdeauna necesar să-l folosim pe acesta sau putem reţine numai
ideea şi, după caz, să scriem mai puţin?

Evident, după ce am înţeles bine metoda, putem renunţa la subprogramul


standard. Pentru aceasta, încorporăm în subprogramul standard unele dintre
subprogramele pe care le-ar fi apelat.

Vom exemplifica această încorporare pentru problema celor n dame, deja


rezolvată standardizat.

 Putem elimina subprogramul init. Este foarte uşor de realizat această


operaţie. În locul apelului vom scrie instrucţiunea prin care componentei k i
se atribuie 0.

 Putem elimina subprogramul solutie. În locul apelului vom testa dacă k


este egal cu n+1.

 Putem elimina subprogramul tipar. În locul apelului vom scrie secvenţa


prin care se afişează st.

 Putem elimina subprogramul succesor. În locul apelului vom testa dacă


st[k]<n şi avem grijă să incrementăm valoarea aflată pe nivelul curent.

Puteţi observa în continuare programul obţinut. Este cu mult mai scurt!


Oricum, ideea de rezolvare rămâne aceeaşi. Subprogramele prezentate au fost
numai încorporate, nu s-a renunţat la ele.
Manual de informatică pentru clasa a XI-a 99

Varianta Pascal Varianta C++


var sol:array[1..9] of integer; #include <iostream.h>
n:integer; #include <math.h>
int n, sol[10];
function
valid(k:integer):boolean; int valid(int k)
{ for (int i=1;i<k;i++)
var i:integer; if (sol[k]==sol[i] ||
begin abs(sol[k]-sol[i])
valid:=true; ==abs(k-i))
for i:=1 to k-1 do return 0;
if (sol[k]=sol[i]) or return 1;
(abs(sol[k]-sol[i])=abs(k-i)) }
then void back(int k)
valid:=false { if (k==n+1) // solutie
end; //tipar
{ for (int i=1;i<=n;i++)
procedure back(k:integer); cout<<sol[i];
cout<<endl;
var i:integer; }
begin else
if k=n+1 {solutie} { sol[k]=0;
then while(sol[k]<n)
begin // succesor
{tipar} { sol[k]++;
for i:=1 to n do if (valid(k))back(k+1);
write(sol[i]); }
writeln; }
end
main()
else
{ cout<<"n="; cin>>n;
begin
back(1);
sol[k]:=0; {init}
while sol[k]<n do }
{succesor}
begin
sol[k]:=sol[k]+1;
if valid(k)
then
back(k+1)
end
end
end;
begin
write('n=');
readln(n);
back(1)
end.

Uneori veţi întâlni şi o rezolvare precum următoarea, care în subprogramul


back, pentru succesor se foloseşte o instrucţiune repetitivă de tip for:
100 Capitolul 4. Metoda backtracking

Varianta Pascal Varianta C++


procedure back(k:integer); void back(int k)
var i:integer; { int i;
begin if (k==n+1)
if k=n+1 { for (i=1;i<=n;i++)
then cout<<sol[i];
begin cout<<endl;
for i:=1 to n do }
write(sol[i]); else
writeln; for (i=1;i<=n;i++)
end { sol[k]=i;
else if (valid(k))
for i:=1 to n do back(k+1);
begin }
sol[k]:=i; }
if valid(k)
then back(k+1)
end
end;

 Exerciţii
1. Testaţi subprogramul anterior pentru problema generării permutărilor.
2. Adaptaţi rezolvarea problemei permutărilor astfel încât să se afişeze numai
permutările în care oricare două numere consecutive nu sunt alăturate.
3. Observaţi că ordinea de afişare a soluţiilor depinde de ordinea în care se
consideră elementele mulţimilor A1, A2, … Ce modificări trebuie aduse
procedurii recursive back astfel încât permutarile de 4 elemente să fie afişate în
ordinea: 4321, 4312, 4231, 4213, 4132, 4123, 3421, 3412 … 1243, 1234?
4. Renunţaţi la utilizarea subprogramului valid, utilizând un vector folosit, în
care folosit[i] are valoarea 0 dacă numărul i nu este deja folosit în soluţie
şi are valoarea 1 în caz contrar. Astfel, plasarea valorii i în vectorul soluţie
(sol[k]i) trebuie însoţită de memorarea faptului că i este utilizat
(folosit[i]1), la revenirea din recursie (cand se înlătură valoarea de pe
poziţia curentă) fiind necesară memorarea faptului că i nu mai este utilizat în
soluţie (folosit[i]0). Condiţia de validare se reduce în acest caz la:
Dacă folosit[i]=0 atunci ...
5. Urmăriţi toate modalităţile diferite de a aşeza patru obiecte identificate prin
numerele 1, 2, 3, 4 pe un cerc, la distanţe egale. Vom observa că nu toate
permutările de patru obiecte sunt configuraţii distincte, datorită distribuţiei pe
cerc. Astfel permutările 1234, 2341, 3412 şi 4123 reprezintă una şi aceeaşi
configuraţie. Scrieţi un program care afişează numai permutările distincte
conform aşezării pe un cerc. Indicaţie: se va considera sol[1]=1 şi se vor
permuta doar celelalte elemente.
Manual de informatică pentru clasa a XI-a 101

4.3. Cazul în care se cere o singură soluţie.


Exemplificare: problema colorării hărţilor

Sunt probleme care se rezolvă cu metoda backtracking şi în care se cere o


singură soluţie.

Implementarea ”ca la carte“ presupune utilizarea unei variabile de


semnalizare (de exemplu, variabila gata) care să fie iniţial 0, la obţinerea soluţiei
dorite aceasta primind valoarea 1. Orice succesor va fi condiţionat în plus de
valoarea variabilei gata.

De această dată, pentru simplitate, vom renunţa la programarea structurată


şi vom opri în mod forţat programul.

 În Pascal, veţi utiliza procedura halt.

 În C++, veţi folosi funcţia exit, cu parametrul EXIT_SUCCESS


(o constantă). Pentru a o putea utiliza, trebuie să includeţi fişierul antet
“stdlib.h“:
#include<stdlib.h>.

În ambele cazuri, secvenţa care determină oprirea forţată este trecută


imediat după ce prima soluţie a fost afişată.

 Exerciţiu. Modificaţi programul care rezolvă problema celor n dame, astfel


încât acesta să afişeze o singură soluţie.

 Problema colorării hărţilor. Fiind dată o hartă cu n ţări, se cere o soluţie de


colorare a hărţii, utilizând cel mult 4 culori, astfel încât două ţări cu frontieră
comună să fie colorate diferit.

Este demonstrat faptul că sunt suficiente numai 4 culori pentru ca orice hartă
să poată fi colorată.

Pentru exemplificare, vom considera harta din figura 4.4., unde ţările sunt
numerotate cu cifre cuprinse între 1 şi 5.

O soluţie a acestei probleme este următoarea:


1
• ţara 1 - culoarea 1; 4
• ţara 2 - culoarea 2; 3
• ţara 3 - culoarea 1; 2
• ţara 4 - culoarea 3; 5
Figura 4.4.
• ţara 5 - culoarea 4.
102 Capitolul 4. Metoda backtracking

Harta este furnizată programului cu ajutorul unei matrice (tablou) An,n:

1, ţara i are frontiera comună cu ţara j


A(i, j) = 
0, altfel

Matricea A este simetrică. Pentru rezolvarea problemei se utilizează


vectorul sol, unde sol[k] reţine culoarea ataşată ţării k. Evident, orice soluţie
are exact n componente.

Varianta Pascal Varianta C++


var sol:array[1..9] of #include <iostream.h>
integer; #include <stdlib.h>
a:array[1..10,1..10] of int n,i,j,sol[10],a[10][10];
integer;
n,i,j:integer; int valid(int k)
{ for (int i=1;i<k;i++)
function
if (sol[k]==sol[i] &&
valid(k:integer):boolean;
a[i][k]==1) return 0;
begin
valid:=true; return 1;
for i:=1 to k-1 do }
if (sol[k]=sol[i]) void back(int k)
and (a[k,i]=1) { int i;
then valid:=false if (k==n+1)
end; { for (int i=1;i<=n;i++)
procedure back(k:integer); cout<<sol[i];
var i:integer; exit(EXIT_SUCCESS);
begin }
if k=n+1 then else
begin for (i=1;i<=4;i++)
for j:=1 to n do { sol[k]=i;
write(sol[j]); if (valid(k))
halt; back(k+1);
end }
else }
for i:=1 to n do
begin main()
sol[k]:=i; { cout<<"Numarul de tari=";
if valid(k) cin>>n;
then back(k+1) for (int i=1;i<=n;i++)
end; for (int j=1;j<=i-1;j++)
end; { cout<<"a["<<I
begin <<','<<j<<"]=";
write('Numarul de tari='); cin>>a[i][j];
readln(n); a[j][i]=a[i][j];
for i:=1 to n do }
for j:=1 to i-1 do back(1);
begin }
write('a[',i,',',j,']=');
readln(a[i,j]);
a[j,i]:=a[i,j]
end;
back(1)
end.
Manual de informatică pentru clasa a XI-a 103

 Exerciţii
1. Soluţia afişată este şi soluţia care utilizează un număr
minim de culori?
2. Dacă ţările din centrul figurii alăturate sunt numerotate
cu 1, 2, 3, 4, iar cele de la exterior cu 5 şi 6, care este
soluţia afişată de programul dat? Este acesta numărul
minim de culori necesare?
3. Câte culori sunt suficiente pentru colorarea unei hărţi
particulare în care orice ţară se învecinează cu cel Figura 4.5.
mult două ţări?
4. Daţi exemplu de particularitate pe care poate să o aibă o hartă pentru a fi
suficiente două culori pentru colorarea tuturor ţărilor?

4.4. Aplicaţii ale metodei backtracking în


combinatorică

4.4.1. O generalizare utilă

Acum, că am învăţat să generăm permutările mulţimii {1,2...n}, se pune


problema să vedem de ce este util acest algoritm. La ce foloseşte faptul că putem
aranja numerele {1,2...n} în toate modurile posibile?

Să observăm că acest algoritm poate fi folosit pentru a aranja oricare n


elemente distincte în toate modurile posibile.

Exemple

1. Se dă o mulţime alcătuită din n litere distincte. Se cer toate cuvintele care se


pot forma cu ele, astfel încât fiecare cuvânt să conţină n litere distincte. De
exemplu, dacă mulţimea este {a,b,c}, vom avea cuvintele: abc, acb, bac,
bca, cab şi cba.

2. Se dau numele a n persoane. Se cere să se afişeze toate modurile posibile în


care acestea se pot aşeza pe o bancă. De exemplu, dacă n=3, iar persoanele sunt
Ioana, Costel şi Mihaela, atunci soluţiile sunt:

Ioana Costel Mihaela;


Ioana Mihaela Costel;
Costel Ioana Mihaela;
...
104 Capitolul 4. Metoda backtracking

În astfel de cazuri, cele n elemente distincte se memorează într-un vector V,


aşa cum vedeţi mai jos:

a b c Ioana Costel Mihaela


1 2 3 1 2 3

Atunci când s-a generat o permutare, de exemplu 213, vom afişa


V[2]V[1]V[3], adică bac, în primul caz sau Costel Ioana Mihaela, în al
doilea caz.
Procedeul de mai sus poate fi folosit pentru oricare altă aplicaţie din
combinatorică, în probleme cum ar fi: generarea tuturor submulţimilor unei mulţimi,
generarea aranjamentelor, a combinărilor sau a tuturor părţilor unei mulţimi.

 Exerciţiu. Scrieţi programul care rezolvă exemplul 2.


Mulţimea permutărilor mulţimii {1,2,...n} reprezintă toate funcţiile
bijective f:{1,2,...,n}→{1,2,...,n}. De exemplu, dacă n=3,
permutarea 213 este funcţia f:{1,2,3}→{1,2,3} definită astfel:
f(1)=2; f(2)=1; f(3)=3.

4.4.2. Produs cartezian

 Enunţ. Se dau n mulţimi: A1, A2,... An, unde Ai={1,2,...,ki}, pentru


k=1,2,...,n. Se cere produsul cartezian al celor n mulţimi.

Exemplu: A1={1,2}, A2={1,2,3}, A3={1,2,3}.


A1×A2×A3={(1,1,1),(1,1,2),(1,1,3),(1,2,1),(1,2,2),(1,2,3),
(1,3,1),(1,3,2),(1,3,3),(2,1,1),(2,1,2),(2,1,3),(2,2,1),
(2,2,2),(2,2,3),(2,3,1),(2,3,2),(2,3,3)}.

 Rezolvare. De la început observăm că este necesar să afişăm toate soluţiile.


Să observăm că o soluţie este de forma x1,x2,...,xn, cu x1∈A1, x2∈A2, ...,
xn∈An. De aici rezultă necesitatea folosirii unui vector sol, cu n componente,
unde sol[1] va conţine numerele naturale între 1 şi k1, sol[2] va conţine
numerele naturale între 1 şi k2, ..., sol[n] va conţine numerele naturale între 1 şi
kn. Observăm că valorile care pot fi luate sol[1] sunt între 1 şi k1, valorile care
pot fi luate sol[2] sunt între 1 şi k2, ..., valorile care pot fi luate sol[n] sunt între
1 şi kn. Pentru a putea reţine aceste valori, vom utiliza un vector a, cu n
componente, unde a[1]= k1, a[2]= k2, …, a[n]= kn. Pentru exemplul dat,
vectorul a va reţine (2,3,3).
Important! Să observăm că orice valoare reţinută de sol[i] între 1 şi ki
îndeplineşte condiţiile de continuare (este validă). Din acest motiv, nu mai este
necesar să utilizăm subprogramul valid.
Manual de informatică pentru clasa a XI-a 105

Mai jos, puteţi observa programul care generează produsul cartezian al


mulţimilor date:

Varianta Pascal Varianta C++


var n,i:integer; #include <iostream.h>
sol,a:array[1..10] of int n, sol[10],a[10],i;
integer; void back(int k)
{ if (k==n+1)
procedure back(k:integer);
{ for (i=1;i<=n;i++)
begin
cout<<sol[i];
if k=n+1 then
cout<<endl; }
begin
else
for i:=1 to n do
{ sol[k]=0;
write(sol[i]);
while(sol[k]<a[k])
writeln
{ sol[k]++; back(k+1); }
end
}
}
else
begin main()
sol[k]:=0; {
while sol[k]<a[k] do cout<<"Numarul de multimi=";
begin cin>>n;
sol[k]:=sol[k]+1; for(int i=1;i<=n;i++)
back(k+1) { cout<<"a["<<i<<"]=";
end cin>>a[i];
end }
end; back(1);
}
begin
write('Numarul de multimi=');
readln(n);
for i:=1 to n do
begin
write('a[',i,']=');
readln(a[i])
end;
back(1)
end.

Observaţii

1. Avem k1×k2×...×kn elemente ale produsului cartezian. De aici rezultă că


algoritmul este exponenţial.

2. O altă interpretare pentru metoda backtracking: fiind date n mulţimi: A1, A2, ...,
An, produsul cartezian al lor A1×A2×...×An se mai numeşte spaţiul soluţiilor. În
acest context, metoda backtracking caută una sau toate soluţiile, care sunt
elemente ale produsului cartezian şi care îndeplinesc anumite condiţii. Astfel, se
poate justifica faptul că, în generarea produsului cartezian, nu este necesar
subprogramul valid pentru că se generează toate elementele produsului
cartezian, fără a verifica anumite condiţii.
106 Capitolul 4. Metoda backtracking

Deşi algoritmul este exponenţial, există aplicaţii utile, evident, atunci când
fiecare mulţime Ai poate lua numai câteva valori şi unde n este suficient de mic.

 Exerciţii
1. Tabelarea anumitor funcţii. Se dă funcţia

f:A1×A2×...×An→R,

unde fiecare mulţime Ai este dată de numerele întregi din intervalul [ai,bi] şi

f=c1x1+c2x2+...+cnxn, ci∈R.

Se cere să se realizeze un tabel, în care pentru fiecare valoare din


domeniul de definiţie să se afişeze valoarea funcţiei corespunzătoare acelei valori.

2. Scrieţi programul care generează toate ”cuvintele” cu patru litere care au prima
şi ultima literă vocale, litera a doua consoană din mulţimea {P, R, S, T}, iar a treia
literă consoană din mulţimea {B, M, R, T, V}.

3. Scrieţi programul care generează şi numără câte cuvinte de cinci litere ale
alfabetului englez se pot forma, cu condiţia să nu existe două consoane alăturate
şi nici două vocale alăturate.

4.4.3. Generarea tuturor submulţimilor unei mulţimi

 Enunţ. Fiind dată mulţimea A={1,2,...,n}, se cere să se afişeze toate


submulţimile ei.
 Rezolvare. Să ne amintim că submulţimile unei mulţimi A se pot reprezenta
prin vectorul caracteristic V, unde:

1, dacă i ∈ A
V[i] = 
0, dacă i ∉ A

De exemplu, dacă A={1,2,3}, pentru submulţimea {1,3} vom avea


V=(1,0,1). De aici, rezultă că problema se reduce la generarea tuturor valorilor
posibile pe care le poate reţine vectorul caracteristic.

Aceasta înseamnă că o soluţie este de forma x1,x2,...,xn, unde


xi∈{0,1}. Şi în acest caz, orice valoare ar reţine componenta i, ea nu trebuie să
îndeplinească nici o condiţie de continuare, motiv pentru care subprogramul valid
nu este necesar.

În continuare, puteţi observa programul care generează toate valorile pe


care le poate reţine vectorul caracteristic:
Manual de informatică pentru clasa a XI-a 107

Varianta Pascal Varianta C++


var n,i:integer; #include <iostream.h>
sol:array[1..10] of int n, sol[10],i;
integer;
void back(int k)
procedure back (k:integer); { if (k==n+1)
begin { for (i=1;i<=n;i++)
if k=n+1 then cout<<sol[i];
begin cout<<endl;
for i:=1 to n do }
write(sol[i]); else
writeln { sol[k]=-1;
end while(sol[k]<1)
else { sol[k]++;
begin back(k+1);
sol[k]:=-1; }
while sol[k]<1 do }
begin }
sol[k]:=sol[k]+1;
back(k+1) main()
end { cout<<"n="; cin>>n;
end back(1);
end; }

begin
write('n=');
readln(n);
back(1)
end.

 Exerciţii
1. Problema nu este rezolvată în totalitate. Programul afişează numai toate valorile
pe care le poate lua vectorul caracteristic. Completaţi-l astfel încât programul să
afişeze toate submulţimile mulţimii {1,2,...,n}!

2. Se citesc numele a 6 elevi. Afişaţi toate submulţimile mulţimii celor 6 elevi.

3. Să se afişeze toate numerele scrise în baza 10 a căror reprezentare în baza 2


are n cifre, dintre care exact k sunt egale cu 1. Valorile n şi k se citesc de la
tastatură (n<12, k<n). De exemplu, pentru n=3 şi k=2, se obţin valorile: 5 şi 6.

4. Realizaţi un program care generează combinaţii de n cfre 0 şi 1 cu proprietatea


că în orice grup de 3 cifre consecutive există cel puţin o cifră de 1. De exemplu,
dacă n=4, se afişează combinaţiile: 0010, 0011, 0100, 0101, 0110, 0111, 1001,
1010, 1011, 1100, 1101, 1110, 1111.

5. Se citesc două numere naturale n şi s (n<10, s<1000). Să se afişeze mulţimile


formate din n numere prime cu proprietatea că suma elementelor din fiecare
mulţime este exact s.
108 Capitolul 4. Metoda backtracking

Observaţii

 Fiind dată o mulţime cu n elemente, avem 2n submulţimi ale ei. Mulţimea


dată şi submulţimea vidă sunt submulţimi ale mulţimii date! De ce? Fiecare
componentă a vectorului caracteristic poate reţine două valori. Prin urmare,
numărul de submulţimi este

⋅ 2
2 ⋅ 2...2
=2 .
n

de n ori

De aici rezultă că algoritmul care generează toate submulţimile mulţimii 1,


2, ..., n este exponenţial.

 Uneori, veţi rezolva probleme în care se dă o mulţime şi se cere o submulţime


a sa care îndeplineşte anumite caracteristici. În anumite situaţii, problema se
poate rezolva prin utilizarea unor algoritmi mai rapizi (polinomiali). Greşeala
tipică care se face este că se generează toate submulţimile, după care se
selectează cea (cele) care îndeplineşte condiţiile date.

Exemplu. Se dă o mulţime de numere reale. Se cere să se determine o


submulţime a sa care are suma maximă. Problema se rezolvă uşor: se
consideră ca făcând parte din submulţime numai numerele pozitive. Altfel,
dacă am genera toate submulţimile...

4.4.4. Generarea combinărilor

Fiind dată mulţimea A={1,2,...,n}, se cer toate submulţimile ei cu p


elemente. Problema este cunoscută sub numele de “generarea combinărilor de
n, luate câte p”. Se ştie că numărul soluţiilor acestei probleme este

n!
C pn = .
(n − p)! p!

De exemplu, dacă n=4 şi p=3, soluţiile sunt următoarele:


{1,2,3}, {1,2,4}, {1,3,4} şi {2,3,4}.

 Enunţ. Se citesc n şi p numere naturale, n≥p. Se cere să se genereze toate


submulţimile cu p elemente ale mulţimii A={1,2,...,n}.

 Rezolvare. O soluţie este de forma x1,x2,...,xp, unde x1, x2, ..., xp∈A.
În plus, x1, x2, ..., xp trebuie să fie distincte. Cum la o mulţime ordinea elementelor
nu prezintă importanţă, putem genera elementele ei în ordine strict crescătoare.
Această observaţie ne ajută foarte mult în elaborarea algoritmului.
a) Pentru k>1, sol[k]>sol[k-1].
Manual de informatică pentru clasa a XI-a 109

b) Pentru fiecare k∈{1,2,...,p}, sol[k]≤n-p+k. Să presupunem, prin


absurd, că această ultimă relaţie nu este respectată. Aceasta înseamnă că ∃k,
astfel încât sol[k]>n-p+k. Deci:

sol[k+1]>n-p+k+1,
...
sol[p]>n-p+p=n.

Absurd. De aici rezultă că:

1≤sol[1]≤n-p+1,
sol[1]<sol[2]≤n-p+2,
...
sol[n-1]<sol[n]≤n-p+p=n.

Relaţiile de mai sus simplifică mult algoritmul, pentru că ţinând cont de ele, nu mai
este necesar să se testeze nici o condiţie de continuare.

Varianta Pascal Varianta C++


var sol:array[1..9] of integer; #include <iostream.h>
n,p:integer; int n,p,sol[10];
procedure back(k:integer); void back(int k)
var i:integer; { int i;
begin if (k==p+1)
if k=p+1 { for (i=1;i<=p;i++)
then cout<<sol[i];
begin cout<<endl;
for i:=1 to p do }
write(sol[i]); else
writeln; { if (k>1) sol[k]=sol[k-1];
end else sol[k]=0;
else while(sol[k]<n-p+k)
begin { sol[k]++;
if k>1 then sol[k]:=sol[k-1] back(k+1); }
else sol[k]:=0; }
while sol[k]<n-p+k do }
begin main()
sol[k]:=sol[k]+1; { cout<<"n="; cin>>n;
back(k+1); cout<<"p="; cin>>p;
end back(1);
end }
end;
begin
write('n=');
readln(n);
write ('p=');
readln(p);
back(1);
end.

Examinând raţionamentul propus putem observa că, în anumite cazuri,


analiza unei probleme conduce la un algoritm cu mult mai rapid.
110 Capitolul 4. Metoda backtracking

 Exerciţii
1. Se dau coordonatele din plan a n puncte. Afişaţi coordonatele vârfurilor tuturor
pătratelor care au ca vârfuri puncte din mulţimea considerată.

2. Se dau n substanţe chimice. Se ştie că, în anumite condiţii, unele substanţe intră
în reacţii chimice cu altele. Fiind date p perechi de forma (i,j) cu semnificaţia că
substanţa i intră în reacţie cu substanţa j, se cer toate grupurile de s<n substanţe
astfel încât oricare două substanţe din grup nu intră în reacţie.

4.4.5. Generarea aranjamentelor

Se dau două mulţimi A={1,2,...,p} şi B={1,2,...,n}. Se cer toate


funcţiile injective definite pe A cu valori în B. O astfel de problemă este una de
generare a aranjamentelor de n luate câte p ( A pn ).

Exemplu: p=2, n=3. Avem: 12, 21, 13, 31, 23, 32. De exemplu, 21 este funcţia
f:A→B dată astfel: f(1)=2; f(2)=1. Avem relaţiile:

n!
A pn = = n(n − 1)...(n − p + 1) .
(n − p)!

 Enunţ. Se citesc n şi p. Să se genereze toate aranjamentele de n luate câte p.


Să observăm că dacă se cunoaşte fiecare submulţime de p elemente a
mulţimii de n elemente, atunci aranjamentele se pot obţine permutând în toate
modurile posibile elementele unei astfel de mulţimi. Pornind de la această
observaţie, suntem tentaţi să generăm toate submulţimile cu p elemente ale
mulţimii cu n elemente şi, din fiecare astfel de submulţime, să obţinem permutările
ei. Exerciţiu!

Pe de altă parte, se poate lucra mult mai eficient. O soluţie este de forma:
x1x2...xp, unde x1, x2, ..., xp∈B. În plus, x1, x2, ..., xp trebuie să fie distincte.
Spre deosebire de algoritmul de generare a combinărilor, aici ne interesează toate
permutările unei soluţii (acestea sunt, la rândul lor, alte soluţii). Aceasta înseamnă
că nu mai putem pune în soluţie elementele în ordine crescătoare. Să recapitulăm:

- o soluţie are p numere din B;

- numerele trebuie să fie distincte.

Rezultă de aici că algoritmul este acelaşi de la permutări, diferenţa fiind dată


de faptul că soluţia are p numere, nu n ca în cazul permutărilor.
Manual de informatică pentru clasa a XI-a 111

Varianta Pascal Varianta C++


var sol:array[1..9]of integer; #include <iostream.h>
n,p:integer; int n,p,sol[10];
function int valid(int k)
valid(k:integer):boolean; { for (int i=1;i<k;i++)
var i:integer; if (sol[k]==sol[i])
begin return 0;
valid:=true; return 1;
for i:=1 to k-1 do }
if sol[k]=sol[i] then
valid:=false void back(int k)
end; { int i,j;
if (k==p+1)
procedure back(k:integer); { for (j=1;j<=p;j++)
var i,j:integer; cout<<sol[j];
begin cout<<endl;
if k=p+1 then }
begin else
for j:=1 to p do for (i=1;i<=n;i++)
write(sol[j]); { sol[k]=i;
writeln if (valid(k))
end back(k+1);
else }
for i:=1 to n do }
begin
main()
sol[k]:=i;
{ cin>>n;
if valid(k) then
cin>>p;
back(k+1)
back(1);
end
}
end;
begin
readln(n);
readln(p);
back(1)
end.

 Exerciţii
1. Se citesc n, p şi apoi n litere distincte. Afişaţi toate cuvintele care se pot forma
cu p dintre ele.

2. Se citesc n şi apoi numele mici a n persoane. Ştiind că toate numele care se


termină cu a reprezintă nume de fată, celelalte fiind nume de băieţi, să se afişeze
toate mulţimile de perechi fată-băiat care se pot forma. Două mulţimi sunt distincte
dacă cel puţin una dintre perechi diferă. De exemplu, pentru n=5, Maria, Ana,
Doina, Doru, Cosmin, se afişează mulţimile: {Maria-Doru, Ana-Cosmin},
{Ana-Cosmin, Maria-Doru}, {Maria-Doru, Doina-Cosmin}, {Doina-Doru,
Maria-Cosmin}, {Ana-Doru, Doina-Cosmin}, {Doina-Doru, Ana-Cosmin}.
112 Capitolul 4. Metoda backtracking

3. Cei n acţionari ai unei firme trebuie să organizeze un număr maxim de şedinţe


tip masă rotundă la care să participe exact p dintre ei. Ştiind că oricare două
şedinţe trebuie să difere fie prin acţionarii prezenţi, fie prin vecinii pe care îi au
aceştia la masă, stabiliţi numărul de şedinţe pe care le pot organiza. De exemplu,
dacă n=4 şi p=3, atunci sunt posibile 5 configuraţii diferite ale celor 3 acţionari
aşezaţi la masa rotundă: 1-2-3; 1-3-2; 1-3-4; 1-4-3; 2-3-4; 2-4-3
(configuraţiile 2-3-1 şi 3-1-2 nu se consideră, deoarece sunt echivalente, la
masa rotundă, cu configuraţia 1-2-3).

4.4.6. Generarea tuturor partiţiilor mulţimii {1, 2, ..., n}

Definiţia 4.1. Fie mulţimea A={1,2,...,n}. Se numeşte partiţie a


mulţimii A, un set de k≤n mulţimi care îndeplinesc condiţiile de mai jos:
a) A1∪A2∪...∪Ak=A;
b) Ai∩Aj=∅, ∀i≠j∈{1,2...n}.

Exemplu. Considerăm mulţimea A={1,2,3}. Avem partiţiile:


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

 Enunţ. Se citeşte un număr natural, n. Se cer toate partiţiile mulţimii


A={1,2,...,n}.

 Rezolvare. Chiar dacă ştim să generăm toate submulţimile unei mulţimi, tot nu ne
ajută să generăm toate partiţiile.

1. Pentru a putea genera toate partiţiile, trebuie să găsim o metodă prin care să
putem reţine o partiţie. O primă idee ne conduce la folosirea unui vector, sol, astfel:
dacă sol[i]=k, atunci elementul i se găseşte în mulţimea k a partiţiei. Totuşi, nu
ştim câte mulţimi sunt în partiţia respectivă. Există o partiţie care conţine n mulţimi
atunci când fiecare element este într-o mulţime şi una care conţine toate mulţimile,
adică tocmai mulţimea A. Cu alte cuvinte, numărul mulţimilor dintr-o partiţie este
între 1 şi n.

2. Pentru a avea o ordine în generarea soluţiilor, elementele mulţimii A trebuie să


aparţină de submulţimi consecutive ale partiţiei.

 Din acest motiv, sol[i] va lua valori între 1 şi


1+max{sol[1], sol[2], ..., sol[i-1]}.
Manual de informatică pentru clasa a XI-a 113

Prin această condiţie se evită situaţia în care, de exemplu, vectorul sol reţine
(1,3,1). Aceasta ar avea semnificaţia că elementele 1 şi 3 se găsesc în
submulţimea 1 a partiţiei, iar elementul 2 se găseşte în submulţimea 3 a partiţiei. În
acest caz, lipseşte submulţimea 2 a partiţiei.

Să exemplificăm funcţionarea algoritmului pentru cazul n=3:

- sol=(1,1,1) - A1={1,2,3);
- sol=(1,1,2) - A1={1,2} A2={3};
- sol=(1,2,1) - A1={1,3} A2={2};
- sol=(1,2,2) - A1={1} A2={2,3};
- sol=(1,2,3) - A1={1} A2={2} A3={3}.

Să observăm că nici în cazul acestei probleme nu trebuie să verificăm existenţa


anumitor condiţii de continuare.

Analizaţi programul astfel obţinut!

Varianta Pascal Varianta C++


var sol:array[0..10]of integer; #include <iostream.h>
n,i,j,maxim:integer; int n, sol[10],
max[10],i,j,maxim;
procedure tipar;
void tipar()
begin
{ maxim=1;
maxim:=1;
for (i=2;i<=n;i++)
for i:=2 to n do
if (maxim<sol[i])
if maxim<sol[i]
maxim=sol[i];
then maxim:=sol[i];
cout<<"Partitie "<<endl;
writeln('Partitie ');
for (i=1;i<=maxim;i++)
for i:=1 to maxim do
{ for (j=1; j<=n;j++)
begin
if (sol[j]==i)
for j:=1 to n do
cout<<j<<" ";
if sol[j]=i
cout<<endl;
then write (j,' ');
}
writeln;
}
end;
end; void back(int k)
{ int i,j,maxprec;
procedure back (k:integer); if (k==n+1) tipar();
var i,j,maxprec:integer; else
begin { maxprec=0;
if k=n+1 for (j=1;j<=k-1;j++)
then tipar if (maxprec<sol[j])
else maxprec=sol[j];
begin for (i=1;i<=maxprec+1;i++)
maxprec:=0; { sol[k]=i; max[k]=sol[k];
for j:=1 to k-1 do back(k+1);}
if maxprec<sol[j] then }
maxprec:=sol[j]; }
114 Capitolul 4. Metoda backtracking

for i:=1 to maxprec+1 do main()


begin { cout<<"n=";
sol[k]:=i; cin>>n;
back(k+1) back(1);
end; }
end
end;

begin
write('n='); readln(n);
back(1);
end.

 Exerciţiu. Puteţi arăta că oricărei partiţii îi aparţine un unic conţinut al


vectorului sol, obţinut ca în program?

Indicaţie. Observaţi că întotdeauna elementul 1 aparţine primei submulţimi a


partiţiei, elementul 2 poate aparţine submulţimilor 1 sau 2 ale partiţiei, ...,
elementul n poate aparţine submulţimilor, 1, 2 sau n ale partiţiei. Pornind de aici,
construiţi vectorul sol!

Ţinând cont de faptul că oricărei partiţii îi corespunde un unic conţinut al


vectorului sol şi oricărui conţinut al vectorului sol îi corespunde o unică
partiţie, am obţinut, practic, o funcţie bijectivă de la mulţimea partiţiilor
mulţimii A la mulţimea conţinuturilor generate de algoritm ale vectorului sol.
Pornind de la această bijecţie, în loc ca algoritmul să genereze partiţiile, el
va determina conţinuturile vectorului sol. Apoi, pentru fiecare conţinut al
vectorului sol, se obţine o partiţie.

 Exerciţiu. Modificaţi programul precedent pentru ca acesta să afişeze toate


partiţile care conţin exact 3 submulţimi.

4.5. Alte tipuri de probleme care se rezolvă prin


utilizarea metodei backtracking

4.5.1. Generalităţi

Toate problemele pe care le-am întâlnit până acum admit soluţii care
îndeplinesc următoarele caracteristici:
 soluţiile sunt sub formă de vector;
 toate soluţiile unei probleme au aceeaşi lungime, unde prin lungime
înţelegem numărul de componente ale vectorului soluţie.
Manual de informatică pentru clasa a XI-a 115

Exemple. Fie mulţimea A={1,2...n}. Atunci:

a) Toate permutările mulţimii A au lungimea n.

b) Toate submulţimile cu p elemente ale mulţimii A (generarea combinărilor) au


lungimea p.

c) Toate soluţiile sub formă de vector ale problemei generării tuturor partiţiilor
mulţimii A au lungimea n.

În realitate, cu ajutorul metodei backtracking se pot rezolva şi probleme care nu


îndeplinesc condiţiile de mai sus. Astfel, există probleme în care nu se cunoaşte de la
început lungimea soluţiei, există probleme care admit mai multe soluţii de lungimi
diferite, există probleme în care soluţia este sub forma unei matrice cu două sau trei
linii etc. Exemplele următoare vă vor convinge.

4.5.2. Generarea partiţiilor unui număr natural

 Enunţ. Se citeşte un număr natural n. Se cere să se tipărească toate modurile


de descompunere a lui ca sumă de numere naturale. De exemplu, pentru n=4,
avem: 4, 31, 22, 211, 13, 121, 112, 1111.

Ordinea numerelor din sumă este importantă. Astfel, se tipăreşte 112 dar
şi 211, 121.

 Rezolvare. De la început, observăm că nu se cunoaşte lungimea unei soluţii.


Ea poate fi cuprinsă între 1, în cazul în care numărul în sine constituie o
descompunere a sa şi n, atunci când numărul este descompus ca sumă a n
numere egale cu 1.

Trecem la stabilirea algoritmului pe care îl vom folosi.

1. Fiecare componentă a vectorului sol trebuie să reţină o valoare mai


mare sau egală cu 1.

2. Mai întâi să observăm că, în procesul de generare a soluţiilor, trebuie ca


în permanenţă să fie respectată relaţia

sol[1]+sol[2]+...sol[k]≤n.

3. Avem soluţie atunci când

sol[1]+sol[2]+...sol[k]=n.

Rezultă de aici că trebuie să cunoaştem, la fiecare pas k, suma


s= sol[1]+sol[2]+...sol[k-1].
116 Capitolul 4. Metoda backtracking

O primă posibilitate ar fi ca la fiecare pas să calculăm această sumă. Dar, se


poate lucra eficient. Suma va fi reţinută în permanenţă într-o variabilă globală,
numită s.
Mai jos, este prezentată funcţionarea algoritmului pentru n=4:

soluţie

1 0 0 0 1 1 0 0 1 1 1 0 1 1 1 1
s=0, k=1 s=1, k=2 s=2, k=3 s=3, k=4

soluţie soluţie soluţie

1 1 2 1 2 0 0 1 2 1 1 3
s=2, k=3 s=1, k=2 s=3, k=3 s=1, k=2

Observaţi modul în care calculăm suma la fiecare pas. De câte ori se trece
la componenta următoare (k+1), la s se adună sol[k], de câte ori se face
pasul înapoi (se trece la componenta k-1), din s se scade sol[k].

Programul este prezentat în continuare:

Varianta Pascal Varianta C++


var sol:array[1..100] of integer; #include <iostream.h>
n,i,s:integer; int sol[100], n,i,s;
procedure back (k:integer); void back(int k)
begin { if (s==n)
if s=n then begin { for (i=1;i<=k-1;i++)
for i:=1 to k-1 do cout<<sol[i];
write(sol[i]); cout<<endl;
writeln; }
end else
else begin { sol[k]=0;
sol[k]:=0; while (sol[k]+s<n)
while sol[k]+s<n do { sol[k]++;
begin s+=sol[k];
sol[k]:=sol[k]+1; back(k+1);
s:=s+sol[k]; back(k+1); s-=sol[k];
s:=s-sol[k] }
end; }
end }
end;
main()
begin { cout<<"n="; cin>>n;
write('n='); readln(n); back(1);
back(1) }
end.
Manual de informatică pentru clasa a XI-a 117

 Exerciţii
1. Cum trebuie procedat în cazul în care se cere ca soluţiile să fie afişate o singură
dată? Spre exemplu, dacă s-a afişat descompunerea 1,1,2 să nu se mai afişeze
2,1,1 sau 1,2,1?
Indicaţie: procedeul a mai fost întâlnit, de exemplu la generarea combinărilor.
Soluţiile se vor genera în ordine crescătoare. Modificaţi programul în acest sens.
2. Adaptaţi metoda de rezolvare astfel încât să se genereze numai partiţiile formate
din numere naturale distincte.
3. Adaptaţi metoda de rezolvare astfel încât să se genereze numai partiţiile formate
din cel puţin p numere naturale distincte (n şi p citite de la tastatură).
4. Adaptaţi metoda de rezolvare astfel încât să se genereze numai partiţiile formate
din numere naturale aflate în intervalul [a,b] (n, a şi b citite de la tastatură).
5. Rezolvaţi problema scrierii numărului natural n ca sumă de numere naturale
alese dintr-o mulţime formată din k valori date {v1, v2, …, vk}. Astfel, 10 se
poate scrie ca sumă de numere alese din mulţimea {2,3,6} în felul următor:
10=2+2+2+2+2, 10=2+2+3+3, 10=2+2+6.

4.5.3. Plata unei sume cu bancnote de valori date

 Enunţ. Se dau suma s şi n tipuri de monede având valori de a1,a2,...,an lei. Se


cer toate modalităţile de plată a sumei s utilizând aceste monede. Se presupune
că se dispune de un număr nelimitat de exemplare din fiecare tip de monedă.

Iată soluţiile pentru Suma=5, n=3 (trei tipuri de monede) cu valorile 1, 2, 3:

1) 1 de 2, 1 de 3; 2) 1 de 1, 2 de 2; 3) 2 de 1, 1 de 3;
4) 3 de 1, 1 de 2; 5) 5 de 1;

 Rezolvare. Valorile celor n monede sunt reţinute de vectorul a. Astfel, a[1] va


reţine valoarea monedei de tipul 1, a[2] valoarea monedei de tipul 2, ş.a.m.d.
Numărul de monede din fiecare tip va fi reţinut de vectorul sol. Astfel, sol[1] va
reţine numărul de monede de tipul 1, sol[2] va reţine
numărul de monede de tipul 2, ş.a.m.d. În aceste condiţii, o sol 2 0 1
soluţie pentru exemplul anterior arată ca alăturat, unde suma 5 a 1 2 3
se formează cu două monede de 1 şi o monedă de 3.

Ce observăm?
118 Capitolul 4. Metoda backtracking

1. Există componente ale vectorului sol care reţin 0. Această situaţie corespunde
cazului în care moneda respectivă nu este luată în calcul. Din acest motiv, fiecare
componentă a vectorului sol va fi iniţializată cu o valoare aflată înaintea tuturor celor
posibile, adică cu -1.

2. Orice soluţie are exact n componente (n este numărul de tipuri de monede).


Acest număr include toate monedele, chiar şi cele care nu sunt luate în calcul.
3. Ca şi la problema anterioară, vom reţine în permanenţă suma obţinută la un
moment dat. Astfel, la pasul k, avem la dispoziţie suma:
s = a[1]*sol[1] + a[2]*sol[2] + ... + a[k-1]*sol[k-1]

4. Avem soluţie dacă:


s = a[1]*sol[1] + a[2]*sol[2] + ... + a[n]*sol[n] = Suma

 Exerciţiu. Încercaţi să arătaţi, prin desene, ca la problema anterioară, modul


de obţinere a tuturor soluţiilor pentru Suma=5 şi n=3.

În continuare, este prezentat programul:

Varianta Pascal Varianta C++


var sol,a:array[1..100] #include <iostream.h>
of integer; int sol[100], a[100],
n,i,s,Suma:integer; n,i,s,Suma;
procedure back (k:integer); void back(int k)
begin { if (s==Suma)
if s=suma then { cout<<"Solutie "<<endl;
begin for(i=1;i<=k-1;i++)
writeln('Solutie'); if(sol[i])
for i:=1 to k-1 do cout<<sol[i]<<"monede de "
if sol[i]<>0 then <<a[i]<<endl;
writeln (sol[i],' monede cout<<endl;
de ',a[i]); }
writeln; else
end { sol[k]=-1;
else while(sol[k]*a[k]+s<Suma
begin && k<n+1)
sol[k]:=-1; {
while (sol[k]*a[k]+s<Suma) sol[k]++;
and (k<n+1) do s+=sol[k]*a[k];
begin back(k+1);
sol[k]:=sol[k]+1; s-=sol[k]*a[k];
s:=s+sol[k]*a[k]; }
back(k+1); }
s:=s-sol[k]*a[k] }
end;
main()
end
{ cout<<"suma="; cin>>Suma;
end;
cout<<"n="; cin>>n;
Manual de informatică pentru clasa a XI-a 119

begin for (i=1;i<=n;i++)


write ('suma='); { cout<<"a["<<i<<"]=";
readln(suma); cin>>a[i];
write('n='); readln(n); }
for i:=1 to n do back(1);
begin }
write ('a[',i,']=');
readln(a[i]);
end;
back(1)
end.

 Exerciţiu. Adaptaţi rezolvarea problemei plăţii unei sumei cu bancnote date,


cunoscând, în plus, pentru fiecare valoare ai numărul limită bi de bancnote cu
valoarea respectivă disponibile. Astfel, pentru s=100, a=(2,5,50), b=(10,6,3),
varinata s=10x5+1x50 nu corespunde cerinţei deoarece nu avem la dispoziţie 10
monede de 5, ci doar 6.

4.5.4. Problema labirintului

 Enunţ. Se dă un labirint sub formă de matrice cu m linii şi n coloane. Fiecare


element al matricei reprezintă o cameră a labirintului. Într-una din camere, de
coordonate lin şi col, se găseşte un om. Se cere să se găsească toate ieşirile
din labirint. Nu este permis ca un drum să treacă de două ori prin aceeaşi cameră.

O primă problemă care se pune este precizarea modului de codificare a


ieşirilor din fiecare cameră a labirintului.

Fie l(i,j) un element al matricei. Acesta poate lua valori între 0 şi 15. Se
consideră ieşirile spre nord, est, sud şi vest, luate în această ordine. Pentru fiecare
direcţie cu ieşire se reţine 1, iar în caz contrar, se reţine 0. Un şir de patru cifre 1
sau 0 formează un număr în baza 2. Acest număr este convertit în baza 10 şi
reţinut în l(i,j). De exemplu, pentru o cameră care are ieşire în nord şi vest,
avem 1001(2)=9(10).

Exemplu. Alăturat este prezentat un labirint. Acolo


15 11 10 14
unde nu este permisă trecerea dintr-o cameră în alta, se
marchează cu o linie oblică. De asemenea, matricea 11 12 11 12
reţine şi valorile corespunzătoare ieşirilor, aşa cum sunt
11 6 15 7
ele cerute de program.

 Rezolvare
1. O cameră vizitată se reţine prin coordonatele ei: lin (linia) şi col(colana). Din
acest motiv, pentru a reţine un traseu vom utiliza o matrice cu două coloane şi mai
multe linii: sol. De exemplu, dacă camera iniţială este cea de coordonate (2,2)
o soluţie este (2,2), (2,3), (1,3).
120 Capitolul 4. Metoda backtracking

2. Nu toate soluţiile au aceeaşi lungime, întrucât există trasee de lungime diferită.


Se obţine o soluţie atunci când coordonatele camerei unde s-a intrat sunt în afara
matricei (nu au linia între 1 şi m şi nu au coloana între 1 şi n). Evident, atunci când
s-a găsit o situaţie, aceasta se afişează.
3. Spunem că o cameră este accesibilă dacă există intrare din camera curentă
către ea. Atenţie la modul (vedeţi programul) în care testăm dacă o cameră este
accesibilă sau nu. Este o operaţie în care se testează conţinutul unui anumit bit.
Acesta se obţine efectuând un ŞI logic între două valori. De exemplu, dacă testăm
ieşirea spre sud, atunci efectuăm ŞI logic între 0010(2)=2(10) şi valoarea reţinută
în matrice pentru camera curentă. Dacă valoarea obţinută este diferită de 0, atunci
avem ieşire din camera curentă către sud.
4. Înainte de a intra într-o cameră accesibilă se testează dacă respectiva cameră
a mai fost vizitată sau nu. Pentru aceasta utilizăm funcţia vizitat. În caz că a
fost vizitată, se face pasul înapoi.
Analizaţi programul:

Varianta Pascal Varianta C++


var sol:array [1..100,1..2] of #include <iostream.h>
integer; int sol[100][2],l[10][10],
l:array [0..10,0..10] of m,n,i,j,lin,col;
integer;
int vizitat(int k,int lin,
m,n,i,j,lin,col:integer;
int col)
function vizitat(k,lin, { int v=0;
col:integer):boolean; for (i=1;i<=k;i++)
begin if (sol[i][0]==lin &&
vizitat:=false; sol[i][1]==col) v=1;
for i:=1 to k-1 do return v;
if (sol[i,1]=lin) and }
(sol[i,2]=col)
void tipar(int k,int lin,
then vizitat:=true;
int col)
end;
{ cout <<" Solutie "<<endl;
procedure for (i=1;i<=k-1;i++)
tipar(k,lin,col:integer); cout<<sol[i][0]<<" "
begin <<sol[i][1]<<endl;
writeln('Solutie'); if (lin==0)
for i:=1 to k-1 do cout<<"iesire prin nord"
writeln(sol[i,1],' ', <<endl;
sol[i,2]); else
if lin=0 then if (lin==m+1)
writeln('iesire prin nord') cout<<"iesire prin sud"
else <<endl;
if lin=m+1 then else
writeln('iesire prin sud') if (col==0)
else cout<<"iesire prin vest"
if col=0 then <<endl;
writeln('iesire prin vest') else
else cout<<"iesire prin est"
writeln('iesire prin est'); <<endl;
readln; }
end;
Manual de informatică pentru clasa a XI-a 121

procedure void back(int k, int lin,


back(k,lin,col:integer); int col)
var i:integer; { if (lin==0 || lin==m+1 ||
begin col==0 || col==n+1)
if (lin=0) or (lin=m+1) or tipar(k,lin,col);
(col=0) or (col=n+1) else
then tipar(k,lin,col) {
else sol[k][0]=lin;
begin sol[k][1]=col;
sol[k,1]:=lin; for (int i=1;i<=4;i++)
sol[k,2]:=col; switch(i)
for i:=1 to 4 do
{
case i of
case 1:
1:if (l[lin,col]
if (l[lin][col] & 8 &&
and 8<>0) and not
vizitat(k,lin-1,col) ! vizitat(k, lin-1,col))
then back(k+1,lin-1,col);
back(k+1,lin-1,col); break;
2:if (l[lin,col] case 2:
and 4<>0) and not if (l[lin][col] & 4 &&
vizitat(k,lin,col+1) ! vizitat(k, lin,col+1))
then back(k+1,lin,col+1);
back(k+1,lin,col+1); break;
3:if (l[lin,col] case 3:
and 2<>0) and not if (l[lin][col] & 2 &&
vizitat(k,lin+1,col) ! vizitat(k, lin+1,col))
then back(k+1,lin+1,col);
back(k+1,lin+1,col); break;
4:if (l[lin,col] case 4:
and 1<>0) and not if (l[lin][col] & 1 &&
vizitat(k,lin,col-1) ! vizitat(k, lin,col-1))
then back(k+1,lin,col-1);
back(k+1,lin,col-1) break;
end; {case} }
end }
end; }
begin main()
write('M='); { cout<<"M=";
readln(m); cin>>m;
write('N='); cout<<"N=";
readln(n); cin>>n;
for i:=1 to m do for (i=1;i<=m;i++)
for j:=1 to n do for(j=1;j<=n;j++)
begin { cout<<"l["<<i<<","
write('l[',i,',',j,']='); <<j<<"]=";
readln(l[i,j]) cin>>l[i][j];
end; }
write('lin='); cout<<"Linie=";
readln(lin); cin>>lin;
write('col='); cout<<"Coloana= ";
readln(col); cin>>col;
back(1,lin,col) back(1,lin,col);
end. }
122 Capitolul 4. Metoda backtracking

Intrebare. Cum s-ar putea găsi un drum de lungime minimă de la camera


iniţială către ieşirea din labirint? Prima idee care ne vine în minte este să
generăm toate ieşirile, ca în program, pentru a o putea depista pe cea de lungime
minimă. Ei bine, răspunsul nu este satisfăcător. Nu uitaţi, tehnica backtracking
necesită un timp exponenţial. Problema se poate rezolva cu mult mai eficient. Dar,
pentru asta, trebuie să studiem teoria grafurilor. Toate la timpul lor...

 Exerciţii
1. Adaptaţi rezolvarea pentru un labirint în care fiecare căsuţă reţine valoarea 1
sau 0 (1 semnificând căsuţă plină, prin care nu se poate trece, iar 0 căsuţă liberă,
pe unde se poate trece). Ca şi în problema prezentată, deplasarea se poate face
dintr-o căsuţă în orice altă căsuţă alăturată, orizontal sau vertical, cu condiţia ca ea
să existe şi să fie liberă. Validaţi poziţia iniţială a omului (lin, col), astfel încât
aceasta să corespundă unei căsuţe libere. Estimaţi spaţiul de memorie utilizat
în această variantă.

2. Realizaţi o variantă a rezolvării de la 1, adăugând câte o linie sau coloană


suplimentară pe fiecare margine a labirintului, astfel încât să nu se mai testeze ca
celula în care se trece să existe. Plasarea într-o celulă de pe margine este
echivalentă cu obţinerea unei soluţii.

4.5.5. Problema bilei

 Enunţ. Se dă un teren sub formă de matrice cu m linii şi n coloane. Fiecare


element al matricei reprezintă un subteren cu o anumită altitudine dată de valoarea
reţinută de element (număr natural). Într-un astfel de subteren, de coordonate
(lin,col) se găseşte o bilă. Ştiind că bila se poate deplasa în orice porţiune de
teren aflată la nord, est, sud sau vest, de altitudine strict inferioară porţiunii pe care
se găseşte bila. Se cere să se găsească toate posibilităţile ca bila să părăsească
terenul.
6 8 9 3
Exemplu. Fie terenul alăturat. Iniţial, bila se află în subterenul
de coordonate (2,2). O posibilitate de ieşire din teren este dată 9 7 6 3
de drumul: (2,2), (2,3), (3,3), (3,4). În program, 5 8 5 4
altitudinile subterenului vor fi reţinute de matricea t.
8 3 7 1

 Analiza problemei şi rezolvarea ei. Problema seamănă cu cea anterioară,


deci putem gândi că dacă înlocuim testul de intrare într-o cameră cu cel de
altitudine mai mică, am rezolvat-o! Este adevărat, se obţine o rezolvare, dar se
poate şi mai uşor. Să analizăm: mai este necesar să testăm dacă bila nu a ajuns
pe un teren pe unde a mai trecut? Nu, deoarece, la fiecare pas, bila se deplasează
pe un teren de altitudine strict inferioară. Prin urmare, problema este mai uşoară
decât precedenta.
Manual de informatică pentru clasa a XI-a 123

În rest, vom propune o rezolvare în care sol este o matrice cu 3 coloane şi


un număr mare de linii. Astfel, sol(k,1) va reţine direcţia în care pleacă bila (1 pt
nord, 2 pentru est, 3 pentru sud şi 4 pentru vest), sol(k,2) va reţine linia
subterenului, iar sol(k,3) va reţine coloana subterenului.

 Exerciţiu. Arătaţi, prin reprezentare grafică, modul de funcţionare a


algoritmului, pe baza structurii de date propusă.

Varianta Pascal Varianta C++


var sol:array [1..100,1..3] #include <iostream.h>
of integer; int sol[100][3],t[10][10],
t:array [0..10,0..10] m,n,i,j,lin,col;
of integer;
m,n,i,j,lin,col:integer; void tipar(int k)
{ cout<<"Solutie "<<endl;
procedure tipar(k:integer); for(i=1;i<=k-1;i++)
begin cout<<sol[i][1]<<" "
writeln('Solutie'); <<sol[i][2]<<endl;
for i:=1 to k-1 do }
writeln(sol[i,2],' ',
sol[i,3]); void back(int k, int lin, int col)
end; { if (lin==0 || lin==m+1 ||
procedure col==0 || col==n+1)
back(k,lin,col:integer); tipar(k);
begin else
if (lin=0) or (lin=m+1) or { sol[k][0]=0;
(col=0) or (col=n+1) sol[k][1]=lin;
then tipar(k) sol[k][2]=col;
else begin while (sol[k][0]<4)
sol[k,1]:=0; {
sol[k,2]:=lin; sol[k][0]++;
sol[k,3]:=col; switch(sol[k][0])
while sol[k,1]<4 do {
begin case 1:
sol[k,1]:=sol[k,1]+1; if(t[lin-1][col]<t[lin][col])
case sol[k,1] of back(k+1,lin-1,col); break;
1:if t[lin-1,col]< case 2:
t[lin,col] then if(t[lin][col+1]<t[lin][col])
back(k+1,lin-1,col); back(k+1,lin,col+1); break;
2:if t[lin,col+1]< case 3:
t[lin,col] then if(t[lin+1][col]<t[lin][col])
back(k+1,lin,col+1); back(k+1,lin+1,col); break;
3:if t[lin+1,col]< case 4:
t[lin,col] then if(t[lin][col-1]<t[lin][col])
back(k+1,lin+1,col); back(k+1,lin,col-1); break;
4:if t[lin,col-1]< }
t[lin,col] then }
back(k+1,lin,col-1);
}
end; {case}
}
end end end;
begin main()
write('M='); readln(m); { cout<<"m="; cin>>m;
write('N='); readln(n); cout<<"n="; cin>>n;
124 Capitolul 4. Metoda backtracking

for i:=1 to m do for (i=1;i<=m;i++)


for j:=1 to n do for (j=1;j<=n;j++)
begin { cout<<"t["<<i<<","<<j<<"]=";
write('t[',i,',',j, cin>>t[i][j];
']='); }
readln(t[i,j]) cout<<"lin="; cin>>lin;
end; cout<<"col="; cin>>col;
write('lin=');readln(lin); back(1,lin,col);
write('col=');readln(col); }
back(1,lin,col)
end.

 Exerciţiu. Modificaţi programul astfel încât să se afişeze şi direcţia în care


bila părăseşte terenul.

4.5.6. Săritura calului

 Enunţ. Se consideră o tablă de şah nxn şi un cal plasat  1 16 11 20 3 


 
în colţul din stânga, sus. Se cere să se afişeze o posibilitate  10 21 2 17 12 
de mutare a acestei piese de şah, astfel încât să treacă o  15 24 19 4 7 
 
singură dată prin fiecare pătrat al tablei. Alăturat, observaţi o  22 9 6 13 18 
soluţie pentru n=5.  25 14 23 8 5 

 Analiza problemei şi rezolvarea ei. Fiind dată o poziţie în care se găseşte
calul, acesta poate sări în alte 8 poziţii. Pentru a scrie mai puţin cod, cele 8 poziţii
sunt reţinute de vectorii constanţi x şi y. Astfel, dacă lin şi col sunt coordonatele
poziţiei unde se găseşte calul, acesta poate fi mutat în:
(lin+x[0],col+y[0])...(lin+x[7],col+y[7]).

Reţineţi acest procedeu! De altfel, acesta poate fi folosit pentru problemele


deja prezentate. Matricea t reţine poziţiile pe unde a trecut calul. În rest, s-a
procedat ca la problema anterioară.

Varianta Pascal Varianta C++


const x:array[1..8] of #include <iostream.h>
integer=(-1,1,2,2,1,-1,-2,-2);
y:array[1..8] of #include <stdlib.h>
integer=(2,2,1,-1,-2,-2,-1,1); const int x[8]={-1,1,2,2,1,-1,
var n:integer; -2,-2};
st: array[1..1000,1..2] of
integer; const int y[8]={2,2,1,-1,-2,-2,
-1,1};
t: array[-1..25,-1..25] of
integer; int n,sol[1000][2],t[25][25];
procedure back(k,lin, void back(int k,int lin,
col:integer); int col)
var i,linie,coloana:integer; { int linie,coloana,i;
Manual de informatică pentru clasa a XI-a 125

begin if (k==n*n)
if k=n*n then { for (i=1;i<=k-1;i++)
begin cout<<sol[i][0]<<" "
for i:=1 to k-1 do <<sol[i][1]<<endl;
writeln(st[i,1],' ', cout<<lin<<" "<<col;
st[i,2]); exit(EXIT_SUCCESS);
writeln(lin,' ',col); }
halt; else
end { sol[k][0]=lin;
else sol[k][1]=col;
begin for (i=0;i<=7;i++)
st[k,1]:=lin; st[k,2]:=col; { linie=lin+x[i];
for i:=1 to 8 do coloana=col+y[i];
begin if (linie<=n && linie>=1
linie:=lin+x[i]; && coloana<=n &&
coloana:=col+y[i]; coloana>=1 &&
if (linie<=n) and t[linie][coloana]==0)
(linie>=1) and {
(coloana<=n) and t[linie][coloana]=1;
(coloana>=1) and back(k+1,linie,coloana);
(t[linie,coloana]=0) t[linie][coloana]=0;
then begin
}
t[linie,coloana]:=1;
}
back(k+1,linie,
}
coloana);
}
t[linie,coloana]:=0;
end;
main()
end
{ cout<<"n=";
end
cin>>n;
end;
back(1,1,1);
begin }
write ('n='); readln(n);
back(1,1,1);
end.

Probleme propuse
1. Avem la dispoziţie 6 culori: alb, galben, roşu, verde, albastru şi negru. Să se
precizeze toate drapelele tricolore care se pot proiecta, ştiind că trebuie
respectate regulile:
 orice drapel are culoarea din mijloc galben sau verde;
 cele trei culori de pe drapel sunt distincte.
2. Dintr-un grup de n persoane, dintre care p femei, trebuie formată o delegaţie
de k persoane, din care l femei. Să se precizeze toate delegaţiile care se
pot forma.
3. La o masă rotundă se aşează n persoane. Fiecare persoană reprezintă o
firmă. Se dau k perechi de persoane care aparţin unor firme concurente. Se
cere să se determine toate modalităţile de aşezare la masă a persoanelor,
astfel încât să nu stea alături două persoane de la firme concurente.
126 Capitolul 4. Metoda backtracking

4. Se dă o permutare a primelor n numere naturale. Se cer toate permutările care


se pot obţine din aceasta astfel încât nici o succesiune de două numere,
existentă în permutarea iniţială, să nu mai existe în noile permutări.
5. Se cer toate soluţiile de aşezare în linie a m câini şi n pisici astfel încât să nu
existe o pisică aşezată între doi câini.
6. Anagrame. Se citeşte un cuvânt cu n litere. Se cere să se tipărească toate
anagramele cuvântului citit. Se poate folosi algoritmul pentru generarea
permutărilor?
7. Se dau primele n numere naturale. Dispunem de un algoritm de generare a
combinărilor de n elemente luate câte p pentru ele. Se consideră un vector cu
n componente şiruri de caractere, unde, fiecare şir reprezintă numele unei
persoane. Cum adaptaţi algoritmul de care dispuneţi pentru a obţine
combinările de n persoane luate câte p?
8. Se citesc n numere naturale distincte. Se cere o submulţime cu p elemente
astfel încât suma elementelor sale să fie maximă în raport cu toate
submulţimile cu acelaşi număr de elemente.
9. Să se determine 5 numere de câte n cifre, fiecare cifră putând fi 1 sau 2, astfel
încât oricare dintre aceste 5 numere să coincidă exact în m poziţii şi să nu
existe o poziţie în care să apară aceeaşi cifră în toate cele 5 numere.
10. Fiind dat un număr natural pozitiv n, se cere să se producă la ieşire toate
descompunerile sale ca sumă de numere prime.
11. “Attila şi regele”. Un cal şi un rege se află pe o tablă de şah. Unele câmpuri
sunt “arse“, poziţiile lor fiind cunoscute. Calul nu poate călca pe câmpuri “arse“,
iar orice mişcare a calului face ca respectivul câmp să devină “ars“. Să se afle
dacă există o succesiune de mutări permise (cu restricţiile de mai sus), prin
care calul să poată ajunge la rege şi să revină la poziţia iniţială. Poziţia iniţială
a calului, precum şi poziţia regelui sunt considerate “nearse“.
12. Se dau n puncte în plan prin coordonatele lor. Se cer toate soluţiile de unire a
acestor puncte prin exact p drepte, astfel încât mulţimea punctelor de
intersecţie ale acestor drepte să fie inclusă în mulţimea celor n puncte.
13. Găsiţi toate soluţiile naturale ale ecuaţiei 3x+y+4xz=100.

14. Să se ordoneze în toate modurile posibile elementele mulţimii {1,2,...,n}


astfel încât numerele i, i+l, ..., i+k să fie unul după celălalt şi în această
ordine (l=1,i+k≤n).

15. Se consideră o mulţime de n elemente şi un număr natural k nenul. Să se


calculeze câte submulţimi cu k elemente satisfac, pe rând, condiţiile de mai jos
şi să se afişeze aceste submulţimi.
 conţin p obiecte date;
 nu conţin nici unul din q obiecte date;
 conţin exact un obiect dat, dar nu conţin un altul;
 conţin exact un obiect din p obiecte date;
Manual de informatică pentru clasa a XI-a 127

 conţin cel puţin un obiect din p obiecte date;


 conţin r obiecte din p obiecte date, dar nu conţin alte q obiecte date.

16. Se dă un număr natural par N. Să se determine toate şirurile de N paranteze


care se închid corect.
Exemplu: pentru N=6: ((( ))),(()()),()()(),()(()),(())().

17. Se dau N puncte albe şi N puncte negre în plan, de coordonate întregi. Fiecare
punct alb se uneşte cu câte un punct negru, astfel încât din fiecare punct, fie el
alb sau negru, pleacă exact un segment. Să se determine o astfel de
configuraţie de segmente astfel încât oricare două segmente să nu se
intersecteze. Se citesc N perechi de coordonate corespunzând punctelor albe
şi N perechi de coordonate corespunzând punctelor negre.
18. Să se genereze toate permutările de N cu proprietatea că oricare ar fi 2≤i≤N,
există 1≤j≤i astfel încâtV(i)-V(j)=1. Exemplu: pentru N=4, permutările
cu proprietatea de mai sus sunt:
2134, 2314, 3214, 2341, 3241, 3421, 4321.

19. O trupă cu N actori îşi propune să joace o piesă cu A acte astfel încât:

 oricare două acte să aibă distribuţie diferită;


 în orice act există, evident, cel puţin un actor;
 de la un act la altul, vine un actor pe scena sau pleacă un actor de pe
scenă (distribuţia a două acte consecutive diferă prin exact un actor).

Să se furnizeze o soluţie, dacă există vreuna.


20. Fiind dat un număr natural N şi un vector V cu N componente întregi, se cere:

 să se determine toate subşirurile crescătoare de lungime [N/5];


 să se calculeze p(1)+p(2)+...+p(k), unde p(k) reprezintă numărul
subşirurilor crescătoare de lungime k.

21. Pe malul unei ape se găsesc c canibali şi m misionari. Ei urmează să treacă


apa şi au la dispoziţie o barcă cu 2 locuri. Se ştie că, dacă atât pe un mal, cât
şi pe celălalt avem mai mulţi canibali decât misionari, misionarii sunt mâncaţi
de canibali. Se cere să se scrie un program care să furnizeze toate soluţiile de
trecere a apei, astfel încât să nu fie mâncat nici un misionar.

22. Se dă un careu sub formă de matrice cu m linii şi n coloane. Elementele


careului sunt litere. Se dă, de asemenea, un cuvânt. Se cere să se găsească
în careu prefixul de lungime maximă al cuvântului respectiv. Regula de căutare
este următoarea:

a) se caută litera de început a cuvântului;


b) litera următoare se caută printre cele 4 elemente învecinate cu elementul
care conţine litera de început, apoi printre cele 4 elemente învecinate cu
elementul care conţine noua literă, ş.a.m.d.
128 Capitolul 4. Metoda backtracking

23. Se dau coordonatele a n puncte din plan. Se cere să se precizeze 3 puncte


care determină un triunghi de arie maximă. Ce algoritm vom folosi?
a) Generarea aranjamentelor; b) Generarea combinărilor;
c) Generarea permutărilor; d) Generarea tuturor submulţimilor.

24. Fiind date numele a n soldaţi, ce algoritm vom utiliza pentru a lista toate
grupele de câte k soldaţi? Se ştie că într-o grupă, ordinea prezintă importanţă.
a) Generarea aranjamentelor; b) Generarea combinărilor;
c) Generarea permutărilor; d) Generarea tuturor partiţiilor.
25. Fiind date n numere naturale, ce algoritm vom utiliza pentru a determina
eficient o submulţime maximală de numere naturale distincte?

a) se generează toate submulţimile şi se determină o submulţime maximală


care îndeplineşte condiţia cerută;
b) se generează toate partiţiile şi se caută o submulţime maximală care
aparţine unei partiţii oarecare şi care îndeplineşte condiţia cerută;
c) se compară primul număr cu al doilea, al treilea, al n-lea, al doilea cu al
treilea, al patrulea, al n-lea, ... şi atunci când se găseşte egalitate se elimină un
număr dintre ele.
d) nici un algoritm dintre cei de mai sus.

26. Dispunem de un algoritm care generează permutările prin backtracking.


Primele două permutări afişate sunt: 321, 312. Care este următoarea
permutare care va fi afişată?
a) 321; b) 123; c) 213; d) 231.

Indicaţii
6. Deşi algoritmul este asemănător, nu este acelaşi, trebuie pusă o condiţie
suplimentară. De exemplu, în cuvântul “mama” nu se poate inversa a de pe poziţia
2 cu a de pe poziţia 4.
7. Dacă vectorul care reţine numele persoanelor este V, în loc să se afişeze i, se
va afişa V[i].
23. b). 24. a). 25. d).
Explicaţie: primele două variante prezintă soluţii exponenţiale, a treia este în
O(n2). Dar dacă sortăm numerele, atunci le putem afişa pe cele distincte dintr-o
singură parcurgere. Sortarea se poate efectua în O(n×log(n)), iar parcurgerea
în O(n). Prin urmare, complexitatea este O(n×log(n)).
26. d).
Explicaţie: pe fiecare nivel al stivei se caută succesorii în ordinea n, n-1, ..., 1.
129

Capitolul 5

Metoda Greedy

5.1. Generalităţi

Enunţ general:

“Se consideră o mulţime A. Se cere o submulţime a sa astfel încât să fie


îndeplinite anumite condiţii (acestea diferă de la o problemă la alta)”.

Uneori, pentru astfel de probleme, se poate folosi metoda Greedy.

Structura generală a unei aplicaţii Greedy este dată mai jos:

Sol:=∅;
Repetă
 Alege x∈A;
 Dacă este Posibil atunci Sol←Sol+x;
_ Până când am obţinut soluţia
Afişează Sol;

Soluţia este iniţial vidă şi, pe rând, se alege dintre elementele mulţimii A,
element cu element, până când se obţine soluţia cerută.

⇒ De regulă, metoda Greedy rezolvă problema în O(nk).

⇒ Datorită faptului că soluţia este construită pas cu pas, fără un mecanism de


revenire ca la Backtracking, metoda se numeşte Greedy (în română,
traducerea este “lacom”).

⇒ Deşi s-a prezentat forma generală a metodei, ea nu poate fi standardizată


(să avem o unică rutină pe care s-o aplicăm la rezolvarea oricărei probleme).

⇒ Există destul de puţine probleme pentru care se poate aplica această


metodă.

⇒ Medoda Greedy se aplică în două cazuri:

1) atunci când ştim sigur că se ajunge la soluţia dorită (avem la bază o


demonstraţie);

2) atunci când nu dispunem de o soluţie în timp polinomial, dar prin Greedy


obţinem o soluţie acceptabilă, nu neapărat optimă.
130 Capitolul 5. Metoda Greedy

5.2. Probleme pentru care metoda Greedy conduce la


soluţia optimă

5.2.1. Suma maximă

 Enunţ. Se consideră o mulţime de n numere reale. Se cere o submulţime a sa,


cu un număr maxim de elemente, astfel încât suma elementelor sale să fie
maximă.

 Rezolvare. Nu este prea greu de observat că trebuie selectate doar elementele


mai mari sau egale cu 0. Datorită faptului că se doreşte ca submulţimea să aibă un
număr maxim de elemente, se selectează şi elementele egale cu 0. Soluţia este
dată de vectorul B. Am căutat între elementele vectorului A şi le-am ales numai pe
cele mai mari sau egale cu 0.

Programul este următorul:

Varianta Pascal Varianta C++


var A,B:array[1..100]of real; #include <iostream.h>
n,m,i:integer; float A[100],B[100];
int n,m,i;
procedure Greedy;
begin void Greedy()
for i:=1 to n do { for(i=1;i<=n;i++)
if A[i]>=0 then if (A[i]>=0)
begin { m++;
m:=m+1; B[m]=A[i];
B[m]:=A[i] }
end; }
end;
main()
begin { cout<<"n=";
write ('n='); cin>>n;
readln(n); for(i=1;i<=n;i++)
for i:=1 to n do { cout<<"A["<<i<<"]=";
begin cin>>A[i];
write('A[',i,']='); }
readln (A[i]); Greedy();
end; for(i=1;i<=m;i++)
Greedy; cout<<B[i]<<" ";
for i:=1 to m do }
writeln(B[i]:4:2);
end.
Manual de informatică pentru clasa a XI-a 131

5.2.2. Problema planificării spectacolelor

 Enunţ. Într-o sală, într-o zi, trebuie planificate n spectacole. Pentru fiecare
spectacol se cunoaşte intervalul în care se desfăşoară: [st,sf). Se cere să se
planifice un număr maxim de spectacole, astfel încât să nu se suprapună.

 Rezolvare. Vom construi o soluţie după următorul algoritm:


P1 Sortăm spectacolele după ora terminării lor.

P2 Primul spectacol programat este cel care se termină cel mai devreme.

P3 Alegem primul spectacol dintre cele care urmează în şir ultimului


spectacol programat care îndeplineşte condiţia că începe după ce s-a
terminat ultimul spectacol programat.

P4 Dacă tentativa de mai sus a eşuat (nu am găsit un astfel de spectacol),


algoritmul se termină; altfel, se programează spectacolul găsit şi algoritmul
se reia de la P3.

 Demonstraţie
Fie I1 I2 ... Ik şirul spectacolelor alese de algoritm şi O1 O2 ... Os o
soluţie optimă.

Dacă k>s, înseamnă că O1 O2 ... Os nu este soluţie optimă.

Dacă k=s, înseamnă că algoritmul a obţinut o soluţie optimă.

Dacă k<s, atunci procedăm astfel:

În soluţia optimă se înlocuieşte O1 cu I1.. Înlocuirea este posibilă, întrucât I1


este spectacolul care se termină cel mai devreme, deci şirul O2 O3 ... Os
poate să-i urmeze. În acest fel nu se afectează optimalitatea soluţiei. Soluţia
este: I1 O2 ... Os. Dar I2 este spectacolul care începe după ce se termină
I1 şi se termină cel mai devreme dintre cele care îi urmează lui I1. Aceasta
înseamnă că în nici un caz nu se termină mai târziu decât O2. Din acest
motiv, îl înlocuim în soluţia optimă pe O2 cu I2.

Printr-un raţionament asemănător, ajungem la soluţia optimă I1 I2 ... Ik,


Ok+1 ... Os. Aceasta înseamnă că după Ik s-au mai putut selecta şi alte
spectacole; absurd, dacă ţinem cont că, după Ik, nu se mai pot selecta alte
spectacole.

Deci, k=s, q.e.d.


132 Capitolul 5. Metoda Greedy

Programul este prezentat mai jos:

Varianta Pascal Varianta C++


var s: array[1..2,1..10] of #include <iostream.h>
integer; int s[2][10],o[10],n,i,
o:array[1..10] of integer; h1,m1,h2,m2,ora;
n,i,h1,m1,h2,m2:integer;
void sortare()
procedure sortare; { int gata,m,i;
var gata:boolean; do
m,i:integer; { gata=1;
begin for (i=1;i<=n-1;i++)
repeat if (s[1][o[i]]>
gata:=true; s[1][o[i+1]])
for i:=1 to n-1 do { m=o[i];
if s[2,o[i]]>s[2,o[i+1]] o[i]=o[i+1];
then o[i+1]=m;
begin gata=0;
m:=o[i]; }
o[i]:=o[i+1]; }
o[i+1]:=m; while (!gata);
gata:=false }
end
until gata; main()
end; { cout<<"n="; cin>>n;
for (i=1;i<=n;i++)
begin { o[i]=i;
write('n='); readln(n); cout<<"ora de inceput
for i:=1 to n do pentru spectacolul "<<i
begin <<" (hh mm)=";
o[i]:=i; cin>>h1>>m1;
write('ora de inceput pentru s[0][i]=h1*60+m1;
spectacolul ',i, cout<<"ora de sfirsit
' (hh mm)='); pentru spectacolul "<<i
readln(h1,m1); <<" (hh mm)=";
s[1,i]:=h1*60+m1; cin>>h2>>m2;
write('ora de sfirsit pentru s[1][i]=h2*60+m2;
spectacolul ',i, }
' (hh mm)='); sortare();
readln(h2,m2); cout<<"ordinea spectacolelor
s[2,i]:=h2*60+m2; este "<<endl<<o[1]<<endl;
end; ora=s[1][o[1]];
sortare; for (i=2;i<=n;i++)
{ greedy } { if (s[0][o[i]]>=ora)
writeln('ordinea { cout<<o[i]<<endl;
spectacolelor este'); ora=s[1][o[i]];
writeln(o[1]); }
for i:=2 to n do }
if s[1,o[i]]>=s[2,o[i-1]] }
then writeln (o[i]);
end.
Manual de informatică pentru clasa a XI-a 133

5.2.3. Problema rucsacului (cazul continuu)

 Enunţ. O persoană are un rucsac cu ajutorul căruia poate transporta o


greutate maximă G. Persoana are la dispoziţie n obiecte şi cunoaşte pentru fiecare
obiect greutatea şi câştigul care se obţine în urma transportului său la destinaţie.

Se cere să se precizeze ce obiecte trebuie să transporte persoana în aşa fel


încât câştigul să fie maxim.

O precizare în plus transformă această problemă în alte două probleme


distincte. Această precizare se referă la faptul că obiectele pot fi sau nu tăiate
pentru transportul la destinaţie. În prima situaţie, problema poartă numele de
problema continuă a rucsacului, iar în a doua avem problema discretă a
rucsacului. Aceste două probleme se rezolvă diferit, motiv pentru care sunt
prezentate separat.

Varianta continuă a problemei rucsacului este tratată în acest paragraf, iar


cea discretă va fi tratată cu ajutorul programării dinamice.

 Rezolvare. Algoritmul este următorul:

 se calculează, pentru fiecare obiect în parte, eficienţa de transport rezultată


prin împărţirea câştigului la greutate (de fapt, acesta reprezintă câştigul obţinut
prin transportul unităţii de greutate);

 obiectele se sortează în ordine descrescătoare a eficienţei de transport şi se


preiau în calcul în această ordine;

 câştigul iniţial va fi 0, iar greutatea rămasă de încărcat va fi G;

 atât timp cât nu a fost completată greutatea maximă a rucsacului şi nu au fost


luate în considerare toate obiectele, se procedează astfel:

 dintre obiectele neîncărcate se selectează acela cu cea mai ridicată


eficienţă de transport şi avem două posibilităţi:

 obiectul încape în totalitate în rucsac, deci se scade, din greutatea


rămasă de încărcat, greutatea obiectului, iar la câştig se cumulează
câştigul datorat transportului acestui obiect; se tipăreşte 1, în sensul
că întregul obiect a fost încărcat;

 obiectul nu încape în totalitate în rucsac, caz în care se calculează ce


parte din el poate fi transportată, se cumulează câştigul obţinut cu
transportul acestei părţi din obiect, se tipăreşte procentul care s-a
încărcat din obiect, iar greutatea rămasă de încărcat devine 0.
134 Capitolul 5. Metoda Greedy

Dăm un exemplu numeric. Greutatea care poate fi transportată cu


ajutorul rucsacului este 3. Avem la dispoziţie 3 obiecte. Greutatea şi
câştigul pentru fiecare obiect sunt prezentate mai jos:

C 2 4 6
G 2 1 3

Eficienţa de transport este 1 pentru primul obiect, 4 pentru al doilea şi 2


pentru al treilea. În concluzie, obiectul 2 se încarcă în întregime în rucsac, obţinând
un câştig de 4 şi rămâne o capacitate de transport de 2 unităţi de greutate. Se
încarcă 2/3 din obiectul 3 (care este al doilea în ordinea eficienţei de transport)
pentru care se obţine câştigul 4. Câştigul obţinut în total este 8.

Se remarcă strategia GREEDY prin alegerea obiectului care va fi transportat,


alegere asupra căreia nu se revine.

Varianta Pascal Varianta C++


type vector=array [1..9] of #include <iostream.h>
real; double c[9],g[9],ef[9],gv,man,
var c,g,ef:vector; castig;
n,i,man1:integer; int n,i,man1,inv,ordine[9];
gv,man,castig:real;
inv:boolean; main()
ordine:array [1..9] of { cout<<"Greutatea ce poate fi
integer; transportata="; cin>>gv;
cout<<"Numar de obiecte=";
begin cin>>n;
write('Greutatea ce poate fi for (i=1;i<=n;i++)
transportata='); { cout<<"c["<<i<<"]=";
readln(gv); cin>>c[i];
write('Numar de obiecte='); cout<<"g["<<i<<"]=";
readln(n); cin>>g[i];
for i:=1 to n do ordine[i]=i;
begin ef[i]=c[i]/g[i];
write('c[',i,']='); }
readln(c[i]); do
write('g[',i,']='); { inv=0;
readln(g[i]); for (i=1;i<=n-1;i++)
ordine[i]:=i; if (ef[i]<ef[i+1])
ef[i]:=c[i]/g[i] { man=ef[i];
end; ef[i]=ef[i+1];
repeat ef[i+1]=man;
inv:=false; man=c[i];
for i:=1 to n-1 do c[i]=c[i+1]; c[i+1]=man;
if ef[i]<ef[i+1] then man=g[i];
begin g[i]=g[i+1]; g[i+1]=man;
man:=ef[i]; inv=1;
ef[i]:=ef[i+1]; man1=ordine[i];
ef[i+1]:=man; ordine[i]=ordine[i+1];
man:=c[i]; ordine[i+1]=man1;
c[i]:=c[i+1]; }
c[i+1]:=man; }
man:=g[i]; while (inv);
Manual de informatică pentru clasa a XI-a 135

g[i]:=g[i+1]; i=1;
g[i+1]:=man; while (gv>0 && i<=n)
inv:=true; { if (gv>g[i])
man1:=ordine[i]; { cout<<"Obiectul "
ordine[i]:=ordine[i+1]; <<ordine[i]<<' '
ordine[i+1]:=man1 <<1<<endl;
end gv-=g[i];
until not inv; castig+=+c[i];
castig:=0; }
i:=1; else
while (gv>0) and (i<=n) do { cout<<"Obiectul "
begin <<ordine[i]<<' '
if gv>g[i] <<gv/g[i]<<endl;
then castig+=c[i]*gv/g[i];
begin gv=0;
writeln('Obiectul }
',ordine[i],' ',1); i++;
gv:=gv-g[i]; }
castig:=castig+c[i] cout<<"Castig total="<<castig;
end }
else
begin
writeln('Obiectul
',ordine[i],
' ',gv/g[i]:1:2);
castig:=castig+
c[i]*gv/g[i];
gv:=0
end;
i:=i+1
end;
writeln('Castig
total=',castig:3:2)
end.

5.2.4. O problemă de maxim

 Enunţ. Se dau o mulţime A cu m numere întregi nenule şi o mulţime B cu n≥m


numere întregi nenule. Se cere să se selecteze un şir cu m elemente din B, x1, x2,
..., xm astfel încât expresia următoare să fie maximă:

E=a1x1+a2x2+...+anxn,

unde, a1, a2, ..., an sunt elementele mulţimii A într-o anumită ordine pe care
trebuie s-o determinaţi.

 Rezolvare. Vom sorta crescător elementele mulţimii A şi obţinem a1, a2, ..., an
şi pe cele ale mulţimii B. E maxim va fi:

Emax=a1bn-m+1+a2bn-m+2+...ambn.
136 Capitolul 5. Metoda Greedy

Cazul 1: m=n. Se presupune A sortat crescător. Dacă B nu este sortat crescător,


fie prima inversare pe care o face algoritmul de sortare prin inversare (metoda
bulelor): bi cu bi+1 (bi>bi+1).

Iniţial, vectorul B este

B=(b1,b2,...,bi,bi+1,...,bm) şi E1=a1b1+...+aibi+ai+1bi+1+...+ambn.

După inversare, B este:

B=(b1,b2,...,bi+1,bi,...,bm) şi E2=a1b1+...+aibi+1+ai+1bi+...+ambn.

E2-E1=aibi+1+ai+1bi-aibi-ai+1bi+1=bi+1(ai-ai+1)-bi(ai-ai+1)=
(bi+1-bi)(ai-ai+1)≥0.

Cu alte cuvinte, orice astfel de inversare, efectuată de sortarea prin inversare,


măreşte valoarea lui E. Prin urmare, valoarea maximă se obţine atunci când
vectorul este sortat crescător.

Cazul 2: m>n.

Dacă B=(b1,b2,...,bm) şi B’=(b’1,b’2,...,b’m), b’1≥b1, b’2≥b2,…,


b’m≥bm

E’-E=a1(b’1-b1)+a2(b’2-b2)+...+am(b’m-bm)>=0.

Aceasta justifică alegerea ultimelor m elemente din vectorul B sortat.

Varianta Pascal Varianta C++


type vector=array[1..10] of #include <iostream.h>
integer;
var A,B:vector; int A[30],B[30],m,n,i,E;
m,n,i,E:integer;
void Sort(int k, int X[20])
procedure Sort(k:integer; {
var X:vector); int inversari,man;
var Inversari:boolean; do
man:integer; {
begin inversari=0;
repeat for(i=1;i<=k-1;i++)
Inversari:=false; if (X[i]>X[i+1])
for i:=1 to k-1 do {
if X[i]>X[i+1] then man=X[i];
begin X[i]=X[i+1];
man:=X[i]; X[i+1]=man;
X[i]:=X[i+1]; inversari=1;
X[i+1]:=man; }
Inversari:=true; } while (inversari);
end; }
until not inversari;
end;
Manual de informatică pentru clasa a XI-a 137

begin main()
write('M=');readln(m); { cout<<"M="; cin>>m;
for i:=1 to m do readln(A[i]); for(i=1;i<=m;i++) cin>>A[i];
write('N=');readln(N); cout<<"N="; cin>>n;
for i:=1 to n do readln(B[i]); for(i=1;i<=n;i++) cin>>B[i];
Sort(m,A); Sort(m,A);
Sort (n,B); Sort(n,B);
for i:=1 to m do for(i=1;i<=m;i++)
E:=E+A[i]*B[n-m+i]; E+=A[i]*B[n-m+i];
writeln ('Emax=', E); cout<<"Emax="<<E;
end. }

5.3. Greedy euristic

Există probleme pentru care se cunosc numai algoritmi exponenţiali de


rezolvare a lor. Aceştia sunt, după cum ştiţi, inacceptabili în practică. Totuşi,
problemele există şi, de multe ori, avem nevoie de o rezolvare. În astfel de cazuri,
una dintre soluţii este aceea de a aplica metoda Greedy, care va obţine o soluţie,
nu neapărat optimă, dar acceptabilă. Un algoritm care conduce la o soluţie
acceptabilă, dar nu optimă, se numeşte euristică, iar dacă acesta este de tip
Greedy, se numeşte Greedy euristic. Exemplele care urmează vă vor lămuri.

5.3.1. Plata unei sume într-un număr minim de bancnote

 Enunţ. Se dau n tipuri de bancnote de valori b1, b2, ..., bm (numere naturale
strict mai mari ca 0). Din fiecare tip se dispune de un număr nelimitat de bancnote.
De asemenea, se ştie că vom avea întotdeauna bancnota cu valoarea 1. Fiind dată
o sumă S, număr natural, se cere ca aceasta să fie plătită prin utilizarea unui
număr minim de bancnote.

 Rezolvare. Algoritmul este simplu: se sortează bancnotele în ordinea descres-


cătoare a valorii lor. Suma este plătită, cât este posibil, cu bancnota din primul tip,
apoi cu bancnota de al doilea tip, ş.a.m.d., până când este achitată în totalitate.
Exemplu: S=67. Avem 3 tipuri de bancnote: de 10, de 5 şi de 1. Soluţia va fi: 6
bancnote de 10, 1 bancnotă de 5 şi 2 bancnote de 1.

Algoritmul nu întoarce întotdeauna soluţia optimă. Pentru S=10 şi 3 tipuri de


bancnote de valori 4, 3, 1, algoritmul va da soluţia: 2 bancnote de 4 şi 2 bancnote
de 1, în total 4 bancnote. În realitate, soluţia optimă este dată de 1 bancnotă de 4
şi 2 de 3, adică 3 bancnote…

Dacă am dori să evităm acest algoritm, care nu conduce întotdeauna la soluţia


optimă, putem aplica metoda Backtracking. Dar algoritmul este exponenţial...
138 Capitolul 5. Metoda Greedy

Varianta Pascal Varianta C++


type vector=array[1..10] of #include <iostream.h>
integer;
int B[30],n,i,S;
var B:vector; void Sort(int k, int X[20])
n,i,S:integer; {
int inversari,man;
procedure Sort(k:integer; do
var X:vector); { inversari=0;
var Inversari:boolean; for(i=1;i<=k-1;i++)
man:integer; if (X[i]<X[i+1])
begin { man=X[i];
repeat X[i]=X[i+1];
Inversari:=false; X[i+1]=man;
for i:=1 to k-1 do inversari=1;
if X[i]<X[i+1] then }
begin } while (inversari);
man:=X[i]; }
X[i]:=X[i+1];
X[i+1]:=man; main()
Inversari:=true; { cout<<"S="; cin>>S;
end; cout<<"N="; cin>>n;
until not inversari; for(i=1;i<=n;i++)
end; { cout<<"Bancnote de val. ";
cin>>B[i];
begin }
write('S='); Sort(n,B);
readln(S); i=1;
write ('n='); while (S)
readln(n); { if (S/B[i])
for i:=1 to n do { cout<<S/B[i]<< " Bancnote
begin de valoare "<<B[i]<<endl;
write ('Bancnote de S-=S/B[i]*B[i];
valoarea '); }
readln(B[i]); i++;
end; }
Sort(n,B); }
i:=1;
while S<>0 do
begin
if S div B[i]<>0
then
begin
writeln( S div B[i], '
bancnote de ', B[i]);
S:=S-(S div B[i])*B[i];
end;
i:=i+1;
end
end.
Manual de informatică pentru clasa a XI-a 139

5.3.2. Săritura calului

 Enunţ. Se dă o tablă de şah cu dimensiunea n×n. Un cal se găseşte în linia 1


şi coloana 1. Găsiţi un şir de mutări ale calului astfel încât acesta să acopere
întreaga tablă fără a trece printr-o căsuţă de două ori.

 Rezolvare. Problema este binecunoscută, am tratat-o la Backtracking. Din


păcate, rezultatele erau nesatisfăcătoare. Pentru n=6, se afişa imediat soluţia,
pentru n=7 se aştepta ceva, iar pentru n=8...

Pentru această problemă vom prezenta un algoritm euristic extrem de


eficient. În ce constă?

La fiecare pas, alegem acea mutare care aşează calul în poziţia cel mai greu
accesibilă la pasul următor. Fie calul în poziţia (l,c). Teoretic, din acea poziţie,
calul poate fi mutat în alte 8 poziţii. Desigur, nu toate sunt accesibile, deoarece
pentru unele dintre ele calul părăseşte tabla, iar pentru altele se ajunge în poziţii
deja vizitate. Dintre toate poziţiile în care se poate ajunge, se alege cea care este
cât mai izolată. Vezi funcţia Numar.

Cu această euristică, rezultatele sunt spectaculoase. De exemplu, pentru


n=10 soluţia se afişează instantaneu.. Însă... nu avem o demonstraţie pentru
aceasta, deci oricând este posibil să găsim o anumită valoare a lui n pentru care
algoritmul nu furnizează nici o soluţie. Rulaţi programul următor şi vă veţi convinge!

Varianta Pascal Varianta C++


const x:array[1..8] of #include <iostream.h>
integer=(-1,1,2,2, 1,-1,-2,-2); const int x[8]={-1,1,2,2,1,-1,-
y:array[1..8] of 2,-2};
integer=( 2,2,1,-1,-2,-2,-1,1); const int y[8]={2,2,1,-1,-2,-
2,-1,1};
var t: array[-1..50,-1..50] of int t[50][50],Mutari,i,j,n;
integer;
Mutari,i,j,n:integer; int Numar(int l, int c)
{
function Numar(l,c:integer) int nr=0,i;
:integer; for (i=0;i<=7;i++)
var nr,i:integer; if (l+x[i]>=1 &&
begin l+x[i]<=n &&
nr:=0; c+y[i]>=1 &&
for i:=1 to 8 do c+y[i]<=n &&
if t[l+x[i],c+y[i]]=0 t[l+x[i]][c+y[i]]==0)
then nr:=nr+1; nr++;
Numar:=nr; return nr;
end; }
140 Capitolul 5. Metoda Greedy

procedure Mut(l,c:integer); void Mut(int l, int c)


var i,min,v,linie, { int i,min,v,linie,
coloana:integer; coloana,gasit;
gasit:boolean; t[l][c]=Mutari+1;
gasit=0;
begin min=9;
t[l,c]:=Mutari+1; for (i=0;i<=7;i++)
gasit:=false; if (l+x[i]>=1 &&
min:=9; l+x[i]<=n &&
for i:=1 to 8 do c+y[i]>=1 &&
if t[l+x[i],c+y[i]]=0 c+y[i]<=n &&
then t[l+x[i]][c+y[i]]==0)
begin { v=Numar(l+x[i],c+y[i]);
v:=Numar(l+x[i],c+y[i]); if (v<min)
if v<min then { min=v;
begin linie=l+x[i];
min:=v; coloana=c+y[i];
linie:=l+x[i]; gasit=1;
coloana:=c+y[i]; }
gasit:=true; }
end; if (gasit)
end; { Mutari++;
if gasit then Mut(linie,coloana);
begin }
Mutari:=Mutari+1; }
Mut(linie,coloana)
end main()
end; { cout<<"n="; cin>>n;
t[1][1]=1;
begin if (Mutari==n*n-1)
write ('n='); readln(n); for(i=1;i<=n;i++)
for i:=0 to n+1 do { for(j=1;j<=n;j++)
begin cout<<t[i][j]<<" ";
t[0,i]:=1; cout<<endl;
t[-1,i]:=1; }
t[n+1,i]:=1; else cout<<"Tentativa esuata";
t[n+2,i]:=1; }
t[i,0]:=1;
t[i,-1]:=1;
t[i,n+1]:=1;
t[i,n+2]:=1;
end;
Mut(1,1);
if Mutari=n*n-1
then
for i:=1 to n do
begin
for j:=1 to n do
write(t[i,j],' ');
writeln;
end
else
writeln('Tentativa Esuata');
end.
Manual de informatică pentru clasa a XI-a 141

5.3.3. Problema comis-voiajorului

 Enunţ. Un comis-voiajor pleacă dintr-un oraş, trebuie să viziteze un număr de


oraşe şi să se întoarcă în oraşul de unde a plecat cu efort minim. Orice oraş i este
legat printr-o şosea de orice alt oraş j printr-un drum de A[i,j] kilometri. Se
cere traseul pe care trebuie să-l urmeze comis-voiajorul, astfel încât să parcurgă
un număr minim de kilometri.

În practică, s-ar putea ca două oraşe să nu fie unite printr-o şosea. Asta nu
face inutilizabilă actuala problemă, pentru că se poate considera că oraşele
sunt unite printr-o şosea cu ∞ km (pentru calculator, un număr foarte mare).

Fie oraşele de mai jos şi matricea care reţine distanţele dintre ele:

1
3 1 0 1 5 5 3
 
5 5 1 0 2 4 1
1
A = 5 1
2
2 0 6
9 5 1  
2 5 4 6 0 9
3 0 
4
4 3  1 1 9
6

Figura 5.1. Exemplu de hartă pentru problema comis-voiajorului

 Rezolvare. Desigur, problema se poate rezolva prin Backtracking, generând


toate drumurile şi selectându-l pe cel de lungime minimă. Dar acest algoritm este
exponenţial. Atunci renunţăm la soluţia optimă şi ne mulţumim cu una “suficient
de bună”.

Ideea este extrem de simplă: se alege un oraş de pornire. La fiecare pas se


selectează un alt oraş, prin care nu s-a mai trecut şi aflat la distanţă minimă faţă de
oraşul de pornire. Algoritmul se încheie atunci când am selectat toate oraşele.
Vectorul S va reţine oraşele deja selectate.

De exemplu, dacă se porneşte cu oraşul 1, se va obţine un drum de lungime 14


care trece prin oraşele: 1, 2, 5, 3, 4 şi 1.

Varianta Pascal Varianta C++


var S:array[1..9] of integer; #include <iostream.h>
A:array[1..9,1..9] of
integer; int S[10],A[10][10],n,i,j,
v,vs,vs1,min,cost;
n,i,j,v,vs,vs1,min,cost:integer;
142 Capitolul 5. Metoda Greedy

main()
begin { cout<< "Numar noduri=";
write('Numar noduri='); cin>>n;
readln(n); //citesc matricea
{citesc matricea} for (i=1;i<=n;i++)
for i:=1 to n do
for (j=i+1;j<=n;j++)
for j:=i+1 to n do
{ cout<<"A["<<i
begin
<<","<<j<<"]=";
write('A[',i,',',j,']=');
cin>>A[i][j];
readln(A[i,j]);
A[j][i]=A[i][j];
A[j,i]:=A[i,j];
}
end;
//Pentru fiecare nod
{Pentru fiecare nod}
cout<<"Nod de pornire ";
write ('Nod de pornire ');
cin>>v;
readln(v);
S[v]=1;
S[V]:=1;
vs1=v;
vs1:=v;
cout<<"Drumul trece prin ";
write('Drumul trece prin ');
for (i=1;i<=n-1;i++)
for i:=1 to n-1 do
{ min=30000;
begin
for (j=1;j<=n;j++)
min:=30000;
if (A[v][j]!=0 && S[j]==0
for j:=1 to n do
&& min>A[v][j])
if (A[v,j]<>0) and (s[j]=0)
{ min=A[v][j];
and (min>a[v,j])
vs=j;
then }
begin cost+=A[v][vs];
min:=A[v,j]; cout<<vs<<" ";
vs:=j S[vs]=1;
end; v=vs;
cost:=cost+A[v,vs]; }
write(vs,' ');
cost+=A[vs1][v];
S[vs]:=1;
cout<<endl<<"Cost="<<cost;
v:=vs;
}
end;
cost:=cost+A[vs1,v];
writeln('Cost=',Cost);
end.

Probleme propuse
1. În cazul unei mulţimi cu n numere reale, care este complexitatea algoritmului
pentru selectarea unei mulţimi cu număr maxim de elemente pentru care suma
elementelor este maximă?

a) O(n); b) O(2n); c) O(n2); d) O(2n).

2. Care este complexitatea algoritmului prezentat în carte care rezolvă “problema


spectacolelor”?

a) O(n); b) O(log(n)); c) O(n2); d) O(2n).


Manual de informatică pentru clasa a XI-a 143

3. Scrieţi un program care rezolvă problema spectacolelor şi are o complexitate


mai mică decât programul prezentat.

4. Care este complexitatea algoritmului prezentat care rezolvă problema continuă


a rucsacului şi cum putem modifica acest algoritm pentru a avea o complexitate
mai “bună”?

5. Rezolvaţi problema rucsacului, cazul continuu, în cazul în care la plecare, se


dispune, din fiecare produs, de cantităţi nelimitate.

6. O întreprindere care fabrică produse alimentare (unt, marmeladă, ulei, etc)


dispune de suma S pentru fabricarea anumitor produse. Se ştie că există n
produse care pot fi fabricate şi pentru fiecare produs se cunoaşte costul obţinerii
unui kg (litru) din produsul respectiv, precum şi preţul de vânzare al unui kg (litru)
din produsul respectiv. Scrieţi un program care determină ce produse pot să fie
fabricate de întreprindere şi în ce cantitate, astfel încât să se obţină o sumă
maximă la vânzarea lor.

7. Rezolvaţi prin Backtracking problema plăţii unei sume în număr minim de


bancnote. Comparaţi soluţiile cu cele obţinute prin Greedy euristic, din punct de
vedere al corectitudinii lor şi din punct de vedere al timpului necesar pentru
obţinerea lor.

8. Care este complexitatea programului care rezolvă prin Greedy euristic


problema comis-voiajorului?

9. Scrieţi un program care rezolvă prin Greedy euristic problema comis-voiajorului,


dar care caută cea mai bună soluţie pornind de la fiecare vârf în parte. Care este
complexitatea acestui algoritm?

10. Codul Gray. Pentru primele 8 numere naturale, în tabelul de mai jos, observaţi
numărul scris în baza 10, în binar şi în codul Gray. După cum puteţi observa,
codul Gray se caracterizează prin faptul că reprezentarea a două numere
consecutive diferă exact cu o poziţie binară.

Număr În cod binar În cod Gray


0 000 000
1 001 001
2 010 011
3 011 010
4 100 110
5 101 111
6 110 101
7 111 100

Fiind dat un număr natural în binar, b1b2...bn, scrieţi un subprogram care


converteşte numărul în cod Gray, g1g2...gn, după următorul algoritm:
g1=b1;
gk=(bk+bk-1) modulo 2; pentru k=2, 3, ..., n.
144 Capitolul 5. Metoda Greedy

n=5, în binar avem 101, iar în Gray avem: 1(1+0)(0+1)=111.


n=7, în binar avem 111, iar în Gray avem: 1(1+1)(1+1)=100.

Observaţi faptul că soluţia se construieşte bit cu bit, ceea ce justifică


includerea acestei probleme la tehnica Greedy.

11. Fiind dat un număr natural în cod Gray, g1g2...gn, scrieţi o funcţie care
converteşte numărul în binar b1 b2...bn după următorul algoritm:

b1=g1;
bk=(gk+bk-1) modulo 2; pentru k=2,3...n

n=5, în Gray avem 111, iar în binar avem: 1(1+1)(1+0)=101.


n=7, în Gray avem 100, iar în binar avem: 1(0+1)(0+1)=111.

12. Scrieţi un program care afişează codurile Gray ale primelor numere naturale
citite.

13. Se consideră o mulţime X cu n elemente. Să se elaboreze un algoritm eficient


şi să se scrie un program pentru generarea şirului tuturor submulţimilor lui X, A1,
A2, ... astfel încât Ai+1 să se obţină din Ai, prin adăugarea sau scoaterea unui
element.

14. Problema colorării hărţilor. Sunt date N ţări, precizându-se relaţiile de


vecinătate. Se cere să se determine o posibilitate de colorare a hărţii (cu cele N
ţări), astfel încât să nu existe ţări vecine colorate la fel.

Răspunsuri / Indicaţii

1. a); 2. c);
3. Sortarea prin metoda bulelor are complexitatea O(n2). Apoi, selecţia specta-
colelor are complexitatea O(n). În concluzie, complexitatea este O(n2). Dar dacă
înlocuim metoda de sortare prin QuickSort, atunci complexitatea medie este
O(n×log(n)). 4. O(n2), dar dacă se schimbă metoda de sortare, se ajunge la
O(n×log(n)). 6. Recunoaşteţi problema rucsacului? În locul capacităţii rucsacului
G, avem suma S, în locul greutăţii fiecărui obiect avem costul lui, în locul câştigului
obţinut din transport, avem câştigul obţinut în urma vânzării produsului. 8. O(n2).
9. O(n3). 12. Se numără în baza 10, numerele se convertesc în binar, apoi în
Gray. 13. De vreme ce vorbim de submulţimi, ne gândim la vectorul caracteristic.
Acesta, însă, nu va mai reţine numărul în binar ci în cod Gray.

14. Indicaţie. Algoritmul propus este următorul:


• ţara 1 va avea culoarea 1;
• presupunem colorate primele i-1 ţări: ţara i va fi colorată cu cea mai mică
culoare (ca număr) astfel încât nici una din ţările vecine să nu fie colorată la fel.
145

Capitolul 6
Programare dinamică

6.1. Generalităţi

Fie o problemă a cărei rezolvare este cerută pentru un număr natural n dat.
Uneori se poate aplica un raţionament de genul: dacă ştim să rezolvăm problema
pentru toate valorile strict mai mici decât n, atunci putem rezolva problema şi
pentru n dat. Dacă este aşa, atunci, pe baza aceluiaşi raţionament, înseamnă că
dacă ştim să rezolvăm problema pentru toate valorile strict mai mici decât n-1,
atunci ştim să rezolvăm problema pentru n-1 şi, aşa cum am arătat, pentru n.
Repetând acest raţionament ajungem să rezolvăm problema pentru n=1 şi,
eventual, pentru n=2. O astfel de problemă este foarte uşor de rezolvat. După care
rezolvăm problema pentru n=3, apoi n=4, ş.a.m.d., până se ajunge la acel n cerut
de problemă. De aici rezultă necesitatea găsirii unor relaţii de recurenţă.

O persoană trebuie să urce n scări. Se ştie că persoana respectivă poate


urca fie o scară, fie două deodată. Întrebarea este: în câte moduri poate
urca persoana n scări?

O primă idee este de a descompune pe n ca sumă de 1 şi 2 în toate


modurile posibile şi de a număra soluţiile. Problema se poate rezolva aplicând
tehnica Backtracking (exerciţiu!). Dar în acest caz obţinem un timp exponenţial…

O rezolvare prin programarea dinamică se poate face în felul următor:

- dacă n=1, o scară se poate urca într-un singur fel;


- dacă n=2, se poate urca o scară şi apoi o altă scară (un fel) sau
deodată două scări (altă modalitate), prin urmare, există două
modalităţi de a urca două scări.

Până acum am rezolvat problema pentru n=1 şi pentru n=2. Dacă notăm cu
un numărul de feluri în care se pot urca n scări, atunci ştim că u1=1 şi u2=2.

Acum, pentru a urca n scări putem proceda astfel: se urcă n-2 scări şi
deodată două scări sau se urcă n-1 scări, după care se mai urcă o scară. În câte
feluri se pot urca n-2 scări? În un-2 feluri. În câte feluri se pot urca n-1 scări? În
un-1 feluri. Atunci, n scări se pot urca în un=un-1+un-2 feluri. De ce le-am adunat?
Pentru că în acest fel se obţin doar soluţii diferite. Orice soluţie care provine din un-1
se termină prin a urca la sfârşit o scară şi orice soluţie care provine din un-2 se
termină prin a urca la sfârşit două scări. Dacă soluţiile sunt diferite, atunci se pot
aduna.
146 Capitolul 6. Programare dinamică

Astfel, am obţinut o relaţie de recurenţă binecunoscută. Acum, problema se


reduce la a calcula un din această relaţie. Deja ştim să rezolvăm o astfel de relaţie,
revedeţi recursivitatea (aţi întâlnit-o la şirul lui Fibonacci). Mai mult, un se poate
calcula în O(n), adică liniar.

Tot atunci când aţi studiat recursivitatea aţi văzut că rezolvarea prin
utilizarea mecanismului recursivităţii a acestei relaţii este catastrofală din punct de
vedere al timpului de calcul, fiind exponenţială.

 În programarea dinamică, de cele mai multe ori, pentru a rezolva o problemă


se scrie o relaţie de recurenţă. Relaţia de recurenţă se rezolvă apoi iterativ.

Problema prezentată face parte din categoria problemelor de numărare. În


acest capitol veţi întâlni şi alte probleme de numărare.

 Prin programare dinamică puteţi rezolva şi probleme de optim în care se


cere o soluţie care să maximizeze sau să minimizeze o anumită funcţie. În
acest caz, criteriile de mai jos vă pot fi de folos. Menţionăm că înţelegerea
lor se face în timp, studiind mai multe exemple, motiv pentru care este bine
ca, atunci când rezolvaţi o problemă, să reveniţi asupra lor.

Se consideră o problemă în care rezultatul se obţine ca urmare a unui şir de


decizii D1, D2,..., Dn. În urma deciziei D1, sistemul evoluează din starea S0 în starea
S1, în urma deciziei D2, sistemul evoluează din starea S1 în starea S2, ..., în urma
deciziei Dn, sistemul evoluează din starea Sn-1 în starea Sn.

Dacă D1, D2,....Dn este un şir de decizii care conduce sistemul în mod optim
din S0 în Sn, atunci trebuie îndeplinită una din condiţiile următoare (principiul de
optimalitate):
1) Dk...Dn este un şir de decizii ce conduce optim sistemul din starea Sk-1
în starea Sn, ∀k, 1≤k≤n;
2) D1...Dk este un şir de decizii care conduce optim sistemul din starea S0
în starea Sk, ∀k, 1≤k≤n;
3) Dk+1...Dn, D1...Dk sunt şiruri de decizii care conduc optim sistemul din
starea Sk în starea Sn, respectiv din starea S0 în starea Sk, ∀k, 1≤k≤n.

⇒ Dacă principiul de optimalitate se verifică în forma 1), spunem că se aplică


programarea dinamică metoda înainte.

⇒ Dacă principiul de optimalitate se verifică în forma 2), spunem că se aplică


programarea dinamică metoda înapoi.

⇒ Dacă principiul de optimalitate se verifică în forma 3), spunem că se aplică


programarea dinamică metoda mixtă.

Programarea dinamică se poate aplica problemelor la care optimul general


implică optimul parţial.
Manual de informatică pentru clasa a XI-a 147

Dacă drumul cel mai scurt între Bucureşti şi Suceava trece prin Focşani,
atunci porţiunea din acest drum, dintre Bucureşti şi Focşani, este cea mai
scurtă, ca şi porţiunea dintre Focşani şi Suceava (dacă n-ar fi aşa,
drumul considerat între Bucureşti şi Suceava nu ar fi optim).

Faptul că optimul general determină optimul parţial, nu înseamnă că optimul


parţial determină optimul general.

Fiind date drumurile cele mai scurte de la Bucureşti la Cluj şi de la Cluj la


Suceava, nu înseamnă că drumul optim de la Bucureşti la Suceava trece
prin Cluj.

Cu toate acestea, faptul că optimul general impune optimul parţial ne este


de mare ajutor: căutăm optimul general, între optimele parţiale, pe care le
reţinem la fiecare pas. Oricum, căutarea se reduce considerabil.

6.2. Problema triunghiului

 Enunţ. Se consideră un triunghi de numere naturale format din n linii. Prima


linie conţine un număr, a doua două numere, ..., iar ultima, n numere naturale. Cu
ajutorul acestui triunghi se pot forma sume de numere naturale în felul următor:
 se porneşte cu numărul din linia 1;
 succesorul unui număr se află pe linia următoare plasat sub el (aceeaşi
coloană) sau pe diagonală la dreapta (coloana creşte cu 1).

Care este cea mai mare sumă care se poate forma astfel şi care sunt
numerele care o alcătuiesc?
Exemplu: Pentru n=4, se consideră triunghiul de mai jos:
2
3 5
6 3 4
5 6 1 4

Se pot forma mai multe sume:


S1=2+3+6+5=16;
S2=2+5+4+1=12;
Sk=2+3+6+6=17 (care este şi suma maximă).

 Rezolvare. Cum am putea rezolva această problemă?


A) O primă idee ar fi să încercăm o abordare a ei prin metoda Greedy. Aceasta
presupune ca, la fiecare pas, să selectăm de pe linia respectivă cel mai mare
element dintre cele două care pot fi alese. Astfel, de pe linia 1 selectăm 2, de pe
linia a 2-a, 5, de pe linia a 3-a, 4, iar de pe linia a 4-a, 4. Înseamnă că suma este
2+5+4+4=15. Observăm că în acest fel nu am reuşit să obţinem suma maximă.
De ce? Când facem o alegere care maximizează suma la un moment dat, s-ar
148 Capitolul 6. Programare dinamică

putea să nu mai putem alege pentru liniile următoare elementele care maximizează
suma. Urmăriţi soluţia optimă din exemplul dat. Prin urmare, "soluţia" propusă nu
este corectă.

B) Să încercăm altfel. Se cere suma maximă. Atunci ar trebui să considerăm


toate "drumurile" posibile de la prima linie la ultima. Pentru fiecare astfel de drum
să calculăm suma care se poate forma şi să selectăm suma maximă. Este corectă
o astfel de rezolvare? Evident, da. Cum am putea genera toate aceste drumuri?
Dacă am spus "toate", atunci ne gândim la metoda Backtracking. Mai jos,
prezentăm programul obţinut aplicând acest algoritm:

Varianta Pascal Varianta C++


var n,i,j,max:integer; int t[50][50],sol[50],
t:array [1..50,1..50] of drum[50],n,i,j,max=0;
integer;
drum, sol:array[1..50] of void back(int k)
integer; { int i,j,s=t[1][1];
if (k==n+1)
procedure back(k:integer); { for (i=2;i<=n;i++)
var i,j,s:integer; s+=t[i][sol[i]];
begin if (s>max)
s:=t[1][1]; { for (i=1;i<=n;i++)
if k=n+1 then drum[i]=sol[i];
begin
max=s;
for i:=2 to n do
}
s:=s+t[i,sol[i]];
}
if s>max then
else
begin
{ for (i=sol[k-1];
for i:=1 to n do
drum[i]:=sol[i]; i<=sol[k-1]+1;i++)
max:=s; { sol[k]=i;
end back(k+1);
end }
else }
for i:=sol[k-1] to sol[k-1]+1 }
do begin
sol[k]:=i; main()
back(k+1); { cout<<"n="; cin>>n;
end for (i=1;i<=n;i++)
end; for (j=1;j<=i;j++)
{ cout<<"t["<<i<<','
begin <<j<<"]=";
write('n='); readln(n); cin>>t[i][j];
for i:=1 to n do }
for j:=1 to i do sol[1]=1;
begin back(2);
write('t[',i,',',j,']=');
cout<<"Suma maxima"<<endl;
readln(t[i,j])
for (i=1;i<=n;i++)
end;
cout<<drum[i]<<" ";
sol[1]:=1;
}
back(2);
writeln('Suma maxima',max);
for i:=1 to n do
write(drum[i],' ');
end.
Manual de informatică pentru clasa a XI-a 149

Să observăm că se pot forma 2n-1 sume de acest fel.

 Exerciţiu. Demonstraţi prin inducţie că numărul de sume care se pot forma


este corect.

A lua în considerare toate aceste sume nu este eficient, pentru că avem un


algoritm în O(2n). Am putea accepta o astfel de soluţie, cu toate consecinţele ei,
numai dacă n-ar exista o alta, eficientă.

 Exerciţiu. Pentru a testa modul în care funcţionează acest algoritm,


modificaţi programul pentru a accepta intrări de la un fişier text, creaţi un astfel de
fişier şi analizaţi timpul în care se obţine soluţia pentru n=40.

C) Încercăm să rezolvăm problema prin aplicarea metodei programării dinamice.


Verificăm principiul programării dinamice. Fie un şir de n numere care respectă
condiţiile problemei şi care formează suma maximă: n1, n2, ..., ni, ..., nn. Este clar
că numerele de la ni ... nn formează o sumă maximă în raport cu sumele care se
pot forma începând cu numărul ni. Dacă această sumă nu ar fi maximă, atunci ar
exista o altă alegere de numere care ar maximiza-o. Înlocuind în şirul iniţial
numerele de la ni ... nn cu cele alese pentru a maximiza suma, vom obţine o
soluţie în care suma este mai mare.

Pentru exemplul dat, cunoaştem soluţia optimă: 2+3+6+6. Aceasta


înseamnă că, dacă de pe linia 2 se porneşte cu 3, cea mai mare sumă care se
poate forma, în condiţiile problemei, este 3+6+6.

Aceasta contrazice ipoteza. În această situaţie, se poate aplica programarea


dinamică, metoda înainte.

Vom forma un triunghi, de la bază către vârf, cu sumele maxime care se pot
forma cu fiecare număr. Dacă citim triunghiul de numere într-o matrice T şi
calculăm sumele într-o matrice C, vom avea relaţiile următoare:

C[n,1]:=T[n,1];
C[n,2]:=T[n,2];
C[n,n]:=T[n,n];

Pentru linia i (i<n), cele i sume maxime se obţin astfel:

C[i,j]=max{T[i,j]+C[i+1,j],T[i,j]+C[i+1,j+1]},
i∈{1,2,...,n-1}, j∈{1,...,i}.

Să rezolvăm problema propusă ca exemplu.

Linia 4 a matricei C va fi linia n a matricei T:

5 6 1 4.
150 Capitolul 6. Programare dinamică

Linia 3 se calculează astfel:

C[3,1]=max{6+5,6+6}=12;
C[3,2]=max{3+6,3+1}=9;
C[3,3]=max{4+1,4+4}=8;

Vom avea:

12 9 8
5 6 1 4

Linia 2:

C[2,1]=max{3+12,3+9}=15;
C[2,2]=max{5+9,5+8}=14;

15 14
12 9 8
5 6 1 4

Linia 1:

C[1,1]=max{2+15,2+14}=17;

17
15 14
12 9 8
5 6 1 4

Aceasta este şi cea mai mare sumă care se poate forma.

Pentru a tipări numerele luate în calcul se foloseşte o matrice numită DRUM în


care pentru fiecare i∈{1,...,n-1} şi j∈{1,...,i} se reţine coloana în care se
găseşte succesorul lui T[i,j].

Varianta Pascal Varianta C++


type matrice =array #include <iostream.h>
[1..50,1..50] of integer;
int t[50][50],c[50][50],
var n,i,j:integer; drum[50][50],n,i,j;
t,c,drum:matrice;
main()
begin { cout<<"n=";
write('n='); cin>>n;
readln(n); for (i=1;i<=n;i++)
for i:=1 to n do for (j=1;j<=i;j++)
for j:=1 to i do { cout<<"t["<<i<<','
begin <<j<<"]=";
write('t[',i,',',j,']='); cin>>t[i][j];
readln(t[i,j]) }
end;
Manual de informatică pentru clasa a XI-a 151

for j:=1 to n do for (j=1;j<=n;j++)


c[n,j]:=t[n,j]; c[n][j]=t[n][j];
for i:=n-1 downto 1 do for (i=n-1;i>=1;i--)
begin { for (j=1;j<=i;j++)
for j:=1 to i do if (c[i+1][j]<c[i+1][j+1])
if c[i+1,j]<c[i+1,j+1] { c[i][j]=t[i][j]+
then c[i+1][j+1];
begin drum[i][j]=j+1;
c[i,j]:=t[i,j]+ }
c[i+1,j+1]; else
drum[i,j]:=j+1 { c[i][j]=t[i][j]+c[i+1][j];
end drum[i][j]=j;
else }
begin }
c[i,j]:=t[i,j]+ cout<<"suma maxima= "
c[i+1,j]; << c[1][1]<<endl;
drum[i,j]:=j i=1; j=1;
end while (i<=n)
end; { cout<<t[i][j]<<endl;
writeln('suma max= ',c[1,1]); j=drum[i][j];
i:=1; i++;
j:=1; }
while i<=n do }
begin
writeln(t[i,j]);
j:=drum[i,j];
i:=i+1
end;
end.

 Exerciţii
1. Complexitatea algoritmului pentru ultima rezolvare este O(n2). De ce?
2. Puteţi rezolva problema prin metoda înapoi?

6.3. Subşir crescător de lungime maximă

 Enunţ. Se consideră un vector cu n elemente întregi. Se cere să se tipărească


cel mai lung subşir crescător al acestuia.

Exemplu. Pentru n=5 se dă vectorul V=(4,1,7,6,7). În acest caz, subşirul


tipărit va fi: 4,7,7.

 Rezolvare. Problema se poate rezolva pornind de la ideea de a calcula, pentru


fiecare element al vectorului, lungimea celui mai lung subşir crescător care se
poate forma începând cu el. În final, este selectat elementul din vector cu care
se poate forma cel mai lung subşir crescător şi acesta este listat.
152 Capitolul 6. Programare dinamică

L(k)={1+max L(i)|V(i)≥V(k), i={k+1,...,n}},k∈{1,2,...,n}.

În practică, folosim un vector L cu n componente, unde L(k) are


semnificaţia explicată. Pentru exemplul nostru vom avea:

L=(3,3,2,2,1).

Componentele vectorului L au fost calculate astfel:


 cel mai lung subşir care se poate forma cu elementul 7, aflat pe ultima
poziţie, are lungimea 1;
 cel mai lung subşir care se poate forma cu elementul 6, aflat pe poziţia
4, are lungimea 2 (1+L(5)), pentru că pe poziţia 5 se găseşte
elementul 7 care este mai mare decât 6;
 cel mai lung subşir care se poate forma cu elementul aflat pe poziţia 3
are lungimea 2 (1+L(5)) deoarece 7 este egal cu 7;
 algoritmul continuă în acest mod până se completează L(1).

După aceasta se calculează maximul dintre componentele lui L, iar cel mai
lung subşir crescător format din elementele vectorului V va avea lungimea dată de
acest maxim. Pentru a lista efectiv acel subşir de lungime maximală se procedează
astfel:
 se caută maximul din vectorul L precum şi indicele t, la care se
găseşte acest maxim;
 se afişează V(t);
 se găseşte şi se listează primul element care este mai mare sau egal
cu V(t) şi are lungimea mai mică cu 1 (max-1), se actualizează
valoarea max cu max-1;
 algoritmul continuă până când se epuizează toate elementele
subşirului.

Programul este următorul:

Varianta Pascal Varianta C++


type vector=array[1..20] of #include <iostream.h>
integer;
var v,l:vector; int v[20],l[20],n,i,k,max,t;
n,i,k,max,t:integer;
begin main()
write(‘n=‘); readln(n); { cout<<"n="; cin>>n;
for i:=1 to n do for (i=1;i<=n;i++)
begin { cout<<"v["<<i<<"]=";
write(‘v[‘,i,’]=); cin>>v[i];
readln(v[i]) }
end; l[n]=1;
Manual de informatică pentru clasa a XI-a 153

l[n]:=1; for (k=n-1;k>=1;k--)


for k:=n-1 downto 1 do { max=0;
begin for (i=k+1;i<=n;i++)
max:=0; if (v[i]>=v[k] && l[i]>max)
for i:=k+1 to n do max=l[i];
if (v[i]>=v[k]) and l[k]=1+max;
(l[i]>max) }
then max:=l[i]; max=l[1];
l[k]:=1+max; t=1;
end; for (k=1;k<=n;k++)
max:=l[1]; if (l[k]>max)
t:=1; { max=l[k];
for k:=1 to n do t=k;
if l[k]>max then }
begin cout<<"lungimea maxima:"<<max
max:=l[k]; <<endl<<v[t]<<endl;
t:=k for (i=t+1;i<=n;i++)
end; if (v[i]>v[t] && l[i]==max-1)
writeln(‘lungimea { cout<<v[i]<<endl;
maxima:’,max); max--;
writeln(v[t]); }
for i:=t+1 do n do }
if (v[i]>v[t]) and
(l[i]=max-1)
then
begin
writeln(v[i]);
max:=max-1
end
end.

 Exerciţiu. Cum verificaţi pentru această problemă principiul optimalităţii?

Complexitatea acestui algoritm este O(n2).

Pentru fiecare element al şirului se calculează un maxim. Aceasta


presupune parcurgerea şirului până la capăt. Pentru elementul aflat pe poziţia n-1
se face o comparare, pentru elementul aflat pe poziţia n-2 se fac 2 comparări, ...,
pentru elementul aflat pe poziţia 1 se fac n-1 comparări.

Prin urmare, numărul de comparări este:

n(n − 1)
S = 1 + 2 + ... + n − 1 = ,
2

deci algoritmul are complexitatea O(n2).


154 Capitolul 6. Programare dinamică

6.4. O problemă cu sume

 Enunţ. Se citeşte n>1, număr natural, Suma, număr natural şi n numere


naturale nenule. Se cere să se decidă dacă numărul Suma poate fi obţinut ca sumă
de numere naturale dintre cele n citite. În caz afirmativ, să se afişeze un set de
numere care, adunate, dau acest rezultat.

 Rezolvare. Putem considera toate submulţimile mulţimii {1,2,...,n} şi


pentru fiecare submulţime calculăm suma obţinută. Dar... câte submulţimi avem?
Avem 2n submulţimi. Înseamnă că avem un algoritm în O(2n)... Inacceptabil!

Vom prefera un alt algoritm, bazat pe metoda programării dinamice.

Notăm numerele citite cu n1, n2, ..., nn. Fie S=n1+n2+...+nn. Evident, orice
sumă care se poate forma cu numerele citite, poate fi un număr între 1 şi S.
Vectorul Sume, un vector cu S componente, va reţine 1 pentru fiecare sumă care
poate fi formată şi 0 în caz contrar.

La pasul i vom calcula toate sumele care se pot calcula cu numerele n1,
n2, ..., ni.

La pasul 1 avem Sume[n1]=1.

La pasul 2 avem Sume[n2]=1 şi Sume[n1+n2]=1.

La pasul 3 avem Sume[n3]=1 şi Sume[n2+n3]=1, Sume[n1+n2+n3]=1.


...

Dar cum reţinem toţi termenii care alcătuiesc o sumă? Mai simplu decât pare
la prima vedere… Un vector, numit Alege, cu S componente, va reţine pentru
fiecare sumă calculată ultimul termen care intră în alcătuirea ei. Atunci când afişăm
soluţia pentru Suma, tipărim Alege[Suma], apoi Alege[Suma-Alege[Suma]]…

Vom prezenta ca exemplu cazul n=3. Numerele citite sunt 2, 3, 5.

Sumele calculate pot lua valori între 1 şi 2+3+5=10. Un vector Suma, cu 10


componente va reţine 1 pentru fiecare sumă care se poate calcula şi 0 dacă, până
la acel pas suma nu s-a putut calcula.

Cu numărul 2 se poate forma o singură sumă: 2.

Sume Alege

0 1 0 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0
1 2 3 4 5 6 7 8 9 10 1 2 3 4 5 6 7 8 9 10
Manual de informatică pentru clasa a XI-a 155

Cu numerele 2 şi 3 se mai pot forma sumele: 3, şi 5=2+3.


Suma Alege

0 1 1 0 1 0 0 0 0 0 0 2 3 0 3 0 0 0 0 0
1 2 3 4 5 6 7 8 9 10 1 2 3 4 5 6 7 8 9 10

Cu numerele 2, 3, 5 se mai pot forma şi sumele: 5 (a mai fost obţinută), 7=2+5


şi 8=3+5, 10=5+5.
Suma Alege

0 1 1 0 1 0 1 1 0 1 0 2 3 0 5 0 5 5 0 5
1 2 3 4 5 6 7 8 9 10 1 2 3 4 5 6 7 8 9 10

Dacă Suma=7, se afişează Alege[7]=5, Alege[7-5]= Alege[2]=2.

Complexitatea algoritmului. Fie S suma numerelor. Algoritmul tratează fiecare


număr citit (n numere) şi pentru fiecare număr citit parcurge vectorul Suma care are
S componente. Prin urmare, complexitatea lui este O(nS).

Pentru a nu calcula o sumă de mai multe ori, pentru calculul noilor sume se
utilizează un vector auxiliar, numit Sume1, după care se execută sau logic
între conţinuturile celor doi vectori (vedeţi programul următor).

Varianta Pascal Varianta C++


var Nr,Sume,Sume1,Alege:array #include <iostream.h>
[0..100] of integer;
int Nr[100],Sume[100],
n,i,j,S,Suma:integer;
Sume1[100],Alege[100],n,i,j,
begin S,Suma;
write ('n='); readln(n);
main()
for i:=1 to n do
{ cout<<"n="; cin>>n;
begin
for(i=1;i<=n;i++)
readln(Nr[i]); S:=S+Nr[i]
{ cin>>Nr[i];
end;
S+=Nr[i];
for i:=1 to n do
}
begin
for (i=1;i<=n;i++)
Sume1[Nr[i]]:=1;
{ Sume1[Nr[i]]=1;
for j:=1 to S do
for (j=1;j<=S;j++)
if (Sume[j]=1)
then Sume1[j+Nr[i]]:=1; if(Sume[j]==1)
for j:=1 to S do Sume1[j+Nr[i]]=1;
if (Sume1[j]=1) and for(j=1;j<=S;j++)
(Sume[j]=0) then if (Sume1[j]==1 &&
begin Sume[j]==0)
Sume[j]:=1; { Sume[j]=1;
Alege[j]=Nr[i];
Alege[j]:=Nr[i];
Sume1[j]=0;
Sume1[j]:=0;
}
end;
}
end;
156 Capitolul 6. Programare dinamică

write('Suma='); readln(Suma); cout<<"Suma="; cin>>Suma;


if Suma<=S then if (Suma<=S)
if Alege[Suma]<>0 then if (Alege[Suma])
while Suma<>0 do while (Suma)
begin { cout<<Alege[Suma]<<endl;
writeln(Alege[Suma]); Suma-=Alege[Suma];
Suma:=Suma-Alege[Suma]; }
end else cout<<"Suma nu se poate
else writeln ('Suma nu se forma";
poate forma ') else cout<<"Suma nu se poate
else writeln ('Suma nu se forma";
poate forma ') }
end.

Dacă numerele sunt introduse în ordine crescătoare, atunci suma se obţine


prin utilizarea unui număr maxim de termeni şi invers, dacă numerele sunt
introduse în ordine descrescătoare, atunci suma se obţine prin utilizarea
unui număr minim de termeni. De ce?

6.5. Problema rucsacului (cazul discret)

 Enunţ (Varianta 1). O persoană are la dispoziţie un rucsac cu o capacitate de


G unităţi de greutate şi intenţionează să efectueze un transport în urma căruia să
obţină un câştig. Persoana are la dispoziţie n obiecte. Pentru fiecare obiect se
cunoaşte greutatea sa Gri (număr natural) şi câştigul obţinut în urma transportului
său: Ci. Ce obiecte trebuie să aleagă persoana pentru a-şi maximiza câştigul şi
care este acesta?

Cerinţă suplimentară. Odată ales un obiect, acesta trebuie transportat integral (nu
se admite transportul unei părţi din el). În acest fel trebuie să rezolvăm problema
discretă a rucsacului (rucsac 0/1).

 Rezolvare. Vom nota obiectele cu 1, 2, ..., n. Există posibilitatea ca problema


să admită mai multe soluţii optime. Fie S={i1,i2,...,ik} o soluţie optimă a
problemei (unde i1<i2<...<ik sunt obiecte dintre cele n). Dacă se înlătură obiectul
ik, atunci greutatea G-Gr(ik) este încărcată optim cu obiectele i1<i2<...<ik-1.
Dacă, prin absurd, ar exista o altă încărcare a rucsacului pentru greutatea G-
Gr(ik), care aduce un câştig mai mare, atunci, dacă la acea încărcătură s-ar
adăuga obiectul ik, am obţine o soluţie mai bună decât cea iniţială, ceea ce
contrazice optimalitatea soluţiei.

Aceasta conduce la următoarea idee de rezolvare a problemei:

• Capacităţile 1,2,...,G se încarcă optim, la început cu obiectul 1, apoi se


îmbunătăţeşte soluţia cu obiectul 2, ... şi, la sfârşit, se îmbunătăţeşte
soluţia cu obiectul n.
Manual de informatică pentru clasa a XI-a 157

• Pentru a calcula câştigul maxim vom utiliza matricea Castign+1,G+1,


unde Castig(i,j) va reţine câştigul obţinut prin transportul obiectelor
1, 2, ..., i, dacă capacitatea de transport este j. Relaţiile de recurenţă
sunt următoarele:

a) Castig(i,0) = 0, i = 1...n

b) Castig(0, j) = 0, j = 1...G

Castig(i − 1, j − Gr(i)) + C(i), dacă Castig(i − 1, j − Gr(i) + C(i)) > Castig(i − 1, j)


c) Castig(i, j) = 
Castig(i − 1, j), altfel

Justificarea relaţiilor

a) Dacă rucsacul are capacitatea 0, evident, nu poate fi transportat nici un obiect,


în concluzie, în toate cazurile câştigul este 0.

b) Dacă nu transportăm nici un obiect, atunci, indiferent de capacitate, câştigul


obţinut este 0.

c) Cazul corespunde deciziei de a alege sau nu pentru greutatea j produsul i.


Dacă pentru greutatea j, se alege produsul i, atunci câştigul care se obţine este
suma dintre câştigul maxim obţinut pentru capacitatea j-Gr(i) la care se adaugă
câştigul obţinut din transportul obiectului i. Dacă nu se alege spre transport
obiectul i, atunci ne mulţumim cu câştigul maxim obţinut pentru greutatea j, în
cazul în care se transportă doar obiecte alese dintre primele i-1. Evident, alegem
sau nu pentru transport obiectul i în funcţie de câştigul care se obţine, care trebuie
să fie maxim.

Să observăm că matricea Castig se completează pe linii.

Exemplu: Pentru n=5:


Obiect 1 2 3 4 5
greutate 3 4 3 10 5
castig 2 3 8 12 7

Capacitatea rucsacului este de 10 unităţi de greutate. Persoana va alege obiectele


3 şi 5 şi va obţine un câştig de 15 unităţi monetare. În acest fel se vor transporta
numai 8 unităţi de greutate.

Iată cum arată matricea Castig şi matricea Alege după actualizarea, pe


rând, cu obiectele 1, 2, ..., n:

matricea Castig matricea Alege


158 Capitolul 6. Programare dinamică

Varianta Pascal Varianta C++


var Castig,alege:array[0..50, #include <iostream.h>
0..50] of integer; int Castig[50][50],
Gr,C:array [1..100] of Alege[50][50],Gr[100],C[100],
integer; i,j,n,G,Obiect;
i,j,n,G,Obiect:integer;
begin main()
write ('G='); readln(G); { cout<<"G=";cin>>G;
write ('n='); readln(n); cout<<"n=";cin>>n;
for i:=1 to n do for(i=1;i<=n;i++)
begin { cout<<"Gr["<<i<<"]=";
write ('Gr[',i,']='); cin>>Gr[i];
readln(Gr[i]); cout<<"C["<<i<<"]=";
write ('C[',i,']='); cin>>C[i];
readln(C[i]); }
end; for(i=1;i<=n;i++)
for i:=1 to n do for(j=1;j<=G;j++)
for j:=1 to G do if (Gr[i]<=j)
if Gr[i]<=j then if(C[i]+Castig[i-1][j-Gr[i]]
if C[i]+Castig[i-1, >Castig[i-1][j])
j-Gr[i]]>Castig[i-1,j] { Castig[i][j]=C[i]+
then Castig[i-1][j-Gr[i]];
begin Alege[i][j]=i;
Castig[i,j]:=C[i]+ }
Castig[i-1,j-Gr[i]];
else
Alege[i,j]:=i;
{Castig[i][j]=Castig[i-1][j];
end
else Alege[i][j]=Alege[i-1][j];
begin }
Castig[i,j]:= else
Castig[i-1,j]; {Castig[i][j]=Castig[i-1][j];
Alege[i,j]:= Alege[i][j]=Alege[i-1][j];
Alege[i-1,j] }
end i=n; j=G;
else cout<<"Castig total "
begin <<Castig[i][j]<<endl;
Castig[i,j]:=Castig[i-1,j]; while (Alege[i][j])
Alege[i,j]:=Alege[i-1,j] { Obiect=Alege[i][j];
end; cout<<" Produsul "
i:=n; j:=G; <<Alege[i][j]
writeln('Castig total= ', <<" Greutate "
Castig[i,j]); <<Gr[Alege[i][j]]
while Alege[i,j]<>0 do << " Castig "
begin <<C[Alege[i][j]]<<endl;
Obiect:=Alege[i,j]; while (Obiect==Alege[i][j])
writeln (' Produsul ', { j-=Alege[i][j];
Alege[i,j],' greutate ', i--;
Gr[Alege[i,j]],' castig ', }
C[Alege[i,j]]);
}
while Obiect=Alege[i,j] do
}
begin
j:= j-Gr[Alege[i,j]];
i:=i-1;
end;
end
end.
Manual de informatică pentru clasa a XI-a 159

 Enunţ (Varianta 2). La fel ca la prima variantă, numai că se presupune că se


dispune de un număr nelimitat de obiecte de un anumit tip. Pentru fiecare tip de
obiect se cunoaşte greutatea unui exemplar şi câştigul obţinut prin transportul său
la destinaţie.

 Rezolvare. Pare mai complicat, dar, în realitate, este algoritmul de la problema


anterioară, simplificat. Matricele Castig şi Alege devin vectori cu G+1
componente. Se parcurg vectorii de mai sus, o dată pentru tipul de obiect 1, apoi
pentru tipul de obiect 2, ... şi, la sfârşit, pentru tipul de obiect n. La fiecare
parcurgere se urmăreşte să se încarce optim greutăţile 1,2,...,G.

Spre deosebire de algoritmul anterior, unde, la pasul i, se actualizează


încărcarea greutăţii j, pornind de la greutatea j-G[i], încărcată optim cu obiecte
de tipuri 1, 2, ..., i-1, aici se actualizează greutatea j-G[i], încărcată optim cu
produsele 1, 2, ..., i-1, i. Aceasta face să se selecteze mai multe produse de
acelaşi tip. Evident, există posibilitatea să obţinem un câştig general mai mare
decât la problema precedentă.

Pentru exemplul anterior, puteţi observa mai jos conţinuturile vectorilor


Castig şi Alege, după rulare. Soluţia aleasă va fi: Castig 24, se alege de 3 ori
obiectul 3.

Programul este prezentat mai jos:

Varianta Pascal Varianta C++


var Castig,Alege:array[0..100] #include <iostream.h>
of integer;
Gr,C:array [1..100] of int Castig[100],Alege[100],
integer; Gr[100],C[100],i,j,n,G;
i,j,n,G:integer;
begin main()
write ('G='); readln(G); { cout<<"G="; cin>>G;
write ('n='); readln(n); cout<<"n="; cin>>n;
for i:=1 to n do for(i=1;i<=n;i++)
begin { cout<<"Gr["<<i<<"]=";
write('Gr[',i,']='); cin>>Gr[i];
readln(Gr[i]); cout<<"C["<<i<<"]=";
write ('C[',i,']='); cin>>C[i];
readln(C[i]); }
end;
160 Capitolul 6. Programare dinamică

for i:=1 to n do for(i=1;i<=n;i++)


for j:=1 to G do for(j=1;j<=G;j++)
if Gr[i]<=j then if (Gr[i]<=j)
if C[i]+Castig[j-Gr[i]] > if(C[i]+Castig[j-Gr[i]] >
Castig[j] then Castig[j])
begin { Castig[j]=C[i]+
Castig[j]:= Castig[j-Gr[i]];
C[i]+Castig[j-Gr[i]]; Alege[j]=i;
Alege[j]:=i; }
end; j=G;
writeln('Castig total=', cout<<"Castig total "
Castig[G]); <<Castig[G]<<endl;
j:=G; while (Alege[j])
while alege[j]<>0 do { cout<<" Produsul "
begin <<Alege[j]
writeln('obiectul ', <<" Greutate "
alege[j], 'castig ', <<Gr[Alege[j]]
C[alege[j]]); << " Castig "
j:=j-Gr[alege[j]]; <<C[Alege[j]]<<endl;
end j-=Gr[Alege[j]];
end. }
}

 Indiferent de variantă, complexitatea este O(nG).

 O astfel de complexitate se numeşte complexitate pseudopolinomială. În


unele cazuri se obţin soluţii într-un timp foarte scurt, dar... ce ne facem dacă
G este foarte mare, de exemplu G=2n.

Mai jos, puteţi analiza modelul matematic al problemei rucsacului, varianta


discretă, cazul 1. Puteţi interpreta relaţiile?

Se cer X1, X 2 ,...X n ∈ {0,1} , astfel încât:

g1X1 + g 2 X 2 + g3 X 3 + ...gn X n ≤ G g1, g 2 ...gn , G ∈ N*



max f = c 1X1 + c 2 X 2 + c 3 X 3 + ...c n X n , c 1, c 2 ...c n ∈ N*

Chiar dacă matematicienilor le place, uneori, să prezinte unele probleme pe


un exemplu copilăresc, ca în cazul de faţă, unde o persoană are un rucsac şi
încearcă să transporte nişte obiecte, problema are o importanţă uriaşă în
economie.

Analizaţi exemplele următoare:

1) O firmă de transport dispune de un vapor şi într-un port găseşte mai multe


produse pe care le poate transporta în ţara de origine. În urma transportului se pot
obţine anumite câştiguri care se cunosc de la început, pentru fiecare produs în
parte. Iată că în locul rucsacului avem un vapor, deja problema prezintă interes...
economic!
Manual de informatică pentru clasa a XI-a 161

 Exerciţii

1. Cum particularizaţi problema pentru cazul continuu al problemei rucsacului


(revedeţi Greedy)?

2. Cum particularizaţi problema pentru cazul discret forma 1?

3. Cum particularizaţi problema pentru cazul discret forma 2?

2) O firmă poate fabrica mai multe produse: p1, p2, …, pn. În acest scop firma
dispune de un o sumă de bani, S. Pentru fiecare produs se cunoaşte costul
fabricării şi beneficiul obţinut în urma vânzării lui. Ce produse trebuie să fabrice
firma pentru ca beneficiul obţinut să fie maxim?

Problema se rezolvă imediat pornind de la modelul matematic al problemei


rucsacului. Ce semnificaţie au în acest caz constantele din model?

3) O firmă dispune de o sumă de bani S, în vederea investirii ei. Se pot face n


investiţii şi pentru fiecare investiţie în parte se cunoaşte suma necesară. De
asemenea, se ştie că fiecare investiţie aduce, după un număr de ani, un câştig
anual. Ce investiţii trebuie să facă firma astfel încât să-şi maximizeze beneficiile
anuale? Şi această problemă se rezolvă imediat pornind de la modelul matematic
al problemei rucsacului. Ce semnificaţie au în acest caz constantele din model?

6.6. Distanţa Levenshtein

 Enunţ. Se consideră două cuvinte A şi B cu m, respectiv, n caractere. Se cere


să se transforme cuvântul A în cuvântul B prin utilizarea a 3 operaţii:

A – adăugarea unei litere;


M – modificarea unei litere;
S – ştergerea unei litere.

Transformarea se va face prin utilizarea unui număr minim de operaţii. Se va


afişa numărul minim de operaţii şi şirul transformărilor.

Exemplu: A=’IOANA’ (m=5), B=’DANIA’ (n=5).

IOANA 
→
S
OANA →
M
DANA →
A
DANIA
Se va afişa 3 (numărul transformărilor).

Numărul minim de transformări se numeşte distanţa Levenshtein (noţiunea


a fost introdusă, împreună cu algoritmul de calcul, în anul 1965 de către
omul de ştiinţă rus Vladimir Levenshtein).
162 Capitolul 6. Programare dinamică

 Rezolvare. Să observăm că numărul de operaţii necesar conversiei este mai


mic sau egal cu maximul dintre m şi n. De exemplu, dacă m<n, putem modifica
primele m caractere şi şterge ultimele n-m caractere.

Fie şirul optim care transformă pe A în B. Atunci, şirul transformărilor de la A


la Tk este optim. Dacă, prin absurd, nu ar fi optim, înseamnă că există un alt şir cu
număr mai mic de transformări de la A la Tk. Dacă înlocuim acest şir în şirul
transformărilor de la A la B, se obţin mai puţine transformări, deci se contrazice
optimalitatea soluţiei iniţiale. În concluzie, şirul transfomărilor de la A la Tk este
optim.

A → T1 → T2 → ...Tk → ...B

În şirul transformărilor, de la un termen la altul, se trece prin efectuarea unei


singure operaţii dintre: adăugarea, modificarea sau ştergerea unui caracter.
Aceasta înseamnă că doi termeni consecutivi ai şirului diferă printr-un singur
caracter. Mai mult, în şirul transformărilor, nu contează ordinea în care efectuăm
calculele.

De exemplu, un şir optim de transformări de la IOANA la DANIA este şi:

IOANA →
A
IOANIA 
→
S
OANIA →
M
DANIA
pe lângă şirul modificărilor de mai jos:

IOANA 
→
S
OANA 
M
→ DANA →
A
DANIA

În ambele şiruri s-au modificat, adăugat, şters aceleaşi caractere, aflate pe


aceleaşi poziţii în şirul iniţial, doar ordinea diferă. Astfel, în primul şir am adăugat
caracterul I pe poziţia 4, am şters caracterul I de pe prima poziţie, iar caracterul
O de pe poziţia 2 a fost modificat în D. În al doilea şir, am şters caracterul I de pe
prima poziţie, am modificat caracterul O de pe a doua poziţie în D şi am adăugat
caracterul I pe poziţia 4.

Dacă ordinea transformărilor nu contează, atunci vom prefera să calculăm


numărul minim al transformărilor, pornind de la A, şi numărând adăugările,
modificările, şi ştergerile caracterelor de la stânga către dreapta (în şirul iniţial).

Pentru rezolvare, vom presupune că fiecare cuvânt (A, B) începe cu un


caracter vid, pe care-l notăm cu V. În aceste condiţii, avem de efectuat
transformarea:

VA1 A2... Am → VB1 B2... Bn

În acest fel, A va avea m+1 caractere, iar B va avea n+1 caractere.


Manual de informatică pentru clasa a XI-a 163

Pentru calculul distanţei vom utiliza matricea Cost cu m+1 linii şi n+1
coloane. Liniile sunt între 0 şi m+1, iar coloanele între 0 şi n+1. Elementul
Cost[i,j] va reţine numărul mimim al transformărilor primelor i caractere ale
cuvântului A în primele j caractere ale cuvântului B. Mai precis, Cost[i,j]
înseamnă costul obţinerii cuvântului intermediar:

B1 B2 ... Bj Ai+1 Ai+2 ... Am

Avem:

1) Cost[0,j]=j, j=0, 1, 2, ..., n. Semnificaţie: costul transformării caracterului


vid, care precede pe A, în primele j caractere ale cuvântului B, este j (se fac j
adăugări).

2) Cost[i,0]=i, i=0, 1, 2, ..., m Semnificaţie: costul transformării primelor i


caractere ale lui A în caracterul vid care îl precede pe B, este i (se fac i ştergeri).

3) 0<i≤m, 0<j≤n +

Cost[i − 1, j − 1], Ai = B j
Cost[i, j] = 
1 + min{cost(i − 1, j − 1), cost(i, j − 1), cost(i − 1, j)}, altfel

Semnificaţie: la fiecare pas, se compară caracterul aflat în A pe poziţia i (A[i]),


cu cel aflat în B pe poziţia j (B[j]). Dacă se cunosc costurile optime (numărul
minim de operaţii): C[i-1,j-1], C[i,j-1], C[i-1,j], atunci:

A) Dacă caracterul aflat pe poziţia i în A este egal cu caracterul aflat pe


poziţia j în B (Ai=Bj), se sare acel caracter. În acest caz,
Cost[i,j]=Cost[i-1,j-1].

B1B2...Bj-1AiAi+1...Am → B1B2...Bj-1BjAi+1...Am

B) Ai≠Bj. Atunci se efectuează una din operaţiile următoare, mai precis,


cea care are costul cel mai mic:

B1) Adăugarea unui caracter (Bj). Atunci costul total este: 1+Cost[i,j-1].
B1B2...Bj-1Ai+1...Am → B1B2...Bj-1BjAi+1...Am

B2) Ştergerea unui caracter, cel aflat pe poziţia Ai. Atunci costul total este
1+Cost[i-1,j].
B1B2...Bj-1BjAiAi+1...Am → B1B2...Bj-1BjAi+1...Am

B3) Modificarea unui caracter, Ai va fi egal Bj. Atunci costul total este
1+Cost[i-1,j-1].
B1B2...Bj-1AiAi+1...Am → B1B2...Bj-1BjAi+1...Am
164 Capitolul 6. Programare dinamică

Cazul ”IOANA→DANIA”. Costul transformării caracterului vid în caracterul


vid este 0, costul transformării caracterului vid în ”D” este 1, costul
transformării caracterului vid în ”DA” este 2, ..., costul transformării
caracterului vid în ”DANIA” este 5. Apoi, costul transformării caracterului vid
în caracterul vid este 0, costul transformării caracterului ”I” în caracterul vid
este 1, costul transformării caracterelor ”IO” în caracterul vid este 2, ...,
costul transformării caracterelor ”IOANA” în caracterul vid este 5.

VDANIA
V012345
I1
O2
A3
N4
A5

În continuare, completăm matricea pe linii:

C[1,1]. A[1]=I≠D=B[1].

1+min{cost[i-1,j-1],cost[i-1,j],cost[i,j-1]}=1+min{cost[0,0],
cost[0,1],cost[1,0]=1+min(0,1,1)=1.
Semnificaţia: modificarea minimă pentru a transforma ”I” în ”D” are costul 1. Se
face o modificare pentru că (i-1,i-1) trece în (i,j).

VDANIA
V012345
I11
O2
A3
N4
A5
...

În final, matricea este:

VDANIA
V012345
I112334
O222344
A332344
N443234
A554333

Numărul minim de transformări este: cost[m,n]=cost[5,5]=3.

După calculul elementelor matricei, se depistează operaţiile efectuate şi acestea se


afişează în ordine inversă.
Manual de informatică pentru clasa a XI-a 165

Depistarea unei operaţii

Pentru i=5 şi j=5,

min{cost[4,4],cost[4,5],cost[5,4]}= min{3,4,3}.

Alegem 3 (prima valoare minimă din şir). Pentru că avem cost[5,5]=min,


înseamnă că la ultima operaţie s-a efectuat un salt (a fost găsită egalitate).
Acum i=4 şi j=4. Apoi:
min{cost[3,3],cost[3,4],cost[4,3]}=min{3,4,2]=2.
cost[4,4]≠min
Pentru că am efectuat o operaţie de tipul (i,j-1)→(i,j) înseamnă că s-a
adăugat pe poziţia 4 a şirului A caracterul de pe poziţia 4 a şirului B (I). Aceasta
este ultima modificare făcută de algoritm.

Avem i=4, j=3. Depistăm apoi, penultima modificare, ş.a.m.d., până când i=0 şi
j=0. Pentru a afişa modificările în ordine inversă vom utiliza o stivă (Sol). De
asemenea, se poate utiliza recursivitatea.

Varianta Pascal Varianta C++


var A,B:string; #include <iostream.h>
Sol:array[1..100] of #include <string.h>
string; char A[100],B[100],
m,n,i,j,min,k:integer; Sol[100][300],op;
op:char; int m,n,i,j,min,k,
cost:array[0..100,0..100] cost[100][100];
of integer;
void Next(int& i, int& j,
procedure Next(var i,j: char& op)
integer;var op:char); { int l=0,c=0;
var l,c:integer; min=1000;
begin if (i>0 && j>0 &&
l:=0;c:=0;
min>=cost[i-1][j-1])
min:=1000;
{ min=cost[i-1][j-1];
if (i>0) and (j>0) and
l=i-1; c=j-1;
(min>cost[i-1,j-1]) then
op='m';
begin
}
min:=cost[i-1,j-1];
if (i>0 && cost[i-1][j]<min)
l:=i-1;
{ min=cost[i-1][j];
c:=j-1;
l=i-1; c=j;
op:='m';
op='s';
end;
}
if (i>0) and (cost[i-1,j]<min) if (j>0 && cost[i][j-1]<min)
then { min=cost[i][j-1];
begin l=i; c=j-1; op='a';
min:=cost[i-1,j]; }
l:=i-1;c:=j; if (cost[i][j]==min) op='v';
op:='s'; i=l; j=c;
end;
}
166 Capitolul 6. Programare dinamică

if (j>0) and (cost[i,j-1]<min) main()


then { int t;
begin cout<<"A="; cin>>A+1;
min:=cost[i,j-1]; cout<<"B="; cin>>B+1;
l:=i;c:=j-1; m=strlen(A+1);
op:='a'; n=strlen(B+1);
end; cout<<m<<" "<<n<<endl;
if cost[i,j]=min then op:='v'; for (i=0;i<=m;i++)
i:=l;j:=c; cost[i][0]=i;
end; for(j=0;j<=n;j++)
begin cost[0][j]=j;
write('A='); readln(A); for (i=1;i<=m;i++)
write('B='); readln(B); for(j=1;j<=n;j++)
m:=length(A); if(A[i]==B[j])
n:=length(B); cost[i][j]=cost[i-1][j-1];
for i:=0 to m do else
cost[i,0]:=i; { min=cost[i-1][j-1];
for j:=0 to n do if (min>cost[i-1][j])
cost[0,j]:=j; min=cost[i-1][j];
for i:=1 to m do if (min>cost[i][j-1])
for j:=1 to n do min=cost[i][j-1];
if A[i]=B[j] cost[i][j]=1+min;
then }
cost[i,j]:=cost[i-1,j-1] cout<<"Distanta "
else <<cost[m][n]<<endl;
begin i=m;
min:=cost[i-1,j-1]; j=n;
if min>cost[i-1,j] then while(i+j)
min:=cost[i-1,j]; { Next(i,j,op);
if min>cost[i,j-1] then if (op!='v')
min:=cost[i,j-1]; { int x=0;
cost[i,j]:=1+min; k++;
end; Sol[k][x++]=op;
writeln ('Distanta=', Sol[k][x++]=' ';
cost[m,n]); for (t=1;t<=j;t++)
i:=m; j:=n; Sol[k][x++]=B[t];
while i+j<>0 do for (t=i+1;t<=m;t++)
begin Sol[k][x++]=A[t];
Next(i,j,op); }
if op<>'v' }
then for(k=cost[m][n];k>=1;k--)
begin cout<<Sol[k]<<endl;
k:=k+1; cout<<B+1;
Sol[k]:=op+' }
'+copy(b,1,j)+
copy(a,i+1,m-i);
end
end;
for k:= cost[m,n] downto 1 do
writeln(Sol[k]);
writeln(B);
end.

După cum se poate observa, algoritmul are complexitatea O(n2).


Manual de informatică pentru clasa a XI-a 167

 Aplicaţie. Un profesor de informatică doreşte să-şi dea seama în ce măsură, la


o lucrare în care elevilor li s-a cerut să elaboreze un program, aceştia s-au
"inspirat" unul de la altul. Profesorul a pornit de la observaţia că elevii "spaţiază"
altfel programul şi schimbă numele unor variabile. Ideea care i-a venit constă în a
prelua programul, eliminând toate blank-urile şi a forma astfel, din tot programul, un
şir de caractere. La fel procedează şi cu alt program. În final, calculează distanţa
Levenshtein între cele două programe. Evident, cu cât costul transformării unui şir
(program) în altul este mai mic, cu atât suspiciunea de inspiraţie este mai mare.
Puteţi să scrieţi programul care realizează această prelucrare? Programele le găsiţi
în fişiere text, cu extensia .pas sau .cpp, aşa cum le scrieţi în mod obişnuit!

6.7. Înmulţirea optimă a unui şir de matrice

Presupunem că avem de înmulţit două matrice: An,p cu Bp,m. În mod evident,


rezultatul va fi o matrice Cn,m. Se pune problema de a afla câte înmulţiri au fost
făcute pentru a obţine matricea C. Prin înmulţirea liniei 1 cu coloana 1 se fac p
înmulţiri, întrucât au p elemente. Dar linia 1 se înmulţeşte cu toate cele m coloane,
deci se fac m*p înmulţiri. În mod analog se procedează cu toate cele n linii ale
matricei A, deci se fac n*m*p înmulţiri. Reţinem acest rezultat.

Să considerăm produsul de matrice A1×A2×...×An (A1(d1,d2),


A2(d2,d3),..., An(dn,dn+1)). Se cunoaşte că legea de compoziţie produs
de matrice nu este comutativă, în schimb este asociativă. De exemplu,
dacă avem de înmulţit trei matrice A, B, C produsul se poate face în
două moduri: (AxB)xC sau Ax(BxC).

Este interesant de observat că nu este indiferent modul de înmulţire a celor


n matrice. Să considerăm că avem de înmulţit patru matrice A1(10,1), A2(1,10),
A3(10,1), A4(1,10).

Pentru înmulţirea lui A1 cu A2 se fac 100 de înmulţiri şi se obţine o matrice cu


10 linii şi 10 coloane. Prin înmulţirea acesteia cu A3 se fac 100 de înmulţiri şi se
obţine o matrice cu 10 linii şi o coloană. Dacă această matrice se înmulţeşte cu A4,
se fac 100 de înmulţiri. În concluzie, dacă acest produs se efectuează în ordine
naturală, au loc 300 de înmulţiri.

Să efectuăm acelaşi produs în ordinea care rezultă din expresia


A1×((A2×A3)×A4).
Efectuând produsul A2 cu A3 se efectuează 10 înmulţiri şi se obţine o matrice
cu o linie şi o coloană. Această matrice se înmulţeşte cu A4, se fac 10 înmulţiri şi
se obţine o matrice cu 1 linie şi 10 coloane. Dacă o înmulţim pe aceasta cu prima,
efectuăm 100 de înmulţiri, obţinând rezultatul final cu numai 120 de înmulţiri.
În concluzie, apare o problemă foarte interesantă şi anume de a afla modul în
care trebuie să se înmulţească cele n matrice, astfel încât numărul de înmulţiri să
fie minim.
168 Capitolul 6. Programare dinamică

Să vedem, mai întâi, în câte moduri se poate calcula un produs de n astfel


de matrice. Pentru n=4, putem avea:
((A1×A2)×(A3×A4)); (((A1×A2)×A)3×A4); ((A1×(A2×A3))×A4);
(A1×(A2×(A3×A4))); (A1×((A2×A3)×A4)).

Astfel, pentru n=4 avem 5 posibilităţi de calcul al acestui produs. Să observăm că


pentru produse de n matrice, sunt necesare n-1 perechi de paranteze.

 Exerciţiu. Puteţi demonstra prin inducţie acest rezultat?


Ţinând cont de aceasta, se poate formula problema următoare: în câte feluri
se pot combina n perechi de paranteze (desigur, pentru problema dată avem n-1
perechi)? Cei pasionaţi de informatică pot studia numărarea arborilor binari,
autoinstruire (vezi Capitolul 9, problema propusă 23). Acum ne limităm să spunem
că acest număr este:
1
C 2nn
n +1
şi că, în general, un astfel de număr este foarte mare.

 Exerciţiu. Calculaţi acest număr pentru n=10, 11, ..., 20.


Prin urmare, un algoritm în care calculăm în toate modurile posibile acest
produs este ineficient.
Din fericire, pentru această problemă există o rezolvare polinomială, prin
utilizarea programării dinamice. Să presupunem că produsul Ai×Ai+1×...Aj s-a
calculat optim. În final, s-au înmulţit două matrice (Ai×...×Ak)×(Ak+1×...×Aj).
Atunci, produsele Ai×...Ak şi Ak+1×...×Aj au fost calculate optim. De ce?
Demonstraţi prin reducere la absurd.

Pentru rezolvare, vom aplica principiul al 3-lea al programării dinamice.

În vederea rezolvării problemei, reţinem o matrice A cu n linii şi n coloane.


Elementul A(i,j), i<j, reprezintă numărul minim de înmulţiri pentru efectuarea
produsului Ai×Ai+1×...×Aj. De asemenea, numărul liniilor şi al coloanelor celor n
matrice sunt reţinute într-un vector DIM cu n+1 componente. Pentru exemplul
nostru DIM reţine următoarele valori: 10, 1, 10, 1, 10.

Pentru rezolvare se ţine cont de următoarele relaţii existente între


componentele matricei A:

1)A(i,i) = 0;
2)A(i,i + 1) = DIM(i) × DIM(i + 1) × DIM(i + 2);
3)A (i, j) = min{A (i,k ) + A (k + 1, j) + DIM(i) × DIM(k + 1) × DIM( j + 1)}.
i≤k < j
Manual de informatică pentru clasa a XI-a 169

Justificarea acestor relaţii este următoarea:

1) o matrice nu se înmulţeşte cu ea însăşi, deci se efectuează 0 înmulţiri;

2) liniile şi coloanele matricei Ai se găsesc în vectorul DIM pe poziţiile i şi i+1, iar


ale matricei Ai+1, pe poziţiile i+1 şi i+2;

3)

 înmulţind matricele Ai×Ai+1×Ak, se obţine o matrice cu un număr de linii egal


cu acela al matricei Ai (DIM(i)) şi cu un număr de coloane egal cu acela al
matricei Ak (DIM(k+1));

 înmulţind matricele Ak+1×...×Aj, se obţine o matrice cu un număr de linii


egal cu acela al matricei Ak+1 (DIM(k+1)) şi cu un număr de coloane egal
cu acela al matricei Aj (DIM(j+1));

 prin înmulţirea celor două matrice se obţine matricea rezultat al înmulţirii


Ai×...×Aj, iar pentru această înmulţire de matrice se efectuează DIM(i)
×DIM(k+1)×DIM(j+1) înmulţiri.

Observaţii

 Relaţia sintetizează faptul că pentru a obţine numărul de înmulţiri optim


pentru produsul Ai×...×Aj se înmulţesc două matrice, una obţinută ca
produs optim între Ai×...×Ak şi cealaltă obţinută ca produs optim între
Ak+1×...×Aj, în ipoteza în care cunoaştem numărul de înmulţiri necesar
efectuării acestor două produse, oricare ar fi k cuprins între limitele date.

 Această observaţie este o consecinţă directă a programării dinamice şi anume


că produsul efectuat optim între matricele prezentate se reduce în ultimă
instanţă la a efectua un produs între două matrice cu condiţia ca acestea să
fie calculate optim (produsul lor să aibă un număr minim de înmulţiri).

 Să observăm faptul că orice secvenţă pentru calculul costului optim este de


forma Ai×Ai+1×...×Aj. Astfel, întotdeauna j≥i. Pentru a reţine costul optim,
A(i,j) vom utiliza o matrice. Cum j≥i înseamnă că din această matrice se
va utiliza numai partea situată deasupra diagonalei principale.

 Din relaţia 1, rezultă că mai întâi trebuie completate cu 0, elementele aflate


pe diagonala principală. Dacă matricea este declarată ca variabilă globală,
elementele ei sunt oricum iniţializate cu 0.

 Din relaţia 2, rezultă că, în continuare, trebuie completate elementele de


coordonate (i,i+1). Toate acestea sunt situate în partea aflată deasupra
diagonalei principale, pe o paralelă la diagonala principală ("cea mai
apropiată").
170 Capitolul 6. Programare dinamică

 Din relaţia 3, rezultă că pentru a afla costul optim pentru produsul


Ai×Ai+1×...×Aj, reţinut de A(i,j), se fac j-i comparări, prin care
produsul se descompune în alte două produse, al căror cost optim este deja
cunoscut şi se alege descompunerea care asigură costul minim. Elementele
necesare din matricea costurilor optime se observă în tabelul de mai jos:

k Produsele Elemente necesare


i (Ai)×Ai+1×...×Aj A(i,i) , A(i+1,j)
i+1 (Ai×Ai+1)×Ai+2×...×Aj A(i,i+1), A(i+2,j)
... ... ...
j-1 (Ai×Ai+1×...× Aj-1)×Aj A(i,j-1), A(j,j)

Din tabel se observă că pentru calculul lui A(i,j) sunt necesare:

a) A(i,i), A(i,i+1), ..., A(i,j-1) - adică toate elementele din matricea


A(i,j), aflate pe linia i, până la coloana j.
b) A(i+1,j), A(i+2,j), ..., A(j,j) - adică toate elementele din matricea
A(i,j), aflate pe coloana j, până la linia i.

De exemplu, dacă n=5, alăturat puteţi observa


elementele implicate în calculul costului optim  0 a1, 2 a1,3 a1, 4 a1,5 
 
a(2,5). Întrebarea este: pentru ce au fost
 0 a2 , 3 a2 , 4 a2 , 5 
prezentate toate aceste amănunte? Răspuns:
pentru a calcula costul optim, reţinut de A(i,j),  0 a3, 4 a3,5 
este necesar ca elementele matricei A să fie  
completate pe diagonala principală, apoi pe cea  0 a 4,5 
mai apropiată paralelă de aceasta, apoi, din
nou, pe cea mai apropiată diagonală paralelă cu 
 0 
ultima completată, până se ajunge să se completeze A(1,n) care va reţine costul
optim pentru problema cerută. În acest fel, pentru orice element care se
calculează, elementele necesare au fost deja determinate.

Se pune problema să aflăm cum putem efectua acest calcul utilizând relaţiile
prezentate. Pentru exemplificare vom utiliza exemplul dat la începutul acestui
paragraf. Datorită relaţiei 1, diagonala principală a matricei A (cu 4 linii şi 4
coloane) va fi alcătuită numai din elemente având valoarea 0.

 Rămâne să determinăm modul de generare a elementelor de pe paralelele


la diagonala principală, mai precis pe cele aflate deasupra diagonalei
principale. Astfel:

- pentru prima "paralelă": A(1,2), A(2,3), ..., A(n-1,n);


- pentru a doua "paralelă": A(1,3), A(2,4), ..., A(n-2,n);
- pentru a treia "paralelă": A(1,3) A(2,5), …, A(n-3,n);
...
- pentru ultima "paralelă": A(1,n).
Manual de informatică pentru clasa a XI-a 171

Observăm că:

- pentru prima paralelă, liniile sunt între 1 şi n-1;


- pentru a doua paralelă, liniile sunt între 1 şi n-2;
- pentru a treia paralelă, liniile sunt între 1 şi n-3;
...
- pentru ultima paralelă liniile sunt între 1 şi 1.

Dacă notăm linia cu i şi coloana cu j, atunci secvenţa:

pentru l de la 1 la n-1
pentru i de la 1 la n-l
j←l+i

generează pentru l=1 linii de la 1 la n-1, pentru l=2, linii de la 1 la n-2, ..., pentru
l=n-1, linia 1. Să observăm faptul că pentru fiecare l şi i, coloana este j. În
acest fel, pentru o anumită valoare a lui l, se obţine o paralelă la diagonala
principală. De exemplu, dacă l=1, i=1, j=2, se obţine A(1,2), dacă l=1, i=2,
j=3, se obţine A(2,3), ..., iar dacă l=1, i=n-1, j=n, se obţine A(n-1,n), adică
prima paralelă la diagonala principală.

Revenim la exemplul dat la începutul acestui paragraf.

Iniţial se pot calcula numai elementele A(i,i+1),


 0 100 x x 
adică A(1,2), A(2,3), A(3,4) - elemente situate pe  
o paralelă la diagonala principală a matricei A. În  x 0 10 x 
A= .
concluzie, avem A(1,2)=100, A(2,3)=10, A(3,4)= x x 0 100 
100. Matricea A va arăta ca alăturat:  
x x 0 
 x
În continuare calculăm:

A (1,3 ) = min {A (1, k ) + A (k + 1,3 ) + DIM(1) × DIM(k + 1) × DIM(4 )} =


1≤k <3
= min{0 + 10 + 10 × 1 × 1,100 + 0 + 10 × 10 × 1} = 20;
A (2,4 ) = min {A (2, k ) + A (k + 1,4 ) + DIM(2) × DIM(k + 1) × DIM(5 )} =
2 ≤k < 4
= min{0 + 100 + 1 × 10 × 10,10 + 0 + 1 × 1 × 10} = 20;
A (1,4 ) = min {A (1, k ) + A (k + 1,4 ) + DIM(1) × DIM(k + 1) × DIM(5 )} =
1≤k < 4
= min{0 + 20 + 10 × 1× 10,100 + 100 + 10 × 10 × 10,20 + 0 + 10 × 1× 10} = 120;

În concluzie, pentru exemplul nostru, se fac minimum 120 de înmulţiri, rezultat luat
din matricea A şi anume A(1,4):

 0 100 20 120 
 
 x 0 10 20 
A= .
x x 0 100 
 
x x x 0 
172 Capitolul 6. Programare dinamică

Mai avem de lămurit o problemă. Chiar dacă cunoaştem costul optim


(numărul minim de înmulţiri), cum determinăm în ce mod se înmulţesc matricele
pentru a obţine acest cost? Să observăm că elementele din matrice, situate sub
diagonala principală, au fost neutilizate. De asemenea, pentru calculul optim al
unui produs Ai×Ai+1×Ai+2×...×Aj se determină un anumit k. Aşa cum am arătat, k
exprimă ordinea de înmulţire a matricelor. De exemplu, dacă k=i+1, avem
produsul (Ai×Ai+1)×Ai+2×...×Aj, înţelegând prin aceasta că produsul se obţine
ca produs între matricele Ai×Ai+1 şi Ai+2×...×Aj, produse pe care ştim să le
obţinem în mod optim. Dar, dacă în urma acestui calcul am determinat k, astfel
încât produsul obţinut să fie optim, această valoare poate fi reţinută de A(j,i).
Pentru exemplul nostru, matricea A este prezentată mai jos:

 0 100 20 120 
 
 1 0 10 20 
A= .
1 2 0 100 
 
1 3 0 
 3

Grafic, valorile se pot prezenta într-un arbore. Întrucât arborii vor fi descrişi
într-un capitol separat, vă rog să reveniţi după parcurgerea acelui capitol la
această problemă.

Priviţi arborele următor:

(1,4)

(1,1) (2,4)

(2,3) (4,4)

(2,2) (3,3)

Figura. 6.1. Reprezentarea valorilor sub forma unui arbore

Pentru produsul matricelor de la 1 la 4, se obţine k=1 (A(4,1)=1). Aceasta


înseamnă că produsul se va efectua astfel: A1×(A2×A3×A4). Pentru produsul
A2×A3×A4 avem k=3. Aceasta înseamnă că produsul va fi calculat astfel:
(A2×A3)×A4. Prin urmare, produsul celor 4 matrice se va calcula în felul următor:
(A1×((A2×A3)×A4)).

Dacă listăm nodurile neterminale ale acestui arbore, arborele fiind parcurs în
postordine, obţinem modul în care se aşează parantezele pentru calculul
produsului de matrice. Pentru exemplul nostru, subprogramul parc afişează
(2,3), (2,4), (1,4). Aceasta ne spune cum să aranjăm parantezele.
Manual de informatică pentru clasa a XI-a 173

Varianta Pascal Varianta C++


var i,n:longint; #include <iostream.h>
dim:array[1..10] of longint; long i,n,dim[10],a[10][10];
a:array[1..10,1..10] of
longint; void parc (int l, int c)
{ int k=a[c][l];
procedure parc( l,c:longint); if (k!=l) parc(l,k);
var k:integer; if (k+1!=c) parc(k+1,c);
begin cout<<l<<" "<<c<<endl;
k:=a[c,l]; }
if k<>l then parc(l,k);
if (k+1)<>c then parc(k+1,c); void costopt ()
writeln(l,' ',c); { long k,i,j,l,m;
end; for (l=1;l<=n-1;l++)
for (i=1;i<=n-l;i++)
procedure costopt; { j=i+l;
var k,i,j,l,m:longint; a[i][j]=100000;
begin for (k=i;k<=j-1;k++)
for l:=1 to n-1 do { m=a[i][k]+a[k+1][j]+
for i:=1 to n-l do dim[i]*dim[k+1]*
begin dim[j+1];
j:=i+l; if (a[i][j]>m)
a[i,j]:=100000; { a[i][j]=m;
for k:=i to j-1 do a[j][i]=k;
begin }
m:=a[i,k]+a[k+1,j]+ }
dim[i]*dim[k+1]* }
dim[j+1]; cout<<"cost optim "
if a[i,j]>m then <<a[1][n]<<endl;
begin }
a[i,j]:=m;
a[j,i]:=k; main()
end { cout<<"n="; cin>>n;
end for (i=1;i<=n+1;i++)
end; { cout<<"d=";
writeln ('cost optim ', cin>>dim[i];
a[1,n]); }
end; costopt();
parc(1,n);
begin
write('n='); readln(n); }
for i:=1 to n+1 do
begin
write('d=');
readln(dim[i]);
end;
costopt;
parc(1,n);
end.

Complexitatea algoritmului este O(n3). Observaţi că există trei cicluri for


imbricate!
174 Capitolul 6. Programare dinamică

6.8. Probleme cu ordinea lexicografică a permutărilor

 Problema 1. Se citeşte n, număr natural, şi o permutare a


numerelor 1,2,...,n. Se cere să se afişeze numărul de ordine al
permutării, dacă se consideră permutările în ordinea lexicografică.
Alăturat, observaţi permutările mulţimii {1,2,3}, listate în ordinea
lexicografică.

Prima idee este să generăm prin Backtracking, în ordine


lexicografică, toate permutările, până la întâlnirea permutării date şi
în paralel să le numărăm. Când am întâlnit-o, afişăm numărul de ordine. Dar, avem
n! permutări. Cum n!=1×2×...n>2×2×2...2=2n-1, observăm că algoritmul este
exponenţial. Evident, renunţăm la o astfel de soluţie.

 Rezolvare. Să observăm primul element afişat al fiecărei permutări. Astfel,


există două permutări care afişează 1 ca prim element, două permutări care
afişează 2, ca prim element şi 2 permutări care afişează 3 ca prim element. Dacă
analizăm şi şirul permutărilor observăm că, în general: elementul 1 este afişat
de (n-1)! ori, elementul 2 este afişat de (n-1)! ori, ..., elementul n este afişat de
(n-1)! ori.

De fapt,

(n − 1)!+(n − 1)!+... + (n − 1)! = n! .



de n ori

Această simplă observaţie ne permite să scriem o relaţie de recurenţă, unde


NR(P(n)) înseamnă numărul de ordine al permutării iniţiale, iar NR(P(n-1))
înseamnă numărul de ordine al permutării alcătuite din ultimele n-1 elemente ale
permutării iniţiale. Dacă permutarea este a1 a2... an, atunci:

(n - 1)! (a1 - 1) + NR(P(n - 1)), n > 1


NR(P(n)) = 
1 n =1

Să observăm faptul că permutarea formată cu ultimele n-1 elemente ale


permutării iniţiale este formată din elementele de la 1 la n, din care lipseşte exact
un element. Din ea, se poate obţine permutarea cu acelaşi număr de ordine
(ordinea lexicografică) a mulţimii {1,2,...,n-1}, dacă din orice element mai
mare decât cifra eliminată se scade 1 (această operaţie nu afectează ordinea
lexicografică!).

Exemplu: pentru n=5, P=4 5 2 3 1.


P=4!3+NR({5,2,3,1})=72+NR({4,2,3,1})=72+3!*3+NR({2,3,1})=
90+2!1+NR({3,1})=92+NR({2,1})=92+1!1+P(1)=93+1=94.
Manual de informatică pentru clasa a XI-a 175

Varianta Pascal Varianta C++


var P:array[1..10] of integer; #include <iostream.h>
n,i,k,Nr,Prod:integer; int P[10],n,i,k,Nr,Prod;
procedure Numar;
var nsalv:integer; void Numar()
begin { int nsalv=n;
nsalv:=n; while (n>1)
while n>1 do { Prod/=n;
begin Nr+=(P[k]-1)*Prod;
Prod:=Prod div n; for (i=k+1;i<=nsalv;i++)
Nr:=Nr+(P[k]-1)*Prod; if (P[i]>P[k]) P[i]--;
for i:=k+1 to nsalv do k++;n--;
if P[i]>P[k] then }
P[i]:=P[i]-1; Nr++;
k:=k+1; n:=n-1; }
end;
Nr:=Nr+1; main()
end; { cout<<"n=";cin>>n;
begin for(i=1;i<=n;i++) cin>>P[i];
write ('n='); readln(n); Prod=1;
for i:=1 to n do readln(P[i]); for(i=2;i<=n;i++) Prod*=i;
Prod:=1; k=1;
for i:=2 to n do Prod:=Prod*i; Numar();
k:=1; cout<<Nr;
Numar; }
writeln(Nr);
end.

Algoritmul are complexitatea O(n2). Justificaţi!

 Problema 2. Se citeşte n>0, număr natural, şi un număr natural 1≤Nr≤n! Care


este permutarea care, în ordine lexicografică, are numărul de ordine Nr?
Exemplu: pentru n=5 şi Nr=94, se va afişa 4 5 2 3 1.

 Rezolvare. Ştim că, în şirul tuturor permutărilor, fiecare element al permutării


apare pe prima poziţie de exact (n-1)! ori. În cazul nostru, (n-1)!=4!=24. Cum
primele (în ordine lexicografică) (n-1)! permutări au elementul 1 pe prima poziţie,
următoarele (n-1)! permutări au elementul 2 pe prima poziţie, înseamnă că:
Primul_element=1+[(Nr-1)/(n-1)!],
unde prin [x] am notat parte întreagă din x.
Observaţie foarte importantă. Dacă primul element al permutării poate fi depistat
ca mai sus, cu următoarele trebuie efectuate operaţii în plus, pentru că se selectează
elementele unei permutări ale cărei elemente nu sunt numere consecutive. De
exemplu, dacă pentru n=5 se selectează ca prim element numărul 3, atunci
următorul element trebuie selectat din mulţimea {1,2,4,5}. În acest caz,
algoritmul va furniza indicele elementului în vector şi nu elementul propriu-zis.
Astfel, dacă elementele ar ocupa în vectorul P poziţii consecutive, acesta ar trebui
să fie P(1,2,4,5) şi dacă algoritmul returnează 3, ar trebui selectat 4.
176 Capitolul 6. Programare dinamică

Datorită celor arătate, vom prefera să lucrăm cu 2


vectori, ca alăturat. Vectorul P reţine numerele 1, 2, P 1 2 3 4 5
..., n, iar O[i] reţine 1 dacă elementul O 0 0 0 0 0
corespunzător din P a fost selectat, şi 0 în caz
contrar. Astfel, dacă algoritmul returnează valoarea k, va trebui afişat al k-lea element
neselectat, după care O[k] va reţine 1. Exemplul care urmează vă va lămuri.

Avem n=5 şi Nr=94.

(94-1)/24+1=3+1=4. Numărul rămas este 94-3*24=94-72=22. Pentru că


elementul selectat este 4 (al 4-lea în ordine), marcăm în vectorul O acest fapt. În
acest mod, problema s-a redus la o alta, mai simplă: care este permutarea cu 4
elemente, 1, 2, 3, 5 care, în ordine lexicografică, are numărul de ordine 22?

P 1 2 3 4 5
O 0 0 0 1 0

Avem: (n-1)!=3!=6. La fel: (22-1)/6+1=3+1=4. Al 4-lea element


neselectat este 5. Îl selectăm pentru a-l afişa şi-l marcăm în O. Până în acest
moment am afişat 4 şi 5. Numărul rămas este 22-3*6=22-18=4.

P 1 2 3 4 5
O 0 0 0 1 1

Problema s-a redus la o alta, mai simplă: care este permutarea cu 3


elemente, 1, 2, 3 care, în ordine lexicografică, are numărul de ordine 4?
(n-1)!=2!=2.
(4-1)/2+1=2.
Numărul rămas este 4-1*2=2.
Selectăm al doilea element neselectat, adică 2 şi afişăm elementul. Astfel,
am afişat 4, 5, 2.

P 1 2 3 4 5
O 0 1 0 1 1
Apoi:
P 1 2 3 4 5
(n-1)!=1!=1.
(2-1)/1+1=2. O 0 1 1 1 1

Numărul rămas este 2-1*1=1. Afişăm 3, pentru că este al doilea element


neselectat, după care marcăm în O elementul 3. Am afişat astfel 4, 5, 2 şi 3.
Afişăm ultimul element neafişat, adică 1. Am obţinut 4, 5, 2, 3 şi 1.
Manual de informatică pentru clasa a XI-a 177

Varianta Pascal Varianta C++


var n,Nr,i,k,fact,ic, #include <iostream.h>
nsalv:integer; int n,Nr,i,k,fact,ic,
P,O:array[1..10] of nsalv,P[10],O[10];
integer; main()
begin { cout<<"n="; cin>>n;
write ('n='); readln(n); cout<<"Nr="; cin>>Nr;
write ('Nr='); readln(Nr); fact=1;
fact:=1; for(i=1;i<=n;i++)
for i:=1 to n do { P[i]=i;
begin fact*=i;
P[i]:=i; }
fact:=fact*i; nsalv=n;
end; while (Nr!=1)
nsalv:=n; { fact/=n;
while Nr<>1 do n--;
begin k=(Nr-1)/fact+1;
fact:=fact div n; Nr-=fact*(k-1);
n:=n-1; ic=0;i=1;
k:=(Nr-1) div fact+1; while(ic<k)
Nr:=Nr-fact*(k-1); { if (O[i]==0) ic++;
ic:=0;i:=1; i++;
while ic<k do }
begin O[i-1]=1;
if O[i]=0 then cout<<P[i-1];
ic:=ic+1; }
i:=i+1; for(i=1;i<=nsalv;i++)
end; if(O[i]==0) cout<<P[i];
O[i-1]:=1; }
write(P[i-1]);
end;
for i:=1 to nsalv do
if O[i]=0 then
write (P[i]);
end.

Algoritmul are complexitatea O(n2). Justificaţi!

6.9. Numărul partiţiilor unei mulţimi cu n elemente

 Enunţ. Se citeşte n, număr natural. Se cere ca programul să afişeze numărul


partiţiilor unei mulţimi cu n elemente.

 Rezolvare. Prima idee care ne vine în minte este să generăm toate partiţiile
unei mulţimi (vedeţi generarea lor prin Backtracking) şi să le numărăm. Dar
numărul partiţiilor este aşa de mare astfel încât o asemenea idee se dovedeşte
dezastruasă. Pornind de la partiţiile unei mulţimi cu n elemente, observaţi, pentru
n=3, cum se pot obţine toate partiţiile unei mulţimi cu n+1 elemente.
178 Capitolul 6. Programare dinamică

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

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

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

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

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


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

Ideea de bază este ca, din fiecare partiţie a mulţimii de n elemente, să se


genereze toate partiţiile care provin din ea, ale mulţimii de n+1 elemente. Aceasta
se obţine dacă se adaugă pe rând, la fiecare mulţime a partiţiei, elementul n+1 şi,
la sfârşit, acesta va forma singur o mulţime a partiţiei. Vedeţi mai sus!
Vom nota prin S(n,k) numărul partiţiilor unei mulţimi cu n elemente, partiţii
în care numărul mulţimilor este k. De exemplu, pentru n=3, avem: S(3,1)=1,
S(3,2)=3 şi S(3,3)=1. În aceste condiţii, numărul partiţiilor unei mulţimi cu 3
elemente este: S(3,1)+S(3,2)+S(3,3)=1+3+1=5. De aici rezultă că, pentru a
calcula numărul partiţiilor unei mulţimi cu n+1 elemente, trebuie să calculăm suma:
S(n+1,1)+S(n+1,2)+...+S(n+1,n+1).

Dacă am găsi o modalitate de calcul pentru S(n,k), atunci problema ar


fi rezolvată.
a) Să observăm că, pentru orice n, S(n,1)=1, adică avem o singură partiţie în care
toate mulţimile au un singur element: {1}, {2}, {3}, …, {n}. Tot aşa, S(n,n)=1,
pentru că avem o singură partiţie în care mulţimile au n elemente: {1,2,...,n}.
b) Urmărim să găsim o relaţie de recurenţă pentru S(n+1,k), adică să găsim
numărul partiţiilor unei mulţimi cu n+1 elemente, partiţii în care toate mulţimile au k
elemente, în condiţiile în care cunoaştem S(n,1), S(n,2) ...,S(n,n).
b1) Dacă cunoaştem numărul partiţiilor unei mulţimi cu n elemente, în care fiecare
partiţie are k submulţimi, S(n,k), atunci putem forma partiţii alcătuite din exact k
mulţimi ale mulţimii cu n+1 elemente. Pentru fiecare partiţie numărată de S(n,k),
adăugăm elementul n+1 în prima mulţime a partiţiei, apoi în a doua mulţime, ..., la
sfârşit în a k-a mulţime a partiţiei. În acest fel, din fiecare partiţie se obţin alte k partiţii.
În concluzie, vom obţine în acest mod k*S(n,k) partiţii. În exemplul dat, observaţi
cum din cele 3 (S(3,2)) partiţii cu 2 submulţimi ale mulţimii cu 3 elemente, am
obţinut 2*S(3,2)=6 partiţii cu 3 submulţimi ale unei mulţimi cu 4 elemente.
Manual de informatică pentru clasa a XI-a 179

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

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

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

b2) Dacă cunoaştem numărul partiţiilor unei mulţimi cu n elemente, partiţii care
sunt alcătuite din k-1 mulţimi, atunci din fiecare astfel de mulţime se poate obţine
o altă partiţie cu k mulţimi ale mulţimii cu n+1 elemente, adăugând la fiecare
partiţie mulţimea alcătuită din elementul n+1. În concluzie, în acest mod vom
obţine alte S(n,k-1) partiţii cu k submulţimi ale unei mulţimi cu n+1 elemente. În
exemplul dat, avem:
{1,2,3} {1,2,3} {4}

Cum alte posibilităţi de obţinere a partiţiilor unei mulţimi cu k clase ale unei
mulţimi cu n+1 elemente nu există şi cum, astfel obţinute, partiţiile nu se repetă,
relaţia este:
S(n+1,k)=S(n-1,k)+k*S(n,k).

Pentru exemplul dat, S(4,2)=S(3,3)+3*S(3,2)=1+2*3=7.

Pentru a scrie programul, completăm, mai întâi prima coloană a matricei cu


1 (pentru că avem S(n,1)=1). Prima linie va avea numai un 1 în prima coloană,
pentru că, evident, S(1,k)=0, pentru k>1. În continuare, completăm pe linii
elementele matricei S. În final, facem suma pe linia n.

Varianta Pascal Varianta C++


var S:array[1..20,1..20] of #include <iostream.h>
longint; long S[20][20],n,k,i,j,s;
n,k,i,j,suma:longint; main()
begin { cout<<"n="; cin>>n;
write('n='); readln(n); for (i=1;i<=n;i++) S[i][1]=1;
for i:=1 to n do S[i,1]:=1; S[i][i]=1;
for i:=2 to n do for(i=2;i<=n;i++)
for j:=2 to i do for (j=2;j<=i;j++)
S[i,j]:=S[i-1,j-1]+j*S[i-1,j]; S[i][j]=S[i-1][j-1]+j*S[i-1][j];
for i:=1 to n do for (i=1;i<=n;i++)
suma:=suma+S[n,i]; s+=S[n][i];
writeln (suma) cout<<s;
end. }

Complexitatea algoritmului este O(n2).

 Exerciţiu. Modificaţi programul de aşa natură astfel încât să afişeze primele n


linii ale tabelului S(n,k). Nu afişaţi prea multe linii, pentru că numerele devin
foarte mari.
180 Capitolul 6. Programare dinamică

Probleme propuse
1. Dintr-un element (ai,j) al unei matrice An,n se poate ajunge în elementele
ai+1,j, ai+1,j+1, ai+1,j-1. Ştiind că fiecare element al matricei reţine un număr
natural, se cere un drum care îndeplineşte condiţiile problemei şi uneşte un
element de pe linia 1 cu unul de pe linia n astfel încât suma numerelor reţinute de
elementele pe unde trece drumul să fie maximă.

 3 1 3
 
Exemplu: Pentru n=3 şi matricea A =  4 1 2  ,
 3 7 1
 
drumul este: a1,1, a2,1, a3,2, iar suma este 3+4+7=14.
Rezolvaţi problema prin utilizarea a 3 metode:
a) Greedy euristic (euristică) - se obţine o soluţie "suficient de bună", deşi
de cele mai multe ori, neoptimă. Se alege un element de maxim de pe linia
1, apoi, pornind de la el, cel mai mare element accesibil de pe linia 2, ...
Care este complexitatea algoritmului în acest caz?
b) Backtracking;
c) Prin programare dinamică - în acest caz, să se rezolve problema prin
metoda înainte şi prin metoda înapoi.
2. Problema diligenţei. Un comis voiajor are de făcut o călătorie cu diligenţa între
două oraşe din vestul sălbatic. Călătoria cu diligenţa se desfăşoară în n etape. În
fiecare etapă diligenţa merge, fără întrerupere, între două oraşe. După o etapă,
diligenţa este schimbată, iar comis-voiajorul decide în care oraş va merge în etapa
următoare. Se ştie că în fiecare etapă, cu excepţia ultimei etape, se poate ajunge
în unul dintre cele k oraşe. Se cunoaşte oraşul de start, S şi cel final, F (în ultima
etapă se ajunge în oraşul F). Pentru orice etapă se cunosc toate distanţele între
oraşele care sunt puncte de pornire şi cele care sunt puncte de destinaţie.

S F

k oraşe
Etapa 1 Etapa 2 Etapa n

Figura. 6.2. Schemă de principiu


Manual de informatică pentru clasa a XI-a 181

Se cere să se decidă care sunt oraşele destinaţie pentru o anumită etapă, astfel
încât lungimea totală a drumului, care trebuie afişată, să fie minimă.

Marius Popescu

3. "Lucrare de control". Mai multor elevi li se cere să pună într-o anumită ordine
un număr de n<200 cuvinte formate numai din litere mici - ordinea exprimă faptul
că aceste cuvinte se succed după o anumită logică.
Exemplu: platon kant marx stalin havel.

Logica succesiunii constă în faptul că fiecare cuvânt poate fi definit folosind


numai cuvintele anterioare.
Fiecare elev îi transmite profesorului succesiunea de cuvinte care i se pare
logică. Sarcina profesorului constă în a acorda fiecărui elev una dintre notele 1, 2,
3, ..., n, corespunzătoare numărului de cuvinte aşezate într-o succesiune corectă.
Pentru exemplul 1 avem următoarele note:
marx stalin kant platon havel 3
havel marx stalin kant platon 2
havel stalin marx kant platon 1

Horia Georgescu, O.N.I.

4. Un patron de gogoşerie a cumpărat un calculator şi doreşte să înveţe să lucreze


pe el. Pentru aceasta va umple un raft de cărţi dintr-o anumită serie. Raftul are
lungimea L cm (L este număr natural). Seria dispune de n titluri 1, 2, ..., n cu
grosimile de n1, n2, ..., nn cm (numere naturale).

Să se selecteze titlurile pe care le va cumpăra patronul astfel încât raftul să


fie umplut complet (suma grosimilor cărţilor cumpărate să fie egală cu lungimea
raftului) şi numărul cărţilor achiziţionate să fie maxim.

5. Algoritm performant, în O(n×log(n)), pentru subşir crescător de lungime


maximală. Vom prezenta un algoritm, mai performant, pentru determinarea unui
subşir crescător de lungime maximală.

Ideea de bază este de a afla, pentru fiecare V[i], numărul de elemente din
şir care îl preced şi sunt mai mici decât el.

Fie V=(6,5,7,3,8,7,9). Vectorul S reţine numere din V prin următoarea


regulă: dacă S[i] este V[k], atunci există în V un subşir crescător de lungime i,
al cărui ultim element este V[k]. Pentru a obţine acest rezultat, regula de
completare a lui S este de a scrie un număr peste primul element din S care este
mai mare sau egal cu el. Dacă nu există un astfel de element, atunci elementul
este adăugat lui S la sfârşit. În acelaşi timp, după ce un element a fost adăugat în
S, indicele lui din S este reţinut de vectorul L pe poziţia din V a numărului adăugat.
182 Capitolul 6. Programare dinamică

Pentru exemplul nostru:


citim 6 S=(6); L=(1);
citim 5 S=(5); L=(1,1);
citim 7 S=(5,7); L=(1,1,2);
citim 3 S=(3,7); L=(1,1,2,1);
citim 8 S=(3,7,8); L=(1,1,2,1,3);
citim 7 S=(3,7,8); L=(1,1,2,1,3,2);
citim 9 S=(3,7,8,9); L=(1,1,2,1,3,2,4).

Acum putem reconstitui, în ordine inversă, subşirul crescător de lungime


maximă, pornind de la L. Valoarea maximă în L este 4 şi are în L indicele 7. V[7]=9.
Următoarea valoare este 3 şi are în L indicele 5. V[5]=8. Următoarea valoare este 2
şi are în L indicele 3. V[3]=7. Următoarea valoare este 1 şi are în L indicele 2.
V[2]=5. Prin urmare, subşirul crescător de lungime maximă este 5, 7, 8, 9.

Observaţi că (3,7,8,9) reţinut de S nu este subşir crescător, dar numărul


de elemente din S este egal cu lungimea unui subşir crescător de lungime
maximă. De ce? Fie i o poziţie completată a lui S. Prin logica modului de
completare a lui S, un element, care există la un moment dat, pe poziţia
i-1, şi care se află în V înaintea elementului memorat în S pe poziţia i,
poate fi înlocuit de alt element care se află în V după elementul memorat în
S pe poziţia i.

Să analizăm complexitatea algoritmului. Fiecare dintre cele n numere


reţinute de V este adăugat în vectorul S. Ar trebui parcurs S pentru a adăuga
numărul în poziţia corespunzătoare. Dar, atenţie, în S numerele sunt în ordine
crescătoare. Prin urmarea, se poate efectua o căutare binară în O(log n).
Identificarea subşirului se face în O(n). Rezultă complexitatea finală O(n×log n).

Cerinţă. Scrieţi un program care implementează algoritmul prezentat. Intrările şi


ieşirile se vor face prin utilizarea fişierelor text.

6. Mere-pere. Se consideră n camere distincte, situate una după alta, astfel încât
din camera i (i∈{1,2,...,n-1}) se poate trece doar în camera i+1. În fiecare
cameră se găsesc mere şi pere în cantităţi cunoscute (bucăţi).

O persoană cu un rucsac suficient de încăpător, iniţial gol, porneşte din


camera 1, trece prin camerele 2, 3, ..., n şi iese. La intrarea în fiecare cameră
descarcă rucsacul, şi încarcă, fie toate merele, fie toate perele din camera
respectivă, după care trece în camera următoare. Se presupune că pentru fiecare
fruct transportat între două camere, persoana consumă o calorie. Ce fel de fructe
trebuie să încarce persoana în rucsac astfel încât după parcurgerea celor n
camere să consume un număr minim de calorii şi care este acest număr?

Horia Georgescu
Manual de informatică pentru clasa a XI-a 183

7. Se citesc n, număr natural şi n numere naturale. Se cere să se afişeze cea mai


mare sumă care se poate forma cu numere dintre cele n (fiecare număr poate
participa o singură dată la calculul sumei) şi care se divide cu n. Afişaţi, de
asemenea, şi numerele care alcătuiesc suma.
8. Se citesc n mulţimi alcătuite din cel mult 200 de numere naturale între 0 şi 199.
Se cere să se determine un număr maxim de mulţimi cu proprietatea că între
oricare două există o relaţie de incluziune. De exemplu, dacă se citesc 4 mulţimi
{2,5,6}, {1,6,9,2} {2}, {2,6} se tipăreşte 3, adică numărul submulţimilor
{2,5,6}, {2}, {2,6}.

9. Problema care urmează prezintă o modalitate de compactare a unui fişier


oarecare (de date, executabil, etc.). După cum se ştie, fişierul este reţinut pe suport
ca o succesiune de 0 şi 1. Noi dispunem de un set de m secvenţe de 0 şi 1 din
care nu lipsesc secvenţa care îl conţine numai pe 0 şi secvenţa care îl conţine
numai pe 1. Se cere să găsim o descompunere a şirului de 0 şi 1 care alcătuieşte
fişierul într-un număr minim de secvenţe din cele m. Dacă rezolvăm această
problemă, putem înlocui secvenţa cu o adresă şi în acest fel fişierul va deveni o
succesiune de adrese (în cazul existenţei unor secvenţe lungi se realizează
economia de suport).
Exemplu: Pentru m=6, secvenţele sunt:
1) 0
2) 1
3) 0 0 1
4) 1 0 0
5) 1 1 1
6) 1 0 1

Fişierul este: 100111001.

Se folosesc secvenţele 4 5 şi 3.

10. Pentru un triunghi ABC, cu vârfurile de coordonate întregi, definim costul său ca
fiind minimul ariilor dreptunghiurilor cu laturile paralele cu axele de coordonate care
au pe laturi vârfurile unui triunghi. De exemplu, cu vârfurile de coordonate (1,0),
(5,0), (0,3) costul ataşat este egal cu 15. Fie un poligon convex, cu
coordonatele vârfurilor numere întregi. Numim triangularizare a poligonului o
partiţionare a sa ale cărei vârfuri sunt vârfuri ale poligonului dat. Numim costul unei
triangularizări ca fiind suma costurilor triunghiurilor componente. Problema constă
în realizarea unei triangularizări de cost minim.

Indicaţii
1. Vezi “problema triunghiului”!

2. Ca la “problema triunghiului”. Se calculează drumurile optime corespunzătoare


etapei n, apoi n-1, ...
184 Capitolul 6. Programare dinamică

3. Subşir crescător de lungime maximă.


4. Vezi "o problemă cu sume"!

6. Aparent, se alege minimul dintre mere şi pere din fiecare cameră. Să analizăm
exemplul următor:
1 2
M 3 11
P 4 14
Dacă luăm merele din prima cameră, în a doua cameră vom avea 14 mere
şi 14 pere. Orice am alege, costul este 3+14=17.
Dar dacă luăm perele, atunci în a doua cameră vom avea 11 mere şi 18
pere. Alegem merele şi avem costul 4+11=15<17. În concluzie, rezolvarea prin
alegerea minimului nu este corectă.
Dacă ar fi să calculăm minimul după toate variantele de tip MP...PMPM,
avem un algoritm în O(2n). Acest lucru se observă uşor dacă, de exemplu, în loc
de M punem 0 şi în loc de P punem 1.
Fie Mi, i=1...n, merele care se află iniţial în camera i.
Fie Pi, i=1...n, perele care se află iniţial în camera i.
Vom nota CMi, i=1, …, n, costul optim dacă se pleacă cu merele din camera i
(i=1...n).
Vom nota CPi, i=1, …, n, costul optim dacă se pleacă cu perele din camera i
(i=1, …, n).
Iniţial, avem:

CMn=Mn;
CPn=Pn.
Pentru i=1, 2, …, n-1 avem:

M i + CPi +1
M + ( M i + M i +1 ) + CPi + 2


i
CM i = min M i + ( M i + M i +1 ) + ( M i + M i +1 + M i + 2 ) + CPi + 3
...


M i + ( M i + M i +1 ) + ( M i + M i +1 + M i + 2 ) + ...( M i + M i +1 ... + M n )

 Pi + CM i +1
P + ( Pi + Pi +1 ) + CM i + 2


i
CPi = min  Pi + ( Pi + Pi +1 ) + ( Pi + Pi +1 + Pi + 2 ) + CM i + 3
...


 Pi + ( Pi + Pi +1 ) + ( Pi + Pi +1 + Pi + 2 ) + ...( Pi + Pi +1 ... + Pn )
Manual de informatică pentru clasa a XI-a 185

Semnificaţia relaţiilor de recurenţă. De exemplu pentru CMi. Caut minimul de


calorii pentru acţiunile:
- se iau merele din camera i, apoi din camera i+1 se iau perele;
- se iau merele din camera i, apoi din camera i+1 tot merele, iar din
camera i+1 se iau perele;
...
- se iau merele din toate camerele.

Pentru CPi se procedează în mod identic. În final, se calculează


min{CM1,CP1} şi rezultatul este soluţia problemei.

7. Se calculează S, suma tuturor numerelor. Se caută cea mai mare sumă mai
mică sau egală cu S, care se divide cu n. Se caută ca la "o problemă cu sume"
numerele care însumate o alcătuiesc. Dacă această sumă nu se poate forma, se
încearcă următoarea, mai mică decât aceasta care se divide cu n, până când se
găseşte o asemenea sumă. Se demonstrează faptul (cum?) că există întotdeauna
o astfel de sumă.

8. Se sortează lexicografic vectorii caracteristici şi apoi se găseşte subşirul


maximal...

9. Ca la înmulţirea optimală a unui şir de matrice. Notăm cu Cost[i,j]


numărul de secvenţe necesar codificării textului între caracterele cu numere de
ordine i şi j. Avem: Cost[i,i]=1.

1, dacă este o secvenţă dată



Cost[i, j] = min{cost[i,l] + cost[l + 1, j}, altfel

 l∈{i,i+1...j-1}

Completarea matricei se face pe paralele la diagonala principală:

cost[1,1], ..., cost[n,n], cost[1,2], ..., cost[n-1,n], ..., cost[1,n].

Soluţia este cost[1,n].

10. Se reţine costul minim triangularizării pentru poligoanele formate din 3, 4, ...,
k-1 puncte, luate în sensul de parcurgere a poligonului. Atunci când calculează
costul minim al triangularizării pentru poligonul cu k puncte, se alege minimul dintre
costul poligonului anterior format, la care se adaugă costul noului triunghi, fie costul
obţinut prin unirea punctelor 1, 2, ..., k-1 cu punctul k.

Figura 6.3. Exemple de triangularizare


186

Capitolul 7
Grafuri neorientate

7.1. Introducere
Uneori, algoritmii trebuie să prelucreze date referitoare la anumite
elemente între care există anumite relaţii. Să analizăm exemplele următoare:

1. Se dau n oraşe. Unele dintre ele sunt unite prin şosele directe (care nu mai
trec prin alt oraş).
2. Se cunosc relaţiile de prietenie dintre n persoane.
3. Se dau n ţări şi se cunoaşte relaţia de vecinătate între ele.
4. Se dau n triunghiuri, iar unele dintre ele sunt asemenea.

Pentru fiecare dintre aceste exemple se poate imagina o reprezentare


grafică care să exprime relaţiile existente.
 Convenim ca fiecare element să-l numim nod sau vârf.
Astfel, în cazul 1. nodul este oraşul, în cazul 2. nodul este persoana, în
cazul 3. nodul este ţara şi în cazul 4. nodul este triunghiul. Convenim ca un nod
(vârf) să-l notăm cu un cerculeţ în care să înscriem numărul lui (de la 1 la n).

 Relaţia existentă între două noduri o vom reprezenta grafic unindu-le


printr-un segment de dreaptă. Convenim ca un astfel de segment să-l numim
muchie şi dacă ea uneşte nodurile i şi j, s-o notăm cu (i,j).

În cazul 1., muchia (i,j) are


semnificaţia că între oraşele i şi j există o şosea 1
directă. În cazul 2., muchia (i,j) are
semnificaţia că persoanele i şi j sunt prietene, în 6
cazul 3. muchia (i,j) are semnificaţia 2
că ţările i şi j sunt vecine, iar în cazul 4., că
triunghiurile i şi j sunt asemenea. 5
3 4
Procedând aşa, obţinem o descriere grafică
precum cea din figura 7.1 şi convenim ca o astfel
de reprezentare s-o numim graf neorientat. Figura 7.1.
Exemplu de graf neorientat
Desigur, această abordare este intuitivă.
Teoria grafurilor este fundamentată matematic şi
în cele ce urmează ne propunem s-o prezentăm sistematic.
Manual de informatică pentru clasa a XI-a 187

7.2. Definiţia grafului neorientat

Definiţia 7.1. . Un graf neorientat este o pereche ordonată G=(V,E),


1

unde:
 V = {v1,v2,...,vn} este o mulţime finită şi nevidă. Elementele
mulţimii V se numesc noduri (vârfuri).
 E este o mulţime finită de perechi neordonate de forma (vi, vj),
unde i≠j, şi vi,vj∈V. Elementele mulţimii E se numesc muchii.
Semnificaţia unei muchii este aceea că uneşte două noduri.
Un graf poate fi desenat aşa cum se observă în
exemplul următor (vezi figura 7.2), unde
1 6
G=(V,E),
V = {1,2,3,4,5,6};
E = {(1,2),(1,3),(1,5),(2,3),(3,4), 2
(4,5)} 5
3
Notaţie: în graful G=(V,E), vom nota cu n 4
numărul nodurilor şi cu m numărul muchiilor.
Figura 7.2.
Observaţii Alt exemplu de graf neorientat

 Două noduri distincte pot fi unite prin cel mult o muchie. În exemplul de
mai sus, (1,2) este muchia care uneşte nodul 1 cu nodul 2. Dacă scriem
(2,1), ne referim la aceeaşi muchie (perechea este neordonată).
 Nu există o muchie care uneşte un nod cu el însuşi (o muchie uneşte
două noduri distincte).

Definiţia 7.2. În graful G=(V,E), nodurile distincte vi,vj∈G sunt


adiacente dacă există muchia (vi,vj)∈E.
Vom spune că muchia (vi,vj)∈E este incidentă la nodurile vi şi vj.

În exemplul dat anterior, nodurile 1 şi 5 sunt adiacente, dar nodurile 2 şi 5 nu


sunt adiacente. Muchia (4,5) este incidentă la nodurile 4 şi 5.

Definiţia 7.3. Într-un graf neorientat, prin gradul unui nod v se înţelege
numărul muchiilor incidente cu nodul v şi se notează cu d(v). Un nod
cu gradul 0 se numeşte nod izolat, iar unul cu gradul 1 se numeşte nod
terminal.
În exemplul dat, d(2)=2, d(1)=3, d(6)=0 (6 este nod izolat).

Definiţia este restrictivă, în unele lucrări veţi întâlni definiţii mai puţin restrictive, de
1

exemplu, poate exista o muchie de la un nod la el însuşi sau nu se cere ca mulţimea


nodurilor să fie finită.
188 Capitolul 7. Grafuri neorientate

O relaţie utilă: fie un graf neorientat cu n noduri şi m muchii. Dacă notăm cu d1,
d2, ..., dn gradele celor n noduri, atunci avem relaţia:

d 1 + d 2 + d 3 + ...d n = 2m.

 Demonstraţie: fiecare muchie face să crească gradele celor două noduri la


care este incidentă cu câte o unitate. Prin urmare, se obţine relaţia anterioară.

Pentru a înţelege bine noţiunile prezentate în acest paragraf, ne vom referi la


exemplele din paragraful 7.1:

Fie afirmaţia: gradul nodului i este k. Pentru exemplul 1., ea are


semnificaţia că din oraşul i pleacă (sosesc) k şosele, pentru exemplul 2., are
semnificaţia că persoana i are k prieteni, pentru exemplul 3., are semnificaţia că
ţara i se învecinează cu k ţări, iar pentru exemplul 4., are semnificaţia că pentru
triunghiul i se cunosc k triunghiuri asemenea. Aici trebuie făcută observaţia că ar
putea să existe şi alte triunghiuri asemenea cu el, dar modul în care putem afla
aceasta va fi tratat separat.
Fie afirmaţia: nodurile i şi j sunt adiacente. Pentru exemplul 1., ea are
semnificaţia că oraşele i şi j sunt unite printr-o şosea care nu trece prin alte
oraşe, pentru exemplul 2., are semnificaţia că persoanele i şi j sunt prietene,
pentru exemplul 3., are semnificaţia că ţările i şi j sunt vecine, iar pentru
exemplul 4., are semnificaţia că triunghiurile i şi j sunt asemenea.

Fie afirmaţia: nodul i este izolat. Pentru exemplul 1., înseamnă că nu există
nici o şosea care leagă oraşul i cu alt oraş, pentru exemplul 2., înseamnă că
persoana i nu are nici un prieten, pentru exemplul 3., înseamnă că ţara i nu se
învecinează cu nici o ţară (este situată pe o insulă), pentru exemplul 4., înseamnă
că nu există nici un triunghi dintre celelalte n-1 triunghiuri care să fie asemenea cu
triunghiul i.

7.3. Memorarea grafurilor

În acest paragraf, prezentăm principalele structuri de date prin care grafurile pot
fi memorate în vederea prelucrării lor. De la început, precizăm faptul că vom alege o
structură sau alta în funcţie de :
2

a) algoritmul care prelucrează datele referitoare la graf;


b) memoria internă pe care programul o are la dispoziţie;
c) dacă graful conţine multe muchii sau nu.

Pentru fiecare structură de date pe care o vom folosi, vom avea câte o
procedură (funcţie) care citeşte datele respective. Toate aceste subprograme se

2
Modul de alegere a structurii îl veţi înţelege pe parcursul studiului acestui capitol.
Manual de informatică pentru clasa a XI-a 189

găsesc grupate în unitatea de program grafuri.pas (pentru Pascal) şi în


grafuri.cpp (pentru C++). Vom fi astfel scutiţi ca, pentru fiecare program pe care îl
realizăm, să fim nevoiţi să adăugăm liniile de cod necesare citirii şi ne permite să ne
concentrăm exclusiv asupra algoritmului pe care îl realizăm.

Toate subprogramele pe care le utilizăm citesc datele dintr-un fişier text, în


care, pe prima linie vom scrie numărul de noduri (n), iar pe următoarele linii,
câte o muchie (i,j), ca în exemplul de mai jos, în care este prezentat un
graf şi liniile fişierului text care este citit pentru el:

6
Fişierul text:
1 6 1 2
1 3
1 5
2 2 3
3 4
5
Figura 7.3.
4 5
3
4 Exemplu de graf neorientat

Trecem la prezentarea structurilor prin care putem memora datele referitoare la


un graf.

A. Memorarea grafului prin matricea de adiacenţă

An,n - o matrice pătratică, unde elementele ei, ai,j au semnificaţia:

1, pentru (i, j) ∈ E


a i, j = 
0, pentru (i, j) ∉ E
Pentru graful din figura 7.3, matricea de adiacenţă este prezentată în
continuare:

0 1 1 0 1 0
1
 0 1 0 0 0
1 1 0 1 0 0
A6, 6 = 
0 0 1 0 1 0
1 0 0 1 0 0
 
0 0 0 0 0 0

Observaţii

1. Întrucât, din modul în care a fost definit graful, rezultă că nu există muchii de la un
nod la el însuşi, rezultă că elementele de pe diagonala principală reţin 0:
ai ,i = 0, ∀i ∈ {1,2,..., n} .
190 Capitolul 7. Grafuri neorientate

2. Matricea de adiacenţă este simetrică:

ai , j = a j ,i , ∀i, j ∈ {1,2,..., n} .

Evident, deoarece muchia (i,j) coincide cu muchia (j,i).

3. Suma elementelor de pe linia i, i ∈{1,2,..., n}, are ca rezultat gradul nodului i,


d(i), pentru că astfel se obţine suma nodurilor j, j ∈{1, 2,..., n}, pentru care
există muchie de la i la j, adică suma muchiilor incidente la i.

De exemplu, pentru graful de mai sus, suma elementelor de pe linia 1 este 3,


adică gradul nodului 1, d(1).

4. Tot aşa, suma elementelor de pe coloana j, j ∈{1, 2, ..., n}, are ca rezultat
gradul nodului j, d(j).

! Raţionamentul este asemănător cu cel de la observaţia precedentă.

5. Suma tuturor elementelor matricei de adiacenţă este, de fapt, suma gradelor


tuturor nodurilor, adică dublul numărului de muchii (2m).

6. Dacă graful citit are un număr mic de muchii, atunci matricea de adiacenţă este o
formă ineficientă de memorare a lui, pentru că ea va reţine o mulţime de 0.

Subprogramele pe care le vom utiliza pentru această modalitate de memorare


sunt prezentate în continuare:

Varianta Pascal Varianta C++


type mat_ad=array[1..50,1..50] of void CitireN
integer; (char Nume_fis[20],
int A[50][50], int& n)
procedure CitireN {
(Nume_Fis:string; int i,j;
var A:Mat_ad; var n:integer); fstreamf (Nume_fis,ios::in);
var f:text; f>>n;
i,j:byte; while (f>>i>>j)
begin A[i][j]=A[j][i]=1;
Assign(f,Nume_Fis); f.close();
Reset(f); }
Readln(f,n);
while(not eof(f)) do
begin
readln(f,i,j);
A[i,j]:=1;
A[j,i]:=1;
end;
close(f);
end;
Manual de informatică pentru clasa a XI-a 191

Programul următor citeşte datele referitoare la un graf şi afişează matricea


sa de adiacenţă:

Varianta Pascal Varianta C++


uses grafuri; #include "grafuri.cpp"
var A:mat_ad; int A[50][50],n;
n,i,j:integer; main()
begin {
CitireN('Graf.txt',A,n); CitireN("Graf.txt",A,n);
for i:=1 to n do for (int i=1;i<=n;i++)
begin { for (int j=1;j<=n;j++)
for j:=1 to n do cout<<A[i][j]<<" ";
write (A[i,j],' '); cout<<endl;
writeln; }
end; }
end.

B. Memorarea grafului prin liste de adiacenţă implementate prin utilizarea


alocării statice

Listele de adiacenţă reprezintă o altă formă de memorare a grafurilor, în care


pentru fiecare nod se cunoaşte lista nodurilor adiacente cu el. De exemplu, pentru
graful din figura 7.4., listele de adiacenţă sunt:

1 1 -> 2,3,5
6
2 -> 1,3
3 -> 1,2,4
2 4 -> 3,5
5 5 -> 1,4
3 6 ->
Figura 7.4. 4

În acest caz, se utilizează un vector cu n componente, pe care îl vom numi


Start şi o matrice T cu 2 linii şi 2m coloane. Semnificaţiile sunt:
 Start – pentru fiecare nod i, Start[i] specifică coloana din T unde
începe lista nodurilor adiacente cu i. Dacă reţine 0, înseamnă că nodul i nu
are noduri adiacente.
 T[0,i] (T[0][i]) - reprezintă un nod al listei nodurilor adiacente.
 T[1,i] (T[1][i]) - reprezintă indicele coloanei din T unde se găseşte
următorul element din listă. Dacă reţine 0, atunci acesta este ultimul elerment
din lista succesorilor.
1 2 3 4 5 6

Start 5 7 9 11 12 0
192 Capitolul 7. Grafuri neorientate

1 2 3 4 5 6 7 8 9 10 11 12

T[0] 2 1 3 1 5 1 3 2 4 3 5 4
T[1] 0 0 1 0 3 0 2 4 8 0 10 6

 Exemplu de utilizare

Dorim să vedem care este lista nodurilor adiacente cu nodul 3. Start[3]=9,


adică lista începe în coloana 9 a matricei T. Primul nod adiacent cu nodul 3 este 4.
Următorul se găseşte la indicele 8. Următorul nod adiacent cu nodul 3 este nodul 2.
Următorul se găseşte la indicele 4. Acesta este nodul 1. El este ultimul din listă,
pentru T(1,4)=0 (T[1][4]).

În continuare, vom arăta modul în care se construiesc listele de adiacenţă


alocate static. Pentru fiecare muchie (i,j) se completează două coloane ale
matricei T, pentru că trebuie înregistrat faptul că i este adiacent cu j şi j este
adiacent cu i. Astfel, vom pune pe j ca prim element în lista nodurilor adiacente lui i
şi pe j ca prim element în lista nodurilor adiacente cu i.

Considerăm primul caz, în care vom pune pe j, ca prim element în lista


nodurilor adiacente cu i. Variabila k, care are iniţial valoarea 0, va reţine indicele
ultimei coloane din T completată. O astfel de operaţie se realizează în patru paşi:

1. Pentru că trebuie completată o nouă coloană în T, se incrementează k.


2. T[0,k] (T[0][k]) va reţine j, pentru că succesorul lui i este j.
3. T[1,k] (T[1][k]) va reţine Start[i], pentru că indicele coloanei în care se
găseşte primul nod adiacent (până la această nouă introducere, când devine al doilea
din listă) este în Start[i].
4. Start[i] va reţine k, pentru că, de acum, primul nod din lista nodurilor
adiacente cu i, este j, care se găseşte în coloana de indice k.

Mai jos, puteţi observa subprogramele care construiesc listele de adiacenţă


(funcţiile au fost adăugate în grafuri.pas şi respectiv, grafuri.cpp):

Varianta Pascal Varianta C++


type lista=array[0..1,1..50] of void Citire_LA_Astatic
integer; (char Nume_fis[20],int
pornire=array[1..50] of integer; T[2][50],int Start[50],int& n)
...
{ int i,j,k=0;
procedure Citire_LA_Astatic fstream f(Nume_fis,ios::in);
(Nume_fis:string;var T:Lista; f>>n;
var Start:pornire; while (f>>i>>j)
var n:integer); { k++;
var i,j,k:integer; T[0][k]=j;
f:text; T[1][k]=Start[i];
Start[i]=k;
Manual de informatică pentru clasa a XI-a 193

begin k++;
k:=0; T[0][k]=i;
Assign(f,Nume_Fis); T[1][k]=Start[j];
Reset(f); Start[j]=k;
Readln(f,n); }
while(not eof(f)) do f.close();
begin }
readln(f,i,j);
k:=k+1;
T[0,k]:=j;
T[1,k]:=Start[i];
Start[i]:=k;
k:=k+1;
T[0,k]:=i;
T[1,k]:=Start[j];
Start[j]:=k;
end;
close(f);
end;

Programul următor citeşte datele despre un graf şi afişează, pentru fiecare


nod, lista nodurilor adiacente:

Varianta Pascal Varianta C++


uses grafuri; #include "grafuri.cpp"
var Start:pornire; int T[2][50],Start[50],
T:lista; n,i,man;
n,i,man:integer;
main()
begin {
Citire_LA_Astatic('Graf.txt', Citire_LA_Astatic
T,Start,n); ("Graf.txt",T,Start,n);
for i:=1 to n do for (int i=1;i<=n;i++)
begin {
writeln('Noduri adiacente cout<< "Noduri adiacente
cu ',i); cu "<<
man:=Start[i]; i<<endl;
while (man<>0)do man=Start[i];
begin while (man)
write(T[0,man],' '); {
man:=T[1,man]; cout<<T[0][man]<<" ";
end; man=T[1][man];
writeln }
end; cout<<endl;
end. }
}
194 Capitolul 7. Grafuri neorientate

C. Memorarea grafului prin liste


de adiacenţă implementate prin 1 2 3 5
utilizarea alocării dinamice
2 1 3
Astfel, un vector cu n
3 1 2 4
componente, corespunzătoare celor
n noduri, va reţine adresele de
4 3 5
început ale celor n liste liniare simplu
înlanţuite. 5 1 4

Vom introduce în fişierul grafuri.pas (respectiv, grafuri.cpp) structura


următoare, care descrie un nod al listelor:

Varianta Pascal Varianta C++


ref=^inr; struct Nod
inr=record {
nd:byte; int nd;
adr_urm:ref; Nod* adr_urm;
end; };
lista_a=array[1..50] of ref;

De asemenea, în acelaşi fişier, vom introduce subprogramul de mai jos, care


are rolul de a crea listele de adiacenţă. Să observăm că fiecare listă simplu
înlănţuită conţine nodurile în ordinea inversă introducerii lor. Aceasta pentru că,
pe de o parte, ordinea în care se reţin nodurile nu contează, iar, pe de altă parte,
algoritmul se simplifică dacă fiecare nod se introduce la începutul listei.

Varianta Pascal Varianta C++


Procedure Cit_LA_Adinamic void Cit_LA_Adinamic(char
(Nume_fis:string; var Nume_fis[20],Nod* L[50],int& n)
L:lista_a; var n:integer); { Nod* p;
var i,j:byte; int i,j;
p:ref; f:text; fstream f(Nume_fis,ios::in);
begin f>>n;
Assign(f,Nume_Fis); while (f>>i>>j)
Reset(f); Readln(f,n); { p=new Nod;
for i:=1 to n do L[i]:=nil; p->adr_urm=L[i];
while (not Eof(f)) do p->nd=j; L[i]=p;
begin p=new Nod;
Readln(f,i,j); p->adr_urm=L[j];
new(p); p^.adr_urm:=L[i]; p->nd=i;L[j]=p;
p^.nd:=j; L[i]:=p; }
new(p); p^.adr_urm:=L[j]; f.close();
p^.nd:=i; L[j]:=p; }
end;
Close(f);
end;
Manual de informatică pentru clasa a XI-a 195

Programul următor citeşte datele despre un graf şi afişează, pentru fiecare


nod, lista nodurilor adiacente:

Varianta Pascal Varianta C++


uses grafuri; #include "grafuri.cpp"
var L:lista_a; Nod* L[50];
n,i:integer; int n,i;
begin void main()
Cit_LA_Adinamic('Graf.txt',L,n); {
for i:=1 to n do Cit_LA_Adinamic("Graf.txt",L,n);
begin for (i=1;i<=n;i++)
writeln ('Noduri adiacente { cout<<"Noduri adiacente cu "
cu ',i); <<i<<endl;
while L[i]<>nil do while (L[i])
begin { cout<<L[i]->nd<<" ";
write(L[i]^.nd,' '); L[i]=L[i]->adr_urm;
L[i]:=L[i]^.adr_urm; }
end; cout<<endl;
writeln; }
end }
end.

D. Memorarea grafului prin lista muchiilor

Se utilizează un vector cu m componente, unde m este numărul muchiilor.


Fiecare componentă va reţine cele două noduri la care muchia respectivă este
incidentă şi, în anumite cazuri alte informaţii referitoare la muchia respectivă.
Reţineţi că există algoritmi pentru grafuri care prelucrează datele pornind de la
această reprezentare (vedeţi arborii parţiali). Uneori, va fi necesar să sortăm
muchiile, fie după nodul de pornire, fie după altă informaţie asociată lor (costul
muchiei, noţiune pe care o vom introduce ulterior).
Mai jos, puteţi observa cum se descrie un vector (V) care reţine muchiile
unui graf:

Varianta Pascal Varianta C++


type muchie=record struct muchie
x,y:integer; { int x,y;
end; };
var V:array[1..50] of muchie; muchie V[50];

 Exerciţiu. Realizaţi un subprogram care citeşte şi memorează datele


referitoare la muchiile unui graf neorientat.

Observaţie foarte importantă. Uneori, nodurilor unui graf li se asociază


anumite informaţii. De exemplu, dacă nodurile unui graf reprezintă oraşe,
pentru fiecare astfel de nod se poate memora numărul obiectivelor turistice.
În astfel de cazuri, pe lângă una dintre metodele de memorare prezentate mai sus,
196 Capitolul 7. Grafuri neorientate

vom asocia grafului un vector cu n (numărul de noduri ale grafului) componente,


unde fiecare componentă va reţine informaţiile referitoare la nodul respectiv.
Pentru exemplul dat, fiecare componentă a grafului reţine numărul de obiective
turistice.

7.4. Graf complet


Să considerăm mulţimea elevilor unei clase. Teoretic, oricare doi elevi din
clasă se cunosc. Pentru a transpune în limbaj specific teoriei grafurilor această
situaţie, vom considera că fiecare elev este un nod al unui graf. Pentru că oricare
doi elevi se cunosc, înseamnă că oricare două noduri sunt unite printr-o muchie.
Astfel, am obţinut un graf aparte, pe care-l vom numi graf complet.

Definiţia 7.4. Prin graf complet vom înţelege un graf neorientat în care
oricare două noduri sunt adiacente. Vom nota un graf complet prin Kn, unde
n este numărul de noduri ale grafului.

Alăturat, aveţi un graf complet cu 4 noduri (K4):


1

2 3

Figura 7.5. 4
Exemplu de graf complet

Relaţii utile:

1. Într-un graf complet, gradul oricărui nod este n-1. Evident, din fiecare nod,
pleacă (sosesc) n-1 muchii.
n(n − 1)
2. Într-un graf complet, avem relaţia: m= , unde m este numărul de
2
muchii, iar n, numărul de noduri.

 Demonstraţie: fiecare muchie uneşte 2 noduri. Numărul muchiilor va fi egal


cu numărul de submulţimi cu 2 elemente ale mulţimii celor n noduri.
Acest număr este C2 = n(n − 1) .
n
2
n(n−1)
3. Avem 2 2 grafuri neorientate cu n noduri.

 Demonstraţie: am văzut că numărul maxim de muchii pe care le poate avea


un graf neorientat este:
Manual de informatică pentru clasa a XI-a 197

n(n − 1)
2
şi corespunde unui graf complet. Se ştie că, fiind dată o mulţime A cu n
elemente, avem 2n submulţimi disjuncte ale acesteia (aici este inclusă şi
submulţimea vidă şi A). Prin urmare, avem:
n(n−1)

2 2

submulţimi ale numărului maxim de muchii. Ori, fiecărei submulţimi de muchii îi


corespunde un graf neorientat, pentru că nodurile sunt aceleaşi.

7.5 Graf parţial, subgraf

Definiţia 7.5. Un graf parţial al unui graf neorientat dat G=(V,E) este un
graf G1=(V,E1), unde E1⊆E.

Un graf parţial al unui graf dat, este el însuşi sau se obţine din G prin
suprimarea anumitor muchii. Priviţi exemplul de mai jos:

Figura 7.6.
Obţinerea unui 1 1
graf parţial

2 3 3
rezultă 2

4 4

G=(V,E) G1=(V,E1)

 Exemple din viaţa reală

1. Fiind date n persoane, între unele dintre ele există o relaţie de prietenie. Asociem
acestei situaţii un graf G. După un timp, unele persoane se ceartă. În teoria grafurilor,
aceasta înseamnă că în G se suprimă anumite muchii şi astfel, se obţine un graf
parţial G1.

2. Fiind date n oraşe, unele dintre ele sunt unite printr-o şosea directă (care nu mai
trece prin alte oraşe). Asociem situaţiei date un graf G. Datorită precipitaţiilor, anumite
şosele se inundă şi nu mai pot fi utilizate. Aceasta înseamnă că în G se suprimă
anumite muchii şi se obţine un graf parţial G1.
198 Capitolul 7. Grafuri neorientate

Intrebare: câte grafuri parţiale are un graf neorientat cu n noduri? Indicaţie:


Câte submulţimi are mulţimea muchiilor {1, 2, …, m}?

Definiţia 7.6. Un subgraf al unui graf neorientat G=(V,E) este un graf


G1=(V1,E1), unde V1⊂V, E1⊂E, iar muchiile din E1 sunt toate muchiile din
E care sunt incidente numai la noduri din mulţimea V1.

Un subgraf al unui graf G este el însuşi sau se obţine din G prin suprimarea
anumitor noduri şi a tuturor muchiilor incidente cu acestea. Priviţi exemplul de
mai jos:

Figura 7.7. 1 1
Obţinerea unui
subgraf

2 3 3
rezultă

4 4

G=(V,E) G1=(V1,E1)

 Exemple din realitate


1. Se dau n firme. Între unele din acestea se stabilesc relaţii de colaborare. Asociem
situaţiei date un graf G. Între timp, anumite firme se desfiinţează. Aceasta înseamnă că în
G vom elimina anumite noduri şi muchiile incidente lor, obţinând un subgraf al lui G, G1.
2. Mai multe calculatoare (n) sunt legate în reţea cu ajutorul unor cabluri. Asociem
situaţiei date un graf G. Între timp, anumite calculatoare se defectează. Astfel, se
obţine un subgraf al lui G, G1.
Întrebare: câte subgrafuri are un graf neorientat cu n noduri? Indicaţie: care
este numărul de submulţimi ale mulţimii {1, 2, …, n}? Întrucât mulţimea
nodurilor, V, este nevidă, vom face abstracţie de mulţimea vidă.

7.6. Parcurgerea grafurilor neorientate

Prin parcurgerea grafurilor înţelegem o modalitate de vizitare a nodurilor


3

acestuia. Parcurgerea eficientă a grafurilor este esenţială în teoria grafurilor,


deoarece o mulţime de algoritmi consacraţi au la bază o astfel de parcurgere.
Din acest motiv, în acest paragraf nu vom insista pe aplicaţiile parcurgerilor şi ne vom
mărgini numai la prezentarea algoritmilor de parcurgere. În paragrafele următoare,
veţi găsi multe exemple utile, în care parcurgerea grafurilor joacă un rol fundamental.

3
În acest paragraf vom exemplifica parcurgerile doar în cazul grafurilor conexe. Cum noţiunea nu
a fost prezentată până în acest moment, precizăm doar că vom exemplifica parcurgerea grafurilor
în care oricare două noduri sunt "legate" printr-o succesiune de muchii.
Manual de informatică pentru clasa a XI-a 199

Există două modalităţi generale de parcurgere şi anume: parcurgerea în


lăţime (BF) şi parcurgerea în adâncime (DF). Acestea vor fi tratate separat.

7.6.1. Parcurgerea în lăţime (BF - Breadth First)

 Parcurgerea în lăţime se face începând de la un anumit nod i, pe care îl


considerăm vizitat.
 Vizităm apoi toate nodurile adiacente cu el - fie ele j1, j2, ..., jk, vizitate în
această ordine.
 Vizităm toate nodurile adiacente cu j1, apoi cu j2, …, apoi cu jk.

 ...
 Parcurgerea continuă în acest mod până când au fost vizitate toate nodurile
accesibile.

Exemple de parcurgeri BF ale aceluiaşi graf, pornind de la noduri diferite:

1
Pentru graful alăturat avem:

2 6 3 Nod pornire 1: 1 3 6 2 7 5 4
Nod pornire 3: 3 7 6 1 2 5 4
Nod pornire 6: 6 3 1 7 2 5 4
4 5 7

Figura 7.8.

⇒ Parcurgerea BF se efectuează prin utilizarea structurii numită coadă, având grijă


ca un nod să fie vizitat o singură dată. Atunci când un nod a fost introdus în coadă
se marchează ca vizitat. Coada va fi alocată prin utilizarea unui vector.

Algoritmul este următorul:

Nodul de pornire este introdus în coadă şi este marcat ca vizitat.


Cât timp coada este nevidă
Pentru nodul aflat la începutul cozii:
 se trec în coadă toate nodurile adiacente cu el, care nu au fost vizitate şi
se marchează ca vizitate;
 se afişează;
 se extrage din coadă.
200 Capitolul 7. Grafuri neorientate

Vom utiliza următoarele notaţii:

- i_c - indicele primei componente a cozii;


- s_c - indicele ultimei componente a cozii;
- coada - vectorul care memorează coada propriu-zisă;
- s - vector ce reţine nodurile vizitate:

0, nodul i nu a fost vizitat


s[i] = 
1, nodul i a fost vizitat

În continuare, vor fi prezentate două exemple de implementări ale


parcurgerii în lăţime (BF) a unui graf, memorat prin liste de adiacenţă şi
matrice de adiacenţă:

1. Programul următor parcurge BF un graf memorat prin liste de adiacenţă:

Varianta Pascal Varianta C++


uses grafuri; #include "grafuri.cpp"
var n,i_c,sf_c,p:integer; int n, coada[50],s[50],i_c=1,
coada,s:array[1..50] of integer; sf_c=1,T[2][50],Start[50],p;
T:lista;
Start:pornire; void bf(int nod)
{ coada[i_c]=nod;
procedure bf(nod:integer); s[nod]=1;
begin while (i_c<=sf_c)
i_c:=1; sf_c:=1; { p=Start[coada[i_c]];
coada[i_c]:=nod; while (p)
s[nod]:=1; { if (s[T[0][p]]==0)
while i_c<=sf_c do {sf_c++;
begin coada[sf_c]=T[0][p];
p:=Start[coada[i_c]]; s[T[0][p]]=1;
while p<>0 do }
begin p=T[1][p];
if s[T[0,p]]=0 then }
begin cout<<coada[i_c]<<endl;
sf_c:=sf_c+1; i_c++;
coada[sf_c]:=T[0,p]; }
s[T[0,p]]:=1; }
end;
p:=T[1,p]; main()
end; {
writeln(coada[i_c]); Citire_LA_Astatic("Graf.txt",
i_c:=i_c+1; T,Start,n);
end bf(6);
end; }
begin
Citire_LA_Astatic
('Graf.txt',T,Start,n);
bf(3);
end.
Manual de informatică pentru clasa a XI-a 201

2. Programul următor parcurge BF un graf memorat prin matricea de adiacenţă:

Varianta Pascal Varianta C++


uses grafuri; #include "grafuri.cpp"
var n,i_c,sf_c,i:integer; int n, coada[50],s[50],i_c=1,
coada,s:array[1..50] of integer; sf_c=1,A[50][50],i;
A:mat_ad;
void bf(int nod)
procedure bf(nod:integer); { coada[i_c]=nod;
begin s[nod]=1;
i_c:=1;sf_c:=1; while (i_c<=sf_c)
coada[i_c]:=nod; { i=1;
s[nod]:=1;
while (i<=n)
while (i_c<=sf_c) do
{if (A[coada[i_c]][i]==1
begin
&& s[i]==0)
i:=1;
{sf_c++;
while i<=n do
coada[sf_c]=i;
begin
s[i]=1;
if ((A[coada[i_c],i]=1)
}
and
i++;
(s[i]=0))
}
then
cout<<coada[i_c]<<endl;
begin
i_c++;
sf_c:=sf_c+1;
}
coada[sf_c]:=i;
}
s[i]:=1;
end; main()
i:=i+1; { CitireN("Graf.txt",A,n);
end; bf(1);
writeln(coada[i_c]); }
i_c:=i_c+1;
end
end;
begin
CitireN('Graf.txt',A,n);
bf(1);
end.

7.6.2. Parcurgerea în adâncime (DF - Depth First)


 Parcurgerea în adâncime se face începând de la un anumit nod i, pe care îl
considerăm vizitat.
 După vizitarea unui nod, se vizitează primul dintre nodurile adiacente,
nevizitate încă, apoi următorul nod adiacent, până când au fost vizitate toate
nodurile adiacente cu el.
 ...
 Parcurgerea continuă în acest mod până când au fost vizitate toate nodurile
accesibile.
202 Capitolul 7. Grafuri neorientate

Exemple de parcurgeri DF ale aceluiaşi graf, pornind de la noduri diferite:

1
Pentru graful alăturat, avem:
Nod pornire 1: 1 3 7 6 2 5 4
2 6 3
Nod pornire 3: 3 7 6 1 2 5 4
Nod pornire 6: 6 3 7 1 2 5 4

4 5 7
Figura 7.9.

Exemple de implementări ale parcurgerii DF

1. Programul următor parcurge DF un graf memorat prin liste de adiacenţă:

Versiunea Pascal Versiunea C++


uses grafuri; #include "grafuri.cpp"
var n:integer; int s[50],n;
s:array[1..50] of integer; T[2][50],Start[50];
T:lista; void df(int nod)
Start:pornire; { int p;
cout<<nod<<" ";
procedure df(nod:integer); p=Start[nod];
var p:integer; s[nod]=1;
begin while (p)
writeln(nod,' '); {
p:=Start[nod]; if (s[T[0][p]]==0)
s[nod]:=1; df(T[0][p]);
while p<>0 do p=T[1][p];
begin }
if s[T[0,p]]=0 }
then df(T[0,p]); main()
p:=T[1,p]; {
end Citire_LA_Astatic("Graf.txt",
end; T,Start,n);
begin df(1);
Citire_LA_Astatic('Graf.txt', }
T,Start,n);
df(1);
end.

2. Programul următor parcurge DF un graf memorat prin matricea de adiacenţă:


Manual de informatică pentru clasa a XI-a 203

Varianta Pascal Varianta C++


uses grafuri; #include "grafuri.cpp"
var n:integer; int s[50],A[50][50],n;
s:array[1..50]of integer;
void df_r(int nod)
A:mat_ad;
{ int k;
procedure df_r(nod:integer); cout<<nod<<" ";
var k:integer; s[nod]=1;
begin for (k=1;k<=n;k++)
write(nod,' '); if(A[nod][k]==1 && s[k]==0)
s[nod]:=1; df_r(k);
for k:=1 to n do }
if (A[nod,k]=1) and (s[k]=0)
then df_r(k); main()
end; { CitireN("Graf.txt",A,n);
df_r(1);
begin }
CitireN('Graf.txt',A,n);
df_r(1);
end.

7.6.3. Estimarea timpului necesar parcurgerii grafurilor

⇒ Parcurgerea BF (sau DF) a grafurilor, pornind de la matricea de adiacenţă, se


face în O(n2). Practic, pornind de la un nod i, se caută pe linia i a matricei
toate nodurile adiacente cu el şi pentru toate cele găsite se procedează în mod
analog.
⇒ Parcurgerea BF (sau DF) a grafurilor pornind de la listele de adiacenţă se face
în O(m). Pornind de la un nod i, se caută toate nodurile adiacente cu el, dar
acestea se găsesc deja grupate în lista asociată nodului respectiv şi numărul
lor corespunde numărului de muchii incidente acestuia. Algoritmul va selecta,
pe rând, toate muchiile, de unde rezultatul de mai sus.

Cum m<n2, pentru parcurgere este preferabil ca graful să fie memorat prin liste
de adiacenţă. Cu toate acestea, pentru simplitate, de multe ori vom efectua
parcurgeri pornind de la matricea de adiacenţă.

7.7. Lanţuri

Reluăm exemplele de la paragraful 7.1.1.

Pentru exemplul 1. Întrebarea este: fiind date două oraşe a şi b, se poate


ajunge cu maşina din a în b?
Pentru exemplul 2. O persoană, a, află o informaţie importantă. Persoana
transmite informaţia tuturor prietenilor, aceştia, la rândul lor, transmit informaţia tuturor
prietenilor lor, ş.a.m.d. Întrebarea este: informaţia ajunge la persoana b?
204 Capitolul 7. Grafuri neorientate

Pentru exemplul 4. Fiind date două triunghiuri, a şi b, sunt ele asemenea? Să


observăm că nu este obligatoriu ca să se fi introdus de la început faptul că triunghiul a
este asemenea cu triunghiul b. Această informaţie poate fi dedusă, de exemplu, prin
faptul că a este asemenea cu k, k cu l şi l cu b.
Analizând aceste întrebări pe graful asociat fiecărei situaţii în parte, ajungem la
concluzia că în toate cazurile trebuie să existe o succesiune de noduri de la
nodul a la nodul b cu proprietatea că oricare două noduri sunt adiacente. Dacă
această succesiune nu există, răspunsul este negativ, altfel răspunsul este pozitiv.
Sau, exprimat în teoria grafurilor, aceasta înseamnă că răspunsul depinde de
existenţa unui lanţ de la a la b. De acum, putem prezenta definiţia lanţului.

Definiţia 7.7. Pentru graful neorientat G=(V,E), un lanţ


L=[v1,v2,...,vp] este o succesiune de noduri cu proprietatea că oricare
două noduri vecine sunt adiacente, adică (v1,v2)∈E, (v2,v3)∈E, ...,
(vp-1,vp)∈E. De altfel, un lanţ poate fi definit prin succesiunea de
muchii (v1,v2)∈E, (v2,v3)∈E, ..., (vp-1,vp)∈E.

 Vârfurile v1 şi vp se numesc extremităţile lanţului.


 Numărul p-1 se numeşte lungimea lanţului. Acesta este dat de numărul de
muchii ce unesc nodurile lanţului.

Definiţia 7.8. Se numeşte lanţ elementar un lanţ care conţine numai


noduri distincte.

Exemple: pentru graful din figura alăturată:


1
1. [1,2,5] este un lanţ elementar cu lungime 2,
între nodurile 1 şi 5.
2 4 3
2. [1,3,4,1,2] este un lanţ (care nu este
elementar) de lungime 4, între nodurile 1 şi 2.

5 Figura 7.10.
 Problema 7.1. Fiind dat un graf şi două
noduri ale sale a şi b, să se scrie un program care decide dacă între ele există un lanţ
sau nu, iar în caz că acest lanţ există, se cere să se afişeze lanţul.

 Rezolvare. Există un lanţ de la a la b, dacă şi numai dacă o parcurgere DF sau


BF, care porneşte de la nodul a, va ajunge să viziteze nodul b. Rămâne de rezolvat
problema modului în care reţinem lanţul de la a la b. Să observăm că fiecare metodă
de parcurgere a grafului, pornind de la un nod j, selectează un altul i, dacă cele
două noduri sunt adiacente şi dacă nodul i este vizitat pentru prima dată. Pentru a
reţine selecţiile astfel efectuate, vom utiliza un vector T, iar elementele acestuia au
semnificaţia următoare:
 j, dacă i este descendent al lui j
T[]
i =
0, dacă i este rădăcina arborelui
Manual de informatică pentru clasa a XI-a 205

Să mai observăm că un nod este selectat o singură dată, deci, în final, T va


reţine, pentru fiecare nod i, nodul j de la care a fost selectat. Pentru nodul de la care
a pornit parcurgerea (a) vom avea T(a)=0, pentru că acest nod nu a fost selectat de
algoritm. De aici, rezultă că drumul poate fi reconstituit, pornind de la T, astfel: se
afişează b, apoi T[b], apoi T[T[b]] ... până când se obţine valoarea 0. Pentru ca
drumul să fie afişat în ordine inversă faţă de modul în care a fost obţinut,
subprogramul refac care îl reconstituie şi îl afişează este recursiv. Programul de mai
jos afişează drumul, pornind de la matricea de adiacenţă a grafului:

Varianta Pascal Varianta C++


uses grafuri; #include "grafuri.cpp"
int s[50],A[50][50],
var n,a1,b:integer;
n,T[50],a,b;
s,T:array[1..50] of integer;
A:mat_ad; void refac (int nod)
procedure refac (nod:integer); {
begin if (nod!=0)
if nod<>0 then {
begin refac (T[nod]);
refac (T[nod]); cout<<nod<<" ";
write(nod,' '); }
end }
end;
void df_r(int nod)
procedure df_r(nod:integer); {
var k:integer; int k;
begin s[nod]=1;
s[nod]:=1; for (k=1;k<=n;k++)
for k:=1 to n do if(A[nod][k]==1 && s[k]==0)
if (A[nod,k]=1) and (s[k]=0) {
then T[k]=nod;
begin df_r(k);
T[k]:=nod; }
df_r(k); }
end
end; main()
{
begin CitireN("Graf.txt",A,n);
CitireN('Graf.txt',A,n); cout<<"a="; cin>>a;
write('a='); readln(a1); cout<<"b="; cin>>b;
write('b='); readln(b); df_r(a);
df_r(a1); if (T[b]!=0) refac(b);
if (T[b]<>0) then refac(b); }
end.

Observaţii foarte importante

1. Să observăm că vectorul T poate fi folosit pentru a obţine lanţuri de la nodul a la


oricare alt nod al grafului.
206 Capitolul 7. Grafuri neorientate

2. Dacă refacem rezolvarea prin utilizarea parcurgerii în lăţime, vom observa că


lanţul obţinut are lungimea minimă. Prin natura ei, parcurgerea BF selectează nodurile
în ordinea "depărtării" lor faţă de nodul de la care a început parcurgerea. Astfel, la
început se vizitează primul nod (a), apoi nodurile pentru care lungimea lanţului de la a
la ele este 1, apoi nodurile pentru care lungimea lanţului de la a la ele este 2, ş.a.m.d.
Programul care urmează afişează un lanţ de lungime minimă între nodurile a şi b:

Varianta Pascal Varianta C++


uses grafuri; #include "grafuri.cpp"
var n,a1,b,i_c,sf_c,i:integer; int n,coada[50],s[50],
s,T,coada:array[1..50] of i_c=1, sf_c=1,
integer; A[50][50],i,T[50],a,b;
A:mat_ad;
void refac (int nod)
procedure refac (nod:integer); { if (nod!=0)
begin { refac (T[nod]);
if nod<>0 then cout<<nod<<" ";
begin }
refac (T[nod]); }
write(nod,' ');
end void bf(int nod)
end; { coada[i_c]=nod;
s[nod]=1;
procedure bf(nod:integer);
while (i_c<=sf_c)
begin
{ i=1;
i_c:=1; sf_c:=1;
while (i<=n)
coada[i_c]:=nod; s[nod]:=1;
{ if (A[coada[i_c]][i]==1
while i_c<=sf_c do
&& s[i]==0)
begin
{ sf_c++;
i:=1;
coada[sf_c]=i;
while i<=n do
s[i]=1;
begin
T[i]=coada[i_c];
if (A[coada[i_c],i]=1) and
}
(s[i]=0)
i++;
then
}
begin
i_c++;
sf_c:=sf_c+1; }
coada[sf_c]:=i; }
s[i]:=1;
T[i]:=coada[i_c]; main()
end; {
i:=i+1; CitireN("Graf.txt",A,n);
end; cout<<"a=";
i_c:=i_c+1; cin>>a;
end cout<<"b=";
end; cin>>b;
begin bf(a);
CitireN('Graf.txt',A,n); if (T[b]!=0) refac(b);
write('a='); readln(a1); }
write('b='); readln(b);
bf(a1);
if T[b]<>0 then refac(b);
end.
Manual de informatică pentru clasa a XI-a 207

Matricea lanţurilor. Întrebări referitoare la situaţiile prezentate la 7.1.1:


a) pentru exemplul 1, întrebarea este: cum putem afla, pentru fiecare oraş în parte,
oraşele în care putem ajunge cu maşina?
b) pentru exemplul 4, întrebarea este: cum putem afla, pentru fiecare triunghi în
parte, care sunt triunghiurile asemenea cu el?
Revenind la graful asociat acestor situaţii, problema constă în a afla pentru
fiecare nod i, nodurile j, pentru care există un lanţ de la i la j. Evident, rezultatele
pot fi reţinute într-o matrice cu n linii şi n coloane (matrice pătratică). Această
matrice se numeşte matricea lanţurilor, iar elementele ei au semnificaţia:

1, dacă ∃ lanţ de la i la j


L(i, j) = 
0, în caz contrar

 Problema 7.2. Fiind dat un graf G, cum putem obţine matricea lanţurilor?
 Răspunsul este uşor de dat. Parcurgem graful începând cu nodul 1. Pentru toate
nodurile j vizitate, vom avea L(1,j)=1, completând astfel prima linie a matricei.
Apoi, vom parcurge din nou, graful, pornind de la nodul 2. Pentru toate nodurile j,
vizitate, vom avea L(2,j)=1, apoi parcurgem graful începând cu nodul 3.... ş.a.m.d.
O anumită îmbunătăţire a algoritmului se obţine dacă ţinem cont de faptul că matricea
lanţurilor este simetrică (de ce?). Lăsăm ca exerciţiu scrierea acestui program.
Întrebare: care este complexitatea acestui algoritm?

7.8. Graf conex


Revenind la exemplele de la 7.1.1, putem pune întrebările:
a) pentru exemplul 1: se poate ajunge cu maşina din orice oraş în oricare altul?
b) pentru exemplul 4: toate triunghiurile sunt asemenea între ele?

Dacă la ambele întrebări răspunsul este afirmativ, ce semnificaţie are el pentru


grafurile asociate? Aceasta înseamnă că pentru orice pereche de noduri, există un
lanţ care le are ca extremităţi. Sau, în limbajul specific teoriei grafurilor, cele două
grafuri sunt conexe.

Definiţia 7.9. Un graf neorientat G=(V,E) este conex, dacă pentru orice
pereche de noduri x,y∈V, există un lanţ în care
Figura 7.11.
extremitatea iniţială este x şi extremitatea finală 1
este y.

1. Graful alăturat este conex. De exemplu, între 2 3


nodurile 1 şi 5 există lanţul [1,2,3,4,5], dar
şi lanţul [1,3,4,5]. Între nodurile 3 şi 5 există
lanţul [3,4,5], ş.a.m.d. Oricum am alege două 4 5
noduri, există lanţul cerut de definiţie.
208 Capitolul 7. Grafuri neorientate

2. Graful alăturat nu este conex. De exemplu, între Figura 7.12.


nodurile 1 şi 4 nu există nici un drum. 4
1

2 3 5

Un graf cu un singur nod este, prin definiţie, conex. Aceasta pentru că nu


există două noduri diferite pentru care să se pună problema existenţei unui lanţ.

 Problema 7.3. Fiind dat un graf G=(V,E), să se scrie un program care să decidă
dacă graful dat este sau nu conex.

 Rezolvare. Ţinând cont de cele învăţate, problema nu este grea. Putem utiliza
una din metodele de parcurgere învăţate, DF sau BF. Ideea este următoarea: dacă,
pornind de la un nod, printr-una din metodele de parcurgere, ajungem să vizităm toate
celelalte noduri, atunci graful dat este conex. Cum putem şti dacă am vizitat toate
nodurile? Simplu, după parcurgere, toate componentele vectorului s reţin 1. Rămâne
sarcina dvs. să scrieţi acest program.

7.9. Componente conexe


Analizăm, din nou, exemplele de la 7.1.1.
a) pentru exemplul 1: se cere o mulţime de oraşe, astfel încât să se poată circula cu
maşina între oricare două oraşe din mulţime, iar dacă un oraş nu aparţine acestei
mulţimi, atunci nu putem ajunge cu maşina de la el la oricare oraş din mulţime.
b) pentru exemplul 4: se cere o mulţime de triunghiuri astfel încât oricare două
triunghiuri din această mulţime sunt asemenea, iar dacă un triunghi nu aparţine
acestei mulţimi, el nu este asemenea cu nici unul din mulţime.
Observăm că fiecare astfel de mulţime este maximală în raport cu relaţia de
incluziune. Dacă nu ar fi aşa, ar exista o altă mulţime care ar include-o. Aceasta ar
avea un element care nu aparţine mulţimii considerate, dar de la care există un lanţ
către oricare alt element din mulţime. Dar aceasta contrazice cerinţa a doua.
Fie graful asociat unuia dintre cazurile prezentate. În termeni din teoria
grafurilor, problema se reduce la determinarea nodurilor unei componente conexe.

Definiţia 7.10. Fie G=(V,E) un graf neorientat şi G1=(V1,E1) un subgraf


al său. Atunci G1=(V1,E1) este o componentă conexă a grafului
G=(V,E) dacă sunt îndeplinite condiţiile de mai jos:
a) Oricare ar fi x,y∈V1, există un lanţ de la x la y.
b) Nu există un alt subgraf al lui G, G2=(V2,E2) cu V1⊂V2 care
îndeplineşte condiţia a).
Manual de informatică pentru clasa a XI-a 209

1. Graful alăturat este alcătuit din două


Figura 7.13.
componente conexe. Prima este alcătuită 4
din nodurile 1, 2, 3 şi muchiile care le 1
unesc pe acestea, a doua este formată din
nodurile 4 şi 5 şi muchia care le uneşte.
2 3 5
2. Graful din figura 7.14. conţine 3 componente
conexe. Aşa cum un graf, cu un singur nod, este
conex, tot aşa un nod izolat alcătuieşte el singur o Figura 7.14.
componentă conexă. 4
1
Observaţii

1. Câte componente conexe poate avea un graf 2 3 5


neorientat cu n noduri? Numărul lor este cuprins
între 1, pentru un graf conex, şi n corespunzător unui graf cu toate nodurile izolate.

2. În unele probleme, se dă un graf neorientat, care nu este conex şi se cere să se


adauge un număr minim de muchii, astfel încât graful să devină conex. În astfel de
cazuri, se determină componentele conexe, fie ele C1, C2, ..., Cp. Fie p numărul lor.
Vom adăuga p-1 muchii, prima uneşte un nod din C1 cu unul din C2, a doua
uneşte un nod din C2 cu unul din C3, ..., ultima uneşte un nod din Cp-1 cu unul
din Cp.

 Problema 7.4. Fiind dat un graf oarecare, se cere să se afişeze nodurile


fiecărei componente conexe.

 Rezolvare. După cum uşor vă puteţi da seama, o parcurgere a grafului (DF sau
BF) pornind de la un anumit nod, vizitează toate nodurile componentei conexe care
îl conţine. Pentru fiecare nod vizitat, s[i] reţine 1. Dacă, după o parcurgere, mai
rămân noduri nevizitate, parcurgerea se reia începând de la primul nod nevizitat.
Evident, numărul componentelor conexe este egal cu numărul de parcurgeri
necesare pentru a fi vizitate toate nodurile.

Varianta Pascal Varianta C++


uses grafuri; #include "grafuri.cpp"
var n,i:integer;
int s[50],A[50][50],n,i;
s:array[1..50]of integer;
A:mat_ad; void df_r(int nod)
{ int k;
procedure df_r(nod:integer);
cout<<nod<<" ";
var k:integer;
s[nod]=1;
begin
for (k=1;k<=n;k++)
write (nod,' ');
if((A[nod][k]==1)
s[nod]:=1;
&& (s[k]==0))
for k:=1 to n do
df_r(k);
if (A[nod,k]=1)and (s[k]=0)
}
then df_r(k);
end;
210 Capitolul 7. Grafuri neorientate

begin main()
CitireN('Graf.txt',A,n); { CitireN("Graf.txt",A,n);
for i:=1 to n do for (i=1;i<=n;i++)
if s[i]=0 then if (s[i]==0)
begin { cout <<"Comp conexa"
writeln('Comp conexa'); <<endl;
df_r(i); df_r(i);
writeln; cout<<endl;
end }
end. }

7.10. Cicluri

Revenim la exemplul cu oraşele legate prin şosele. Întrebarea este următoarea:


există vreun oraş din care putem face o excursie cu maşina, să nu trecem decât o
singură dată pe o şosea, să vizităm mai multe oraşe şi să ne întoarcem de unde am
plecat?
Problema se reduce la a afla dacă graful asociat are sau nu cel puţin un ciclu.
Mai întâi, să definim noţiunea.

Definiţia 7.11. Un lanţ L care conţine numai muchii distincte şi pentru care
nodul iniţial coincide cu nodul final se numeşte ciclu. Dacă, cu excepţia
ultimului nod, care coincide cu primul, lanţul este elementar, atunci ciclul
este elementar (adică, cu excepţia ultimului nod, care coincide cu primul,
conţine numai noduri distincte).

Pentru graful neorientat din figura 7.15. Figura 7.15.


avem:
4
a. [1,2,3,1] este ciclu elementar. 1

b. [1,2,1] nu este ciclu, pentru că (1,2) şi


(2,1) reprezintă o aceeaşi muchie, deci nu conţine 2 3 5
numai muchii distincte.
c. [1,2,3,2,1] nu este ciclu, pentru că nu conţine numai muchii distincte.

 Problema 7.5. Fiind dat un graf conex, G=(V,E), să se scrie un program care
decide dacă graful conţine cel puţin un ciclu.

 Rezolvare. Începem prin a observa că dacă graful nu este conex, putem rezolva
problema verificând dacă există un ciclu într-o componentă conexă a sa. Pentru
simplitate, am preferat să considerăm că graful este conex. Şi aici, problema se poate
rezolva pornind de la o parcurgere DF. Graful conţine cel puţin un ciclu dacă, în
timpul parcurgerii, algoritmul va ajunge în situaţia de a vizita un nod de două ori
Manual de informatică pentru clasa a XI-a 211

(tentativă oricum respinsă, pentru că algoritmul testează acest lucru, vedeţi rolul
vectorului s). Vom da un exemplu, cu graful de mai jos, pe care îl parcurgem DF.

Vizităm nodul 1, apoi primul nod adiacent lui, fie el nodul


2, apoi primul nod adiacent al lui 2, fie el 3, apoi se încearcă 1
vizitarea nodului adiacent cu 3, şi anume 1. Dar acest nod a mai
fost vizitat şi tentativa este respinsă. De ce s-a ajuns în situaţia 2 3
să se încerce vizitarea nodului 3 de două ori? Pentru că nodul 1,
a fost vizitat ca nod de pornire şi pentru că se încearcă vizitarea
lui prin lanţul [1,2,3].
4
Astfel, s-a obţinut ciclul [1,2,3,1]. Figura 7.16.

Programul următor testează dacă un graf conţine sau nu cicluri. Să observăm


că, odată vizitat un nod, accesat prin intermediul muchiei (nod,k), se şterge din
matricea de adiacenţă muchia (k,nod), pentru că altfel ar fi selectată şi această
muchie şi s-ar ajunge în situaţia să fie semnalat un ciclu fals.

Varianta Pascal Varianta C++


uses grafuri,wincrt; #include "grafuri.cpp"
var n:integer; int s[50],A[50][50],gasit,n;
s:array[1..50]of integer;
void df(int nod)
A:mat_ad;
{ int k;
gasit:boolean;
s[nod]=1;
procedure df(nod: integer);
for(k=1;k<=n;k++)
var k:integer;
if (A[nod][k]==1)
begin
{
s[nod]:=1;
A[k][nod]=0;
for k:=1 to n do
if (s[k]==0) df(k);
if A[nod,k]=1 then
else gasit=1;
begin
}
A[k][nod]:=0;
}
if s[k]=0 then df(k)
else gasit:=true; main()
end { CitireN("Graf.txt",A,n);
end; df(1);
begin if (gasit) cout<<"Da";
CitireN('Graf.txt',A,n); else cout<<"Nu";
df(1); }
if gasit then writeln('Da')
else writeln('Nu')
end.

Observaţie

Deşi parcurgerea se face în timp polinomial şi cu toate că programul este


simplu, se putea proceda într-un mod cu mult mai inteligent. Mai mult, aproape că nu
este cazul să facem un program. Graful fiind conex, este suficient să verificăm relaţia
m=n-1,
212 Capitolul 7. Grafuri neorientate

unde m este numărul de muchii, iar n este numărul de noduri. Dacă relaţia este
verificată, înseamnă că graful nu conţine cicluri, altfel, dacă m>n-1 înseamnă că
graful conţine cel puţin un ciclu, iar dacă m<n-1 înseamnă că nu este conex, şi ar
contrazice cerinţa. De unde această observaţie? Pentru a o înţelege, trebuie să
studiem arborii…

7.11. Ciclu eulerian, graf eulerian


Un turist se găseşte într-o cabană, situată lângă gară. Există n cabane, care
sunt unite prin m trasee. Turistul doreşte să parcurgă toate traseele, fiecare traseu
trebuie să fie parcurs o singură dată şi să se întoarcă în cabana iniţială. Este posibil?
În graful asociat ar fi necesar să existe un ciclu eulerian.
Definiţia 7.12. Un lanţ L al unui graf G=(V,E) care conţine fiecare muchie
o dată şi numai o dată se numeşte lanţ eulerian. Dacă x0=xp şi lanţul este
eulerian atunci, ciclul respectiv se numeşte ciclu eulerian.

Definiţia 7.13. Un graf care conţine un ciclu eulerian se numeşte eulerian.

 Faptul că un graf este eulerian nu înseamnă că nu are vârfuri izolate.

Graful alăturat este


eulerian. 2 5

Un ciclu eulerian este:


[1,2,4,5,6,7,4,3,1] 1
4 6

3
Figura 7.17. 7

Teorema 7.1. Un graf G=(V,E), fără vârfuri izolate, este eulerian dacă şi
numai dacă este conex şi gradele tuturor vârfurilor sale sunt numere pare.

 Demonstraţie
⇒ Fie un graf fără vârfuri izolate care conţine un ciclu eulerian (graf eulerian).

Demonstrăm că graful este conex şi gradele tuturor vârfurilor sale sunt pare.
a) Graful este conex. Fie un ciclu eulerian. Fie x şi y două vârfuri ale grafului. Cum
x nu este izolat, rezultă că există o muchie incidentă cu x. Tot aşa, există o
muchie incidentă cu y. Cum ciclul este eulerian (conţine toate muchiile) va
conţine şi cele două muchii. Prin urmare, x şi y sunt unite printr-un lanţ. Cum x şi
y au fost alese întâmplător, rezultă că oricare două vârfuri sunt unite printr-un
lanţ. În concluzie, graful este conex.
Manual de informatică pentru clasa a XI-a 213

b) Gradele tuturor muchiilor sunt pare. Să presupunem că vârful x apare de k ori


în lanţ. La fiecare apariţie a lui x, se vor utiliza 2 muchii (care nu mai pot fi
reutilizate). În concluzie, vom avea d(x)=2k. Cum vârful x a fost luat arbitrar,
rezultă, c.c.t.d.

⇐ Să presupunem că graful G este conex şi are gradele tuturor vârfurilor numere


pare. Să arătăm că G conţine un ciclu eulerian.
Pornim cu unul din vârfurile grafului. Fie el v. Intenţionăm să formăm un ciclu,
(nu neapărat eulerian), care începe şi se termină cu v. Conform ipotezei, există un
număr par de muchii incidente cu v. Selectăm una dintre ele, incidentă la vârful v1.
Atât v cât şi v1 rămân cu un număr impar de muchii incidente. Selectăm, o muchie
incidentă la v1 şi repetăm raţionamentul. Mulţimea vârfurilor este finită. În concluzie,
vom ajunge, la un moment dat, într-unul din vârfurile prin care am trecut (în procesul
de selecţie a muchiilor). Dacă acesta este chiar v am obţinut ciclul căutat. Să
presupunem că am ajuns în alt vârf decât v. Ţinând cont de algoritm, au fost selectate
un număr impar de muchii incidente acelui vârf. Prin ipoteză, există un număr par de
muchii incidente acestuia. Prin urmare, există o muchie pe care o putem selecta.
Procedeul se repetă până când selectăm o muchie incidentă la v.

În acest fel am găsit un ciclu (care începe şi se termină cu v). Să-l notăm cu C.
Există două posibilităţi:
1) Ciclul obţinut este eulerian, caz în care teorema este demonstrată.
2) Ciclul nu este eulerian. În acest caz, operăm asupra grafului G, renunţând la
muchiile selectate. În acest fel, se obţine un graf parţial al lui G pe care îl notăm
H. În acelaşi timp, reţinem ciclul găsit. Din analiza grafului H rezultă:

 gradele tuturor vârfurilor sale sunt pare (pentru fiecare vârf din ciclu, au fost
eliminate muchii în număr par, deci gradul său a rămas par);
 mulţimea muchiilor lui H este nevidă (ciclul găsit nu este eulerian);
 cel puţin una din muchiile lui H are o extremitate comună (vârf) cu una din
muchiile ciclului selectat (contrar, înseamnă că n-ar exista drum între un vârf
care nu aparţine ciclului C şi unul care aparţine acestuia, caz în care se
contrazice faptul că graful este conex).
 Pornind dintr-un astfel de vârf comun, selectăm întocmai ca la început un alt
ciclu C1. Formăm din C şi C1 un nou ciclu (prin intermediul vârfului comun).
Extragem muchiile ciclului C1...
 Repetăm procedeul până la selecţia unui ciclu eulerian.

 Problema 7.6. Fiind dat un graf conex şi care are gradele tuturor muchiilor
pare, se cere să se găsească un ciclu eulerian.
 Rezolvare. Menţionăm că s-ar fi putut da un graf oarecare şi, dacă există, să se
găsească un ciclu eulerian. Ar fi trebuit să verificăm dacă gradele tuturor muchiilor
sunt pare şi dacă graful este conex. Ambele cerinţe sunt foarte uşor de realizat, şi ar fi
complicată înţelegerea algoritmului.
214 Capitolul 7. Grafuri neorientate

Rezolvarea se bazează pe demonstraţia dată problemei. Programul va găsi un


ciclu, apoi, pentru fiecare nod prin care trece ciclul se va căuta, un nou ciclu, iar dacă
acesta este găsit se va intercala în ciclul iniţial. Vectorul eul reţine ciclurile găsite la
un moment dat. Iniţial, prima componentă a vectorului eul reţine 1 (un nod al
grafului). Subprogramul unCiclu are ca parametru de intrare indicele din eul al
nodului de la care se încearcă găsirea unui ciclu. Dacă se găseşte ciclul, acesta este
intercalat în eul începând de la poziţia ind+1. Pentru că inserarea se face într-un
vector, pentru fiecare element introdus, toate elementele vectorului, începând de la
eul+1 se deplasează către dreapta cu o poziţie.
Mai jos, puteţi observa modul în care se execută algoritmul. Iniţial, prima
componentă a lui eul reţine 1. Se găseşte primul ciclu. Testăm dacă se poate găsi
un nou ciclu pentru indicele 2. Nu se găseşte. Se testează existenţa unui ciclu pentru
indicele 3. Se determină un ciclu care se intercalează.
sf

eul 1 0 0 0 0 0 0 0 0 0 0 0

sf

eul 1 2 4 3 1 0 0 0 0 0 0 0

sf

eul 1 2 4 5 6 7 4 3 1 0 0 0

Varianta Pascal Varianta C++


uses grafuri; #include "grafuri.cpp"
var A:Mat_ad; int A[50][50],n,i,j,k,sf,eul[100];
eul:array[1..100] of integer;
n,i,j,k,sf:integer; void unCiclu(int ind)
procedure unCiclu(ind:integer); { for (k=1;k<=n;k++)
begin if (A[eul[ind]][k]==1)
for k:=1 to n do {
if A[eul[ind],k]=1 A[eul[ind]][k]=0;
then
A[k][eul[ind]]=0;
begin
A[eul[ind],k]:=0; sf++;
A[k,eul[ind]]:=0; for(j=sf;j>ind;j--)
sf:=sf+1; eul[j]=eul[j-1];
for j:=sf downto ind+1 do eul[++ind]=k;
eul[j]:=eul[j-1]; unCiclu(ind);
eul[ind+1]:=k; }
ind:=ind+1; }
unCiclu(ind);
end
end;
Manual de informatică pentru clasa a XI-a 215

begin main()
CitireN('Graf.txt',A,n); { CitireN("Graf.txt",A,n);
eul[1]:=1; eul[1]=1; sf=1;
sf:=1; for (i=1;eul[i];i++)
i:=1; unCiclu(i);
while eul[i]<>0 do for (i=1;i<=sf;i++)
begin cout<<eul[i];
unCiclu(i); }
i:=i+1;
end;
for i:=1 to sf do
write(eul[i]);
end.

 Exerciţiu. Încercaţi să îmbunătăţiţi algoritmul de găsire a unui ciclu eulerian.

Astfel, veţi obţine un ciclu ca o listă liniară simplu înlănţuită. În acest fel,
intercalarea unui alt ciclu, în ciclul iniţial devine o operaţie mai simplă, nefiind
necesară deplasarea elementelor vectorului.
Care este complexitatea algoritmului?

7.12. Grafuri bipartite

Într-o firmă există n salariaţi. Fiecare salariat lucrează la propriul birou. Se ştie
că unii dintre aceşti salariaţi sunt prieteni. În vederea sporirii productivităţii muncii,
patronul doreşte să amplaseze birourile celor n salariaţi în două camere, astfel încât
prietenii fiecărui salariat să se găsească în celălalt birou. Se cere să se decidă dacă
acest lucru este posibil, iar în caz afirmativ, se cere o soluţie a problemei, adică
salariaţii care lucrează în fiecare cameră.

Să asociem problemei date un graf cu n noduri, unde un nod reprezintă un


salariat. Dacă salariaţii i şi j sunt prieteni, atunci în graf va exista o muchie care
uneşte muchiile i şi j. A găsi o soluţie la această problemă înseamnă a descompune
mulţimea nodurilor, V, în două submulţimi disjuncte A şi B, asfel încât orice muchie a
grafului să unească un nod din A cu un nod din B. Un graf cu această proprietate se
numeşte graf bipartit.

Definiţia 7.14. Graful neorientat G=(V,E) se numeşte bipartit dacă există


o partiţie a mulţimii V în două submulţimi, A şi B, astfel încât oricare două
vârfuri din aceeaşi mulţime să nu fie adiacente. Un graf bipartit se mai
notează şi G=(A,B,E).

Graful următor este bipartit. El este reprezentat şi în forma care îi pune în


evidenţă această proprietate.
216 Capitolul 7. Grafuri neorientate

-1 -1 2

2 5 1

1 1
1 3
4 6 4
1

3 7 5
-1 -1 6

Figura 7.18. Exemplu de graf bipartit

 Problema 7.7. Se dă un graf neorientat. Să se decidă dacă este bipartit, iar în


caz afirmativ, să se listeze nodurile celor două mulţimi.
 Rezolvare. Ideea este de a marca într-un fel nodurile grafului, de exemplu cu 1 şi
-1, astfel încât două noduri adiacente să fie marcate diferit. Dacă o astfel de marcare
este posibilă, atunci mulţimea A va fi dată de nodurile marcate cu 1 şi mulţimea B va fi
dată de nodurile marcate cu -1. Evident, dacă o astfel de marcare nu este posibilă,
atunci graful nu este bipartit.
Cum realizăm marcarea? Printr-o parcurgere DF. Când se poate ajunge în
situaţia în care marcarea nu este posibilă? La o astfel de situaţie se poate ajunge în
momentul în care un nod este vizitat de două ori şi acesta ar trebui să fie marcat în
acelaşi timp şi cu 1 şi cu -1. Componenta i a vectorului marc va reţine marcajul
nodului i. Dacă marcarea reuşeşte, prima mulţime va fi cea a nodurilor marcate cu 1
şi a doua mulţime va fi cea a nodurilor marcate cu -1. Iată şi programul:

Varianta Pascal Varianta C++


uses grafuri; #include "grafuri.cpp"
int s[50],A[50][50],n,
var A:Mat_ad;
marc[50],bipart=1,j;
s,marc:array[1..50] of integer;
n,j:integer; void df_r(int nod,int v)
bipart:boolean; { int k;
marc[nod]=v; s[nod]=1; v*=-1;
procedure df_r(nod,v:integer);
for (k=1;k<=n;k++)
var k:integer;
{ if (A[nod][k]==1)
begin
if (s[k]==0) df_r(k,v);
marc[nod]:=v; else
s[nod]:=1; if (marc[k]!=v) bipart=0;
v:=-v; }
for k:=1 to n do }
Manual de informatică pentru clasa a XI-a 217

begin main()
if A[nod,k]=1 then { CitireN("Graf.txt",A,n);
if s[k]=0 df_r(1,1);
then df_r(k,v) if (bipart)
else {
if marc[k]<>v cout<<" mult A"<<endl;
then bipart:=false; for (j=1;j<=n;j++)
end if (marc[j]==1)
end; cout<<j<<" ";
cout<<endl<<" mult B"<<endl;
begin
for (j=1;j<=n;j++)
CitireN('Graf.txt',A,n);
if (marc[j]==-1) cout<<j<<" ";
bipart:=true;
}
df_r(1,1);
}
if (bipart) then
begin
writeln('Mult A');
for j:=1 to n do
if marc[j]=1 then write(j,' ');
writeln;
writeln('Mult B');
for j:=1 to n do
if marc[j]=-1 then write(j,' ');
end
end.

Să observăm că dacă un graf orientat are cicluri de lungime impară, atunci el


nu este bipartit.
Care este complexitatea algoritmului prin care se decide dacă un graf este sau
nu bipartit. Indicaţie: care este complexitatea unei parcurgeri DF?

7.13. Grafuri hamiltoniene

 Problema comis-voiajorului. Un comis-voiajor doreşte să viziteze un număr de


n oraşe. Iniţial, acesta se găseşte într-unul dintre ele, notat 1. Comis-voiajorul doreşte
să nu treacă de două ori prin acelaşi oraş şi, la întoarcere, să revină în oraşul 1.
Cunoscând şoselele existente între oraşe se cere să se afişeze toate drumurile pe
care le poate efectua comis-voiajorul.
Dacă încercăm să rezolvăm problema în graful asociat, vom vedea că va trebui
să generăm ciclurile care trec prin prin toate nodurile grafului şi care trec print-un nod
o singură dată. Un astfel de ciclu se numeşte ciclu hamiltonian.

Definiţia 7.15. Fie G=(V, E) un graf. Se numeşte ciclu hamiltonian un


ciclu care trece o singură dată prin toate nodurile grafului (cu excepţia
nodului de început).

 Un graf care admite un ciclu hamiltonian se numeşte graf hamiltonian.


218 Capitolul 7. Grafuri neorientate

Graful alăturat este


2
9 hamiltonian. Un ciclu
3
hamiltonian este:
1
1,2,3,9,8,7,5,6,4,1
8

4 5

6 7
Figura 7.19.

⇒ Pentru determinarea ciclurilor hamiltoniene nu se cunosc algoritmi care să


rezolve problema în timp polinomial. Acesta este motivul pentru care folosim
metoda Backtracking.

 Problema 7.8. Se dă un graf neorientat, prin matricea de adiacenţă. Se cere să


se determine, dacă există, un ciclu hamiltonian.

 Rezolvare. Cum nu se cunosc algoritmi polinomiali, nu ne rămâne decât să


folosim metoda Backtracking. Programul următor găseşte toate ciclurile hamiltoniene:

Varianta Pascal Varianta C++


uses grafuri; #include "grafuri.cpp"
var A:Mat_ad; int n, st[50],A[50][50];
n:integer; int succesor (int k)
st:array[1..50] of integer;
{
function succesor if (st[k]++<n) return 1;
(k:integer):boolean; else return 0;
begin }
if st[k]<n then
begin int valid (int k)
st[k]:=st[k]+1; succesor:=true {
end if (A[st[k-1]][st[k]]==0)
else succesor:=false; return 0;
end;
else
function valid for (int i=1;i<=k-1;i++)
(k:integer):boolean; if (st[i]==st[k]) return 0;
var vl:boolean;i:integer; if (k==n && A[1][st[k]]==0)
begin return 0;
vl:=true;
if A[st[k-1],st[k]]=0 return 1;
then vl:=false }
else void tipar()
for i:=1 to k-1 do
if st[i]=st[k] {
then vl:=false; for (int i=1;i<=n;i++)
if (k=n) and (A[1,st[k]]=0) cout<<st[i];
then vl:=false; cout<<st[1]<<endl;
valid:=vl; }
end;
Manual de informatică pentru clasa a XI-a 219

procedure tipar; void back(int k)


var i:integer; {
begin if (k==n+1) tipar();
for i:=1 to n do write(st[i]); else
writeln (st[1]); {
end; st[k]=1;
while(succesor(k))
procedure back(k:integer);
if (valid(k)) back(k+1);
begin
}
if k=n+1 then tipar
}
else
begin main()
st[k]:=1; {
while(succesor(k)) do CitireN("Graf.txt",A,n);
if valid(k) st[1]=1;st[2]=1;
then back(k+1); back(2);
end }
end;
begin
CitireN('Graf.txt',A,n);
st[1]:=1;
st[2]:=1;
back(2);
end.

Uneori, în anumite condiţii, se poate decide dacă un graf este hamiltonian. Vom
prezenta o serie de teoreme în acest sens. Dar, dacă condiţiile din aceste teoreme nu
sunt verificate, nu înseamnă că graful nu este hamiltonian.

Propoziţia 7.1. Fie G=(V,E) un graf neorientat şi un lanţ elementar care


trece prin toate nodurile grafului: [v1, v2, ..., vn]. Dacă d(v1)+d(vn)≥n,
atunci graful este hamiltonian.

 Demonstraţie
Dacă v1 este adiacent cu vn, problema este rezolvată. Dacă nu sunt adiacente,
vom demonstra prin reducere la absurd.
Vom presupune că graful nu conţine un ciclu hamiltonian. Fie A, mulţimea
nodurilor care sunt adiacente cu v1 şi B mulţimea nodurilor care nu sunt
adiacente cu vn. Evident, numărul de elemente din A este d(v1) şi numărul de
elemente din B este n-1-d(vn) (fără vn avem n-1 noduri, dintre care d(vn) noduri
sunt adiacente cu vn).
Vom arăta că ∀ vi∈A, ∃ vj∈B. Fie vi∈A. Dacă vi-1 este adiacent cu vn,
atunci se poate forma ciclul (vezi figura 7.20):
v1, v2, ..., vi-1, vn, vn-1, ..., vi, v1.

Astfel, se contrazice ipoteza că graful nu conţine un ciclu hamiltonian.


220 Capitolul 7. Grafuri neorientate

v1 v2 vi-1 vi vn-1 vn

Figura 7.20. Graf utilizat la demonstrarea Propoziţiei 7.1

Prin urmare, vi-1 nu este adiacent cu vn sau, altfel spus, vi-1∈B. Deoarece
pentru orice nod din mulţimea A, există un nod în mulţimea B, rezultă că numărul de
elemente din A este mai mic sau egal cu numărul de elemente din B, adică:
d(v1)≤n-1-d(vn), de unde rezultă d(v1)+d(vn)≤n-1. Această ultimă relaţie
contrazice ipoteza d(v1)+d(vn)≥n. Prin urmare, graful este hamiltonian.

Teorema 7.2. Fie graful neorientat G=(V,E), cu n noduri. Dacă pentru


orice pereche de noduri neadiacente vi≠vj, avem relaţia
d(vi)+d(vj)≥n, atunci graful este hamiltonian.

 Demonstraţie. Vom presupune, prin reducere la absurd, că G nu este


hamiltonian. Evident, dacă adăugăm muchii lui G, la un moment dat, G va deveni
hamiltonian. La limită vorbind, prin adăugarea tuturor muchiilor posibile, am obţine un
graf complet, care, se ştie, este hamiltonian. Fie G' graful obţinut din G, prin
adăugarea unor muchii, care îndeplineşte simultan condiţiile de mai jos:
1. Nu este hamiltonian.
2. Orice muchie i-am adăuga, acesta devine hamiltonian.
Fie vi≠vj două noduri din G' (nodurile lui G' sunt tot nodurile lui G) care nu sunt
adiacente. Conform ipotezei 2, dacă vom uni cele două noduri printr-o muchie, G'
va deveni hamiltonian. Aceasta înseamnă că va exista ciclul hamiltonian vi, ..., vj,
vi. Acum, eliminăm muchia (vivj). Prin eliminarea acestei muchii din ciclul
anterior, rămâne doar un lanţ [vi...vj]. În plus, cum vi şi vj nu sunt adiacente,
avem relaţia d(vi)+d(vj)≥n. Dar, din propoziţia anterior demonstrată, rezultă că
din acest lanţ putem obţine un ciclu. Prin urmare graful rămâne hamiltonian.
Aceasta contrazice ipoteza 1 făcută asupra lui G'. Cum G' se putea obţine fără
probleme din G, înseamnă că ipoteza făcută asupra lui G este greşită. Prin urmare,
G este hamiltonian.

Teorema 7.3. Dacă G=(V,E) este un graf cu n noduri şi gradul oricărui


n
vârf este mai mare sau egal , atunci G este hamiltonian.
2
 Demonstraţie. Teorema se demonstrează imediat cu ajutorul celei anterioare.
Pentru orice perechi de noduri, în particular neadiacente, suma gradelor este mai
mare sau egală cu n. Aceasta înseamnă că graful este hamiltonian.
Manual de informatică pentru clasa a XI-a 221

Probleme propuse

1. O cunoştinţă mi-a zis: la mine în birou suntem 5 persoane. Fiecare dintre noi
colaborează cu exact 3 persoane. A zis adevărul?
2. Demonstraţi că într-un graf neorientat numărul nodurilor de grad impar este par.
3. Fiind date n persoane şi m relaţii de prietenie între ele de forma: persoana i este
prietenă cu persoana j, se cere să se stabilească corespondenţele între afirmaţiile
din stânga şi cele din dreapta.

1. În grupul celor n persoane fiecare a. Graful asociat are noduri


persoană are cel puţin un prieten. terminale.
2. Fiecare persoană este prietenă cu b. Graful asociat conţine un subgraf
oricare alta din grup. complet cu k noduri.
3. Există persoane care nu au decât c. Graful asociat este complet.
un singur prieten.
d. Graful asociat nu are noduri
4. Există k persoane din grup astfel izolate.
încât oricare dintre ele este prietenă
cu toate celelalte k-1.

4. Se dau n drepte şi m relaţii de forma: di||dj - dreapta i este paralelă cu


dreapta j. Se ştie că graful asociat este conex. Care dintre afirmaţiile de mai jos este
falsă?
a) Pentru orice dreaptă i, există o dreaptă j care este paralelă cu ea.
b) Toate dreptele sunt paralele între ele.
c) Mulţimea punctelor de intersecţie ale acestor drepte este nevidă.

5. Între n firme există relaţii de colaborare. O relaţie de colaborare este dată ca o


pereche de forma i⇔j şi are semnificaţia că firma i colaborează cu firma j.
Stabiliţi corespondenţa între afirmaţiile din stânga şi cele din dreapta.

1. Orice firmă colaborează cu toate a. Există un lanţ de la i la j.


celelalte. b. Graful asociat are un nod de
grad n-1.
2. Anumite firme îşi întrerup relaţia c. Graful asociat se transformă
de colaborare. într-un subgraf al său.
3. Anumite firme dau faliment şi nu d. Graful asociat este complet.
mai funcţionează.
e. Graful asociat se transformă
4. O firmă colaborează cu toate într-un graf parţial al său.
celelalte.
5. Cu ajutorul unor firme interme-
diare dintre cel n firme, firma i poate
face afaceri cu firma j.
222 Capitolul 7. Grafuri neorientate

6. La un ştrand există 6 bazine. Unele dintre ele sunt unite printr-o ţeavă prin care
poate circula apa. Astfel, bazinul 1 este unit cu bazinul 2, bazinul 4 cu bazinul 5, şi
bazinul 2 cu bazinul 3.
6.1 Ştiind că fiecare bazin poate fi dotat cu un robinet, se cere numărul minim de
robinete care asigură umplerea tuturor bazinelor.
6.2. Care este numărul minim de ţevi prin care pot uni două bazine, astfel încât să
se poată umple toate bazinele cu un singur robinet? Daţi exemple de bazine unite
care asigură cerinţa problemei.
7. Fiind dat un grup de n persoane, în care dintre situaţiile de mai jos se poate
folosi pentru modelare un graf neorientat?
a) Unele persoane din grup cunosc alte persoane din grup.
b) Unele persoane din grup simpatizează alte persoane din grup.
c) În cazul în care toate persoanele lucrează într-o firmă, unele persoane din grup
sunt şefi pentru alte persoane din grup.
d) Unele persoane din grup sunt prietene cu alte persoane din grup.

Exerciţiile 8 şi 9 se referă la graful din


figura 7.21: 1
2

3
5
4
6
8. Care este matricea de adiacenţă a 7
Figura 7.21.
grafului?
1 1 1 1 0 1 0 0 1 1 1 0 1 0 0 1 1 1 0 1 0 0 1 1 1 0 1 0
       
1 1 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0
1 0 1 1 0 0 0 1 0 0 1 0 0 0 1 0 0 1 0 0 0 1 0 0 1 0 0 0
       
1 0 1 1 0 0 1 1 0 1 0 0 0 1 1 0 1 0 0 0 1 1 0 1 0 0 0 1
0
 0 0 0 1 0 0  0
 0 0 0 0 0 0 
0
 0 0 0 0 1 0  1
 1 1 1 1 1 1 
1 0 0 0 0 1 1 1 0 0 0 0 0 1 1 0 0 0 0 0 1 1 0 0 0 0 0 1
       
0 0 0 1 0 1 1 0 0 0 1 0 1 0 0 0 0 1 0 1 0 0 0 0 1 0 1 0

a) b) c) d)

9. Care este valoarea de adevăr a afirmaţiilor de mai jos (A adevărat, iar F, fals):
9.1 Graful este alcătuit din 2 componente conexe.
1
9.2 [1,7,4] este un lanţ. 2
9.3 [2,1,4,3,1] este un ciclu.
9.4 Nodul 2 este izolat. 3

9.5 Graful din problemă are ca graf parţial graful 4


6
alăturat.
7
9.6 Graful din problemă are ca subgraf graful alăturat.
Figura 7.22.
Manual de informatică pentru clasa a XI-a 223

9.7 Nodul 1 are gradul 2.


1 2 3 4
9.8 Nodul 4 are gradul 3.
2 1
9.9 Graful conţine un ciclu de lungime 3.
3 1 4
9.10 Graful alăturat, reprezentat cu
ajutorul listelor de adiacenţă, este subgraf 4 1 3
al grafului din problemă.
Figura 7.23.

10. Care dintre matricele de mai jos ar putea fi matricea de


adiacenţă a grafului alăturat?
0 1 0 1 0 0 1 0 1 0 0 1 0 1 0 0 1 0 0 0
       
1 0 1 0 0 1 0 1 0 0 1 0 1 0 0 1 0 1 0 1
0 1 0 1 1 0 1 0 1 1 0 1 0 1 0 0 1 0 1 1
       
1 0 1 0 1 1 0 1 0 0 1 0 1 1 1 0 0 1 0 1
0 0 1 1 0  0 0 1 0 0  0 0 0 1 0  0 1 1 1 0 
   
Figura 7.24.
a b c d

11. Care dintre matricele de mai jos poate fi matricea de adiacenţă a unui graf
neorientat?
0 1 0 1 0 0 1 0 0 0 1 1 1 1 1 0 1 0 1 0
       
1 0 1 0 0 1 0 1 0 0 1 1 1 1 1 1 0 1 0 0
0 1 0 1 1 0 1 0 1 1 1 1 1 1 1 0 1 0 1 1
       
1 0 1 1 1 1 0 1 0 1 1 1 1 1 1 1 0 1 0 1
0 0 1 1 0  0 0 1 1 0  1 1 1 1 1 0 0 1 1 0 
   
a b c d

12. Câte muchii are graful neorientat reprezentat de matricea 0 1 0 1 1


 
de adiacenţă alăturată? 1 0 0 1 1
0 0 0 1 1
 
1 1 1 0 1
1 1 1 1 0 

13. Care este numărul maxim de componente conexe pe care le poate avea un
graf neorientat cu 5 noduri şi 4 muchii?

14. Care dintre matricele de mai jos este matricea de


adiacenţă a unui subgraf al grafului alăturat?

0 0 0 0 0 1 0 0 0 1 1 0 0 1 1 1
       
0 0 0 0 1 0 0 0 1 0 0 0 1 0 1 1
0 0 0 0 0 0 0 0 1 0 0 0 1 1 0 1
       
0 0 0 0  0 0 0 1  0 0 0 0  1 1 1 0 
   
a b c d Figura 7.25.
224 Capitolul 7. Grafuri neorientate

15. Care este numărul minim şi care este numărul maxim de componente conexe
pe care le poate avea un graf neorientat cu 8 noduri şi 6 muchii?
16. Care este numărul de cifre 0 pe care îl reţine matricea de adiacenţă a unui
graf neorientat cu n noduri şi m muchii?
17. Care este numărul minim şi numărul maxim de noduri izolate pe care îl poate
avea un graf neorientat cu 10 noduri şi 10 muchii?
18. Care este numărul de grafuri neorientate cu 5 noduri.
19. Precizaţi care dintre afirmaţiile
următoare sunt adevărate şi care sunt 1 3
2
false. Toate afirmaţiile se referă la graful
alăturat.
19.1 "6 1 2 3 5 4 7" reprezintă o 5
parcurgere în adâncime a grafului. 4
6
19.2 "3 2 5 1 4 6 7" reprezintă o 7
parcurgere în lăţime a grafului. Figura 7.26.

19.3 Există două noduri din graf pentru care nu există un lanţ care le uneşte.
19.4 "6 1 2 3 5 4 7" este un lanţ în graful dat.
19.5 "3 2 5 1 4 6 7" este un lanţ în graful dat.
19.6 Numărul maxim de muchii care pot fi eliminate astfel încât graful să rămână
conex este 3.
19.7 Numărul minim de muchii care pot fi eliminate pentru ca graful să nu conţină
cicluri este 3.
20. Precizaţi dacă afirmaţiile de mai jos sunt adevărate sau false.
20.1 Cu ajutorul parcurgerii în adâncime se poate determina dacă un graf
neorientat are cel puţin un ciclu.
20.2 Cu ajutorul parcurgerii în lăţime se poate determina dacă un graf este conex.
20.3 Un graf este alcătuit din două componente conexe. Pentru ca graful să devină
conex, este suficient să eliminăm o anumită muchie.
20.4 Un graf este alcătuit din două componente conexe. Fiecare dintre ele
alcătuieşte un graf parţial al grafului dat.
20.5 Un graf este alcătuit din două componente conexe. Fiecare dintre ele
alcătuieşte un subgraf al grafului dat.
20.6 Cu ajutorul pacurgerii în lăţime se poate determina, dacă există, un lanţ între
două noduri ale grafului.
20.7 Cu ajutorul pacurgerii în adâncime se poate determina, dacă există, un lanţ
între două noduri ale grafului.
20.8 Există un graf complet cu n>2 noduri care nu conţine cicluri.
20.9 Orice graf complet este alcătuit dintr-o singură componentă conexă.
Manual de informatică pentru clasa a XI-a 225

21. Se dă un graf neorientat memorat sub forma matricei de adiacenţă. Să se


afişeze toate nodurile care au gradul maxim.
22. Să se scrie un subprogram care transformă matricea de adiacenţă a unui graf în
liste de adiacenţe.
23. Să se scrie o funcţie care transformă listele de adiacenţe în matrice de
adiacenţă.
24. Se dă un graf neorientat şi o succesiune de noduri ale lui. Se cere să se scrie un
subprogram care decide dacă succesiunea dată este sau nu un lanţ.
25. Se dă un graf neorientat memorat prin liste de adiacenţă. Să se scrie un
subprogram care decide dacă graful dat conţine sau nu cicluri.
26. Se dă un graf memorat prin matricea de adiacenţă şi un nod al său, v. Se cere
să se parcurgă graful în lăţime, pornind de la nodul v. Algoritmul va utiliza coada
creată ca listă liniară simplu înlănţuită implementată dinamic.
27. Se dă un graf memorat sub forma matricei de adiacenţă. Se cere să se
afişeze matricea drumurilor. Algoritmul va utiliza parcurgerea în adâncime.
28. Fiind dată matricea drumurilor unui graf, se cere să se scrie programul care
afişează componentele conexe.
29. Partiţia determinată de o relaţie de echivalenţă. Se consideră o mulţime A.
O relaţie oarecare R între elementele acestei mulţimi este o relaţie de echivalenţă
dacă respectă următoarele trei condiţii:
• oricare ar fi x∈A, x R x (x este echivalent cu x), proprietate numită reflexivitate;
• oricare ar fi x,y∈A, din x R y, rezultă y R x, proprietate numită simetrie;
• oricare ar fi x,y,z∈A, din x R y şi y R z, rezultă x R z, proprietate numită
tranzitivitate.

Se citeşte o mulţime de numere între 0 şi 255, sub forma a n perechi,


(x,y), de numere de acest tip. Printr-o astfel de pereche se înţelege că x este
echivalent cu y. Se cere să se determine partiţia generată de relaţia de
echivalenţă considerată pe mulţime.
Exemplu: citim (1 2), (4 5), (2 3), (6 7), (7 1).
Se obţine partiţia: {1,2,3,6,7} {4,5} a mulţimii {1,2,...,7}.
30. Se dau n puncte distincte în plan: Pi(xi,yi) cu 0≤xi,yi≤200, pentru orice
i=1, 2, ..., n. Considerăm că fiecare punct este unit cu cel mai apropiat punct
diferit de el (dacă există mai multe puncte la distanţa minimă, se uneşte cu fiecare
dintre acestea). Numim regiune o mulţime maximală de puncte cu proprietatea că
oricare dintre ele sunt unite printr-un lanţ. Să se determine numărul de regiuni şi să
se vizualizeze regiunile (punctele şi legăturile dintre ele).

31. Se dă matricea de adiacenţă a unui graf neorientat. Se cere să se afişeze


toate ciclurile de lungime k. Un ciclu se va afişa o singură dată.
32. Se dă matricea de adiacenţă a unui graf neorientat. Se cere să se afişeze
toate ciclurile de lungime 4. Se cere un algoritm eficient.
226 Capitolul 7. Grafuri neorientate

33. Stabiliţi dacă următoarele propoziţii sunt sau nu adevărate. Justificaţi, de


fiecare dată, răspunsul. Exemple.
33.1 Orice graf complet cu n>2 noduri este hamiltonian.
33.2 Orice graf complet cu n>2 noduri este eulerian.
33.3 Un graf complet cu 15 noduri este eulerian.
33.4 Orice graf complet este bipartit.
33.5 Există un graf complet care este si bipartit.
33.6 Orice graf hamiltonian este şi eulerian.
33.7 Orice graf eulerian este hamiltonian.
33.8 Există un graf eulerian care este şi hamiltonian.
33.9 Există un graf bipartit care este şi eulerian.
34. Se consideră figura alăturată. Se cere să se traseze figura
neridicând creionul de pe hârtie (este posibil). Contravine acest
Figura 7.27.
fapt teoremei studiate pentru grafuri euleriene?
35*. Algoritmul lui Lee. Se dă un labirint sub forma unei matrice pătratice, L.
L(i,j)=-1, dacă prin camera respectivă nu se poate trece şi 0 în caz contrar. Să se
afişeze distanţele minime de la camera de coordonate (l,c) la toate camerele
accesibile din camera iniţială.
36*. La fel ca la problema anterioară. Se cere drumul care trece printr-un număr
minim de camere între o cameră iniţială şi una finală.
37*. Pe o tablă de şah de dimensiuni nxn se poate deplasa un nebun conform
regulilor obişnuite ale şahului. În plus, pe tablă se pot afla obstacole la diferite
coordonate; nebunul nu poate trece peste aceste obstacole. Să se indice dacă
există vreun drum între două puncte A(X1,Y1) şi B(X2,Y2) de pe tablă şi, în caz
afirmativ, să se tipărească numărul minim de mutări necesare. Se citesc: N, X1,
Y1, X2, Y2, apoi perechi de coordonate ale obstacolelor.
*
38 . Sortare în limita posibilităţilor. Se consideră că într-un vector V cu n
componente se pot inversa numai conţinuturile anumitor componente dintre cele n.
O pereche de componente de indice i şi j ale căror conţinuturi se pot inversa este
dată de perechea i şi j. Fiind date m astfel de perechi şi ştiind că vectorul conţine
numerele 1, 2, …, n într-o ordine oarecare, se cere ca vectorul să conţină numerele
1, 2, ..., n sortate. Pentru sortare se inversează numai conţinuturile componentelor
care se pot inversa (care sunt perechi dintre cele m). Dacă sortarea este posibilă se
vor afişa indicii componentelor care se inversează, iar dacă sortarea nu este posibilă
se afişează Nu. Datele de intrare se găsesc în fişierul text date.in astfel:

Linia 1 n
Linia 2 1, ..., n într-o ordine oarecare.
Linia 3 m
următoarele m linii conţin fiecare câte o pereche de indici i, j.
Manual de informatică pentru clasa a XI-a 227

Exemplu:
3 Programul va afişa:
3 1 2 1 2
2 2 3
2 3
1 2

*
39 . Lucrare în echipă. Se doreşte scrierea unei aplicaţii de informare a călătorilor
privind transportul în comun într-un oraş. Se cunosc cele n staţii de autobuz din oraşul
respectiv. De asemenea, se ştie traseul a k linii de autobuz (staţiile prin care acestea
trec). Se cere ca aplicaţia să furnizeze modul în care o persoană se poate deplasa cu
autobuzul între două staţii date, în ipotezele:
a) În număr minim de staţii.
b) Prin utilizarea unui număr minim de linii de autobuz.

Este sarcina dvs. să organizaţi intrările şi ieşirile de date!

Răspunsuri

1. Nu. Ar rezulta un graf cu 5 noduri. Cum fiecare persoană colaborează cu exact 3


persoane, înseamnă că fiecare nod are gradul 3. De aici, rezultă că suma gradelor
este 15. Rezultă astfel că 2m=15, deci m nu ar fi număr întreg.
2. Dacă ar fi impar, suma gradelor impare ar fi un număr impar. Cum suma gradelor
pare este un număr par, rezultă că suma tuturor gradelor este un număr impar. Ori,
acesta trebuie să fie un număr par, pentru că ea este egală cu dublul numărului de
muchii. Absurd.
3. 1-d; 2-c; 3-a; 4-b.
4. c) Faptul că graful este conex, înseamnă că între oricare două noduri există un
lanţ care le are ca extremităţi. Dar din di||dj şi dj||dk ⇒ di||dk rezultă că toate
dreptele sunt paralele între ele.
5. 1-d; 2-e; 3-c; 4-b; 5-a.
6. 6.1 Graful conţine 3 componente conexe. Pentru fiecare componentă conexă
este necesar un robinet. 6.2 Pentru a folosi un singur robinet, este necesar ca graful
să fie conex. Cum are 3 componente conexe, sunt suficiente 2 ţevi.
7. Pentru a putea modela anumite relaţii cu ajutorul unui graf neorientat trebuie ca
relaţia existentă între i şi j să fie reciprocă, pentru că muchia (i,j) presupune că i
este în relaţie cu j şi j este în relaţie cu i. Dacă i cunoaşte pe j, nu este obligatoriu
ca j să cunoască pe i, dacă i simpatizează pe j, nu este obligatoriu ca j să
simpatizeze pe i, dacă i este şeful lui j, j nu poate fi şeful lui i. În concluzie,
răspunsul este d) pentru că relaţia de prietenie este reciprocă.
228 Capitolul 7. Grafuri neorientate

8. b).
9. 9.1 A; 9.2 F; 9.3 F; 9.4 F; 9.5 F; 9.6 A; 9.7 F; 9.8 A;
9.9 A; 9.10 A.
10. b). Dacă matricea este de adiacenţă, atunci vă puteţi orienta după gradele
vârfurilor. Evident, graful reprezentat de matricea de adiacenţă trebuie să aibă
vârfurile cu aceleaşi grade cu vârfurile grafului reprezentat în desen.
11. d). Desigur, puteţi desena graful, dar, mai uşor, eliminaţi variantele în care
aveţi 1 pe diagonala principală, sau acelea în care matricea nu este simetrică.
12. 8. Dacă matricea este dată corect, nu este nevoie să desenaţi graful pentru
ca, apoi, sa-i număraţi muchiile. Se ştie că suma gradelor tuturor nodurilor este
egală cu dublul numărului de muchii. Prin urmare, este suficient să însumaţi
elementele reţinute de matrice si să împărţiţi rezultatul la 2.
13. 2.
14. a) Dacă matricea are 4 linii şi 4 coloane, este clar că subgraful ar rezulta prin
eliminarea unui singur nod şi a muchiilor incidente lui. Dacă eliminăm nodul din
centru, se obţin 4 noduri izolate. Oricare alt nod am elimina, rămân un nod cu
gradul 3 şi 3 noduri cu gradul 1.
15. 2 componente conexe şi 5 componente conexe.
16. n2-2m. Matricea de adiacenţă are n2 elemente. Am văzut faptul că suma
tuturor cifrelor de 1 (adică a gradelor vârfurilor) este 2m (unde m este numărul de
muchii).
17. 0 şi 5.
18. 210.
19. 19.1 A, 19.2 A, 19.3 F, 19.4 A, 19.5 F, 19.6 A, 19.7 A.
20. 20.1 A, 20.2 A, 20.3 F, 20.4 F,
20.5 A, 20.6 A, 20.7 A, 20.8 F, 20.9 A.
29., 30. Descompunerea unui graf în componente conexe.
31. Backtracking. O soluţie are lungimea k. Pentru a evita ca un ciclu să fie
afişat de k ori, elementele vor fi aşezate în stivă în ordine strict crescătoare.
32. Fie i<j<k<l 4 noduri care formează un ciclu. Avem:
A(i,j)=1 A(i,l)=1
A(k,j)=1 A (k,l)=1
Astfel, în matricea de adiacenţă se formează un dreptunghi. Trebuie identificate
toate dreptunghiurile astfel formate.
33. 33.1 A; 33.2 F; 33.3 A; 33.4 F. 33.5 A (pentru n=2); 33.6 F; 33.7 F;
33.8 A; 33.9. A.
Manual de informatică pentru clasa a XI-a 229

34. Eliminând baza plicului vom avea un graf eulerian care se poate trasa "fără a
ridica creionul". Se pleacă de la unul dintre nodurile aflate la baza "plicului". După
trasarea ciclului, vom trasa baza "plicului". Evident, în final se ajunge în celălalt nod
al "bazei plicului".
35. Se poate lucra direct pe matricea L. Ideea: pentru camera iniţială vom avea
L(i,j)=1. Pentru toate camerele accesibile, vecine cu ea, vom avea L(i,j)=2,
apoi pentru toate camerele accesibile cu ele vom avea L(i,j)=3, ... ş.a.m.d.
Pentru a obţine această marcare vom parcurge în lăţime graful asociat. Putem
evita memorarea acestuia. Vom introduce în coadă coordonatele camerei iniţiale.
Vom încărca în coadă coordonatele tuturor camerelor vecine pentru care
L(i,j)=1. Pentru fiecare astfel de cameră, pentru care, iniţial, L(i,j)=0, vom
avea L(i,j)=2. Se trece apoi la următorul element din coadă cu care se
procedează asemănător. Se ştie că prin parcurgerea în lăţime se vizitează
nodurile în ordinea lungimii drumului, de la ele, la nodul iniţial. Deducem,
astfel, că marcarea este corectă. Algoritmul se termină când coada este vidă. În
final, se afişează matricea L.
36. Se procedează ca la problema anterioară. Imediat ce a fost vizitată camera
finală, se reface drumul de la camera iniţială către ea. Astfel, se pleacă de la
camera finală, marcată cu k. Printre vecinele acestei camere se caută una care
este marcată cu k-1. Printre camerele vecine cu ea se caută una care este
marcată cu k-2. Se procedează în mod asemănător până se ajunge la camera
iniţială marcată cu 1. Drumul se afişează în ordinea inversă găsirii lui, de la camera
finală la cea iniţială.
37. Algoritmul lui Lee.
38. Asociem problemei un graf neorientat. Nodurile sunt indicii elementelor
vectorului, de la 1 la n. Când conţinuturile a două elemente se pot inversa, nodurile
corespunzătoare sunt unite printr-o muchie. Dacă nodurile i1, i2, ..., ik sunt unite
printr-un drum: atunci interschimbările
(i1, i2), (i2, i3), ..., (ik-1, ik), (ik-1, ik-2), ..., (i2, i1)
inversează conţinuturile elementelor de indice i1 şi ik, lăsând conţinuturile
celorlalte elemente de indici i2, ..., ik-1 nemodificate. O parcurgere în lăţime
determină distanţa minimă între două noduri.
230

Capitolul 8
Grafuri orientate

8.1. Noţiunea de graf orientat

Nu întotdeauna grafurile neorientate pot exprima "relaţiile" existente între


anumite elemente. Pentru a ne da seama de acest fapt, vom da câteva exemple.

1. Considerăm un grup de n persoane, unde fiecare persoană deţine un telefon


mobil. Pe agenda telefonului mobil a fiecărei persoane se găsesc numere de telefon
ale altor k≥0 persoane din grup. Să observăm că dacă persoana i are în agenda
telefonică numărul de telefon al persoanei j, nu este obligatoriu ca persoana j să
aibă în agendă numărul de telefon al persoanei i.

2. Un autor doreşte să introducă anumite noţiuni. Pentru a obţine o lucrare


valoroasă, acesta doreşte ca orice noţiune pe care o introduce să fie precedată în
lucrare de noţiunile pe care le presupune deja introduse.
3. Un program este alcătuit din n instrucţiuni, atribuiri, apeluri de subprograme,
afişări, citiri, instrucţiuni decizionale. Aici sunt excluse instrucţiunile repetitive. Acestea
rezultă în urma instrucţiunilor decizionale şi a ordinii de executare a instrucţiunilor.
Pentru a prezenta ordinea în care se execută instrucţiunile, unui program i se poate
asocia o schemă logică.

Elementele între care există anumită relaţii se numesc şi de această dată


noduri sau vârfuri. Două vârfuri pot fi unite printr-un arc. Arcul (i,j) este
reprezentat ca o săgeată de la i la j şi are semnificaţia generală că există o relaţie
de la i la j (atenţie, nu şi de la j la i).

Pentru exemplul 1, arcul (i,j) are semnificaţia că i are în agenda telefonică


numărul lui j. Pentru exemplul 2, arcul (i,j) are semnificaţia că este necesară
cunoaşterea noţiunii i pentru a putea înţelege noţiunea j. Pentru exemplul 3, arcul
(i,j) are semnificaţia că după executarea
instrucţiunii i, este posibil să se execute 1
instrucţiunea j.
6
Procedând aşa cum am arătat, obţinem 2
un graf orientat ca în figura alăturată:

5
Figura 8.1. 3 4
Exemplu de graf orientat
Manual de informatică pentru clasa a XI-a 231

Definiţia 8.1. Se numeşte graf orientat perechea ordonată G=(V,A),


unde:
 V={v1, v2, ..., vn} este o mulţime finită de elemente numite vârfuri
sau noduri.
 A este o mulţime de arce. Vom nota un arc prin perechea ordonată
(vi,vj) cu i≠j.

De exemplu, pentru graful din figura 8.1., avem V={1,2,3,4,5,6} şi


A={(1,6),(6,1),(6,5),(4,5),(2,1),(2,3)}.

Observaţii

1. În cazul grafurilor orientate, dacă există arcul (vi,vj) nu înseamnă că există


automat şi arcul (vj,vi). Din acest motiv, în definiţie, s-a precizat că un arc este o
pereche ordonată, de forma (vi,vj).
2. Din definiţie, rezultă că nu există arce de la un nod la el însuşi. Astfel, un arc a
fost definit (vi,vj) cu i≠j.
3. Să observăm că mulţimea arcelor A, respectă
relaţia A⊂V×V, unde V×V este produsul cartezian 1 1
al mulţimii V cu ea însăşi.
2 2
4. Grafurile neorientate sunt cazuri particulare
de grafuri orientate, mai precis acele grafuri
a) b)
orientate în care pentru orice arc (vi,vj) există
arcul (vj,vi). Alăturat, puteţi observa un graf Figura 8.2.
neorientat (figura 8.2., a)), reprezentat sub forma a) graf neorientat; b) graf orientat.
unui graf orientat (figura 8.2., b)).
5. Grafurile orientate se mai numesc şi digrafuri.

Definiţia 8.2. În graful orientat G=(V,A), vârfurile distincte vi,vj∈G sunt


adiacente dacă există cel puţin un arc care le uneşte.

Astfel, avem următoarele cazuri:

a) Există numai arcul (vi,vj)∈A - în acest caz, spunem că arcul (vi,vj)∈A este
incident spre exterior cu vi şi spre interior cu vj.
b) Există numai arcul (vj,vi)∈A - în acest caz, spunem că arcul (vj,vi)∈A este
incident spre interior cu vi şi spre exterior cu vj.
c) Există arcul (vi,vj)∈A şi arcul (vj,vi)∈A.

Definiţia 8.3. Într-un graf orientat, prin gradul exterior al unui vârf v vom
înţelege numărul arcelor incidente spre exterior cu v. Gradul exterior al
unui nod va fi notat cu d+(v).
232 Capitolul 8. Grafuri orientate

Definiţia 8.4. Într-un graf orientat prin gradul interior al unui nod v vom
înţelege numărul arcelor incidente spre interior cu v. Gradul interior al
unui nod va fi notat cu d-(v).

Pentru vârful i din figura alăturată, avem:


d+(i)=3, d-(i)=2.
i
Figura 8.3.
Exemplu de nod cu mai multe
arce incidente

O relaţie utilă: fie un graf orientat cu n vârfuri şi m arce. Avem relaţia:

d + (1) + d + (2) + ... + d + (n) = d − (1) + d − (2) + ... + d − (n) = m .

 Demonstraţie. Relaţia este adevărată, pentru că fiecare arc este incident spre
exterior cu un vârf şi fiecare arc este incident spre interior cu un vârf.

Revenim la exemplele prezentate la începutul acestui paragraf.


Pentru exemplul 1, dacă gradul exterior al nodului i este k, înseamnă că
persoana i are în agenda telefonică numerele a k persoane din grup, iar dacă gradul
interior al nodului i este l înseamnă că numărul persoanei i este în agenda
telefonică a l persoane.
Pentru exemplul 2, dacă gradul exterior al nodului i este k, înseamnă că
noţiunea i este necesară pentru înţelegerea a altor k noţiuni, iar dacă gradul interior
al nodului i este l, înseamnă că pentru a înţelege noţiunea i, este necesar ca alte l
noţiuni să fie înţelese.
Pentru exemplul 3, dacă gradul exterior al nodului i este k, înseamnă că după
instrucţiunea i se pot efectua alte k instrucţiuni (i este instrucţiune decizională), iar
dacă gradul interior al nodului i este l, înseamnă că instrucţiunea i se poate executa
după executarea uneia din cele l instrucţiuni.
n ( n −1)
O relaţie utilă: avem 4 2
grafuri orientate cu n noduri.

 Demonstraţia se face prin inducţie. Dacă n=1, avem 1 graf orientat. Dacă
n=2, cele două noduri pot să nu fie sau să fie adiacente. În acest din urmă caz,
putem avea arcul (v1,v2) sau arcul (v2,v1) sau putem avea ambele arce
(v1,v2) şi (v2,v1). În total, avem 4 grafuri orientate, valoare care rezultă şi din
formulă, dacă înlocuim n cu 2.
Presupunem formula adevărată, adică dacă sunt n vârfuri, avem
n ( n −1)
2
4
grafuri orientate. Trebuie să demonstrăm că dacă sunt n+1 vârfuri, avem
n ( n +1)
2
4
Manual de informatică pentru clasa a XI-a 233

grafuri orientate. Adăugăm vârful n+1. Acest nod poate fi adiacent cu fiecare
dintre celelalte n vârfuri în exact 3 moduri (vedeţi adiacenţa) sau poate să nu fie
adiacent. Atunci, numărul de grafuri orientate cu n+1 noduri este
n ( n −1) n ( n −1) n ( n +1)
+n
4 2 ×4 =n
4 2 = 4 2 .

8.2. Memorarea grafurilor orientate

Memorarea grafurilor orientate se face la fel precum memorarea grafurilor


neorientate.

Pentru fiecare structură de date pe care o vom folosi vom avea câte o
procedură (funcţie) care citeşte datele respective. Toate aceste subprograme se
găsesc grupate în unitatea de program grafuri.pas (pentru Pascal) şi în
grafuri.cpp (pentru C++).

Toate subprogramele pe care le utilizăm citesc datele dintr-un fişier text, în care
pe prima linie vom scrie numărul de noduri (n), iar pe următoarele linii, câte o muchie
(i,j) ca în exemplul următor, în care este prezentat un graf şi liniile fişierului text
care este citit pentru el:

Fişierul 6
1 text: 1 2
6
1 3
1 5
2 2 3
5 3 4
3 4 5
4
Figura 8.4.

Trecem la prezentarea structurilor prin care putem memora datele referitoare


la un graf.

A. Memorarea grafului prin matricea de adiacenţă

An,n - o matrice pătratică, unde elementele ei, ai,j au semnificaţia:

0 1 1 0 1 0
1, pentru (i, j) ∈ A
a i, j =  0
 0 1 0 0 0
0, pentru (i, j) ∉ A 0 0 0 1 0 0
 
Pentru graful din figura 8.4, matricea de adiacenţă 0 0 0 0 1 0
este prezentată alăturat. 0 0 0 0 0 0
 
0 0 0 0 0 0
234 Capitolul 8. Grafuri orientate

Observaţii

1. Întrucât, din modul în care a fost definit graful, rezultă că nu există arce de la un
nod la el însuşi, rezultă că elementele de pe diagonala principală reţin 0 (ai,i=0,
oricare ar fi i∈{1,2,...,n}).
2. Matricea de adiacenţă nu este în mod obligatoriu simetrică.
3. Suma elementelor de pe linia i, i ∈{1,2,...,n} are ca rezultat gradul exterior
al nodului i, d+(i).
4. Tot aşa, suma elementelor de pe coloana i, i ∈{1,2,...,n} are ca rezultat
gradul interior al nodului i, d-(i).
5. Suma tuturor elementelor matricei de adiacenţă este, de fapt, suma gradelor
exterioare (sau interioare) adică suma arcelor, m.
6. Dacă graful citit are un număr mic de muchii, atunci matricea de adiacenţă este o
formă ineficientă de memorare a lui, pentru că ea va reţine o mulţime de 0.

Subprogramele pe care le utilizăm sunt:

Varianta Pascal Varianta C++


procedure CitireO void CitireO(char Nume_fis[20],
(Nume_Fis:string;
int A[50][50], int& n)
var A:Mat_ad; var n:integer);
{ int i,j;
var f:text; fstream f(Nume_fis,ios::in);
i,j:byte; f>>n;
begin while (f>>i>>j) A[i][j]=1;
Assign(f,Nume_Fis); f.close();
Reset(f); Readln(f,n); }
while(not eof(f)) do
begin
readln(f,i,j);
A[i,j]:=1;
end;
close(f);
end;

B. Memorarea grafului prin liste de adiacenţe implementate prin utilizarea


alocării statice

Se face la fel ca în cazul grafurilor neorientate, diferenţa este dată de faptul că


arcul (i,j) este înregistrat o singură dată (nu ca în cazul grafurilor neorientate când
reţineam (i,j) şi (j,i)).

În continuare, vor fi prezentate subprogramele care construiesc listele de


adiacenţă alocate static:
Manual de informatică pentru clasa a XI-a 235

Varianta Pascal Varianta C++


procedure Citire_LA_AstaticO void Citire_LA_AstaticO
(Nume_fis:string;var T:Lista; (char Nume_fis[20],
var Start:pornire; int T[2][50], int Start[50],
var n:integer); int& n)
var i,j,k:integer; { int i,j,k=0;
f:text; fstream f(Nume_fis,ios::in);
f>>n;
begin
while (f>>i>>j)
k:=0;
{ k++;
Assign(f,Nume_Fis);
T[0][k]=j;
Reset(f);
T[1][k]=Start[i];
Readln(f,n);
Start[i]=k;
while(not eof(f)) do
}
begin
f.close();
readln(f,i,j);
}
k:=k+1;
T[0,k]:=j;
T[1,k]:=Start[i];
Start[i]:=k;
end;
close(f);
end;

C. Memorarea grafului prin liste de adiacenţe implementate prin utilizarea


alocării dinamice

Varianta Pascal Varianta C++


procedureCit_LA_AdinamicO void Cit_LA_AdinamicO
(Nume_fis:string; (charNume_fis[20],Nod* L[50],
var L:lista_a; var n:integer); int& n)
var i,j:byte; { Nod* p;
p:ref; int i,j;
f:text; fstream f(Nume_fis,ios::in);
f>>n;
begin
while (f>>i>>j)
Assign(f,Nume_Fis);
{ p=new Nod;
Reset(f);
p->adr_urm=L[i];
Readln(f,n);
p->nd=j;
while (not Eof(f)) do
L[i]=p;
begin
}
Readln(f,i,j);
f.close();
new(p);
}
p^.adr_urm:=L[i];
p^.nd:=j;
L[i]:=p;
end;
Close(f);
end;
236 Capitolul 8. Grafuri orientate

D. Memorarea grafului prin lista arcelor se face la fel ca în cazul grafurilor


neorientate unde, fiecare componentă a unui vector reţine extremităţile arcului şi,
eventual alte informaţii referitoare la arc, cum ar fi costul acestuia.

E. Memorarea grafului prin matricea ponderilor

Fie G=(V,A) un graf orientat. Ataşăm fiecărui arc (x,y)∈A o pondere (un
cost): cx,y>0. Priviţi exemplul de mai jos:

2
Modul în care fişierul
grafuri.txt reţine
7
1 datele:
4
5
1 2 1
1 9
3 1 3 9
1 5 3
3 2 4 3
1 3 2 3 7
2 4 3 2
4 1 1
5 2 5 2 4
4 5 4 2

Figura 8.5. Exemplu de graf orientat

1. Definim pe mulţimea V×V o funcţie, astfel:

c x, y , x ≠ y, ( x, y ) ∈ A

f : V × V → ℜ+ , f (( x, y )) = ∞, x ≠ y, ( x, y ) ∉ A
0, x=y

Funcţia este reţinută de o matrice, numită matricea ponderilor forma 1.
Pentru graful din exemplu, matricea ponderilor forma 1 este prezentată mai jos:

0 1 9 ∞ 3
 
∞ 0 7 3 ∞
∞ ∞ 0 ∞ ∞
 
 1 ∞ 2 0 ∞
∞ 4 ∞ 2 0 
 
În continuare, este prezentat subprogramul care realizează citirea grafului în
forma matricei ponderilor în forma 1.
Manual de informatică pentru clasa a XI-a 237

Varianta Pascal Varianta C++


const Pinfinit = 1.e20; const float PInfinit = 1.e20;
type mat_c=array[1..50,1..50] of void Citire_cost(char
real; Nume_fis[20], float A[50][50],
int& n)
var A:mat_c;
... {
int i,j;
procedure float c;
Citire_cost(Nume_Fis:string; var fstream f(Nume_fis,ios::in);
A:Mat_c; var n:integer); f>>n;
var f:text; for (i=1;i<=n;i++)
i,j:byte; for (j=1;j<=n;j++)
if (i==j) A[i][j]=0;
begin else A[i][j]=PInfinit;
Assign(f,Nume_Fis); while (f>>i>>j>>c)
Reset(f); A[i][j]=c;
Readln(f,n); f.close();
for i:=1 to n do }
for j:=1 to n do
if i=j
then A[i,j]:=0
else A[i,j]:=Pinfinit;
while (not eof(f)) do
readln(f,i,j,A[i,j]);
close(f);
end;

2. Definim pe mulţimea V×V o funcţie, astfel:

c x, y , x ≠ y, ( x, y ) ∈ A

g : V × V → ℜ, g (( x, y )) = − ∞, x ≠ y, ( x, y ) ∉ A
0, x=y

Funcţia este reţinută de o matrice, numită matricea ponderilor forma 2.


Pentru graful din figura 8.5, vom obţine astfel:

 0 1 9 −∞ 3 
 
− ∞ 0 7 3 − ∞
 − ∞ − ∞ 0 − ∞ − ∞
 
 1 −∞ 2 0 − ∞
− ∞ 4 − ∞ 2
 0 

În continuare, puteţi observa subprogramul care realizează citirea grafului sub


forma matricei ponderilor în forma 2.
238 Capitolul 8. Grafuri orientate

Varianta Pascal Varianta C++


const Minfinit = -1.e20; float MInfinit = -1.e20;
procedure ...
Citire_cost1(Nume_Fis:string; void Citire_cost1(
var A:Mat_c; var n:integer); char Nume_fis[20],
var f:text; float A[50][50], int& n)
i,j:byte; {
c:real; int i,j;
begin float c;
Assign(f,Nume_Fis); fstream f(Nume_fis,ios::in);
Reset(f); Readln(f,n); f>>n;
for i:=1 to n do for (i=1;i<=n;i++)
for j:=1 to n do for (j=1;j<=n;j++)
if i=j if (i==j) A[i][j]=0;
then A[i,j]:=0 else A[i][j]=MInfinit;
else A[i,j]:=Minfinit; while (f>>i>>j>>c)
while (not eof(f)) do A[i][j]=c;
begin f.close();
readln(f,i,j,c); }
A[i,j]:=c;
end;
Close(f);
end;

8.3. Graf parţial, subgraf

Definiţia 8.5. Un graf parţial al unui graf orientat G=(V,A) este un graf
G1=(V,A1), unde A1⊆A.

Un graf parţial al unui graf dat este el însuşi sau se obţine din G prin
suprimarea anumitor arce.

1 1

2 3 3
2
rezultă

4 4
Figura 8.6.
Obţinerea unui
graf parţial G=(V,A) G1=(V,A1)

Referitor la exemplul 1 din paragraful 8.1, unele persoane îşi şterg din agendă
numerele altor persoane din grup. Aceasta înseamnă că noul graf nu va mai avea
anumite arce, deci va deveni un graf parţial al grafului iniţial.
Manual de informatică pentru clasa a XI-a 239

 Exerciţiu! Câte grafuri parţiale are un graf cu m arce?

Definiţia 8.6. Un subgraf al unui graf orientat G=(V,A) este un graf


G1=(V1,A1), unde V1⊂V, A1⊂A, iar arcele din A1 sunt toate arcele din A
care sunt incidente numai la vârfuri din mulţimea V1.

Un subgraf al unui graf G este graful G sau se obţine din G prin suprimarea
anumitor vârfuri şi a tuturor arcelor incidente cu acestea.

1 1

2 3 3
rezultă

4 4
Figura 8.7.
Obţinerea unui G1=(V1,A1)
subgraf G=(V,A)

Referitor la exemplele din paragraful 8.1:

1. Pot exista persoane din grup care îşi pierd telefonul mobil. Astfel, numerele de
telefon ale respectivelor persoane aflate în agenda altora, pentru moment, nu mai
folosesc. De asemenea, ceilalţi din grup nu mai păstrează numerele de telefon ale
acestora. În graful iniţial se renunţă la vârfurile respective şi la arcele adiacente lor.
Astfel, se obţine un subgraf al grafului iniţial.
2. Autorul renunţă la prezentarea anumitor noţiuni. Din nou, se obţine un subgraf al
grafului iniţial.

 Exerciţiu! Câte subgrafuri are un graf cu n vârfuri?

8.4. Parcurgerea grafurilor. Drumuri. Circuite

 Parcurgerea grafurilor orientate se face la fel precum parcurgerea grafurilor


neorientate. Aceasta înseamnă că parcurgerea se poate face în două moduri:
în adâncime (DF) şi în lăţime (BF). Subprogramele sunt aceleaşi.
 Pentru graful orientat G=(V,A), un drum D=[v1,v2,...,vp] este o succesiune
de vârfuri (v1,v2)∈A, (v2,v3)∈A, ..., (vp-1,vp)∈A. Vârfurile v1 şi vp se
numesc extremităţile drumului. Numărul p-1 se numeşte lungimea
drumului. Acesta este dat de numărul arcelor ce unesc vârfurile drumului.
Determinarea existenţei unui drum între două vârfuri şi determinarea unui drum
între două vârfuri date, eventual a unui drum de lungime minimă, se face la fel
ca în cazul grafurilor neorientate.
240 Capitolul 8. Grafuri orientate

 Un drum D=[v1,v2,...,vp] este elementar dacă conţine numai vârfuri


distincte.
 Un circuit, într-un graf orientat, este un drum în care vârful iniţial coincide cu
vârful final şi care conţine numai arce distincte. Printr-o simplă parcurgere DF
putem determina, ca şi în cazul grafurilor neorientate, dacă un graf conţine sau
nu un circuit.

Anumite noţiuni prezentate în cazul grafurilor neorientate se regăsesc şi în


cazul grafurilor orientate.
 Pentru graful orientat G=(V,A), un lanţ D=[v1,v2,...,vp] este o succesiune
de vârfuri astfel încât între oricare vârfuri distincte din vi,vi+1 există fie arcul
(vi,vi+1), fie arcul (vi+1,vi).
 Un lanţ L=[v1,v2,...,vp] este elementar dacă conţine numai vârfuri distincte.

Revenim la exemplele din paragraful 8.1.

Pentru exemplul 1: persoana i are un mesaj de transmis persoanei j. Dacă


există posibilitatea ca ea să sune o persoană al cărei număr îl are în agendă, aceasta
o altă persoană ş.a.m.d până este sunată persoana j, înseamnă că există un drum
de la i la j. De asemenea, dacă există posibilitatea ca persoana i să primească
mesajul pe care l-a transmis, înseamnă că există un circuit de la i la j.
Pentru exemplul 2: dacă graful are un circuit înseamnă că există cel puţin o
noţiune care nu poate fi explicată decât prin
intermediul altora care, la rândul lor, ar trebui 1
explicate exact prin noţiunea care nu poate fi
explicată fără ele. Se mai întâmplă şi aşa. Un
exemplu de limbaj care nu poate fi predat în mod
clasic este Java. De exemplu, cel mai simplu
2
program utilizează din plin programarea orientată
pe obiecte, care se studiază după ce am învăţat să 3 4
scriem programe simple.
Pentru exemplul 3: dacă după executarea
5
instrucţiunii i, pentru cel puţin un set de date de
intrare, se ajunge să se execute instrucţiunea j,
înseamnă că există un lanţ de la i la j. De
asemenea, dacă după ce se execută instrucţiunea 6
i, se ajunge să se execute din nou instrucţiunea i,
atunci programul conţine structuri repetitive, iar
graful asociat conţine circuite. 7
Aşa cum am definit pentru grafuri neorientate
matricea lanţurilor, similar, se poate forma pentru Figura 8.8.
grafuri orientate matricea drumurilor:

1, dacă există drum de la i la j


D(i, j) = 
0, în caz contrar
Manual de informatică pentru clasa a XI-a 241

Matricea drumurilor nu este, în cazul general, simetrică. Pentru a o determina,


pentru fiecare nod i, parcurgem graful şi aflăm toate nodurile pentru care există drum
de la i la j. Pentru toate nodurile atinse (mai puţin i), vom avea L(i,j)=1. Astfel,
se completează linia i.

 Exerciţiu! Scrieţi programul care, pornind de la un graf, afişează matricea


drumurilor.

8.5. Graf complet şi graf turneu

Definiţia 8.7. Un graf orientat este complet dacă oricare două vârfuri, i şi
j (i≠j), sunt adiacente.

n ( n −1)
2
Lema 8.1. Avem 3 grafuri complete cu n noduri.

 Demonstraţia se face prin inducţie. Dacă n=1 avem 1 graf complet. Dacă n=2,
cele două noduri sunt adicente dacă avem arcul (1,2), sau avem arcul (2,1) sau
avem ambele arce (1,2) şi (2,1). În total, avem 3 grafuri orientate complete,
valoare care rezultă şi din formulă, dacă îl înlocuim pe n cu 1.
Presupunând formula adevărată, există
n ( n −1)
2
3
grafuri complete cu n vârfuri. Trebuie să demonstrăm că avem
n ( n +1)
2
3
grafuri complete cu n+1 vârfuri. Pentru fiecare graf complet cu n vârfuri adăugăm
vârful n+1. Acest nod este adiacent cu fiecare dintre celelalte n vârfuri în exact 3
moduri. Astfel, pentru fiecare graf complet cu n vârfuri, avem

3×
3 ×3 = 3 n
...
de n ori

grafuri complete cu n+1 vârfuri. Atunci, numărul de grafuri complete cu n+1 vârfuri
este
n ( n −1) n ( n −1) n ( n +1)
+n
3 2
× 3n = 3 2
=3 2
.

Definiţia 8.8. Un graf orientat este turneu, dacă oricare ar fi două vârfuri i
şi j, i≠j, între ele există un singur arc: arcul (i,j) sau arcul (j,i).
242 Capitolul 8. Grafuri orientate

Proprietăţi
1. Orice graf turneu este graf complet.
n ( n −1)
2. Avem 2 grafuri turneu cu n noduri. Ca şi în cazul numărului de grafuri
2

complete, demonstraţia se poate face prin inducţie completă. Exerciţiu!


3. În orice graf turneu există un drum elementar care trece prin toate vârfurile grafului.

 Demonstraţie. Fie un drum care trece prin k<n vârfuri distincte, [v1v2...vk].
Fie v un vârf prin care nu trece acest drum.
a. Dacă există arcul (v,v1) atunci găsim drumul vv1v2...vk, drum care trece
prin k+1 noduri. Vom presupune că există arcul (v1,v).
b. Dacă există arcul (vk,v) atunci găsim drumul v1v2...vkv, drum care trece
prin k+1 noduri. Vom presupune că există muchia (v,vk).

Astfel, am ajuns în situaţia reprezentată grafic mai jos:

v1 v2 v3 vk-1 vk

v
Figura 8.9.

Dacă există arcul (vk-1,v), atunci v1v2...vk-1vvk este un drum care îndeplineşte
condiţiile şi trece prin k+1 noduri. Vom presupune atunci că există arcul (v,vk-1).
Dacă există arcul (vk-2,v), atunci v1v2...vk-2vvk-1vk este un drum care
îndeplineşte condiţiile şi trece prin k+1 noduri. Vom presupune că există arcul (v,vk-2).
...
Dacă există arcul (v2,v), atunci v1v2vv3...vk-1vvk este un drum care îndeplineşte
condiţiile şi trece prin k+1 noduri. Vom presupune că există arcul (v,v2).
Atunci v1vv2...vk este un drum care îndeplineşte condiţiile şi trece prin k+1 noduri.

În concluzie, am obţinut un drum care trece prin k+1 noduri distincte.

Raţionamentul se repetă până când se obţine dumul care trece prin n noduri
distincte.
La o analiză mai atentă, proprietatea 3 este surprinzătoare. De exemplu, să
presupunem că la un concurs de tenis participă n jucători şi oricare 2 jucători joacă
exact o partidă. Cum remiza este exclusă, înseamnă că acestui concurs i se poate
asocia un graf orientat, în care vârfurile sunt jucătorii iar arcul (vi,vj) are
semnificaţia că jucătorul vi a învins jucătorul vj. Existenţa lanţului elementar care
Manual de informatică pentru clasa a XI-a 243

trece prin toate vârfurile ne asigură că putem aranja cei n jucători într-un şir astfel
încât jucătorul v1 l-a învins pe v2, jucătorul v2 l-a învins pe jucătrorul v3, ..., jucătorul
vn-1 l-a învins pe jucătorul vn.
Ce credeţi, aceasta înseamnă ca jucătorul v1 a câştigat concursul?

 Problema 8.1. Fiind dat un graf turneu, se să cere se afişeze un drum elementar
care trece prin toate vârfurile grafului.

 Indicaţie: aveţi un excelent exemplu prin care puteţi scrie un program bazat pe
un algoritm inspirat din demonstraţia unei teoreme (proprietăţi). Aţi mai întâlnit un
astfel de exemplu, atunci când s-a scris un program care găseşte un ciclu eulerian.
Întrebare suplimentară: care este complexitatea algoritmului?

8.6. Graf tare conex. Componente tare conexe

Reluăm exemplul 1 din paragraful 8.1. Să presupunem că grupul celor n


persoane efectuează o excursie la munte şi, din păcate, s-au rătăcit. Întrebarea este:
există posibilitatea ca oricare membru al grupului, să propună un loc de întâlnire şi
să-şi anunţe telefonic prietenii din agendă, aceştia să-i sune pe alţii ş.a.m.d., astfel
încât toţi membrii grupului să afle de acest loc?
Judecând după graful orientat asociat, ar trebui ca de la oricare membru al
grupului să existe un drum către oricare alt membru al grupului. Aceasta înseamnă că
oricare ar fi nodurile i şi j, există un drum de la i la j şi există şi un drum de la j la
i. Un graf cu această proprietate se numeşte graf tare conex.

Definiţia 8.9. Graful orientat G=(V,A) este tare conex dacă ∀x,y∈V, ∃
drum de la x la y şi drum de la y la x.

Definiţia 8.10. Subgraful G1=(V1,A1) al grafului G=(V,A) reprezintă o


componentă tare conexă dacă:
1. ∀x, y∈V1, ∃ drum de la x la y şi drum de la y la x.
2. Nu există un alt subgraf al lui G, G2=(V2,A2) cu V1⊂V2 care
îndeplineşte condiţia 1.

Graful alăturat are patru


5 componente tare conexe:
1 4 - subgraful care conţine
vârfurile: 1 2 3
- subgraful care conţine
7 vârfurile: 5 7
2 3
6 - subgraful care conţine vârful 4
- subgraful care conţine vârful 6
Figura 8.10.
244 Capitolul 8. Grafuri orientate

 Problema 8.2. Fie un graf orientat G=(V,A), memorat prin intermediul matricei de
adiacenţă. Se cere să se determine vârfurile fiecărei componente tare conexă.

 Rezolvare
a) Vom numi succesori ai vârfului i, toate nodurile j, pentru care există drum de la
i la j, la care se adaugă i. De exemplu, pentru graful dat, succesorii vârfului 1 sunt
vârfurile 1, 2, 3 şi 4. Pentru a determina toţi succesorii vârfului i, vom efectua o
parcurgere DF a grafului pornind de la acest vârf. Succesorii nodului i vor fi reţinuţi în
vectorul suc.
b) Fie i un vârf al grafului. Vom numi predecesori ai vârfului i, toate vârfurile j,
pentru care există drum de la j la i, la care se adaugă i. Pentru graful dat,
predecesorii vârfului 1 sunt: 1, 2 şi 3.

c) Dacă un vârf este simultan succesor şi predecesor al lui i, atunci el va face parte
din componenta tare conexă a vârfului i. Mulţimea nodurilor cu această proprietate va
fi o componentă tare conexă a grafului. De ce? Pentru că între două vârfuri k şi l,
există atât drum de la k la l (de la k la i şi de la i la l) cât şi drum de la l la k (de la
l la i şi de la i la k). Mulţimea nodurilor cu această proprietate este maximală în
raport cu relaţia de incluziune. Dacă, prin absurd, ar mai exista un vârf cu această
proprietate, care nu aparţine acestei mulţimi, atunci ar trebui să existe drum de la i la
el, şi de la el la i, caz în care acesta ar fi fost găsit prin procedeul dat.

d) De acum, putem redacta algoritmul. Variabila nrc, cu valoarea iniţială 1, va reţine


numărul curent al componentei tare conexe care urmează să fie identificată. Fiecare
componentă a vectorilor suc şi pred reţine, iniţial, valoarea 0.

pentru fiecare vârf i


dacă suc[i]=0
toţi succesorii lui i, inclusiv i, vor reţine nrc;
toţi predecesorii lui i, inclusiv i, vor reţine nrc;
toate componentele i, pentru care suc[i]≠pred[i] vor reţine 0;
se incrementează nrc.

se afişează vârfurile fiecărei componente conexe.

Mai jos, puteţi observa evoluţia vectorilor suc şi pred:

1 2 3 4 5 6 7 1 2 3 4 5 6 7

suc 1 1 1 1 0 0 0 suc 1 1 1 0 0 0 0
pred 1 1 1 0 0 0 0 pred 1 1 1 0 0 0 0
Manual de informatică pentru clasa a XI-a 245

1 2 3 4 5 6 7 1 2 3 4 5 6 7

suc 1 1 1 2 0 0 0 suc 1 1 1 2 0 0 0
pred 1 1 1 2 2 2 0 pred 1 1 1 2 0 0 0

1 2 3 4 5 6 7 1 2 3 4 5 6 7

suc 1 1 1 2 3 0 3 suc 1 1 1 2 3 4 3
pred 1 1 1 2 3 0 3 pred 1 1 1 2 3 4 3

Programul este prezentat în continuare:

Varianta Pascal Varianta C++


uses grafuri; #include "grafuri.cpp"
var suc,pred:array[1..50] of int suc[50], pred[50],
byte; A[50][50], n,nrc,i,j;
A:mat_ad; void df_r1(int nod)
n,nrc,i,j:integer; { int k;
procedure df_r1(nod:byte); suc[nod]=nrc;
var k:byte; for (k=1;k<=n;k++)
begin if (A[nod][k]==1 &&
suc[nod]:=nrc; suc[k]==0)
for k:=1 to n do df_r1(k);
if (A[nod,k]=1) and (suc[k]=0) }
then df_r1(k);
end; void df_r2(int nod)
{ int k;
procedure df_r2(nod:byte); pred[nod]=nrc;
var k:byte; for (k=1;k<=n;k++)
begin if (A[k][nod]==1 &&
pred[nod]:=nrc; pred[k]==0)
for k:=1 to n do df_r2(k);
if (A[k,nod]=1) and (pred[k]=0) }
then df_r2(k);
end; main()
begin { CitireO("Graf.txt",A,n);
CitireO('Graf.txt',A,n); nrc=1;
nrc:=1; for (i=1;i<=n;i++)
for i:=1 to n do if (suc[i]==0)
if suc[i]=0 { suc[i]=nrc;
then df_r1(i); df_r2(i);
begin for (j=1;j<=n;j++)
suc[i]:=nrc; if(suc[j]!=pred[j])
df_r1(i); suc[j]=pred[j]=0;
df_r2(i); nrc++;
for j:=1 to n do }
if suc[j]<>pred[j] then for (i=1;i<nrc;i++)
begin {cout<<"Componenta"<<i<<endl;
suc[j]:=0; for (j=1;j<=n;j++)
pred[j]:=0; if (suc[j]==i) cout<<j<<" ";
end; cout<<endl;
nrc:=nrc+1 }
end; }
246 Capitolul 8. Grafuri orientate

for i:=1 to nrc-1 do


begin
writeln ('Componenta ',i);
for j:=1 to n do
if suc[j]=i
then write (j,' ');
writeln;
end;
end.

Şi în cazul grafurilor orientate se păstrează noţiunea de graf conex şi noţiunea


de componentă conexă.

Definiţia 8.11. Graful orientat G=(V,A) este conex dacă ∀x,y∈V,


∃ lanţ de la x la y şi lanţ de la y la x.

Definiţia 8.12. Subgraful G1=(V1,A1) al grafului G=(V,A) este o


componentă conexă dacă:
1. ∀ x, y ∈V1, ∃ lanţ de la x la y.
2. Nu există un alt subgraf al lui G, G2=(V2,A2) cu V1⊂V2 care
îndeplineşte condiţia 1.

8.7. Drumuri de cost minim

8.7.1. Introducere

Să presupunem că avem n oraşe. Unele dintre ele sunt unite prin şosele.
Există posibilitatea ca pe unele şosele să se poată circula într-un singur
sens. Pentru fiecare şosea care uneşte oraşele i şi j, se cunoaşte
numărul de kilometri între i şi j.

Se pot pune următoarele întrebări:


1. Un turist se află într-un oraş, s. Se cere să se afle traseul de lungime
minimă dintre oraşele s şi f.
2. Care sunt traseele de lungime minimă între s şi toate celelalte oraşe?
3. Se cer traseele de lungime minimă între oricare două oraşe.

Să observăm că oraşele reprezintă vârfurile unui graf orientat. În acest graf, o


şosea poate fi reprezentată printr-un singur arc, dacă se poate circula pe ea într-un
singur sens sau prin două arce, dacă pe ea se poate circula în ambele sensuri.
În toate aceste cazuri, se cere lungimea unui drum pentru care suma costurilor
arcelor este minimă. Un astfel de drum va fi numit şi drum de cost minim sau, când
nu există posibilitatea unor confuzii, drum optim.
Manual de informatică pentru clasa a XI-a 247

1. La prima întrebare avem sursa unică, destinaţia unică.


2. La a doua întrebare avem sursa unică, destinaţia multiplă.
3. La a treia întrebare avem sursa multiplă, destinaţia multiplă.

Noi vom studia doi algoritmi care rezolvă aceste probleme. Primul, algoritmul
Roy-Floyd, rezolvă cazul 3 (sursa multiplă, destinaţia multiplă). Al doilea
algoritm, cel al lui Dijkstra, rezolvă cazul 2 (sursa unică, destinaţia multiplă).
Evident, din rezultatele furnizate de oricare din cei doi algoritmi, se poate obţine
răspunsul la prima întrebare. De asemenea, dacă aplicăm algoritmul lui Dijkstra de
n ori, se poate rezolva şi cazul 3 (sursa multiplă, destinaţia multiplă).

Observaţii

 Pentru ambii algoritmi, vom considera că între oricare două noduri distincte, i
şi j, există un arc de la i la j şi un arc de la j la i. Dacă arcul nu există, vom
memora pentru el o valoare foarte mare, drept cost, pe care o vom presupune
ca fiind +∞.
 În anumite condiţii, algoritmul Roy-Floyd, uşor modificat, poate furniza şi
drumuri de cost maxim. În acest din urmă caz, când nu există nod pentru un
arc, vom considera că el are costul -∞.
 Întrucât grafurile neorientate sunt cazuri particulare de grafuri orientate, putem
aplica cei doi algoritmi şi în cazul lor.

8.7.2. Algoritmul Roy-Floyd

Fiind dat un graf orientat G=(V,A), memorat prin matricea ponderilor în


forma 1, se cere să se determine, pentru orice x,y∈X, lungimea minimă a drumului
de la nodul x la nodul y. Prin lungimea unui drum înţelegem suma costurilor arcelor
care-l alcătuiesc.

1. Iniţial, matricea ponderilor reţine numai lungimea drumurilor directe între două
noduri, adică nu este permis ca un drum între două noduri să treacă printr-un
alt nod. Pentru arce inexistente se reţine, aşa cum s-a precizat, +∞, (în
program, o valoare foarte mare).
2. La început, încercăm să obţinem drumuri mai scurte, între oricare două
vârfuri i,j∈V, permiţând ca acestea să poată trece prin nodul 1. Aceasta
înseamnă că pentru ∀i,j∈V se face comparaţia: A(i,j)>A(i,1)+A(1,j),
adică se compară dacă lungimea drumului direct (care nu trece prin alte noduri)
este mai mare decât cea a drumului care trece prin nodul 1. În caz afirmativ se
face atribuirea A(i,j)←A(i,1)+A(1,j). După acest pas, matricea va reţine
lungimea optimă a drumurilor între oricare două noduri, drumuri care pot trece
prin nodul 1.
248 Capitolul 8. Grafuri orientate

3. Încercăm să obţinem drumuri mai scurte, între oricare două noduri i,j∈V,
permiţând ca acestea să poată trece şi prin nodul intermediar 2. Aceasta
înseamnă că pentru ∀i,j∈V, se face comparaţia: A(i,j)>A(i,2)+A(2,i),
adică se compară dacă lungimea drumului de la i la j, drum care nu poate
trece decât cel mult prin nodul 1 este mai mare decât cea a drumului care trece
prin nodul 2. În caz afirmativ, se face atribuirea A(i,j)←A(i,2)+A(2,i).
După acest pas, matricea va reţine lungimea optimă a drumurilor între oricare
două noduri, drumuri care pot trece prin nodurile intermediare 1 2.
...

Algoritmul continuă în acest mod, prin eventuale îmbunătăţiri succesive ale lungimii
drumurilor, considerând ca noduri intermediare 3, 4, ..., n.

Fie graful de mai jos:

2
7 0 1 9 ∞ 3
1  
∞ 7 3 ∞
4
0
1 9 ∞ ∞ 0 ∞ ∞
 
3

3 1 ∞ 2 0 ∞
∞ 4 ∞ 2 0 
1 3
2 
5 2
4
Figura 8.11.

Pasul 1. Se caută drumurile optime între oricare două noduri, 0 1 9 ∞ 3


unde drumurile pot trece numai prin nodul intermediar 1.  
∞ 0 7 3 ∞
A(4,2)=∞>A(4,1)+A(1,2)=1+1=2. A(4,2)=2. ∞ ∞ 0 ∞ ∞
 
A(4,5)=∞>A(4,1)+A(1,5)=1+3=4. A(4,5)=4. 1 2 2 0 4
∞
 4 ∞ 2 0 

Pasul 2. Se caută drumurile optime între oricare două noduri, 0 1 8 4 3


unde drumurile pot trece şi prin nodul intermediar 2.  
∞ 0 7 3 ∞
A(1,3)=9>A(1,2)+A(2,3)=1+7=8. A(1,3)=8. ∞ ∞ 0 ∞ ∞
 
A(1,4)=∞>A(1,2)+A(2,4)=1+3=4. A(1,4)=4. 1 2 2 0 4
A(5,3)=∞>A(5,2)+A(2,3)=4+7=11. A(5,3)=11. ∞
 4 11 2 0 
Manual de informatică pentru clasa a XI-a 249

Pasul 3. Se caută drumurile optime între oricare două noduri, unde drumurile pot
trece şi prin nodul intermediar 3. Nu se obţine nici o îmbunătăţire.

Pasul 4. Se caută drumurile optime între oricare două noduri, unde drumurile pot
trece şi prin nodul intermediar 4.
A(1,3)=8>A(1,4)+A(4,3)=4+2=6. A(1,3)=6. 0 1 6 4 3
 
A(2,1)=∞>A(2,4)+A(4,1)=3+1=4. A(2,1)=4. 4 0 5 3 7
A(2,3)=7>A(2,4)+A(4,3)=3+2=5. A(2,3)=5. ∞ ∞ 0 ∞ ∞
 
A(2,5)=∞>A(2,4)+A(4,5)=3+4=7. A(2,5)=7. 1 2 2 0 4
3 4 4 2 0
A(5,1)=∞>A(5,4)+A(4,1)=2+1=3. A(5,1)=3.  
A(5,3)=11>A(5,4)+A(4,3)=2+2=4. A(5,3)=4.

Pasul 5. Se caută drumurile optime între oricare două noduri, unde drumurile pot
trece şi prin nodul intermediar 5. La acest pas nu se obţin îmbunătăţiri. În concluzie,
am obţinut matricea de mai sus, a drumurilor minime.

Acest algoritm conduce într-adevăr la soluţia optimă? Se poate demonstra


prin inducţie completă.

1) Dacă n=0, matricea ponderilor reţine costul arcelor, dacă ele există, altfel reţine
+∞. Prin urmare, matricea ponderilor reţine costul drumurilor optime în cazul în care
nu este permis ca acest drum să conţină decât pe un singur arc (i,j).
2) Presupunem proprietatea adevărată pentru pasul k. Aceasta înseamnă că
matricea A reţine pentru oricare două vârfuri, i şi j, costul drumului optim între ele,
drum care trece doar prin vârfurile 1, 2, ..., k. Trebuie să arătăm că după pasul k+1,
matricea A va reţine costul drumurilor optime care trec prin primele k+1 vârfuri. Mai
întâi, să observăm că, la pasul k+1, nu se modifică A(k+1,i) şi A(j,k+1),
i,j∈{1,2, ..., n} pentru că nu se obţine îmbunătăţirea costului drumurilor [k+1...i]
sau [j...k+1] dacă se permite ca drumul să treacă şi prin nodul k+1. De altfel,
aceasta permite algoritmului să utilizeze o singură matrice, A, care la pasul 0, este
matricea ponderilor. La pasul k+1, determinăm dacă drumul optim de la i la j,
i,j∈{1,2, ..., n}, trece sau nu prin vârful k+1.
 Dacă trece, conform principiilor programării dinamice, drumurile de la i la
k+1 şi de la k+1 la j, sunt optime, iar suma costurilor lor este egală cu costul
drumului optim de la i la j. Prin urmare, A(i,j) obţinut la pasul anterior, care
reţine costul drumului optim care trece numai prin vârfurile 1, 2, k va reţine, o
valoare mai mare decât A(i,k+1)+A(k+1,j). În acest caz, se efectuează
atribuirea: A(i,j)←A(i,k+1)+A(k+1,j).
 Dacă drumul optim de la i la j, nu trece prin nodul k+1, atunci valoarea
reţinută de A(i,j) este mai mică decât A(i,k+1)+A(k+1,j). Această
ultimă sumă exprimă, în acest caz, costul unui drum de la i la j, drum care
"ocoleşte" prin nodul k+1. În acest caz, valoarea lui A(i,j), obţinută la pasul
anterior, rămâne nemodificată.
250 Capitolul 8. Grafuri orientate

Evident, după n paşi, A va reţine costurile drumurilor optime de la i la j,


i,j∈{1, 2, ..., n}, drumuri care pot trece prin n noduri, adică, va reţine drumurile
optime pe ansamblul grafului orientat.

Algoritmul are complexitatea O(n3).

Până în prezent, am aflat costul drumurilor optime între oricare două vârfuri.
Dar ce valoare are acest rezultat dacă nu cunoaştem şi pe unde trece un astfel de
drum. Astfel, se pune următoarea problemă: fiind dată matricea drumurilor optime şi
fiind date două vârfuri i şi j, se cere să se reconstituie nodurile prin care trece unul
din drumurile optime între i şi j (pot exista mai multe de aceeaşi lungime, minimă).

1. Dacă drumul optim între i şi j trece prin nodul intermediar k, atunci:


A(i,j)=A(i,k)+A(k,j).
2. Dacă există vârful k astfel încât A(i,j)=A(i,k)+A(k,j), atunci k se
găseşte pe unul din drumurile optime de la i la j.

În concluzie, vom aplica strategia generală DIVIDE ET IMPERA pentru depistarea


nodurilor pe unde trece drumul optim între i şi j (vezi subprogramul drum). Iată
programul:

Varianta Pascal Varianta C++


uses grafuri,wincrt; #include "grafuri.cpp"
float A[50][50];int n;
var A:mat_c;
n:integer; void Drum(int i,int j)
{ int k=1,gasit=0;
procedure Drum(i,j:integer);
while ( (k<=n) && !gasit)
var k:integer;
{ if ( i!=k && j!=k &&
gasit:boolean;
A[i][j]==A[i][k]+A[k][j])
begin { Drum(i,k); Drum(k,j);
k:=1; gasit=1; }
gasit:=false; k++;
while (k<=n)and not gasit do }
begin if (!gasit) cout<<j<<" ";
if (i<>k) and (j<>k) and }
(A[i,j]=A[i,k]+A[k,j]) void Scriu_drum(int
then Nod_Initial, int Nod_Final)
begin {
Drum(i,k); if (A[Nod_Initial][Nod_Final]
Drum(k,j); <PInfinit)
gasit:=true; {cout<<"Drumul de la "
end; <<Nod_Initial<<" la "
k:=k+1; <<Nod_Final<<" are lungimea "
end; <<A[Nod_Initial][Nod_Final]
if not gasit then write(j,' ') <<endl;
end; cout<<Nod_Initial<<" ";
Drum(Nod_Initial,Nod_Final);
}
Manual de informatică pentru clasa a XI-a 251

procedure scriu_drum else


(Nod_Initial, Nod_Final:byte); cout<<"Nu exista drum de la "
begin <<Nod_Initial<<" la "
if A[Nod_Initial,Nod_Final] <<Nod_Final;
<Pinfinit }
then
begin void Lungime_Drumuri()
writeln(' Drumul de la ', {int i,j,k;
Nod_Initial,' la ', for (k=1;k<=n;k++)
Nod_final,' are lungimea ', for (i=1;i<=n;i++)
A[Nod_Initial,Nod_final]:3:0); for (j=1;j<=n;j++)
write(Nod_initial,' '); if (A[i][j]>A[i][k]+A[k][j])
drum(Nod_initial, Nod_final); A[i][j]=A[i][k]+A[k][j];
end }
else
write('Nu exista drum de la ', main()
Nod_Initial,' la ',Nod_Final); { Citire_cost("Graf.txt",A,n);
end; Lungime_Drumuri();
procedure Lungime_Drumuri; Scriu_drum(5,3);
var i,j,k:integer; }
begin
for k:=1 to n do
for i:=1 to n do
for j:=1 to n do
if A[i,j]>A[i,k]+A[k,j]
then A[i,j]:=A[i,k]+A[k,j];
end;
begin
Citire_cost('Graf.txt',A,n);
Lungime_Drumuri;
scriu_drum(5,3);
end.

8.7.3. Utilizarea algoritmului Roy-Floyd pentru


determinarea drumurilor de cost maxim
Problema pe care o punem este următoarea: se poate adapta algoritmul
Roy-Floyd pentru a găsi drumuri de lungime maximă?

Priviţi graful următor. Care este lungimea drumului maxim de la nodul 1 la


nodul 4? După cum vedeţi, graful conţine un circuit. Prin urmare, printr-un drum de
forma: 1 2 3 2 3 ... 2 3 ... 4, se obţine un cost oricât de mare. Acesta este,
de fapt, +∞.
4

3 3 1 4
1 2

Figura 8.12. 2
252 Capitolul 8. Grafuri orientate

1) Pentru ca algoritmul să găsească soluţia corectă este necesar ca graful să nu


conţină circuite.
2) Dacă se utilizează matricea ponderilor în forma 1, atunci când între două noduri
nu există un arc care le uneşte, în matrice se reţine +∞. Cum drumul care se caută
este cel de lungime maximă, algoritmul va alege, de fapt, acel pseudoarc, deci
rezultatul va fi eronat. Pentru ca algoritmul să găsească soluţia corectă este necesar
să utilizăm matricea ponderilor în forma 2, citită cu subprogramul Citire_cost1.
3) Pentru ca algoritmul să găsească soluţia corectă, adică să se orienteze către
drumul de cost maxim, este necesar ca testul de comparare a lungimilor drumurilor să
fie invers:
if A(i,j)<A(i,k]+A(k,j)...

8.7.4. Algoritmul lui Dijkstra

Fiind dat un graf G=(V,A) memorat prin matricea ponderilor în forma 1, se


cere să se determine pentru r∈V fixat, lungimea minimă a drumului de la r la
oricare y∈V.

⇒ Algoritmul selectează nodurile grafului, unul câte unul, în ordinea crescătoare a


costului drumului de la nodul r la ele, într-o mulţime S, care iniţial conţine
numai nodul r. În felul acesta, algoritmul se încadrează în strategia generală
GREEDY.

⇒ În procesul de prelucrare, se folosesc 3 vectori: D, S şi T.

 Vectorul D este vectorul lungimii drumurilor de la vârful r la toate celelalte


vârfuri ale grafului. Prin D[i], unde i∈{1,..., n}, se înţelege costul drumului
găsit la un moment dat, între vârful r şi vârful i.
 Vectorul T indică drumurile găsite între nodul R şi celelalte noduri ale
grafului. Pentru aceasta se utilizează o memorare specială a drumurilor,
numită legătura de tip “tata”, în care pentru nodul i se reţine nodul
precedent pe unde trece drumul de la r la i. Pentru r se memorează 0.
 Vectorul S, numit şi vector caracteristic, indică mulţimea S a nodurilor
selectate: S[i]=0 dacă nodul i este neselectat şi S[i]=1 dacă nodul i este
selectat.

Prezentarea algoritmului
Pasul 1. Vârful r este adăugat mulţimii S iniţial vidă (S[r]=1);
- costurile drumurilor de la r la fiecare nod al grafului se preiau în vectorul D de
pe linia r a matricei A;
- pentru toate nodurile i, având un cost al drumului de la r la ele, finit, se pune
T(i)=r.
Manual de informatică pentru clasa a XI-a 253

Pasul 2. Se execută de n-1 ori secvenţa:

- printre nodurile neselectate se caută cel aflat la distanţa minimă faţă de r şi se


selectează, adăugându-l mulţimii S. Fie poz acest vârf.
- pentru nodurile neselectate, j, se actualizează în D costul drumurilor de la r la
ele, utilizând ca nod intermediar nodul selectat, poz, procedând în felul
următor:
 se compară costul existent în vectorul D, pentru j, D(j), cu suma dintre
costul existent în D pentru nodul selectat, poz, şi distanţa de la nodul
selectat, poz, la nodul pentru care se face actualizarea distanţei, j:
D(poz)+A(poz,j). În cazul în care suma este mai mică, elementul din D
corespunzător nodului pentru care se face actualizarea, j, reţine suma
(D(j)←D(poz)+A(poz,j) şi elementul din T corespunzător aceluiaşi vârf,
iar valoarea vârfului selectat: T(j)←poz (drumul trece prin acest vârf).

Pasul 3. Pentru fiecare vârf al grafului, cu excepţia lui r, se trasează drumul de


la R la el.

Fie graful de mai jos pentru care se doreşte aflarea drumurilor minime
de la nodul 1 la toate celelalte:

2
7
1
0 1 9 ∞ 3
4
 
1 9 ∞ 0 7 3 ∞
3
∞ ∞ 0 ∞ ∞
3  
1 3  1 ∞ 2 0 ∞
2
∞ 4 ∞ 2 0 
 
5 2
4 Figura 8.13.

Valorile iniţiale ale lui S, D, T sunt: D 0 1 9 ∞ 3

S 1 0 0 0 0

T 0 1 1 0 1

Cel mai apropiat nod de 1 este D 0 1 8 4 3


nodul 2. Avem:
S 1 1 0 0 0
D[3]=9>D[2]+A[2][3]=1+7=8
⇒ D[3]=8, T[3]=2; T 0 1 2 2 1
254 Capitolul 8. Grafuri orientate

D[4]= ∞>D[2]+A[2][4]=1+3=4 ⇒ D[4]=4, T[4]=2;


D[5]= 3<D[2]+A[2][5]=1+∞=∞

Se selectează nodul 5, dar pentru D 0 1 8 4 3


acesta nu se obţine nici o
îmbunătăţire. S 1 1 0 0 1

T 0 1 2 2 1

Se selectează nodul 4. D 0 1 6 4 3

D[3]=8>D[4]+A[4][3]=4+2=6 S 1 1 0 1 1
⇒ D[3]=6, T[3]=4;
T 0 1 4 2 1

Se selectează nodul 3 pentru care nu se mai pot face îmbunătăţiri.

Cum interpretăm rezultatele? De exemplu, distanţa de la nodul 1 la nodul 3


este D[3]=6. T[3]=4, T[4]=2, T[2]=1, T[1]=0. Drumul este 1, 2, 4, 3.

 Pentru a determina lungimea drumului de la un nod la toate celelalte,


algoritmul are complexitatea O(n2).

 Pentru a determina lungimea drumului de la orice vârf la toate celelalte,


algoritmul are complexitatea O(n3), se procedează aşa cum am văzut
pentru fiecare vârf în parte, nu numai pentru nodul 1.

 Să demonstrăm corectitudinea algoritmului lui Dijkstra.

Observăm că algoritmul selectează, pe rând, vârfurile grafului, într-o mulţime


S (dacă un nod este selectat, avem S[i]=1, altfel avem S[i]=0). Astfel, în timpul
executării programului, mulţimea V, a vârfurilor este împărţită în două submulţimi:
S, mulţimea vârfurilor selectate, şi N=V-S, mulţimea vârfurilor neselectate. Iniţial,
la pasul 1, S={r}. La fiecare pas se adaugă în S un vârf care este extras din N. La
fiecare pas, D[v] reţine costul drumului de la r la v.

Fie v un nod al grafului. Vom demonstra, prin inducţie după n, că:

a) dacă v∈S, atunci D[v] reţine costul minim în raport cu toate


drumurile de la r la v.
b) dacă v∈N, atunci D[v] reţine costul minim în raport cu toate drumurile
de la r la v care, cu excepţia lui v, trec numai prin vârfuri din S.
Pentru n=1, a) şi b) se verifică imediat. Vom avea D[v]=0, iar în rest,
pentru fiecare vârf v, diferit de r, D[v] reţine costul arcului de la r la el.
Manual de informatică pentru clasa a XI-a 255

Să arătăm că P(k-1)⇒P(k) (adică dacă presupunem a) şi b) adevărate


pentru n=k-1, unde k>1, atunci ele sunt adevărate şi pentru n=k. La pasul k>1 se
alege un vârf i din N, pentru care D[i] este minimă, în raport cu toate nodurile din N.

Demonstrăm a). Presupunem, prin absurd, că ar exista un alt drum de la r la i,


care are un cost mai mic decât cel reţinut de D[i]. Fie j primul vârf din acest
drum care nu aparţine lui S. Avem, astfel, drumul de cost minim [r...j...i].
Aceasta înseamnă că drumul [r...j] este de cost minim, contrar, [r...j...i]
n-ar fi de cost minim. Dar, din ipoteza b) avem costul drumului [r...j] ≥ costul
drumului de [r...i] pentru că ambele drumuri, cu excepţia vârfului final, trec
numai prin noduri din S, iar dintre acestea cel de cost minim este [r...i].
Absurd! Prin urmare, costul drumului [r,i] este minim.

Demonstrăm b). După selecţia nodului i, acesta este extras din N şi introdus în S.
De asemenea, se actualizează costurile drumurilor de la r la vârfurile rămase din
N. Fie j∈N un astfel de nod. Conform ipotezei de inducţie b) la pasul anterior
respecta condiţia. Faţă de pasul anterior, S conţine, în plus, vârful i. Singura
posibilitate ca D[j] să reţină o valoare mai mică, ar fi ca drumul de la r la el să
treacă prin i. Dar acest caz a fost tratat, atunci când s-au actualizat costurile prin
vârful i.

Iată şi programul:

Varianta Pascal Varianta C++


uses grafuri; #include "grafuri.cpp"
var a:mat_c; float A[50][50],D[50],min;
S,T:array [1..50] of byte; int S[50],T[50],n,i,j,r,poz;
D:array [1..50] of Real; void drum(int i)
n,i,j,r,poz:integer; { if (T[i]) drum(T[i]);
min:real; cout<<i<<" ";
procedure drum (i:integer); }
begin main()
if t[i] <>0 then drum(t[i]); { Citire_cost("graf.txt",A,n);
write(i,' '); cout<<"r=";
end; cin>>r;
begin S[r]=1;
Citire_cost('Graf.txt',A,n); for (i=1;i<=n;i++)
writeln('Introduceti nodul de { D[i]=A[r][i];
pornire '); if (i!=r)
write('r='); if (D[i]<PInfinit) T[i]=r;
readln(r); }
s[r]:=1; for (i=1;i<=n-1;i++)
for i:=1 to n do { min=PInfinit;
begin for(j=1;j<=n;j++)
d[i]:=a[r,i]; if (S[j]==0)
if i<>r then if (D[j]<min)
if d[i]<PInfinit { min=D[j];
then t[i]:=r poz=j; }
end; S[poz]=1;
256 Capitolul 8. Grafuri orientate

for i:=1 to n-1 do for (j=1;j<=n;j++)


begin if (S[j]==0)
min:=Pinfinit; if (D[j]>D[poz]+A[poz][j])
for j:=1 to n do { D[j]=D[poz]+A[poz][j];
if S[j]=0 then T[j]=poz;
if D[j]<min }
then }
begin for (i=1;i<=n;i++)
min:=d[j]; cout<<D[i]<<" ";
poz:=j cout<<endl;
end; for (i=1;i<=n;i++)
S[poz]:=1; if (i!=r)
for j:=1 to n do if(T[i])
if S[j]=0 {cout<<"Dist de la"<<r <<
then " la "<<i<<" este "
if D[j]>D[poz]+A[poz,j] <<D[i]<<endl;
then drum(i);
begin cout<<endl;
D[j]:=D[poz]+A[poz,j]; }
T[j]:=poz; else
end cout<<"Nu exista drum de la "
end; <<r<<" la "<<i<<endl;
for i:=1 to n do }
if i<>r then
if T[i]<>0
then
begin
writeln('Dist de la ' ,r ,
' la ',i,' este ',d[i]:5:0);
drum(i);
writeln
end
else writeln('Nu exista drum
de la ',r,' la ',i)
end.

Probleme propuse
1. Într-un grup de n persoane, anumite persoane, împrumută alte persoane cu
diverse sume de bani! Modelând problema cu ajutorul grafurilor orientate, se cere să
stabiliţi corespondenţa dintre afirmaţiile din stânga şi cele din dreapta. Observaţie:
dacă persoana i împrumută cu bani pe persoana j, atunci există un arc de la i la j.

1. Persoana i nu a împrumutat cu a) gradul interior al nodului i este 0.


bani alte persoane din grup.
b) gradul exterior al nodului i este 0.
2. Persoana i nu a împrumutat bani
de la alte persoane din grup.
Manual de informatică pentru clasa a XI-a 257

2. Se dau n mulţimi de numere naturale: A1, A2...An. Acestor mulţimi li se asociază


un graf orientat astfel: dacă mulţimea Ai este inclusă în mulţimea Aj, în graful asociat
vom avea arcul (Ai,Aj). Nu vom considera cazul de incluziune a unei mulţimi în ea
însăşi. Stabiliţi corespondenţa dintre operaţiile din stânga şi cele din dreapta.

1. Ai⊂Aj⊂Ak a) De la A1 la An există un lanţ de


lungime n-1.
2. Ai⊂Aj; Ak⊂Aj
b) De la Ak la Ai există un lanţ de
3. A1⊂A2⊂... An-1⊂An
lungime 2.
4. A1=A2=... An-1=An
c) Graful este tare conex.
d) De la Ai la Ak există un drum de
lungime 2.

3. Refaceţi problema anterioară în cazul în care se consideră n numere naturale şi


relaţia de divizibilitate. De asemenea, încercaţi să adăugaţi noi situaţii în care se cere
corespondenţa.
4. Câte componente conexe şi câte componente tare conexe
1
conţine graful alăturat?
a) 1 1; b) 1 3; c) 1 0; d) 0 0.
3

2
Figura 8.14.

5. În graful din figura 8.15, care este lungimea celui mai


lung lanţ elementar şi care este lungimea celui mai lung 1
drum elementar?

a) 3 2; b) 2 2; c) 2 3; d) 1 2. 4
3

Figura 8.15. 2

Problemele de la 6. la 9. se referă la graful din figura 8.16.


6. Câte circuite conţine?
5
a) 3; b) 2; c) 1; d) 4. 1
7. Câte componente tare conexe conţine?
a) 4; b) 3; c) 2; d) 1. 4 3

8. Care este nodul cu grad interior maxim şi care


este nodul cu grad exterior minim? 2

a) 1 1; b) 1 2; c) 2 2; d) 2 5.
Figura 8.16.
258 Capitolul 8. Grafuri orientate

9. Care este numărul minim de arce care trebuie adăugate pentru ca graful să
devină tare conex?
a) 1; b) 2; c) 3; d) 4.

10. Se dă un graf orientat. Se cere să se afişeze, pentru fiecare vârf în parte, gradul
interior şi gradul exterior. Problema se va rezolva în cazul în care graful dat prin
matricea de adiacenţă şi în cazul în care el este dat prin liste de adiacenţe.
11. Fiind date un graf orientat şi o succesiune de vârfuri să se decidă dacă
succesiunea este drum, iar în caz afirmativ se va preciza dacă este sau nu un drum
elementar. Problema se va rezolva în cazul în care graful dat prin matricea de
adiacenţă şi în cazul în care el este dat prin liste de adiacenţe.
12. La fel ca mai sus, dar se cere să se determine dacă succesiunea respectivă
este sau nu lanţ (lanţ elementar).
13. Se dă un graf prin lista muchiilor. Programul va decide dacă graful este
neorientat.
14. Se dau listele de adiacenţe a unui graf orientat. Programul va afişa matricea de
adiacenţă.
15. Se dă matricea de adiacenţă a unui graf orientat. Programul va afişa listele de
adiacenţe ale acestuia.
16. Se dă matricea de adiacenţă a unui graf orientat. Se cere să se listeze toate
circuitele de lungime 3.

17. Se dă matricea de adiacenţă a unui graf orientat. Se cere să se listeze toate


ciclurile de lungime 3.

18. Într-un grup de n persoane, fiecare persoană declară dacă cunoaşte sau nu
celelalte persoane din grup. Scrieţi un program care decide care este persoana cea
mai cunoscută din grup.
19. La fel ca la problema anterioară, se cere să se determine dacă există o "vedetă"
a grupului. Prin "vedetă" vom înţelege o persoană care este cunoscută de toate
celelalte persoane, dar nu cunoaşte nici o persoană.
20. Se dă un graf orientat. Se cere să se afişeze, dacă există, un circuit care conţine
toate arcele grafului (circuit eulerian).
21. Se dă un graf orientat. Se cere să se afişeze, dacă există, un drum elementar
care trece prin toate vârfurile grafului (drum hamiltonian).
22. Se dă un graf orientat şi două noduri ale sale, u şi v. Se cere să se afişeze,
dacă există, un drum de la u la v.

23. La fel ca mai sus, numai că se cere ca drumul să aibă lungimea minimă (să
conţină un număr minim de arce).
Manual de informatică pentru clasa a XI-a 259

24. Se dă un graf orientat. Să se decidă dacă acesta conţine circuite.

25. Se dă un graf orientat. Să se decidă dacă este complet.

26. Se dă un graf orientat. Să se decidă dacă acesta este graf turneu.

27. Un graf orientat este bipartit dacă mulţimea vârfurilor sale poate fi împărţită în
două submulţimi disjuncte astfel încât orice arc este incident la un vârf din prima
mulţime şi la un vârf din a doua mulţime. Fiind dat un graf orientat, se cere să se scrie
un program care decide dacă graful dat este bipartit şi în caz afirmativ, afişează cele
două mulţimi de noduri.
28. Se dă un graf orientat. Se cere să se afişeze (prin mulţimile de muchii) toate
grafurile parţiale pe care le admite.
29. Se dă un graf orientat. Se cere să se afişeze (prin mulţimile de muchii) toate
subgrafurile pe care le admite.
30. Fie grafurile G1=(V1,A1) şi G2=(V2,A2). Să se scrie un program care să
verifice dacă G2 este subgraf pentru G1.

31. La fel ca la problema anterioară numai că se va verifica dacă G2 este graf


parţial al lui G1.

32. Fie graful orientat G=(V,A) dat prin matricea de adiacenţă. Fie A⊂V. Se cere
să se listeze mulţimea arcelor care au o extremitate într-un vârf din A şi altă
extremitate într-un vârf din V-A.

33. Fie graful orientat G=(V,A) dat prin liste de adiacenţe. Fie A⊂V. Se cere să se
listeze mulţimea arcelor care au o extremitate într-un nod din V-A şi altă extremitate
într-un nod din A.

34. Mai multe oraşe 1,2, ..., n sunt legate prin autostrăzi cu sens unic. Nu toate
oraşele sunt legate între ele prin legătură directă. Fiind dat oraşul în care se află un
turist cu maşina sa, k∈{1, 2, ..., n}, distanţele între oraşe (acolo unde ele sunt unite
prin autostrăzi) se cer următoarele (programe separate):
a) lista oraşelor în care turistul poate ajunge;

b) lista oraşelor în care turistul poate ajunge, dar se poate şi întoarce fără a trece prin
nici un oraş dintre oraşele întâlnite pe traseul de plecare;
c) drumul cu distanţa minimă între k şi celelalte oraşe;

d) care este oraşul cel mai apropiat de oraşul k?

e) care sunt oraşele care se pot vizita, pornind din oraşul k şi pentru care drumul
până la ele trece exact prin 3 oraşe (cu excepţia lui k şi a lor) şi care este lungimea
drumului de la k la ele?

f) care este oraşul (oraşele) aflat(e) în ordinea crescătoare a distanţelor de la k la ele


aflat pe poziţia a patra?
260 Capitolul 8. Grafuri orientate

g) care sunt drumurile minime între oricare perechi de oraşe, drum care nu poate
trece decât prin oraşele a, b, c?

35. Într-o fabrică există n secţii: o secţie de plecare, n-2 secţii intermediare şi o
secţie unde sosesc produsele finite. Fabricii i se poate asocia un graf orientat în
care nodurile sunt secţiile fabricii, iar muchiile sunt traseele pe care produsele
intermediare circulă prin secţii. O secţie nu poate furniza produsul intermediar în
care este specializată până când nu îi sosesc toate "ingredientele" de la secţiile de
care ea depinde. De asemenea, drumul între două secţii durează un anumit
interval de timp. Citindu-se N (numărul de secţii) şi o mulţime de tripleţi (i,j,k),
unde k este timpul necesar produselor pentru a ajunge din secţia i în secţia k, să
se tipărească timpul necesar pentru fabricarea produsului finit din momentul în
care secţia de plecare începe să funcţioneze.

Notă. Datele se introduc în mod corect (nu este necesară validarea lor).

Răspunsuri
1. 1-b, 2-a.
2. 1-d; 2-b; 3-a; 4-c;
4. b)
5. a)
6. c)
7. b)
8. c)
9. a)

34. a) parcurgere DF începând cu vârful k;


b) circuite care trec prin vârful k;
c) algoritmul lui Dijkstra;
d) arc de cost minim incident spre exterior la vârful k;
e) parcurgere BF;
f) cum selectează vârfurile algoritmul lui Dijkstra?
g) cum lucrează algoritmul Roy-Floyd?

35. Graful nu prezintă circuite. Dacă, prin absurd, o secţie ar trimite "ingredientele"
către o alta de la care a primit "ingrediente", ţinând cont de faptul că întreaga fabrică
începe să funcţioneze cu secţia 1, fabricaţia n-ar putea să aibă loc: secţia va aştepta
"ingredientele" care nu sosesc pentru că ea n-a trimis înapoi "ingredientele" necesare
la secţia de unde le aşteaptă. În aceste condiţii se poate aplica Roy-Floyd pentru a
afla drumul de cost maxim de la secţia 1 la secţia n.
261

Capitolul 9

Arbori

9.1. Noţiunea de arbore

Să presupunem că o firmă doreşte să conecteze la TV, prin cablu, cele n case


ale unui sat. Cum vor fi conectate casele la cablu? Logic, va trebui ca fiecare casă să
fie conectată. Apoi, la o casă va sosi cablul dintr-un singur loc şi, eventual, de la ea va
porni cablul către altă casă. Dacă analizăm situaţia prezentată prin prisma teoriei
grafurilor, vom avea un graf conex (fiecare casă trebuie conectată), iar graful nu va
conţine cicluri. Dacă ar conţine un ciclu, atunci, evident, putem renunţa la cablul care
uneşte două case care aparţin ciclului respectiv. Astfel, obţinem un graf cu proprie-
tăţile de mai sus, numit arbore.

Definiţia 9.1. Se numeşte arbore un graf 1 2


neorientat care este conex şi nu conţine
cicluri.

Alăturat, aveţi un exemplu de arbore cu 5 noduri. 3 4


Figura 9.1. 5
Exemplu de arbore

 Problema 9.1. Se citeşte un graf. Să se scrie un program care verifică dacă


acesta este arbore.

 Rezolvare. Conexitatea ştim să o verificăm. Dacă într-o parcurgere DF se


vizitează toate nodurile, atunci graful este conex. Dacă un graf are cicluri, este, din
nou, uşor de verificat, cu aceeaşi parcurgere DF. Să ne amintim că, în cazul în care
graful are cicluri, în timpul parcurgerii, va exista cel puţin o a doua tentativă de vizitare
a unui nod. Prin urmare, printr-o simplă parcurgere DF (sau BF) se poate stabili dacă
graful este conex sau nu.

Varianta Pascal Varianta C++


uses grafuri; #include "grafuri.cpp"
var n,i,suma:integer; int s[50],A[50][50],gasit,n,i,
s:array[1..50]of integer; suma;
A:mat_ad;
void df(int nod)
gasit:boolean;
{ int k;
s[nod]=1;
262 Capitolul 9. Arbori

procedure df(nod:integer); for(k=1;k<=n;k++)


var k:integer; if (A[nod][k]==1)
begin { A[k][nod]=0;
s[nod]:=1; if (s[k]==0) df(k);
for k:=1 to n do else gasit=1;
if A[nod,k]=1 then }
begin }
A[k,nod]:=0;
if s[k]=0 then df(k) main()
else gasit:=true; {
CitireN("Graf.txt",A,n);
end
df(1);
end;
suma=0;
begin for (i=1;i<=n;i++) suma+=s[i];
CitireN('Graf.txt',A,n); if (suma!=n)
df(1); cout<<"Nu este conex"'
suma:=0; else
for i:=1 to n do if (gasit)
suma:=suma+s[i]; cout<<"Are ciclu "'
if suma<>n else cout<<"este arbore ";
then }
writeln('Nu este conex')
else
if gasit
then writeln('Are ciclu')
else writeln('Arbore');
end.

Teorema 9.1. Fie G un graf neorientat cu n noduri. G este arbore dacă şi


numai dacă are n-1 muchii şi nu conţine cicluri.

 Demonstraţie
⇒ Fie G un arbore (graf neorientat, conex şi fără cicluri). Trebuie să demonstrăm
că are n-1 muchii. Vom demonstra prin inducţie. Dacă n=1, numărul muchiilor
este 0 (se verifică, are n-1 muchii). Vom presupune proprietatea adevărată
pentru arbori cu n noduri (adică au n-1 muchii). Fie un arbore cu n+1 noduri.
Există cel puţin un nod terminal (nod care are o singură muchie incidentă).
Dacă nu ar exista un astfel de nod, să considerăm un lanţ care porneşte
dintr-un nod oarecare. La fiecare pas, vom selecta o muchie. Până la urmă,
pentru că mulţimea nodurilor este finită şi pentru că nu există nod terminal,
lanţul va trece de două ori printr-un acelaşi nod. Asta înseamnă că arborele ar
conţine cicluri (absurd, se contrazice definiţia). Eliminăm nodul terminal şi
muchia care îi este incidentă. Obţinem un arbore cu n noduri. Conform ipotezei
făcute, acesta va avea n-1 muchii. Înseamnă că arborele cu n+1 noduri va
avea n muchii (n-1+1).

⇐ Fie G un graf cu n-1 muchii, care nu conţine cicluri. Rămâne de dovedit că


G este conex. Vom demonstra prin reducere la absurd. Presupunem că G nu
este conex. Considerăm G1, G2, ..., Gp, componentele conexe ale grafului.
Manual de informatică pentru clasa a XI-a 263

Fiecare dintre ele îndeplineşte condiţiile:


a) este conexă (aşa a fost aleasă);
b) nu conţine cicluri (pentru că G nu conţine cicluri).

Rezultă că fiecare dintre ele este arbore. Fie mi numărul muchiilor şi ni


numărul nodurilor fiecărui arbore Gi. Avem mi=ni-1. Dar m1+m2+....mp=n-1.
Rezultă: n1-1+n2-1+.....np-1=n-1, deci n1+n2+.....np=n+p-1. Dar G
are n noduri. Rezultă: n=n+p-1, deci p=1. În concluzie, există o singură
componentă conexă, care nu conţine cicluri. Rezultă că G este arbore.

Propoziţia 9.1. Dacă G=(V,E) este un arbore cu n noduri şi m muchii, şi


dacă d1, d2, ..., dn sunt gradele celor n noduri, atunci avem relaţia:
d1+d2...dn=2(n-1).

 Demonstraţie
Să ne amintim un rezultat referitor la gradele nodurilor grafurilor neorientate cu
n noduri şi m muchii. Avem relaţia: d1+d2+...+dn=2m. Dar, pentru un arbore avem
egalitatea m=n-1.

9.2. Noţiunea de arbore parţial

Să presupunem că într-un judeţ se impune repararea tuturor şoselelor care


unesc diversele localităţi. Pentru aceasta s-a obţinut un credit extern care permite ca
toate şoselele să fie reparate. Desigur, se doreşte ca repararea şoselelor să se facă
cât mai repede, dar, în acelaşi timp, trebuie ca pe parcursul reparaţiilor să se poată
circula, astfel încât să nu rămână localităţi inaccesibile pentru traficul rutier. Se cere
un număr minim de şosele, care să nu intre în reparaţii în prima fază, astfel încât
condiţia de mai sus să poată fi respectată.
Dacă considerăm graful în care nodurile sunt localităţile, iar muchiile sunt
şoselele, va trebui să păstrăm un număr minim de muchii, astfel încât graful să
rămână conex. Care este acel număr minim de muchii care conservă conexitatea?
Evident, n-1. Cum graful rămâne conex, în urma eliminării anumitor muchii se obţine,
aşa cum rezultă din teorema dată în paragraful anterior, un arbore.

 Problema 9.2. Se dă un graf conex,


G=(V,E). Se cere un graf parţial al său, 1 1
care este arbore. O astfel de structură se
numeşte arbore parţial. Evident, există 2 3 2 3
posibilitatea ca dintr-un graf conex să se
poată obţine mai mulţi arbori parţiali.

În figura 9.2., puteţi observa un graf 4 5 4 5


conex (stânga) şi un arbore parţial al său
(dreapta).
Figura 9.2. Exemplu de arbore parţial
264 Capitolul 9. Arbori

 Rezolvare. Pentru a rezolva problema, vom folosi, din nou, o metodă de


parcurgere a unui graf, mai precis parcurgerea DF. Vom afişa numai muchiile
selectate de algoritm, adică cele care nu determină cicluri. Programul următor citeşte
datele referitoare la un graf conex şi afişează muchiile unui arbore parţial:

Varianta Pascal Varianta C++


uses grafuri,wincrt; #include "grafuri.cpp"
var n:integer; int s[50],A[50][50],n;
s:array[1..50]of integer;
A:mat_ad; void df_r(int nod)
procedure df_r(nod:integer); {int k;
s[nod]=1;
var k:integer;
for (k=1;k<=n;k++)
begin
if (A[nod][k]==1 && s[k]==0)
s[nod]:=1;
{ cout<<nod<<" "<<k<<endl;
for k:=1 to n do
df_r(k);
if (A[nod,k]=1) and (s[k]=0)
}
then begin
}
writeln(nod,' ',k);
df_r(k); main()
end; { CitireN("Graf.txt",A,n);
end; df_r(1);
begin }
CitireN('Graf.txt',A,n);
df_r(1);
end.

9.3. Mai mult despre cicluri1


Până în prezent am învăţat să determinăm, printr-o simplă parcurgere DF, dacă
un graf conţine sau nu cicluri. Acum, după prezentarea noţiunii de arbore, putem să
continuăm studiul nostru privitor la cicluri.
Fie G=(V,E) un graf neorientat conex. Dacă G are n-1 muchii, atunci este
arbore. Înseamnă că el nu conţine cicluri. În acest caz, orice muchie am adăuga lui G
nu pierde proprietatea de conexitate, dar va conţine cicluri. Prin
urmare, problema deciziei, pentru un graf conex, dacă conţine 1 5
sau nu cicluri se reduce la a compara numărul de muchii m, cu
n-1. Dacă m>n-1, atunci graful conţine cicluri. Dacă m=n-1,
graful nu conţine cicluri.
2
 Problema 9.3. Fiind dat G=(V,E) un graf conex, se cere să
se separe n-1 muchii ale unui arbore parţial şi m-(n-1) muchii
care nu fac parte din acesta. 3 4
Pentru graful alăturat, muchiile care alcătuiesc arborele pot fi
cele trasate îngroşat, iar celelalte, normal. Figura 9.3. Exemplu

1
Paragraful a fost introdus în acest capitol întrucât înţelegerea lui implică cunoaşterea
noţiunii de arbore.
Manual de informatică pentru clasa a XI-a 265

 Rezolvare. Vom folosi, din nou, parcurgerea DF (în exemplu, pornind de la nodul
1). Astfel, muchiile care constituie arborele parţial, vor fi numite muchii de avans, iar
cele care sunt în plus se vor numi muchii de întoarcere. O muchie este de
întoarcere dacă în timpul parcurgerii, după ce este selectată, va "atinge" un nod deja
vizitat.
Altfel spus, o muchie de întoarcere, determină un ciclu. Aceasta înseamnă că
cele m-(n-1) muchii de întoarcere determină m-(n-1) cicluri.

Definiţia 9.2. Fiind dat un graf neorientat, vom numi set de cicluri
elementare, un număr maxim de cicluri în graf, astfel încât fiecare ciclu
conţine cel puţin o muchie care nu mai apare în nici un alt ciclu din set.

Teorema 9.2. Un graf conex conţine m-(n-1) cicluri elementare.

Cum fiecare muchie de întoarcere determină un ciclu, înseamnă că avem un


set de m-(n-1) cicluri. Fiecare dintre aceste cicluri conţine o muchie care nu mai
apare în nici un alt ciclu din set şi anume, muchia de întoacere. Se demonstrează că
m-(n-1) este numărul maxim de cicluri cu această proprietate.

Să observăm că un graf poate conţine mai multe cicluri decât m-(n-1), dar
unele dintre ele nu conţin nici o muchie care nu apare în alte cicluri.
Pentru graful din exemplu, un set de cicluri elementare poate fi:

1) 1 2 3 4 1
2) 2 3 4 2
3) 1 2 3 4 5 1

În acelaşi timp, graful mai are şi ciclul 1 2 4 1. Dar muchia (1,2) este în
ciclul 1), muchia (2,4) este în ciclul 2), iar muchia (4,1) este în ciclul 1).

 Problema 9.4. Fiind dat un graf conex, G=(V,E) să se determine un set de cicluri
elementare.

 Rezolvare. Vom utiliza o parcurgere DF. Pentru a reţine selecţiile astfel efectuate,
vom utiliza un vector T, iar elementele acestuia au semnificaţia deja cunoscută:

 j,
T[]
dacă i este descendent al lui j
i =
0, dacă i este rădăcina arborelui

În momentul în care este identificată o muchie (nod,k), care "atinge" un nod


deja selectat k, atunci nodul k este memorat, iar subprogramul ciclu afişează ciclul
începând cu valoarea memorată a lui k, după care, restul nodurilor conţinute de acest
ciclu sunt regăsite cu ajutorul vectorului T, care a reţinut drumul alcătuit de muchiile
de înaintare.
266 Capitolul 9. Arbori

Programul este următorul:

Varianta Pascal Varianta C++


uses grafuri,wincrt; #include "grafuri.cpp"

var s,T:array[1..50] of integer; int s[50],A[50][50],T[50],gasit,


A:Mat_ad; n,st;
gasit:boolean;
n,st:integer; void ciclu (int k)
{ cout<<st<<" ";
procedure ciclu(k:integer); while (k!=st)
begin { cout<<k<<" ";
write(st,' '); k=T[k];
while (k<>st)do }
begin cout<<st<<endl;
write (k,' '); }
k:=T[k];
end; void df(int nod)
writeln(st); { int k;
end; s[nod]=1;
for(k=1;k<=n;k++)
procedure df(nod:integer); if (A[nod][k]==1)
var k:integer; { A[k][nod]=0;
begin if (s[k]==0)
s[nod]:=1; { T[k] =nod;
for k:=1 to n do s[k]=1;
if A[nod,k]=1 then df(k);
begin }
A[k,nod]:=0; else
if s[k]=0 then { gasit=1;
begin st=k;
T[k]:=nod; ciclu(nod);
s[k]:=1; }
df(k); }
end }
else
begin main()
gasit:=true; { CitireN("Graf.txt",A,n);
st:=k; df(8);
ciclu(nod); if (!gasit)
end cout<<"Graful nu contine
end cicluri";
end; }

begin
CitireN('Graf.txt',A,n);
df(1);
if not gasit
then
writeln('Graful nu contine
cicluri');
end.
Manual de informatică pentru clasa a XI-a 267

9.4. Arbori cu rădăcină

9.4.1. Noţiunea de arbore cu rădăcină

Fiind dat un arbore, există posibilitatea să stabilim un nod ca radăcină a


arborelui. Neştiinţific vorbind, este ca şi cum "am prinde arborele de un nod, iar restul
nodurilor ar cădea".

În exemplul de mai jos, puteţi observa acelaşi arbore, reprezentat aşa cum am
fost obişnuiţi până acum (figura a)) şi ca arbore cu rădăcina 1 (figura b)):

1
1 2

5 4 2

3 4
5
3
a) b)

Figura 9.4. Exemplu


a) arbore obişnuit; b) arbore cu rădăcina 1.

Definiţia 9.3. Un arbore cu rădăcină este un set finit T, de unul sau mai
multe noduri, astfel încât:
a) Există un nod cu destinaţie specială, numit rădăcina arborelui.
b) Celelalte noduri sunt repartizate în m≥0 seturi disjuncte T1, T2, ..., Tm
şi fiecare din aceste seturi este un arbore. Arborii T1, T2, ..., Tm se
numesc subarbori ai rădăcinii.

Terminologie

Pentru arborii cu rădăcină se poate utiliza o terminologie specifică. Astfel, un


nod se numeşte rădăcină (tulpină, vârf), în exemplul de mai sus nodul 1 este
rădăcina. Se observă, că nodurile sunt aşezate pe mai multe niveluri, unde rădăcina
este pe nivelul 0, următoarele pe nivelul 1 (în exemplu: 2, 4, 5), ş.a.m.d. Dacă
nodurile i şi j sunt adiacente, atunci nodul aflat pe un nivel mai mic se numeşte
ascendent (tata), iar cel aflat pe nivelul următor este descendent (fiu) (ascendent,
descendent, în raport de celălalt nod). De exemplu, nodul 3 este descendentul (fiul)
nodului 5, iar nodul 5 este ascendentul (tatăl) nodului 3. Problema care se pune în
continuare este de a analiza modul în care poate fi memorat un arbore cu rădăcină.
268 Capitolul 9. Arbori

9.4.2. Memorarea arborilor cu rădăcină prin utilizarea


referinţelor descendente

A memora un arbore cu rădăcină prin referinţe descendente înseamnă,


pornind de la definiţia recursivă dată arborelui cu rădăcină, că nodul rădăcină va
reţine:
 o informaţie asociată, (de exemplu, numărul nodului respectiv);
 adresele tuturor celor m subarbori.

A) Un nod reţine adresele tuturor descendenţilor săi

Nu uitaţi, definiţia este recursivă. Prin urmare, fiecare nod al arborelui, pe lângă
informaţia asociată, va reţine adresele descendenţilor săi. Structura unui astfel de nod
este:

info adr1 adr2 adr3 adrm

O astfel de reprezentare prezintă dezavantajul risipei de memorie:


a) pentru un arbore oarecare nu se cunoaşte întotdeauna numărul maxim al
descendenţilor unui nod (m, în definiţie). Cum numărul maxim de descendenţi al
unui nod este n-1 (n este numărul de noduri), rezultă că m=n-1.

Vă daţi seama că în acest caz, risipa de memorie este uriaşă!


b) chiar dacă cunoaştem numărul maxim al descendenţilor unui nod m şi
acesta nu este foarte mare, de exemplu 4, tot se pierde multă memorie.
Aceasta se datorează faptului că un nod poate avea, de exemplu, un singur
nod descendent sau niciunul. Însă, el va ocupa spaţiul necesar celor patru
adrese, chiar dacă va reţine adrese nule (către nici un nod).

Cu toate acestea, o astfel de reprezentare este mult utilizată în cazul arborilor


binari. Motivul? Un nod poate avea cel mult 2 descendenţi (m=2). Pentru că arborii
binari sunt prezentaţi separat, exemplele de implementare ale acestui procedeu se
vor găsi acolo.

B) Reprezentarea prin liste de adiacenţe

Atunci când numărul de posibili descendenţi ai unui nod nu este cunoscut, sau
este cunoscut dar este mare, pentru a evita risipa de memorie, vom utiliza listele de
adiacenţe. În plus, trebuie cunoscută rădăcina.

Listele de adiacenţe au fost studiate, motiv pentru care nu le mai prezentăm


aici. Amintim doar faptul că acestea pot fi implementate prin alocarea statică
sau alocarea dinamică a memoriei.
Manual de informatică pentru clasa a XI-a 269

9.4.3. Memorarea arborilor cu rădăcină prin utilizarea


referinţelor ascendente
Să observăm că, în cazul unui arbore cu rădăcină, un nod poate avea mai mulţi
descendenţi, dar nu poate avea decât un singur ascendent. De ce? Revedeţi
definiţia arborilor cu rădăcină. Aceasta ne conduce la ideea că arborele cu rădăcină
poate fi memorat sub forma legăturii de tip Tata. Pentru aceasta, se utilizează un
vector T cu componente, unde:

 j, dacă j este ascendentul lui i


T[]
i =
0, dacă i este rădăcina arborelui

Pentru fiecare din cei doi arbori de mai jos, cu rădăcina 1, respectiv 3,
puteţi observa reprezentarea lor cu ajutorul unor referinţe ascendente:

3
1

5
2 4 5
1
sau
3 2 4

0 1 5 1 1 5 1 0 1 3

1 2 3 4 5 1 2 3 4 5

Figura 9.5. Exemplu de arbore cu rădăcină

 Problema 9.5. Se citeşte un graf neorientat, cu n noduri, despre care ştim că


este arbore. De asemenea, se citeşte un număr natural 1≤k≤n. Se cere să se
afişeze vectorul T, corespunzător arborelui cu rădăcina k. De exemplu, dacă se
introduc muchiile arborelui din figura 9.5 (stânga) şi se citeşte k=3, atunci se obţine
arborele alăturat (cel din dreapta).

 Rezolvare. Problema se rezolvă uşor, printr-o simplă parcurgere DF, cu nodul de


pornire k. De câte ori se selectează o muchie, se completează datele din vectorul T.

Varianta Pascal Varianta C++


uses grafuri; #include "grafuri.cpp"
var A:mat_ad;
int s[50],A[50][50],T[50],n,k;
s,T:array[1..50] of integer;
k,n,i:integer;
270 Capitolul 9. Arbori

procedure df(nod:integer); void df(int nod)


var k:integer; { int k;
begin s[nod]=1;
s[nod]:=1; for (k=1;k<=n;k++)
for k:=1 to n do if (A[nod][k]==1
if (A[nod,k]=1) and && s[k]==0)
(s[k]=0) { T[k]=nod;
then df(k);
begin }
T[k]:=nod; }
df(k);
end main()
end; { CitireN("Graf.txt",A,n);
cout<<"k="; cin>>k;
begin
df(k);
CitireN('Graf.txt',A,n);
for (int i=1;i<=n;i++)
write('k='); readln(k);
cout<<T[i];
df(k);
}
for i:=1 to n do write(T[i]);
end.

9.4.4. Înălţimea unui arbore cu rădăcină

Fiind dat un arbore cu rădăcină,


pentru fiecare nod al său, se poate 3 nivelul 0
determina nivelul pe care acesta se află.
Astfel, rădăcina are nivelul 0, descendenţii
ei au nivelul 1, descendenţii descendenţilor 5 nivelul 1
au nivelul 2, ş.a.m.d.
nivelul 2
Arborele alăturat are rădăcina 3: 1

nivelul 3
2 4
Figura 9.6. Exemplu

Definiţia 9.4. Fiind dat un arbore cu rădăcină, vom defini înălţimea


arborelui ca fiind cel mai mare nivel pe care se află un nod al său.
De exemplu, arborele de mai sus are are înălţimea 3.

 Problema 9.6. Se dă un arbore pentru care se cunoaşte rădăcina. Se cere ca


programul să afişeze, pentru fiecare nod în parte, nivelul pe care acesta se află.

 Rezolvare. Să ne aducem aminte de faptul că parcurgerea în lăţime (BF) oferă


posibilitatea de a afla distanţa de la nodul de pornire la fiecare nod în parte. În acest
caz, nodul de pornire va fi chiar rădăcina arborelui. Nivelul unui nou nod introdus în
coadă se calculează adăugând 1 la nivelul nodului aflat în vârful cozii.
Manual de informatică pentru clasa a XI-a 271

Varianta Pascal Varianta C++


uses grafuri; #include "grafuri.cpp"
var A:mat_ad;
s,coada,niv:array[1..50] of int n,coada[50],s[50],i_c=1,
integer; sf_c=1,A[50][50],i,niv[50],
n,i_c,i,sf_c,rad:integer; rad;
procedure bf(nod:integer); void bf(int nod)
begin { coada[i_c]=nod;
coada[i_c]:=nod; s[nod]=1;
s[nod]:=1; while (i_c<=sf_c)
while i_c<=sf_c do { i=1;
begin while (i<=n)
i:=1; { if (A[coada[i_c]][i]==1
while i<=n do && s[i]==0)
begin { sf_c++;
if (A[coada[i_c],i]=1) and coada[sf_c]=i;
(s[i]=0) s[i]=1;
then niv[coada[sf_c]]=
begin niv[coada[i_c]]+1;
sf_c:=sf_c+1; }
coada[sf_c]:=i; i++;
s[i]:=1; }
niv[coada[sf_c]]:= i_c++;
niv[coada[i_c]]+1; }
end; }
i:=i+1;
end; main()
i_c:=i_c+1; { CitireN("Graf.txt",A,n);
end cout<<"Radacina este ";
end; cin>>rad;
bf(rad);
begin
CitireN('Graf.txt',A,n); for (int i=1;i<=n;i++)
write('Radacina este '); cout<<"Nodul "<<i<< "
readln(rad); nivelul "<<niv[i]<<endl;
i_c := 1; }
sf_c := 1;
bf(rad);
for i:=1 to n do
writeln ('Nodul ',i,
' nivelul ',niv[i]);
end.

 Exerciţiu. Modificaţi programul de mai sus astfel încât să afişeze înălţimea


arborelui citit.

Intrebare: de ce credeţi că prezintă importanţă noţiunea de înălţime a unui


arbore cu rădăcină? Există o serie de algoritmi în care, pornind de la un nod,
trebuie să se găsească rădăcina sau invers, pornind de la rădăcină, se cere să se
regăsească informaţia reţinută de un anumit nod terminal. În ambele cazuri, operaţia
va fi cu atât mai rapidă cu cât înălţimea arborelui este mai mică.
272 Capitolul 9. Arbori

9.5. Noţiunea de pădure


Începem prin a observa că un vector de tip Tata poate reţine mai mulţi arbori
care au nodurile în mulţimea {1,2,...,n}:

4
1
0 1 1 0 4
Figura 9.7. 2 3
1 2 3 4 5 5
Exemplu

Definiţia 9.5. O pădure este dată de 1≤k≤n arbori cu nodurile în mulţimea


{1,2,...,n}, unde, dacă Ai i∈{1,2..k} sunt submulţimile nodurilor
fiecărui arbore, atunci:
a) A1∪ A2∪... ∪ Ak={1,2...n};
b) Ai∩ Aj=∅ ∀ i≠j∈{1,2...n}.

Sau, altfel spus, mulţimile nodurilor celor k arbori alcătuiesc o partiţie a mulţimii
{1,2,...,n}.

Rezultă de aici că printr-un vector de tip Tata putem reţine o partiţie a mulţimii
{1,2,...,n}. Desigur, ne putem întreba: la ce foloseşte asta? Şi, de ce ar fi nevoie
ca o submulţime a partiţiei să fie reţinută sub formă de arbore, pentru că, se ştie, între
elementele ei nu există nici o relaţie?
Există două motive esenţiale pentru a reţine o partiţie ca o pădure:
1) Cum testăm cărei mulţimi îi aparţine un element? Foarte simplu, vedem care este
tatăl său, apoi tatăl tatălui, ş.a.m.d., până ajungem la un element din vectorul T care
reţine 0. Dacă, în exemplul de mai sus vrem să vedem cărei mulţimi îi aparţine
elementul 3, atunci avem T[3]=1 şi T[1]=0. Prin urmare, elementul 3 aparţine
mulţimii nodurilor arborelui care are ca vârf nodul 1. Procedând astfel, avem avantajul
că această operaţie se efectuează în O(log(n)), adică foarte rapid. Într-o
reprezentare clasică, am identifica mulţimea în O(n)...
2) Reuniunea a două mulţimi din partiţie se face foarte uşor. Pur şi simplu, se
subordonează vârful unui arbore i, vârfului unui alt arbore j. Operaţia este T[i]=j.
Pentru a beneficia din plin de primul avantaj, va trebui ca atunci când reunim
două mulţimi din partiţie, să subordonăm arborele cu înălţimea mai mică,
arborelui cu înălţimea mai mare. Pentru a înţelege acest fapt, vom da un exemplu
în care iniţial nu ţinem cont de această observaţie, apoi arătăm cum trebuie procedat.
Să presupunem că iniţial avem 4 arbori, unde fiecare arbore are un singur
nod. În termeni din teoria mulţimilor, iniţial avem o partiţie din 4 mulţimi şi
fiecare mulţime are un singur element.
Manual de informatică pentru clasa a XI-a 273

Situaţia iniţială este:


1 2 3 4
Figura 9.8.

A) Fără a ţine seama de observaţie, reunim mulţimea {1} cu mulţimea {2}, apoi
mulţimea obţinută cu {3}, apoi mulţimea obţinută cu {4}. Arborele obţinut are
înălţimea 3, deci regăsirea informaţiei se face în O(n).

2 3 4 2 4 2

1 1 1

3 3

4
Figura 9.9.

B) Ţinem seama de observaţia precedentă şi efectuăm aceleaşi operaţii. De această


dată, arborele obţinut are înălţimea 1:

2 3 4 2 4 2

1 1 3 4 1 3

Figura 9.10.

 Problema 9.7. Să se scrie un program care citeşte un număr de relaţii de forma


(i,j). Semnificaţia este: triunghiul i este asemenea cu triunghiul j. Apoi se vor citi,
din nou, două numere naturale i,j cu semnificaţia de triunghiuri. Întrebarea este: sunt
asemenea triunghiurile i şi j?

 Rezolvare. Putem asocia perechilor citite un graf. Descompunem graful în


componente conexe. A stabili dacă cele două triunghiuri sunt asemenea înseamnă
a vedea dacă ele sunt în aceeaşi componentă conexă. Complexitatea este O(n2).
Dar se poate proceda şi altfel. Iniţial se consideră că nu există două triunghiuri
asemenea. Aceasta înseamnă că, în vectorul Tata, toate componentele reţin 0. A citi
o pereche de triunghiuri asemenea, revine la citirea unei muchii (i,j). Aceasta
înseamnă că trebuie găsite vârfurile celor doi arbori care conţin nodurile i şi j. Dacă
acestea sunt diferite, atunci arborele cu înălţimea mai mică este subordonat arborelui
cu înălţimea mai mare.
274 Capitolul 9. Arbori

Pentru fiecare vârf al grafului, vectorul H reţine înălţimea sa. Iniţial, toate
componentele lui H reţin 0. Când se schimbă numărul de niveluri ale unui vârf? Atunci
când un arbore se subordonează altuia şi numai atunci când cei doi arbori au aceeaşi
înălţime. În acest caz, înălţimea arborelui care subordonează alt arbore creşte cu 1.
Dacă înălţimile celor doi arbori nu sunt egale, atunci se subordonează arborele cu
înălţime mai mică, arborelui cu înălţime mai mare. Acesta din urmă rămâne cu
aceeaşi înălţime. Exemplu: se reunesc cei doi arbori din stânga şi se obţine arborele
din dreapta:

2 5 2

5
1 3 1 3
6
6

4 4

Figura 9.11. Exemplu

Programul foloseşte două subprograme:

Arb() - returnează vârful arborelui căruia îi aparţine nodul i;


Adaug() - identifică vârful arborelui căruia îi aparţine nodul i şi cel al arborelui
căruia îi aparţine nodul j. Dacă cei doi arbori sunt diferiţi (au vârfuri diferite), unul
dintre ei, cel cu înălţime mai mică, este subordonat celui cu înălţime mai mare.

Varianta Pascal Varianta C++


uses grafuri; #include "grafuri.cpp"
int n,i,j,T[50],H[50];
var T,H:array[1..50] of integer;
n,i,j:integer; int Arb(int nod)
f:text; { while (T[nod]) nod=T[nod];
return nod;
function Arb(nod:integer) }
:integer;
begin void Adaug(int i, int j)
while T[nod]<>0 do { int v1=Arb(i),v2=Arb(j);
nod:=T[nod]; if (v1!=v2)
Arb:=nod; if (H[v1]==H[v2])
end; { T[v1]=v2;
H[v2]++;
procedure Adaug(i,j:integer); }
var v1,v2:integer; else
begin if (H[v1]<H[v2]) T[v1]=v2;
v1:=Arb(i); else T[v2]=v1;
v2:=Arb(j); }
Manual de informatică pentru clasa a XI-a 275

if v1<>v2 then main()


if H[v1]=H[v2] { fstream
then f("graf.txt",ios::in);
begin f>>n;
T[v1]:=v2; while (f>>i>>j) Adaug(i,j);
H[v2]:=H[v2]+1; f.close();
end cout<<"i=";cin>>i;
else cout<<"j=";cin>>j;
if H[v1]<H[v2] if (Arb(i)==Arb(j))
then T[v1]:=v2 cout<<"asemenea";
else T[v2]:=v1; else cout<<" Nu sunt
end; asemenea";
}
begin
assign(f,'graf.txt');
reset(f);
readln(f,n);
while not eof(f) do
begin
readln(f,i,j);
Adaug(i,j);
end;
close(f);
write('i=');readln(i);
write('j=');readln(j);
if Arb(i)=Arb(j)
then write('asemenea')
else write('Nu sunt
asemenea');
end.

Complexitatea algoritmului

Avem m muchii. Pentru fiecare muchie se testează apartenenţa nodurilor


incidente la un arbore. Prin urmare, se efectuează de două ori o operaţie de
complexitate O(log(m)). Complexitatea finală este O(m×log(m)).

9.6. Arbori parţiali de cost minim


Fie G=(V,E) un graf neorientat, conex. Fiecare muchie (i,j) are asociat
un cost ci,j≥0. Se cere un arbore parţial al lui G, astfel încât suma costului
muchiilor sale să fie minimă.

Un asemenea arbore se numeşte arbore parţial de cost minim.


În figura 9.12., a), aveţi un graf neorientat conex. În figura b), aveţi un
arbore parţial, dar nu de cost minim. În figurile c) şi d), sunt prezentaţi doi
arbori parţiali de cost minim. Puteţi observa că, uneori, un astfel de graf
are mai mulţi arbori parţiali de cost minim.
276 Capitolul 9. Arbori

1 1
3 2

5 5 2 5 5 2
2 2
4 1 7 4 7

4 4 3 4 3
a) b)

1 1
3 3 2

5 2 5 2
2
1 1

4 4 3 4 4 3
c) d)

Figura 9.12. Exemplu

Pentru rezolvarea acestei probleme, vom prezenta doi algoritmi. Primul


dintre ei, algoritmul lui Kruskal, are complexitatea O(m×log m). Al doilea,
algoritmul lui Prim, are complexitatea O(n2). În funcţie de numărul muchiilor din
graf, vom alege un algoritm sau altul. Astfel, dacă numărul muchiilor este mic, vom
prefera algoritmul lui Kruskal. Dacă, dimpotrivă, numărul muchiilor este mare,
atunci, după cum am învăţat (revedeţi graful complet), numărul lor se apropie
de n2, motiv pentru care vom prefera algoritmul lui Prim.

De reţinut: ambii algoritmi se încadrează în tehnica Greedy.

9.6.1. Algoritmul lui Kruskal


Algoritmul este următorul:

 Iniţial, fiecare nod va constitui un arbore. Prin urmare, vom avea o pădure
alcătuită din n arbori. Apoi, se execută de n-1 ori pasul următor:
 Se caută muchia de cost minim care uneşte noduri care aparţin la doi
arbori diferiţi. Se selectează muchia respectivă.

După selectarea a n-1 muchii, se obţine un arbore parţial de cost minim. Vom
da un exemplu, pentru a arăta cum funcţionează algoritmul. Fie graful următor:
Manual de informatică pentru clasa a XI-a 277

1 1 1
3 2

5 5 2 5 2 5 2
2
4 1 7 1

4 4 3 4 3 4 3

Graful Iniţial Selectăm (2,4)

1 1 1
2 3 2 3 2

5 2 5 2 5 2

1 1 1

4 3 4 3 4 4 3

Selectăm (1,2) Selectăm (1,5) Selectăm (4,3)

Figura 9.13. Exemplu pentru algoritmul lui Kruskal

Să observăm faptul că prin aplicarea algoritmului se obţine un arbore. Mai întâi,


se selectează n-1 muchii. Pentru că, la fiecare pas, se selectează o muchie care
uneşte doi arbori distincţi, graful obţinut la un moment dat nu conţine cicluri.

 Demonstraţia algoritmului. Fie arborele rezultat după executarea programului


care implementează algoritmul. Acesta este dat sub forma muchiilor selectate: (m1,
m2, m3, ..., mn-1), unde m1≤m2≤m3...≤mn-1. Presupunem că arborele obţinut nu este
optim, adică există un arbore pe care îl presupunem optim, dat şi el sub forma unei
liste de muchii: (o1, o2, o3, ..., on-1), unde o1≤o2≤o3...≤on-1.
Pasul 1. Presupunem m1 diferit de o1. Adăugăm arborelui optim muchia m1 şi se
obţine (m1, o1, o2, o3, ..., on-1). Avem m1≤o1, pentru că, în caz contrar, algoritmul ar fi
selectat muchia o1. Graful astfel obţinut conţine un ciclu (are n noduri şi n muchii).
Dintre muchiile care alcătuiesc acest ciclu, eliminăm o muchie, alta decât cele ale
arborelui generat de algoritm. Obţinem astfel un nou arbore, dar tot de cost minim
pentru că muchia introdusă are un cost mai mic sau egal decât oricare altă muchie.
Pasul 2. Să presupunem că cei doi arbori, cel generat de algoritm (m1, m2, ..., mk, ...,
mn-1), şi cel optim (m1, m2, ..., mk, ok+1, ..., on-1), au primele k muchii comune. Avem,
m1≤m2≤...mk≤mk+1...≤mn-1 şi m1≤m2≤...mk, ≤...≤on-1. În arborele optim, adăugăm
mk+1. Avem relaţia: mk+1≤ok+1≤...on-1, contrar, se contrazice modul de alegere a
278 Capitolul 9. Arbori

muchiei mk+1. Evident, mk+1 va induce un ciclu. Ciclul nu este alcătuit exclusiv din
muchiile m1, m2, ..., mk, mk+1 pentru că ele fac parte din arborele generat de algoritm.
Prin urmare, ciclul va mai conţine o muchie, alta decât cele ale arborelui iniţial, muchie
pe care o eliminăm. Cum mk+1 are cea mai mică lungime, arborele rămâne optim,
dar are o muchie, în plus, comună cu arborele generat de algoritm. Procedând la
fel, vom transforma arborele parţial de cost minim în arborele generat de algoritm.
Prin urmare, arborele determinat de algoritm este optim.

Pentru buna înţelegere a demonstraţiei, vom da un exemplu. Mai jos, aveţi


arborele generat de algoritm şi un arbore optim:

1 1
3 2 3

5 2 5 2
2
1 1

4 4 3 4 4 3

a) b)
Arborele generat de algoritm (M) Arborele optim (O)

Figura 9.14. Exemplu

M={(2,4),(1,2),(1,5),(3,4)} şi O={(2,4),(1,4),(1,5),(3,4)}.

Cei doi arbori au diferită muchia a 2-a. Adăugăm


arborelui O muchia a 2-a din arborele M. Obţinem ciclul 1
[1,2,4,1]. Muchiile (2,4), (1,2) nu pot fi eliminate, 3 2
pentru că ele fac parte din arborele iniţial. Eliminăm
muchia (1,4). În acest fel, am transformat arborele 5 2
optim în arborele generat de algoritm. 2
1

4 4 3
Figura 9.15.
Implementarea algoritmului

Să observăm că pentru a reţine arborii care se formează se poate utiliza o


pădure. După cum ştim, pentru a reţine pădurea, putem folosi un vector de tip Tata.
La fiecare pas, trebuie să selectăm o muchie de cost minim. Pentru a asigura
costul minim, muchiile vor fi sortate crescător în funcţie de cost. Acceptarea unei
muchii se face numai dacă nodurile incidente ei aparţin la doi arbori diferiţi. În cazul în
care muchia este acceptată, arborele cu înălţime mai mică este subordonat vârfului
arborelui cu înălţimea mai mare. Precizăm faptul că muchiile, cu costurile lor, sunt
reţinute în matricea M. O muchie este de forma i, j, c, unde i şi j sunt nodurile la
Manual de informatică pentru clasa a XI-a 279

care muchia este incidentă, iar c este costul ei. Atenţie! Din motive strict didactice,
pentru ca sursa programului să nu fie prea mare, am renunţat la sortarea muchiilor
după cost. Dar programul dvs. va trebui să conţină subprogramul respectiv.

Varianta Pascal Varianta C++


uses grafuri; #include "grafuri.cpp"
var T,H:array[1..50]of integer; int n,nr_muchii,k=1,
M:array[1..3,1..50] of T[50],H[50],M[3][50];
integer;
n,nr_muchii,k:integer; void Citire()
procedure Citire; {
var f:text; fstream f("graf.txt",
begin ios::in);
k:=1; f>>n;
assign (f,'graf.txt'); while (f>>M[0][k]>>
reset(f); readln(f,n); M[1][k]>>M[2][k]) k++;
while not eof(f) do f.close();
begin }
readln(f,M[1,k],M[2,k],M[3,k]);
k:=k+1; // sortare
end; int Arb(int nod)
close(f); { while (T[nod]) nod=T[nod];
end; return nod;
{sortare} }
function Arb(nod:integer)
:integer; main()
begin { Citire();
while T[nod]<>0 do nod:=T[nod]; //sortare
Arb:=nod; k=1;
end; do
{while (Arb(M[0][k])==
begin
Arb(M[1][k])) k++;
Citire;
nr_muchii++;
{sortare}
cout<<M[0][k]<<"
k:=1;
"<<M[1][k]<<" "<<M[2][k]
repeat
<<endl;
while
Arb(M[1,k])=Arb(M[2,k]) do if (H[M[0][k]]==H[M[1][k]])
k:=k+1; { T[M[0][k]]=M[1][k];
nr_muchii:=nr_muchii+1; H[M[1][k]]++;
writeln(M[1,k],' ',M[2,k], }
' ',M[3,k]); else
if H[M[1,k]]=H[M[2,k]] then if (H[M[0][k]]<H[M[1][k]])
begin T[M[0][k]]=M[1][k];
T[M[1,k]]:=M[2,k]; else T[M[1][k]]=M[0][k];
H[M[2,k]]:=H[M[2,k]]+1; k++;
end } while (nr_muchii<n-1);
else }
if (H[M[1,k]]<H[M[2,k]])
then T[M[1,k]]:=M[2,k]
else T[M[2,k]]:=M[1,k];
k:=k+1;
until nr_muchii=n-1;
end.
280 Capitolul 9. Arbori

Estimarea timpului de calcul

Sortarea se face în O(m×log(m)). Pentru fiecare muchie, se testează


apartenenţa celor două noduri la care este incidentă, la arbori diferiţi. O astfel de
testare se face în O(log(m)). Cum avem, teoretic, m muchii testate, testele se
efectuează în O(m×log(m)). În concluzie, eficienţa algoritmului este O(m×log(m)).

9.6.2. Algoritmul lui Prim


Algoritmul este următorul:

 Se porneşte de la un nod al grafului.


 La fiecare pas din cei n-1, se va adăuga arborelui obţinut la pasul anterior
o muchie de cost minim. Minimul se referă la muchiile care au o singură
extremitate în arborele obţinut la pasul anterior.
În figura 9.16., prezentăm un exemplu de aplicare a algoritmului lui Prim,
pornindu-se cu nodul 1:

1 1 1
3 2 2

5 5 2 2
2
4 1 7

4 4 3

Graful Iniţial Selectăm (1,2)

1 1 1
2 3 2 3 2

2 5 2 5 2

1 1 1

4 4 3 4 4 3

Selectăm (2,4) Selectăm (1.5) Selectăm (4,3)

Figura 9.16. Exemplu pentru algoritmul lui Prim


Manual de informatică pentru clasa a XI-a 281

 Demonstraţie. În urma aplicării algoritmului, se obţine un arbore (vom avea N


noduri şi N-1 muchii, iar graful astfel obţinut este conex).
Să observăm că, la fiecare pas, algoritmul selectează o muchie. Muchiile
selectate până la pasul k-1, împreună cu nodurile la care sunt incidente,
alcătuiesc un arbore cu k noduri. Acest arbore va fi conţinut de arborele final,
generat de algoritm.
Fie G=(V,E) arborele parţial obţinut în urma aplicării algoritmului. Vrem să
demonstrăm că este de cost minim. Vom proceda prin reducere la absurd, adică
vom presupune că arborele generat nu este de cost minim. Fie, atunci,
Gmin =(V,E') un arbore parţial de cost minim.
Vom presupune că G şi Gmin au primele k-1 muchii comune: m1, m2, ..., mk-1,
unde k>0. Vom presupune că acestea unesc nodurile mulţimii
Vk-1={v1,v2,…,vk}.
Structura astfel obţinută este un arbore.
Să presupunem că, la pasul k, algoritmul selectează muchia (vi,vj)
(evident, vi∈Vk-1 şi vj∉Vk-1). De asemenea, vom presupune că această muchie
nu aparţine lui Gmin. Cum Gmin este conex, înseamnă că el conţine un lanţ de la vi
la vj. Nu toate nodurile acestui lanţ sunt în mulţimea Vk-1. Dacă, prin absurd, ar fi
aşa, ar însemna că algoritmul nostru ar fi generat un graf care conţine un ciclu,
pentru că, pe de o parte, conţine muchia (vi,vj), pe de altă parte, conţine lanţul,
deci se ajunge la o absurditate. De aici rezultă că, în acest lanţ există o muchie
(vm,vp) cu vm∈Vk, vp∉Vk. Înlocuim în Gmin muchia (vm,vp) cu muchia (vi,vj) şi
obţinem un graf G’. G’ are tot n-1 muchii (s-a înlocuit o muchie cu alta) şi rămâne
conex. Prin urmare, este arbore. În plus, costul muchiei (vi,vj) este mai mic sau
egal cu cel al muchiei (vm,vp), contrar algoritmul nostru ar fi selectat muchia
(vm,vp). Aceasta înseamnă că G’ este arbore parţial de cost minim. Prin urmare,
cu ajutorul acestor transformări, un graf parţial de cost minim se poate transforma
în arbore parţial generat de algoritm. Astfel, algoritmul determină un arbore
parţial de cost minim.

1 1
3 2 3

5 2 5 2
2
1 1

4 4 3 4 4 3

Arborele generat de algoritm (M) Arborele optim (O)

Figura 9.17. Exemplu


282 Capitolul 9. Arbori

Să dăm un exemplu (cel din figura 9.17). La


primul pas (k=1), algoritmul selectează muchia 1
(1,2). Muchia nu este conţinută în arborele 3 2
optim. Dacă adăugăm muchia (1,2) arborelui optim,
vom obţine un graf care conţine un ciclu. Avem V0={1}. 5 2
Nodurile 1 şi 2 sunt unite în arborele optim prin lanţul 2
{1,4,2}. În acest lanţ, prima muchie care uneşte un 1
nod din V0 cu unul care nu aparţine lui V0 este muchia
4 4 3
(1,4), muchie care este eliminată. Astfel, cei doi arbori,
cel generat de algoritm şi cel optim, au muchie comună
în plus (în acest exemplu le au pe toate). Figura 9.18

Graful va fi introdus prin matricea costurilor (vedeţi subprogramul Citesc). O


primă formă de implementare, mai puţin eficientă, va utiliza vectorul S aşa cum
suntem deja obişnuiţi: S[i]=1, dacă nodul i a fost selectat şi S[i]=0, în caz
contrar. Muchia (i,j) de cost minim va fi căutată pentru toate nodurile cu S[i]=1
şi S[j]=0. De aici rezultă că identificarea unei muchii se face în O(n2). Cum se
selectează n-1 muchii, astfel implementat, algoritmul are complexitatea O(n3).

Varianta Pascal Varianta C++


uses grafuri; #include "grafuri.cpp";
int n,S[50],i,j,k,c,lin,col;
var S:array[1..50] of integer;
float min,C[50][50];
C:array[1..50,1..50] of real;
n,i,j,k,m,lin,col:integer; void Citesc()
min:real; { fstream f("graf.txt",
ios::in);
procedure Citesc; f>>n;
var f:text; for (i=1;i<=n;i++)
begin for (j=1;j<=n;j++)
Assign(f,'Graf.txt'); if (i==j) C[i][j]=0;
Reset(f); else
Readln(f,n); C[i][j]=C[j][i]=PInfinit;
for i:=1 to n do while (f>>i>>j>>c)
for j:=1 to n do C[i][j]=C[j][i]=c;
if i=j f.close();
then C[i,j]:=0 }
else main()
begin { Citesc();
C[i,j]:=PInfinit; S[1]=1;
C[j,i]:=PInfinit; for (k=1;k<=n-1;k++)
end; { min=PInfinit;
while(not eof(f)) do for (i=1;i<=n;i++)
begin for (j=1;j<=n;j++)
readln(f,i,j,m); if (S[i]==1 && S[j]==0
C[i,j]:=m; && min>C[i][j])
C[j,i]:=m; { min=C[i][j];
end; lin=i; col=j; }
close(f); cout<<lin<<" "<<col
end; <<" "<<min<<endl;
S[col]=1; }
}
Manual de informatică pentru clasa a XI-a 283

begin
Citesc;
S[1]:=1;
for k:=1 to n-1 do
begin
min:=PInfinit;
for i:= 1 to n do
for j:=1 to n do
if (S[i]=1) and (S[j]=0)
and (min>C[i,j])
then
begin
min:=C[i][j];
lin:=i;
col:=j;
end;
writeln(lin,' ',col,
' ',min:2:0);
S[col]:=1;
end
end.

Cum putem implementa eficient algoritmul?

1. După ce am ales primul nod, vom presupune arborele deja construit,


considerând toate nodurile adiacente cu nodul de pornire. Dacă un nod i, nu este
adiacent cu nodul de pornire, îl vom considera adiacent, dar cu o muchie de cost
+∞. În acest fel, algoritmul va lucra întotdeauna asupra unui arbore care conţine
toate nodurile grafului iniţial (graful iniţial este presupus a fi complet).
2. Ca şi în cazul algoritmului lui Dijkstra, nodurile arborelui vor fi împărţite în
două submulţimi: nodurile selectate până la acel pas şi nodurile care nu sunt
selectate. Iniţial, este selectat numai nodul de pornire, iar alegerea unei muchii are
ca efect selectarea unui nou nod.
3. Un nod neselectat se consideră adiacent la unul dintre nodurile selectate şi
anume la acel nod pentru care costul muchiei este minim.
4. Cum am putea să determinăm, într-o singură parcurgere, muchia de cost
minim la unul dintre nodurile deja selectate? La început, când nu avem selectat
decât un nod, nu este nici o problemă. Dar, pe parcursul executării vom avea
selectate mai multe noduri. Atunci? Din acest motiv, componentele vectorului S vor
avea o altă semnificaţie:

0, dacă nodul i aparţine arborelui deja construit


S[i] = 
k , dacă sunt respectate cele două condiţii de mai jos:

a) nodul i nu aparţine arborelui deja construit;


b) muchia de cost minim care uneşte pe i cu unul dintre nodurile
arborelui deja construit este [i,k].
284 Capitolul 9. Arbori

Ce avantaje avem procedând astfel? Să presupunem că alegem o muchie.


Ea trebuie să fie incidentă unui nod neselectat şi unui nod selectat. Muchia este
incidentă unui nod neselectat dacă S[i]≠0. În acelaşi timp, S[i] reţine şi nodul
selectat la care este incidentă muchia. Prin urmare, muchia este (S[i],i).
Atunci, muchia de cost minim va putea fi obţinută într-o singură parcurgere,
pentru toţi i între 1 şi n, dacă S[i]≠0 şi se va calcula valoarea minimă pentru
C(s[i],i).
5. După selectarea unui nod, pentru fiecare nod neselectat, va trebui să găsim
muchia de cost minim de la el la unul din nodurile selectate. Să presupunem că la
pasul k se selectează nodul j. În acest moment, orice nod neselectat este legat
cu o muchie de cost minim la unul dintre nodurile selectate la paşii 1,2, …, k-1.
După selectarea nodului j, fiecare nod neselectat i, va fi legat fie la nodul j, dacă
de la el la j există o muchie de cost mai mic decât costul muchiei care-l lega la
unul dintre nodurile selectate, C(i,s(i))>C(i,j), fie, în caz contrar, rămâne
legat acolo unde era legat la pasul anterior. Algoritmul are complexitatea O(n2).

Programul este următorul:

Varianta Pascal Varianta C++


uses grafuri; #include "grafuri.cpp";
var S:array[1..50] of integer; int n,S[50],i,j,k,c;
C:array[1..50,1..50] of real; float min,C[50][50];
n,i,j,k,m:integer;
void Citesc()
min:real;
...
procedure Citesc;
main()
...
{ Citesc();
begin for (i=2;i<=n;i++) S[i]=1;
Citesc; for (k=1;k<=n-1;k++)
for i:=2 to n do S[i]:=1; { min=PInfinit;
for k:=1 to n-1 do for (i=1;i<=n;i++)
begin if (S[i])
min:=PInfinit; if (min>C[S[i]][i])
for i:=1 to n do { min=C[S[i]][i];
if S[i]<>0 then j=i;
if min>C[S[i],i] then }
begin cout<<S[j]<<" "<<j<<
min:=C[S[i]][i]; " "<<C[j][S[j]]<<endl;
j:=i; for (i=1;i<=n;i++)
end; if (S[i] &&
writeln (S[j],' ',j,' ', C[i][S[i]]>C[i][j])
C[j,S[j]]:2:0); S[i]=j;
for i:=1 to n do S[j]=0;
if (S[i]=1) and }
(C[i,S[i]]>C[i,j]) }
then S[i]:=j;
S[j]:=0;
end
end.
Manual de informatică pentru clasa a XI-a 285

9.7. Arbori binari

9.7.1. Noţiunea de arbore binar. Proprietăţi

Definiţia 9.6. Printr-un arbore binar vom înţelege un arbore cu


rădăcină, în care fiecare nod are cel mult două noduri descendente: cel
stâng şi cel drept.

Alăturat, puteţi observa un arbore binar cu 7


noduri. Nodul 1 este rădăcina. Ea subordonează 1
două noduri 2 şi 3. Nodul 3 are un singur
descendent, nodul 6. El este descendent drept. 2 3
Nodul 6 are un singur descendent, cel stâng şi
anume nodul 7. Nodurile 4,5,7 sunt noduri
terminale (au gradul 1). 4 5 6

Figura 9.19.
7
Exemplu de arbore binar

Observaţie. Vom face distincţie între


descendentul stâng şi descendentul drept. 1 1
Astfel, vom considera arborii binari alăturaţi ca fiind
diferiţi.

Figura 9.20. 2 2
Exemple de descendenţi

Vom da o a doua definiţie, recursivă, pentru arborii binari.


Definiţia 9.7. Un arbore binar este un set finit T, de unul sau mai multe
noduri, astfel încât:
 există un nod cu destinaţie specială, numit rădăcina (tulpina)
arborelui binar;
 celelalte noduri sunt repartizate în 2 seturi disjuncte T1, T2 şi fiecare
dintre aceste seturi este un arbore binar. Arborii T1, T2 se numesc
subarbori ai rădăcinii. Arborele T1 va fi numit subarborele stâng, iar
arborele T2 va numit subarborele drept.

Desigur, ne putem întreba: pentru ce


este necesară această a doua definiţie? După
cum va rezulta din studiul arborilor binari, mulţi
algoritmi care prelucrează arborii binari sunt
realizaţi recursiv, prin utilizarea tehnicii DIVIDE
ET IMPERA. T1 T2
Figura 9.21.
286 Capitolul 9. Arbori

Ideea este următoarea: se prelucrează informaţia asociată rădăcinii, apoi


informaţiile asociate subarborelui binar stâng şi cea asociată subarborelui binar
drept. Ori, această definiţie marchează exact rădăcina, subarborele stâng şi
subarborele drept.

Proprietăţi

În continuare, vom prezenta câteva proprietăţi ale arborilor binari:


1) Există cel mult 2i noduri pe nivelul i al arborelui. Pe nivelul 0 se află un nod
rădăcina (20), pe nivelul 1 cel mult 2 două noduri (21), pe nivelul 2, cel mult 4
noduri (22), ş.a.m.d. Se demonstrează uşor prin inducţie. Exerciţiu!

2) Un arbore binar de înălţime h are cel mult 2h noduri pe ultimul nivel. Ultimul
nivel este h.

3) Un arbore binar cu înălţimea h are cel mult 2h+1-1 noduri.

 Demonstraţie. Se însumează numărul maxim de noduri de pe fiecare nivel:


1+21+22+...2h=2h+1-1

(să observăm că avem o progresie geometrică cu n+1 termeni, primul termen este
1 şi raţia este 2).

4) Într-un graf cu n noduri şi înălţimea h, avem relaţia: h ≥ log 2 (n + 1) − 1 .

 Demonstraţie. Înălţimea este minimă dacă pe fiecare nivel avem un număr


maxim de noduri. În acest caz, din 3) rezultă că numărul total de noduri este
2h+1-1. Rezultă că trebuie rezolvată ecuaţia cu necunoscuta h,

2h+1-1=n ⇔ 2h+1=n+1

şi, pentru rezolvare, se logaritmează în baza 2.

5) Dacă a este numărul nodurilor terminale şi c este numărul nodurilor care au


exact 2 descendenţi, atunci a=c+1.

 Demonstraţie. Notăm cu b numărul nodurilor cu un singur descendent. Cum


orice nod are fie doi descendenţi, fie unul, fie nici unul, avem relaţia:
i) a+b+c=n (n este numărul de noduri).

Dacă considerăm arborele ca un graf orientat, unde există un arc de la tată la fiu,
atunci fiecare nod are gradul exterior 0,1,2. Cum suma gradelor exterioare este
egală cu numărul de muchii m, şi cum m este egal cu n-1, atunci avem şi relaţia:

ii) b+2c=n-1.

Din i) şi ii) prin eliminarea lui b, se obţine a-c=1⇔a=c+1.


Manual de informatică pentru clasa a XI-a 287

9.7.2. Modalităţi de reprezentare a arborilor binari


De regulă, arborii binari se reprezintă prin referinţe descendente. Astfel,
pentru fiecare nod trebuie să se cunoască nodul care constituie vârful subarborelui
stâng şi nodul care reprezintă vârful subarborelui drept.

1. Reprezentarea cu ajutorul vectorilor

Un arbore binar poate fi reprezentat cu ajutorul a doi vectori, pe care îi vom


numi st (de la stânga) şi dr (de la dreapta). Pentru fiecare nod i dintre cele n,
st[i] reţine numărul de ordine al nodului stâng subordonat de i, iar dr[i]
reţine numărul de ordine al nodului drept subordonat de i. Dacă nu există nod
subordonat, se reţine 0.

Arborele binar cu 6 noduri şi v=1 din figura 9.22 se reprezintă astfel:

1 1 2 3 4 5 6

St 2 4 0 0 0 0
2 3
Dr 3 5 6 0 0 0
4 5 6
Figura 9.22.

2. Reprezentarea în HEAP

Fiecare nod al arborelui este reprezentat în HEAP de o înregistrare cu următoarea


structură: Un exemplu de creare şi parcurgere a unui arbore binar este dat în
paragraful anterior.

Varianta Pascal Varianta C++


type ref=^Nod; struct Nod
Nod=record {
nr:integer; int nr;
stg,drt:ref; Nod *stg, *drt;
end; };

Desigur, nimeni nu ne opreşte să reprezentăm un arbore binar prin referinţe


ascendente (vectorul Tata).

9.7.3. Modalităţi de parcurgere a arborilor binari


În scopul prelucrării, este necesar ca nodurile arborelui binar să fie vizitate.
Există mai multe modalităţi de parcurgere a arborilor binari care diferă prin
ordinea de vizitare a nodurilor.
288 Capitolul 9. Arbori

Principalele modalităţi de parcurgere ale unui arbore binar sunt:

A) Parcurgerea în inordine (SVD) - se parcurge mai întâi subarborele stâng,


apoi vârful, apoi subarborele drept.
B) Parcurgerea în preordine (VSD) - se parcurge mai întâi vârful, apoi
subarborele stâng, apoi subarborele drept.
C) Parcurgerea în postordine (SDV) - se parcurge mai întâi subarborele stâng,
apoi subarborele drept, apoi vârful.
D) Parcurgerea în lăţime - a fost studiată, aici nu mai este prezentată.

Ordinea de parcurgere pentru


1 arborele alăturat este:
Inordine (SVD): 4 2 5 1 3 6
2 3 Preordine (VSD) 1 2 4 5 3 6
Postordine (SDV) 4 5 2 6 3 1
4 5 6
Figura 9.23.

 Problema 9.8. Se citesc datele despre un arbore binar. Se cere să se parcurgă


în inordine, preordine, postordine pornind de la reprezentarea arborelui prin
vectori. Datele se citesc dintr-un fişier text în care pe prima linie se găseşte n, pe
următoarea linie succesorii din stânga ai fiecărui nod, iar pe a treia linie, succesorii
din dreapta ai fiecărui nod. De exemplu, pentru arborele de mai sus, datele sunt:
6
2 4 0 0 0 0
3 5 6 0 0 0

Varianta Pascal Varianta C++


var St,Dr:array[1..50] of #include <fstream.h>
integer; int i,n,St[50],Dr[50];
i,n:integer;
void Citire()
procedure Citire; { fstream f("graf.txt",
var f:text; ios::in);
begin f>>n;
Assign(f,'graf.txt'); for (i=1;i<=n;i++) f>>St[i];
reset (f); for (i=1;i<=n;i++) f>>Dr[i];
readln(f,n); f.close();
for i:=1 to n do }
read(f,St[i]);
void SVD(int nod)
readln(f);
{ if (nod)
for i:=1 to n do
{ SVD(St[nod]);
read(f,Dr[i]);
cout<<nod;
close(f);
SVD(Dr[nod]); }
end;
}
Manual de informatică pentru clasa a XI-a 289

procedure SVD(nod:integer); void VSD(int nod)


begin { if (nod)
if nod<>0 then { cout<<nod;
begin VSD(St[nod]);
SVD(St[nod]); VSD(Dr[nod]);
write(nod); }
SVD(Dr[nod]); }
end
end; void SDV(int nod)
{ if (nod)
procedure VSD(nod:integer); { SDV(St[nod]);
begin SDV(Dr[nod]);
if nod<>0 then cout<<nod;
begin }
write(nod); }
VSD(St[nod]);
main()
VSD(Dr[nod]);
{ Citire();
end
SVD(1);
end;
cout<<endl;
procedure SDV(nod:integer); VSD(1);
begin cout<<endl;
if nod<>0 then SDV(1);
begin }
SDV(St[nod]);
SDV(Dr[nod]);
write(nod);
end
end;
begin
Citire;
SVD(1); writeln;
VSD(1); writeln;
SDV(1);
end.

 Problema 9.9. Se citesc datele despre un arbore binar. Se cere să se parcurgă


în inordine, preordine, postordine pornind de la reprezentarea arborelui prin
referinţe descendente alocate dinamic. Modul de introducere a datelor este cel de
la problema anterioară.

Varianta Pascal Varianta C++


type ref=^Nod; #include <fstream.h>
Nod=record
nr:integer; int i,n,St[50],Dr[50];
stg,drt:ref; struct Nod
end; {
var St,Dr:array[1..50] of int nr;
integer; Nod *stg, *drt;
i,n:integer; };
v:ref; Nod* v;
290 Capitolul 9. Arbori

{procedura Citire de la /* functia Citire de la


programul anterior} programul anterior */
function CreArb(nd:integer):ref; Nod* CreArb(int nod)
var c:ref; { if (nod)
begin { Nod * c=new Nod;
if nd<>0 then c->nr=nod;
begin c->stg=CreArb(St[nod]);
new (c); c->drt=CreArb(Dr[nod]);
c^.nr:=nd; return c;
c^.stg:=CreArb(St[nd]); }
c^.drt:=CreArb(Dr[nd]); else return 0;
CreArb:=c; }
end
else CreArb:=nil; void SVD(Nod* c)
end; { if (c->nr)
{ SVD(c->stg);
procedure SVD(c:ref);
cout<<c->nr;
begin
SVD(c->drt);
if c<>nil
}
then
}
begin
SVD(c^.stg);
void VSD(Nod* c)
write (c^.nr);
{ if (c->nr)
SVD(c^.drt);
{ cout<<c->nr;
end
VSD(c->stg);
end;
VSD(c->drt);
procedure VSD(c:ref); }
begin }
if c<>nil
then void SDV(Nod* c)
begin { if (c->nr)
write (c^.nr); { SDV(c->stg);
VSD(c^.stg); VSD(c^.drt); SDV(c->drt);
end cout<<c->nr;
end; }
}
procedure SDV(c:ref);
begin main()
if c<>nil { Citire();
then v=CreArb(1);
begin SVD(v); cout<<endl;
SDV(c^.stg); VSD(v); cout<<endl;
SDV(c^.drt); SDV(v); cout<<endl;
write (c^.nr) }
end
end;
begin
Citire;
v:=CreArb(1);
SVD(v); writeln;
VSD(v); writeln;
SDV(v);
end.
Manual de informatică pentru clasa a XI-a 291

9.7.4. O aplicaţie a arborilor binari: forma poloneză a


expresiilor

Se dă o expresie aritmetică ce foloseşte operatorii '+', '-', '*', '/' precum şi


'(', ')'. Fiecare operand este o literă. Se cere să se convertească această expresie
într-o expresie în forma poloneză (postfixată).

Definim expresia în forma poloneză (postfixată) recursiv, astfel:

- un operand este o expresie în forma poloneză;


- dacă E1 şi E2 sunt două expresii în formă poloneză şi # un operator, E1
E2 # este o expresie în forma poloneză;
- orice expresie în forma poloneză se obţine aplicând regulile de mai
sus, de un număr finit de ori.
Priviţi mai jos câteva exemple:

EXPRESIE ARITMETICĂ EXPRESIE ÎN FORMĂ POLONEZĂ

a+b ab+
a*(b+c) abc+*
a*(b+c)-e/(a+d)+h abc+*ead+/-h+

Forma poloneză este o formă de scriere a expresiilor aritmetice în care


parantezele nu mai sunt puse (din acest motiv o expresie scrisă în forma
poloneză se mai numeşte şi expresie scrisă fără paranteze), dar aceasta nu
conduce la calculul eronat al expresiei.

Expresiile aritmetice pot fi reprezentate utilizând arbori binari, respectând


următoarele reguli:

- fiecare operaţie corespunde unui nod neterminal, având ca informaţie


utilă operaţia respectivă;
- fiecare nod terminal este etichetat cu o variabilă sau cu o constantă;
- pentru fiecare nod neterminal subarborele din stânga şi cel din dreapta
reprezintă, în această ordine, cei doi operanzi;
- rădăcina corespunde ultimei operaţii executate la evaluarea expresiei.

Expresiei (a+b)*c-d/e i se asociază arborele binar din figura Z.24.


Parcurgerea acestui arbore în postordine va da chiar forma poloneză:
ab+c*de/-.
Utilizând cele spuse, pentru rezolvarea problemei, vom proceda în felul următor:
292 Capitolul 9. Arbori

⇒ construim arborele binar


asociat expresiei aritmetice; -
⇒ îl parcurgem în postordine
pentru a obţine forma * /
poloneză.

+ c d e

a b
Figura 9.24.

Pentru construcţia arborelui, vom acorda priorităţi operatorilor şi operanzilor


(mai puţin parantezelor), după cum urmează:

- prioritatea iniţială a operatorilor ‘+’, ‘-’ este 1;


- prioritatea iniţială a operatorilor ‘*’, ‘/’ este 10;
- la prioritatea unui operator se adună 10 pentru fiecare pereche de paranteze
între care se găseşte;
- prioritatea unui operand este 1000.

În program, acest lucru se realizează astfel:

- se citeşte expresia aritmetică în variabila e;


- se utilizează o variabilă j care indică ce număr se adaugă la prioritatea
iniţială a unui operator (la întâlnirea unei paranteze deschise j creşte cu 10,
iar la întâlnirea unei paranteze închise j scade cu 10);
- se parcurge expresia caracter cu caracter şi se pun în vectorul p priorităţile
acestor operatori şi operanzi (mai puţin ale parantezelor);
- în efp se construieşte expresia fără paranteze (la expresia aritmetică iniţială
lipsesc parantezele), iar în pfp se obţine vectorul priorităţilor, din care, spre
deosebire de p, lipsesc componentele corespunzătoare parantezelor
(acestea nu aveau nici o valoare).

Utilizând efp şi pfp, cu ajutorul funcţiei arb, se construieşte arborele


ataşat expresiei aritmetice. Un nod al acestui arbore are ca informaţie utilă un
operator sau un operand.

Conform tehnicii DIVIDE ET IMPERA, subprogramul arb procedează în felul următor:


⇒ de la limita superioară către limita inferioară (limite corespunzătoare
subşirurilor de caractere tratate din efp), caută operatorul sau operandul cu
prioritate minimă, reţinând poziţia acestuia. Acesta constituie informaţia utilă
din nod, care va fi completată.
⇒ în situaţia în care limita inferioară este diferită de limita superioară, pentru
completarea adresei subarborelui din stânga şi a subarborelui din dreapta se
reapelează funcţia, iar în caz contrar aceste câmpuri capătă valoarea 0.
Manual de informatică pentru clasa a XI-a 293

Pentru listarea în postordine, se utilizează funcţia parc. Programul este


prezentat în continuare:

Varianta Pascal Varianta C++


type ref=^inr; #include <iostream.h>
sir=array [1..30] of struct Nod
char; { char op;
Nod *as,*ad;
vector=array [1..30] of
};
integer;
char e[30],efp[30],a;
inr=record
as,ad:ref; int pfp[30],p[30],i,j,n;
op:char
Nod *c;
end;
var e,efp:sir; Nod* arb(int li,int ls,char
pfp,p:vector; efp[30],int pfp[30])
i,j,n:integer; { Nod* c;
c:ref; int i,j,min;
a:char; min=pfp[ls];
i=ls;
function arb
for (j=ls;j>=li;j--)
(li,ls:integer;var efp:sir;var
if (pfp[j]<min)
pfp:vector):ref;
{ min=pfp[j];
var c:ref;
i=j;
i,j,min:integer;
}
begin
c=new Nod;
min:=pfp[ls]; c->op=efp[i];
i:=ls; if(li==ls) c->as=c->ad=0;
for j:=ls downto li do else
if pfp[j]<min { c->as=arb(li,i-1,efp,pfp);
then c->ad=arb(i+1,ls,efp,pfp);
begin }
min:=pfp[j];
return c;
i:=j
}
end;
new(c);
void parc(Nod* c)
arb:=c;
{ if (c)
arb^.op:=efp[i];
{
if li=ls
parc(c->as);
then
parc(c->ad);
begin
cout<<(c->op);
arb^.as:=nil;
}
arb^.ad:=nil
}
end
else
main()
begin
{ j=0;cin>>a;n=1;
arb^.as:=arb(li,i-1,
while (a!='.')
efp,pfp);
{
arb^.ad:=
e[n++]=a;
arb(i+1,ls,efp,pfp)
cin>>a;
end
}
end; n--;
294 Capitolul 9. Arbori

procedure parc (c:ref); for (i=1;i<=n;i++)


begin switch(e[i])
if c<>nil {
then case ')': j-=10; break;
begin case '(': j+=10; break;
parc(c^.as); case '+': p[i]=j+1; break;
parc(c^.ad); case '-': p[i]=j+1; break;
write(c^.op) case '*': p[i]=j+10; break;
end case '/': p[i]=j+10; break;
end; default: p[i]=1000;
}
begin j=1;
j:=0; for (i=1;i<=n;i++)
read(a); if ( e[i]!=')' && e[i]!='(' )
n:=1; { efp[j]=e[i];
while a<>'.' do pfp[j]=p[i];
begin j++;
e[n]:=a; }
n:=n+1; c=arb(1,j-1,efp,pfp);
read(a) parc(c);
end; }
n:=n-1;
for i:=1 to n do
case e[i] of
')':j:=j-10;
'(':j:=j+10;
'+','-':p[i]:=j+1;
'*','/':p[i]:=j+10
else p[i]:=1000
end;
j:=1;
for i:=1 to n do
if (e[i]<>')') and
(e[i]<>'(')
then
begin
efp[j]:=e[i];
pfp[j]:=p[i];
j:=j+1
end;
c:=arb(1,j-1,efp,pfp);
parc(c);
writeln
end.

 Problema 9.10. Dându-se o expresie în forma poloneză postfixată, să se scrie


un program care generează programul de calcul al expresiei utilizând numai
instrucţiuni de atribuire cu un singur operator. Se vor introduce variabilele auxiliare
x0, x1, ..., xn.

Pentru expresia aritmetică a*(b+c)-e/(a+d)+h, forma poloneză este


abc+*ead+/-h+.
Manual de informatică pentru clasa a XI-a 295

Se generează următorul program de calcul:


x0=b+c
x1=a*x0
x2=a+d
x3=e/x2
x4=x1-x3
x5=x4+h

Pentru realizarea scopului propus, procedăm astfel:

a) folosim o stivă ST, care permite memorarea variabilelor x0, x1, ..., xn, a, b, ..., z.
b) forma poloneză se găseşte în FP;
c) din FP, se citeşte caracter cu caracter iar rezultatul citirii se tratează
diferenţiat astfel:
- în cazul în care caracterul citit este operand, se încarcă în stivă;
- în cazul în care caracterul citit este operator, se scot din stivă
conţinuturile ultimelor două niveluri şi se face tipărirea sub forma:
xj = variabila penultimă în stivă, operator, variabila ultimă în stivă,
iar variabila tipărită se reţine în stivă.
Procedeul se repetă atât timp cât nu au fost citite toate caracterele.
Pentru exemplul considerat, rularea decurge astfel:

b a, b, c se încarcă în stivă;

x0
la citirea operatorului “+”, se tipăreşte x0=b+c şi x0 se pune în stivă;
a

citirea operatorului “*”, determină tipărirea x1 = a*x0 şi x1 se pune


x1 în stivă;

e e, a, d se încarcă în stivă;

x1
296 Capitolul 9. Arbori

x2

e se tipăreşte x2 = a+d;
x1

x3
se citeşte ”/”, se tipăreşte x3 = e/x2;
x1

x4 se citeşte ”-”, se tipăreşte x4 = x1-x3;

h
caracterul ”h” se pune în stivă;
x4

la citirea operatorului “+”, se tipăreşte x5 = x4+h.


x5

Întrucât au fost citite toate caracterele, algoritmul se încheie.

Programul este prezentat în continuare:

Varianta Pascal Varianta C++


type stiva=array [1..100,1..2] #include <iostream.h>
of char; #include <string.h>
#include <stdlib.h>
var st:stiva;
fp:string[100]; char st[100][2],fp[100],c[2];
i,k,j:integer; int i,j,k;
c:string[1];
main()
begin {
cout<<"Forma poloneza este ";
write('forma poloneza este=');
cin>>fp;
readln(fp);
for (i=0;i<=strlen(fp)-1;i++)
k:=0;
if (fp[i]>='a' &&
j:=0;
fp[i]<='z')
for i:=1 to length(fp)
{ k++;
do
st[k][1]=fp[i];
case fp[i] of
st[k][0]=' ';
'a'..'z':
}
begin
else
k:=k+1;
if (fp[i]=='+' ||
st[k,2]:=fp[i]; fp[i]=='-' ||
st[k,1]:=' ' fp[i]=='*' ||
end; fp[i]=='/')
Manual de informatică pentru clasa a XI-a 297

'+','-','*','/': {
begin cout<<"x"<<j<<"="
writeln('x',j,'=', <<st[k-1][0]
st[k-1,1], <<st[k-1][1]<<fp[i]
st[k-1,2],fp[i], <<st[k][0]<<st[k][1]
st[k,1],st[k,2]); <<endl;
k:=k-1; k--;
t[k,1]:='x';str(j:1,c); st[k][0]='x';
st[k,2]:=c[1]; itoa(j,c,10);
j:=j+1 st[k][1]=c[0];
end j++;
end {case} }
end. }

Procedeul indicat este utilizat pentru compilarea expresiilor. Se cunoaşte


faptul că procesorul are instrucţiuni care permit o singură operaţie şi doi
operanzi. O expresie trebuie adusă sub forma prezentată, pentru obţinerea codului
maşină. Algoritmii utilizaţi sunt de acest tip (evident, aceştia sunt şi optimizaţi).

9.7.5. Arbore binar complet

Definiţia 9.8. Se numeşte arbore binar plin, un arbore binar unde pe


fiecare nivel i∈{0,1,2,...,h} (h este înălţimea arborelui) se găsesc exact
2i noduri.

Alăturat, puteţi observa


un arbore binar plin: 1

2 3
Figura 9.25.
Exemplu de arbore binar plin
4 5 6 7

Proprietăţi

1. Un arbore binar plin cu înălţimea h are 2h+1-1 (vezi proprietăţile arborilor binari).

2. Un arbore binar plin cu n noduri are înălţimea log 2 (n + 1) − 1 .


 Demonstraţie. Dacă arborele are înălţimea h, atunci are 2h+1-1 noduri. Vom
avea: log 2 (n + 1) − 1 = log 2 (2 h +1 − 1 + 1) − 1 = log 2 (2 h +1 ) − 1 = h + 1 − 1 = h .
De ce credeţi că prezintă interes studiul arborilor binari plini? Motivul este dat
de faptul că, în raport cu numărul de noduri, un astfel de arbore are înălţimea
minimă. De multe ori suntem interesaţi ca pornind de la un nod terminal, să
ajungem în număr minim de "paşi" către nodul rădăcină. Acest număr de paşi este
dat de înălţimea arborelui, h.
298 Capitolul 9. Arbori

În realitate, nu întotdeauna vom putea lucra cu arbori plini deoarece, pentru


aceştia, numărul de noduri n trebuie să fie de forma 2h+1-1. Cum vom putea
aborda problema, pentru un număr oarecare n de noduri? Pentru aceasta, vom
utiliza arborii compleţi.

Definiţia 9.9. Un arbore binar complet este un arbore în care fiecare


nivel i∈{0,1,2,...,h-1} (h este înălţimea arborelui) are exact 2i noduri,
iar de pe nivelul h, lipsesc p≥1 noduri, fie primele din dreapta, fie
primele din stânga.

În figura de mai jos, aveţi doi arbori binari compleţi:

1 1

2 3 2 3

4 5 6 4 5

Figura 9.26. Exemple de arbori binari compleţi

Proprietăţile unui arbore binar complet

1. Toate nodurile terminale se găsesc pe ultimele două niveluri.


2. h = [log 2 (n)].

 Demonstraţie. Dacă arborele are noduri pe ultimul nivel, atunci n>2h-1 ⇒


n≥2h. Cum arborele are noduri pe nivelul h, rezultă că n≤2h+1-1 ⇒ n<2h+1. Astfel,
avem dubla inegalitate:

2 h ≤ n < 2 h +1 ⇔ log 2 2 h ≤ log 2 (n) < log 2 2 h +1 ⇔ h ≤ log 2 (n) < h + 1 ⇔ h = [log 2 (n)]

Problema care se pune în continuare este de a găsi o structură de date


care permite memorarea unui arbore binar complet.

Un arbore binar complet cu n noduri poate fi reţinut cu ajutorul unui vector V,


cu n componente, astfel:
1. Rădăcina este dată de valoarea reţinută de V[1].
2. Pentru nodul V[i], vârful subarborelui stâng este V[2i] şi vârful
subarborelui drept este V[2i+1].

În continuare, puteţi observa cum se reprezintă un arbore binar complet


cu 6 noduri cu ajutorul unui vector V, cu 6 componente:
Manual de informatică pentru clasa a XI-a 299

1 2 3 4 5 6
4 6
V 1 4 6 3 1 5

3 1 5

Figura 9.27. Exemplu de arbore binar complet

Observaţii

 Să observăm că utilizând o astfel de convenţie, nu se pot reprezenta decât


arbori compleţi de la care, pe ultimul nivel, lipsesc numai noduri din partea
dreaptă. Dar dacă facem convenţia că, pentru nodul V[i], vârful
subarborelui drept este V[2i] şi vârful subarborelui stâng este V[2i+1],
atunci se pot reprezenta şi arbori binari compleţi la care, pe ultimul nivel,
lipsesc noduri din stânga.

 În cele ce urmează, vom considera numai arbori binari compleţi la care


presupunem că, pe ultimul nivel, lipsesc numai noduri din dreapta.

9.7.6. MinHeap-uri şi MaxHeap-uri. Aplicaţii

Definiţia 9.10. Un vector v se numeşte MinHeap dacă:

n  v[i] ≤ v[2 ⋅ i]
∀i ∈ {1, 2,...  } ⇒ 
2 v[i] ≤ v[2 ⋅ i + 1] , pentru 2 ⋅ i + 1 ≤ n

Definiţia 9.11. Un vector v se numeşte MaxHeap dacă:

n  v[i] ≥ v[2 ⋅ i]
∀i ∈ {1, 2,...  } ⇒ 
2 v[i] ≥ v[2 ⋅ i + 1] , pentru 2 ⋅ i + 1 ≤ n

În cazul arborelui reprezentat de un vector de tip MinHeap, orice nod


subordonează noduri cu etichete mai mari, iar în cazul vectorului de tip
MaxHeap, orice nod subordonează noduri cu etichete mai mici.
300 Capitolul 9. Arbori

 Problema 9.11. Considerăm că vectorul V reţine un minHeap. Presupunem că


facem atribuirea V[1]←a. Se cere să se reorganizeze V ca minHeap.
 Rezolvare. Vom porni de la un exemplu. Vectorul V reţine minHeap-ul desenat:

2
1 2 3 4 5 6

V 2 3 7 6 4 9 3 7

6 4 9

Figura 9.28. Exemplu

Să presupunem că se face atribuirea: V[1]←5. Arborele astfel obţinut nu


mai este un minHeap, dar vârful subordonează două minHeap-uri:

5
1 2 3 4 5 6

V 5 3 7 6 4 9 3 7

6 4 9

Figura 9.29. Arborele obţinut în urma atribuirii V[1]←5

Va trebui să interschimbăm conţinuturile componentelor vectorului V, astfel


încât acesta să reţină un minHeap.
Să observăm că informaţia asociată vârfului noului minHeap va trebui să fie
valoarea cea mai mică între valoarea atribuită lui V[1], şi cele asociate vârfurilor
celor două minHeap-uri. Mai întâi vom calcula valoarea minimă reţinută de ambele
minHeap-uri: min{3,7}=3. Cum noua valoare 5 este mai mare decât minimul
obţinut, vom efectua o primă interschimbare, ca în figura de mai jos:

3
1 2 3 4 5 6

V 3 5 7 6 4 9 5 7

6 4 9

Figura 9.30. Arborele obţinut în urma primei interschimbări


Manual de informatică pentru clasa a XI-a 301

De acum, rămâne de rezolvat aceeaşi problemă pentru arborele care în vârf


reţine valoarea 5 şi minHeap-rile stâng şi drept ale acestuia: min {6,4}=4 şi
cum 5>4, efectuăm din nou o interschimbare:

3
1 2 3 4 5 6

V 3 4 7 6 5 9 4 7

6 5 9

Figura 9.31. Arborele obţinut în urma altei interschimbări

Algoritmul se încheie fie când noua valoare este mai mică decât valorile
reţinute de vârfurile celor două minHeap-uri, fie când se ajunge la un nod fără
descendenţi (are indicele mai mare decât [n / 2] ).

Să observăm că nu întotdeauna nodul i are două noduri subordonate.


Pentru indicele i ≤ [n / 2] , vom avea întotdeauna subordonat nodul de indice 2i,
dar sunt cazuri când nodul de indice 2i+1 s-ar putea să nu existe.

Programul conţine două subprograme:

 indValMin(i,n) - returnează indicele nodului vârf de minHeap care reţine


valoarea minimă între cele două vârfuri de minHeap-uri.

 Combinare(i,n) - porneşte de la o valoare oarecare reţinută de V[i] şi două


minHeap-uri de vârfuri 2i şi 2i+1. Are rolul de a organiza informaţiile ca minHeap
cu vârful de indice i. Subprogramul este realizat recursiv: pe un anumit nivel,
interschimbă, dacă este cazul, V[i] cu V[2i] sau V[i] cu V[2i+1].

Varianta Pascal Varianta C++


var V:array[1..20]of integer; #include <fstream.h>
n,i:integer;
f:text; int V[20],n,i;
function indValMin(i,
n:integer):integer; int indValMin(int i, int n)
begin {
if (2*i+1)<=n if (2*i+1<=n)
then if (V[2*i]<=V[2*i+1])
if V[2*i]<=V[2*i+1]
then return 2*i;
indValMin:=2*i else return 2*i+1;
else else return 2*i;
indValMin:=2*i+1 }
else indValMin:=2*i;
end;
302 Capitolul 9. Arbori

procedure void Combinare(int i,int n)


Combinare(i,n:integer); { int ind,man;
var ind,man:integer; if(i<=n/2)
begin { ind=indValMin(i,n);
if i<=n div 2 then
begin if (V[i]>V[ind])
ind:=indValMin(i,n); { man=V[i];
if V[i]>V[ind] then V[i]=V[ind];
begin V[ind]=man;
man:=V[i]; Combinare(ind,n); }
V[i]:=V[ind]; }
V[ind]:=man; }
Combinare(ind,n);
end main()
end { fstream f("graf.txt",
end; ios::in);
begin f>>n;
assign(f,'graf.txt'); reset (f); for (i=1;i<=n;i++) f>>V[i];
readln(f,n); f.close();
for i:=1 to n do read(f,V[i]); Combinare(1,n);
close(f); for (i=1;i<=n;i++)
Combinare(1,n); cout <<V[i]<<" ";
for i:=1 to n do write(V[i],' '); }
end.

[
Estimarea timpului de calcul. Pentru că graful are înălţimea log 2 ( n) , şi cum ]
interschimbările se fac în adâncime, atunci algoritmul are complexitatea
[ ]
O( log 2 (n) ). Raţionamentul este de tipul DIVIDE ET IMPERA.

 Problema 9.12. Se citeşte un vector cu n componente numere naturale. Să se


organizeze vectorul ca minHeap.
Exemplu: se citeşte 7 1 6 7 4 2 şi se afişează 1 4 2 7 7 6.

 Rezolvare. Evident, un singur element alcătuieşte un minHeap. Se porneşte


cu i = [n / 2] . Putem organiza un minHeap cu vârful i şi minHeap-urile cu vârfurile
2i şi 2i+1. Se repetă operaţia cu i = [n / 2] − 1 , ..., inclusiv cu i=1. În final, vectorul
va fi organizat ca minHeap. Programul va fi la fel ca precedentul, numai că vom
apela subprogramul minHeap.
Estimarea timpului de calcul. Avem [n / 2] iteraţii. Fiecare iteraţie implică
rezolvarea unei probleme precum cea anterioară, O([log 2 (n)]). Prin urmare,
complexitatea este O(n × log 2 n ). Se observă rezolvarea unei probleme complexe
pornind de la probleme simple şi prin combinarea soluţiilor.

Varianta Pascal Varianta C++


var V:array[1..20]of integer; #include <fstream.h>
n,i:integer;
f:text; int V[20],n,i;

function indValMin int indValMin(int i, int n)


... ...
Manual de informatică pentru clasa a XI-a 303

procedure void Combinare(int i,int n)


Combinare(i,n:integer); ...
...
void minHeap()
procedure minHeap; { for (i=n/2;i>=1;i--)
begin Combinare(i,n);
for i:= n div 2 downto 1 do }
Combinare(i,n);
end; main()
{
begin fstream f("graf.txt",
assign(f,'graf.txt'); ios::in);
reset (f); f>>n;
readln(f,n); for (i=1;i<=n;i++) f>>V[i];
for i:=1 to n do f.close();
read(f,V[i]); minHeap();
close(f); for (i=1;i<=n;i++)
minHeap; cout <<V[i]<<" ";
for i:=1 to n do }
write(V[i],' ');
end.

 Problema 9.13. HeapSort. Se citeşte un vector cu n componente numere


naturale. Să se sorteze descrescător.

 Rezolvare. Iniţial, se organizează vectorul ca minHeap. Evident, pe prima


poziţie se va afla valoarea cea mai mică. Se inversează conţinuturile primei poziţii
şi a ultimei poziţii. Aceasta înseamnă că:
 pe ultima poziţie se găseşte valoarea cea mai mică;
 cele n-1 componente rămase au pe prima poziţie o valoare oarecare, dar,
în rest, V[2] şi V[3] sunt organizate ca minHeap-uri.

Se reorganizează vectorul ca minHeap, dar, de data aceasta cu n-1


componente şi se inversează conţinuturile primei componente şi a componentei
n-1. Procedeul se reia pentru vectorul cu n-2 componente, ş.a.m.d.

Varianta Pascal Varianta C++


var V:array[1..20]of integer; #include <fstream.h>
n,i:integer; int V[20],n,i;
f:text;
int indValMin(int i, int n)
function indValMin(i,n:integer) ...
...
void Combinare(int i, int n)
procedure Combinare(i, ...
n:integer);
... void minHeap()
...
procedure minHeap;
...
304 Capitolul 9. Arbori

procedure heapSort; void heapSort()


var man:integer; { int man;
begin minHeap();
minHeap; for (int i=n;i>=1;i--)
for i:= n downto 1 do { man=V[i];
begin V[i]=V[1];
man:=V[i]; V[1]=man;
V[i]:=V[1]; Combinare(1,i-1);
V[1]:=man; }
Combinare(1,i-1); }
end
end; main()
{ fstream f("graf.txt",
begin
ios::in);
assign(f,'graf.txt');
f>>n;
reset (f);
for (i=1;i<=n;i++) f>>V[i];
readln(f,n);
f.close();
for i:=1 to n do
heapSort();
read(f,V[i]);
for (i=1;i<=n;i++)
close(f);
cout<<V[i]<<" ";
heapSort;
}
for i:=1 to n do
write(V[i],' ');
end.

9.7.7. Arbori de căutare

Definiţia 9.12. Se numeşte arbore de căutare un arbore binar în care


fiecare nod are o unică cheie de identificare care respectă condiţiile
următoare:
 orice cheie asociată unui nod al subarborelui stâng este mai mică
decât cheia asociată nodului;
 orice cheie asociată unui nod al subarborelui drept este mai mare
decât cheia asociată nodului.

Alăturat, puteţi observa un arbore de căutare:


7
Asupra unui arbore de căutare se pot efectua
mai multe operaţii. Acestea vor fi prezentate în
continuare. 3 9

Figura 9.32. 8
Exemplu de arbore de căutare
1 4

A) Inserarea. Se dă o valoare oarecare şi adresa unui vârf al unui arbore de


căutare. Se cere să se insereze în arbore un nod care are drept cheie de
identificare valoarea dată. Regula de inserare este următoarea:
Manual de informatică pentru clasa a XI-a 305

Dacă arborele este nevid atunci avem posibilităţile următoare:


 valoarea coincide cu cheia vârfului - se renunţă la inserarea valorii;
 valoarea este mai mică decât cheia asociată vârfului - se reia problema
pentru subarborele stâng;
 valoarea este mai mare decât cheia asociată vârfului - se reia problema
pentru subarborele drept.
Altfel
 se creează nodul care are drept cheie valoarea respectivă. Să observăm
faptul că transmiterea prin referinţă a adresei nodului care va fi inserat
are ca efect legarea acestuia automat de părinte.
Pentru a crea arborele de căutare, se poate folosi în mod repetat operaţia de
inserare. Mai jos, este prezentat subprogramul de inserare:

Varianta Pascal Varianta C++


type ref=^inr; #include <iostream.h>
inr=record struct Nod
nr:integer; {int nr;
as,ad:ref Nod *as,*ad;
end; };
var v,man:ref; Nod *v, *man;
k:integer; char opt;
opt:char; int k;
void Inserare(Nod*& c, int k)
procedure Inserare
{ if (c)
(var c:ref;k:integer);
if (c->nr==k)
cout<<"nr deja inserat "
begin
<<endl;
if c<>nil else
then if (c->nr<k)
if c^.nr=k Inserare (c->ad,k);
then else
writeln('nr deja Inserare (c->as,k);
inserat ') else
else
{ c=new Nod;
if c^.nr<k
c->as=c->ad=0;
then
c->nr=k;
Inserare(c^.ad,k)
}
else
}
Inserare(c^.as,k)
else
begin
new(c);
c^.as:=nil;
c^.ad:=nil;
c^.nr:=k;
end;
end;
306 Capitolul 9. Arbori

B) Căutarea. Se dă o valoare oarecare şi adresa unui vârf al unui arbore de


căutare. Se cere să se decidă dacă există în arbore un nod care are cheia de
identificare egală cu valoarea dată.

Regula de căutare este următoarea:

Dacă arborele este nevid, atunci avem posibilităţile următoare:


 valoarea coincide cu cheia vârfului - valoarea există în arbore;

 valoarea este mai mică decât cheia asociată vârfului - se reia problema
pentru subarborele stâng;

 valoarea este mai mare decât cheia asociată vârfului - se reia problema
pentru subarborele drept.

Altfel
 nu există în arbore o cheie egală cu valoarea dată.

În continuare este prezentat subprogramul Cautare:

Varianta Pascal Varianta C++


procedure Cautare void Cautare
(var c,adr:ref;k:integer); (Nod*& c,Nod*& adr,int k)
begin {
if c<>nil then if (c)
if c^.nr<k if (c->nr<k)
then Cautare(c^.ad,adr,k) Cautare(c->ad,adr,k);
else else
if c^.nr>k then if (c->nr>k)
Cautare(c^.as,adr,k) Cautare(c->as,adr,k);
else adr:=c else adr=c;
else adr:=nil; else adr=0;
end; }

C) Ştergerea. Se dă o valoare oarecare şi adresa unui vârf al unui arbore de


căutare. Dacă există un nod care are cheia egală cu valoarea dată, se cere ca
acest nod să fie şters.

Regula de ştergere este următoarea:

Dacă arborele este nevid atunci avem posibilităţile următoare:


 valoarea coincide cu cheia vârfului - valoarea există în arbore;
 dacă nodul este terminal (subarborii stâng şi drept sunt vizi), acesta
este şters, iar adresa reţinută de părinte pentru el va fi nulă.
 dacă numai subarborele drept este nevid, nodul este şters, iar
părintele lui va reţine, în locul adresei lui, adresa subarborelui drept.
Manual de informatică pentru clasa a XI-a 307

 dacă numai subarborele stâng este nevid, nodul este şters, iar
părintele lui va reţine, în locul adresei lui, adresa subarborelui stâng.
 dacă ambii subarbori sunt nevizi:
 se identifică cel mai din dreapta nod al subarborelui stâng;
 cheia nodului astfel identificat va fi memorată de nodul analizat;
 nodul astfel identificat se şterge, iar ştergerea se efectuează ca în
cazul în care nodul subordonează numai subarborele stâng.
 valoarea este mai mică decât cheia asociată vârfului - se reia problema
pentru subarborele stâng;
 valoarea este mai mare decât cheia asociată vârfului - se reia problema
pentru subarborele drept.
Altfel
 nu există în arbore o cheie egală cu valoarea dată.

Exemple de ştergeri
7 7
1. În arborele din stânga
se şterge nodul 8. Acesta
este nod terminal. 3 9 3 9

1 4 8 1 4
Figura 9.33.

2. În arborele din stânga


se şterge nodul 9. Acesta 7 7
subordonează un singur
subarbore nevid, cel drept.
3 9 3 8

Figura 9.34. 1 4 8 1 4

3. În arborele din stânga


se şterge nodul 3. 7 7
Arborele subordonează 2
arbori nevizi. Cel mai din
dreapta nod din subarbo- 3 9 4 8
rele stâng are cheia 4.
Cheia este copiată la
1 4 8 1
nodul 3, iar nodul astfel
identificat este şters.
Figura 9.35.
308 Capitolul 9. Arbori

4. În arborele din stânga


se şterge nodul 7. În 7 4
subarborele stâng, cel mai
din dreapta nod are cheia
4. Cheia sa va fi reţinută 3 9 3 9
de nodul care avea cheia
7, iar nodul cu cheia 4
este şters. 1 4 8 1 8

Figura 9.36.

Justificăm faptul că ştergerea se efectuează corect în cazul în care nodul


care urmează să fie şters subordonează ambii arbori nevizi. Cheia celui mai din
dreapta nod din subarborele stâng este cea mai mare valoare din subarborele
stâng. Cum cheia a fost găsită în subarborele stâng al nodului, ea este oricum mai
mică decât cheia oricărui nod din subarborele drept. Prin urmare, prin această
operaţie, se conservă proprietatea de arbore de căutare.

Se poate alege şi cheia celui mai din stânga nod din subarborele drept! De
ce? Justificaţi răspunsul!

Vedeţi mai jos subprogramele Sterg şi cmmd. Subprogramul cmmd


efectuează toate operaţiile corespunzătoare cazului de ştergere atunci când nodul
are ambii subarbori (cel stâng şi cel drept) nevizi:

Varianta Pascal Varianta C++


procedure cmmd(var c,f:ref); void cmmd(Nod*& c, Nod*& f)
begin {
if f^.ad<>nil if (f->ad) cmmd(c,f->ad);
then cmmd(c,f^.ad) else
else { c->nr=f->nr;
begin man=f;
c^.nr:=f^.nr; f=f->as;
man:=f; delete man;
f:=f^.as; }
dispose(man); }
end
end; void Sterg(Nod*& c,int k)
{ Nod* f;
procedure Sterg
(var c:ref;k:integer); if (c)
var f:ref; if (c->nr==k)
begin if( c->as==0 && c->ad==0)
if c<>nil then { delete c;
if c^.nr=k then c=0;
if }
(c^.as=nil)and(c^.ad=nil) else
then if (c->as==0)
begin {f=c->ad;
dispose(c); delete c;
c:=nil c=f;
end }
Manual de informatică pentru clasa a XI-a 309

else else
if c^.as=nil then if (c->ad==0)
begin { f=c->as;
f:=c^.ad; delete c;
dispose(c); c:=f; c=f;
end }
else else cmmd(c,c->as);
if c^.ad=nil then else
begin if (c->nr<k)
f:=c^.as; Sterg(c->ad,k);
dispose(c); c:=f else Sterg(c->as,k);
end
else
else cmmd(c,c^.as)
else cout<<"numar absent
if c^.nr<k - tentativa esuata ";
then sterg(c^.ad,k) }
else sterg(c^.as,k)
else
writeln('numar absent
-tentativa esuata ')
end;

D) Listarea. Informaţia se poate lista utilizând oricare dintre metodele cunoscute


pentru parcurgerea arborilor. Dacă dorim listarea informaţiilor în ordinea strict
crescătoare a cheilor, se utilizează metoda stânga-vârf-dreapta (inordine). De ce?
Justificaţi.

Varianta Pascal Varianta C++


procedure Parcurg(c:ref); void Parcurg(Nod* c)
begin { if (c)
if c<>nil then { Parcurg(c->as);
begin cout<<c->nr<<endl;
Parcurg(c^.as); Parcurg(c->ad);
writeln(c^.nr); }
Parcurg(c^.ad) }
end;
end;

Puteţi testa subprogramele de mai sus cu ajutorul programului principal (în


Pascal) sau a funcţiei main() (în C++).

Varianta Pascal Varianta C++


begin main()
v:=nil; {v=0;
repeat do
write('optiunea '); {cout<<"optiunea ";
readln(opt); cin>>opt;
case opt of switch (opt)
'i': begin {case 'i':
write('k='); readln(k); cout<<"k="; cin>>k;
Inserare(v,k) Inserare (v,k);
end; break;
310 Capitolul 9. Arbori

'l': Parcurg(v); case 'l':


'c': begin Parcurg(v); break;
write ('k=');readln(k); case 'c':
Cautare(v,man,k); cout<<"k="; cin>>k;
if (man<>nil) Cautare(v,man,k);
then writeln(k) if (man)
else cout<<k<<endl;
writeln('Nu exista else
acest nod'); cout<<"nu exista
end; acest nod"<<endl;
's': begin break;
write(' se va sterge case 's':
numarul '); cout<<"se va sterge
readln(k); numarul ";
sterg(v,k) cin>>k;
end; Sterg(v,k);
end {case} break;
until opt='t' }
end. } while (opt!='t');
}

Teoria regăsirii informaţiei este deosebit de importantă pentru viaţa practică.


Aceasta conduce la o căutare rapidă (puţine comparaţii).

Probleme propuse
1. Stabiliţi valoarea de adevăr a propoziţiilor următoare:
a) Dacă un graf neorientat cu n noduri are n-1 muchii, atunci el este arbore.
b) Orice arbore este un graf conex.
c) Orice graf neorientat conex este arbore.
d) Orice graf neorientat care nu conţine cicluri este o pădure.
e) Orice pădure este un graf conex fără cicluri.
f) O partiţie a mulţimii {1,2,...,n} poate fi reprezentată ca o pădure.
g) O pădure poate fi dată sub forma unei partiţii.
h) Orice arbore este graf bipartit.
i) Orice graf bipartit este arbore.
2. Căte cicluri elementare are un graf complet cu n noduri?
3. Un arbore cu 6 noduri este reprezentat cu ajutorul matricei de adiacenţă. Câte
cifre de 0 conţine aceasta?
4. Fie un arbore cu 5 noduri. Care dintre şirurile de mai jos poate reprezenta şirul
gradelor nodurilor acestui arbore?

a) 1 1 1 1 0; b) 1 2 0 0 2; c) 2 4 0 1 1; d) 2 3 1 1 1.

5. Care este înălţimea maximă pe care o poate avea un arbore cu 9 noduri?

6. Care este înălţimea minimă pe care o poate avea un arbore cu 9 noduri?


Manual de informatică pentru clasa a XI-a 311

7. Scrieţi un program care, pornind de la un arbore cu referinţe descendente


memorat în HEAP, returnează acelaşi arbore reprezentat cu referinţe ascendente.
Pentru arborele dat, se cunoaşte faptul că orice nod are cel mult 3 succesori.
8. Se dă un arbore cu referinţe descendente memorat în HEAP. Se cere un
program care, citind pe k, afişează numărul de noduri aflat pe nivelul k.
9. Scrieţi un program care, pornind de la un arbore reprezentat cu referinţe
ascendente, construieşte arborele reprezentat cu referinţe ascendente, în HEAP.
10. Se dau n calculatoare. Se cunoaşte distanţa între oricare două calculatoare. Se
cere să se găsească o soluţie astfel încât acestea să fie legate între ele, iar lungimea
totală a cablului utilizat să fie minimă. Datele se găsesc în fişierul text retea.in şi
sunt organizate astfel:
- linia 1, n;
- următoarele n(n-1)/2 linii ale fişierului conţin fiecare trei valori naturale i, j, k,
cu semnificaţia: distanţa între calculatorul i şi calculatorul j este de k metri.

Pentru ca două calculatoare să poată comunica este necesar ca ele să fie


legate, dar nu în mod obligatoriu direct.
11. Se citeşte un arbore dat sub forma legăturii de tip TATA. Nodurile sale sunt 1,
2, ..., n. Poate fi el memorat sub forma unei liste liniare? În caz afirmativ, să se
creeze lista şi să se afişeze.

Exemplu: arborele de mai jos este reprezentat sub forma unei liste:

2 4
3 2 1 4 5

3
5 Figura 9.37. Exemplu

12. Se citeşte un arbore cu nodurile 1, 2, ..., n şi se memorează sub forma listelor de


adiacenţă. De asemenea, se citeşte un nod care va fi considerat vârf. Se cere să se
afişeze numărul de niveluri ale arborelui.
13. Se citeşte un arbore care este introdus prin legătura de tip TATA. Nodurile sale
sunt 1, 2, ..., n. Se cere să se afişeze nodurile sale terminale.
14. Se citesc doi arbori, unde fiecare are nodurile 1, 2, ..., n şi sunt reprezentaţi prin
legătura de tip TATA. Se cere să se decidă dacă ei reprezintă unul şi acelaşi arbore.
15. Scrieţi o funcţie care returnează numărul de niveluri ale unui arbore binar
memorat cu ajutorul a doi vectori.
16. Scrieţi o funcţie care copiază un arbore binar memorat în HEAP şi care are
adresa vârfului în v şi obţine un altul, memorat tot în HEAP, care are adresa în v1.
312 Capitolul 9. Arbori

17. Scrieţi o funcţie care eliberează memoria din HEAP ocupată de un arbore binar.
18. Scrieţi o funcţie care afişează nodurile unui arbore binar memorat în HEAP în
ordinea obţinută în urma parcurgerii acestuia în lăţime.
19. Scrieţi o funcţie care listează nodurile de pe nivelul k ale unui arbore binar
memorat în HEAP.
20. Scrieţi o funcţie care listează nodurile de pe nivelul k ale unui arbore binar
memorat cu ajutorul a doi vectori.
21. Scrieţi o funcţie care listează nodurile terminale ale unui arbore binar memorat în
HEAP.
22. Scrieţi o funcţie care listează nodurile terminale ale unui arbore binar memorat cu
ajutorul vectorilor.
23. Numărarea arborilor binari. Autoinstruire.
23.1. Se citeşte n, număr natural. În câte feluri se pot aşeza în linie n litere P şi n
litere S?
Exemplu. Pentru n=2, avem: PPSS, SSPP, SPSP, SPPS, PSSP, PSPS. Se va afişa 6.

 Indicaţie. Dacă fiecare literă ar fi distinctă, am avea (2n)! posibilităţi de a aşeza


literele. Întrucât pentru fiecare permutare nu contează dacă s-a inversat P cu P,
(2n)! se împarte la n! şi pentru că nu contează dacă s-a inversat S cu S, împărţim
din nou la n!. Astfel, obţinem:
(2n) ! = Cn
2n .
n!⋅n!
23.2. La fel ca la 23.1., dar se cere numărul secvenţelor cu n+1 litere P şi n-1
litere S.

 Răspuns. Printr-un raţionament asemănător, se obţine: C n2n-1.


23.3. Se citeşte n, număr natural. Fie permutarea 1,2, ..., n şi o secvenţă cu n litere
S şi n litere P. Se presupune existenţa unei stive. Secvenţa de litere se interpretează
ca 2n comenzi de scoatere din stivă (S) sau punere în stivă (P).
Exemplu. Pentru n=3, permutarea este 1 2 3, iar secvenţa este PPSPSS. Executăm
comenzile:
P 1 se pune în stivă: rămâne 2 3. 1

2
P 2 se pune în stivă: rămâne 3.
1

S 2 se scoate din stivă. 1 2


Manual de informatică pentru clasa a XI-a 313

P 3 se pune în stivă. 1 2

S 3 se scoate din stivă. 1 3 2

S 1 se scoate din stivă. 0 1 3 2

Astfel, din permutarea iniţială 1 2 3 am obţinut permutarea 1 3 2 - ca în


cazul manevrelor de vagoane.
Se cere ca programul dvs. să decidă dacă secvenţa de comenzi este admisibilă, iar
în caz afirmativ, să se tipărească permutarea care se obţine executând secvenţa.
Indicaţie. Ca o secvenţă să fie admisibilă, este necesar ca pentru fiecare k între 1
şi 2n, secvenţa 1...k să conţină un număr de comenzi P mai mare sau egal decât
numărul de comenzi S (altfel, stiva este vidă şi nu se poate executa S).

23.4. Se citeşte n, număr natural. Se cere să se listeze toate permutările mulţimii 1,


2, ..., n care se pot obţine din permutarea identică, ca în problema anterioară.

23.5. Se citeşte n. Câte permutări afişează programul anterior?


Rezolvare. În nici un caz nu le numărăm. Am aştepta cam mult... O demonstraţie
surprinzătoare a fost obţinută în 1878 de D. Andree.
n
⇒ Numărul total de secvenţe este: C 2n .

⇒ Fie o secvenţă inadmisibilă. Fie k, minim, astfel încât secvenţa 1...k să fie
inadmisibilă.
Exemplu: PSPSSP. Aici, k=5.

⇒ În secvenţa 1...k toate comenzile P devin comenzi S şi toate comenzile S


devin comenzi P (pentru exemplul dat, obţinem SPSPPP). În acest fel, am
obţinut o secvenţă cu n-1 comenzi S şi n+1 comenzi P.

⇒ Fie acum o secvenţă cu n-1 comenzi S şi n+1 comenzi P. Căutăm k minim


astfel încât secvenţa 1...k să conţină mai multe comenzi P decât comenzi S.
Exemplu: pentru SPSPPP, k=5. Inversăm P cu S în secvenţa 1...k. Pentru
exemplul dat, avem: PSPSSP.

⇒ Să observăm că orice secvenţă cu n-1 comenzi S şi n+1 comenzi P se


transformă într-o secvenţă inadmisibilă prin procedeul indicat (asta pentru că în
secvenţa obţinută există mai multe comenzi S decât P).
314 Capitolul 9. Arbori

⇒ Prin urmare, am stabilit o bijecţie de la mulţimea secvenţelor inadmisibile la cea a


secvenţelor care au n-1 comenzi S şi n+1 comenzi P. Deoarece cele două mulţimi
sunt finite, înseamnă că au acelaşi număr de elemente. Dar numărul secvenţelor
cu n-1 comenzi S şi n+1 comenzi P îl cunoaştem. Vezi 23.2. De aici rezultă că
numărul secvenţelor admisibile cu n comenzi S şi n comenzi P este:
1
C n2n − C n2n−1 = Cn .
n + 1 2n
23.6. Se citeşte n, număr natural. În câte feluri se pot aşeza n perechi de
paranteze () într-o expresie aritmetică astfel încât expresia să aibă sens?
Indicaţie. Problema este identică cu cea anterioară dacă înlocuim P cu ( şi S cu ).
Prin urmare, rezultatul este cel de mai sus.
23.7. Fie A mulţimea arborilor binari şi P mulţimea secvenţelor admisibile de n
perechi de paranteze, ca la problema anterioară. Fiecărui arbore binar din A îi
ataşăm o secvenţă admisibilă de paranteze după formula:
vârf=([subarbore stâng])[(subarbore drept)]
Ce este trecut între paranteze drepte este facultativ (pentru cazul în care subarborii
stâng sau drept lipsesc). Exemplu: reprezentăm toţi arborii binari cu trei noduri:

(())() ()(())

((()))

()()()

(()())

Figura 9.38. Reprezentarea arborilor binari cu trei noduri

Să se scrie un program care citeşte un arbore binar cu n noduri reprezentat cu


ajutorul vectorilor şi tipăreşte succesiunea admisibilă de paranteze.
23.8. Care este numărul arborilor binari cu n noduri, făcând abstracţie de
numerotarea acestora? Indicaţie. Se demonstrează că la 23.7. am definit o funcţie
F:A→P, bijectivă. De aici rezultă că numărul acestora este:
1
Cn2n .
n +1
Manual de informatică pentru clasa a XI-a 315

24. Un arbore oarecare, de vârf dat, poate fi reţinut ca un arbore binar astfel: pentru
fiecare nod i, vârful subarborelui stâng este dat de primul descendent al său (în
ordinea de la stânga la dreapta), iar vârful subarborelui drept, de primul nod aflat pe
acelaşi nivel, în aceeaşi ordine.
Exemplu. Arborele oarecare din figura 9.39, a) se reprezintă ca arbore binar, asa
cum rezultă din figura b) şi c).

1 1

2
2 3 7

3
4 5 6
4 7
a)

5
1

6
2 3 7
c)

4 5 6

b)
Figura 9.39. Exemplu

Citiţi un arbore oarecare într-o reprezentare convenabilă şi memoraţi-l în


HEAP ca arbore binar. Invers, porniţi de la un arbore binar memorat în HEAP şi
reprezentaţi-l ca un arbore oarecare, reprezentat într-o structură convenabilă.
25. Refaceţi programul care implementează algoritmul lui Kruskal prin organizarea
intrărilor sub formă de MinHeap.

Răspunsuri

1. a) F; b) A; c) F; d) A; e) A; f) A; g) F; h) A; i) A.

n(n − 1) (n − 1)(n − 2)
2. − (n − 1) = .
2 2

3. Suma gradelor este 2(n-1) = 10 = suma cifrelor de 1 din matrice. Numărul


de cifre 0 este 6*6-10 =26. 4. d); 5. 8; 6. 1.
316

Anexa 1
Aplicaţii practice ale grafurilor

Cu siguranţǎ, unii dintre voi v -aţi pus o serie de întreb


ǎri referitoare la
aplicabilitatea teoriei grafurilor în problemele reale:

• unde pot utiliza grafurile şi de ce?


• existǎ aplicaţii din alte domenii, în afarǎ de Informaticǎ, ce pot fi
rezolvate cu ajutorul teoriei grafurilor?
În fapt, dupǎ cum veţi vedea în continuare, grafurile sunt foarte utile într -o
multitudine de aplicaţii din diverse domenii, iar prin utilizarea lor, se poate obţine
o bunǎ optimizare a resurselor (umane sau materiale) sau a timpului.

A.1. Reţele de comunicaţie

Comunicaţia între diversele dispozitive electronice din zilele noastre reprezintă


poate cea mai răspândită aplicaţie practică a teoriei grafurilor. Spre exemplu, dacă
ne referim la reţelele de calculatoare sau la Internet şi dacă considerăm fiecare
calculator ca fiind un nod, atunci vom avea un graf extrem de complex şi foarte
diversificat din punct de vedere al structurii. În continuare, vom prezenta o schemă
de principiu care descrie o reţea de calculatoare, legată la Internet:

Internet

Router

Switch 1 Switch 2

Subreţeaua 1 Subreţeaua 2

Figura A.1. Exemplu de reţea de calculatoare legată la Internet


Manual de informatică pentru clasa a XI-a 317

Observaţii

 Structura anterioară este de tip arbore. Pe fiecare nivel însă, protocoalele de


comunicaţie efectuează operaţii specifice pentru asigurarea transmisiei
bidirecţionale între fiecare dispozitiv terminal (calculator).
 Router-ul este un dispozitiv electronic care decide calea (drumul optim) pe
care vor fi trimise informaţiile de la un calculator din Subreţeaua 1, către un
altul din Subreţeaua 2. La nivel local, Switch-ul decide la rândul său, în
funcţie de adresa MAC (Media Access Control, identificator unic pe glob) a
fiecărei plăci de reţea, cărui destinatar îi este dedicat blocul de date. Pentru
a se conecta la reţeaua Internet, Router-ul are o legătură cu un ISP
(Internet Service Provider).
 Există o întreagă teorie legată de reţelele de calculatoare, dar ceea ce este
însă de reţinut este faptul că din punct de vedere topologic, o reţea de
calculatoare se poate reprezenta sub forma unui graf. Comunicaţia optimă
(calea cea mai scurtă între două noduri) este realizată cu ajutorul
protocoalelor specializate de routare, cum ar fi: IP (Internet Protocol), NAT
(Network Address Translation), RIP (Routing Information Protocol), etc.

Protocoale de routare. Un protocol de routare are rolul de a obţine şi de a trimite


informaţiile topologice ale reţelei către Router-e, permiţându-le acestora să ia
decizii la nivel local. Fiecare Router deţine o serie de liste, numite tabele de
routare, în care sunt memorate adresele (fizice şi logice) tuturor nodurilor care au
legătură fizică directă cu el şi drumurile optime deja cunoscute şi parcurse. Aceste
liste trebuie reactualizate frecvent pentru a preveni anumite modificări topologice
ale reţelei.

Router-ele utilizează protocoalele de comunicaţie care au la bază algoritmi


de optimizare ce trebuie să determine cea mai bună cale. Când ne referim la
drumul cel mai bun, avem în vedere numărul de “hopuri” (din engleză, ”hops”) pe
care trebuie să le parcurgă datele până la destinaţie sau un alt punct intermediar
sau durata/viteza de trimitere a informaţiilor. Există două tipuri de algoritmi de
routare mai importante, utilizate în funcţie de modalitatea router-ului de a reţine şi
de a analiza informaţiile structurale ale reţelei:
• Algoritmi de routare globali. Fiecare router reţine toate informaţiile
despre celelalte router-e existente în reţea şi despre trafic. Când se
porneşte un astfel de router, el trimite un mesaj către toate celelalte
router-e din reţea, fără a cunoaşte în prealabil destinatarii (mesaj de tip
broadcast). Fiecare router îi va răspunde cu un mesaj în care va ataşa
adresa IP a sa, identificându-se astfel. Se face apoi un test prin care se
analizează timpul de răspuns, trimiţându-se un mesaj de tip echo
(“ecou”) către router-ele determinate anterior. Răspunsul primit de la
fiecare este reţinut pentru a fi utilizat în continuare. Algoritmul de
determinare a drumului minim între oricare două noduri ale reţelei (de
exemplu, se poate utiliza Dijkstra) este apoi aplicat, considerându-se
pentru fiecare legătură un cost ce depinde de timpul de răspuns, media
traficului sau, mai simplu, numărul de noduri intermediare. Astfel,
318 Anexa 1 - Aplicaţii practice ale grafurilor

dispozitivul obţine o “hartă” a reţelei pe care o reţine apoi în tabelul său


de routare. În cazul unei reţele de dimensiuni foarte mari, un algoritm de
acest tip funcţionează corect, dar poate încetini traficul, scăzând astfel
eficienţa reţelei.

• Algoritmi de routare descentralizaţi. Router-ele ce au implementate


un astfel de algoritm reţin informaţiile doar despre nodurile legate în mod
direct (adiacente). Astfel, router-ul memorează costul fiecărei legături
directe şi la o anumită perioadă de timp, face schimb de tabele cu
celelalte router-e, reactualizându-şi astfel informaţiile. De exemplu, dacă
avem trei router-e legate în serie:

L1 L2

Router 1 Router 2 Router 3


Figura A.2. Exemplu de reţea

în cazul în care Router 1 trebuie să trimită date către Router 3,


informaţiile vor trece automat prin Router 2. Când pachetele de date
ajung la Router 2, el verifică lista sa de routare şi decide cum să trimită
pachetele de date spre destinaţie.
Problemele reale pe care le întâmpină reţelele de calculatoare se datorează
numărului mare de dispozitive (noduri) din reţea. Cu cât această valoare este mai
mare, cu atât numărul de calcule efectuate la nivel de router este mai mare. Astfel,
se poate implementa virtual o ierarhizare a reţelei, împărţindu-se pe regiuni.
Fiecare router deţine informaţii doar despre toate router-ele din regiunea sa.
Legătura cu celelalte regiuni se face prin anumite router-e, ca un fel de “porţi” de
ieşire spre exterior. Astfel, un router dintr-o regiune nu reţine nici o informaţie
despre un altul dintr-o altă regiune, ci doar calea către acea regiune.

A.2. Instrumente de management economic

Proiectele şi situaţiile economice determinate de punerea în


practică a acestora, presupun efectuarea unor activităţi interco-
nectate, care pot fi modelate prin intermediul grafurilor.
Managementul informatic al proiectelor permite gestiunea, coordonarea,
planificarea şi controlul resurselor astfel încât obiectivele propuse să se atingă în
mod optim şi la timp.
O aplicaţie foarte răspândită a grafurilor orientate o constituie simularea
proiectelor complexe ce presupun o multitudine de activităţi distincte, efectuate în
serie sau în paralel. Teoria grafurilor vine în ajutorul oricărui analist de proiect prin
modelarea acestor activităţi, prin structurarea grafică a dependenţelor dintre ele şi
prin determinarea timpului necesar de realizare a proiectului.
Manual de informatică pentru clasa a XI-a 319

În evaluarea oricărui proiect este necesară cunoaşterea timpului maxim de


execuţie a întregii lucrări. Acesta reprezintă drumul cel mai lung de la faza iniţială
la faza finală a proiectului şi este numit drum critic.

Un graf de activităţi este un graf asociat unei lucrări complexe a cărei


realizare presupune desfăşurarea mai multor acţiuni (procese, activităţi). Un astfel
de graf presupune două tipuri de componente:

 arcele – reprezintă activităţile sau etapele elementare ale lucrării, iar


lungimea asociată unui arc semnifică timpul de desfăşurare al activităţii.
Exemple: proiectarea unei componente, implementarea unui algoritm, etc. În
cadrul unui proiect, activităţile se pot efectua:
- în serie: o activitate nu poate începe până când alta nu a fost terminată;
- în paralel: mai multe activităţi desfăşurate în acelaşi timp.

 nodurile – reprezintă evenimente care pot fi interpretate ca indicând


realizarea unor obiective parţiale ale lucrării; ele sunt un punct de verificare
al evoluţiei lucrării. Exemple: terminarea etapei de analiză, sosirea
materialelor de construcţie, terminarea unor teste, etc.

Proiectul este format dintr-o serie de activităţi (şi evenimente), efectuate


într-o anumită perioadă de timp (cu un început şi un sfârşit definit). La final,
rezultatul este scopul pentru care a fost dezvoltat acel proiect.

Numim drum critic al unui graf de activităţi un drum de lungime maximă


care leagă nodul iniţial de cel final. Drumul critic reuneşte activităţi a căror
întârziere duce la întârzierea realizării întregului proiect, de aceea trebuie
supravegheate cu mare atenţie. Activităţile şi evenimentele ce formează drumul
critic poartă şi ele denumirea de critice.

Figura A.3. Exemplu de graf de activităţi

În figura A.3, drumul critic este format din nodurile: 1, 2, 7, 5 şi 6. Timpul de


terminare al proiectului este de 21 de unităţi (măsura de unitate a costului). De
altfel, nu există în mod obligatoriu un singur drum critic. Sunt cazuri în care graful
conţine mai multe drumuri critice, însă cu suma ponderilor arcelor egală. În
Capitolul 9 aţi studiat grafurile şi algoritmul lui Roy-Floyd, metodă ce permite
determinarea drumului maxim într-un graf. Această tehnică se poate implementa
cu succes pentru a detecta drumul critic într-un graf de activităţi.
320 Anexa 1 - Aplicaţii practice ale grafurilor

Având cunoscut drumul critic pentru un graf asociat unui proiect, se pot
analiza în detaliu anumite aspecte particulare ale fiecărui eveniment sau activitate.
Dorim să cunoaştem cum se pot derula celelalte activităţi, care nu sunt critice, în
funcţie de durata drumului critic. Astfel, au fost introduse câteva noţiuni teoretice,
ce vor fi prezentate în continuare.

Se consideră un graf de activităţi, pentru care notăm cu Vi (vârfurile)


evenimentele şi cu A[i,j] (arcul de la Vi la Vj) activităţile. Vom defini:
ti - data aşteptată a unui eveniment Vi ca fiind drumul cel mai lung de la
V1 la Vi (cea mai mare distanţă);
ti* - data limită a unui eveniment Vi ca fiind diferenţa între tn (data
aşteptată a lui Vn) şi drumul maxim de la Vi la Vn.

Să revenim la exemplul din figura A.3. Pentru evenimentul 4, vom avea data
aşteptată egală cu 10 (5+2+3) unităţi, iar data limită, egală cu 16 (21-5) unităţi.
Putem astfel considera că evenimentul 4 trebuie să fie atins după 10 unităţi
temporale, iar în cazul unei întârzieri, atingerea sa nu poate să dureze cu mai mult
de 6 (16-10) unităţi faţă de data sa aşteptată de terminare.

Cele două valori asociate evenimentului Vi determină un interval de


fluctuaţie, notat cu [ti, ti*], ce specifică perioada de timp în care poate avea loc
evenimentul Vi, fără a schimba timpul total asociat proiectului (drumul critic).

În urma unor calcule uşoare, se poate observa că pentru toate evenimentele


ce aparţin drumului critic, ti = ti*.

Considerând cunoscute toate datele aşteptate şi cele limită pentru graf,


definim în continuare două noţiuni privitoare la arce:
ML[i,j] – marginea liberă a unei activităţi, ca fiind tj-ti-d(A[i,j]), ce
semnifică durata cu care se poate întârzia începerea activităţii A[i,j], fără a
modifica data de aşteptare a evenimentului Vj;
MT[i,j] – marginea totală a unei activităţi, ca fiind tj*-ti-d(A[i,j]),
ce semnifică durata cu care se poate întârzia începerea activităţii A[i,j], fără a
modifica data limită a evenimentului Vj.

Arcele ce formează drumul critic au aceste două valori nule (nu le este
permisă nici o întârziere).
Intervalul de fluctuaţie permite managerului de proiect să utilizeze resursele,
echipamentele şi utilajele rămase libere pentru a ajuta alte activităţi şi implicit
pentru a micşora durata de efectuare a întregului proiect (în cazul în care se poate
realiza acest lucru).
Grafurile de activităţi sunt extrem de utile în evaluarea lucrărilor complexe, iar
reprezentarea lor permite analistului de proiect o viziune de ansamblu şi totodată, o
modalitate prin care poate testa o multitudine de variante, înainte de a o alege pe cea
considerată optimă. De asemenea, soft-urile specializate ce oferă metode complexe
de analiză, utilizează cu succes metode de optimizare ca cea a “Drumului Critic“.

S-ar putea să vă placă și