Sunteți pe pagina 1din 316

Vlad Huţanu Tudor Sorin

INFORMATICĂ

(filiera teoretică, profilul real, specializarea


matematică-informatică) şi
(filiera vocaţională, profil militar MApN, specializarea
matematică-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ă : manual pentru ciclul superior al liceului : clasa a XI-
a - (filiera teoretică, profilul real, specializarea matematică-informatică)
şi (filiera vocaţională, profil militar MApN, specializarea matematică-
informatică) / Vlad Huţanu, Tudor Sorin. - Bucureşti : Editura L & S Soft,
2006
ISBN (10) 973-88037-1-3; ISBN (13) 978-973-88037-1-8

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. Tablouri ………………………………………………………… 7
1.1. Noţiunea de tablou ………………………………………………………………….. 7
1.2. Cum citim şi cum afişăm un tablou bidimensional?……………………………... 8
1.3. Aplicaţii cu tablouri bidimensionale………………………………………………..10
Probleme propuse……………………………………………………………………….. 16
Răspunsurile la testele grilă……………………………………………………………..19

Capitolul 2. Subprograme ………………………………………………… 20


2.1. Noţiunea de subprogram …………………………………………………………. 20
2.2. Subprograme în Pascal................................................................................... 22
2.2.1. Un exemplu de utilizare a funcţiilor..................................................... 22
2.2.2. Un exemplu de utilizare a procedurilor............................................... 24
2.2.3. Structura unui subprogram................................................................. 25
2.2.3.1. Structura subprogramelor de tip funcţie.............................. 25
2.2.3.2. Structura subprogramelor de tip procedură........................ 26
2.2.4. Definirea şi declararea unui subprogram............................................ 27
2.2.5. Apelul subprogramelor........................................................................ 30
2.2.5.1. Apelul funcţiilor.................................................................... 30
2.2.5.2. Apelul procedurilor.............................................................. 31
2.2.5.3. Transmiterea parametrilor la apel....................................... 31
2.2.5.4. Cum memorează subprogramele parametrii trimişi?.......... 33
2.2.5.5. Transmiterea parametrilor prin valoare............................... 33
2.2.5.6. Transmiterea parametrilor prin referinţă.............................. 35
2.2.6. Variabile locale şi globale................................................................... 36
2.2.7. Greşeli frecvente................................................................................. 38
2.2.8. Unităţi de program............................................................................... 39
2.3. Subprograme în C++....................................................................................... 42
2.3.1. Exemple de utilizare a funcţiilor.......................................................... 42
2.3.2. Structura unei funcţii........................................................................... 44
2.3.3. Declararea variabilelor........................................................................ 46
2.3.4. Transmiterea parametrilor.................................................................. 49
2.3.5. Definirea şi declararea unui subprogram............................................ 53
2.4. Aplicaţii care folosesc subprograme................................................................ 55
Probleme propuse……………………………………………………………………….. 62
Răspunsuri...………………….………………………………………………………….. 72

Capitolul 3. Şiruri de caractere …………………………………………… 73


3.1. Generalităţi …………………………………………………………………………. 73
3.2. Şiruri de caractere în Pascal……………………………………………………….74
3.2.1. Noţiuni introductive............................................................................. 74
3.2.2. Concatenarea şirurilor........................................................................ 76
4 Cuprins

3.2.3. Compararea şirurilor........................................................................... 77


3.2.4. Lungimea şirurilor de caractere.......................................................... 79
3.2.5. Subşiruri.............................................................................................. 80
3.2.6. Conversii de la şiruri la valori numerice şi invers................................ 84
3.2.7. Citirea şi scrierea datelor de tip String din şi în fişiere text................. 88
3.3. Şiruri de caractere în C++…………………………………………………………. 89
3.3.1. Generalităţi………............................................................................... 89
3.3.2. Citirea şi scrierea şirurilor de caractere.............................................. 89
3.3.3. Tipul char*………………………………............................................... 92
3.3.4. Lungimea unui şir de caractere.......................................................... 93
3.3.5. Copierea şi concatenarea şirurilor de caractere................................. 94
3.3.6. Căutarea unui caracter într-un şir....................................................... 95
3.3.7. Compararea şirurilor........................................................................... 97
3.3.8. Subşiruri.............................................................................................. 99
3.3.9. Alte funcţii utile în prelucrarea şirurilor.............................................. 101
3.3.10. Conversia şirurilor în valori numerice şi invers................................ 104
3.3.11. Citirea şi scrierea şirurilor de caractere din şi în fişiere text............ 108
3.3.11.1. Operaţia de citire............................................................. 108
3.3.11.2. Operaţia de scriere.......................................................... 109
3.3.12. O modalitate de conversie de la şir la alt tip.................................... 109
Probleme propuse……………………………………………………………………… 110

Capitolul 4. Structuri de date neomogene …………………………… 112


4.1. Noţiuni introductive……………………………………………………………….. 112
4.2. Structuri neomogene în Pascal………………………………………………….. 112
4.2.1. Tipul Record...…............................................................................... 112
4.2.2. Accesul simplificat la câmpuri........................................................... 114
4.2.3. Înregistrări imbricate.......................................................................... 115
4.2.4. Vectori de înregistrări........................................................................ 115
4.2.5. Înregistrare cu variante...................................................................... 116
4.3. Structuri neomogene în C++….…………………………………………………. 118
4.3.1. Tipul struct...….................................................................................. 118
4.3.2. Înregistrări imbricate.......................................................................... 120
4.3.3. Înregistrări cu structură variabilă....................................................... 121
Probleme propuse……………………………………………………………………… 123

Capitolul 5. Structuri de date …………………………………………… 124


5.1. Conceptul de structură de date………………………………………………….. 124
5.2. Structura de tip listă liniară………………………………………………………..126
5.2.1. Prezentarea structurii........................................................................ 126
5.2.2. Liste alocate secvenţial..................................................................... 127
5.2.3. Liste alocate înlănţuit......................................................................... 128
5.2.4. Implementarea alocării înlănţuite prin utilizarea vectorilor.................129
5.3. Structura de tip stivă…………………………………………………………..….. 133
5.4. Structura de tip coadă……….………………………………………………..….. 138
Probleme propuse……………………………………………………………………… 138
Răspunsuri...………………….………………………………………………………… 140
Manual de informatică pentru clasa a XI-a 5

Capitolul 6. Introducere în recursivitate ……………………………… 141


6.1. Prezentare generală ………………………………………………………………141
6.2. Modul în care se realizează autoapelul….………………………………………141
6.2.1. Realizarea autoapelului în Pascal..................................................... 141
6.2.2. Realizarea autoapelului în C++......................................................... 142
6.3. Mecanismul recursivităţii….……………………………………………………… 143
6.4. Cum gândim un algoritm recursiv?……...……………………………………….147
6.5. Aplicaţii recursive……...………………………………..………………………… 148
6.5.1. Aplicaţii la care se transcrie o formulă recursivă............................... 148
6.5.2. Aplicaţii la care nu dispunem de o formulă de recurenţă.................. 153
Probleme propuse……………………………………………………………………… 159
Indicaţii / Rezolvări…………….………………………………………………………. 166

Capitolul 7. Metoda Divide et Impera ………………………………… 172


7.1. Prezentare generală ………………………………………………………………172
7.2. Aplicaţii ……………………………………………………………………………..172
7.2.1. Valoarea maximă dintr-un vector...................................................... 172
7.2.2. Sortarea prin interclasare................................................................. 174
7.2.3. Sortarea rapidă................................................................................. 176
7.2.4. Turnurile din Hanoi........................................................................... 179
7.2.5. Problema tăieturilor.......................................................................... 180
7.3. Fractali …………………………………………………………………………….. 183
7.3.1. Elemente de grafică.......................................................................... 183
7.3.1.1. Generalităţi (varianta Pascal)............................................ 183
7.3.1.2. Generalităţi (varianta C++)............................................... 185
7.3.1.3. Setarea culorilor şi procesul de desenare (Pascal şi C++)... 186
7.3.2. Curba lui Koch pentru un triunghi echilateral.................................... 188
7.3.3. Curba lui Koch pentru un pătrat........................................................ 191
7.3.4. Arborele.............................................................................................193
Probleme propuse……………………………………………………………………… 195
Răspunsuri…………….……………………………………………………………….. 196

Capitolul 8. Metoda Backtracking ……………………………………… 199


8.1. Prezentarea metodei …………………………………………………………….. 199
8.1.1. Când se utilizează metoda backtracking?........................................ 199
8.1.2. Principiul ce stă la baza metodei backtracking................................. 199
8.1.3. O modalitate de implementare a metodei backtracking.................... 201
8.1.4. Problema celor n dame..................................................................... 204
8.2. Mai puţine linii în programul sursă………………………………………………. 207
8.3. Cazul în care se cere o singură soluţie. Ex.: problema colorării hărţilor……. 210
8.4. Aplicaţii ale metodei backtracking în combinatorică…………………………... 212
8.4.1. O generalizare utilă........................................................................... 212
8.4.2. Produs cartezian............................................................................... 213
8.4.3. Generarea tuturor submulţimilor unei mulţimi................................... 215
8.4.4. Generarea combinărilor.................................................................... 217
6 Cuprins

8.4.5. Generarea aranjamentelor................................................................ 219


8.4.6. Generarea tuturor partiţiilor mulţimii {1,2, ..., n}................................ 221
8.5. Alte tipuri de probleme care se rezolvă prin utilizarea metodei backtracking…. 223
8.5.1. Generalităţi....................................................................................... 223
8.5.2. Generarea partiţiilor unui număr natural........................................... 224
8.5.3. Plata unei sume cu bancnote de valori date..................................... 226
8.5.4. Problema labirintului......................................................................... 228
8.5.5. Problema bilei................................................................................... 231
8.5.6. Săritura calului.................................................................................. 233
Probleme propuse……………………………………………………………………… 235
Indicaţii…………….……………………………………………………………………. 238

Capitolul 9. Grafuri …………………………………………………………239


9.1. Grafuri neorientate…………………………………………………………….….. 239
9.1.1. Introducere........................................................................................ 239
9.1.2. Definiţia grafului neorientat............................................................... 240
9.1.3. Memorarea grafurilor......................................................................... 242
9.1.4. Graf complet...................................................................................... 247
9.1.5. Graf parţial, subgraf.......................................................................... 248
9.1.6. Parcurgerea grafurilor neorientate.................................................... 250
9.1.6.1. Parcurgerea în lăţime (BF – bredth first)........................... 250
9.1.6.2. Parcurgerea în adâncime (DF – depth first)...................... 253
9.1.6.3. Estimarea timpului necesar parcurgerii grafurilor.............. 255
9.1.7. Lanţuri............................................................................................... 255
9.1.8. Graf conex......................................................................................... 259
9.1.9. Componente conexe......................................................................... 260
9.1.10. Cicluri.............................................................................................. 262
9.1.11. Arbori............................................................................................... 264
9.1.11.1. Noţiunea de arbore......................................................... 264
9.1.11.2. Noţiunea de arbore parţial............................................... 266
9.2. Grafuri orientate……………………………………………………………….….. 267
9.2.1. Noţiunea de graf orientat.................................................................. 267
9.2.2. Memorarea grafurilor orientate......................................................... 270
9.2.3. Graf parţial, subgraf.......................................................................... 272
9.2.4. Parcurgerea grafurilor. Drumuri. Circuite.......................................... 273
9.2.5. Graf tare conex. Componente tare conexe....................................... 275
Probleme propuse……………………………………………………………………… 278
Răspunsuri…………….……………………………………………………………….. 286

Anexa 1. Memento ………………………………………………………… 289

Anexa 2. Aplicaţii practice ale grafurilor ……………………………… 309

Anexa 3. Codul ASCII ……………………………………………………… 316


7

Capitolul 1

Tablouri

1.1. Noţiunea de tablou

Anul trecut am studiat tablourile unidimensionale numite uneori, prin analogie


cu matematica, vectori. În acest an studiem tablourile bidimensionale numite, tot
prin analogie cu matematica, matrice.

Definiţia 1.1. Un tablou este o structură omogenă (formată din elemente


de acelaşi fel) cu un număr bine determinat de componente. Tabloul se
identifică printr-un singur nume, iar componentele sale se identifică prin
intermediul unui sistem de indici.
Alăturat avem reprezentat un tablou. Un  a1,1 a1, 2 . . a1,n 
element al acestuia, ai,j, se găseşte pe linia i şi  
 a2,1 a2 , 2 . . . 
coloana j. Este esenţial de reţinut faptul că toate  a a3, 2 . .a3,n 
elementele tabloului au acelaşi tip.  3,1 
A= . . . . . 
 .
 . . . . 
 am −1,1 am −1, 2 . . am −1,n 
 
 am ,1 am , 2 . . am ,n 

1. Un magazin oferă spre vânzare n produse. Se doreşte să se reţină


vânzările lunare, valorice, din fiecare tip de produs. Putem organiza
datele sub formă de tablou astfel:
 vom numerota cele n produse cu 1, 2, ..., n;
 lunile le vom numerota cu 1, 2, ..., 12;
 elementul ai,j semnifică valoarea încasată din vânzarea produslui i în luna
j a anului - prin urmare, elementele tabloului sunt de tip real.

Intrebări posibile

a) Dacă magazinul vinde 5 produse, câte elemente are tabloul?


b) Care sunt indicii de adresare în tablou pentru a afla vânzările din produsul
5 în luna septembrie?
c) Cum se poate calcula suma încasată în luna mai?
8 Capitolul 1. Tablouri

d) Ştiind că, din punct de vedere economic, anul este împărţit în patru
trimestre şi că fiecare trimestru are trei luni, cum se poate calcula suma
încasărilor din trimestrul patru al anului?
e) Cum putem determina produsul din vânzarea căruia s-a încasat anual
suma maximă?
f) Care este luna cu cele mai mari încasări?

2. Tot aşa, se poate memora, sub formă de tablou, situaţia la învăţătură a celor m
elevi ai unei clase. Dacă numerotăm elevii cu 1, 2, ..., m şi materiile pe care aceştia
le studiază cu 1, 2, ..., n, atunci ai,j reprezintă media pe care o are elevul i la
materia j.

Întrebări posibile
a) Dacă în clasă sunt 30 de elevi şi aceştia studiază 8 materii, câte elemente
are matricea?
b) Care este materia la care elevii au cele mai bune rezultate?
c) Care este media generală a elevului i?
d) Care este media generală a elevilor unei clase?
Şirul exemplelor ar putea continua pentru că sunt foarte multe situaţii în care
se utilizează tablouri bidimensionale (matrice).

1.2. Cum citim şi cum afişăm un tablou bidimensional ?

În clasa a X-a am învăţat să lucrăm cu masive unidimensionale. Pentru a


adresa un element al unui vector se utilizează un singur indice. În cazul matricelor,
vom utiliza doi indici. Mai jos, puteţi observa cum se declară o matrice cu 10 linii şi
9 coloane, cu elemente de tip întreg:

Varianta Pascal Varianta C++


type tablou = array[1..10,1..9] of real; int a[10][9];
...
var a:tablou;
sau direct
var a:array [1..10,1..9] of real;

 În Pascal, matricea are liniile 1,2,...,10 şi coloanele 1,2, …,9 şi, de exemplu,
elementul de pe linia a treia şi coloana a patra se adresează prin a[3,4].
 În C++, matricea are liniile 0,1,...,9 şi coloanele 0,1,…,8 şi, de exemplu,
elementul de pe linia a treia şi coloana a patra se adresează prin a[2][3].
Uneori, pentru simplitate, vom folosi liniile şi coloanele matricei începând de
la 1. În aceste condiţii se pierde o linie şi o coloană, fiecare de indice 0.
Considerăm acest fapt neesenţial.
Manual de informatică pentru clasa a XI-a 9

De multe ori nu ştim câte linii şi câte coloane va


trebui să aibă tabloul. În acest caz, tabloul se declară
cu un număr maxim de linii şi un număr maxim de
coloane, în aşa fel încât acesta să corespundă oricărui
set de date de intrare. Evident, într-un astfel de caz
există o risipă de memorie internă, dar... programul nu
se va termina cu eroare. În figura alăturată, aveţi
reprezentat grafic un tablou cu 8 linii şi 8 coloane, din
care, într-un anumit caz, utilizăm numai primele 4 linii şi
primele 5 coloane.

Figura 1.1. Exemplu de tablou

Programul care-l utilizează va funcţiona corect dacă avem cel mult 8 linii şi
cel mult 8 coloane.

În programul următor se citeşte şi se afişează un tablou. Iniţial se citesc


numărul de linii şi de coloane ale tabloului (m şi n). Observaţi modul în
care am afişat tabloul - de aşa natură încât şi pe ecran să arate ca o
matrice, adică fiecare linie să fie scrisă pe un rând.

Varianta Pascal Varianta C++


var m,n,i,j:integer; #include <iostream.h>
a:array[1..10,1..9] of integer; main()
begin { int m,n,i,j,a[10][9];
write ('m='); cout<<"m="; cin>>m;
readln(m); cout<<"n="; cin>>n;
write ('n='); for (i=0;i<m;i++)
readln(n); for(j=0;j<n;j++)
for i:=1 to m do { cout<<"a["<<i+1<<','
for j:=1 to n do <<j+1<<"]=";
begin cin>>a[i][j];
write ('A[',i,',',j,']='); }
readln(a[i,j]); for (i=0;i<m;i++)
end; { for (j=0;j<n;j++)
cout<<a[i][j]<<' ';
for i:=1 to m do
cout<<endl;
begin
}
for j:=1 to n do
}
write (a[i,j],' ');
writeln;
end
end.

 În memorie, tablourile sunt reţinute pe linii. Aceasta înseamnă că la început


este memorată prima linie, apoi a doua, ş.a.m.d.
10 Capitolul 1. Tablouri

1.3. Aplicaţii cu tablouri bidimensionale

 Aplicaţia 1.1. Interschimbare de linii. Se citeşte un tablou cu m linii şi n


coloane. Se citesc, de asemenea, şi două numere naturale distincte x şi y,
cuprinse între 1 şi m. Se cere să se interschimbe linia x cu linia y. La început vom
afişa tabloul iniţial, apoi pe cel obţinut prin interschimbarea liniilor x şi y.
Exemplu: Considerăm tabloul de mai jos, cu m=3, n=3 şi liniile x=2, y=3:
 1 2 3
 
 4 5 6
 7 8 9
 
În urma interschimbării liniilor 2 şi 3 se obţine:
 1 2 3
 
 7 8 9
 4 5 6
 

 Rezolvare. După cum am învăţat, pentru a interschimba conţinutul a două


variabile se utilizează o a treia, de manevră. De această dată, se interschimbă două
linii. Am fi tentaţi ca manevra să fie un vector cu n componente. Cu puţină atenţie ne
dăm seama că putem folosi ca manevră o singură variabilă de acelaşi tip cu cel al
componentelor de bază ale tabloului. În rest, analizaţi programul de mai jos:

Varianta Pascal Varianta C++


type matrice = array[1..10, #include <iostream.h>
1..10] of byte; main()
var mat: matrice; { int mat[10][10],m,n,i,
m, n, i, j, x, j,x,y,man;
y, man: integer; cout<<"m="; cin>>m;
begin cout<<"n="; cin>>n;
{-citirea datelor-} for(i=0;i<m;i++)
write ('m= '); readln(m); for(j=0;j<n;j++)
write ('n= '); readln(n); {
for i := 1 to m do cout<<"mat["<<i+1<<','
for j := 1 to n do <<j+1<<"]=";
begin cin>>mat[i][j];
write('mat[',i,',',j,']='); };
readln(mat[i,j]); cout<<"x=";
end; cin>>x;
write('x= '); readln(x); cout<<"y=";
write('y= '); readln(y); cin>>y;
{- tiparesc tabloul initial-} cout<<endl;
for i := 1 to m do for (i=0;i<m;i++)
begin {
for j := 1 to n do for (j=0;j<n;j++)
write (mat[i,j],' '); cout<<mat[i][j]<<' ';
writeln; cout<<endl;
end; }
Manual de informatică pentru clasa a XI-a 11

{-interschimbare linii-} for(j=0;j<n;j++)


for j:=1 to n do { man=mat[x-1][j];
begin mat[x-1][j]=mat[y-1][j];
man := mat[x,j]; mat[y-1][j]=man;
mat[x,j] := mat[y,j]; }
mat[y,j] := man; cout<<endl;
end; for (i=0;i<m;i++)
writeln; { for (j=0;j<n;j++)
cout<<mat[i][j]<<' ';
{-tiparesc tabloul inversat-} cout<<endl;
for i := 1 to m do }
begin }
for j := 1 to n do
write(mat[i,j],' ');
writeln;
end;
end.

 Aplicaţia 1.2. Spirala. Se citeşte un tablou cu n linii şi n coloane. Se cere să se


afişeze elementele tabloului în ordinea rezultată prin parcurgerea acestuia în
spirală, începând cu primul element din linia 1, în sensul acelor de ceas.

Exemplu. Fie tabloul:

 1 2 3
 
 4 5 6
 7 8 9
 
Elementele vor fi afişate în ordinea:
1 2 3 6 9 8 7 4 5.

 Rezolvare. Pentru a putea rezolva problema, privim tabloul ca pe un ansamblu


alcătuit din mai multe dreptunghiuri "concentrice":

* * * * *
 
* * * * *
* * * * *
 
* * * * *
 
* * * * *

Tabloul de mai sus este un ansamblu format din trei dreptunghiuri


"concentrice" - ultimul are un singur element şi nu a putut fi reprezentat. După ce
stabilim numărul de pătrate (cum?), afişăm elementele aflate pe fiecare latură a
fiecărui pătrat în ordinea cerută, având grijă ca elementele aflate în colţuri să nu fie
afişate de două ori.
12 Capitolul 1. Tablouri

Varianta Pascal Varianta C++


type matrice = array[1..10, #include <iostream.h>
1..10] of integer; main()
var mat: matrice; { int mat[10][10],n,i,j,k;
n, i, j, k: integer; cout<<"n="; cin>>n;
begin for(i=1;i<=n;i++)
{-citirea datelor-} for (j=1;j<=n;j++)
write ('n= '); readln(n); { cout<<"mat["<<i<<','
for i := 1 to n do <<j<<"]=";
for j := 1 to n do cin>>mat[i][j];
begin }
write('mat[',i,',',j,']='); for(k=1;k<=n/2+1;k++)
readln(mat[i,j]); { for(i=k;i<=n-k+1;i++)
end; cout<<mat[k][i]<<endl;
{-listare-} for(i=k+1;i<=n-k+1;i++)
for k := 1 to n div 2 + 1 do cout<<mat[i][n-k+1]<<endl;
begin for(i=n-k;i>=k;i--)
for i := k to n-k+1 do cout<<mat[n-k+1][i]<<endl;
writeln(mat[k,i]); for(i=n-k;i>=k+1;i--)
for i := k+1 to n-k+1 do cout<<mat[i][k]<<endl;
writeln(mat[i,n-k+1]); }
for i := n-k downto k do }
writeln(mat[n-k+1,i]);
for i := n-k downto k+1 do
writeln(mat[i,k])
end;
end.

 Aplicaţia 1.3. Pe o tablă cu n linii şi m coloane (n, m numere naturale,


1≤n,m≤30), sunt plasate pe unele poziţii jetoane cu litere, conform unui joc corect
de SCRABBLE. Ştiind că vocalele au câte un punct, iar consoanele câte două,
stabiliţi valoarea totală a cuvintelor de pe tablă. Valorile n şi m şi configuraţia tablei
se citesc din fişierul ”tabla.txt”, conform exemplului (locurile goale de pe tablă
sunt memorate sub forma unor caractere punct).

Exemplu. Pentru datele de intrare:


3 5
D.SAU
ALTI.
..A..
se afişează 20
(DA=3, STA=5, ALTI=6, SAU=4, AI=2).

 Rezolvare. Căutarea cuvintelor pe tablă şi cumularea punctelor lor este, de


obicei, prima soluţie la care ne gândim. Algoritmul corespunzător acestei soluţii pare
însă destul de complicat. Simpla parcurgere a matricei de caractere şi adunarea
valorilor corespunzătoare literelor nu este o strategie bună, deoarece se pierde
Manual de informatică pentru clasa a XI-a 13

punctajul literelor care apar într-un cuvânt pe orizontală şi unul pe verticală şi care ar
trebui luate de două ori în calcul. De aceea, vom analiza în plus, pentru fiecare literă,
dacă ea face parte dintr-un singur cuvânt (are litere vecine doar pe orizontală sau
doar pe verticală) şi se punctează obişnuit, sau face parte din două cuvinte (are litere
vecine şi pe orizontală şi pe verticală) şi atunci se punctează dublu.

Varianta Pascal Varianta C++


#include <fstream.h>
var a:array[1..30,1..30]of char; char a[31][31];
n,m,i,j,dir:byte; int n,m,i,j,dir,p,pt;
p,pt:integer; ifstream f("tabla.txt");
f:text;
begin void main()
assign(f,'tabla.txt'); {
reset(f); f>>n>>m;
readln(f,n,m); for(i=1;i<=n;i++)
for i:=1 to n do for(j=1;j<=m;j++)
begin f>>a[i][j];
for j:=1 to m do pt=0;
read(f,a[i,j]); for(i=1;i<=n;i++)
readln(f) for(j=1;j<=m;j++)
end; if (a[i][j]!='.')
pt:=0; {
for i:=1 to n do if(a[i][j]=='A'||
for j:=1 to m do a[i][j]=='E'||
if a[i,j]<>'.' then a[i][j]=='I'||
begin a[i][j]=='O'||
if (a[i,j]='A') or a[i][j]=='U')
(a[i,j]='E') or p=1;
(a[i,j]='I') or else p=2;
(a[i,j]='O') or dir=0;
(a[i,j]='U') then p:=1 if (i>1 && a[i-1][j]!='.'
else p:=2; || i<n &&
dir:=0; a[i+1][j]!='.')
if (i>1) and (a[i-1,j]<>'.') dir++;
or if (j>1 && a[i][j-1]!='.'
(i<n) and (a[i+1,j]<>'.') || j<m &&
then dir:=dir+1; a[i][j+1]!='.')
if (j>1) and (a[i,j-1]<>'.') dir++;
or pt+=p*dir;
(j<m) and (a[i,j+1]<>'.') }
then dir:=dir+1; cout<<pt<<endl;
pt:=pt+p*dir }
end;
writeln(pt)
end.

Dacă utilizăm artificiul de a borda matricea cu caractere punct:


┌pentru i=0,n+1 execută
│ a0,i'.'; an+1,i'.'
│ ai,0'.'; ai,n+1'.'
└■
14 Capitolul 1. Tablouri

atunci testarea vecinilor elementelor de pe margine se realizează întocmai ca şi în


cazul celor din interiorul matricei:

┌dacă ai-1,j≠'.' sau ai+1,j≠'.'


│ dirdir+1 (cel puţin un vecin pe direcţia verticală)
└■

┌dacă ai,j-1≠'.' sau ai,j+1≠'.'


│ dirdir+1 (cel puţin un vecin pe direcţia orizontală)
└■

Se mai poate analiza şi cazul în care o literă nu are nici un vecin pe niciuna
dintre direcţiile orizontală sau verticală (caz în care litera nu s-ar puncta deloc), însă
problema precizează că literele sunt aşezate corect conform jocului de scrabble,
deci nu pot fi amplasate litere izolate.

 Aplicaţia 1.4. Sortarea fără comparaţii este o metodă de sortare care permite
sortarea a n numere naturale, fără a face nici măcar o comparaţie între ele.

Vom prezenta algoritmul pe un exemplu în care se sortează crescător 6


numere naturale: 6, 36, 41, 25, 40 şi 30.

Iniţial, şirul celor n numere se împarte în 10 clase: prima clasă conţine


numerele care se termină cu 0, a 2-a clasă, cele care se termină cu 1, ... a 10
clasă, cele care se termină cu 9.

Pentru memorarea numerelor care aparţin fiecărei clase vom utiliza o matrice,
denumită Mat, cu 10 coloane, în care prima linie are indicele 0. Elementele din linia 0
reţin numărul de elemente din şir care se găsesc pe coloana respectivă:
0 1 2 3 4 5 6 7 8 9

2 1 0 0 0 1 2 0 0 0
40 41 25 6
30 36

Obţinem din nou şirul de numere, aşezându-le în ordinea coloanelor şi în


ordinea în care le-am pus în fiecare coloană: 40, 30, 41, 25, 6, 36. Împărţim din
nou şirul în 10 clase, după a 2-a cifră a numerelor:
0 1 2 3 4 5 6 7 8 9

1 0 1 2 2 0 0 0 0 0
6 25 30 40
36 41
Manual de informatică pentru clasa a XI-a 15

Obţinem din nou şirul de numere, aşezându-le în ordinea coloanelor şi în


ordinea în care le-am pus în fiecare coloană: 6, 25, 30, 36, 40, 41. De
această dată, şirul este sortat.

Observaţii

 Dacă k este numărul maxim de cifre a numerelor din şir, operaţia de


împărţire a numerelor în 10 clase se va relua de k ori.

 Algoritmul este "mare consumator" de memorie!

Varianta Pascal Varianta C++


var A:array [1..100] of #include <iostream.h>
integer; int A[100],Mat[100][100],n,
Mat:array[0..100,0..100] NrCif,i,j,k,Cif,Zece;
of integer;
n,NrCif,i,j,k,Cif,Zece: main()
integer; { cout<<"n="; cin>>n;
begin for (i=1;i<=n;i++)
write('n= '); readln(n); { cout<<"A["<<i<<"]=";
for i := 1 to n do cin>>A[i];
begin }
write('A[',i,']= '); Zece=1;
readln(A[i]) for(NrCif=1;NrCif<=4;NrCif++)
end; { if (NrCif>1) Zece*=10;
Zece:=1; for (i=1;i<=n;i++)
for NrCif:=1 to 4 do {Cif= (A[i] / Zece) % 10;
begin Mat[0][Cif]++;
if NrCif>1 then Mat[Mat[0][Cif]][Cif]=A[i];
Zece:=Zece*10; }
for i:=1 to n do //Extrag din mat. in vector
begin k=0;
Cif:=(A[i] div Zece) for (i=0;i<=9;i++)
mod 10; if (Mat[0][i])
Mat[0,Cif]:=Mat[0,Cif]+1; for (j=1;j<=Mat[0][i];j++)
Mat[Mat[0,Cif],Cif]:=A[i] { k++;
end; A[k]=Mat[j][i];
{Extrag din mat. in vector} }
k:=0; for (i=0;i<=9;i++)
for i:=0 to 9 do Mat[0][i]=0;
if Mat[0,i]<> 0 then }
for j:=1 to Mat[0,i] do for (i=1;i<=n;i++)
begin cout<<A[i]<<endl;
k:=k+1; }
A[k]:=Mat[j,i]
end;
for i:=0 to 9 do
Mat[0,i]:=0;
end;
for i:=1 to n do
writeln(A[i]);
end.
16 Capitolul 1. Tablouri

Probleme propuse
1. Se citeşte un tablou cu n linii şi n coloane, numere întregi. Un astfel de tablou, în
care numărul liniilor coincide cu numărul coloanelor, se numeşte tablou pătratic.
a) Pentru un tablou pătratic A, numim diagonală principală, elementele aflate pe
"linia" care uneşte A[1,1] cu A[n,n].

Exemplu. Pentru tabloul de mai jos

 1 2 3
 ,
 4 5 6
 
 7 8 9
elementele sunt: 1, 5 şi 9.

Se cere:

a1) suma elementelor aflate pe diagonala principală;


a2) suma elementelor aflate deasupra diagonalei principale;
a3) suma elementelor aflate sub diagonala principală.

b) Pentru un tablou pătratic A, numim diagonală secundară, elementele aflate pe


"linia" care uneşte A[n,1] cu A[1,n].

Exemplu. Pentru tabloul

 1 2 3
 
 4 5 6 ,
 
 7 8 9
elementele sunt: 7, 5 şi 3.

Se cere:
b1) suma elementelor aflate pe diagonala secundară;
b2) suma elementelor aflate deasupra diagonalei secundare;
b3) suma elementelor aflate sub diagonala secundară.

Cerinţe suplimentare:

 pentru fiecare cerinţă se va face un program separat;


 în nici un program nu se va folosi instrucţiunea if.

2. Interschimbaţi coloanele unei matrice cu m linii şi n coloane astfel încât în linia k,


elementele să fie în ordine crescătoare.
Manual de informatică pentru clasa a XI-a 17

3. Un teren este dat sub forma unui tablou A cu n linii şi m coloane. Elementul
A[i,j] reţine altitudinea pătrăţelului de coordonate i şi j. Să se afişeze
coordonatele "pătrăţelelor vârf" (un pătrăţel este vârf dacă toţi vecinii săi au o
altitudine strict mai mică).

4. Determinaţi elementele şa ale unei matrice cu n linii şi m coloane (elemente


minime pe linie şi maxime pe coloană sau maxime pe linie şi minime pe coloană).

 Indicaţie. Atenţie! Maximele sau minimele pe linii sau coloane pot să nu fie
unice. Dacă vom considera numai o valoare dintre acestea, s-ar putea să pierdem
soluţii.

5. Pentru o matrice a, cu n linii şi m coloane citită din fişierul ”mat1.txt”, se cere


să se afişeze cel mai mare element din matrice şi indicii acestuia. Scrieţi programul
pseudocod corespunzător.

6. Pentru o matrice de numere reale a, cu 5 linii şi 4 coloane, citită de la tastatură,


se cere să se afişeze suma elementelor strict pozitive din matrice.

7. Pentru o matrice a, cu n linii şi n coloane citită din fişierul ”mat3.txt”, se cere


să se decidă pe care dintre cele două diagonale, suma elementelor constitutive
este mai mare. Se va afişa un mesaj corespunzător.

8. Pentru 5 cifre n,a,b,c,d citite de la tastatură, se cere să se creeze fişierul


”mat5.txt” care să conţină o matrice cu 2n linii şi 2n coloane, formată din 4
submatrice disjuncte, fiecare cu n linii şi n coloane, una dintre ele fiind formată
numai cu componente egale cu a, una numai cu componente egale cu b, una cu c
şi una cu d. Este necesară construirea în prealabil a matricei în memorie?

9. Fereastra. Fiind dată o matrice cu m linii şi n coloane. Se cere să se afişeze


toate submatricele cu 3 linii şi 3 coloane ale matricei iniţiale. Un astfel de procedeu
este utilizat atunci când, de exemplu, o imagine este mult prea mare şi ea este
afişată cu ajutorul unei ferestre.

Exemplu: m=4, n=4. Matricea iniţială şi submatricele sunt prezentate mai jos:

10. Fiind dată o matrice cu m linii şi n coloane, se cere să se afişeze elementele în


ordinea sugerată în figura de mai jos:

Figura 1.2. Exemplu


18 Capitolul 1. Tablouri

11. Fiind dată o matrice cu m linii şi n coloane se cere să se


afişeze elementele în ordinea sugerată în imaginea alăturată.

Figura 1.3. Exemplu

12. Fiind dată o matrice cu n linii şi n coloane (pătratică) cu numere naturale şi fiind
date două elemente ale matricei de coordonate (x1,y1) şi (x2,y2), care dintre
relaţiile de mai jos testează dacă elementele se găsesc pe o dreaptă paralelă cu
una dintre diagonalele matricei (principală sau secundară)?

Varianta Pascal Varianta C++


a) if x1-x2=y1-y2 a) if (x1-x2==y1-y2)
then writeln('Da') cout<<"Da";
else writeln('Nu'); else cout<<"Nu";
b) if x1-y1=x2-y2 b) if (x1-y1==x2-y2)
then writeln('Da') cout<<"Da"
else writeln('Nu'); else cout<<"Nu";
c) if abs(x1-y1)=abs(x2-y2) c) if (abs(x1-y1)==abs(x2-y2))
then writeln('Da') cout<<"Da";
else writeln('Nu'); else cout<<"Nu";
d) if abs(x1-x2)=abs(y1-y2) d) if (abs(x1-x2)==abs(y1-y2))
then writeln('Da') cout<<"Da";
else writeln('Nu'); else cout<<"Nu";

13. Se dă un vector V cu m*n componente numere întregi. Care dintre secvenţele


de mai jos copiază vectorul V în matricea Mat cu m linii şi n coloane? Copierea se
realizează completând mai întâi elementele de pe linia 1, apoi elementele de pe
linia 2, ..., iar la sfârşit, elementele de pe linia m.

 Copierea se realizează prin utilizarea unui singur ciclu for!

Varianta Pascal Varianta C++


a) for k:=1 to m*n do Mat[1+k a) for (k=1;k<=m*n;k++)
div n,1+k mod m]:=V[k]; Mat[1+(k-1)/n][1+(k-1)% m]=V[k];
b) for k:=1 to m*n do b) for (k=1;k<=m*n;k++)
Mat[1+(k-1) div m,1+(k-1) mod Mat[1+(k-1)/m][1+(k-1)% n]=V[k];
n]:=V[k]; c) for (k=1;k<=m*n;k++)
c) for k:=1 to m*n do Mat[1+(k-1)/n][1+(k-1)% n]=V[k];
Mat[1+(k-1) div n,1+(k-1) mod d) for (k=1;k<=m*n;k++)
n]:=V[k]; Mat[1+k/n][1+k%n]=V[k];
d) for k:=1 to m*n do Mat[1+k
div n,1+k mod n]:=V[k];
Manual de informatică pentru clasa a XI-a 19

14. Se dă un vector V cu m*n componente numere întregi şi o matrice Mat cu m


linii şi n coloane. Care dintre secvenţele de mai jos copiază matricea Mat în
vectorul V? Copierea se realizează astfel: mai întâi se copiază linia 1, apoi linia 2,
..., iar la sfârşit, linia m.

Varianta Pascal Varianta C++


a) for i:=1 to n do a) for (i=1;i<=n;i++)
for j:=1 to m do for (j=1;j<=m;j++)
V[j+(i-1)*m]:=Mat[i,j]; V[j+(i-1)*m]=Mat[i][j];
b) for i:=1 to m do b) for (i=1;i<=m;i++)
for j:=1 to n do for (j=1;j<=n;j++)
V[j+(i-1)*n]:=Mat[i,j]; V[j+(i-1)*n]=Mat[i][j];
c) for i:=1 to m do c) for (i=1;i<=m;i++)
for j:=1 to n do for (j=1;j<=n;j++)
V[i+(j-1)*n]:=Mat[i,j]; V[i+(j-1)*n]=Mat[i][j];
d) for i:=1 to m do d) for (i=1;i<=m;i++)
for j:=1 to n do for (j=1;j<=n;j++)
V[j+(i-1)*n]:=Mat[j,i]; V[j+(i-1)*n]=Mat[i,j];

Răspunsurile la testele grilă

12. d); 13. c); 14. b).


20

Capitolul 2

Subprograme

2.1. Noţiunea de subprogram

Definiţia 2.1. Prin subprogram vom înţelege un ansamblu alcătuit din


declarări şi instrucţiuni scrise în vederea unei anumite prelucrări,
ansamblu implementat separat şi identificat printr-un nume.

Până în prezent am fost doar utilizatori de subprograme. Exemple de astfel


de subprograme folosite sunt:

 matematice: sin, cos, abs, exp, trunc (Pascal) / floor (C++);


 de manipulare a fişierelor: close (Pascal) / .close() (C++).

În acest capitol învăţăm să lucrăm cu subprograme. Pentru a înţelege


noţiunea de subprogram, vom porni de la două exemple.

1. Se consideră funcţia:

 x +1
1 + x 2 , pentru x ∈ [-1,1];

f ( x ) =  x + 1, pentru x ∈ (-∞,-1); .
 6
 1+ x , pentru x ∈ (1, ∞ ).

Se citesc două valori reale a şi b. Să se scrie un program care afişează care dintre
valorile f(a) şi f(b) este cea mai mare.

Ce observăm? Că atât pentru calculul valorii funcţiei în punctul a, cât şi


pentru calculul valorii funcţiei în punctul b, aplicăm acelaşi tip de raţionament:
încadrăm valoarea respectivă (a sau b) într-unul dintre cele trei intervale şi aplicăm
formula de calcul corespunzătoare.

Cum procedăm? Prin utilizarea cunoştinţelor dobândite până în prezent,


scriem secvenţa de program care calculează valoarea funcţiei calculată pentru x=a
şi se mai scrie o dată (sau se copiază şi se adaptează) secvenţa de program care
calculează valoarea funcţiei calculată pentru x=b.
Manual de informatică pentru clasa a XI-a 21

Oare nu se poate lucra şi altfel? Răspunsul este afirmativ. Se scrie un


subprogram care calculează valoarea funcţiei într-un punct x oarecare şi se
apelează subprogramul: o dată pentru x=a şi încă o dată pentru x=b. Valorile
calculate la cele două apeluri, se compară între ele şi se afişează rezultatul cerut.

2. Se citeşte n, un număr natural. Se citesc apoi n numere reale. Se


cere să se afişeze cele n numere în ordinea crescătoare a valorilor lor.

Desigur, ştim să rezolvăm această problemă în mai multe feluri, pentru că


ştim să memorăm un şir de valori (folosind un vector) şi am studiat mai multe
metode prin care se poate obţine ordonarea crescătoare a unui şir de valori
(folosind algoritmi de sortare). De această dată, vom implementa o metodă
cunoscută, cea de sortare, dar vom utiliza subprogramele.

Vom scrie un subprogram care citeşte un vector, unul care tipăreşte un


vector şi un al treilea care sortează vectorul după una din metodele cunoscute.

În acest caz, programul ar arăta astfel:

Pasul 1 - se apelează subprogramul care citeşte vectorul.


Pasul 2 - se apelează subprogramul care sortează vectorul.
Pasul 3 - se apelează subprogramul care tipăreşte vectorul.

Faţă de scrierea clasică, aici problema a fost descompusă în trei


probleme mai simple (citire, sortare şi tipărire). În general, o problemă complexă
se rezolvă mai uşor dacă o descompunem în alte subprobleme mai mici. Apoi,
şansele de a greşi la scrierea unui subprogram sunt cu mult mai mici decât acelea
de a greşi la scrierea unui program mare.

În plus, dacă într-un alt program este necesară sortarea altui vector de
numere reale, metoda clasică ne permite să alegem din secvenţa de instrucţiuni ce
formează programul pe cele ce realizează sortarea, să le copiem în noul program
şi să facem eventualele adaptări (numărul de componente şi numele vectorului pot
fi altele). Aceste operaţii sunt destul de greoaie şi necesită multă atenţie. Prin
implementarea modulară, cu ajutorul subprogramelor, ”preluarea“ se realizează
mult mai uşor.

Putem acum enumera unele dintre avantajele utilizării subprogramelor:

 reutilizarea codului - odată scris, un subprogram poate fi utilizat de către


mai multe programe;
 elaborarea algoritmilor prin descompunerea problemei în altele mai
simple - în acest fel, rezolvăm cu mult mai uşor problema;
 reducerea numărului de erori care pot apărea la scrierea programelor;
 depistarea cu uşurinţă a erorilor - verificăm la început subprogramele,
apoi modul în care le-am asamblat (le-am apelat din cadrul programului);
 realizarea unor programe uşor de urmărit (lizibile).
22 Capitolul 2. Subprograme

2.2. Subprograme în Pascal

2.2.1. Un exemplu de utilizare a funcţiilor

 Problema 2.1. Se citeşte n, număr natural. Să se scrie programele care


afişează valoarea calculată a expresiilor:

n
1 1 1  1 1 1
E1 = 1 + + +  + ; E 2 = 1 + + +  +  .
2 3 n  2 3 n

 Rezolvare. Funcţia care calculează E1 este:


function subp(n:integer):real;
var s:real;
i:integer;
begin
s:=0;
for i:=1 to n do s:=s+1/i;
subp:=s;
end;

Analiza programului anterior

 Antetul funcţiei este “function subp(n:integer):real;“.

 Funcţia se numeşte “subp“.

 Ea este recunoscută de compilator prin faptul că antetul este precedat de


cuvântul cheie “function“.

 Funcţia are un parametru numit n. Rolul său este important şi anume


precizează pentru ce valoare trebuie calculată expresia. Aşa cum vom
vedea, există posibilitatea să avem mai mulţi parametri, de diferite tipuri.
 Funcţia are un anumit tip, care precizează natura rezultatului. În exemplu,
tipul este real întrucât expresia calculată este de acest tip.

 Funcţia are variabile proprii - adică variabile care sunt definite în cadrul ei. În
exemplu, ele sunt s şi i. Aceste variabile se numesc variabile locale.

 Am văzut că funcţia întoarce un anumit rezultat - în exemplu, de tip real.


Observaţi mecanismul prin care am obţinut aceasta. Calculez expresia în
mod obişnuit. Rezultatul este reţinut de variabila locală s. Prin atribuirea
“subp=s;“, funcţia a primit ca valoare de retur conţinutul variabilei s.

În continuare, prezentăm cele două programe care utilizează funcţia:


Manual de informatică pentru clasa a XI-a 23

var n:integer;
function subp(n:integer):real;
var s:real;
i:integer;
begin
s:=0;
for i:=1 to n do s:=s+1/i;
subp:=s;
end;
begin
write ('n='); readln(n);
write(subp(n):5:2);
end.
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

var n,i:integer;
rez,prod:real;
function subp(n:integer):real;
var s:real;
i:integer;
begin
s:=0;
for i:=1 to n do s:=s+1/i;
subp:=s;
end;
begin
write ('n='); readln(n);
rez:=subp(n);
prod:=1;
for i:=1 to n do prod:=prod*rez;
write(prod:5:2);
end.

 Funcţia este apelată întotdeauna din cadrul unei expresii.


Apelul funcţiei în primul program s-a făcut astfel: “write(subp(n):5:2);“,
iar în al doilea, “rez:=subp(n)“. În primul caz expresia este trecută ca parametru
pentru write, iar în al doilea, avem o expresie de atribuire.

În terminologia utilizată în teoria subprogramelor - în particular, în cazul


funcţiilor - se utilizează termenii parametri formali şi parametri efectivi.

Definiţia 2.2. Parametrii care se găsesc în antetul funcţiei se numesc


parametri formali.
Atunci când scriem o funcţie, nu cunoaştem valoarea propriu-zisă a
parametrilor. Funcţia trebuie să întoarcă rezultatul corect, oricare ar fi valoarea lor.
Din acest punct de vedere ei se numesc formali.

Definiţia 2.3. Parametrii care se utilizează la apel se numesc parametri


efectivi.
La apel, lucrurile stau altfel: valorile acestora sunt cunoscute. Prin urmare
aceştia se numesc parametri efectivi.
24 Capitolul 2. Subprograme

Între parametrii formali si parametrii efectivi trebuie să existe o concordanţă,


care va fi studiată la momentul potrivit.

2.2.2 Un exemplu de utilizare a procedurilor

 Problema 2.2. Se citeşte n, număr natural. Se citeşte un vector cu n


componente numere reale. Se cere să se tipărească vectorul sortat.

 Rezolvare. Programul este următorul:


type vector=array [1..9] of integer;
var n,i:integer;
v:vector;
procedure citesc;
begin
write('n='); readln(n);
for i:=1 to n do
begin
write('v[',i,']='); readln(v[i]);
end;
end;
procedure tiparesc;
begin
for i:=1 to n do writeln(v[i]);
end;
procedure sortez;
var gasit:boolean;
man:integer;
begin
repeat
gasit:=false;
for i:=1 to n-1 do
if v[i]>v[i+1]
then
begin
gasit:=true;
man:=v[i]; v[i]:=v[i+1]; v[i+1]:=man
end
until not gasit;
end;
begin
citesc;
sortez;
tiparesc;
end.

Programul conţine trei proceduri: citesc, sortez şi tiparesc. Forma


simplificată prin care este descrisă o procedură este:
procedure nume;
begin
........
end;
Manual de informatică pentru clasa a XI-a 25

 Apelul unei proceduri se face prin utilizarea numelui ei, sub forma nume;.
Mai precis, apelul unei proceduri este instrucţiune, numită instrucţiunea
de apel.
 La apel, controlul programului este transferat la prima instrucţiune a
procedurii - după cum se întâmplă şi la funcţii. După executarea procedurii,
se revine în programul principal la prima instrucţiune care urmează celei de
apel - în cazul de faţă se întâlneşte o altă instrucţiune de apel.
 Ca şi funcţiile, procedurile se pot plasa în cadrul programului între
declaraţiile de variabile şi instrucţiunea compusă.
 Orice variabilă a programului principal este şi variabilă a procedurii (invers nu
este adevărat).

În acest exemplu, procedurile sunt apelate fără utilizarea parametrilor. În


realitate, procedurile pot avea parametri întocmai ca şi funcţiile.

2.2.3. Structura unui subprogram

În esenţă, un subprogram este alcătuit din:

• Antet - conţine mai multe informaţii importante necesare compilatorului,


numele subprogramului, lista parametrilor formali. În cazul subprogramelor de
tip funcţie, conţine şi tipul rezultatului (valoarea întoarsă de funcţie).

• Bloc - cuprinde definiţiile tipurilor de variabile, ale variabilelor, ale


subprogramelor şi o instrucţiune compusă. Structura sa este identică cu cea a
programului principal, indiferent dacă este bloc de procedură sau de funcţie.

Programul principal conţine un antet (program nume) şi un bloc.

Pentru prezentarea structurii subprogramelor de tip funcţie vom utiliza


diagramele de sintaxă.

2.2.3.1. Structura subprogramelor de tip funcţie

În figura următoare este prezentată structura antetului:

Figura 2.1. Structura antetului unui subprogram de tip funcţie


26 Capitolul 2. Subprograme

Exemplu:
function suma(x,y:integer):real;
begin
suma:=x+y;
end

- antetul este “function suma(x,y:integer):real;“;

- identificatorul (numele) funcţiei este suma. Lista parametrilor formali este:


“x,y:integer“. Funcţia este de tip real (identificatorul de tip);

- blocul este (în acest caz lipsesc variabilele locale):

begin
suma:=x+y;
end

Important: identificatorul de tip (tipul valorii întoarse de funcţie) poate fi: de


orice tip ordinal, de orice tip real, de orice tip pointer sau de tipul string
(acest tip va fi studiat în acest manual).

2.2.3.2. Structura subprogramelor de tip procedură

În figura următoare este prezentat antetul de procedură:

Figura 2.2. Structura antetului unui subprogram de tip procedură

Exemplu:
procedure suma(var s:real; x,y:real);
begin
s:=x+y;
end;

- antetul este: “procedure suma(var s:real; x,y:real); “

- identificatorul (numele procedurii) este suma;

- lista parametrilor formali este: “var s:real; x,y:real;“;

- blocul este:
begin
s:=x+y;
end;
Manual de informatică pentru clasa a XI-a 27

2.2.4. Definirea şi declararea unui subprogram

Deşi aparent asemănătoare, cele două noţiuni diferă. Buna înţelegere a lor
ne ajută să evităm anumite erori. Mai mult, aceste noţiuni sunt utilizate de orice
limbaj de programare, nu numai de Pascal.

Definiţia 2.4. A defini un subprogram, înseamnă a-l scrie efectiv,


după structura anterior prezentată. O problemă importantă este locul
unde se defineşte subprogramul.

În Pascal, subprogramele se definesc în trei locuri:

1. În blocul programului care-l utilizează - numit şi program principal - între


declaraţia variabilelor şi instrucţiunea compusă:
program exemplu1;
var x:integer;
procedure t;
begin
writeln(x)
end;
begin
x:=3;
t
end.

2. În cadrul unităţilor de program - vor fi studiate ulterior.

3. În blocul unui alt subprogram. Această formă este proprie limbajului


Pascal. În exemplu, procedura t, are ca subprogram procedura z. Iniţial
programul va atribui variabilei x valoarea 3. Apoi va apela procedura t. Ea
tipăreşte conţinutul lui x (3), apoi apelează procedura z. La rândul ei,
aceasta afişează un mesaj: 'eu sunt z'.

program exemplu2;
var x:integer;
procedure t;
procedure z;
begin
writeln('eu sunt z');
end;
begin
writeln(x); z;
end;

begin
x:=3;
t
end.
28 Capitolul 2. Subprograme

Definiţia 2.5. A declara un subprogram, înseamnă a-l anunţa. Un


subprogram nedeclarat nu poate fi folosit.

1. În cazul în care subprogramul este definit în blocul programului principal


sau în blocul subprogramului, se consideră că a fost şi declarat pentru
acesta, deci poate fi folosit. În acest caz, declaraţia coincide cu definiţia.

Exemple

 În programul exemplu1, procedura t este definită în blocul programului


principal. Ea poate fi apelată din programul principal.

 În programul exemplu2, procedura t este definită în blocul programului


principal, deci poate fi apelată de acesta. Procedura z este definită în blocul
procedurii t. Ea poate fi apelată din t. În schimb, nu poate fi apelată din
programul principal.

2. În blocul programului principal sau în blocul unui subprogram se definesc, în


ordine, subprogramele s1, s2, ..., sk. În acest caz, automat s1 este
declarat pentru s2, s3, ..., sk, apoi s2 este declarat pentru s3, ..., sk, iar sk-1
este declarat pentru sk.

Exemple

 Procedurile s1 şi s2 pot fi apelate din programul principal. Procedura s1


poate fi apelată din procedura s2, dar s2 nu poate fi apelată, în absenţa altei
clauze, din s1:

procedure s1;
begin
writeln('s1')
end;
procedure s2;
begin
s1;
writeln ('s2');
end;
begin
s1;
s2;
end.

 Procedura test conţine definiţia a două proceduri s1 şi s2. Nici una din
aceste proceduri nu se consideră declarată pentru programul principal.
Amândouă sunt declarate pentru procedura test. Pentru procedura s2, s1
este declarată, dar pentru s1 procedura s2 nu este declarată.
Manual de informatică pentru clasa a XI-a 29

procedure test;
procedure s1;
begin
writeln('s1')
end;
procedure s2;
begin
s1;
writeln('s2');
end;
begin
s1;
s2;
end;

begin
test;
end.

3. În situaţia prezentată în cazul 2, se poate totuşi ca orice subprogram definit


la nivelul unui bloc să fie declarat pentru toate subprogramele definite la
nivelul aceluiaşi bloc, ca în exemplul următor.

Declaraţia se face prin antet, urmat de cuvântul cheie “forward“.

Observaţi că, în acest caz, definiţia nu coincide cu declaraţia.

procedure b; forward;

procedure a;
begin
writeln('Eu sunt a');
b;
end;

procedure b;
begin
writeln('Eu sunt b')
end;

begin
a;
end.

4. În cazul în care subprogramul a fost definit în cadrul unei unităţi de program,


declaraţia unităţii, clauza “uses“, are efect de declaraţie pentru toate
subprogramele definite în cadrul ei (unităţile de program vor fi studiate în
cadrul acestui capitol).
30 Capitolul 2. Subprograme

2.2.5. Apelul subprogramelor

Definiţia 2.6. A apela un subprogram, înseamnă a-l lansa în


executare. Pentru a putea fi apelat din cadrul unui bloc, un subprogram
trebuie declarat la nivelul blocului respectiv.

2.2.5.1. Apelul funcţiilor

În programul de mai jos este apelată o funcţie care calculează produsul a


două numere întregi. Programul tipăreşte suma între 1 şi produsul calculat. În
exemplu, valoarea găsită este 7:

var p,x,y:integer;

function prod(x,y:integer):integer;
begin
prod:=x*y;
end;

begin
x:=2;
y:=3;
p:=1+prod(x,y);
writeln(p)
end.

 Apelul unei funcţii se realizează din interiorul unei expresii. În exemplu,


expresia este: ”1+prod(x,y)”. Rezultatul ar fi fost obţinut mai simplu dacă
scriam ”writeln(1+prod(x,y));”.

Observaţii

 În cadrul expresiei, apelul este un operand. El intră în calcul cu valoarea


returnată de funcţie.
 După apelul funcţiei, se continuă evaluarea expresiei.

Mai jos este prezentat operandul care reprezintă apelul funcţiei.

Figura 2.3. Structura operandului care reprezintă apelul funcţiei


Manual de informatică pentru clasa a XI-a 31

2.2.5.2. Apelul procedurilor

Cele câteva exemple date până acum fac inutilă prezentarea unui alt
exemplu. Trebuie să ştim că:

1. Apelul unei proceduri se face printr-o instrucţiune, numită instrucţiune


procedurală. Sintaxa acestei instrucţiuni este prezentată mai jos:

Figura 2.4. Structura instrucţiunii procedurale

2. După apel, se va executa instrucţiunea care urmează instrucţiunii procedurale.

2.2.5.3. Transmiterea parametrilor la apel

În ce priveşte mecanismul de transmitere a parametrilor nu există nici o


diferenţă între proceduri şi funcţii. Din acest motiv, transmiterea parametrilor se va
trata unitar.

 Parametrii formali sunt cei trecuţi în antetul subprogramului.


 Parametrii efectivi sunt cei trecuţi la apelul subprogramului.

Aşa cum am văzut, subprogramele pot lucra cu variabilele globale ale


blocului care conţine definiţia lor. Atunci care este motivul pentru care sunt
necesari parametrii?

Cunoştinţele dobândite până în acest moment nu permit o justificare


completă. Putem spune numai că utilizarea parametrilor permite ca subprogramul
să fie scris independent de programul principal. Să luăm un exemplu: ni se cere să
scriem un subprogram care calculează suma a două valori reale. Ce adunăm?
Pentru un program ar trebui să adunăm x cu y, iar pentru altul, m cu n.

var x,y,sum:integer;
procedure suma(a,b:integer; var s:integer);
begin
s:=a+b;
end;
begin
x:=3; y:=4;
suma(x,y,sum); writeln(sum);
suma(2,3,sum); writeln(sum);
end.
32 Capitolul 2. Subprograme

În programul anterior, procedura suma calculează suma a două numere


întregi. Ea este apelată de două ori din cadrul programului principal.

- parametrii formali sunt: ”a,b:integer; var s:integer;”;


- pentru primul apel, parametrii efectivi sunt: x, y, sum;
- pentru al doilea apel, parametrii efectivi sunt: 2, 3, sum.

Iată câteva reguli care trebuie respectate la apel:

 Numărul parametrilor formali trebuie să coincidă cu numărul


parametrilor efectivi. În exemplu, acest număr este 3.

 Tipul parametrilor formali trebuie să coincidă cu tipul parametrilor


efectivi sau tipul parametrilor efectivi să poată fi convertit implicit către
tipul parametrilor formali, la fel ca în cazul atribuirii.

De exemplu, dacă parametrii formali a şi b sunt de tipul integer, nu este


permis ca parametrii efectivi să fie de alt tip (de exemplu, de tipul real).

Nu este obligatoriu ca numele parametrilor formali să coincidă cu numele


parametrilor efectivi.

 Nu este permis să definim tipul parametrilor în cadrul antetului subpro-


gramului. Tipul trebuie să fie predefinit sau să fie declarat în prealabil.

Exemple
type vector=array [1..9] of integer;
...
procedure test(n:integrer, v:vector); {corect}
procedure test(n:integrer, v:array[1..9] of integrer); {incorect}

Parametrii formali sunt de două feluri: transmişi prin valoare şi transmişi


prin referinţă. În cazul în care aceştia sunt precedaţi de cuvântul cheie var, se
consideră că sunt transmişi prin referinţă, contrar, prin valoare.

Tratarea transmiterii prin valoare şi prin referinţă se va face în paragrafele


următoare. Pentru moment, prezentăm sintaxa parametrilor formali şi a
parametrilor efectivi:

Figura 2.5.
Sintaxa parametrilor trimişi
Manual de informatică pentru clasa a XI-a 33

2.2.5.4. Cum memorează subprogramele parametrii transmişi ?

În acest paragraf vom analiza modul prin care sunt memoraţi parametrii
transmişi în momentul lansării în executare a subprogramului.

 Pentru memorarea parametrilor, subprogramele folosesc o zonă de memorie


numită stivă (mai exact, această zonă se numeşte segment de stivă).
 Memorarea parametrilor transmişi se face în ordinea în care aceştia
figurează în antet: de la stânga la dreapta.
 Pentru parametrii transmişi prin valoare, se memorează valoarea transmisă,
iar pentru cei transmişi prin referinţă, se memorează adresa variabilei
transmisă ca parametru.
 În cadrul subprogramului, parametrii transmişi şi memoraţi în stivă sunt
variabile. Numele lor este cel din lista parametrilor formali.

Reluăm exemplul anterior:

 antetul este: ”procedure suma(a,b:integer; var s:integer);”;


 la primul apel, parametrii sunt: x, y, sum, unde x reţine ”3” şi y ”4”.

În stivă sunt memorate variabilele subprogramului. În figură, sunt prezentate


şi valorile memorate:

stiva 3 4 adresa variabilei sum

a b s
La al doilea apel, parametrii sunt: 2, 3, sum.

stiva 2 3 adresa variabilei sum

a b s
Observaţii

 La revenirea în blocul apelant, conţinutul variabilelor memorate în stivă se va


pierde.
 Memorarea în stivă are şi alte consecinţe, dar acestea vor fi tratate la
momentul potrivit (vezi recursivitatea!).

2.2.5.5. Transmiterea parametrilor prin valoare

Transmiterea prin valoare se utilizează atunci când suntem interesaţi ca


subprogramul să lucreze cu acea valoare, dar în prelucrare, nu ne interesează ca
parametrul efectiv (cel din blocul apelant) să reţină valoarea modificată în subprogram.
34 Capitolul 2. Subprograme

Astfel, se pot transmite prin valoare:

 valorile reţinute de variabile - în acest caz, parametrii efectivi trebuie să fie


numele variabilelor.

Exemplu:
var n:integer;
procedure test(n:integer);
begin
n:=n+1;
writeln(n)
end;
begin
n:=1;
test(n);
writeln(n);
end.

- parametrul n este transmis prin valoare.

 În programul principal, avem declarată variabila n, care este iniţializată cu 1.

 Apelăm procedura. La apel, se rezervă spaţiu în stivă, spaţiu care are


numele parametrului (deci tot n) şi este iniţializat cu valoarea memorată de
variabila n a programului principal. În acest moment avem două variabile n şi
ambele reţin valoarea 1.

 În procedură, variabila n este incrementată (adică la vechiul conţinut se


adaugă 1). Evident, este vorba de variabila memorată în stivă.

 Afişăm conţinutul variabilei n (cea din stivă), deci se tipăreşte 2.

 La ieşirea din procedură, variabila n (din stivă) se pierde - adică nu mai are
spaţiu alocat. Prin urmare, valoarea 2 este pierdută.

 În programul principal se tipăreşte conţinutul variabilei n, adică 1.

 parametrii efectivi sunt valori sau expresii, care mai întâi se evaluează.

Exemplu:
procedure test(n:integer);
begin
writeln(n)
end;

begin
test(3);
test(3+4*5);
end.
Manual de informatică pentru clasa a XI-a 35

În procedură se creează o variabilă în segmentul de stivă, numită n, care la


primul apel reţine valoarea 3 şi la al doilea, valoarea 23. La ieşirea din procedură
conţinutul variabilei se pierde.

Aşa cum am văzut, în cazul transmiterii prin valoare, se pot folosi ca


parametri efectivi expresii. De asemenea, expresiile pot avea ca operanzi funcţii.
Următorul program citeşte două valori x şi y şi calculează expresia (x-y)*(x+y).

Exemplu:
program test_param;
var x,y:integer;
function suma (a,b:integer):integer;
begin
suma:=a+b;
end;
function dif (a,b:integer):integer;
begin
dif:=a-b;
end;
function prod(a,b:integer):integer;
begin
prod:=a*b;
end;
begin
write('x='); readln(x);
write('y='); readln(y);
writeln(prod(dif(x,y),suma(x,y)));
end.

2.2.5.6. Transmiterea parametrilor prin referinţă

Parametrii sunt transmişi prin referinţă atunci când ne interesează ca, la


revenirea din subprogram, variabila transmisă să reţină valoarea stabilită în timpul
executării subprogramului.

 În cazul transmiterii prin referinţă, parametrii efectivi trebuie să fie variabile.


 În cazul subprogramelor de tip procedură, transmiterea prin referinţă este
mecanismul clasic prin care acestea întorc valori către blocul apelant.
 Pentru ca un parametru să fie transmis prin referinţă este necesar ca
parametrul formal să fie precedat de cuvântul cheie ”var”.

 În cazul transmiterii prin referinţă, subprogramul reţine în stivă adresa variabilei.

În acest caz, ne putem întreba care este mecanismul prin care, deşi pentru o
variabilă transmisă se reţine adresa sa, în subprogram putem adresa
variabila normal (nu indirect)? La compilare, orice referinţă la variabila
respectivă, este "tradusă" ca adresare indirectă.
36 Capitolul 2. Subprograme

Exemplu. Programul următor utilizează o procedură care interschimbă valorile


reţinute de două variabile. Acestea sunt transmise prin referinţă.
var x,y:integer;
procedure interschimb(var x,y:integer);
var man:integer;
begin
man:=x; x:=y; y:=man;
end;
begin
x:=2; y:=3;
interschimb(x,y);
write(x,' ',y);
end.

2.2.6. Variabile locale şi globale

Definiţia 2.7. Variabilele globale sunt variabilele programului principal.


Ele sunt rezervate într-o zonă specială de date, numită segment de date.
În versiunea 7.0 a limbajului Pascal, variabilele numerice sunt
iniţializate cu 0.

Definiţia 2.8. Variabilele locale sunt variabile declarate în interiorul


subprogramelor. Aceste variabile sunt memorate în segmentul de stivă,
după variabilele generate de parametri. Este sarcina programatorului să
asigure iniţializarea lor cu valorile dorite. La ieşirea din subprogram
conţinutul variabilelor locale se pierde.

Un termen des folosit în practica limbajelor de programare este acela de


vizibilitate. A preciza vizibilitatea unei variabile înseamnă a spune care sunt
blocurile de unde se poate adresa (ca să-i atribuim o valoare, de exemplu).

 Variabilele globale sunt vizibile la nivelul programului principal şi la nivelul


subprogramelor.
var x:integer; Programul alăturat tipăreşte de trei ori
procedure a; 2, conţinutul variabilei globale x.
procedure b;
begin
writeln(x)
end;
begin
x:=2;
b;
writeln(x)
end;
begin
a;
x:=2;
writeln(x);
end.
Manual de informatică pentru clasa a XI-a 37

 Variabilele locale sunt vizibile doar la nivelul subprogramului în care au fost


declarate şi la nivelul subprogramelor definite în acesta.
procedure a; Programul alăturat tipăreşte de două ori 2,
var x:integer; conţinutul variabilei locale x. Dacă am fi
procedure b;
încercat să tipărim x din cadrul programului
begin
writeln(x) principal, programul ar fi dat eroare de
end; compilare.
begin
x:=2;
b;
writeln(x)
end;
begin
a;
end.

Atenţie! Există situaţii în care o variabilă globală nu poate fi adresată din


cadrul subprogramului, adică atunci când acesta conţine declaraţia unei alte
variabile, cu acelaşi nume. În acest caz, prin nume, se adresează variabila
locală. Regula este următoarea: în cazul în care două variabile au acelaşi
nume, dar vizibilităţi diferite, se adresează întotdeauna variabila cu
vizibilitatea mai redusă. Evident, în cazul existenţei a două variabile cu
aceeaşi vizibilitate şi acelaşi nume, compilatorul va da eroare de sintaxă.

Programul de mai jos tipăreşte 2, nu 3:


var x:integer;
procedure a;
begin
x:=2;
writeln(x)
end;
begin
x:=3;
a;
end.

Alt termen des utilizat în practica programării este durata de viaţă a unei
variabile. El se referă la perioada de timp în care variabila este alocată în memorie.
Astfel, avem:

 durată statică - variabila are alocat spaţiu pe tot timpul executării programului.
 durata locală - variabila are alocat spaţiu doar în timpul cât se execută un
anumit bloc.
 durată dinamică - alocarea spaţiului de memorie se face în timpul executării prin
subprograme speciale sau operatori (în Pascal avem subprograme speciale).
Variabilele cu această durată de viaţă se numesc dinamice şi nu se tratează în
acest manual.
38 Capitolul 2. Subprograme

Din acest punct de vedere putem spune că variabilele globale au durată de


viaţă statică, iar variabilele locale au durata de viaţă locală.

Un ultim termen este dat de clasa de memorare a unei variabile. Se referă


la locul unde variabila este memorată. Astfel, o variabilă poate fi memorată în
segmentul de date, în segmentul de stivă sau în heap.

Astfel, pe lângă tip, o variabilă se caracterizează prin: clasă de memorare,


vizibilitate şi durată de viaţă.

2.2.7. Greşeli frecvente

1. Greşeli care apar la transmiterea tipurilor nestandard

Reamintim faptul că un tip este standard dacă este cunoscut de limbaj (de
exemplu, integer). În caz contrar tipul este nestandard (de exemplu, tipul care
descrie un vector cu componente de tip real). Iată o procedură scrisă greşit:
procedure er (var v:array[1..99] of real);
...
begin
...
end;

Eroarea este dată de faptul că tipul nestandard al variabilei v este descris în


lista parametrilor. Programul va da eroare de sintaxă.
Pentru a proceda corect, tipul este descris separat, în cadrul programului
principal, prin type.
type vect=array[1..99] of real;
...
var v:vect;
...
procedure er (var v:vect);
...
begin
...
end;

2. Greşeli care apar la atribuirea valorii de retur a unei funcţii (valoarea pe


care funcţia o întoarce)

Iată o funcţie, scrisă eronat, care întoarce o valoare întreagă s (o sumă).


function suma( n,i:integer):integer;
var s:integer;
begin
calculez s;
.......
suma(n,i):=s;
end;
Eroarea constă în faptul că în atribuirea ”suma(n,i):=s;” au fost trecuţi
parametri. Corect este ”suma:=s;”.
Manual de informatică pentru clasa a XI-a 39

2.2.8. Unităţi de program

În activitatea practică se fac aplicaţii cu un număr mare de programe care de


multe ori sunt de mari dimensiuni. O preocupare constantă a informaticienilor este
de a pune la dispoziţia programatorilor instrumente eficiente de programare. În
acest sens, limbajul Pascal oferă posibilitatea lucrului cu unităţi de program
(unit-uri).

Definiţia 2.9. Prin unitate de program înţelegem un ansamblu de date,


proceduri şi funcţii, plasate şi compilate împreună, care pot fi uşor utilizate
în scrierea altor programe, fără a se cunoaşte amănunte necesare
realizării lor.

Forma generală a unei unităţi de program este următoarea:

UNIT <nume unitate>;


INTERFACE
[USES <numele altor unităţi de program pe care le utilizează>;]
[tipuri de date şi variabile globale în programul care utilizează
unitatea]
antete de proceduri şi funcţii

IMPLEMENTATION
[tipuri de date şi variabile locale (valabile numai pentru unitatea de
program)]
procedurile şi funcţiile utilizate de unitatea de program
[BEGIN
....... o eventuală secvenţă de program care se execută
....... în momentul declarării unităţii de program în
....... programul care o utilizează.
END.]

 Prima parte, obligatorie, este precedată de clauza ”INTERFACE”. Aici se


găsesc definiţiile tipurilor de date, variabilelor, şi sunt enumerate procedurile
şi funcţiile ce pot fi utilizate din programul principal. Pentru orice tip de
variabilă declarat aici, se pot declara variabile de acest tip atât în cadrul
unităţii cât şi în cadrul programului care foloseşte unitatea. Se observă că o
unitate de program are în mod obligatoriu două părţi, opţional, o a treia.
Toate procedurile şi funcţiile care aici sunt numai enumerate pot fi folosite de
orice program care foloseşte unitatea.

 A doua parte, obligatorie, se găseşte plasată sub clauza ”IMPLEMENTATION”.


Aici se pot declara tipurile şi variabilele vizibile numai pentru unitatea dată.
Tot aici se scriu clar toate procedurile şi funcţiile care în parte au fost numai
declarate, precum şi altele ce pot fi folosite numai în cadrul unităţii.

 Partea opţională a fost prezentată în cadrul formei generale a unităţii.


40 Capitolul 2. Subprograme

În continuare, vom da un exemplu pur didactic de unitate de program,


numită OPERATII. Aceasta conţine patru proceduri: de adunare, scădere,
înmulţire şi împărţire de variabile reale. Variabilele a şi b sunt globale
pentru programul care utilizează această unitate, iar variabila c poate fi utilizată
numai în cadrul unităţii. În secvenţa de iniţializare se citesc valorile variabilelor a şi
b.

Unitatea de program este prezentată în continuare:


unit operatii;

interface
var a,b:real;
procedure adun(a,b:real);
procedure scad(a,b:real);
procedure produs(a,b:real);
procedure impart(a,b:real);

implementation
var c:real;

procedure adun(a,b:real);
begin
c:=a+b;
writeln(c:3:2);
end;

procedure scad(a,b:real);
begin
c:=a-b;
writeln(c:3:2);
end;

procedure produs(a,b:real);
begin
c:=a*b;
writeln(c:3:2);
end

procedure impart(a,b:REAL);
begin
if b=0 then writeln('Impartirea nu se poate face ')
else
begin
c:=a/b;
writeln(c:3:2);
end;
end;

begin
write('a=');
readln(a);
writeln('b=');
readln(b);
end.
Manual de informatică pentru clasa a XI-a 41

Programul următor utilizează această unitate. Faptul că programul utilizează


o unitate se precizează prin clauza ”USES”. Rularea sa decurge în felul următor:

• se cer valorile variabilelor a şi b (declarate în unitate);

• se apelează procedurile de adunare, scădere, înmulţire şi împărţire iar


rezultatele se tipăresc;

• se apelează aceleaşi proceduri utilizând pentru apel două variabile


globale ale programului (c şi d).

uses operatii;
var c,d:real;
begin
adun(a,b);
scad(a,b);
produs(a,b);
impart(a,b);
write('c=');
readln(c);
write('d=');
readln(d);
adun(c,d);
scad(c,d);
produs(c,d);
impart(c,d);
readln;
end.

După ce am văzut modul de alcătuire şi construcţie a unei unităţi de


program, trebuie să precizăm faptul că limbajul Pascal este livrat cu câteva unităţi
de program standard (produse de firma BORLAND). Amintim câteva:

SYSTEM, CRT, DOS sau GRAPH.

Procedurile şi funcţiile puse la dispoziţie de limbaj se găsesc în aceste unităţi.


Toate subprogramele folosite de noi până în prezent se află în unitatea de program
SYSTEM. Pentru utilizarea lor nu este necesară clauza ”USES”.
42 Capitolul 2. Subprograme

2.3. Subprograme în C++


2.3.1. Exemple de utilizare a funcţiilor
 Problema 2.3. Se citeşte n, număr natural. Să se scrie programele care
tipăresc valoarea calculată a expresiilor:
n
1 1 1  1 1 1
E1 = 1 + + + ... + ; E 2 = 1 + + + ... +  .
2 3 n  2 3 n

 Rezolvare. În continuare prezentăm cele două programe care utilizează funcţia:


#include <iostream.h>
double subp(int n)
{ double s=0; int i;
for (i=1;i<=n;i++) s+=1./i;
return s;
}
main()
{ int n;
cout<<"n="; cin>>n;
cout<<subp(n);
}
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

#include <iostream.h>
double subp(int n)
{ double s=0;
int i;
for (i=1;i<=n;i++) s+=1./i; // sau s=s+1./i
return s;
}
main()
{ int n,i;
double rez, prod=1;
cout<<"n="; cin>>n;
rez=subp(n);
for (i=1;i<=n;i++) prod*=rez;
cout<<prod;
}

Funcţia care calculează E1 este:


double subp(int n)
{ double s=0;int i;
for (i=1;i<=n;i++) s+=1./i; // sau s=s+1./i
return s; }

 Antetul funcţiei este ”double subp(int n);”.


 Funcţia se numeşte ”subp”.
 Funcţia are un parametru numit n. Rolul său este important deoarece
precizează pentru ce valoare trebuie calculată expresia. Aşa cum vom
vedea, există posibilitatea să avem mai mulţi parametri, de diferite tipuri.
Manual de informatică pentru clasa a XI-a 43

 Funcţia are un anumit tip, care precizează natura rezultatului. În exemplu,


tipul este double întrucât expresia calculată este de acest tip.
 Funcţia are variabile proprii - adică variabile care sunt definite în cadrul ei. În
exemplu, s şi i. Aceste variabile se numesc variabile locale.
 Am văzut că funcţia întoarce un anumit rezultat - în exemplu, de tip double.
Observaţi mecanismul prin care am obţinut aceasta. Calculez expresia în
mod obişnuit. Rezultatul este reţinut de variabila locală s. Prin instrucţiunea
”return s;”, funcţia a primit ca valoare de retur conţinutul variabilei s.

În terminologia utilizată în teoria subprogramelor - în particular, în cazul


funcţiilor - se utilizează termenii parametri formali şi parametri efectivi.

Definiţia 2.10. Parametrii care se găsesc în antetul funcţiei se numesc


parametri formali.

Atunci când scriem o funcţie nu cunoaştem valoarea propriu-zisă a


parametrilor. Funcţia trebuie să întoarcă rezultatul corect, oricare ar fi valoarea lor.
Din acest punct de vedere ei se numesc formali.

Definiţia 2.11. Parametrii care se utilizează la apel se numesc parametri


efectivi.

La apel, lucrurile stau altfel: valorile acestora sunt cunoscute. Prin urmare,
aceştia se numesc parametri efectivi.

Pentru apelul ”rez=subp(n); ”, parametrul efectiv este n.

 Problema 2.4. Se citeşte n, număr natural. Se citeşte un vector cu n


componente numere reale. Se cere să se tipărească vectorul sortat.

#include <iostream.h>
void citesc(int vt[10],int n)
{ int i;
for(i=0;i<n;i++)
{ cout<<"v["<<i+1<<"]=";
cin>>vt[i];
}
}
void sortez(int vt[10],int n)
{ int gasit,i,man;
do
{ gasit=0;
for (i=0;i<n-1;i++)
if (vt[i]>vt[i+1])
{ man=vt[i];
vt[i]=vt[i+1];
vt[i+1]=man;
gasit=1; }
} while (gasit);
}
44 Capitolul 2. Subprograme

void scriu(int vt[10],int n)


{ int i;
for(i=0;i<n;i++)
cout<<vt[i]<<endl;
}
main()
{ int v[10],n;
cout<<"n=";
cin>>n;
citesc(v,n);
sortez(v,n);
scriu(v,n);
}

Programul conţine trei funcţii: citesc, sortez şi tiparesc.

 La apel, controlul programului este transferat la prima instrucţiune a funcţiei.


După executarea funcţiei, se revine în programul principal la prima
instrucţiune care urmează celei de apel - în cazul de faţă se întâlneşte o altă
instrucţiune de apel.

 Cele trei funcţii au tipul void, adică nu au valoare de retur. Ele returnează
rezultatul prin intermediul parametrilor.

2.3.2. Structura unei funcţii

În esenţă, o funcţie este alcătuită din:

 Antet - acesta conţine mai multe informaţii importante necesare


compilatorului: numele funcţiei, lista parametrilor formali, tipul rezultatului.

Structura antetului este:


tip nume(lista parametrilor formali)

Lista parametrilor formali este de forma:


parametru1, parametru2, ..., parametrun

Există şi posibilitatea ca lista parametrilor formali să fie vidă. Fiecare


parametru are forma:

tip nume.

 O instrucţiune compusă - aceasta cuprinde declaraţiile variabilelor locale,


şi instrucţiunile propriu-zise.

Poate fi tip al unei funcţii orice tip de dată cu excepţia masivelor.


Manual de informatică pentru clasa a XI-a 45

Exemple de antete:
- int suma(int a, int b) - funcţia se numeşte suma, returnează un
rezultat de tip int şi are doi parametri formali de tip int, numiţi a şi b.
- void t(int n, float v[20]) - funcţia se numeşte t, este de tip void
(nu returnează rezultat prin nume), are doi parametri formali, primul numit n, de
tip int, al doilea numit v, de tip float* (un tip care reţine adrese de vectori
cu elemente de tipul float).

 O funcţie returnează rezultatul la întâlnirea instrucţiunii return, care are


forma:
return expresie;

Trebuie ca tipul expresiei să coincidă cu tipul funcţiei.

La întâlnirea instrucţiunii return, după atribuirea valorii, execuţia funcţiei se


încheie şi se revine la funcţia care a apelat-o. În absenţa instrucţiunii return,
execuţia funcţiei se încheie după execuţia ultimei instrucţiuni. În acest caz nu se
întoarce nici o valoare.

 O funcţie poate fi apelată de sine stătător (prin nume şi lista parametrilor


efectivi), dar poate fi inclusă şi în cadrul expresiilor, caz în care, la evaluarea
expresiei este apelată. Această ultimă formă de apel nu este valabilă în cazul
funcţiilor de tip void.

#include <iostream.h> În programul alăturat este apelată o


funcţie care calculează produsul a
int prod (int x, int y) două numere întregi.
{ return x*y; }
main() Programul tipăreşte suma între 1 şi
{ int x=2,y=3; produsul calculat. În exemplu, 7.
cout<<1+prod(x,y);
}

Apelul funcţiei s-a realizat din interiorul expresiei: ”1+prod(x,y)”.

Observaţii

 În cadrul expresiei, apelul este un operand. El intră în calcul cu valoarea


returnată de funcţie.

 După apelul funcţiei se continuă evaluarea expresiei.

 La apel, ordinea de evaluare a parametrilor nu este definită. De exemplu,


dacă apelăm funcţia test astfel:

test(2-3,2+3),

nu ştim dacă înainte se efectuează scăderea sau adunarea.


46 Capitolul 2. Subprograme

2.3.3. Declararea variabilelor

Până în prezent am declarat variabile doar în corpul funcţiilor - inclusiv în cel


al funcţiei main(). Variabilele declarate astfel se numesc locale.

 Fiecărui program i se alocă trei zone distincte în memoria internă în care se


găsesc memorate variabilele programului:

Segment de date

Segment de stivă

Heap

Figura 2.5. Cele trei zone în memoria internă

De asemenea, există posibilitatea ca variabilele să fie memorate într-un


anumit registru al microprocesorului. În acest caz timpul de acces la astfel de
variabile este foarte mic, deci se pot obţine programe optimizate.

 În general, o variabilă se caracterizează prin 4 atribute. Acestea sunt:

• clasa de memorare;
• vizibilitate;
• durata de viaţă;
• tipul variabilei, singurul pe care l-am studiat până în prezent.

1. Clasa de memorare - precizează locul unde este memorată variabila


respectivă. O variabilă poate fi memorată în segmentul de date, în cel de
stivă, în heap sau într-un registru al microprocesorului.

2. Vizibilitatea - precizează liniile textului sursă din care variabila respectivă


poate fi accesată.

Astfel avem:

a. Vizibilitate la nivel de bloc (instrucţiune compusă).

b. Vizibilitate la nivel de fişier - în cazul în care programul ocupă un


singur fişier sursă, singurul caz pe care îl tratăm acum.

c. Vizibilitate la nivel de clasă - este în legătură cu programarea pe


obiecte, pe care o veţi studia individual.
Manual de informatică pentru clasa a XI-a 47

3. Durata de viaţă - reprezintă timpul în care variabila respectivă are alocat


spaţiu în memoria internă. Astfel avem:

a. Durată statică - variabila are alocat spaţiu în tot timpul execuţiei


programului.
b. Durată locală - variabila are alocat spaţiu în timpul în care se execută
instrucţiunile blocului respectiv.
c. Durată dinamică - alocarea şi dezalocarea spaţiului necesar variabilei
respective se face de către programator prin operatori sau funcţii
speciale.

În C++ variabilele pot fi împărţite în trei mari categorii: globale, locale şi


dinamice. Noi studiem numai primele două categorii, variabilele globale şi locale.

A) Variabile globale

Acestea se declară în afara corpului oricărei funcţii, ca mai jos:


#include <iostream.h>
int a;
int t()
{ a=3;
cout<<a;
}
int b;
main()
{ b=4;
cout<<a<<endl;
t();
}

Variabilele a şi b sunt globale. În astfel de cazuri, variabilele respective pot


fi utilizate de toate funcţiile care urmează în textul sursă declaraţiei variabilei
respective. Din acest motiv, astfel de variabile se numesc globale.

La declarare, variabilele globale sunt iniţializate cu 0.

Atributele variabilelor globale sunt:

1. Clasa de memorare - segmentul de date.

2. Vizibilitatea - În cazul în care declaraţiile acestora sunt înaintea tuturor


funcţiilor, acestea sunt vizibile la nivelul întregului program (fişier). Dacă anumite
funcţii se află plasate înaintea declaraţiilor acestor variabile, atunci ele sunt vizibile
doar pentru funcţiile care sunt plasate după aceste declaraţii. În exemplul anterior,
variabila a poate fi accesată din corpul oricărei funcţii, dar variabila b poate fi
accesată doar din funcţia main().
48 Capitolul 2. Subprograme

3. Durata de viaţă a variabilelor globale este statică. Ele au spaţiu rezervat în tot
timpul execuţiei programului.

B) Variabile locale

Acestea sunt declarate în corpul funcţiilor. Mai precis, pot fi declarate în orice
bloc (instrucţiune compusă) al acestora.

Variabilele declarate în corpul funcţiei main() sunt tot locale. În programul


următor, variabilele a şi b sunt locale.

Variabila a este declarată în corpul funcţiei t(), iar variabila b este declarată
în corpul funcţiei main().

Exemplu:
void t()
{ int a=3;
}
main()
{ int b=4;
}

1. Clasa de memorare a variabilelor locale este implicit segmentul de stivă.


Există posibilitatea ca acestea să fie alocate în registrele microprocesorului, caz în
care declaraţia lor trebuie precedată de cuvântul cheie ”register”.

Exemplu:
register int b=4;

Variabilele locale nu sunt iniţializate implicit cu 0. În ipoteza în care acestea


nu sunt iniţializate explicit de programator, reţin o valoare oarecare, numită
valoare reziduală.

2. Vizibilitatea variabilelor locale este la nivelul blocului în care au fost declarate.

În funcţia următoare am declarat două variabile de tip int, numite b şi c.


Variabila b este vizibilă la nivelul funcţiei, dar variabila c este vizibilă doar la nivelul
blocului în care a fost declarată.

Exemplu:
void t()
{ int b=4;
{ int c=3;
cout<<b<<" "<<c;
}
}
Manual de informatică pentru clasa a XI-a 49

În programul următor am declarat trei variabile, toate numite a. Una este


globală, iar două sunt locale, dar declarate în blocuri diferite:
#include <iostream.h>
int a;
void t()
{ int a=4;
{ int a=3;
cout<<a<<endl;
}
cout<<a<<endl;
}
main()
{ a=5;
t();
cout<<a;
}

În program, se afişează conţinutul tuturor acestor variabile (3,4,5).

Observaţii

 În cazul în care, într-un anumit bloc sunt vizibile (se pot accesa) mai multe
variabile, toate cu acelaşi nume, dar au domenii de vizibilitate diferite, se
accesează variabila cu vizibilitatea cea mai mică. De exemplu, dacă în
programul anterior se tipăreşte variabila a din cadrul subblocului funcţiei, se
tipăreşte 3, pentru că acesta este conţinutul variabilei cu cea mai mică
vizibilitate (cea declarată în subblocul respectiv).

 Există posibilitatea ca, un ciclu for să conţină declaraţia unei variabile locale.
În secvenţa următoare se calculează suma primelor 4 numere naturale.
Variabila i este declarată (şi în consecinţă vizibilă) doar în blocul for:

int n=4, s=0;


for (int i=1;i<=n;i++) s+=i;
cout<<s;

3. Durata de viaţă a variabilelor locale este atât timp cât durează execuţia blocului
respectiv.

2.3.4. Transmiterea parametrilor

Aşa cum am arătat, parametrii care se găsesc în antetul funcţiei se numesc


parametri formali, iar cei care se găsesc în instrucţiunea de apel se numesc
parametri efectivi.

Priviţi programul următor. Acesta conţine o funcţie care calculează suma


a două numere naturale.
50 Capitolul 2. Subprograme

#include <iostream.h> Parametrii formali sunt a şi b.


int suma(int a , int b) Funcţia este apelată de mai
{ return a+b; multe ori. Parametrii efectivi sunt,
} pe rând:
main()  2, 3;
{ int c=4,d=3;  2+7, 3-1*2;
cout<<suma(2,3)<<endl;
cout<<suma(2+7,3-1*2)<<endl;
 c, d;
cout<<suma(c,d)<<endl;  1.9, 3.3.
cout<<suma(1.9,3.3)<<endl;
}

După fiecare apel al funcţiei se tipăreşte suma obţinută. Între parametrii


formali şi cei efectivi trebuie să existe o anumită concordanţă, care este descrisă
prin regulile următoare:

• Numărul parametrilor formali trebuie să coincidă cu numărul


parametrilor efectivi. La această regulă există o excepţie care va fi
prezentată într-un paragraf separat. În exemplul dat, numărul
parametrilor formali este 2, iar cel al parametrilor efectivi este tot 2
(pentru un apel).

• Tipul parametrilor formali trebuie să coincidă cu tipul parametrilor


efectivi sau tipul parametrilor efectivi să poată fi convertit implicit
către tipul parametrilor formali, la fel ca în cazul atribuirii.

- suma(2,3) - parametrii formali sunt expresii constante de tip întreg;

- suma(2+7,3-1*2) - parametrii formali sunt expresii constante de tip întreg;


în acest caz, înainte de apel se evaluează expresiile de mai sus;

- suma(c,d) - parametrii formali sunt expresii de tip întreg (dat de tipul


variabilelor);

- suma(1.9, 3.3) - parametrii formali sunt expresii constante de tip real. În


acest caz, ele sunt convertite către int la fel ca în cazul atribuirii (se
trunchiază zecimala). Prin urmare, suma calculată este 4=1+3.

Nu este obligatoriu ca numele parametrilor formali să coincidă cu numele


parametrilor efectivi.

Vom analiza modul în care sunt memoraţi parametrii transmişi în momentul


lansării în execuţie a funcţiei.

 Pentru memorarea parametrilor subprogramele folosesc segmentul de


stivă, întocmai ca pentru variabilele locale.

 Memorarea parametrilor transmişi se face în ordinea în care aceştia


figurează în antet: de la stânga la dreapta.
Manual de informatică pentru clasa a XI-a 51

 În cadrul subprogramului, parametrii transmişi şi memoraţi în stivă sunt


variabile. Numele lor este cel din lista parametrilor formali.

 Variabilele obţinute în urma memorării parametrilor transmişi sunt variabile


locale.

Iată, de exemplu, cum sunt memoraţi în stivă parametrii în cazul primului apel:

stiva 2 3

a b

Observaţii

 La revenirea în blocul apelant, conţinutul variabilelor memorate în stivă se


pierde - după cum ştim, durata de viaţă a variabilelor locale este locală.

 Memorarea în stivă are şi alte consecinţe, dar acestea vor fi tratate la


momentul potrivit.

Există două mecanisme de transmitere a parametrilor, transmiterea prin


valoare şi transmiterea prin referinţă. Acestea vor fi tratate în continuare.

A. Transmiterea prin valoare se utilizează atunci când suntem interesaţi ca


subprogramul să lucreze cu acea valoare, dar în prelucrare, nu ne
interesează ca parametrul efectiv (cel din blocul apelant) să reţină valoarea
modificată în subprogram.

Se pot transmite prin valoare:

1. Valorile reţinute de variabile;


2. Expresii (acestea pot conţine şi funcţii).

1. Valorile reţinute de variabile. În acest caz, parametrii efectivi trebuie să fie


numele variabilelor care se trimit prin valoare.

Exemplu:
#include <iostream.h>
void test (int n)
{ n+=1;
cout<<n<<endl;
}
main()
{ int n=1;
test(n);
cout<<n<<endl;
}

Parametrul n este transmis prin valoare.


52 Capitolul 2. Subprograme

 În main(), avem declarată variabila n, care este iniţializată cu 1.

 Apelăm funcţia. La apel, se rezervă spaţiu în stivă, spaţiu care are numele
parametrului (deci tot n) şi este iniţializat cu valoarea memorată de variabila
n a programului principal. În acest moment avem două variabile n şi ambele
reţin valoarea 1.

 În funcţie, variabila n este incrementată (adică la vechiul conţinut se adaugă 1).


Evident, este vorba de variabila rezervată în cadrul ei.

 Tipărim conţinutul variabilei n (cea din stivă), deci se tipăreşte 2.

 La ieşirea din funcţie, variabila n (din stivă) se pierde - adică nu mai are
spaţiu alocat. Prin urmare, valoarea 2 este pierdută.

 În main() se tipăreşte conţinutul variabilei n, adică 1.

2. Parametrii efectivi sunt expresii care mai întâi se evaluează.

Exemplu:
#include <iostream.h>
void test (int n)
{ cout<<n<<endl;
}
main()
{ test(3);
test(3+4*5);
}

 Transmiterea prin valoare a masivelor permite ca funcţiile să întoarcă noile


valori ale acestora (care au fost atribuite în funcţii). De ce? Numele masivului
este un pointer către componentele lui. Prin valoare se transmite acest
nume. Cu ajutorul acestuia accesăm componentele masivului.

În programul următor funcţia vector iniţializează vectorul transmis ca


parametru, iar în main() se afişează rezultatul:

#include <iostream.h>
void vector (int x[10])
{ for (int i=0;i<10;i++) x[i]=i;
}
main()
{ int a[10];
vector(a);
for (int i=0;i<10;i++) cout<<a[i]<<" ";
}
Manual de informatică pentru clasa a XI-a 53

B. Transmiterea prin referinţă. Parametrii sunt transmişi prin referinţă atunci


când ne interesează ca la revenirea din subprogram, variabila transmisă să reţină
valoarea stabilită în timpul execuţiei subprogramului.

 În cazul transmiterii prin referinţă, parametrii efectivi trebuie să fie referinţe la


variabile.

 În cazul transmiterii prin referinţă, subprogramul reţine în stivă adresa


variabilei.

Programul următor utilizează o funcţie care interschimbă valorile reţinute


de două variabile. Acestea sunt transmise prin referinţă.

#include <iostream.h>
void intersc(int &a,int &b)
{ int man=a;
a=b;
b=man;
}
main()
{ int x=2,y=3;
intersc(x,y);
cout<<x<<" "<<y;
}

2.3.5. Definirea şi declararea unui subprogram

Deşi aparent asemănătoare, cele două noţiuni diferă. Buna înţelegere a lor
ne ajută să evităm anumite erori. Mai mult, aceste noţiuni sunt utilizate de orice
limbaj de programare, nu numai în C++.

Definiţia 2.12. A defini un subprogram, înseamnă a-l scrie efectiv, după


structura anterior prezentată. O problemă importantă este locul unde se
defineşte subprogramul.

Definiţia 2.13. A declara un subprogram, înseamnă a-l anunţa. Un


subprogram nedeclarat nu poate fi folosit.

Definiţia unui subprogram ţine loc şi de declaraţie!

Programul următor conţine două funcţii: s1 şi s2. Definiţiile ambelor


funcţii se găsesc înaintea funcţiei main(). Din acest motiv ele pot fi
apelate din main(). Definiţia funcţiei s1 este înaintea definiţiei lui s2,
deci funcţia s1 poate fi apelată din s2. În schimb, din s1 nu poate fi
apelată funcţia s2, pentru că definiţia lui s2 este după cea a lui s1.
54 Capitolul 2. Subprograme

#include <iostream.h>
void s1()
{ cout<<"Eu sunt s1"<<endl;
}
void s2()
{ s1(); cout<<"Eu sunt s2"<<endl;
}
main()
{ s1();
s2();
}

În situaţia prezentată, totuşi se poate ca s1 să apeleze pe s2, chiar dacă


sunt definite în aceeaşi ordine.

În astfel de cazuri se foloseşte prototipul funcţiei (antetul urmat de “;“).


Prototipul are rolul de a declara o funcţie. El nu conţine definiţia acesteia.

#include <iostream.h>
void s2();
void s1()
{ s2(); cout<<"Eu sunt s1"<<endl;
}
void s2()
{ cout<<"Eu sunt s2"<<endl;
}
main()
{ s1();
}

Programatorii în C++ obişnuiesc să scrie mai întâi prototipurile tuturor


funcţiilor utilizate de program - fără main() - iar după funcţia main() să le
definească. În acest fel, orice funcţie - mai puţin main() - poate fi apelată
din oricare alta:
#include <iostream.h>
void s1(); // prototip s1
void s2(); // prototip s2
main()
{ s1();
}
void s1()
{ s2(); cout<<"Eu sunt s1"<<endl;
}
void s2()
{ s1(); cout<<"Eu sunt s2"<<endl;
}
Manual de informatică pentru clasa a XI-a 55

2.4. Aplicaţii care folosesc subprograme

 Aplicaţia 2.1. Revenim la exemplul de funcţie dat în paragraful 2.1. Acesta va fi


prezentat în totalitate. Se consideră funcţia:

 x +1
1 + x 2 , pentru x ∈ [-1,1];

f ( x ) =  x + 1, pentru x ∈ (-∞,-1); .
 6
 1+ x , pentru x ∈ (1, ∞ ).

Se citesc două valori reale a şi b. Să se scrie un program care afişează care


dintre valorile f(a) şi f(b) este cea mai mare.

 Rezolvare. Programul este prezentat în continuare:


Varianta Pascal Varianta C++
var a,b,va,vb:real; # include <iostream.h>
function f(x:real):real; float f(float x)
var y:real; { float y;
begin y=x+1;
y:=x+1; if(x<-1) return y;
if x<-1 then f:=y else if(x>1) return 6/y;
else if x>1 then f:=6/y else return y/(1+x*x);
else f:=y/(1+x*x) }
end;
void main()
begin { float a,b,va,vb;
write('a='); readln(a); cout<<"a="; cin>>a;
write('b='); readln(b); cout<<"b="; cin>>b;
va:=f(a); va=f(a);
vb:=f(b); vb=f(b);
if va>vb if (va>vb) cout<<va<<endl;
then writeln(va) else cout<<vb<<endl;
else writeln(vb) }
end.

 Aplicaţia 2.2. Se citesc două numere întregi m şi n. Se cere să se tipărească


cel mai mare divizor comun şi cel mai mic multiplu comun al lor.

 Rezolvare. Cunoaşteţi deja modul în care se calculează cel mai mare divizor
comun pentru două numere naturale date. Cel mai mic multiplu comun a două
numere se poate determina împărţind produsul lor la cel mai mare divizor comun al
lor. Prin urmare, scriem o funcţie cmmdc cu doi parametri formali m şi n, care
întoarce o valoare întreagă - cel mai mare divizor comun al lor.
56 Capitolul 2. Subprograme

Varianta Pascal Varianta C++


var m,n:integer; #include <iostream.h>
function cmmdc(m,n:integer) int cmmdc(int m, int n)
:integer; { while (m!=n)
begin if (m>n) m-=n;
while m<>n do else n-=m;
if m>n then m:=m-n return m;
else n:=n-m; }
cmmdc:=m;
end; void main()
{ int cm,m,n;
begin cout<<"m="; cin>>m;
write ('m='); readln(m); cout<<"n="; cin>>n;
write ('n='); readln(n); cm=cmmdc(m,n);
writeln(cmmdc(m,n)); cout<<cm<<" "<<m*n/cm;
writeln(m*n div cmmdc(m,n)); }
end.

 Exerciţii
1. Găsiţi două motive pentru care parametrii funcţiei cmmdc trebuie să fie transmişi
prin valoare şi nu prin referinţă.
2. Scrieţi un subprogram cmmmc care să returneze cel mai mic multiplu comun a
două numere naturale. Stabiliţi locul unde trebuie integrat acesta în programul dat
şi modificaţi programul principal astfel încât să se utilizeze subprogramul cerut.
3. Câte expresii diferite care au ca rezultat cel mai mare divizor comun al
numerelor naturale nenule a, b şi c există? În orice expresie se utilizează numai
apeluri ale funcţiei cmmdc şi variabilele a, b şi c. De exemplu, trei expresii sunt:
cmmdc(cmmdc(a,b),c),
cmmdc(cmmdc(b,a),c) şi
cmmdc(c,cmmdc(a,b)).

 Aplicaţia 2.3. Se citeşte un vector cu n componente numere întregi. Se cere să


se tipărească cel mai mare divizor comun al valorilor reţinute de vector.

 Rezolvare. Dispunem deja de o funcţie care calculează cel mai mare divizor
comun a două numere şi am testat-o în programul anterior. Pentru varietate, de
data aceasta implementăm o funcţie cu algoritmul lui Euclid pentru calculul celui
mai mare divizor comun, algoritm prin împărţiri, nu prin scăderi repetate. Cu
ajutorul ei calculăm cel mai mare divizor comun pentru valorile reţinute de primele
două componente şi memorăm această valoare în variabila c. Apoi, pentru fiecare
dintre componentele următoare, se calculează cel mai mare divizor comun între
valoarea curentă memorată în c şi cea reţinută de componenta curentă a
vectorului. Cel mai mare divizor comun va fi reţinut din nou de c.
Manual de informatică pentru clasa a XI-a 57

Varianta Pascal Varianta C++


var v:array[1..20]of integer; #include <iostream.h>
n,i,c:integer; int v[21],n;
function cmmdc(m,n:integer) int cmmdc(int m, int n)
:integer; { int r;
var r:integer; do { r=m%n;
begin m=n;
repeat n=r;
r:=m mod n; } while (r!=0)
m:=n; n:=r return m;
until r=0; }
cmmdc:=m
end; void main()
{ int i,c;
begin cout<<"n="; cin>>n;
write ('n='); readln(n); cout<<"Componente vector:";
write('Componente vector:'); for(i=1;i<=n;i++)
for i:=1 to n do read(v[i]); cin>>v[i];
c:=cmmdc(v[1],v[2]); c=cmmdc(v[1],v[2]);
for i:= 3 to n do for(i=3;i<=n;i++)
c:=cmmdc(c,v[i]); c=cmmdc(c,v[i]);
writeln(c) cout<<c;
end. }

 Exerciţiu
În locul secvenţei
c  cmmdc(v[1],v[2])
pentru i=3,n execută
c  cmmdc(c,v[i])
sfârşit pentru

dorim să utilizăm, mai compact, secvenţa:


pentru i=1,n execută
c  cmmdc(c,v[i])
sfârşit pentru

Care este valoarea pe care trebuie să o aibă variabila c înaintea acestei secvenţe?

 Aplicaţia 2.4. Triunghi special. Se citesc două numere naturale m şi n mai


mici decât 10. Se cere ca programul dvs. să afişeze un triunghi, după regulile pe
care le deduceţi din exemplele următoare:
2. m=7 n=9
1. m=6 n=1
9
1 1 2
2 3 3 4 5
4 5 6 6 7 8 9
7 8 9 1 1 2 3 4 5
2 3 4 5 6 6 7 8 9 1 2
7 8 9 1 2 3 3 4 5 6 7 8 9 Junior Division
58 Capitolul 2. Subprograme

 Rezolvare. Se observă că dacă n=5 trebuie să obţinem şirul 5, 6, 7, 8,


9, 1, 2, 3,... Problema este de a putea număra începând de la n, iar când
s-a depăşit valoarea 9, de a relua numărarea de la 1. Vom utiliza o funcţie (succ)
care primeşte ca parametru de intrare un număr k şi returnează valoarea
următoare, în logica prezentată mai sus.
Alt aspect al problemei este realizarea tipăririi acestor valori pe m linii, linia 1
având o singură valoare, linia a doua două valori etc. Algoritmul este implementat
în procedura tipar, care are ca parametri formali pe m şi n, transmişi prin valoare.
Valorile lor sunt citite în programul principal.

Varianta Pascal Varianta C++


var m,n:integer; #include <iostream.h>
function suc(k:integer):integer; int suc(int k)
begin { if (k==9) return 1;
if k=9 then succ:=1 else return k+1;
else succ:=k+1 }
end;
void tipar(int m,int n)
procedure tipar(m,n:integer); { int i,j;
var i,j:integer; for(i=1;i<=m;i++)
begin { for (j=1;j<=i;j++)
for i:=1 to m do begin { cout<<n;
for j:=1 to i do begin n=suc(n);
write (n); }
n:=suc(n) cout<<endl;
end; }
writeln }
end
end; void main()
{ int m,n;
begin cout<<"m="; cin>>m;
write('m='); readln(m); cout<<"n="; cin>>n;
write('n='); readln(n); tipar(m,n);
tipar(m,n) }
end.

 Exerciţii
1. După apelul tipar(m,n) din programul principal, dacă se adaugă o instruc-
ţiune care afişează valoarea lui n, ce valoare estimaţi că se va tipări?
2. Cum trebuie să arate definiţia subprogramului suc, dacă în subprogramul
tipar, în loc de nsuc(n), se utilizează instrucţiunea suc(n)?
3. Cum putem defini subprogramul suc astfel încât secvenţa de afişare din
subprogramul tipar să fie:
pentru i=1,m execută
pentru j=1,i execută
scrie suc(n)
sfârşit pentru
scrie EOLN
sfârşit pentru
Manual de informatică pentru clasa a XI-a 59

 Aplicaţia 2.5. Se citesc două numere naturale m<n. Se cere să se tipărească


toate numerele palindrom aflate între m şi n. Un număr este palindrom dacă, citit de
la stânga la dreapta şi citit de la dreapta către stânga, rezultatul este acelaşi. De
exemplu, numărul 121 este palindrom.

 Rezolvare. Să analizăm problema. Este necesar să fie testate toate numerele


între m şi n, pentru ca apoi să fie tipărite numai cele care îndeplinesc condiţia cerută.

Pentru fiecare număr, avem subproblema verificării proprietăţii de număr


palindromic. Pentru aceasta, vom scrie o funcţie numită palin care are rolul de a
testa dacă un număr este sau nu palindrom. Analiza proprietăţii de număr
palindromic o efectuăm construind răsturnatul (inversul) acelui număr şi verificăm
dacă cele două valori (numărul şi răsturnatul său) sunt egale.

Construirea ”inversului“ (nu în sens matematic) unui număr natural este şi ea


o subproblemă a problemei verificării proprietăţii de palindrom. Vom scrie o funcţie
invers, care va returna inversul unui număr natural transmis ca parametru.

Urmărind apelurile, vom constata că programul principal apelează funcţia


palin, iar funcţia palin apelează la rândul ei funcţia invers. Programul apelant
al funcţiei palin este programul principal, în timp ce programul apelant al funcţiei
invers este subprogramul palin.

Care este avantajul scrierii fiecărei funcţii? Acesta este dat de faptul că,
atunci când scriem funcţia, ne concentrăm exclusiv asupra ei, deci posibilitatea de
a greşi este mai mică. Dacă dispunem de funcţie, algoritmul în continuare devine
mult mai simplu. În plus, este posibil ca oricare dintre funcţii să poată fi folosită şi în
alte cazuri, la rezolvarea altor probleme.

Observaţii

 Ordinea definirii celor două subprograme devine importantă, deoarece funcţia


invers, de care are nevoie funcţia palin, trebuie declarată anterior acesteia.
 Faptul că numărul al cărui invers se calculează este transmis subprogramului
prin valoare este iarăşi un avantaj. Cu toate că algoritmul ce construieşte
inversul lui n distruge de fapt valoarea acestuia, acest lucru nu este resimţit de
programul apelant.

Varianta Pascal Varianta C++


function invers(n:longint):longint; #include <iostream.h>
var r:longint;
begin long invers(long n)
r:=0; { long r=0;
while n<>0 do begin while(n!=0) {
r:=r*10+n mod 10; r=r*10+n%10;
n:=n div 10 n/=10;
end; }
invers:=r return r;
end; }
60 Capitolul 2. Subprograme

function palin(n:longint):boolean; int palin(long n)


begin { return invers(n)==n;
palin:=invers(n)=n; }
end;
void main()
var m,n,i:integer; { int m,n,i;
begin cout<<"m="; cin>>m;
write('m='); readln(m); cout<<"n="; cin>>n;
write('n='); readln(n); for(i=m;i<=n;i++)
for i:=m to n do if (palin(i))
if palin(i) then writeln(i); cout<<i<<endl;
end. }

 Exerciţii
1. Modificaţi antetul funcţiei invers, transformând parametrul n în parametru
transmis prin referinţă şi urmăriţi efectele modificării efectuate.

2. Modificaţi antetul funcţiei invers, transformând parametrul n în parametru


transmis prin referinţă şi testaţi proprietatea de palindrom folosind compararea
n=invers(n) în loc de invers(n)=n. Explicaţi de ce această modificare nu
reprezintă o corecţie a erorii apărute la cerinţa 1?

3. Un număr natural este superpalindrom dacă este palindrom atât el cât şi


pătratul său. Scrieţi, pe baza programului dat, un program care listează toate
numerele cu această proprietate aflate între doi întregi a şi b (a<b<32000).

 Aplicaţia 2.6. Suma cuburilor cifrelor. Se citeşte un număr natural


n≤9999999. Se calculează suma cuburilor cifrelor sale. Exemplu: dacă se citeşte
25, se calculează 23+53=133. Cu numărul obţinut procedăm la fel: 13+33+33=55.
Repetăm procedeul: 53+53=250. Şi iar: 23+53+03=133. Repetăm procedeul până
ce obţinem un număr care este deja prezent în seria generată. Se cere să se
afişeze şirul de valori calculate prin acest procedeu.

Exemplu: pentru n=25, se afişează seria

25 133 55 250 133.

Junior Division

 Rezolvare. Suma cuburilor cifrelor unui număr natural este calculată de funcţia
suma. Numărul n (citit) va fi memorat de prima componentă a vectorului seria.
Apoi, suma cuburilor sale, de componenta a doua, suma cuburilor numărului reţinut
de a doua componentă va fi reţinută de a treia componentă, ş.a.m.d. După ce este
memorată o valoare, ea este comparată, pe rând, cu cele obţinute anterior. Dacă
este regăsită, se tipăreşte numărul de termeni ai seriei şi seria propriu-zisă.
Funcţia suma calculează suma cuburilor cifrelor unui număr. Funcţia este verifică
dacă valoarea n există deja în şirul s format din ls componente.
Manual de informatică pentru clasa a XI-a 61

Varianta Pascal Varianta C++


type vec=array[1..100] of longint; #include <iostream.h>
var n:longint; seria:vec;
ind,i:integer; long n;
int seria[100],ind,i;
function suma(n:longint):longint;
var s,i,c:integer; int suma(int n)
begin { int s=0,c;
s:=0; while (n) {
while n<>0 do c=n%10;
begin s+=c*c*c;
c:=n mod 10; n/=10;
s:=s+c*c*c; }
n:=n div 10 return s;
end; }
suma:=s int este(long n,int s[100],
end; int ls)
function este(n:longint;var s:vec; { int i;
ls:integer):boolean; for(i=0;i<ls;i++)
var i:integer; if(s[i]==n) return 0;
begin return 0;
este:=true; }
for i:=1 to ls do void main()
if s[i]=n then este:=false { cout<<"n="; cin>>n;
end; ind=0;
begin seria[ind]=n;
write('n='); readln(n); do {
seria[1]:=n; n=suma(seria[ind-1]);
ind:=1; seria[++ind]=n;
repeat } while(! este(n,seria,
n:= suma(seria[ind-1]); ind-1));
ind:=ind+1; for (i=1;i<=ind;i++)
seria[ind]:=n; cout<<seria[i]<<endl;
until este(n,seria,ind-1); }
for i:=1 to ind do
writeln(seria[i]);
end.

 Aplicaţia 2.7. Să se scrie o procedură care citeşte o matrice cu elemente


numere întregi din fişierul text MAT.TXT. Pe prima linie a fişierului sunt două valori:
m (numărul de linii ale matricei) şi n (numărul de coloane ale matricei). Următoarele m
linii ale fişierului conţin, în ordine, elementele aflate pe fiecare linie a matricei.
Verificaţi subprogramul definit, integrându-l într-un program de testare.

Exemplu: fişierul f.in conţine:

3 4
3 1 7 9
1 2 3 4
9 1 3 8
62 Capitolul 2. Subprograme

 Rezolvare. Pentru a putea fi folosită pentru citirea oricărei matrice de numere


întregi, procedura va trebui să utilizeze ca parametri de ieşire matricea, numărul de
linii şi numărul de coloane ale matricei.

În programul următor se testează procedura, prin listarea matricei:

Varianta Pascal Varianta C++


type mat=array[1..100,1..100] #include <fstream.h>
of integer;
var m:mat; void citesc(int mat[100][100],
linii,coloane,i,j:integer; int& m, int& n)
{ fstream f("fis.txt",ios::in);
procedure citesc(var t:mat; f>>m>>n;
var m,n:integer); for (int i=1;i<=m;i++)
var i,j:integer; for (int j=1;j<=n;j++)
f:text; f>>mat[i][j];
begin f.close();
assign(f,'mat.txt'); }
reset(f);
readln(f,m,n); void main()
for i:=1 to m do { int m,n, mat[100][100];
for j:=1 to n do citmat(m,n,mat);
read(f,t[i,j]); for (int i=1;i<=m;i++)
close(f) { for (int j=1;j<=n;j++)
end; cout<<mat[i][j]<<" ";
cout<<endl;
begin }
citesc(m,linii,coloane); }
for i:= 1 to linii do
begin
for j:=1 to coloane do
write(m[i,j],' ');
writeln
end
end.

Probleme propuse

1. Scrieţi un program care calculează şi afişează valoarea fiecăreia dintre expresiile


de mai jos, pentru x citit de la tastatură:

a) G(x) = sin(x)+cos(x)∗cos(2∗x);

b) H(x) = 10∗{x} (am notat prin {x} partea fracţionară a lui x).
Manual de informatică pentru clasa a XI-a 63

2. Coifurile NFL . Există 28 de echipe de fotbal în NFL (Liga Naţională


de Fotbal în SUA). Unele magazine au aparate care oferă coifuri în
miniatură ale echipelor contra 0.25$. Când pui moneda în aparat, nu
ştii ce coif vei obţine, oricare din cele 28 de coifuri diferite îţi poate veni
din aparat (sunt oferite aleator). Scrieţi un program care simulează punerea
monezii într-un astfel de aparat până când se obţin toate cele 28 de coifuri.
Programul trebuie să tipărească şi totalul cheltuielilor efectuate pentru obţinerea
celor 28 de coifuri.
Junior Division

 Indicaţie. Utilizaţi subprogramele predefinite Random şi Randomize.


3. Scrieţi o funcţie care primeşte ca parametru lungimea laturii unui pătrat şi
returnează aria sa.
4. Scrieţi un subprogram care primeşte ca parametru lungimea laturii unui pătrat şi
returnează atât lungimea diagonalei, cât şi perimetrul pătratului.
5. Scrieţi o funcţie care primeşte ca parametri de intrare lungimile celor două catete
ale unui triunghi dreptunghic şi returnează lungimea ipotenuzei.
6. Scrieţi o funcţie care primeşte 3 parametri de tip real, cu semnificaţia de lungimi
pentru 3 segmente. Funcţia va returna 1 dacă cele trei segmente pot forma un
triunghi şi 0, în caz contrar.

7. Scrieţi o funcţie care returnează prima cifră a unui număr natural. De exemplu,
dacă parametrul efectiv este 127, funcţia va returna 1.

8. "Numai ultima cifră". Se citesc n (număr natural, n<100) şi n numere naturale


nenule x1 x2 x3 ... xn, numere mai mici ca 30000. Se cere:

• ultima cifră a numărului x1 + x 2 +....+ x n ;


ultima cifră a numărului x1 2 3 n .
x x ..... x

Exemplu: n=3; x1=11; x2=4; x3=3. Programul tipăreşte:

• 8 pentru că ultima cifră a sumei 11+4+3 este 8;


• 1 pentru că ultima cifra a lui 1112 este 1.

Concursul Naţional al Elevilor, Sinaia

9. Să se citească două matrice şi să se efectueze suma lor. Programul se va


realiza astfel:

• se scrie un subprogram de citire a unei matrice cu m linii şi n coloane;


• se scrie un subprogram de tipărire a unei matrice cu m linii şi n coloane;
• se scrie un subprogram care adună două matrice;
• programul principal rezultă din apelul acestor subprograme.
64 Capitolul 2. Subprograme

10. Să se calculeze coeficienţii polinomului P(x)=(x+a)n, în două variante.

a) Programul utilizează un subprogram care înmulţeşte un polinom oarecare


de grad k (coeficienţii se găsesc într-un vector, transmis prin referinţă)
cu polinomul x+a.

b) Programul utilizează un subprogram care calculează coeficienţii, aşa cum


rezultă din formula (binomul lui Newton):
n
( x + a) n = ∑C
k =0
k k n−k
na x .

11. Să se tipărească toate numerele prime aflate între doi întregi citiţi. Programul va
folosi o funcţie care testează dacă un număr este prim sau nu.

12. Scrieţi un program care tipăreşte numerele întregi găsite între două valori citite,
numere care se divid cu suma cifrelor lor. Programul va utiliza o funcţie care
returnează suma cifrelor unui număr întreg primit ca parametru.

13. Să se scrie o procedură care permută două linii date ale unei matrice pătratice.

14. Scrieţi o procedură care interclasează doi vectori sortaţi ştiind că aceştia sunt
ordonaţi descrescător.

15. Se citesc două polinoame de grade m şi n (coeficienţii fiecărui polinom se citesc


într-un vector). Să se afişeze coeficienţii polinomului sumă şi cei ai polinomului
produs. În ambele cazuri se vor utiliza proceduri de citire, adunare, înmulţire.

16. Se citesc 3 valori a, b, n (a şi b reale, n întreg). Se cere să se calculeze valorile


unei funcţii F, definită pe intervalul [a,b] în n+1 puncte rezultate din împărţirea
intervalului [a,b] în n părţi egale şi să se afişeze cea mai mare dintre aceste
valori. Funcţia F va fi dată sub forma unui subprogram de tip funcţie.

17. Un număr raţional este reţinut prin utilizarea unui vector cu două componente.
Scrieţi subprogramele care adună, scad, înmulţesc şi împart două numere
raţionale. Subprogramele trebuie să returneze numărul simplificat.

18. Design triunghiular. Triunghiul de mai jos este generat de un algoritm.


Descoperiţi acest algoritm şi folosiţi-l pentru a scrie un program care citeşte
numărul n de linii şi generează triunghiul cu n linii corespunzător.
1
232
34543
4567654
567898765
67890109876
7890123210987

Junior Division
Manual de informatică pentru clasa a XI-a 65

19. Ce afişează programul următor?

Varianta Pascal Varianta C++


var z:integer; #include <iostream.h>
procedure test(a:integer); void test(int a)
begin { a++;
a:=a+1; write(a); cout<<a;
end; }
begin void main()
z:=5; { int z=5;
test(z); test(z);
write(z) cout<<z;
end. }

a) 6 6; b) 5 6; c) 6 6; d) 6 5.

20. Ce afişează programul următor?

Varianta Pascal Varianta C++


function f(x:integer):integer; #include <iostream.h>
begin int f(int x)
f:=x+1 { return x+1;
end; }
procedure t(a:integer); void t(int a)
begin { a++; cout<<a;
a:=a+1; writeln(a); }
end;
void main()
begin { t(f(f(f(1))));
t(f(f(f(1)))) }
end.

a) 3; b) 4; c) 5; d) eroare de sintaxă.

21. Ce afişează programul următor:

Varianta Pascal Varianta C++


var a:integer; #include <iostream.h>
function f:integer; int a;
var a:integer; int f()
begin { int a=3;
a:=3; cout<<a;
write(a); return a;
f:=a; }
end;
void main()
begin { a=2;
a:=2; cout<<a;
writeln(a,f); cout<<f();
end. }

a) 233; b) 23; c) 32; d) 232.


66 Capitolul 2. Subprograme

22. Ştiind că valorile citite ale variabilelor a şi b sunt numere naturale nenule, care
dintre secvenţe utilizează subprogramul increment pentru a obţine produsul celor
două numere?

Varianta Pascal Varianta C++


var a,b,i,j,t:integer; #include <iostream.h>
procedure increment(var x:integer); int a,b,i,j,t;
begin void increment(int& x)
x:=x+1 { x++; }
end; void main()
begin { cin>>a;
readln(a); readln(b); cin>>b;
t:=a; t=a;
... ...
writeln(t) cout<<t;
end. }

a) for i:=1 to a do for(i=1;i<=a;i++)


for j:=1 to b do for(j=1;j<=b;j++)
increment(t); increment(t);
b) for i:=1 to a do for(i=1;i<=a;i++)
for j:=1 to b-1 do for(j=1;j<b;j++)
increment(t); increment(t);
c) for i:=1 to b do for(i=1;i<=b;i++)
for j:=1 to t do for(j=1;j<=t;j++)
increment(t); increment(t);
d) for i:=1 to a do for(i=1;i<=b;i++)
increment(t); increment(t);

23. Pentru varianta de limbaj preferată, stabiliţi care dintre următoarele antete de
subprogram sunt corecte:
a) procedure p(n:integer; var void p(int n, int& v[10])
v:array[1..10] of integer);
b) function p(n:integer;var p(int n, float& v)
v:real);
c) procedure p(n:integer;v:real); void p(int n, float v)
d) function p(n):integer; int p(int& n; float v)

24. Pentru funcţiile definite mai jos, care dintre afirmaţii este falsă?

Varianta Pascal Varianta C++


function f(x:real):real; float f(float x)
begin { if (x<3) return 3*x-1;
if x<3 then f:=3*x+1 else return x-1;
else f:=x-1 }
end;
function g(x:real):real; float g(float x)
begin { if (x<10) return 4*x;
if x<10 then g:=4*x else return x-1;
else g:=x-1 }
end;
Manual de informatică pentru clasa a XI-a 67

a) Expresia f(g(3)) produce valoarea 11;


b) Expresia g(f(3)) produce valoarea 8;
c) Expresia f(g(f(3))) produce valoarea 7;
d) Expresia (f+g)(3) produce valoarea 14.

25. Fiind dată funcţia următoare, care dintre afirmaţii sunt adevărate?

Varianta Pascal Varianta C++


function Cat(n1,n2:integer): long Cat(int n1,int n2)
longint; { int inv=0;
var inv:integer; while (n2!=0)
begin { inv=inv*10+n2%10;
inv:=0; n2=n2/10;
while n2<>0 do begin }
inv:=inv*10+ n2 mod 10; while (inv!=0)
n2:=n2 div 10 { n1=n1*10+inv%10;
end; inv=inv/10;
while inv<>0 do begin }
n1:=n1*10+inv mod 10; return n1;
inv:=inv div 10 }
end;
Cat:=n1
end;

a) Cat(12,10) returnează 1210;


b) Cat(10,12) returnează 1012;
c) Cat(0,12) returnează 12;
d) Cat(12,0) returnează 120.

26. Fiind dată funcţia următoare, care dintre afirmaţii este falsă?

Varianta Pascal Varianta C++


function Cat(n1,n2:integer) long Cat(int n1,int n2)
:longint; { int n2s;
var n2s:integer; n2s=n2;
begin while (n2!=0)
n2s:=n2; { n2/=10;
while n2 <>0 do begin n1*=10;
n2:=n2 div 10; }
n1:=n1*10 return n1+n2s;
end; }
Cat:=n1+n2s
end;

a) Cat(-12,13) returnează -1213;


b) Cat(12,0) returnează 12;
c) Cat(0,12) returnează 12;
d) Cat(13,12) returnează 1312.
68 Capitolul 2. Subprograme

27. Se consideră declarările următoare:

Varianta Pascal Varianta C++


type vect=array[1..40] of byte; void Concat(int A[20], int
B[20], int m, int n, int C[40],
procedure Concat(A,B:vect; int& p)
m,n:byte;var C:vect;var p:byte);
void Reun(int A[20], int B[20],
procedure Reun(A,B:vect; int m, int n, int C[40], int&
m,n:byte;var C:vect;var p:byte); p)
void Inters(int A[20], int
procedure Inters(A,B:vect;
B[20],int m, int n, int C[40],
m,n:byte;var C:vect;var p:byte);
int& p)
procedure Dif(A,B:vect;m,n:byte; void Dif(int A[20], int B[20],
var C:vect;var p:byte); int m, int n, int C[40], int&
p)
procedure Elimin(var A:vect;
var m:byte); void Elimin (int A[20],int& m)

în care:

- Concat are rolul de a returna vectorul C cu p componente, vector obţinut din


concatenarea componentelor vectorului A de m elemente cu ale vectorului B cu n
elemente. De exemplu, din A=(1,2,3,7), m=4, B=(2,5,3), n=3, se obţine
C=(1,2,3,7,2,5,3), p=7.
- Reun are rolul de a returna vectorul C cu p componente, vector obţinut din
reuniunea mulţimilor de numere naturale din vectorii A cu m elemente şi B cu n
elemente (presupunând că A conţine valori distincte şi B conţine valori distincte).
De exemplu, din A=(1,2,3,7), m=4, B=(2,5,3), n=3, se obţine
C=(1,2,3,7,5), p=5.
- Inters are rolul de a returna vectorul C cu p componente, vector obţinut din
intersecţia mulţimilor de numere naturale din vectorii A cu m elemente şi B cu n
elemente (în aceleaşi condiţii ca şi în cazul reuniunii). De exemplu, din
A=(1,2,3,7), m=4, B=(2,5,3), n=3, se obţine C=(2,3), p=2.
- Dif are rolul de a returna vectorul C cu p componente, vector obţinut din
diferenţa mulţimilor de numere naturale din vectorii A cu m elemente şi B cu n
elemente (în aceleaşi condiţii ca şi în cazul reuniunii). De exemplu, din
A=(1,2,3,7), m=4, B=(2,5,3), n=3, se obţine C=(1,7), p=2.
- Elimin are rolul de a returna vectorul care conţine toate elementele distincte
ale vectorului iniţial cu m componente, prin eliminarea valorilor multiple. De
exemplu, pentru A=(1,1,2,3,2), m=5, se returnează A=(1,2,3), m=3.

A. Se citesc doi vectori X cu a numere naturale şi Y cu b numere naturale.


Elementele din fiecare vector nu sunt neapărat distincte. Care dintre secvenţele de
mai jos nu calculează reuniunea mulţimilor numerelor naturale reţinute de cei doi
vectori? Rezultatul se va găsi în vectorul Z.
Manual de informatică pentru clasa a XI-a 69

a) Concat(X,Y,a,b,Z,p);
Elimin(Z,p);
b) Elimin(X,a); Elimin(Y,b);
Concat(X,Y,a,b,Z,p);
c) Elimin(Concat(X,Y,a,b,Z,p),p);
d) Elimin(X,a); Elimin(Y,b);
Reun(X,Y,a,b,Z,p);

B. Se citesc doi vectori X cu a numere naturale şi Y cu b numere naturale.


Elementele din fiecare vector sunt distincte. Care dintre secvenţele de mai jos
calculează intersecţia mulţimilor numerelor naturale reţinute de cei doi vectori?
Rezultatul se va găsi în vectorul Z.
a) Dif(X,Y,a,b,Z,p);
Dif(X,Z,a,p,Z,p);
b) Dif(X,a,Y,b,Z,p);
Dif(Y,Z,b,p,Z,p);
c) Intersect(X,a,Y,b,Z,p)
d) Intersect(X,Y,a,b,Z,p);
C. Se citesc doi vectori X cu a numere naturale şi Y cu b numere naturale.
Elementele din fiecare vector sunt distincte. Care dintre secvenţele de mai jos nu
calculează nici X-Y, nici Y-X? Rezultatul se va găsi în vectorul Z.
a) Inters(X,Y,a,b,Z,p);
Dif(X,Z,a,p,Z,p);
b) Inters(X,Y,a,b,Z,p);
Dif(Y,Z,b,p,Z,p);
c) Inters(Y,X,a,b,Z,p);
Dif(X,Z,a,b,Z,p);
d) Dif(X,Y,a,b,Z,p);

28. Se consideră subprogramul următor:

Varianta Pascal Varianta C++


procedure Afis (n:integer); void Afis (int n)
var i,j,k:integer; { int i,j,k;
begin k=2*n-1; i=1;
k:=2*n-1;i:=1; while (i<=n)
while i<=n do {for(j=1;j<=(k-2*i+1)/2;j++)
begin cout<<' ';
for j:=1 to (k-2*i+1)div 2 do for(j=1;j<=2*i-1;j++)
write (' '); cout<<'*';
for j:=1 to 2*i-1 do i++;
write ('*'); cout<<endl;
i:=i+1; }
writeln }
end
end;
70 Capitolul 2. Subprograme

A. Dacă n=8, câte caractere ”*” se afişează pe ultimul rând tipărit?

a) 8; b) 16; c) 15; d) 17.

B. Dacă n=4, câte caractere ”*” se afişează în total (pe toate rândurile)?

a) 8; b) 16; c) 12; d) 20.

C. Dacă n=20, câte spaţii se scriu înaintea primului caracter ”*”, pe penultimul rând?

a) 10; b) 20; c) 19; d) 1.

29. Se consideră subprogramul următor:

Varianta Pascal Varianta C++


type vect=array[1..10] of byte; void xx(int V[10])
{ int A[4][4],i,j,z;
procedure xx(var V:vect); for (i=0;i<4;i++)
var A:array[1..4,1..4] of byte; for (j=3;j>=0;j--)
i,j,z:byte; { A[i][j]=V[i]%2;
begin V[i]/=2;
for i:=1 to 4 do }
for j:=4 downto 1 do for (i=0;i<4;i++)
begin { z=A[0][i];
A[i,j]:=V[i] mod 2; for (j=1;j<4;j++)
V[i]:=V[i] div 2; z=z*2+A[j][i];
end; V[i]=z;
for i:=1 to 4 do }
begin }
z:=A[1,i];
for j:=2 to 4 do
z:=z*2+A[j,i];
v[i]:=z;
end;
end;

A. Care este conţinutul lui V după apel, dacă înainte este V=(1,2,3,4)?

a) V=(1,2,3,4); b) V=(4,3,2,1);
c) V=(0,1,6,10); d) V=(0,4,5,9).

B. Care trebuie să fie iniţial conţinutul lui V, dacă după apel este V=(1,6,10,0)?

a) V=(2,4,6,8); b) V=(1,6,10,0);
c) V=(0,10,6,1); d) V=(1,9,3,2).

C. Pentru care dintre exemplele următoare de configuraţii ale lui V la intrarea în


subprogram, conţinutul acestuia la ieşire coincide cu cel de la intrare?
a) V=(13,10,5,11); b) V=(2,3,9,13);
c) V=(0,1,2,12); d) V=(1,4,9,15.
Manual de informatică pentru clasa a XI-a 71

30. Se consideră subprogramul următor:

Varianta Pascal Varianta C++


type vect=array[1..10] of integer; int Cmp(int m, int n,
int A[10], int B[10])
function Cmp(m,n:integer; { int i=0;
A,B:vect):byte; while (i<=m && i<=n &&
var i:integer; A[i]==B[i]) i++;
begin if(i>n && i>m) return 0;
i:=1; else if(i>n) return 1;
while (i<=m) and (i<=n) else if(i>m) return -1;
and (A[i]=B[i]) do i:=i+1; else if(A[i]<B[i])
if (i>n) and (i>m) return -1;
then Cmp:=0 else
else if i>n then Cmp:=1 return 1;
else if i>m then Cmp:=-1 }
else if A[i]<B[i]
then Cmp:=-1
else Cmp:=1;
end;

A. Ce valoare returnează funcţia apelată pentru fiecare dintre cazurile următoare?

a) m=3, n=3, A=(1,2,3), B=(1,2,3);


b) m=2, n=3, A=(1,2), B=(1,2,3);
c) m=2, n=3, A=(1,3), B=(1,2,3);
d) m=1, n=1, A=(1), B=(1).

B. Pentru m=5, n=3, A=(1,2,5,7,2), B=(1,2, ), stabiliţi ce valoare poate fi


a treia componentă a vectorului B astfel încât funcţia să returneze valoarea 0.

a) 3; b) 5;
c) nu există o astfel de valoare; d) 2.

C. Pentru X=(1, ,5,7), Y=(1,2, ,7), stabiliţi ce valori pot fi scrise în casete
astfel încât apelul cmp(4,4,X,Y) să returneze aceeaşi valoare ca şi apelul
cmp(4,4,Y,X).

a) 2 şi respectiv 3; b) 2 şi respectiv 5;
c) nu există astfel de valori; d) 3 şi respectiv 6.

D. Pentru care şir de vectori, funcţia Cmp, apelată pentru oricare doi vectori
consecutivi în şir, returnează aceeaşi valoare?

a) A=(1,2,3), B=(1,2,3), C(1,2,2);


b) A=(1,2), B=(1,2), C=(1,3);
c) A=(1,3), B=(1,2), C=(1,3);
d) A=(2), B=(1), c=(0).
72 Capitolul 2. Subprograme

31. Scrieţi subprograme pentru:

A. Calculul valorilor ce caracterizează o sferă (arie şi volum) pe baza valorii razei


sale r.
B. Calculul valorilor caracteristice ale unui con (raza bazei, generatoarea şi înălţimea)
pe baza valorii volumului conului şi a ariei sale laterale.
C. Obţinerea descompunerii în factori primi a unui număr natural n.
D. Ordonarea componentelor pozitive ale unui şir de valori, valorile negative
rămânând pe poziţiile lor iniţiale.
E. Determinarea unei subsecvenţe palindromice a unui şir de numere naturale de
cel mult două cifre.
F. Verificarea respectării limitei de viteză pe un sector de drum pentru un vehicul
care îl parcurge şi care este înregistrat la diferite momente de timp de o cameră
video. Se transmit: viteza maximă admisă v, numărul de camere video n, distanţele
faţă de punctul de început al sectorului de drum unde sunt amplasate camerele:
d1,d2,…,dn şi momentele la care s-a înregistrat trecerea mobilului prin dreptul
fiecărei camere t1,t2,…,tn.

Răspunsuri

19. d); 20. c); 21. a); 22. b); 23. c); 24. d); 25. b), c); 26. a);

27. A. b), c); B. a), d); C. c);

28. A. c); B. b); C. d).

29. Indicaţie: fiecare număr este scris în binar pe o linie a matricei. Se afişează
numerele zecimale obţinute din numerele în binar de pe fiecare coloană a matricei.
A. c), B. a), C. a).

30. Indicaţie: funcţia realizează compararea lexicografică a doi vectori care reţin
numere naturale. A. a) 0; b) -1; c) 1; d) 0; B. c); C. b); D. d).
73

Capitolul 3

Şiruri de caractere

3.1. Generalităţi

Programele nu lucrează numai cu numere. Ele utilizează din plin şirurile de


caractere. În absenţa lor ar fi foarte greu ca, de exemplu, un program să poată citi
sau afişa numele unei persoane sau numele unei localităţi. Ne putem imagina un
şir de caractere ca un vector în care fiecare componentă reţine un caracter. După
cum se ştie, fiecare caracter este memorat prin utilizarea codului ASCII. Şirurile de
caractere pot fi citite, afişate şi prelucrate cu ajutorul unor subprograme puse la
dispoziţie de limbaj.
Şirurile de caractere au aplicaţii uriaşe în viaţa reală. Să vedem câteva dintre
acestea:
 Cine n-a folosit un editor de texte? Chiar acum, când scriem programele
Pascal sau C++ utilizăm un editor de texte. Luaţi codul sursă al unui program
(fişiere cu extensii .pas sau .cpp). Observaţi că sursa este un fişier text în
care fiecare linie a ei, este o linie a fişierului text. Prin urmare, fiecare linie a
fişierului text conţine un şir de caractere. Ce operaţii putem face cu
editoarele? De exemplu, putem modifica o linie. O astfel de operaţie constă
în includerea sau ştergerea unui subşir (caractere consecutive din interiorul
unui şir). Astfel de operaţii se fac, şi vom vedea cum, prin prelucrarea
şirurilor de caractere. Alte operaţii: a) Find - este o operaţie de identificare
a unui subşir în cadrul unui şir de caractere; b) Replace este o operaţie de
înlocuire a unui subşir cu un altul; c) Cut este o operaţie prin care, din cadrul
unui şir se extrage un subşir al său şi acesta este memorat în Clipboard.
Prin Paste se inserează un şir de caractere în cadrul altui şir de caractere.
Şirul exemplelor ar putea continua. Puteţi găsi altele?
 Aţi învăţat, la ”Tehnologia Informaţiei şi a Comunicaţiilor”, cum, de exemplu,
putem crea tabele (Excel, Word sau Access). Am văzut că liniile tabelului,
pot conţine valori numerice, dar şi alfabetice, care sunt şiruri de caractere.
Putem sorta liniile unui tabel după o anumită coloană (de exemplu, după
numele persoanelor, în ordine alfabetică). De aici, rezultă că între şirurile de
caractere există o relaţie de ordine, corezpunzătoare ordonării alfabetice. În
informatică, o astfel de relaţie se numeşte ordine lexicografică. Cum se
ordonează alfabetic (lexicografic) mai multe şiruri de caractere? Care sunt
principiile care stau la baza acestei ordonări? Despre toate acestea vom
învăţa în acest capitol.
 Limbajele actuale permit numai introducerea datelor sub formă de şiruri de
caractere. Luaţi, de exemplu, o componentă de tip edit, componentă studiată
la ”Tehnologia Informaţiei şi a Comunicaţiilor”. Chiar dacă vreţi, de exemplu,
74 Capitolul 3 – Şiruri de caractere

să introduceţi numărul 12, veţi introduce de fapt şirul de caractere '12'.


Programul va converti şirul de caractere '12' în numărul întreg 12. Nu
întotdeauna o astfel de operaţie reuşeşte, pentru că este posibil ca persoana
care introduce data respectivă să greşească. De exemplu, în loc de şirul '12',
să introducă şirul '1@'. Aceasta înseamnă că în timpul operaţiei de conversie,
se efectuează şi o anumită validare. În cazul de faţă, trebuie efectuată o
validare numerică. Componentele studiate la Tehnologia Informaţiei afişează
şiruri de caractere. Prin urmare, dacă realizăm un astfel de program, este
necesar să convertim valorile numerice în şiruri şi invers. Cum efectuăm astfel
de conversii? Conversiile sunt prezentate în acest capitol.
Întrucât există diferenţe semnificative între modul în care sunt reţinute şi
prelucrate şirurile de caractere în cele două limbaje (Pascal şi C++), prezentarea
acestora se va face separat.

3.2. Şiruri de caractere în Pascal

3.2.1. Noţiuni introductive

Poate aţi observat că, până în acest moment, nu am memorat cuvinte.


Acestea au fost doar afişate. În Pascal se poate lucra cu uşurinţă cu ele, datorită
faptului că limbajul este înzestrat cu un tip de date special, numit string.

Definiţia 3.1. O succesiune de caractere cuprinse între două caractere


apostrof se numeşte şir de caractere.

Exemple: 'Un sir'; 'toamna se numara bobocii'.

O întrebare: între caracterele care alcătuiesc şirul se poate găsi şi


apostroful? De exemplu, şirul 'Alt 'sir' este scris corect?

Răspunsul comportă o anumită nuanţare. Pentru a include apostroful, se


foloseşte un artificiu: acesta este trecut de două ori. Prin urmare, şirul 'Alt
'sir' este incorect, corect este 'Alt ''sir'. Atenţie! În memorie, şirul 'Alt
''sir' este reţinut ca 'Alt 'sir', deci cu un singur apostrof. De ce este
nevoie să se procedeze astfel? Motivul este dat de faptul că, dacă am proceda
altfel, compilatorul nu ar putea decide unde se termină şirul, cu alte cuvinte, care
este apostroful final.
În Pascal, pentru a putea lucra cu şirurile de caractere se folosesc variabilele
de tip string. Tipul string este predefinit, adică este cunoscut, nu avem nevoie
să-l declarăm cu type.
var t: string; Programul alăturat are declarată o
begin variabilă t, de tip string. Ea este
t := 'Iepuras'; iniţializată cu şirul 'Iepuras', apoi este
writeln(t);
end. afişată.
Manual de informatică pentru clasa a XI-a 75

Pentru început, să analizăm modul în care o variabilă de tip string


memorează un şir de caractere. Pentru aceasta, pornim de la exemplul dat în
programul anterior:

7 I e p u r a s

t[0] t[1] t[2] t[3] t[4] t[5] t[6] t[7] t[8] t[255]

Ce observăm? Pentru o variabilă de tip string se rezervă automat un vector


cu 256 de octeţi, numerotaţi de la 0 la 255. Primul dintre ei are rolul de a reţine
numărul de octeţi ocupaţi de şir.
În exemplul dat, şirul 'Iepuras' este alcătuit din şapte caractere. Octeţii de
la 1 la 7 memorează caracterele din care este alcătuit şirul. Restul octeţilor, de la 8
la 255 au un conţinut neprecizat. De altfel, nici nu ne interesează conţinutul lor.
Observaţi faptul că afişarea s-a realizat, în ansamblu, prin precizarea numelui.
Întrucât o variabilă de tip string memorează cuvintele sub formă de vector
de caractere, există posibilitatea să accesăm direct conţinutul unui octet, aşa cum
suntem obişnuiţi. Astfel, t[1] reţine codul caracterului I, t[2] reţine codul
caracterului e, ş.a.m.d. Programul care urmează afişează acelaşi cuvânt, pe litere:
var t: string;
i: integer;
begin
t := 'Iepuras';
for i := 1 to n do write(t[i]);
end.

Mai mult, putem modifica conţinutul unui singur octet, aşa cum rezultă din
programul următor.
var t: string;
begin
t := 'Iepuras';
t[6] := 'i';
write(t);
end.

În loc de 'a', t[6] reţine 'i'. Prin urmare, programul afişează 'Iepuris'.

În cazul variabilelor de tip string există posibilitatea ca atribuirea să se facă


direct, nu pe litere, aşa cum rezultă din programul următor:
var t, z: string;
begin
t := 'Iepuras';
z := t;
write(z);
end.
În urma atribuirii, variabila z reţine cuvântul "iepuras" şi acesta este afişat.
76 Capitolul 3 – Şiruri de caractere

Am văzut că pentru o variabilă de tip string se reţin automat 256 de octeţi,


din care primul reţine lungimea. În multe cazuri, acest număr este prea mare - se
consumă memorie inutil. Din acest motiv există posibilitatea ca o variabilă de tip
string să fie declarată în aşa fel încât să ocupe un număr mai mic de octeţi.

Exemplu: var t: string[4];

Variabila t, ocupă 5 octeţi. Primul, cel de indice 0, are rolul de a reţine


lungimea cuvântului memorat. În acest caz, variabila poate reţine cuvinte ce au cel
mult 4 caractere. Programul afişează 'mama'.
var t: string[4];
begin
t := 'mama';
write(t);
end.

Ce facem în cazul în care cuvântul care va fi memorat, are un număr mai


mare de litere decât numărul de octeţi ai variabilei care îl memorează?
Exemplu:
var t: string[4];
...
t:= 'DANSATOR';

În astfel de cazuri se reţin numai primele caractere, atâtea câte pot fi


memorate. În exemplu, variabila t reţine cuvântul 'DANS'.

După cum am învăţat, caracterul blank se memorează ca oricare altul, prin


codul său. Prin urmare, o variabilă de tip string poate reţine mai multe cuvinte,
separate prin unul sau mai multe blank-uri.
Exemplu:
var nume: string;
...
nume:= 'Ion Zaharia';

În concluzie, prin utilizarea variabilelor de tip string avem avantajul că


putem adresa şirul de caractere atât în ansamblu, prin utilizarea numelui variabilei,
cât şi pe caractere, prin utilizarea parantezelor drepte.

Acesta nu este singurul avantaj. Limbajul este înzestrat cu proceduri şi funcţii


care uşurează mult lucrul cu şirurile de caractere.

3.2.2. Concatenarea şirurilor

Pentru a concatena două şiruri de caractere se foloseşte operatorul '+'.


Operatorul '+' este binar (adică are doi operanzi) şi poate acţiona asupra datelor
de tip string. Ce înţelegem prin concatenare?
Manual de informatică pentru clasa a XI-a 77

Definiţia 3.2. Concatenarea este operaţia prin care din două şiruri de
caractere se obţine un al treilea, format astfel: primul şir (cel aflat în
stânga operatorului), urmat de al doilea şir (cel aflat în dreapta
operatorului).

Exemplu:
program st6;
var t, z: string;
begin
t := 'acest';
z := ' exemplu';
t := t+z;
writeln(t);
end.

În programul st6 se concatenează două şiruri de caractere (t:=t+z;):

• variabila t reţine şirul 'acest';


• variabila z reţine şirul ' exemplu';
• prin operaţia (t+z;) se obţine şirul 'acest exemplu';
• şirul obţinut este atribuit variabilei t.

Observaţii

1. Dacă am fi scris t:=z+t; programul ar fi afişat ' exempluacest'. De


aici tragem concluzia că operaţia de concatenare nu este comutativă (contează
ordinea în care sunt trecuţi cei doi operanzi).
2. Observăm cât este de important să ţinem cont de poziţia blank-ului (al
doilea şir este precedat de blank). Observaţi că dacă am făcut concatenarea z+t,
am obţinut şirul ' exempluacest', care este precedat de blank.

3. Trebuie să avem în vedere că şirul obţinut în urma concatenării să poată fi


memorat în întregime - variabila căreia i se atribuie să fie declarată cu un număr de
octeţi care să permită memorarea sa, altfel şirul va fi memorat trunchiat - adică vor
fi memorate numai primele caractere, atâtea câte încap.

3.2.3. Compararea şirurilor

Oricât ar părea de curios, şirurile de caractere pot fi comparate. Astfel, două


şiruri de caractere (notate a şi b), se pot găsi în una din relaţiile:

• a=b - cele două şiruri sunt egale;


• a>b - şirul a este mai mare decât şirul b;
• a<b - şirul a este mai mic decât şirul b.
78 Capitolul 3 – Şiruri de caractere

Dar cum se pot compara două şiruri? Să nu uităm că şi două caractere se


pot compara după codul lor. Am văzut modul în care au fost codificate caracterele,
după o anumită logică: dacă un caracter urmează altuia, în ordine alfabetică, atunci
el are drept cod un număr mai mare decât al caracterului căruia îi urmează.
Exemplu: codul caracterului d este mai mare cu o unitate decât codul caracterului c.

Şirurile sunt succesiuni de caractere. Prin urmare, compararea se face pe


caractere. În continuare prezentăm algoritmul de comparare a două şiruri.

Fie două şiruri de caractere, notate cu a (cu m caractere) şi b (cu n


caractere).

⇒ Se compară codurile primelor două caractere (aflate în stânga şirurilor, de


indice 1). Avem 3 posibilităţi:

• dacă codul primului caracter al şirului a este mai mare decât codul
primului caracter al şirului b, atunci a>b.

• dacă codul primului caracter al şirului b este mai mare decât codul
primului caracter al şirului a, atunci a<b (b>a).

• în caz de egalitate se compară codurile caracterelor de indice 2.


...

⇒ Dacă m<n, şi în urma comparării primelor m caractere a rezultat egalitate, atunci


a<b (b are mai multe caractere).

⇒ Dacă m>n, şi în urma comparării primelor n caractere a rezultat egalitate,


atunci a>b (a are mai multe caractere).

⇒ Dacă m=n şi în urma comparării tuturor caracterelor a rezultat egalitate, atunci


cele două şiruri sunt egale (a=b).

Exemple:
• a='abc', b='bactr'. Atunci a<b, pentru că a[1] este a şi are codul mai
mic decât b[1] care este b.

• a='abc', b='aba'. Atunci a>b, codul lui a[1] este egal cu codul lui b[1],
codul lui a[2] este egal cu codul lui b[2], iar codul lui a[3] este mai mare
decât codul lui b[3].

• a='abc', b='abca'. Aici m=3, n=4. Atunci a<b, codul lui a[1] este egal cu
codul lui b[1], codul lui a[2] este egal cu codul lui b[2], codul lui a[3]
este egal cu codul lui b[3] şi n>m (şirul b are mai multe caractere).

Compararea şirurilor de caractere este extrem de utilă în sortarea alfabetică


a cuvintelor (ca în dicţionar). Ordinea astfel impusă se mai numeşte şi ordine
lexicografică.
Manual de informatică pentru clasa a XI-a 79

În programul următor se sortează alfabetic mai multe şiruri de caractere


(cuvinte). Pentru aceasta, se declară un vector în care fiecare
componentă este de tip string.

Algoritmul de sortare este unul studiat (care?), motiv pentru care nu revenim
asupra lui. Programul este prezentat în continuare:

type cuvinte = array[1..10] of string;


var v: cuvinte;
man: string;
n, i: byte;
inv: boolean;
begin
write('n= ');
readln(n);
for i := 1 to n do
readln(v[i]);
repeat
inv := false;
for i := 1 to n-1 do
if v[i] > v[i+1] then
begin
man := v[i];
v[i] := v[i+1];
v[i+1] := man;
inv := true;
end
until not inv;
for i := 1 to n do
writeln(v[i]);
end.

3.2.4. Lungimea şirurilor de caractere

Definiţia 3.3. Prin lungimea unui şir de caractere înţelegem numărul de


caractere pe care acesta le conţine.

Exemple:
• şirul 'mama' are lungimea 4;
• şirul 'mama ' are lungimea 5 (şi blank-ul este caracter).

Pentru aflarea lungimii unui şir avem două posibilităţi:

1. Prin utilizarea funcţiei length, care are forma generală:


function length(S: String): Integer;

şi întoarce lungimea şirului S - număr întreg.


80 Capitolul 3 – Şiruri de caractere

 Exemple de utilizare

a) var a: string;
c: integer;
...
a := 'un test';
c := length(a);

În urma atribuirii, variabila c reţine 7.

b) În condiţiile de mai sus, writeln(length(a)) afişează 7.

2. După cum am învăţat, octetul de indice 0 reţine lungimea şirului. Pentru a-l
obţine folosim funcţia ord astfel: ord(a[0]). De ce aşa? Atunci când scriem
a[0] ne referim la un caracter. Folosind această expresie obţinem caracterul care
are codul dat de număr (în binar). Deci, pentru a obţine codul său, utilizăm
funcţia ord.

În programul următor se afişează lungimea unui şir citit. Tipărirea se


face în cele două moduri prezentate:

var a: string;
begin
write('a= '); readln(a);
writeln('lungimea sirului a este ', length(a));
writeln('lungimea sirului a este ', ord(a[0]));
end.

Un caz aparte de şir este şirul vid. Prin şir vid înţelegem un şir fără nici un
caracter. Evident, şirul vid are lungimea 0.

Exemplu: a:=''. Am iniţializat variabila a, de tip string, cu şirul vid.

3.2.5. Subşiruri

Definiţia 3.4. Fiind dat un şir de caractere, prin subşir al său se înţelege
un şir de caractere consecutive care se regăsesc în şirul iniţial.

Exemple:

• Şirul 'harnic' are ca subşir şirul 'rni'. Acesta începe în poziţia a treia din
şirul iniţial (3 caractere);

• Şirul 'mama' are ca subşir şirul 'ma'. Observăm faptul că subşirul apare de
două ori - începând cu poziţiile 1 şi 3. Prin urmare, în cadrul unui şir, un subşir
poate apărea de mai multe ori.

• Şirul 'harnic' nu are ca subşir şirul 'rnit', chiar dacă primele trei
caractere (rni) se regăsesc în şirul iniţial.
Manual de informatică pentru clasa a XI-a 81

Funcţii ce acţionează asupra şirurilor de caractere

Există mai multe proceduri şi funcţii care acţionează asupra şirurilor de


caractere. Acestea sunt prezentate în continuare.

 Funcţia copy are rolul de a extrage un subşir din cadrul unui şir dat:
function copy(s:string;inceput,lungime:integer):string;
unde:

• variabila s (de tip string) - conţine şirul din care se face extragerea;
• inceput - reţine poziţia de început a subşirului care se extrage;
• lungime - reţine numărul de caractere care se extrag.

În cazul în care prin parametrul lungime specificăm mai multe caractere


decât are şirul, se extrag caracterele până la sfârşitul şirului.

Fie programul următor:


var a, b: string;
i, j: byte;
begin
write('a= '); readln(a);
write('i= '); readln(i);
write('j= '); readln(j);
b := copy(a, i, j);
writeln(b);
end.

Dacă se citeşte şirul 'un text', 4 pentru i şi 3 pentru j, variabila b va reţine


subşirul 'tex'.

 Funcţia pos are rolul de a verifica dacă un şir este subşir pentru altul:
function pos(subsir,sir:string):byte;

Se scriu, în ordine, următorii parametri:

• subsir - subşirul căutat;


• sir - şirul în care se face căutarea.

Funcţia returnează:

• 0, dacă nu a fost găsit subşirul cerut;


• poziţia de început a subşirului, în cazul în care acesta a fost găsit.

Exemple: 1. Fie şirul 'abcde'. Se caută subşirul 'bcd'. Acesta este găsit, iar
funcţia returnează 2 (poziţia de început a subşirului). Dacă se caută subşirul 'cz',
funcţia returnează 0 ('cz' nu este subşir al şirului 'abcde').
82 Capitolul 3 – Şiruri de caractere

2. Programul următor citeşte două şiruri a şi b şi verifică dacă b este subşir al lui
a. În ambele cazuri se dau mesaje. Dacă b nu a fost identificat ca subşir al lui a,
sau, în caz contrar, se afişează poziţia de început a sa.
var a, b: string;
n: integer;
begin
write('a= '); readln(a);
write('b= '); readln(b);
n := pos(b,a);
if n = 0 then writeln('b nu este subsir al lui a')
else writeln('b este subsir al lui a si incepe in
pozitia ',n);
end.

 Procedura insert are rolul de a insera un şir de caractere începând cu o


anumită poziţie, în alt şir. Pentru aceasta, ea primeşte ca parametri
următoarele:
procedure insert (sir_de_ins: string; var şir_unde_ins:
string; poz: integer);
unde:

• sir_de_ins - şirul care urmează a fi inserat;


• sir_unde_ins - şirul în care se face inserarea;
• poz - poziţia din care se face inserarea.

Exemplu:
var a, b: string;
begin
write('a= '); readln(a);
write('b= '); readln(b);
insert(b, a, 3);
writeln(a);
end.

În programul anterior am citit două variabile de tip string, a şi b. Şirul reţinut


de variabila b este inserat în şirul reţinut de variabila a începând cu poziţia 3.

Exemplu: dacă a reţine şirul 'abcd' şi b reţine şirul '123', programul afişează
'ab123cd'.

 Procedura delete are rolul de a şterge un subşir din cadrul unui şir dat.
Pentru aceasta, ea are ca parametri, în ordine:
procedure delete(var sir: string; indice, nr_car: integer);

unde:

• Sir - variabila care conţine şirul din care se face ştergerea;


• Indice - indicele primului caracter al subşirului care se şterge;
• nr_car - numărul de caractere pe care îl are subşirul.
Manual de informatică pentru clasa a XI-a 83

Exemplu:

Programul următor citeşte un şir de caractere într-o variabilă de tip string,


numită a. Şirului citit i se şterge subşirul care începe în poziţia 3 şi are lungimea 2
(două caractere). De exemplu, dacă variabila a reţine şirul 'abcde', se afişează
'abe'.
var a:string;
begin
write('a='); readln(a);
delete(a,3,2);
writeln(a);
end.

 Aplicaţia 3.1. În programul următor se listează indicii tuturor apariţiilor


caracterului citit în şir:
var sir: string;
ch: string[1];
pozn, pozv: byte;
begin
writeln('introduceti sirul'); readln(sir);
writeln('introduceti caracterul '); readln(ch);
pozn := pos(ch, sir);
pozv := 0;
while pozn <> 0 do
begin
pozv := pozv+pozn;
writeln(pozv);
sir := copy(sir, pozn+1, 255);
pozn := pos(ch, sir);
end
end.

 Aplicaţia 3.2. Ştergerea tuturor apariţiilor unui subşir din cadrul unui şir:

var sir, subsir: string;


poz, lung: byte;
begin
writeln('introduceti sirul'); readln(sir);
writeln('introduceti subsirul '); readln(subsir);
lung := length(subsir);
poz := pos(subsir, sir);
while poz <> 0 do
begin
delete(sir, poz, lung);
poz := pos(subsir, sir);
end;
writeln(sir);
end.
84 Capitolul 3 – Şiruri de caractere

 Aplicaţia 3.3. Înlocuirea tuturor apariţiilor unui subşir cu alt subşir. Analizaţi
programul următor pentru a descoperi modul în care se realizează aceasta:
var sir, sir_sters, sir_adaugat: string;
poz, lung: byte;
begin
writeln('introduceti sirul '); readln(sir);
writeln('introduceti sirul care se sterge);
readln(sir_sters);
writeln('introduceti sirul care se adauga);
readln(sir_adaugat);
lung := length(sir_sters);
poz := pos(sir_sters, sir);
while poz <> 0 do
begin
delete(sir,poz,lung);
insert(sir_adaugat, sir, poz);
poz := pos(sir_sters, sir);
end;
writeln(sir);
end.

3.2.6. Conversia de la şiruri la valori numerice şi invers


Fie şirul '123'. Evident, acesta este diferit de numărul 123. După cum ştim,
şirul ocupă 4 octeţi (nu uitaţi, primul octet reţine lungimea). Numărul 123, dacă este
memorat de o variabilă de tip integer, ocupă doi octeţi şi este reţinut în cod
complementar. Limbajul dispune de două proceduri care realizează conversia de la
valori numerice la şiruri şi invers.
 Procedura str are rolul de a transforma o valoare numerică în şir.
procedure Str(X [: Lg [: Zec ]]; var S:string);

Pentru aceasta i se transmit, în ordine, doi parametri:


• X - variabila (valoarea) numerică - poate fi întreagă sau reală;
• S - variabila de tip string care reţine şirul convertit.

Exemplu:
var a: string;
n: integer;
begin
write ('n= '); readln(n);
str(n, a);
writeln(a)
end.

În programul de mai sus se citeşte o valoare întreagă (n). Valoarea este


convertită către tipul string - variabila a are acest tip. Rezultatul este afişat. De
exemplu, dacă se citeşte 123 se afişează 123. Până aici nimic spectaculos. Dacă
Manual de informatică pentru clasa a XI-a 85

am fi afişat conţinutul variabilei n, am avea acelaşi rezultat. Atunci la ce folosesc


astfel de conversii? Pentru a putea da răspunsul trebuie să mai învăţăm ceva
despre modul în care putem apela procedura str. Am fi putut să scriem apelul şi
aşa: str(n:4,a);. Observaţi faptul că, după numele variabilei care se afişează
(n) au fost puse caracterul ':' şi numărul 4. Aceasta înseamnă că am cerut ca
rezultatul (de tip string) să ocupe 5 octeţi - unde primul reţine lungimea, deci 4
octeţi pentru memorare.

Să analizăm acum modul de efectuare a conversiei.

⇒ Dacă variabila n reţine numărul 1234, variabila a va reţine şirul


'1234'.

⇒ Dacă variabila n reţine numărul 123, atunci a va reţine şirul ' 123'.
Rezultă că şirul, fără octetul de lungime, ocupă 4 octeţi. În astfel de
cazuri, şirul este completat în stânga cu numărul de blank-uri necesar.

⇒ Dacă variabila n reţine numărul 12345 atunci şirul va fi '12345'. Cu


alte cuvinte nu se respectă cerinţa noastră, pentru că dacă ar fi fost
respectată, rezultatul ar fi fost eronat.

Acum putem răspunde la întrebarea: la ce folosesc astfel de conversii?


Programatorul va avea grijă ca întotdeauna numărul de octeţi ai şirului să fie mai
mare sau egal cu numărul de octeţi ai valorii convertite. În acest fel, la afişare, vom
şti care este spaţiul ocupat de şir şi putem să afişăm rezultatele aliniate (tabele). În
continuare, ne ocupăm de conversia valorilor reale către şiruri de caractere.
Exemplu:
var a: string;
x: real;
begin
x := -67.789;
str(x: 10: 2, a);
writeln(a)
end.

În programul de mai sus se converteşte numărul -67.789 către un şir.


Cerinţa este ca şirul efectiv să ocupe 10 caractere, dintre care ultimele două să fie
zecimale. Evident, două caractere vor fi semnul '-' şi punctul zecimal. În exemplu,
se obţine şirul ' -67.79'. Ce observăm?

 Întotdeauna numărul zecimalelor solicitat de programator este respectat. În


cazul în care numărul efectiv de zecimale este mai mare decât numărul
solicitat pentru conversie, înaintea conversiei numărul este rotunjit. Dacă
numărul solicitat de zecimale ar fi fost 0, s-ar fi afişat -67 (deci punctul
zecimal nu ar fi afişat).
 În cazul în care numărul total de cifre al părţii întregi + numărul total de
zecimale + 2 (caracterele de semn şi punctul zecimal) ocupă mai mult
decât numărul total de octeţi solicitaţi pentru afişare, acesta din urmă nu este
86 Capitolul 3 – Şiruri de caractere

respectat. Cu alte cuvinte, se trunchiază numai zecimalele nu şi partea


întreagă a valorii reale. De exemplu, dacă am fi scris x:1:2, şirul obţinut ar
fi fost '-67.79'.

 În cazul în care numărul care se converteşte este pozitiv, semnul '+' nu


este trecut.

Acum studiem conversia inversă de la tipul string către valori numerice


(întregi sau reale). De exemplu, şirul '123' se poate converti către valoarea de tip
integer: 123.

De la început precizăm că nu întotdeauna conversia reuşeşte. De exemplu,


dacă încercăm să convertim şirul '1a2' către o valoare de tip integer, conversia
nu reuşeşte, pentru că şirul conţine caracterul 'a'.

 Pentru realizarea conversiei utilizăm procedura val. Ea are trei parametri şi


anume:
procedure val(s:string; var variabila_numerica; var cod_er:integer);
unde:
• s - conţine şirul ce urmează a fi convertit;

• variabila_numerica - variabila de tip întreg sau real care va


reţine rezultatul conversiei (valoarea numerică).
• cod_er - variabilă de tip întreg. După conversie, aceasta va reţine 0
dacă conversia a reuşit sau o valoare diferită de 0, în caz contrar.

Să analizăm programul următor:

var a: string;
x, er: integer;
begin
write('Sirul este '); readln(a);
val(a, x, er);
if er = 0 then writeln(' conversia a reusit ', x)
else
begin
writeln ('conversia nu a reusit');
writeln(x)
end
end.

Programul citeşte un şir de caractere care este reţinut în variabila de tip


string a. Se încearcă conversia şirului către o variabilă de tip integer. În cazul
în care tentativa a reuşit, se afişează mesajul corespunzător şi conţinutul variabilei,
altfel se afişează numai un mesaj prin care se anunţă faptul că tentativa a eşuat.
Manual de informatică pentru clasa a XI-a 87

Observaţii

 Dacă şirul de caractere cifre este precedat de un număr de blank-uri,


conversia reuşeşte.

Exemplu: şirul ' 123' se poate converti către valoarea 123.

 Dacă şirul de caractere cifre este urmat de un număr de blank-uri, conversia


nu reuşeşte.

Exemplu: şirul '123 ' nu poate fi convertit către o valoare numerică.

 Dacă şirul conţine un singur caracter literă, el nu poate fi convertit către o


valoare numerică. De exemplu, şirul '12i' nu poate fi convertit. Excepţie
fac şirurile de caractere care respectă sintaxa unei constante reale în formă
ştiinţifică (de exemplu, '1.E-3')

 Dacă variabila care reţine rezultatul este de tip întreg, iar şirul conţine punctul
zecimal, conversia nu reuşeşte.

Exemplu: şirul '1.23' nu poate fi convertit către o variabilă de tip întreg,


dar poate fi convertit către o variabilă de tip real.

 Dacă în urma conversiei se obţine o valoare numerică care nu poate fi


memorată de variabila respectivă, programul se termină anormal, prin eroare
de executare.

 Aplicaţia 3.4. Programul următor testează dacă o valoare introdusă este


numerică şi dacă este cuprinsă în intervalul [10,20]:

var sir: string;


eroare: integer;
valoare: real;
begin
writeln('introduceti sirul ');
readln(sir);
val(sir, valoare, eroare);
if eroare<>0
then
writeln('val. introdusa este eronata')
else
if (valoare < 10) or (valoare > 20) then
writeln('val. nu este in intervalul dorit ')
else
writeln('ok!')
end.
88 Capitolul 3 – Şiruri de caractere

3.2.7. Citirea şi scrierea datelor de tip String din şi


în fişiere text

A) Citirea datelor de tip String

Aceste variabile se citesc începând din poziţia curentă a cursorului până este
citit numărul de caractere necesar tipului sau până s-a ajuns la sfârşitul de linie.

Programul de mai jos demonstrează acest fapt (dacă linia 1 a fişierului are 3
caractere, se vor afişa două pe un rând şi unul pe al doilea rând). După citirea unei
astfel de variabile, pointerul se află sau pe caracterul ce urmează după ultimul
caracter citit sau pe CR. În situaţia în care pointerul se află pe CR şi se forţează o
nouă citire de tip String, se returnează şirul vid.

var f: text;
a: string[2];
b: string;
begin
assign(f,'f1.dat');
reset(f);
read(f,a);
writeln(a);
read(f,b);
writeln(b);
close(f);
end.

B) Scrierea datelor de tip String

În general, conţinutul unei date de tip string se scrie în fişier în totalitate.


Opţional, numele variabilei poate fi urmat de ':' şi de un parametru, m. Pentru
datele de acest tip, se iau în considerare lungimea efectivă a şirului şi valoarea
parametrului m ce specifică numărul de poziţii pe care se face scrierea. În cazul în
care valoarea lui m este mai mică decât lungimea efectivă a şirului, aceasta se
ignoră, şirul va fi scris în întregime. Dacă valoarea lui m este mai mare decât
lungimea efectivă a şirului, acesta este scris pe m poziţii aliniat dreapta, iar în faţă
se pun blank-uri.
Exemplu: se scrie şirul ' Marian'.
var f:text;
sir:string[6];
begin
assign(f,'F1.txt');
rewrite (f);
sir:='Marian';
write(f,sir:10);
close(f);
end.
Manual de informatică pentru clasa a XI-a 89

3.3. Şiruri de caractere în C++

3.3.1. Generalităţi

Am învăţat faptul că o constantă de tip şir de caractere se declară între două


caractere ", de exemplu: "calculator". Dar cum se reţine în memoria internă?
Aceasta este reţinută sub forma unui vector de caractere. Primul element, cel de
indice 0, reţine codul ASCII al caracterului 'c', al doilea reţine codul ASCII al
caracterului 'a', ..., ş.a.m.d. Convenţia este ca ultimul octet să reţină 0 (codul
caracterului nul). Prin urmare, pentru a reţine şirul "calculator" trebuie să fie
rezervate cel puţin 11 elemente de tip char (10 litere plus caracterul nul) - adică
11 octeţi. Menţionăm că pentru fiecare caracter este reţinut codul ASCII.

c a l c u 0

a[0] a[1] a[2] ................................... a[10]

Vectorii de caractere pot fi iniţializaţi la declarare, caracterul nul fiind


memorat automat.

Exemple:
 char vect[11]="calculator".
 char vect[]="calculator". În acest caz, compilatorul face calculul
numărului de octeţi necesari.
 char vect[100]="calculator". Am rezervat mai mulţi octeţi decât era
necesar.

3.3.2. Citirea şi scrierea şirurilor de caractere

Se propune următoarea problemă: să se citească şi să se afişeze cuvântul


"calculator". Pentru aceasta, ar trebui să procedăm astfel:

• reţinem un vector cu cel puţin 11 componente de tip char - în exemplu 20;


• citim cuvântul, caracter cu caracter;
• îl afişăm.
Programul următor realizează toate acestea:
#include <iostream.h>
main()
{ char a[20];
int i;
for(i=0;i<10;i++) cin>>a[i];
a[10]=0;
for(i=0;i<10;i++) cout<<a[i];
}
90 Capitolul 3 – Şiruri de caractere

Să recunoaştem că o astfel de modalitate de lucru este deosebit de greoaie.


Dar dacă citim un cuvânt cu patru litere? S−ar putea scrie o secvenţă care să
citească cuvântul până la întâlnirea caracterului ENTER, însă şi această modalitate
este greoaie.

Limbajul C++ permite ca lucrul cu şiruri de caractere să fie cu mult mai


simplu. Refacem programul anterior:

#include <iostream.h>
main()
{
char a[20];
cin>>a;
cout<<a;
}

Observaţii

 Caracterul nul este adăugat automat.


 Procedând ca mai sus, putem citi orice cuvânt cu un număr de până la 19
caractere - excluzând caracterul nul.
 Putem să rezervăm, în limita memoriei pe care o are la dispoziţie programul,
un număr mai mare de octeţi.
Exemplu: char a[1000].

 Un vector poate fi adresat pe componente.


Exemplu: a[0]='c', a[1]='a', ş.a.m.d.

Din păcate, prin metoda de mai sus nu poate fi citit un şir care conţine mai
multe cuvinte separate prin spaţii. De exemplu, dacă la rularea programului anterior
tastăm şirul " Un om", se va afişa "Un". Aceasta înseamnă că citirea se face astfel:
• Se sar toate caracterele albe. În exemplu, s-au sărit blank-urile.

• Se citeşte şirul care începe cu primul caracter care nu este alb şi se


sfârşeşte la întâlnirea primului caracter alb (în exemplu, blank).
Din acest motiv, pentru citirea şirurilor de caractere vom utiliza o funcţie de
un tip special, pe care o prezentăm în continuare.

 Funcţia:
cin.get(vector_de_caractere, int nr, char='\n')

citeşte un şir de caractere până când este îndeplinită una dintre condiţiile de mai jos:
• au fost citite nr-1 caractere;

• a fost întâlnit caracterul transmis ca ultim parametru (implicit, "\n").


Manual de informatică pentru clasa a XI-a 91

Observaţii

 Sunt citite şi caracterele albe.


 Este inserat caracterul nul.
 Caracterul transmis ca ultim parametru nu este inserat în şir.
Al treilea parametru este trecut în mod facultativ. Dacă nu este trecut, se
presupune că este '\n'.

Priviţi următoarele exemple:

1. Se citeşte un şir de maximum 2 caractere:


char a[10];
cin.get(a,3);
cout<<a;

De exemplu, dacă tastăm 'mama' şi Enter se citeşte şirul "ma", care va fi afişat.

2. La fel ca mai sus, dar citirea se întrerupe la întâlnirea caracterului 'g' sau când
au fost citite 9 caractere ale şirului.
char a[10];
cin.get(a,10,'g');
cout<<a;

În C++ pot exista mai multe funcţii cu acelaşi nume, dar care diferă prin
parametrii primiţi. Astfel, există şi funcţia:
cin.get()
fără parametri. Ea are rolul de a citi un caracter (fie că este alb, fie că nu).
Observaţie. În cazul utilizării repetate a funcţiei cin.get() cu trei
parametri, apare o problemă. Analizaţi programul următor:
#include <iostream.h>
#include <string.h>
main()
{ char sir1[1000], sir2[25];
cout<<"sir 1 ";
cin.get(sir1,1000);
cin.get();
cout<<"sir 2 ";
cin.get(sir2 ,25);
}

Dacă după prima citire nu am fi folosit funcţia cin.get() fără parametri, a


doua citire nu ar mai fi fost efectuată. De ce? Sfârşitul primului şir introdus a fost
marcat prin tastarea Enter. Aceasta a făcut ca în memorie (buffer) să fie păstrat
caracterul '\n'. La a doua citire, noul şir va începe cu acesta. Prin logica funcţiei
92 Capitolul 3 – Şiruri de caractere

cu trei parametri, citirea se face până la întâlnirea lui. În concluzie, se citeşte şirul
vid. Utilizatorul nu mai apucă să îşi tasteze textul. Prin utilizarea funcţiei fără
parametri, acel caracter se citeşte şi noua citire se poate efectua fără probleme.

3.3.3. Tipul char*


Vectorii de caractere au o proprietate deosebită. Priviţi programul următor:
#include <iostream.h>
main()
{
char a[]="masa";
cout<<a+1<<" "<<a+2<<" "<<a+3;
}

În urma executării, se tipăreşte: "asa sa a".

De ce? Limbajul C++ permite ca un vector de caractere să fie adresat


începând de la un anumit octet al său:
 când scriem a, adresăm vectorul - în ansamblul lui - începând cu
primul său octet (cel care reţine primul caracter al şirului de caractere);
echivalent, puteam scrie a+0;
 când scriem a+1, adresăm vectorul începând cu al doilea octet;
 când scriem a+2, adresăm vectorul începând cu al treilea octet;
 ...

Din acest motiv, programul tipăreşte pentru a+1 şirul "asa", pentru a+2
şirul "sa", ş.a.m.d. Mai mult, vectorii astfel adresaţi pot fi accesaţi aşa cum suntem
deja obişnuiţi. Astfel, pentru exemplul anterior, (a+1)[0] reţine caracterul 'a',
(a+1)[1] reţine caracterul 's', etc.

După cum observaţi, a+1, a+2, ... sunt expresii. Orice expresie are un
anumit tip. Care este tipul lor? Tipul acestor expresii este char*. Ce semnificaţie
are? Semnificaţia este cea de adresă.

Definiţia 3.5. Numărul de ordine al unui octet în memoria internă se


numeşte adresa octetului respectiv.

Definiţia 3.6. Adresa unui vector de caractere este adresa primului


său octet.

 O variabilă de tipul char* poate reţine adresa unui vector de caractere.

 În C++, numele unui vector de caractere este o adresă constantă de vector


şi poate fi atribuit unei variabile de tip char*.
Manual de informatică pentru clasa a XI-a 93

Considerăm următorul exemplu:

#include <iostream.h>
main()
{ char a[]="Exemplu", *p;
p=a; cout<<p<<endl;
p++; cout<<p<<endl;
p++; cout<<p<<endl;
cout<<p[1]<<endl;
cout<<p-a;
}

Am declarat un vector de caractere numit a, pe care l-am iniţializat cu un şir.


De asemenea, am declarat şi o variabilă de tip char* numită p. În aceste condiţii,
putem afirma că:
 p=a; este o atribuire corectă. Variabila p va reţine adresa vectorului de
caractere a, adică adresa primului său octet. În schimb, atribuirea a=p este
incorectă, pentru că a este o constantă ce reprezintă o adresă.
 după atribuire, putem utiliza variabila p în aceleaşi condiţii ca variabila v. De
exemplu, putem afişa şirul reţinut prin "cout<<p<<endl;".
 dacă la conţinutul variabilei p se adaugă 1, aceasta va reţine adresa
vectorului al cărui prim octet coincide cu al doilea octet al vectorului v. De
exemplu, dacă tipăresc vectorul se va afişa "xemplu". Noul vector se poate
adresa şi pe octeţi. Exemplu: p[1].
 se pot face şi scăderi între adrese. În acest caz, rezultatul este întreg. De
exemplu, prin p-a se obţine indicele în v, al primului octet al vectorului
reţinut de p. Testaţi programul!

3.3.4. Lungimea unui şir de caractere

Pentru a putea fi folosite funcţiile de prelucrare a şirurilor de caractere,


trebuie să fie inclus fişierul antet "string.h", tot aşa cum includem fişierul
"iostream.h":
#include <iostream.h>

 Funcţia strlen are rolul de a returna lungimea efectivă a unui şir (în calculul
lungimii nu intră caracterul nul). Forma generală este:
size_t strlen(char*);
unde:
• size_t este un tip întreg, utilizat în adresarea memoriei, definit în "string.h"
(îl putem privi ca pe tipul unsigned int);
• argumentul este de tip char* (adică o adresă către un şir).
94 Capitolul 3 – Şiruri de caractere

Programul de mai jos citeşte un şir şi afişează numărul de caractere pe care


le are şirul citit (exclusiv caracterul nul):

#include <iostream.h>
#include <string.h>
main()
{ char a[100];
cin.get(a,100);
cout<<"Sirul citit are "<<strlen (a)<<" caractere";
}

Aşa cum ştim din lucrul cu tablouri, atribuirile de forma a=b, unde a şi b sunt
vectori de caractere, nu sunt permise. Tot aşa, o atribuire de forma a="un sir"
nu este permisă. Astfel de operaţii ca şi multe altele se fac cu anumite funcţii, puse
la dispoziţie de limbaj. Pentru ca acestea să poată fi folosite, trebuie să fie inclus
fişierul antet "string.h", tot aşa cum includem fişierul "iostream.h". Ordinea
de includere nu are importanţă. În continuare, vom prezenta cele mai uzuale funcţii
şi modul în care acestea se folosesc.

3.3.5. Copierea şi concatenarea şirurilor de caractere

 Funcţia strcpy are forma generală:


char *strcpy(char* dest, char* sursa);

şi are rolul de a copia şirul de adresă sursa la adresa dest. Copierea se termină
după ce a fost copiat caracterul nul. Se returnează adresa dest. Analizaţi
codul următor:
#include <iostream.h>
#include <string.h>
main()
{ char a[100]="un sir", b[100]="alt sir";
strcpy (a,b);
cout<<a;
}

În programul de mai sus se copiază în vectorul de caractere a şirul de


caractere reţinut de b. Programul va afişa "alt sir". Această copiere simulează
atribuirea: a=b.

 Funcţia standard strcat are forma generală:


char* strcat(char* dest, char* sursa);

şi rolul de a adăuga şirului de adresă dest şirul de adresă sursa. Şirul de adresă
sursa rămâne nemodificat. Această operaţie se numeşte concatenare şi nu este
comutativă. Rezultatul este adresa şirului sursa, iar şirul va avea ca lungime,
suma lungimilor celor două şiruri care au fost concatenate.
Manual de informatică pentru clasa a XI-a 95

Programul următor tipăreşte "mama merge":


#include <iostream.h>
#include <string.h>
main()
{
char a[20]="mama", b[100]=" merge";
strcat (a,b);
cout<<a;
}

 Funcţia strncat are forma generală:

char *strncat(char *dest, const char *sursa, size_t nr);

şi acelaşi rol ca strcat cu deosebirea că adaugă şirului destinaţie primii nr octeţi ai


şirului sursă. Adăugarea caracterelor se face înaintea caracterului nul. Funcţia
returnează adresa de început a şirului destinaţie.

3.3.6. Căutarea unui caracter într-un şir

 Funcţia strchr are forma generală:

char* strchr(char *s, int c);

şi rolul de a căuta caracterul 'c' în şirul s. Căutarea se face de la stânga la


dreapta. În cazul în care caracterul este găsit, funcţia întoarce adresa subşirului
care începe cu prima apariţie a caracterului citit şi se termină cu caracterul nul al
şirului în care se face căutarea. Altfel, întoarce o expresie de tip char* cu valoarea
0 (adică o adresă vidă de şir). Exemplele care urmează ne vor lămuri.

În programul următor se caută în şirul a caracterul 't'. Acesta este găsit, iar
programul tipăreşte şirul "ta este". Evident, acesta este subşirul care începe cu
caracterul 't'.
#include <iostream.h>
#include <string.h>
main()
{
char a[20]="Acesta este";
cout<<strchr (a,'t');
}

 Aplicaţia 3.5. În unele cazuri ne interesează indicele în cadrul vectorului al


caracterului căutat. Acesta se obţine ca diferenţă între două valori de tipul char*.
Descăzutul este adresa returnată de funcţie, iar scăzătorul este adresa vectorului în
care se face căutarea.
96 Capitolul 3 – Şiruri de caractere

Programul de mai jos tipăreşte indicele primei apariţii a caracterului 't', şi anume 4:
#include <iostream.h>
#include <string.h>
main()
{
char a[20]="Acesta este";
cout<<strchr (a,'t')-a;;
}

 Aplicaţia 3.6. În programul următor se citeşte un şir şi un caracter. Dacă


acesta este găsit în şir, se tipăreşte indicele primei apariţii a caracterului în şirul
solicitat, altfel programul semnalează faptul că acest caracter nu există în şir.
#include <string.h>
main()
{
char a[100], *t,c;
cout<< "introduceti sirul "; cin.get(a,100);
cout<< "caracterul cutat "; cin>>c;
t=strchr(a,c);
if (t) cout<<"Indicele este "<<t-a;
else cout<<"sirul nu contine acest caracter ";
}

Observaţii

 Variabila t este de tipul char*.

 Testul de apartenenţă a caracterului la şir s-a făcut prin a vedea dacă


variabila t reţine sau nu 0 (adică o adresă nulă de şir).

 Aplicaţia 3.7. În programul următor se listează indicii tuturor apariţiilor


caracterului citit în şir:
#include <iostream.h>
#include <string.h>
main()
{
char a[100], *t,c;
cout<< "introduceti sirul "; cin.get(a,100);
cout<< "caracterul cutat "; cin>>c;
t=a-1;
do
{
t++;
t=strchr(t,c);
if (t) cout<<"Indicele este "<<t-a<<endl;
} while (t);
}

Fiecare adresă de subşir, care are ca prim caracter cel reţinut de c, intră în
calculul indicelui acelui caracter - din ea se scade adresa de început.
Manual de informatică pentru clasa a XI-a 97

 Funcţia strrchr are forma generală:


char *strrchr(const char *s, int c);

şi acelaşi rol cu strchr, deosebirea fiind că întoarce adresa ultimei apariţii a


caracterului (căutarea se face de la dreapta către stânga). Ea este utilizată în
ipoteza în care se caută ultima apariţie a caracterului în cadrul şirului.

3.3.7. Compararea şirurilor

 Funcţia strcmp are forma generală:


int strcmp(const char *s1, const char*s2);
şi rolul de a compara două şiruri de caractere Valoarea returnată este:

• <0, dacă s1<s2;


• =0, dacă s1=s2;
• >0, dacă s1>s2.

Dar care este mecanismul prin care se compară două şiruri de caractere?
Fie m numărul de caractere al şirului s1 şi n numărul de caractere al şirului s2.
Să presupunem că primele i caractere ale lui s1 coincid cu primele i ale lui s2.

 În cazul în care codul caracterului i+1 al şirului s1 este mai mare decât codul
caracterului corespunzător şirului s2, avem s1>s2.

 În cazul în care codul caracterului i+1 al şirului s1 este mai mic decât codul
caracterului corespunzător şirului s2, avem s1<s2. În cazul în care şi la
această comparaţie avem egalitate, avem patru posibilităţi:

• ambele şiruri au un număr strict mai mare de caractere decât i+1, caz în
care se compară ca înainte caracterele de pe poziţia i+2;

• s1 are i+1 caractere, iar s2 are un număr de caractere mai mare decât
i+1, în acest caz s1<s2;

• s2 are i+1 caractere, iar s1 are un număr de caractere mai mare decât
i+1, în acest caz s1>s2;

• atât s1 cât şi s2 au i+1 caractere, caz în care s1=s2.

Pe scurt, un şir s1 este mai mic ca altul s2, dacă în dicţionar s1 ar figura
înaintea lui s2.

Exemple: "soare">s;
"tata">mama;
98 Capitolul 3 – Şiruri de caractere

Probaţi relaţia de ordine dintre două cuvinte (şiruri care nu conţin


caractere albe) prin utilizarea programului următor:
#include <iostream.h>
#include <string.h>
main()
{
char a[100],b[100];
int semnal;
cout<< "introduceti sirul a "; cin>>a;
cout<< "introduceti sirul b "; cin>>b;
semnal=strcmp(a,b);
if (semnal<0) cout<<"a<b";
else
if (semnal>0) cout<<"a>b";
else cout<<"a=b";
}

Funcţia strcmp face distincţie între literele mari şi mici ale alfabetului.

 Funcţia stricmp are forma generală:


int stricmp(char *s1,char *s2);

şi acelaşi rol ca strcmp. Diferenţa este că nu face distincţie între literele mari şi
mici.
Vectori de cuvinte. Există aplicaţii în care este necesar să se lucreze cu n
cuvinte - înţelegând prin cuvânt o succesiune de caractere care nu sunt albe. În
acest caz avem posibilitatea să declarăm vectori de cuvinte. Acestea sunt, de fapt,
matrice cu elemente de bază de tip char.

Exemplu: char a[10][25];

Fiecare linie din cele 10 ale matricei poate reţine un şir de caractere. Acesta poate
avea cel mult 25 de caractere (inclusiv caracterul nul). Cuvintele pot fi adresate prin
a[0] (primul cuvânt), a[1] cuvântul al doilea, ş.a.m.d.

 Aplicaţia 3.8. În programul următor se citesc n cuvinte. Acestea sunt sortate


alfabetic:
#include <iostream.h>
#include <string.h>
main()
{
char cuvinte[10][25], man[25];
int i,n,gasit;
cout<<"n=";
cin>>n;
for (i=0;i<n;i++)
{ cout<<"cuvant ";
cin>>cuvinte[i];
}
Manual de informatică pentru clasa a XI-a 99

do
{ gasit=0;
for (i=0;i<n-1;i++)
if (strcmp(cuvinte[i],cuvinte[i+1])>0)
{ strcpy(man,cuvinte[i]);
strcpy(cuvinte[i],cuvinte[i+1]);
strcpy(cuvinte[i+1],man);
gasit=1;
}
}
while (gasit);
for (i=0;i<n;i++) cout<<cuvinte[i]<<endl;
}

Dacă pot compara două cuvinte, atunci pot să le sortez. Am folosit sortarea
prin interschimbare. Iată că, algoritmii, cu precădere cei fundamentali, pot fi folosiţi
şi într-un context diferit de cel în care au fost prezentaţi.

3.3.8. Subşiruri

 Funcţia strstr are forma generală:


char *strstr(const char *s1, const char *s2);

şi are rolul de a identifica dacă şirul s2 este subşir (caractere succesive) al şirului
s1. Dacă acesta este identificat, funcţia returnează adresa de început în cadrul
şirului s1, altfel returnează adresa nulă (0). Căutarea se face de la stânga la
dreapta. În cazul în care s2 apare de mai multe ori în cadrul lui s1, se returnează
adresa de început a primei apariţii.
Exemplu: fie char s1[]="xyzt", s2[]="yz", atunci strstr(s1,s2);
returnează s1+1 (adresa caracterului y în s1).

 Aplicaţia 3.9. În programul următor se citesc două şiruri de caractere şi se


testează dacă al doilea este subşir al primului. În caz afirmativ, programul afişează
indicele caracterului de început al subşirului.
#include <iostream.h>
#include <string.h>
main()
{
char sir[1000],subsir[25], *t;
cout<<"introduceti textul ";
cin.get(sir,1000);
cin.get();
cout<<"introduceti subsirul cautat ";
cin.get(subsir,25);
t=strstr(sir,subsir);
if (t) cout<<"este subsir si are indicele "<<t-sir;
else cout<<"nu este subsir";
}
100 Capitolul 3 – Şiruri de caractere

 Aplicaţia 3.10. Ştergerea tuturor apariţiilor unui subşir din cadrul unui şir.
Imediat ce am identificat adresa de început a subşirului, restul şirului (fără subşir)
este copiat pe poziţia de început a subşirului.
#include <iostream.h>
#include <string.h>
main()
{ char sir[1000],subsir[25],*p;
int lung_subsir;
cout<<"introduceti textul ";
cin.get(sir,1000);
cin.get();
cout<<"introduceti subsirul ";
cin.get(subsir,25);
lung_subsir=strlen(subsir);
p=strstr(sir,subsir);
while (p)
{ strcpy(p,p+lung_subsir);
p=strstr(p,subsir);
}
cout<<sir;
}

 Aplicaţia 3.11. Înlocuirea tuturor apariţiilor unui subşir cu alt subşir. Vă las pe
dvs. să descoperiţi modul în care programul următor realizează aceasta:
#include <iostream.h>
#include <string.h>
main()
{ char sir[100], man[100], sterg[25], adaug[25], *p;
int lung_sterg, lung_adaug;
cout<<"introduceti textul ";
cin.get(sir,100);
cin.get();
cout<<"inlocuim subsirul ";
cin.get(sterg,25);
cin.get();
cout<<"cu subsirul ";
cin.get(adaug,25);
lung_sterg=strlen(sterg);
lung_adaug=strlen(adaug);
p=strstr(sir,sterg);
while (p)
{
man[0]=0;//subsir vid;
strncat(man,sir,p-sir);
strcat(man,adaug);
strcat(man,p+lung_sterg);
strcpy(sir,man);
p=strstr(p+lung_adaug,sterg);
}
cout<<sir;
}
Manual de informatică pentru clasa a XI-a 101

3.3.9. Alte funcţii utile în prelucrarea şirurilor

 Funcţia strtok are forma generală:


char *strtok(char *s1, const char *s2);

Principiul de executare este următorul:

• şirul s1 este considerat ca fiind alcătuit din 0, 1, ..., n entităţi separate prin
unul sau mai multe caractere cu rol de separator, iar şirul s2 ca fiind alcătuit
din unul sau mai multe caractere cu rol de separator;
Exemplu: şirul s1 este " mama, tata si bunicul". Şirul s2 este:
" ,". Întrucât caracterele separatoare sunt blank-ul şi virgula, rezultă că
entităţile sunt: "mama", "tata" "si" "bunicul".

• la prima apelare care trebuie să fie de forma strtok(s1,s2);, funcţia


întoarce adresa primului caracter al primei entităţi (în exemplu "mama"). În
plus, după prima entitate separatorul este înlocuit automat prin caracterul
nul (cod 0);

• următoarele apeluri ale funcţiei sunt de forma strtok(NULL,s2);, iar


funcţia întoarce de fiecare dată adresa primului caracter al următoarei entităţi
şi, după ea, este adăugat caracterul nul - aceasta permite ca entitatea să
poată fi extrasă cu uşurinţă.
...
• în momentul în care şirul rămas nu mai conţine entităţi, funcţia întoarce
adresa nulă.

 Aplicaţia 3.12. În programul următor se citeşte un şir de caractere. Entităţile se


consideră a fi cuvinte - şiruri de caractere care nu sunt albe - separate prin blank-uri
şi/sau virgule. Programul listează entităţile depistate, fiecare pe un rând.
#include <iostream.h>
#include <string.h>
main()
{ char sir[1000],separator[]=" ,", *p;
cin.get(sir,1000);
p=strtok(sir,separator);
while (p)
{
cout<<p<<endl;
p=strtok(NULL, separator);
}
}

Variabila p reţine adresa de început a entităţii. Dacă entitatea este găsită, p


va reţine o valoare diferită de 0, caz în care entitatea este afişată şi se va căuta
următoarea entitate.
102 Capitolul 3 – Şiruri de caractere

 Aplicaţia 3.13. Programul următor citeşte un şir de caractere şi tipăreşte şirul


obţinut prin eliminarea blank-urilor. În fapt, se separă entităţile, care sunt afişate
una după alta.
#include <iostream.h>
#include <string.h>
main()
{ char sir[1000], separator[]=" ,", *p;
cin.get(sir,1000);
p=strtok(sir,separator);
while (p)
{ cout<<p;
p=strtok(NULL, separator); }
}

 Funcţia strcspn are forma generală:


size_t strcspn(const char *s1, const char *s2);
şi are rolul de a returna numărul de caractere al şirului s1 - caractere consecutive
care încep obligatoriu cu primul caracter - care nu se găsesc în şirul s2.

 Funcţia standard strspn are forma generală:


size_t strspn(char *s1, char *s2);
şi are rolul de a returna numărul de caractere al şirului s1 - caractere consecutive
care încep obligatoriu cu primul caracter - care se găsesc în şirul s2.
Exemple
1. Fie char* s1="AB2def"; *s2="123";. Atunci: strcspn(s1,s2)
returnează 2 (pentru că primele două caractere 'A' şi 'B' din s1 nu se găsesc în
s2), iar strspn(s1,s2) returnează 0 (primul caracter din s1 nu se găseşte în s2).
2. Fie char* s1="AB2def"; *s2="16A32BF";. Atunci: strcspn(s1,s2)
returnează 0 (pentru că primul caracter 'A' din s1 se găseşte în s2), iar
strspn(s1,s2) returnează 3 (caracterele 'A', 'B', '2' din s1 se găsesc în s2).

 Aplicaţia 3.14. Se citeşte un şir de caractere care nu conţine caractere albe. Să


se verifice dacă şirul este alcătuit exclusiv din caractere numerice.
Vom reţine un vector numit cifre. Componentele lui reţin toate caracterele
numerice 0-9. Şirul de caractere citit are toate caracterele numerice dacă toate
caracterele sale - numărul lor este dat de strlen - se regăsesc printre
caracterele vectorului cifre.
#include <iostream.h>
#include <string.h>
main()
{ char cuvant[100], cifre[]="0123456789";
cout<<"Introduceti cuvantul "; cin>>cuvant;
if (strspn(cuvant,cifre)==strlen(cuvant)) cout<<"numeric";
else cout<<"nenumeric";
}
Manual de informatică pentru clasa a XI-a 103

 Aplicaţia 3.15. Se citeşte un şir de caractere care nu conţine caractere albe. Să


se verifice dacă şirul e alcătuit exclusiv din caractere nenumerice.
Vom reţine un vector numit cifre. Componentele lui reţin toate caracterele
numerice: 0-9. Cuvântul citit are toate caracterele nenumerice dacă toate
caracterele vectorului cifre - numărul lor este 10 - nu se regăsesc printre
caracterele vectorului cuvant.
#include <iostream.h>
#include <string.h>
main()
{ char cuvant[100], cifre[]="0123456789";
cout<<"Introduceti cuvantul "; cin>>cuvant;
if (strcspn(cifre,cuvant)==10) cout<<"corect ";
else cout<<"incorect";
}

 Funcţia strlwr are forma generală:


char *strlwr(char *s);
Ea converteşte toate literele mari ('A' ... 'Z') în litere mici ('a' ... 'z').
Restul caracterelor rămân neschimbate. Funcţia întoarce adresa s.
 Funcţia strupr are forma generală:
char *strupr(char *s);
şi rolul de a converti toate literele mici ('a' ... 'z') în litere mari ('A ... Z').
Restul caracterelor rămân neschimbate. Funcţia întoarce adresa s. În programul
următor se citeşte un cuvânt. Cuvântul citit se tipăreşte cu litere mari.
#include <iostream.h>
#include <string.h>
main()
{ char a[20];
cout<<" Introduceti cuvantul "; cin>>a;
cout<<strupr(a);
}

 Funcţia strpbrk are forma generală:


char *strpbrk(char *s1, char *s2);
şi acţionează în felul următor:
• Caută primul caracter al şirului s1 în s2. Dacă este găsit, returnează adresa
sa din cadrul şirului s1 şi execuţia se termină, altfel trece la pasul următor.
• Caută al doilea caracter al şirului s1 în s2. Dacă este găsit, returnează adresa
sa din cadrul şirului s1 şi execuţia se termină, altfel trece la pasul următor.
• ...
• dacă nici un caracter al şirului s1 nu aparţine şirului s2, funcţia returnează
adresa nulă.
104 Capitolul 3 – Şiruri de caractere

Exemplu: fie char *s1="abcdefghijklmnopqrstuvwxyz",*s2 = "c12";.


Atunci strpbrk(s1, s2) returnează adresa s1+2 (adresa caracterului c, pentru
că acesta este primul caracter din s1 găsit în s2).

 Aplicaţia 3.16. Se citesc două cuvinte. Se cere să se afişeze toate caracterele


primului cuvânt care se regăsesc în al doilea.
#include <iostream.h>
#include <string.h>
main()
{ char cuvant1[10], cuvant2[10],*p;
cout<<"Introduceti primul cuvant "; cin>>cuvant1;
cout<<"Introduceti al doilea cuvant "; cin>>cuvant2;
p=strpbrk(cuvant1,cuvant2);
while(p)
{ cout<<p[0]<<endl;
p++;
p=strpbrk(p,cuvant2);
}
}

Caracterele primului cuvânt, începând de la primul, sunt căutate în al doilea


cuvânt. Orice caracter găsit se tipăreşte. Aceasta se face pornind de la adresa
şirului, returnată de strpbrk. Apoi, variabila care conţine această adresă se
incrementează. Aceasta pentru ca, la următoarea căutare, să nu fie returnată
aceeaşi adresă de şir.

3.3.10. Conversia şirurilor în valori numerice şi invers

Următoarele funcţii au prototipul în "stdlib.h" şi folosesc pentru


conversia valorilor numerice în şir şi invers.
 Funcţia atof converteşte un şir către tipul double. Dacă conversia eşuează
(se întâlneşte un caracter nenumeric) valoarea întoarsă este 0. Dacă primele
caractere ale şirului sunt albe, acestea sunt ignorate.
double atof(const char *s);

Observaţiile sunt valabile şi pentru următoarele 3 funcţii:


 Funcţia _atold converteşte un şir către tipul long double:
long double _atold(const char *s);

 Funcţia atoi converteşte un şir către tipul int:


int atoi(const char *s);

 Funcţia atol converteşte un şir către tipul long:


long atol(const char *s);
Manual de informatică pentru clasa a XI-a 105

 Aplicaţia 3.17. Se citeşte un text alcătuit din mai multe cuvinte. Se cere să se
calculeze suma valorilor numerice întâlnite în text. Se presupune că valorile
numerice sunt introduse corect.
Şirul se separă în entităţi. Dacă o entitate este alcătuită exclusiv din '+',
'-', '.' şi cifre, atunci ea este convertită în double şi adunată la o variabilă s,
iniţializată cu 0.
#include <iostream.h>
#include <string.h>
#include <stdlib.h>
main()
{ char sir[1000],separator[]=" ",cifre[]="0123456789.+-", *p;
double s=0;
cin.get(sir,1000);
p=strtok(sir,separator);
while (p)
{ if (strspn(p,cifre)==strlen(p)) s+=atof(p);
p=strtok(NULL, separator);
}
cout<<"suma numerelor intalnite in sir este "<<s;
}

 Funcţia ecvt are rolul de a converti o valoare de tip double către un şir.
Caracterul nul este adăugat automat şirului obţinut. Forma generală este:
char* ecvt(double valoare, int poz, int* zec, int* semn);
unde:
• valoare - valoarea de convertit;
• poz - numărul de poziţii pe care trebuie să le ocupe şirul obţinut în urma
conversiei;
• zec - adresa unei variabile, de tip int, care reţine, după apel, numărul
zecimalelor pe care le are numărul;
• semn - adresa unei variabile, de tip int, care are rolul de a memora,
după apel, 1, dacă numărul este negativ, sau 0, în caz contrar.

Până în prezent nu am studiat modul în care putem obţine adresa unei


variabile. Reţineţi că dacă a este o variabilă, &a este adresa ei. Funcţia utilizează
acest mecanism de transmitere a parametrilor pentru ca să poată returna anumite
rezultate pe care le găsim în variabilele respective. În cazul ei, variabila zec va
reţine după apel numărul întregilor, iar variabila semn semnul rezultatului.

Funcţia nu inserează în şir nici punctul zecimal, nici semnul numărului. Şirul
obţinut începe cu cifra cea mai semnificativă a numărului. Pentru ca şirul obţinut să
fie corect, este necesar să fie prelucrat, în continuare, de către programator. Dacă
numărul poziţiilor ocupate de şir (poz) este mai mare decât ocupă data, şirul este
completat în stânga cu un număr de 0 necesar. În cazul în care numărul poziţiilor
este mai mic decât cel ocupat de număr, rezultatul este rotunjit.
Exemple: ecvt(val,3,&zec,&semn);
106 Capitolul 3 – Şiruri de caractere

− dacă val reţine 1234, atunci se returnează şirul "123", zec reţine 4 şi
semn reţine 0.
− dacă val reţine 1239, atunci se returnează şirul "124", zec reţine 4 şi
semn reţine 0.
− dacă val reţine 1, atunci se returnează şirul "100", zec reţine 1 şi semn
reţine 0.
− dacă val reţine -0.001, atunci se returnează şirul "100", zec reţine -3
şi semn reţine 1.

Pentru a vă convinge, rulaţi programul următor:


#include <iostream.h>
#include <stdlib.h>
#include <string.h>
main()
{ double numar;
int zec,semn;
char numar_sir[20]="", numar_prel[20]="";
cout<<"n="; cin>>numar;
//convertesc numarul si tiparesc rezultatul
// asa cum este furnizat de functie
strcpy(numar_sir,ecvt(numar,19,&zec, &semn));
cout<<"Sirul este "<<numar_sir<<" "<<zec<<" "<<semn<<endl;
// prelucrez sirul pentru a tipari rezultatul asa cum
// este asteptat de utilizator.
if (semn) strcat(numar_prel,"-");
strncat(numar_prel,numar_sir,zec);
strcat(numar_prel,".");
strncat(numar_prel,numar_sir+zec,3);
cout<<numar_prel;
}

 Funcţia itoa are rolul de a converti o valoare de tip int într-un şir, a cărui
adresă este memorată în variabila sir. Valoarea baza reţine baza de numeraţie
către care să se facă conversia (de obicei baza este 10). În cazul bazei 10, şirul
obţinut reţine şi, eventual, semnul “-“. Funcţia întoarce adresa şirului obţinut.

char* itoa(int valoare, char *sir, int baza);

 Funcţia ltoa are acelaşi efect ca itoa, deosebirea fiind dată de faptul că se
converteşte către şir o valoare de tip long int.

char* ltoa(long value, char *sir, int baza);

 Funcţia ultoa are acelaşi efect ca itoa, deosebirea fiind dată de faptul că
se converteşte către şir o valoare de tip unsigned long.

char* ultoa(unsigned long value, char *sir, int baza);

Pentru conversii, se mai pot utiliza următoarele funcţii:


Manual de informatică pentru clasa a XI-a 107

 Funcţia
long strtol(const char *s, char **endptr, int baza);

are rolul de a converti un şir către long. În afara şirului care trebuie convertit,
funcţia primeşte ca parametru de intrare adresa unei variabile de tip char*. După
apel, această variabilă va reţine poziţia primului caracter din şir care nu poate fi
convertit. De exemplu, dacă şirul este "12m21", după apel, variabila a cărei adresă
a fost transmisă ca parametru reţine adresa caracterului 'm'. Dar care este rostul
existenţei acestei informaţii? Cu ajutorul ei se pot depista eventualele erori apărute
atunci când se introduc datele. Priviţi şirul dat anterior ca exemplu: e clar că
utilizatorul a dorit să introducă un număr, dar în locul unei cifre a tastat 'm'.

 Aplicaţia 3.18. Programul următor testează dacă o valoare introdusă este


numerică şi dacă este cuprinsă în intervalul [10,20]. Testul propriu-zis se face
atunci când comparăm numărul caracterelor convertite cu lungimea şirului.
Egalitatea are semnificaţia că întreg şirul este numeric. Variabila radix trebuie să
conţină 8, 10 sau 16, adică baza în care este considerat numărul sub formă de şir.
#include <iostream.h>
#include <stdlib.h>
#include <string.h>
main(void)
{ char numar[20], *adresa;
long v;
cin>>numar;
v=strtol(numar,&adresa,10);
if(adresa-numar!=strlen(numar))
cout<<"Data contine caractere nenumerice";
else
if (v<10 || v>20) cout<<"data numerica in afara limitei ";
else cout<<v<<endl;
}

 Funcţia
double strtod(const char *s, char **endptr);
converteşte un şir către double.
 Funcţia
long double _strtold(const char *(s), char **endptr);
converteşte un şir către long double.
 Funcţia
unsigned long strtoul(const char *s,char **endptr,int baza);
converteşte un şir către unsigned long.

În caz de depăşire - adică numărul nu poate fi memorat deoarece este în


afara tipului - ultimele patru funcţii returnează cea mai mare sau cea mai mică
valoare care poate fi memorată de tipul în care se face conversia, după
cum valoarea este pozitivă sau negativă.
108 Capitolul 3 – Şiruri de caractere

3.3.11. Citirea şi scrierea şirurilor de caractere din şi în


fişiere text

3.3.11.1. Operaţia de citire

Pentru a citi o linie a unui fişier text, se poate folosi funcţia următoare:

getline(char* Adresa_sir, int Nr_car, char='\n').

Funcţia citeşte un şir de caractere, până când una dintre condiţiile următoare
este îndeplinită:

a) au fost citite Nr_car-1 caractere;


b) a fost detectat sfârşitul de linie: '\n'.

Evident, în acest fel, la o citire pot fi aduse în memorie cel mult 32766
caractere (vezi limitele tipului int).

Observaţii

 Caracterul care marchează sfârşitul liniei '\n' nu este inserat în şir, în


schimb este inserat caracterul nul (marchează sfârşitul şirului).
 Este suficient să apelăm funcţia cu primii doi parametri, ultimul fiind implicit.
 Dacă se doreşte, ultimul parametru poate fi apelat explicit cu o valoare
convenabilă, caz în care citirea se face până la întâlnirea ei sau până când
au fost citite Nr_car-1 caractere.

Programul următor citeşte linie cu linie un fişier text ale cărui linii nu au
mai mult de 500 de caractere şi afişează liniile pe monitor. Nu se
cunoaşte lungimea fiecărei linii.
#include <fstream.h>
main()
{ fstream f("f.dat",ios::in);
char Linie_citita[501];
while (f.getline(Linie_citita,501))
cout<<Linie_citita<<endl;
}

Observaţie foarte importantă! Prin f>>Linie_citita, citirea se


efectuează astfel:
a) începând cu poziţia curentă a pointerului se sar toate caracterele albe;
b) se citesc toate caracterele până la întâlnirea unui caracter alb.
De exemplu, dacă o linie a fişierului conţine şirul " Afara ploua", se
citeşte şirul "Afara". Testaţi!
Manual de informatică pentru clasa a XI-a 109

3.3.11.2. Operaţia de scriere

Scrierea se face prin comanda f<< sir;. Prin această instrucţiune se


scrie întreg şirul, începând cu poziţia curentă a pointerului, inclusiv caracterele albe.
Analizaţi exemplul următor:
#include <fstream.h>
main()
{ fstream f("f.dat",ios::out);
f<<" Afara este"<<endl;
f<<"primavara!";
f.close();
}

3.3.12. O modalitate de conversie de la şir la alt tip

Există şi alte posibilităţi de conversie de şiruri la alte tipuri de date. Un tip


special, numit istrstream, permite chiar declararea stream-urilor (fluxurilor) de
la şiruri către variabile. "Citirea" se efectuează cu ajutorul operatorului “>>” la fel ca
din fişier.

Şir de caractere Variabilă

Un şir X reţine "1 2 3 4 5". O funcţie specială (numită constructor)


ataşează şirului X, un stream (flux), numit ins. Ea are doi parametri: şirul X şi
lungimea lui: istrstream ins(X, strlen(X));.

"Citirea" se efectuează cu conversie către tipul respectiv. Programul va afişa


numerele 1,2,3,4,5 câte unul pe linie!
#include <iostream.h>
#include <strstrea.h>
#include <string.h>
main()
{ char X[]="1 2 3 4 5";
istrstream ins(X, strlen(X));
int nr;
while (ins>>nr) cout<<nr<<endl;
}

Observaţi cât de simplu se detectează sfârşitul şirului.

Tot aşa, se pot "citi" mai multe cuvinte dintr-un şir. Programul următor citeşte
cuvintele şi le afişează. De această dată se consideră că este şir valid şi cel de
lungime 0, fapt care conduce la o nouă citire şi deci la ciclare. Pentru aceasta,
de fiecare dată, se testează ca lungimea şirului citit să fie nevidă.
110 Capitolul 3 – Şiruri de caractere

Programul este prezentat în continuare:


#include <iostream.h>
#include <strstrea.h>
#include <string.h>
main()
{
char X[]="1 mama tata 4 bunica";
char cuvant[20];
istrstream ins(X, strlen(X));
while (ins>>cuvant && strlen(cuvant))
cout<<cuvant<<endl;
}

Probleme propuse
1. Se citeşte de la tastatură un text. Cuvintele se consideră separate prin virgulă,
spaţiu sau punct. Câte cuvinte are textul citit?
2. Se citeşte de la tastatură un text şi o succesiune de caractere. De câte ori
întâlnim această succesiune în cadrul textului?
3. Se citeşte un text. Două cuvinte pot fi separate printr-unul sau mai multe spaţii.
Se cere să se elimine spaţiile inutile.
4. Se citeşte un fişier text care conţine o singură propoziţie, pe o linie. Programul
rearanjează literele în fiecare propoziţie, în ordine alfabetică, păstrând locul
cuvintelor. Numai literele de la 'A' la 'Z' sunt afectate. Celelalte caractere rămân
neschimbate. Ieşirea va fi tot un fişier text. Numele fişierelor este ales de dvs.
Exemple:

THE PRICE OF BREAD IS $1.25 PER POUND.


ABC DDEEE EF HIIINO OP $1.25 PPR RRSTU.
THE LICENSE PLATE READ G76-ZA3.
AAA CDEEEEE GHILL NPRS T76-TZ3.
Junior Division
5. Creştere automată. Scrieţi un program care să mărească toate numerele care
apar într-un document (citit dintr-un fişier text) după un procent citit de la tastatură.
De exemplu, dacă se introduce 12, procentul este: 12%. Toate numerele trebuie
afişate cu două zecimale. Testaţi programul dvs. pe fraza:

"Batranul Mc Donald's avea 7 vaci care dadeau 120 de litri de


lapte pe zi. Ele veneau acasa la 4 P.M"

Junior Division
Manual de informatică pentru clasa a XI-a 111

6. Eliminare. "MSSSSPP" este versiunea micşorată a cuvântului "MISSISSIPPI",


obţinută prin eliminarea tuturor literelor "I". O frază poate fi micşorată şi ea:
"TRAVERSAREA CAII FERATE" are versiunea micşorată "TRVRSR C FRT", prin
eliminarea literelor "A", "I", "E". Se cere ca programul dvs. să elimine literele dintr-o
frază citită din fişierul text "input.txt" şi scrisă pe o singură linie. Literele care se
elimină se citesc de la tastatură, iar ieşirea este în fişierul "output.txt".
Junior Division

7. Problemă de lucru în colectiv - numai pentru elevii care studiază limbajul


Pascal. Şiruri generalizate.
Presupunem că nu sunteţi mulţumiţi de faptul că, în Pascal, şirurile cu care
se poate lucra pot avea cel mult 255 de caractere.

De exemplu, dorim să lucrăm cu şiruri care au cel mult 3000 de caractere.


Un astfel de şir va fi reţinut ca un vector de 3000 de caractere, iar sfârşitul unui şir
va fi marcat prin memorarea valorii 0 (în binar).

Scrieţi subprogramele următoare:


 subprogram care determină lungimea unui şir;
 subprogram care permite concatenarea a două şiruri;
 subprogram care identifică dacă un şir este sau nu subşir pentru altul;
 subprogram care converteşte un şir către un întreg;
 subprogram care converteşte un întreg către un şir;
 subprogram care converteşte un şir către un număr real;
 subprogram care converteşte un real către un şir;
 subprogram care inserează un şir pe o anumită poziţie a altui şir;
 subprogram care şterge un subşir al unui şir dat (se cunoaşte poziţia
de început a subşirului);
 subprogram care permite scrierea unui şir într-un fişier text;
 subprogram care permite citirea unui şir dintr-un fişier text.
112

Capitolul 4
Structuri de date neomogene

4.1. Noţiuni introductive

La ”Tehnologia Informaţiei şi a Comunicaţiilor” am învăţat să lucrăm cu


tabele (Excel sau Access). Să considerăm o linie a unui tabel. De exemplu, ea
conţine numele unei persoane (30 caractere alfabetice), vârsta (un întreg) şi
salariul brut (un număr real). Liniile, în totalitatea lor, alcătuiesc tabelul. Făcând
abstracţie de faptul că la ”Tehnologia Informaţiei şi a Comunicaţiilor” se folosesc
programe gata făcute, specializate, destinate unor utilizatori cărora nu li se pretinde
să fie experţi în informatică, se pot pune mai multe întrebări, cum ar fi:
 cum se poate reţine în memorie o linie a unui tabel?
 cum se poate reţine un tabel pe suport extern?

Răspunsul la prima întrebare este dat de existenţa unor variabile speciale,


care au o structură neomogenă. O astfel de variabilă este împărţită, la rândul ei, în
mai multe subvariabile numite, uneori, câmpuri. Pentru exemplul considerat, o
variabilă, s-o numim Pers, va conţine o subvariabilă numită Nume, care este un şir
de caractere; o alta, numită Varsta, pentru care se alege un tip întreg şi o alta,
numită Salariu, de un tip real. Astfel, observăm că variabila Pers este
neomogenă din punct de vedere al structurii, spre deosebire, de exemplu, de un
vector care are toate componentele de acelaşi tip. Cum se declară şi cum se
utilizează o astfel de variabilă vom învăţa în acest capitol.

În ceea ce priveşte răspunsul la a doua întrebare, o posibilitate de a reţine un


tabel este dată de crearea unui fişier cu tip.
Crearea şi exploatarea fişierelor cu tip nu face parte din programa dvs., dar
este recomandabil să le studiaţi în mod individual.

4.2. Structuri neomogene în Pascal

4.2.1. Tipul Record


În practică, apar situaţii în care toate tipurile de date învăţate până în prezent
nu ne sunt de mare folos. Să analizăm un exemplu.

Presupunem că dorim să prelucrăm anumite date referitoare la mai mulţi


elevi. Pentru fiecare elev, cunoaştem:
Manual de informatică pentru clasa a XI-a 113

1. Numele şi prenumele - se reţine în string[20];


2. Nota la matematică – variabilă de tip real;
3. Nota la fizică - variabilă de tip real;
4. Vârsta - variabilă de tip byte;
5. Dacă este băiat sau fată - o variabilă de tip char reţine 'B' sau 'F'.

Observaţi faptul că informaţiile referitoare la un elev sunt eterogene (de la


numele de tip string, până vârsta de tip byte). În Pascal, există posibilitatea ca
toate aceste informaţii să se regăsească într-un singur tip de înregistrare,
numit RECORD.
În programul următor, observăm cum se declară (în acest caz, elev), cum
se citeşte şi cum se afişează o variabilă (numită e) de acest tip:
type elev = record
nume: string[20];
n_mat, n_fiz: real;
varsta: byte;
sex: char;
end;
var e: elev;
begin
write('nume elev '); readln(e.nume);
write('nota matematica '); readln(e.n_mat);
write('nota fizica '); readln(e.n_fiz);
write('varsta '); readln(e.varsta);
write ('sexul '); readln(e.sex);
writeln('nume ', e.nume);
writeln('nota matematica ', e.n_mat);
writeln('nota fizica ', e.n_fiz);
writeln('varsta ', e.varsta);
writeln('sexul ', e.sex);
end.

Observaţii

 Pentru a adresa un anumit câmp al variabilei de tip RECORD, se foloseşte


numele ei, urmat de '.', apoi de numele câmpului.

Exemplu: e.nume reprezintă câmpul nume al variabilei e.


 În cazul în care avem mai multe câmpuri adiacente (care urmează unul după
altul) şi de acelaşi tip, le putem declara deodată, separându-le prin virgulă.

Exemplu: câmpurile: ”Nota la matematică” şi ”Nota la fizică” sunt adiacente


şi de tip real, deci declaraţia este:
n_mat, n_fiz: real;
114 Capitolul 4. Structuri de date neomogene

 Iată cum arată în memorie o înregistrare de acest tip - ocupă 35 de octeţi:

21 octeti 6 octeti 6 octeti 1 octet 1 octet


       
nume n.mat n. fiz virsta sex

 Câmpurile unei variabile de acest tip pot fi citite sau afişate individual. De
asemenea, li se pot atribui valori, pot intra în calcule, tot individual.
 Dacă a şi b sunt două variabile de acelaşi tip RECORD se pot face fără
probleme atribuiri de genul a:=b;.

4.2.2. Accesul simplificat la câmpuri

Aşa cum a fost prezentat, modul de acces la câmpurile unei variabile de tip
RECORD este deosebit de greoi - întotdeauna punem numele variabilei în faţă. În
realitate, accesul la câmpurile unei astfel de variabile se poate face mult mai uşor,
prin utilizarea instrucţiunii WITH, cu forma generală:
with var1, var2, ..., varn do instrucţiune

şi are rolul ca, în cadrul instrucţiunii subordonate, adresarea să se facă simplificat,


adică prin utilizarea exclusivă a numelui câmpului.

Reluăm programul anterior, dar de această dată am utilizat instrucţiunea with:


type elev = record
nume: string[20];
n_mat, n_fiz: real;
varsta: byte;
sex: char;
end;
var e: elev;
begin
with e do
begin
write('nume elev '); readln(nume);
write('nota matematica '); readln(n_mat);
write('nota fizica '); readln(n_fiz);
write('varsta '); readln(varsta);
write ('sexul '); readln(sex);
writeln('nume ',nume);
writeln('nota matematica ',n_mat);
writeln('nota fizica ',n_fiz);
writeln('varsta ',varsta);
writeln('sexul ',sex);
end
end.
Manual de informatică pentru clasa a XI-a 115

4.2.3. Înregistrări imbricate

În general, câmpurile care alcătuiesc un tip RECORD pot avea orice tip. O
întrebare: dar RECORD? Răspunsul este afirmativ. Analizaţi programul următor:
program r3;
type elev = record
nume: string[20];
data_n: record
zi, luna: byte;
an: integer;
end;
n_mat, n_fiz: real;
varsta: byte;
sex: char;
end;
var e: elev;
begin
with e do
begin
write('nume elev '); readln(nume);
with data_n do
begin
write('ziua nasterii '); readln(zi);
write('luna nasterii '); readln(luna);
write('anul nasterii '); readln(an);
end;
end
end.

Pentru un elev, se citesc numele şi data naşterii. Aceasta din urmă este de
tipul RECORD (include ziua, luna şi anul naşterii). Pentru adresarea simplificată a
câmpurilor care o alcătuiesc s-a folosit, din nou, instrucţiunea WITH. Adresarea s-ar
fi putut face şi aşa: e.data_n.zi, e.data_n.luna, e.data_n.an. Evident, o
astfel de adresare este greoaie şi nerecomandată…
Putem avea oricâte niveluri de imbricare (un tip RECORD include un alt tip
RECORD, care include un altul, ş.a.m.d.).

4.2.4. Vectori de înregistrări


Aşa cum s-a arătat, elementele unui vector pot fi de orice tip, deci inclusiv
de tip RECORD.
Adresarea câmpurilor se face prin numele vectorului, urmat de perechea de
paranteze drepte între care este trecut indicele componentei, apoi selecţia
propriu-zisă se face aşa cum am învăţat.
Dacă v este vectorul, iar înregistrarea este de tip elev (ca în programul
următor), atunci numele se selectează prin v[i].nume, iar ziua naşterii
prin v[i].nume.zi.
116 Capitolul 4. Structuri de date neomogene

Şi de această dată, pentru selecţia simplificată, se poate utiliza cu succes


instrucţiunea WITH, aşa cum rezultă din programul următor, unde se citeşte un
vector cu n înregistrări de tip elev:

type elev = record


nume: string[20];
data_n: record
zi,luna:byte;
an:integer;
end;
end;
vector_inregistrari = array[1..9] of elev;
var v: vector_inregistrari;
n, i: integer;
begin
write('n='); readln(n);
for i := 1 to n do
with v[i] do
begin
write('nume elev '); readln(nume);
with data_n do
begin
write('ziua nasterii '); readln(zi);
write('luna nasterii '); readln(luna);
write('anul nasterii '); readln(an);
end;
end
end.

4.2.5. Înregistrare cu variante

Nu toate înregistrările au o structură fixă (acelaşi număr de câmpuri) aşa


cum au fost cele prezentate. Sunt cazuri când un tip înregistrare are o parte fixă
urmată de o parte variabilă.

Să presupunem că ne interesează o situaţie referitoare la studiile unei


persoane. O astfel de înregistrare are o parte fixă dată de câmpurile care reţin
numele, vârsta şi tipul de studii. O persoană poate să nu aibă studii, caz în care nu
mai este necesar să avem alte informaţii, poate să fi făcut câteva clase de şcoală
generală (ne-ar putea interesa câte clase), să fi terminat liceul (caz în care dorim
să ştim anul terminării şi oraşul) sau să aibă studii superioare (şi atunci ne
interesează numele facultăţii şi numărul de ani de studiu în cadrul facultăţii
respective). În concluzie, în funcţie de tipul de studii, înregistrarea arată altfel.
Limbajul permite ca înregistrările să aibă o structură variabilă. Cum se
realizează aceasta?
În primul rând, trebuie reţinut că partea variabilă este plasată în cadrul
înregistrării după partea fixă. O parte variabilă se dezvoltă în cadrul
înregistrării după valorile pe care le ia un câmp situat în cadrul părţii fixe.
Manual de informatică pentru clasa a XI-a 117

În programul care urmează, se exemplifică descrierea unui tip de înregistrare


variabilă, selecţia părţii variabile făcându-se în funcţie de valorile pe care le ia
câmpul studii. Pentru selectare, se foloseşte o clauză specială numită CASE.
Câmpul după care se face selecţia apare descris în această clauză.
Câmpul selector trebuie să fie de tip ordinal cu un număr finit de
elemente. În funcţie de valorile pe care le poate lua câmpul selector, se va dezvolta
partea variabilă. În esenţă, se scriu pe rând valorile posibile ale câmpului selector.
În dreptul fiecărei valori se trece partea pe care trebuie să o conţină înregistrarea în
acest caz. Aceasta se încadrează între paranteze rotunde şi poate fi chiar vidă.
Câmpurile prezente între paranteze se scriu separate prin ';'.

Analizaţi programul următor (care citeşte o singură înregistrare):


type persoana = record
nume: string[30];
varsta: byte;
case studii: char of
'f': ( );
'g': (nr_cl: integer);
'l': (an_t: integer;
oras: string);
's': (fac: record
nume_f: string[20];
an_s: byte
end)
end;
var p: persoana;
begin
write('nume '); readln(p.nume);
write('varsta '); readln(p.varsta);
write('studii '); readln(p.studii);
case p.studii of
'g': begin
write('numar clase '); readln(p.nr_cl);
end;
'l': begin
write('anul terminarii liceului '); readln(p.an_t);
write('orasul '); readln(p.oras);
end;
's': begin
write('numele facultatii '); readln(p.fac.nume_f);
writeln(' ani de studii facultate ');
readln(p.fac.an_s);
end
end {case}
end.

Observaţii

 Pentru fiecare înregistrare de acest tip compilatorul rezervă numărul de


octeţi necesari celei mai lungi variante.
 Este preferabil ca citirea unei variabile de tip înregistrare cu variante să se
facă prin utilizarea instrucţiunii CASE.
118 Capitolul 4. Structuri de date neomogene

4.3. Structuri neomogene în C++

4.3.1. Tipul struct

În practică, apar situaţii în care toate tipurile de date învăţate până în prezent
nu ne sunt de mare folos. Pentru a înţelege aceasta, vom porni de la un exemplu.

Presupunem că dorim să prelucrăm date referitoare la mai mulţi elevi.


Astfel, pentru fiecare elev cunoaştem:
1. Numele - char[20];
2. Prenumele - char[20];;
2. Nota matematică - float;
3. Nota informatică - float;
4. Vârsta - int;

Observaţi faptul că informaţiile referitoare la un elev sunt eterogene: şiruri de


caractere, numere reale sau întregi. Cum am putea rezolva problema prin utilizarea
cunoştinţelor de care dispunem? Ar fi necesari 5 vectori, câte unul pentru fiecare
informaţie. Astfel am avea doi vectori cu elemente de bază şiruri de caractere
pentru nume şi prenume, doi vectori cu elemente de bază de tip float pentru note
şi unul cu elemente de bază de tip int pentru vârstă. Fiecărui elev i, îi corespund
componentele i ale fiecărui vector. O astfel de abordare este greoaie, nenaturală.
Ar fi cu mult mai bine dacă limbajul ar dispune de un mecanism prin care fiecărui
elev să-i corespundă o singură înregistrare.
În C++ există un tip de date, numit struct, care ne permite acest lucru.
Forma generală este (ce este trecut între paranteze drepte este considerat facultativ):
struct [nume structura]
{
[<tip> <nume variabila[, nume variabila, ...]>] ;
[<tip> <nume variabila[, nume variabila, ...]>] ;
...
} [lista de variabile] ;

Pentru exemplul dat, structura este:


struct elev
{
char nume[20], prenume[20];
float nota_mate,nota_info;
int varsta;
};

şi se numeşte elev.
Manual de informatică pentru clasa a XI-a 119

Există două posibilităţi de declarare a variabilelor care alcătuiesc structura.

1. Aşa cum rezultă din forma generală, scriind la sfârşit numele variabilelor:
struct elev
{ char nume[20], prenume[20];
float nota_mate,nota_info;
int varsta;
}inr1,inr2;

Aici, inr1 şi inr2 sunt două variabile de tipul elev.

2. Clasic, declarând variabilele aşa cum suntem obişnuiţi:


elev inr1, inr2;

Definiţia structurii poate figura atât în cadrul funcţiei main() cât şi în faţa ei,
după includerile de fişiere antet. În ce ne priveşte, vom prefera a doua variantă.
Se pune următoarea întrebare: fiind dată o variabilă de un tip struct, care
este modalitatea de acces la câmpurile ei? Pentru aceasta, se foloseşte operatorul
de selecţie directă, notat cu '.', operator cu prioritate maximă.

Fie inr o variabilă de tipul elev. Atunci:

• inr.nume - reprezintă şirul nume al variabilei inr;


• inr.nume[0] – reprezintă primul caracter al şirului nume;
• inr.nota_mate – reprezintă câmpul nota_mate al variabilei inr.

În programul următor, se citeşte şi se tipăreşte o variabilă de tipul elev:


#include <iostream.h>
struct elev
{ char nume[20], prenume[20];
float nota_mate,nota_info;
int varsta;
};
main()
{ elev inr;
cout<<"Nume "; cin>>inr.nume;
cout<<"Prenume "; cin>>inr.prenume;
cout<<"Nota matematica ";
cin>>inr.nota_mate;
cout<<"Nota informatica ";
cin>>inr.nota_info;
cout<<"Varsta ";
cin>>inr.varsta;
cout<<"Am citit:"<< endl
<<inr.nume<<" "<<inr.prenume <<endl
<<inr.nota_mate<<endl
<<inr.nota_info <<endl
<<inr.varsta;
}
120 Capitolul 4. Structuri de date neomogene

Între două variabile de acelaşi tip struct se poate folosi atribuirea. Astfel,
dacă inr1, inr2 sunt două variabile de tip elev, prin atribuirea inr1=inr2,
variabila inr1 ia aceeaşi valoare ca variabila inr2. În C++, o astfel de atribuire se
mai numeşte copiere bit cu bit.

4.3.2. Înregistrări imbricate

Există situaţii când un tip structurat conţine în interiorul său un alt tip
structurat. Priviţi tipul următor:
struct elev
{ char nume[20], prenume[20];
struct
{ int clasa;
float note[20];
}situatie;
int varsta;
};

Structura de bază este elev. În interiorul său se găseşte o altă structură, de


această dată fără nume, dar pentru care există declarată "o variabilă" numită
situatie. În realitate nu este vorba de o variabilă, ci de un nume prin intermediul
căruia poate fi accesat un element al structurii. Am văzut că, în general, elementele
structurii se accesează prin numele variabilei de tipul structurii respective.

Fie inr o înregistrare de tipul elev. În aceste condiţii, accesarea


elementelor situate în interiorul substructurii se face ca mai jos:

• inr.situatie.clasa - se accesează câmpul clasa al


substructurii;
• inr.situatie.note[0] - se accesează prima notă a
vectorului inclus în substructură.

Exemplul următor prezintă o altă posibilitate de declarare a structurilor:


struct elev1
{ char nume[20], prenume[20];
struct
{ int clasa;
float note[20];
}situatie_1, situatie_2;
int varsta;
};

Tipul structurat elev1, subordonează, pe lângă alte tipuri, două structuri


situatie_1 şi situatie_2. Forma este echivalentă cu cea în care cele două
structuri sunt descrise una după alta.

În practică, se foloseşte termenul "imbricate" pentru una sau mai multe


structuri incluse una în alta, ca mai sus.
Manual de informatică pentru clasa a XI-a 121

4.3.3. Înregistrări cu structură variabilă

Pentru început, vom studia un tip aparte de dată structurată, numit union.
Analizaţi programul următor:
#include <iostream.h>
union test
{
int a;
char b[10];
double c;
};
main()
{
test var;int i;
cin>>var.c; cout<<var.c<<endl;
cin>>var.b; cout<<var.b;
}

Variabila var este de tipul union. Ea conţine un întreg, un vector de


caractere şi o variabilă reală. Cele trei câmpuri subordonate ocupă respectiv 2
octeţi, 10 octeţi şi 8 octeţi. În realitate, pentru toate variabilele s-au reţinut 10 octeţi
- adică octeţii necesari pentru a memora câmpul cel mai lung. Aceasta înseamnă
că, la un moment dat, se poate memora doar un singur câmp dintre cele
subordonate. În exemplu, am utilizat la început variabila întreagă, apoi şirul
de caractere.
Iată forma generală a unei uniuni:
union [<numele uniunii>]
{
<tip> <nume variabila> ;
...
} [lista de variabile] ;

Cu excepţia faptului că numai un câmp poate fi ocupat la un moment dat,


toate celelalte reguli sunt identice cu cele de la structuri. Bine, veţi întreba, dar la
ce folosesc "uniunile"? Sunt situaţii în care înregistrările nu au format fix, ci variabil.
Ca să fiu clar, voi da un exemplu.
Să presupunem că ne interesează o situaţie referitoare la studiile unei
persoane. O astfel de înregistrare are o parte fixă dată de câmpurile care reţin
numele, vârsta şi tipul de studii. O persoană poate să nu aibă studii, caz în care nu
mai este necesar să avem alte informaţii, poate să fi făcut câteva clase de şcoală
generală (ne−ar putea interesa câte clase), să fi terminat liceul (caz în care dorim
să ştim anul terminării şi oraşul) sau să aibă studii superioare (şi atunci ne
interesează numele facultăţii şi numărul de ani de studiu în cadrul facultăţii
respective). În concluzie, în funcţie de tipul de studii, înregistrarea arată altfel.
Limbajul permite ca înregistrările să aibă o structură variabilă. Cum se
realizează aceasta?
122 Capitolul 4. Structuri de date neomogene

Uniunile pot fi incluse în structuri. La rândul lor, structurile pot fi incluse în


uniuni. Pentru exemplul nostru, înregistrarea are o parte fixă care este alcătuită din
numele persoanei respective şi o variabilă de tip char, numită studii, care reţine
tipul studiilor pe care le are persoana respectivă. Astfel, dacă persoana respectivă
nu are studii, reţine 'f', dacă are câteva clase de generală, reţine 'g', dacă are
liceul, reţine 'l', iar dacă are studii superioare, 's'.

Apoi urmează partea variabilă care este o uniune. Aceasta conţine o


variabilă nr_clase - pentru cazul în care studii reţine 'g', o structură pentru
cazul în care studii, reţine 'l', ş.a.m.d. În rest, citirea şi afişarea înregistrării se
face, de fiecare dată, sub switch, în funcţie de datele citite sau conţinute. În
concluzie, înregistrarea are structura variabilă, dar ocupă un număr fix de octeţi.
Uniunea poate figura oriunde în interiorul structurii - nu este obligatoriu ca
aceasta să fie scrisă la sfârşitul structurii. Analizaţi programul:
#include <iostream.h>
struct persoana
{ char nume[30], studii;
union
{
int nr_clase;
struct
{int an_t;
char oras[20];
}liceu;
struct
{char nume_f[30];
int nr_ani;
}facultate;
}std;
};

main()
{ persoana pers;
cout<<"Nume persoana ";cin.get(pers.nume,30);
cout<<"Studii f-fara g-generala,l-liceu;s-superioare) ";
cin>>pers.studii;
switch (pers.studii)
{
case 'g': cout<<" numar clase ";
cin>>pers.std.nr_clase;
break;
case 'l': cout<<"anul terminarii liceului ";
cin>>pers.std.liceu.an_t;
cout<<"orasul ";
cin>>pers.std.liceu.oras;
break;
case 's': cout<<"numele facultatii ";cin.get();
cin.get(pers.std.facultate.nume_f,30);
cout<<"nr ani de studiu ";
cin>>pers.std.facultate.nr_ani;
}
Manual de informatică pentru clasa a XI-a 123

//afisez inregistrarea
cout<<pers.nume<<endl;
switch (pers.studii)
{
case 'f': cout<<"n-are saracu' studii "; break;
case 'g': cout<<" numar clase "<<pers.std.nr_clase; break;
case 'l': cout<<"a terminat liceul in "
<<pers.std.liceu.an_t
<<" in orasul "<<pers.std.liceu.oras; break;
case 's': cout<<"numele facultatii "
<<pers.std.facultate.nume_f
<<" nr ani de studiu "
<< pers.std.facultate.nr_ani;
}
}

Probleme propuse
1. Citiţi o variabilă cu următoarea structură:

• nume_elev: 30 caractere;
• data_nasterii:
zi : intreg;
luna : intreg;
an : intreg;
• nota matematica – real;
• nota informatica – real;
• nota engleza – real;
• media – real;

(media se calculează, nu se citeşte).

Testaţi dacă datele au fost introduse corect. Citirea se va verifica prin afişarea
rezultatului.
2. Citiţi n înregistrări de tipul celei de mai sus şi afişaţi-le în ordinea alfabetică a
numelui.

3. Aceeaşi problemă ca cea anterioară, numai că afişarea se va face în ordinea


descrescătoare a mediilor.

4. Presupunând că înregistrările se referă la un examen de admitere dat, să se


afişeze în ordine descrescătoare a mediilor, în limita unui număr de locuri sau până
când se epuizează toate înregistrările elevilor cu medii mai mari sau egale cu 5. În
cazul în care pe ultimul loc avem mai mulţi elevi cu aceeaşi medie, toţi aceştia sunt
consideraţi admişi. Programul va afişa numărul de locuri în plus.
124

Capitolul 5
Structuri de date

5.1. Conceptul de structură de date

Orice algoritm primeşte date de intrare, le prelucrează şi obţine date de


ieşire. În fiecare caz, datele de intrare, datele intermediare - cele create în timpul
prelucrării - şi datele de ieşire sunt structurate (organizate) într-un anumit fel care
corespunde intrării, necesităţilor de prelucrare sau a celor de utilizare ulterioară.

Pentru a veni în sprijinul programatorilor, limbajele de programare evoluate


(de exemplu, C++ sau Pascal) pun la dispoziţia acestora posibilitatea organizării
datelor în anumite "şabloane", numite tipuri de date. Mai precis, prin tip de date
se înţelege:

 o mulţime de valori;
o regulă de codificare a acestora;
1

 o mulţime de operaţii definite pe mulţimea datelor.

La rândul lor, tipurile de date pot fi:

 simple - descriu date care aparţin unor mulţimi care nu sunt


rezultate ca produs cartezian al altor mulţimi. Exemplu: int.

 structurate - descriu date care aparţin unor mulţimi rezultate ca


produs cartezian al altor mulţimi.

1. Priviţi declaraţia de mai jos:

Varianta Pascal Varianta C++


type rational=record struct rational
p,q:integer; { int p,q;
end };

Prin tipul de mai sus se descrie structura unei variabile capabilă să reţină
numere raţionale. Fie A1 mulţimea valorilor care pot fi memorate prin utilizarea
tipului întreg. Fie A2=A1-{0}. Atunci, o variabilă de tip rational poate memora
valori care aparţin mulţimii A1×A2. Evident, este sarcina programatorului ca valoarea
reţinută de variabila corespunzătoare lui q să fie diferită de 0.

În unele lucrări veţi întâlni definiţii ale tipului de date care exclud regula de codificare. Aici
1

se porneşte de la ideea că nu întotdeauna este necesar, pentru a lucra cu tipul respectiv,


să cunoaştem regula de codificare.
Manual de informatică pentru clasa a XI-a 125

2. Mai jos, este prezentat un alt exemplu:

Varianta Pascal Varianta C++


type vector=array [1..100] of real; double a[100];
var a:vector;

Fie B mulţimea valorilor care pot fi reţinute de tipul real. Atunci, variabila a
poate reţine la un moment dat un element al mulţimii:

B ×
 B × ...
 B .
×
de n ori

Practica impune utilizarea unor structuri ale datelor de o mare varietate, care
nu se suprapun întotdeauna peste tipurile care pot fi descrise prin limbaj.

Definiţia 5.1. Prin structură de date vom înţelege un ansamblu de date


caracterizat prin relaţiile existente între ele şi prin operaţiile care pot fi
efectuate cu datele respective.

Vom numi nod, o variabilă de un tip oarecare. De obicei, acest tip este
structurat. După caz, termenul nod poate fi înlocuit cu articol, înregistrare sau
entitate.

În cele mai multe cazuri, "ansamblul de date" care formează structura este
alcătuit dintr-o mulţime cu un număr variabil de noduri.

Relaţiile existente între noduri şi operaţiile care pot fi efectuate cu ele vor fi
prezentate prin exemple.

De reţinut!

 Tipul variabilei care alcătuieşte nodul nu caracterizează structura de date.

 Structura de date este un concept abstract. Mai precis, conceptul în sine nu


precizează locul unde structura respectivă va fi memorată (clasa de
memorare) şi nici detaliile de implementare (cele care ţin de limbajul folosit).

 În facultăţile de profil (calculatoare, informatică) se studiază disciplina numită


"Structuri de date". Ea este una dintre disciplinele care se includ în cadrul
mai larg al informaticii, tot aşa cum, de exemplu, algebra se studiază ca
disciplină aparte a matematicii.

 În practică s-au impus anumite structuri. Acest lucru este datorat faptului că
există mulţi algoritmi care le utilizează. Ele vor fi tratate, pe scurt, în
paragrafele următoare.
126 Capitolul 5. Structuri de date

5.2. Structura de tip listă liniară

5.2.1. Prezentarea structurii

Definiţia 5.2. 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.

Faptul că structura este liniară înseamnă că fiecare nod, cu excepţia


ultimului, are un singur nod succesor (care îi urmează în listă) şi, cu excepţia
primului nod, are un singur predecesor (care se află imediat înaintea lui
în listă).

Dacă vorbim la modul general de o structură de date, nu ne interesează,


pentru moment, modul în care aceasta va fi implementată (adică unde este
memorată şi cum se efectuează operaţiile permise asupra ei). Pur şi simplu, ne
imaginăm lista ca mai jos, unde fiecare nod i memorează informaţia infi:

inf 1 inf 2 inf n


nod1 nod2 nodn

Exemple de aplicaţii care utilizează liste liniare:

a) Evidenţa situaţiei şcolare a elevilor unei clase. Fie n numărul elevilor. Aici, un
nod reţine numele unui elev şi notele la diversele materii. Vom avea deci, n noduri.
Nodurile vor fi memorate în ordinea alfabetică a numelor elevilor. În clasa
respectivă pot fi transferaţi elevi din alte clase, caz în care se adaugă noduri. Din
clasă, unii elevi pot pleca în alte clase, caz în care se şterg noduri.

b) Se doreşte să se reţină un şir de numere naturale, în ordinea în care au fost


citite de la tastatură. Aici, un nod reţine un număr natural.

Nu toate aplicaţiile utilizează liste liniare. Exemple de structuri care nu sunt


liste liniare:
• se dau n oraşe şi şoselele care unesc unele dintre aceste oraşe;
• arborele genealogic al unei persoane.
Manual de informatică pentru clasa a XI-a 127

5.2.2. Liste alocate secvenţial

Din acest moment ne punem problema să vedem modul în care se poate


implementa o listă liniară.

O primă formă de alocare este cea secvenţială. În cazul acestei alocări,


nodurile listei ocupă poziţii succesive în memorie. Acest tip de alocare se întâlneşte
des, de câte ori utilizăm vectorii. Altfel spus, primul nod al listei va fi reţinut de
primul element al vectorului, al doilea nod al listei de al doilea element al vectorului,
ş.a.m.d.

În continuare, urmărim modul în care putem efectua operaţiile permise cu o


listă liniară.

a) Accesul la oricare nod al listei se poate face cu mare uşurinţă. Dacă dorim să
adresăm nodul k, atunci scriem V[k] (presupunând că vectorul care reţine nodul
se numeşte V).

b) Ştergerea unui nod, indiferent de poziţia pe care o ocupă în listă

Fie lista alocată secvenţial:

7 3 1 2 8 9 5 8 3 2 6

Eliminăm al doilea nod - conţinut 3 (ştergem conţinutul nodului 2):

7 1 2 8 9 5 8 3 2 6

Pentru a păstra structura şi pentru a nu pierde informaţii, este obligatoriu ca


informaţiile reţinute de nodurile care urmează nodului şters să fie deplasate către
stânga:

7 1 2 8 9 5 8 3 2 6

Practic, de această dată, nodul 2 (elementul de indice 2 al vectorului) va


reţine informaţia celui de-al treilea nod; nodul 3, va reţine informaţia celui
de-al patrulea nod; ş.a.m.d. În multe aplicaţii, această modificare nu prezintă
importanţă.

c) Adăugarea unui nod, indiferent de poziţia pe care o ocupă în listă

Fie lista alocată secvenţial:

7 3 1 2 8 9 5 8 3 2 6
128 Capitolul 5. Structuri de date

Nodul 2 va conţine 5 (practic, adăugăm un nod). Începând cu nodul 2,


deplasăm toate informaţiile asociate nodurilor către dreapta:

7 3 1 2 8 9 5 8 3 2 6

Acum se completează valoarea reţinută de nodul 2:

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

Observaţii

 Şi de această dată, nodurile vor reţine informaţii diferite. De exemplu, nodul


3 va reţine ce anterior reţinea nodul 2, ş.a.m.d.

 În concluzie, la alocarea secvenţială, accesul la nod este foarte rapid, dar


adăugarea sau ştergerea unui nod se fac cu efort de calcul, pentru că
necesită deplasări ale conţinuturilor nodurilor.

5.2.3. Liste alocate înlănţuit

Există două feluri de alocare înlănţuită: alocare simplu înlănţuită şi alocare


dublu înlănţuită. În acest paragraf prezentăm principiile alocării înlănţuite, urmând ca
în paragraful următor să arătăm modul în care implementăm listele alocate înlănţuit.

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

in1 adr2 in2 adr3 inn nil


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ă.

 nil - 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.

2. Alocarea dublu înlănţuită. Alocarea simplu înlănţuită permite parcurgerea listei


într-un singur sens (de la stânga la dreapta). În cazul în care se doreşte ca lista să
poată fi parcursă în ambele sensuri, se utilizează alocarea dublu înlănţuită. Aici,
Manual de informatică pentru clasa a XI-a 129

fiecare nod reţine adresele predecesorului şi succesorului său, aşa cum se vede în
figura următoare:

nil in1 adr2 adr1 in2 adr3 adrn-1 inn nil

adr1 adr2 adrn

5.2.4. Implementarea alocării înlănţuite prin utilizarea


vectorilor

Aşa cum am învăţat, lista liniară este alcătuită din mai multe noduri între care
există o relaţie de ordine. În cazul alocării înlănţuite, informaţia memorată de
fiecare nod va cuprinde şi un câmp de adresă -în cazul alocării simplu înlănţuită-
sau două câmpuri de adresă -în cazul alocării dublu înlănţuită. În acest paragraf
vom studia implementarea alocării simplu înlănţuită.

 Iată cum se descrie informaţia dintr-un nod, în cazul listelor alocate simplu
înlănţuit, atunci când acesta reţine un număr întreg.

info adresa nodului următor

Varianta Pascal Varianta C++


type Adresa=Integer; typedef int adresa;
nod=record struct nod
info:integer; { int info;
adr_urm:Adresa; adresa adr_urm;
end; };

 Pentru memorarea listei folosim un vector care are componentele de tip Nod,
descris mai jos:

Varianta Pascal Varianta C++


Lista=array[1..1000] of Nod; nod L[1000];
var L:lista;

Din descriere rezultă că lista poate avea cel mult 1000 de noduri. Acesta
este spaţiul disponibil.

Priviţi exemplul de mai jos:

L 7 3 5 4 1 5 4 0

1 2 3 4 5 6
130 Capitolul 5. Structuri de date

Dacă facem abstracţie de implementare, lista este:

7 3 5 4 1 5 4 0

1 3 4 5

Ce observăm?

a) Fiecare nod trebuie să reţină şi adresa nodului următor. Adresa este, de fapt,
indicele componentei din vector care reţine informaţia asociată nodului următor.
Prin urmare, necesarul de memorie este mai mare.

b) Nodurile nu ocupă adrese succesive în memorie. De exemplu, deşi primul nod


al listei este reţinut de prima componentă a vectorului (de indice 1), al doilea nod al
listei este reţinut de componenta de indice 3 a vectorului. Din acest motiv, vom face
distincţie între numărul unui nod (acesta este în cadrul listei) şi indicele vectorului
unde este memorat. În exemplul de mai sus, nodul 2 reţine 5, iar el este memorat
de componenta de indice 3 a vectorului.

Pentru realizarea practică a implementării apar o serie de probleme. Acestea


vor fi rezolvate în cele ce urmează.

 Problema 1. Gestiunea memoriei. Conceptul de listă nu precizează numărul


de noduri pe care ea le poate avea. În practică, numărul de noduri este limitat.
Aceasta înseamnă că, la un moment dat, numărul de noduri poate fi depăşit. Prin
urmare, programul care lucrează cu o astfel de listă trebuie să gestioneze
spaţiul disponibil.

În aceste condiţii, vectorul care reţine nodurile, va fi dublat de un altul, ale


cărui componente reţin 1 sau 0, după cum componenta de acelaşi indice a
vectorului L reţine sau nu un nod. Eventual, se poate utiliza o variabilă care
memorează numărul de componente ale vectorului care reţine nodurile.

L 3 5 4 1 5 4

1 2 3 4 5 6

ocupat 1 0 1 1 1 0

1 2 3 4 5 6

 Problema 2. Accesul la un nod al listei. Spre deosebire de alocarea


secvenţială, unde accesul este imediat, la alocarea înlănţuită accesul se face
începând cu primul nod al listei. Dacă nu acesta este nodul căutat, se trece la nodul
următor (orice nod conţine adresa nodului următor, ş.a.m.d.).

 Problema 3. Adăugarea unui nod. Să presupunem că în lista de mai jos


dorim să adăugăm, după al treilea nod, un nod cu informaţia 9.
Manual de informatică pentru clasa a XI-a 131

L 7 3 5 4 1 5 4 0

1 2 3 4 5 6

ocupat 1 0 1 1 1 0

1 2 3 4 5 6

Prin testarea vectorului ocupat, se observă că primul element liber al


vectorului L este cel de indice 2.

a) Marcăm nodul ca ocupat (ocupat[2] va reţine 1).

b) Memorăm informaţia: 9.

L 7 3 9 5 4 1 5 4 0

1 2 3 4 5 6

ocupat 1 1 1 1 1 0

1 2 3 4 5 6

c) Noul nod va reţine ca adresă următoare, adresa următoare reţinută de al


treilea nod (pentru că introducerea noului nod se face după acesta):

L 7 3 9 5 5 4 1 5 4 0

1 2 3 4 5 6

ocupat 1 1 1 1 1 0

1 2 3 4 5 6

d) Al treilea nod va reţine ca adresă următoare, adresa nodului nou introdus,


pentru că acesta s-a introdus după el:

L 7 3 9 5 5 4 1 2 4 0

1 2 3 4 5 6

ocupat 1 1 1 1 1 0

1 2 3 4 5 6

După această modificare, lista va fi:

7 3 5 4 1 5 9 5 4 0

1 3 4 5 5
132 Capitolul 5. Structuri de date

 Problema 4. Ştergerea unui nod. Să presupunem că în lista de mai jos dorim


să ştergem al doilea nod.

L 7 3 5 4 1 5 4 0

1 2 3 4 5 6

ocupat 1 0 1 1 1 0

1 2 3 4 5 6

Al doilea nod se găseşte în elementul de indice 3. Prin urmare, ocupat[3]


va reţine 0. În continuare, primul nod va reţine ca adresă următoare adresa nodului
3 (pentru că acesta urmează nodului 2). Această adresă se ia din câmpul de
adresă următoare a nodului care urmează să fie şters.

L 7 4 5 4 1 5 4 0

1 2 3 4 5 6

ocupat 1 0 0 1 1 0

1 2 3 4 5 6

Observaţi faptul că, deşi nu am şters informaţia asociată nodului, acesta


devine inaccesibil prin parcurgerea listei începând cu primul nod.

Lista va deveni:

7 4 1 5 4 0

1 4 5

 Dezavantajele alocării înlănţuite sunt:

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ă, prezente în cadrul fiecărui nod ocupă memorie.

 Avantajele alocării înlănţuite sunt date de faptul că operaţiile de adăugare


sau eliminare a unui nod se fac rapid.

Exemplele sunt date pentru lista liniară simplu înlănţuită, dar bine înţelese,
ne permit să deducem singuri modul de efectuare a operaţiilor respective pentru
liste dublu înlănţuite.
Manual de informatică pentru clasa a XI-a 133

5.3. Structura de tip stivă

Definiţia 5.3. 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".

Pentru a înţelege modul de lucru cu stiva, ne imaginăm un număr n de


farfurii identice, aşezate una peste alta (o "stivă" de farfurii). Adăugarea sau
scoaterea unei farfurii se face, cu uşurinţă, numai în vârful stivei. După ce am scos
toate farfuriile din stivă, spunem că aceasta este vidă. Oricât ar părea de simplu
principiul stivei, el are consecinţe uriaşe în programare.

 Stivele se pot aloca secvenţial (ca vectorii). Fie ST[i] un vector. ST[1],
ST[2], ..., ST[n] pot reţine numai litere sau numai cifre. O variabilă k indică
în permanenţă vârful stivei, adică ultimul element introdus.

Exemplificăm, în continuare, modul de lucru cu stiva:

A În stiva iniţial vidă se introduce litera A, vârful stivei va fi la nivelul 1 (k=1).

B
Introducem în stivă litera B, deci k va lua valoarea 2.
A

Scoatem din stivă pe B (A nu poate fi scos deocamdată); k=1.


A

Scoatem din stivă pe A; stiva rămâne vidă k=0.

Observaţii

 În mod practic, la scoaterea unei variabile din stivă, valoarea variabilei ce


indică vârful stivei scade cu 1, iar atunci când scriem ceva în stivă, o
eventuală valoare reziduală se pierde.
134 Capitolul 5. Structuri de date

 Pe un anumit nivel se reţine, de regulă, o singură informaţie (literă sau cifră),


însă este posibil să avem mai multe informaţii.

 În cazul stivei, alocarea secvenţială nu prezintă mari dezavantaje, ca în cazul


mai general al listelor, pentru că nu se fac operaţii de inserare sau ştergere
în interiorul stivei, ci numai în vârful ei.
Singurul dezavantaj, în comparaţie cu alocarea dinamică înlănţuită este dat
de faptul că numărul de noduri care pot fi memorate la un moment dat este
mai mic - depinde de gradul de ocupare al segmentului de date.

 În literatura de specialitate veţi întâlni termenul PUSH pentru operaţia de


adăugare în stivă a unei înregistrări şi POP, pentru extragere.

 Este posibil să vă întrebaţi: de ce nu putem accesa un element al stivei, chiar


dacă nu este ultimul introdus? Nimeni nu ne opreşte, doar că, în acest caz,
nu respectăm principiul stivei.

Exemple

1. Funcţia Manna-Pnueli. Se citeşte x∈Z. Se cere programul pentru calculul


funcţiei:

x − 1, x ≥ 12
F(x) = 
F(F(x + 2)),x < 12

Vom începe prin a studia modul de calcul al funcţiei pentru x=15 şi x=8:

f(15)=14;
f(8)=f(f(10))=f(f(f(12)))=f(f(11))=f(f(f(13)))=f(f(12))=f(11)
=f(f(13))=f(12)=11.

Algoritmul va folosi o stivă ST şi o variabilă numită k, ce indică în


permanenţă vârful stivei.

Algoritmul se bazează pe următoarele considerente:

 la o nouă autoapelare a funcţiei f, se urcă în stivă (k se incrementează


cu 1) şi se pune noua valoare;

 în situaţia în care, pentru valoarea aflată pe nivelul k, se poate calcula


funcţia, se coboară în stivă, punându-se pe acest nivel noua valoare;

 algoritmul se încheie când se ajunge în stivă la nivelul 0.


Manual de informatică pentru clasa a XI-a 135

Pentru exemplul dat, prezentăm schematic funcţionarea sa:

12 13

10 10 11 11

8 8 8 8 8

12 13

8 11 11 12
f=11

Programul este prezentat în continuare:

Varianta Pascal Varianta C++


var st:array [1..100] of #include <iostream.h>
integer; int st[100],n,k;
n,k:integer; main()
begin { cout<<"n=";
write('n='); readln(n); cin>>n;
k:=1; st[1]:=n; k=1;
while k>0 do st[1]=n;
if st[k]<12 then begin while (k>0)
k:=k+1; if (st[k]<12)
st[k]:=st[k-1]+2 { k++;
end st[k]=st[k-1]+2;
else begin }
k:=k-1; else
if k>0 { k--;
then st[k]:=st[k+1]-1 if (k>0) st[k]=st[k+1]-1;
end; }
writeln('f=',st[1]-1) cout<<"n="<<st[1]-1;
end. }

Se poate demonstra uşor că pentru valori mai mici decât 12, funcţia ia
valoarea 11. Observaţia simplifică mult programul, dar exemplul a fost dat în
alt scop.

2. Funcţia lui Ackermann. Se dă funcţia de mai jos, definită pe produsul cartezian


N×N. Se citesc m şi n. Să se calculeze Ack(m,n).

n + 1, m=0

Ack(m, n) = Ack(m − 1,1), n=0
Ack(m − 1, Ack(m, n − 1)), altfel

136 Capitolul 5. Structuri de date

Pentru a elabora algoritmul, studiem un exemplu numeric:


ack(2,1)=ack(1,ack(2,0))=ack(1,ack(1,1))=ack(1,ack(0,ack(1,0)))=
= ack(1,ack(0,ack(0,1)))=ack(1,ack(0,2))=ack(1,3)=ack(0,ack(1,2)) =
=ack(0,ack(0,ack(1,1)))=ack(0,ack(0,ack(0,ack(1,0))))=
=ack(0,ack(0,ack(0,ack(0,1))))=ack(0,ack(0,ack(0,2)))=
ack(0,,ack(0,3))=ack(0,4)=5.

Pentru calculul acestei funcţii, folosim o stivă dublă, ST. Iniţial, valorile m şi n
se reţin la nivelul 1. Pe nivelul k al stivei se reţin valorile curente m şi n. În funcţie de
valorile acestora se procedează astfel:

 pentru m şi n diferite de 0, este necesar un nou calcul de funcţie, caz în care


se urcă în stivă şi pe noul nivel se pun argumentele m şi n-1;

 pentru cazul n=0, se rămâne pe acelaşi nivel în stivă, punând în locul lui m
valoarea m-1, iar în locul lui n, valoarea 1;

 în situaţia în care m=0, funcţia se poate calcula; se coboară în stivă şi se


înlocuieşte valoarea lui m cu m-1, valoarea lui n cu valoarea calculată
anterior.

În continuare, prezentăm grafic modul de funcţionare a algoritmului pentru


exemplul ack(2,1):

1 0 0 1
2 0 1 1 1 1 1 1

2 1 2 1 2 1 2 1 2 1

1 0

1 1 1 1
0 2 1 3 1 2 1 2

2 1 1 3 1 2 1 3 1 3

0 1

1 1 0 2
1 2 1 2 0 3

1 3 1 3 1 3 0 4 ack(2,1)=5.
Manual de informatică pentru clasa a XI-a 137

Varianta Pascal Varianta C++


type stiva=array [1..10000,1..2] int st[10000][2];
of integer;
main()
var st:stiva; { int m,n,k;
m,n,k:integer; cout<<"m="; cin>>m;
cout<<"n="; cin>>n;
begin k=1;
write('m='); st[k][0]=m;
readln(m); st[k][1]=n;
write('n='); while (k>0)
readln(n); if (st[k][0] && st[k][1])
k:=1; { k++;
st[k,1]:=m; st[k][0]=st[k-1][0];
st[k,2]:=n; st[k][1]=st[k-1][1]-1;
while k>0 do }
if (st[k,1]<>0) and else
(st[k,2]<>0) if (!st[k][1])
then { st[k][0]=st[k][0]-1;
begin st[k][1]=1;
k:=k+1; }
st[k,1]:=st[k-1,1]; else
st[k,2]:=st[k-1,2]-1 { k--;
end if (k>0)
else { st[k][0]=st[k][0]-1;
if st[k,2]=0 t[k][1]=st[k+1][1]+1;
then }
begin }
st[k,1]:=st[k,1]-1; cout<<"ac("<<m<<','<<
st[k,2]:=1 n<<")="<<st[1][1]+1;
end }
else
begin
k:=k-1;
if k>0
then
begin
st[k,1]:=st[k,1]-1;
st[k,2]:=st[k+1,2]+1
end
end;
writeln('ac(',m,',',n,')=',
st[1,2]+1)
end.

Funcţia lui Ackermann ia valori extrem de mari pentru valori mici ale lui m şi
n. De exemplu, nu veţi reuşi să calculaţi Ack(4,4). Încercaţi...
138 Capitolul 5. Structuri de date

5.4. Structura de tip coadă

Definiţia 5.4. O coadă este o listă pentru care toate inserările sunt făcute
la unul din capete, 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".

Este cu totul nerecomandabilă alocarea secvenţială a cozii, deoarece în


această situaţie, are loc un fenomen de migraţie a datelor către ultimele
componente ale vectorului (cele de indice mare).

Să presupunem că simulăm o coadă cu ajutorul unui vector cu zece


componente, care reţin numere întregi. Introducem în coadă, pe rând,
numerele 1, 2, 3, 4.

1 2 3 4

Dacă scoatem din coadă pe 1 şi introducem în coadă pe 5, coada va arăta


în felul următor:

2 3 4 5

Scoatem din coadă pe 2 şi introducem pe 6:

3 4 5 6

Se observă acest fenomen de "migraţie".

Probleme propuse
1. Care dintre structurile de mai jos nu este liniară?

a) b) c) d)

2. Un vector reţine pe poziţiile de la 1 la k nodurile unei liste liniare. Fiecare element


al vectorului (nod) reţine un număr natural. Se cere să se scrie un subprogram care
inserează în listă, pe poziţia p, un număr natural citit de la tastatură.
Manual de informatică pentru clasa a XI-a 139

Exemplu: k=3; V=(1,2,3); p=2. Numărul citit este 5. După rulare trebuie să
avem:
k=4; V=(1,5,2,3).

3. Un vector reţine pe poziţiile de la 1 la k nodurile unei liste liniare. Fiecare


element al vectorului (nod) reţine un număr natural. Se cere să se scrie un
subprogram care şterge din listă nodul aflat pe poziţia p, 1≤p≤k.

Exemplu: k=3; V=(1,2,3); p=2. După rulare trebuie să avem: k=2; V=(1,3).

4. Pentru problema anterioară, care dintre afirmaţiile de mai jos este falsă?

a) Pentru a şterge un nod aflat pe poziţia p sunt necesare k-p deplasări spre
stânga ale conţinuturilor celorlalte noduri.
b) Subprogramul va avea parametrul k transmis prin valoare.
c) Subprogramul va avea parametrul k transmis prin referinţă.
d) Dacă k=p, nu se efectuează deplasări către stânga.

5. Lucrare în colectiv. Implementarea listelor alocate simplu înlănţuit. Scrieţi


un set de subprograme care creează şi gestionează o listă liniară simplu înlănţuită,
alocată secvenţial (prin utilizarea unui vector).

6. Lucrare în colectiv. Implementarea listelor alocate simplu înlănţuit. Scrieţi


un set de subprograme care creează şi gestionează o listă liniară simplu înlănţuită,
alocată înlănţuit.

7. Sortaţi n numere naturale utilizând algoritmul de sortare prin inserţie. Programul


va utiliza o listă liniară alocată înlănţuit. Care sunt avantajele utilizării listei alocate
înlănţuit în cazul algoritmului de sortare prin inserţie?

8. Creaţi o listă liniară cu n noduri alocată secvenţial. Nodul 1 va conţine numărul 1,


nodul 2 numărul 2, ş.a.m.d. Se generează aleator un număr natural k, mai mare ca
1 şi mai mic decât n. Nodurile de la k la n vor fi, în această ordine, primele în listă,
urmate de nodurile de la 1 la k-1, în această ordine. Afişaţi lista după p astfel de
inversări. Observaţie: aceasta este o modalitate de a genera aleator o permutare a
primelor n numere naturale.

9. În cazul în care, pentru o listă liniară alocată î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.
140 Capitolul 5. Structuri de date

10. În jurul arbitrului sunt aşezaţi N jucători numerotaţi în sens orar. Arbitrul,
începând de la un jucător K numără până la M. Persoana la care s-a oprit
numărătoarea este eliminată din cerc. Arbitrul repetă procedeul începând cu
persoana următoare celei eliminate. Procedeul se repetă până când rămâne un
jucător L. Să se scrie un program care:
 citeşte M, N, K şi-l determină pe L;
 citeşte M, N, L şi-l determină pe K.

11. Urmăriţi secvenţa următoare, care se referă la o stivă, unde Push a este
operaţia prin care se pune în stivă valoarea a, iar Pop este operaţia prin care se
extrage din stivă.
Push 1; Push 2; Pop; Push 3; Push 4; Pop; Pop
Care din afirmaţiile de mai jos nu este adevărată după executarea secvenţei
de mai sus?
a) Din stivă au fost extrase, în această ordine, valorile: 2, 4, 3;
b) stiva este vidă; c) stiva conţine valoarea 1; d) stiva are un singur nivel.

12. Scrieţi subprogramele care implementează o stivă.


B A
13. Se citesc n valori numere naturale. Se cere ca, prin 4
utilizarea unei stive, vedeţi problema anterioară, valorile citite 2 3
1
să se afişeze în ordine inversă.
14. În figura alăturată avem 4 vagoane, numerotate cu
1,2,3,4. Se presupune că pe linia C încap toate cele 4
vagoane şi că un vagon aflat pe linia C poate fi mutat numai
pe linia B. Se cere ca, prin mutări succesive de vagoane, să
avem pe linia B vagoanele într-o anumită ordine, în care C
primul vagon este cel aflat la ieşirea de pe linia B. Care Figura 5.1.
dintre şirurile de vagoane de mai jos nu poate fi obţinut?
a) 1 2 3 4; b) 4 3 2 1; c) 3 4 2 1; d) 3 4 1 2.

15. Să se scrie un program care, pentru problema de mai sus, citeşte ca date de
intrare şirul vagoanelor aflate pe linia A şi şirul vagoanelor care trebuie obţinut pe
linia B. Se cere să se afişeze şirul mutărilor de tip Push nr_vagon şi Pop
nr_vagon (aţi recunoscut, desigur, o structură de tip stivă) prin care se poate
obţine şirul vagoanelor de pe lina B. Dacă acest şir nu se poate obţine, în momentul
în care se ajunge în situaţia unei mutări imposibile, să se afişeze mesajul
‘Imposibil’.

16. Scrieţi un set de subprograme care gestionează o coadă. Coada va fi


implementată prin utilizarea alocării înlănţuite.

Răspunsuri

1. d) 4. b) 11. b) 14. d)
141

Capitolul 6

Introducere în recursivitate

6.1. Prezentare generală

Recursivitatea este una din noţiunile fundamentale ale informaticii.


Utilizarea frecventă a recursivităţii s-a făcut după anii 80. Multe din limbajele de
programare evoluate şi mult utilizate - Fortran, Cobol - nu permiteau scrierea
programelor recursive.

Definiţia 6.1. Recursivitatea este un mecanism general de elaborare a


programelor. Ea constă în posibilitatea ca un subprogram să se
autoapeleze.

Recursivitatea a apărut din necesităţi practice date de transcrierea directă a


formulelor matematice recursive. În timp, acest mecanism a fost extins, fiind utilizat
în elaborarea multor algoritmi.

6.2. Modul în care se realizează autoapelul


În acest paragraf vom învăţa modul în care subprogramele se autoapelează.
Mecanismul recursivităţii şi modul cum se gândeşte un algoritm recursiv vor fi
prezentate în paragrafele următoare.

6.2.1. Realizarea autoapelului în limbajul Pascal

După cum ştim, în limbajul Pascal subprogramele sunt de două feluri:


proceduri şi funcţii. Oricare ar fi tipul subprogramului, acesta se poate autoapela,
însă modul în care se realizează autotransferul diferă.

 În cazul procedurilor, autoapelul se realizează prin apelul procedurii


respective, din interiorul ei. Apelul se face la fel ca în cazul în care procedura
este apelată din exterior.

Procedura prezentată în continuare este recursivă şi afişează, pe rânduri


separate, numerele 7, 6, ..., 1:
142 Capitolul 6. Introducere în recursivitate

procedure exemplu(n:integer);
begin
if n<>0 then
begin
writeln(n);
exemplu(n-1);
end
end;
begin
exemplu(7);
end.

 În cazul funcţiilor, autoapelul se realizează printr-o operaţie de atribuire,


operaţie prin care numele funcţiei trebuie să figureze în partea dreaptă a
operatorului de atribuire.

Funcţia următoare calculează suma 1+2+...+7:

function suma(n:integer):integer;
begin
suma:=0;
if n<>0 then
suma:=n+suma(n-1);
end;
begin
writeln(suma(7));
end.

6.2.2. Realizarea autoapelului în limbajul C++

După cum ştim, în C++ funcţiile pot fi de tipul void sau de un alt tip. În acest
din urmă caz, funcţiile returnează o anumită valoare. Oricare ar fi tipul funcţiei,
aceasta se poate autoapela, însă modul în care se realizează autotransferul, diferă.

 În cazul funcţiilor de tip void, autoapelul se realizează prin apelul funcţiei


respective, din interiorul ei. Apelul se face la fel ca în cazul în care funcţia
este apelată din exterior.

Funcţia de mai jos este recursivă. Ea afişează, pe rânduri separate,


numerele 7, 6, ..., 1:
#include <iostream.h>
void exemplu(int n)
{ if (n!=0)
{ cout<<n<<endl;
exemplu (n-1);
}
}
main()
{ exemplu(7);
}
Manual de informatică pentru clasa a XI-a 143

 În cazul funcţiilor care nu sunt de tipul void, autoapelul se realizează prin


instrucţiunea return. Ea este de forma return expresie, dar în
expresia respectivă trebuie să intre şi funcţia care se autoapelează.

Funcţia următoare calculează suma 1+2+...+7:

#include <iostream.h>
int suma (int n)
{ if (n!=0) return n+suma(n-1);
}
main()
{ cout<<suma(7);
}

6.3. Mecanismul recursivităţii

 Problemă. Să se calculeze recursiv n!.


A) Pentru a scrie o funcţie recursivă care efectuează acelaşi calcul, vom porni de
la o definiţie recursivă a lui n!. Aceasta este:

1, n=0
n! = fact(n) =  cu n ∈ N
n ⋅ fact(n − 1), altfel

De exemplu, pentru a calcula 3!, procedăm astfel:

3!=fact(3)=3×fact(2)=3×2×fact(1)=3×2×1×fact(0)=3×2×1×1=6.

Funcţia recursivă fact nu face altceva decât să transcrie definiţia recursivă


prezentată anterior:

Varianta Pascal Varianta C++


var n:integer; #include <iostream.h>
int fact(int n)
function fact(n:integer): { if (!n) return 1;
integer; else return n*fact(n-1);
begin }
if n=0 then fact:=1
else fact:=n*fact(n-1) main()
end; { int n;
cout<<"n=";
begin cin>>n;
write('n='); cout<<fact(n);
readln(n); }
writeln(fact(n))
end.
144 Capitolul 6. Introducere în recursivitate

Care este mecanismul prin care subprogramele se pot autoapela? Să ne


amintim modul în care subprogramele memorează parametrii transmişi.

 Pentru memorarea parametrilor, subprogramele folosesc o zonă de


memorie numită stivă (mai exact, această zonă se numeşte segment
de stivă).
 Memorarea parametrilor transmişi se face în ordinea în care aceştia
figurează în antet: de la stânga la dreapta.
 Pentru parametrii transmişi prin valoare, se memorează valoarea
transmisă, iar pentru cei transmişi prin referinţă se memorează adresa
variabilei.
 În cadrul subprogramului, parametrii transmişi şi memoraţi în stivă
sunt variabile. Numele lor este cel din lista parametrilor formali.

În capitolul anterior am studiat proprietăţile structurii numită stivă. Exact


aceleaşi proprietăţi le are şi segmentul de stivă. Singura diferenţă este dată de
faptul că gestiunea segmentului de stivă este făcută automat. Mai exact, codul în
limbaj maşină, obţinut în urma compilării, conţine secvenţe prin care se
gestionează segmentul de stivă.

 La apelul subprogramului se depun în stivă, în ordine, parametrii transmişi.


De asemenea, tot în stivă se rezervă spaţiu pentru variabilele locale (cele
declarate în subprogram). Acesta este un prim nivel al stivei.

 În cazul în care subprogramul se autoapelează pe un al doilea nivel, se


depun din nou parametrii transmişi şi se rezervă un nou spaţiu pentru
variabilele locale.

În continuare, prezentăm grafic modul în care funcţionează recursivitatea în


cazul funcţiei fact a programului anterior. Această funcţie nu are variabile locale,
deci în stivă se depune doar parametrul n.

La primul apel al funcţiei fact se creează în


n 3 segmentul de stivă o variabilă numită n, care
reţine 3 - valoarea parametrului formal.

Funcţia se autoapelează. De această dată


n 2 parametrul n ia valoarea 2. În stivă se creează un
nou nivel, care reţine n cu valoarea 2.
n 3

n 1 Funcţia se autoapelează. Parametrul n ia valoarea


1. În stivă se creează un nou nivel, care reţine n
n 2 cu valoarea 1.
n 3
Manual de informatică pentru clasa a XI-a 145

n 0 Funcţia se autoapelează. Parametrul n ia valoarea


fact=1
0. În stivă se creează un nou nivel, care reţine n
n 1
cu valoarea 0. Funcţia ia valoarea 1, autoapelul
n 2 nu mai are loc. Se revine pe nivelul 3.

n 3

n 1 Pe nivelul 3 se efectuează calculul 1*1=1.


fact=1
n Se revine apoi pe nivelul 2.
2

n 3

n 2 Pe nivelul 2 se efectuează calculul 2*1=2.


fact=2
Se revine apoi pe nivelul 1.
n 3

Pe nivelul 1 se efectuează calculul 3*2=6.


n 3
Se revine apoi în main().
fact=6

Aici se afişează 6, adică s-a calculat 3!.

B) Mai puţin eficient, n! se poate calcula şi printr-o funcţie ca în exemplul următor:

Varianta Pascal Varianta C++


var val,n,p:integer; #include <iostream.h>
procedure fact(val,n:integer; void fact(int val,int n,int&
var prod:integer); prod)
begin { if (val<=n)
if val<=n then { prod*=val;
begin fact(val+1,n,prod);
prod:=prod*val; }
fact(val+1,n,prod) }
end;
end; main()
{ int val,n,p=1;
begin cout<<"n="; cin>>n;
write ('n='); readln(n); fact(1,n,p);
p:=1; cout<<p;
fact(1,n,p); }
writeln(p);
end.
146 Capitolul 6. Introducere în recursivitate

Să analizăm şi pentru acest exemplu modul în care se efectuează calculul,


pentru n=3. Funcţia are trei parametri: doi transmişi prin valoare, unul prin referinţă.

Iată conţinutul variabilelor după primul apel:

1 1 3 Referinţă către p

p val n prod

Pentru că val este mai mic sau egal ca n se efectuează: prod=prod*val;.


Întrucât variabila p a fost transmisă prin referinţă, programul lucrează cu această
variabilă, în loc de prod. În concluzie, se efectuează p=p*val;. Apoi, funcţia se
autoapelează:

2 3 Referinţă către p

1 1 3 Referinţă către p

p val n prod

Pentru că val este 2 - mai mic ca 3 - se efectuează prod=prod*val, după


care funcţia se autoapelează.

3 3 Referinţă către p

2 3 Referinţă către p

2 1 3 Referinţă către p

p val n prod

Pentru că val este 3 - mai mic sau egal cu 3 - se efectuează prod=prod*val, deci p
ia valoarea 6, după care se revine pe nivelul 2, apoi 1, apoi în programul principal.

Observaţii

 După cum rezultă din aceste exemple, subprogramul lucrează cu datele aflate
pe un anumit nivel al stivei pentru variabilele transmise prin valoare, variabilele
locale şi variabilele transmise prin referinţă.
 Există posibilitatea ca subprogramul să lucreze direct cu variabilele globale fără
ca acestea să fie transmise prin referinţă. După cum ştim variabilele globale pot fi
accesate din orice subprogram. În exemplu, am preferat varianta de a trece ca
parametru variabila transmisă prin referinţă, atât din motive didactice cât şi pentru
a asigura independenţa subprogramului. Dezavantajul este dat de faptul că stiva
va fi încărcată suplimentar cu adresa acestei variabile.
Manual de informatică pentru clasa a XI-a 147

 În cazul unui număr mare de autoapelări, există posibilitatea ca segmentul


de stivă să depăşească spaţiul alocat, caz în care programul se va termina
cu eroare.

 Recursivitatea presupune mai multă memorie în comparaţie cu iterativitatea.

6.4. Cum gândim un algoritm recursiv ?


Pentru a ne familiariza cu raţionamentul recursiv, vom porni de la câteva
exemple intuitive.

1. Wirth. O cameră de luat vederi are în obiectiv un televizor care transmite


imaginile primite de la cameră. Evident, în televizor se va vedea un televizor,
iar în acesta un televizor..., ş.a.m.d.
Pe scurt, în orice televizor se vede un televizor.
2. Anunţ. În anumite restaurante am întâlnit anunţul: “Azi nu se fumează!”.
3. Constatare. Ştiţi povestea cu săracul care a prins un peştişor de aur. Acesta
îi spune omului că dacă îi dă drumul îi îndeplineşte 3 dorinţe, oricare ar fi ele.
Omul îi dă drumul, îi spune prima dorinţă, peştişorul i-o îndeplineşte, îi spune
a doua dorinţă, peştişorul i-o îndeplineşte. A treia dorinţă a omului este să i
se îndeplinească 3 dorinţe...

Lăsând gluma la o parte, constatăm că, în general, o gândire recursivă


exprimă concentrat o anumită stare, care se repetă la infinit. Această gândire se
aplică în elaborarea algoritmilor recursivi, dar cu o modificare esenţială: adăugarea
condiţiei de terminare. În absenţa acestei condiţii, nu poate fi vorba de algoritm,
întrucât algoritmul trebuie să fie finit.

⇒ În elaborarea algoritmilor recursivi se aplică raţionamentul: ce se întâmplă la


un nivel, se întâmplă la orice nivel.

⇒ Subprogramul care se autoapelează trebuie să conţină instrucţiunile


corespunzătoare unui nivel.

⇒ Starea tratată de subprogram se găseşte pe un anumit nivel al stivei


(variabilele rezultate în urma transmiterii parametrilor şi variabilele locale ale
subprogramului).

Observaţii

 Un algoritm recursiv se elaborează utilizând acest tip de gândire, nu o


gândire precum cea folosită până acum, când am elaborat algoritmi iterativi.
 Pentru orice algoritm recursiv există unul iterativ care rezolvă aceeaşi
problemă.
 Nu întotdeauna alegerea unui algoritm recursiv reprezintă un avantaj.
 Numeroasele exemple care urmează vă vor lămuri asupra celor afirmate.
148 Capitolul 6. Introducere în recursivitate

6.5. Aplicaţii recursive

6.5.1. Aplicaţii la care se transcrie o formulă recursivă

 Aplicaţia 6.1. Se citeşte x∈Z. Se cere programul pentru calculul funcţiei:


x − 1, x ≥ 12
F(x) = 
F(F(x + 2)),x < 12

 Rezolvare. Această aplicaţie a fost tratată iterativ, prin utilizarea stivei. În cazul
tratării recursive, nu facem altceva decât să transcriem definiţia recursivă a funcţiei.

Varianta Pascal Varianta C++


var x:integer; #include <iostream.h>
int x;
function manna
(x:integer):integer; int manna (int x)
begin { if (x>=12) return x-1;
if x>=12 else
then manna:=x-1 return manna(manna(x+2));
else manna:=manna(manna(x+2)) }
end; main()
begin { cout<<"x=";
write(‘x=‘); cin>>x;
readln(x); cout<<manna(x);
writeln(manna(x)) }
end.

În comparaţie cu abordarea iterativă, abordarea recursivă prezintă avantajul


scrierii concentrate.

 Aplicaţia 6.2. Se dă funcţia de mai jos, definită pe N×N. Se citesc numerele


m şi n. Să se calculeze Ack(m,n).

n + 1, m=0

Ack(m, n) = Ack(m − 1,1), n=0
Ack(m − 1, Ack(m, n − 1)), altfel

 Rezolvare. Şi această aplicaţie a fost tratată iterativ, prin utilizarea stivei. În


cazul tratării recursive, nu facem altceva decât să transcriem definiţia recursivă
a funcţiei.
Algoritmul recursiv nu necesită comentarii.
Manual de informatică pentru clasa a XI-a 149

Varianta Pascal Varianta C++


var m,n:integer; #include <iostream.h>
int m,n;
function
ack(m,n:integer):integer; int Ack(int m,int n)
begin { if (!m) return n+1;
if m=0 else
then ack:=n+1 if (!n)
else return Ack(m-1,1);
if n=0 then ack:=ack(m-1,1) else
else return
ack:=ack(m-1,ack(m,n-1)) Ack(m-1,Ack(m,n-1));
end; }
begin main()
write(‘m=‘); readln(m); { cout<<"m="; cin>>m;
write(‘n=‘); readln(n); cout<<"n="; cin>>n;
writeln(ack(m,n)) cout<<Ack(m,n);
end. }

În comparaţie cu abordarea iterativă, abordarea recursivă prezintă avantajul


scrierii concentrate şi acela al scutirii programatorului de un efort suplimentar
în elaborarea algoritmului.

 Aplicaţia 6.3. Şirul lui Fibonacci. Se consideră şirul definit astfel:


 0, n=0

U n = 1, n =1

U n −1 + U n − 2 altfel
Se citeşte n, număr natural. Să se calculeze Un.

 Rezolvare. Funcţia fib transcrie definiţia recursivă:


Varianta Pascal Varianta C++
var n:integer; #include <iostream.h>
int n;
function
fib (n:integer):integer; int fib (int n)
begin { if (!n) return 0;
if n=0 then fib:=0 else
else if (n==1) return 1;
if n=1 then fib:=1 else
else fib:=fib(n-1)+fib(n-2) return fib(n-1)+fib(n-2);
end; }
begin main()
write(‘N=‘); readln(n); { cout<<"n="; cin>>n;
writeln(fib(n)) cout<<fib(n);
end. }
150 Capitolul 6. Introducere în recursivitate

În această situaţie, este corect să se folosească un program care calculează


Un iterativ. Să ne imaginăm cum funcţionează această funcţie.

Pentru calculul lui fib(n) este necesar să se cunoască fib(n-1) şi


fib(n-2). Parametrii acestor funcţii sunt depuşi în stivă. Procedeul continuă până
când este calculat fib(n-1), apoi se reia calculul pentru fib(n-2). Acest lucru
este extrem de ineficient pentru valori mari ale lui n (ex. n=100). Se calculează
u100 ca sumă între u99 şi u98. Pentru calculul lui u99 se calculează u98 şi u97. După
ce calculăm u99, reluăm calculele pentru u98.

O astfel de recursivitate se numeşte recursivitate în cascadă. Rulaţi


programul pentru o valoare mai mare a lui n şi ... aşteptaţi. Cât de simplu se
rezolvă problema iterativ şi cât este de rapidă această metodă...

Prezentăm varianta iterativă, care nu necesită comentarii.

Varianta Pascal Varianta C++


var n,f0,f1,f2,i:integer; #include <iostream.h>
begin
write('n='); readln(n); main()
f0:=0; { int n,f0=0,f1=1,f2;
f1:=1; cout<<"n="; cin>>n;
if n=0 then writeln(f0) if (!n) cout<<f0;
else else
if n=1 then writeln(f1) if (n==1) cout<<f1;
else else
begin { for (int i=2;i<=n;i++)
for i:=2 to n do { f2=f0+f1;
begin f0=f1;
f2:=f0+f1; f1=f2;
f0:=f1; }
f1:=f2 cout<<f2;
end; }
writeln(f2) }
end
end.

 Aplicaţia 6.4. Se dau două numere naturale a şi b. Se cere să se calculeze cel


mai mare divizor comun al lor.

 Rezolvare. Utilizăm o definiţie recursivă a celui mai mare divizor comun pentru
două numere naturale a şi b:

a, a=b

cmmdc(a, b) = cmmdc(a − b, b),a > b
cmmdc(a, b − a), a < b

Manual de informatică pentru clasa a XI-a 151

Această definiţie este transcrisă în funcţia recursivă cmmdc:

Varianta Pascal Varianta C++


var a,b:integer; #include <iostream.h>
int a,b;
function
cmmdc (a,b:integer):integer; int cmmdc (int a,int b)
begin { if (a==b) return a;
if a=b then cmmdc:=a else
else if a>b if (a>b)
then cmmdc:=cmmdc(a-b,b) return cmmdc(a-b,b);
else cmmdc:=cmmdc(a,b-a) else return cmmdc(a,b-a);
end; }
begin
write(‘a=‘); readln(a); main()
write(‘b=‘); readln(b); { cout<<"a="; cin>>a;
writeln(cmmdc(a,b)) cout<<"b="; cin>>b;
end. cout<<cmmdc(a,b);
}

Rezolvăm aceeaşi problemă iterativ (utilizând definiţia de mai sus):

Varianta Pascal Varianta C++


var a,b:integer; #include <iostream.h>
main()
begin { int a,b;
write('a='); readln(a); cout<<"a="; cin>>a;
write('b='); readln(b); cout<<"b="; cin>>b;
while a<>b do while (a!=b)
if a>b then a:=a-b if (a>b) a=a-b;
else b:=b-a; else b=b-a;
writeln('cmmdc=',a) cout<<"cmmdc="<<a;
end. }

Pentru această problemă, este indiferent ce variantă de rezolvare se alege.

 Aplicaţia 6.5. Să se scrie o funcţie recursivă pentru a calcula suma cifrelor unui
număr natural.

 Rezolvare. Iniţial, prezentăm varianta iterativă:


Varianta Pascal Varianta C++
var n,s:integer; #include <iostream.h>
begin main()
write('n='); readln(n); { int n,s=0;
s:=0; cout<<"n="; cin>>n;
while n<>0 do begin while (n)
s:=s+n mod 10; { s+=n%10;
n:=n div 10; n/=10; }
end; cout<<"s="<<s;
writeln('s=',s) }
end.
152 Capitolul 6. Introducere în recursivitate

Reţinem ideea: se izolează ultima cifră, iar lui n i se atribuie câtul întreg
dintre vechea valoare şi 10. Această idee foloseşte pentru a găsi o relaţie de
recurenţă, necesară elaborării variantei recursive. Relaţiile sunt scrise prin
utilizarea operatorilor din Pascal (stânga) şi C++ (dreapta):

0, n=0 0, n=0


S(n) =  S(n) = 
n mod 10+ S(n div 10), altfel n % 10+ S(n / 10), altfel

Programul de mai jos calculează suma utilizând relaţia prezentată:

Varianta Pascal Varianta C++


var n:integer; #include <iostream.h>
int n;
function s(n:integer):integer;
begin int s(int n)
if n=0 then s:=0 { if (!n) return 0;
else s:=n mod 10 + s(n div 10) else return n%10 + s(n/10);
end; }
begin main()
write('n='); readln(n); { cout<<"n="; cin>>n;
writeln(s(n)) cout<<s(n);
end. }

Există posibilitatea ca un subprogram să se autoapeleze prin intermediul altui


subprogram. Din moment ce s-a realizat autoapelul, este vorba de recursivitate şi
cum acesta nu s-a realizat direct - ca în exemplele anterioare - o astfel de
recursivitate se numeşte indirectă.

 Aplicaţia 6.6. Se consideră şirurile definite recurent astfel: a0=a; b0=b; a,b>0:

a n−1 + b n−1
an = , b n = a n−1b n−1 .
2

Să se scrie un program care să citească a, b şi n şi să se calculeze an şi bn.

Varianta Pascal Varianta C++


#include <iostream.h>
var a,b:real; #include <math.h>
n:integer; double a,b;
function bn(n:integer):real; int n;
forward;
function an(n:integer):real; double bn(int n);
begin double an(int n)
if n=0 { if (!n) return a;
then an:=a else
else an:=(an(n-1+bn(n-1))/2 return (an(n-1)+bn(n-1))/2;
end; }
Manual de informatică pentru clasa a XI-a 153

function bn(n:integer):real; double bn (int n)


begin { if (!n) return b;
if n=0 then bn:=b else return
else bn:=sqrt(an(n-1)*bn(n-1) sqrt(an(n-1)*bn(n-1));
end; }

begin main()
write(‘a=‘); readln(a); { cout<<"a="; cin>>a;
write(‘b=‘); readln(b); cout<<"b="; cin>>b;
write(‘n=‘); readln(n); cout<<"n="; cin>>n;
writeln(an(n):5:10,’ ‘, cout<<an(n)<<' '<<bn(n);
bn(n):5:10) }
end.

6.5.2. Aplicaţii la care nu dispunem de o formulă de


recurenţă

 Aplicaţia 6.7. Să se scrie o funcţie recursivă care citeşte caractere şi le


afişează în ordinea inversă citirii. Sfârşitul şirului este marcat de caracterul “0”.

 Rezolvare. Conform principiului, ce se întâmplă la un nivel se întâmplă la orice


nivel, vom gândi funcţia recursivă astfel:

 se citeşte un caracter;
 dacă este diferit de 0, se reapelează funcţia;
 se tipăreşte caracterul.

Raţionamentul este suficient pentru a scrie programul.

Varianta Pascal Varianta C++


procedure inv; #include <iostream.h>
var a:char; void inv()
begin { char a; cin>>a;
read(a); if (a!='0') inv();
if a<>'0' then inv; cout<<a;
write(a) }
end;
main()
begin { inv();
inv; cout<<endl;
writeln; }
end.

 Exerciţiu. Modificaţi programul astfel încât caracterul 0 - care marchează


sfârşitul şirului - să nu fie tipărit.
154 Capitolul 6. Introducere în recursivitate

 Aplicaţia 6.8. Să se scrie o funcţie recursivă pentru a transforma un număr


natural n, din baza 10 în baza k (1<k<10).

 Rezolvare. Să ne amintim algoritmul clasic de trecere din baza 10 în baza k.


Numărul se împarte la k, se reţine restul, câtul se împarte la k, se reţine restul...
până când câtul este mai mic decât împărţitorul. Rezultatul se obţine prin scrierea
în ordine inversă a resturilor obţinute. Practic, tipărirea restului se face după
autoapel (ca şi la problema anterioară).

Varianta Pascal Varianta C++


var n,b:integer; #include <iostream.h>
int n,b;
procedure
transform(n,b:integer); void transform(int n,int b)
var rest:integer; { int rest=n%b;
begin if (n>=b) transform(n/b,b);
rest:=n mod b; cout<<rest;
if n>b }
then transform(n div b,b);
write(rest) main()
end; { cout<<"n="; cin>>n;
cout<<"baza="; cin>>b;
begin transform(n,b);
write('n='); readln(n); }
write('baza='); readln(b);
transform(n,b);
writeln;
end.

 Aplicaţia 6.9. Se dă mulţimea {1,2,...,n} şi se cer toate permutările acesteia.


Exemplu: pentru n=3 avem:
{3,1,2}, {2,3,1}, {2,1,3}, {3,2,1}, {1,3,2}, {1,2,3}.

 Rezolvare. În vederea rezolvării problemei, observăm următoarele:

 mulţimea cu un singur element {1} are o singură permutare: {1};

 din fiecare permutare a mulţimii {1,2,...,n-1} ({a1,a2,...,an-1}), se


obţin următoarele permutări ale mulţimii {1,2,...,n}:
{n,a2,a3,...,an-1,a1};
{a1,n,a3,...,an-1,a2};
{a1,a2,n,...,an-1,a3};
...
{a1,a2,a3,...,an-1,n}.
Manual de informatică pentru clasa a XI-a 155

Pentru n=3, priviţi reprezentarea din 321


figura alăturată:
21 231

213
1
312

12 132
Figura 6.1.
Cazul în care n=3
123

Avem în vedere posibilitatea de revenire la situaţia iniţială: după ce am


operat o interschimbare a elementelor ai şi aj, urmată de o reapelare a funcţiei
pentru valoarea k+1, interschimbăm din nou ai cu aj.

Varianta Pascal Varianta C++


var p: array [1..8] of integer; #include <iostream.h>
n,i:integer; int p[10],n;
procedure tipar; void tipar()
var i:integer; { for (int i=1;i<=n;i++)
begin cout<<p[i];
for i:=1 to n do write(p[i]); cout<<endl;
writeln }
end;
void permut(int k,int n,
procedure permut(k,n:integer; int p[10])
var p:vector); { int i,c;
var i,j,c:integer; if (k==n+1) tipar();
begin else
if k=n+1 { p[k]=k;
then tipar for (i=1;i<=k;i++)
else { c=p[i];
begin p[i]=p[k];
p[k]:=k; p[k]=c;
for i:=1 to k do permut(k+1,n,p);
begin c=p[i];
c:=p[i]; p[i]=p[k];
p[i]:=p[k]; p[k]=c;
p[k]:=c; }
permut(k+1,n,p); }
c:=p[i]; }
p[i]:=p[k];
p[k]:=c main()
end { cout<<"n=";
end cin>>n;
end; permut(1,n,p);
}
begin
write('n='); readln(n);
permut(1,n,p)
end.
156 Capitolul 6. Introducere în recursivitate

 Aplicaţia 6.10. Algoritmul Fill. Se dă o matrice binară. Valorile 1 delimitează


o anumită suprafaţă închisă în cadrul matricei (elementele aparţinând acestei
suprafeţe sunt marcate cu 0). De asemenea, se dau coordonatele x şi y ale unui
element al matricei, semnificând un punct din interiorul acestei suprafeţe.

Fie matricea de mai jos:

0 1 1 0
 
0 0 0 1
A=
0 1 1 1
 
1 0 
 0 0

Suprafaţa închisă este dată de elementele A(1,1), A(2,1), A(2,2),


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

Considerăm coordonatele (2,3) ale unui punct situat în interiorul acestei


suprafeţe. După executarea programului, matricea trebuie să arate astfel:

1 1 1 0
 
1 1 1 1
A=
1 1 1 1
 
1 0 0 0 

Algoritmul se dovedeşte extrem de util în colorarea unei suprafeţe închise


atunci când sunt cunoscute coordonatele unui punct situat în interiorul ei.
Acest algoritm este cunoscut şi sub denumirea de algoritmul FILL.

 Rezolvare. Pentru rezolvare se foloseşte funcţia Scriu(), care se autoape-


lează. Iniţial, matricea se bordează cu două linii şi două coloane ce conţin elemente
care au valoarea 1. Aceasta are ca scop evitarea testului de ieşire din matrice.

Funcţia Scriu() funcţionează astfel:

• testează dacă elementul matricei la care s-a ajuns (de coordonate


(x,y)) are valoarea 0;

• în caz afirmativ, acesta ia valoarea 1, iar funcţia se autoapelează


pentru fiecare dintre elementele învecinate (sus, jos, dreapta, stânga);

• în caz contrar, se iese din funcţie.

Programul este prezentat în continuare:


Manual de informatică pentru clasa a XI-a 157

Varianta Pascal Varianta C++


program umplere; #include <iostream.h>
var a:matrice: array int a[10][10],i,j,m,n,x,y;
[0..9,0..9] of integer;
i,j,m,n,x,y:integer; void scriu (int x,int y,
int a[10][10])
procedure scriu (x,y:integer; { if (!a[x][y])
var a:matrice); { a[x][y]=1;
begin scriu(x+1,y,a);
if a[x,y]=0 then scriu(x,y+1,a);
begin scriu(x-1,y,a);
a[x,y]:=1; scriu(x,y-1,a);
scriu(x+1,y,a); }
scriu(x,y+1,a); }
scriu(x-1,y,a);
scriu(x,y-1,a) main()
end { cout<<"M="; cin>>m;
end; cout<<"N="; cin>>n;
for (i=1;i<=m;i++)
begin for (j=1;j<=n;j++)
write('M='); readln(m); { cout<<"a["<<i<<','
write('N='); readln(n); <<j<<"]=";
for i:=1 to m do cin>>a[i][j];
for j:=1 to n do }
begin
for (i=1;i<=n;i++)
write('a[',i,',',j,']=');
{ a[0][i]=1;
readln(a[i,j])
a[m+1][i]=1;
end;
}
for i:=1 to n do
begin for (i=1;i<=m;i++)
a[0,i]:=1; { a[i][0]=1;
a[m+1,i]:=1 a[i][n+1]=1;
end; }
for i:=1 to m do cout<<"X="; cin>>x;
begin cout<<"Y="; cin>>y;
a[i,0]:=1; for (i=1;i<=m;i++)
a[i,n+1]:=1 { for (j=1;j<=n;j++)
end; cout<<a[i][j];
write('X='); readln(x); cout<<endl;
write('Y='); readln(y); }
for i:=1 to m do scriu(x,y,a);
begin cout<<endl<<endl;
for j:=1 to n do for (i=1;i<=m;i++)
write(a[i,j]); { for (j=1;j<=n;j++)
writeln cout<<a[i][j];
end; cout<<endl;
scriu(x,y,a); }
writeln; }
writeln;
for i:=1 to m do
begin
for j:=1 to n do
write(a[i,j]);
writeln
end
end.
158 Capitolul 6. Introducere în recursivitate

 Aplicaţia 6.11. Problema fotografiei (aplicaţie Fill). O fotografie alb-negru


este reprezentată sub forma unei matrice binare. Ea înfăţişează unul sau mai multe
obiecte. Porţiunile corespunzătoare obiectului (sau obiectelor) în matrice au valoarea
”1”. Se cere să se determine dacă fotografia reprezintă unul sau mai multe obiecte.

În matricea următoare sunt reprezentate două obiecte:

1 1 0 0
 
0 0 0 1
A= ,
1 1 1 1
 
1 1 
 1 1

iar în matricea de mai jos este reprezentat un singur obiect:

0 1 1 0
 
0 0 0 1
A= .
0 1 1 1
 
1 0 0 0 

Ca şi în problemele anterioare, pentru a evita testul ieşirii din matrice,
aceasta este bordată cu linii şi coloane având valoarea ”0”. Algoritmul este tot cel
din problema anterioară (Fill), dar aici căutarea se face pe 8 direcţii.
În programul principal se citeşte matricea şi se caută primul element ”1”
printre elementele acesteia. Se apelează apoi funcţia compact() care are rolul de
a marca cu 0 toate elementele matricei care aparţin acestui prim obiect identificat.
La revenire, se testează dacă mai există elemente cu valoarea ”1” în matrice. În
caz afirmativ, se poate trage concluzia că în fotografie aveam iniţial mai multe
obiecte (altfel, fotografia conţinea un singur obiect).

Varianta Pascal Varianta C++


var a: array [0..9,0..9] of #include <iostream.h>
integer; int a[10][10], i,j,m,n,
i,j,m,n,x,y:integer; x,y,gasit;
gasit:boolean;
procedure compact void compact(int x,int y,
(x,y:integer; var a:matrice); int a[10][10])
begin { if (a[x][y])
if a[x,y]=1 then begin { a[x][y]=0;
a[x,y]:=0; compact(x-1,y,a);
compact(x-1,y,a); compact(x-1,y+1,a);
compact(x-1,y+1,a); compact(x,y+1,a);
compact(x,y+1,a); compact(x+1,y+1,a);
compact(x+1,y+1,a); compact(x+1,y,a);
compact(x+1,y,a); compact(x+1,y-1,a);
compact(x+1,y-1,a); compact(x,y-1,a);
compact(x,y-1,a); compact(x-1,y-1,a);
compact(x-1,y-1,a); }
end }
end;
Manual de informatică pentru clasa a XI-a 159

begin main()
write('M='); readln(m); { cout<<"M=";cin>>m;
write('N='); readln(n); cout<<"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<<"a["<<i<<','
write('a[',i,',',j,']='); <<j<<"]=";
readln(a[i,j]) cin>>a[i][j];
end; }
for i:=1 to n do for (i=1;i<=n;i++)
begin { a[0][i]=0;
a[0,i]:=0; a[m+1][i]=0;
a[m+1,i]:=0; }
end; for (i=1;i<=m;i++)
for i:=1 to m do { a[i][0]=0;
begin a[i][n+1]=0;
a[i,0]:=0; }
a[i,n+1]:=0 x=0;
end; do
x:=0; { x++;
repeat y=0;
x:=x+1; do
y:=0; { y++; }
repeat while (y!=n && a[x][y]!=1);
y:=y+1 }
until (y=n) or (a[x,y]=1) while ((x!=m) && a[x][y]!=1);
until (x=m) or (a[x,y]=1); compact(x,y,a);
compact(x,y,a); gasit=0;
gasit:=false; for (i=1;i<=m;i++)
for i:=1 to m do for (j=1;j<=n;j++)
for j:=1 to n do if (a[i][j]==1) gasit=1;
if a[i,j]=1 then if (gasit)
gasit:=true; cout<<"mai multe obiecte";
if gasit else cout<<"un obiect";
then writeln('mai multe }
obiecte')
else writeln('un obiect')
end.

Probleme propuse
1. Calculaţi recursiv suma a n numere naturale citite.

2. Calculaţi recursiv expresiile:

a) 1×2+2×3+...+n×(n+1);
b) 1+1/2+...+1/n;
c) 1/(2×3)+2/(3*4)+...+n/((n+1)(n+2)).
160 Capitolul 6. Introducere în recursivitate

3. Se citesc n şi k (numere naturale n>k). Calculaţi recursiv Cnk , prin utilizarea


formulei de recurenţă: Cnk = Cnk−−11 + Cnk−1 . Este eficient?
4. Scrieţi un program iterativ care rezolvă problema anterioară utilizând aceeaşi
formulă.

5. Calculaţi recursiv Cnk prin utilizarea formulei:

1, k = 0;
C =  n − k + 1 k −1
k
n
Cn altfel.
 k

Comparaţi timpul de rezolvare cu cel necesar pentru rezolvarea problemei 4.

6. Scrieţi un subprogram recursiv prin care calculatorul ghiceşte un număr natural


ascuns de dumneavoastră (numărul este cuprins între 1 şi 30000). Atunci
când calculatorul propune un număr i, se va răspunde prin:
1, dacă numărul este prea mare;
2, dacă numărul este prea mic;
0, dacă numărul a fost ghicit.

7. Scrieţi un subprogram recursiv care calculează câte cuvinte distincte cu 2n


caractere se pot forma cu n caractere A şi n caractere B.

8. Calculaţi conform formulei următoare valoarea maximă reţinută de un vector de


numere naturale cu n componente:

V[1], n = 1;
max(V[1], V[2],...V[n]) = 
max(max(V[1], V[2],...V[n − 1]), V[n]) altfel.
9. Se citeşte un număr natural n. Se cere să se scrie o funcţie recursivă care
returnează cea mai mică bază în care se poate considera n.

10. Scrieţi o funcţie recursivă care testează dacă un număr natural n>1 este prim.

11. Scrieţi o funcţie recursivă care returnează suma elementelor pare ale unui
vector citit.
Exemplu: Pentru n=4 şi V=(2,2,5,6), se returnează 10.

12. Scrieţi o funcţie recursivă prin care se testează dacă un număr natural x se
regăseşte între componentele unui vector V cu n numere naturale.

13. Scrieţi o funcţie recursivă care primeşte ca parametri două numere naturale
i<j şi un număr real x∈[i,j] şi returnează [x] (parte întreagă din x). Nu se
vor folosi funcţiile specializate ale limbajului.
Manual de informatică pentru clasa a XI-a 161

14. Scrieţi o funcţie recursivă care verifică dacă un vector cu componente numere
naturale este palindrom (afişarea componentelor de la 1 la n coincide cu afişarea
lor de la n la 1).

15. Scrieţi o funcţie recursivă care returnează numărul cifrelor pe care le are un
număr natural primit ca parametru.

16. Scrieţi un subprogram recursiv care afişează, cifră cu cifră, oglinditul unui
număr natural.
Exemplu: pentru n=123, se afişează 321.

17. Scrieţi un subprogram recursiv care returnează, oglinditul unui număr natural.
Exemplu: pentru n=123, se returnează 321.

18. Scrieţi o funcţie recursivă care testează dacă un vector cu n numere naturale
reţine numai valori distincte, caz în care funcţia returnează true, iar în caz contrar,
returnează false.

19. Scrieţi un subprogram recursiv care descompune în toate modurile posibile un


număr natural n în două numere n1 şi n2, n1≤n2 a căror sumă este n.

20. Pentru un vector cu n componente 0 sau 1 care are semnificaţia de număr


binar, se cere să se scrie o funcţie recursivă care afişează numărul în baza 10.
Exemplu: pentru n=4 şi V=(1011), se va returna 11.

21. Scrieţi o funcţie recursivă care afişează valoarea unui polinom în punctul a.
Coeficienţii polinomului sunt daţi într-un vector. Astfel, pentru V=(1,2,3) avem
polinomul P=x2+2x+3.

22. Fie funcţia definită pe N*×N*. Se citesc n şi k. Se cere să se scrie o funcţie


recursivă care evaluează funcţia:

 0, k>n

S(n, k) =  1, k ∈ {1, n}
S(n − 1, k − 1) + kS(n − 1, k) 1 < k < n

23. Calculaţi S(n,k) nerecursiv.

24. Calculaţi recursiv şi nerecursiv funcţia definită pe N*×N:

0, k = 0 sau k > n;



F(n, k) = 1 k = n;
F(n − 1, k − 1) − nF(n − 1, k) 1 < k < n.

Comparaţi eficienţa celor două moduri de calcul.
162 Capitolul 6. Introducere în recursivitate

25. Să se calculeze recursiv şi nerecursiv P(n,k), definit pe N*×N*:

P(n+k,k)=P(n,1)+P(n,2)+...+P(n,k), dacă 1<k<n;


P(n,k)=1, dacă k=1 sau k=n;
P(n,k)=0, dacă k>n.

26. Calculaţi iterativ şi recursiv cel mai mare divizor comun pentru două numere
naturale m şi n, utilizând algoritmul lui Euclid:

cmmdc(n, m mod n), n ≠ 0 cmmdc(n, m % n), n ≠ 0


cmmdc(m, n) =  cmmdc(m, n) = 
m, n=0 m, n=0
Pascal C++

27. Ce se afişează la: writeln(t(12)); / cout<<t(12); ?

Varianta Pascal Varianta C++


function t(n:integer):integer; int t(int n)
begin { if (n) return 10*t(n/10)+
if n<>0 then t:=10*t(n div 10) n%10;
+(n mod 10) else return 0;
else t:=0; }
end;

a) 12;
b) 21;
c) eroare de executare;
d) 0.

28. Ce calculează funcţia următoare?

Varianta Pascal Varianta C++


function t(n:integer):integer; int t(int n)
begin { if (n)
if n<>0 then return (n%2==0)*n +t(n-1);
t:=ord(n mod 2=0)*n+t(n-1) else return 0;
else t:=0; }
end;

a) suma primelor n numere naturale impare;


b) suma primelor n numere naturale pare;
c) suma numerelor naturale pare strict mai mici decât n;
d) suma numerelor naturale pare mai mici sau egale cu n.
Manual de informatică pentru clasa a XI-a 163

29. Pentru care dintre numerele de mai jos, care sunt parametri de intrare pentru
funcţia următoare, ultimele 2 numere afişate vor fi 0?

Varianta Pascal Varianta C++


procedure t(n:integer); void t(int n)
begin { if (n>15)
if n>15 then { t(n/16);
begin cout<<n%16<<endl;
t(n div 16); }
writeln(n mod 16) else cout<<n<<endl;
end }
else writeln (n);
end;

a) 295;
b) 1024;
c) 1000;
d) 10000.

Testele de la 30 la 33 se referă la funcţia de mai jos, unde V1 şi V2 sunt vectori


cu n componente numere naturale:

Varianta Pascal Varianta C++


procedure t(n,i,j:integer); void t(int n, int i, int j)
begin { if (i<=n && j<=n)
if (i<=n) and (j<=n) then { if (V1[i]<V2[j])
begin cout<<V1[i++]<<" ";
if V1[i]<V2[j] then else cout<<V2[j++]<<" ";
begin t(n,i,j);
write (V1[i],’ ‘); }
i:=i+1 }
end
else // V1 si V2 contin date
begin // incepand cu indicele 1
write (V2[j],’ ‘);
j:=j+1
end;
t(n,i,j);
end;
end;

30. Dacă n este egal cu 3, V1=(1,2,3) şi V2=(4,5,1) ce se afişează la


apelul t(n,1,1)?

a) 1 2 3;
b) 1 4 2 5 3 1;
c) cerinţa nu este corectă;
d) 4 5 1.
164 Capitolul 6. Introducere în recursivitate

31. Care dintre afirmaţiile de mai jos sunt corecte dacă n este 3 şi apelul este
t(n,1,1)?

a) întotdeauna se vor afişa cel mult 6 numere;


b) întotdeauna se vor afişa cel puţin 3 numere;
c) întotdeauna se vor afişa cel mult 3 numere;
d) întotdeauna se vor afişa cel puţin 6 numere.

32. Dacă n este 3, pentru care din datele de mai jos se afişează un număr maxim
de valori distincte, dacă apelul este t(n,1,1)?

a) V1=(1,2,8) şi V2=(4,5,6);
b) V1=(1,2,5) şi V2=(4,5,6);
c) V1=(4,5,6) şi V2=(1,2,3);
d) V1=(1,2,3) şi V2=(4,5,6).

33. Ce va afişa funcţia la apelul t(1,1,n) dacă n=3, V1=(1,2,3) şi


V2=(4,5,6)?

a) 1 2 3; b) 1 2 3 4 5 6;
c) 4 5 6; d) nici o valoare.

Testele de la 34 la 36 se referă la programul următor:

Varianta Pascal Varianta C++


type vector=array[1..10] of #include <iostream.h>
integer; int m,n,i,V[20];
var m,n,i:integer;
V:vector; void b(int k, int n)
{ if (n)
procedure b(k,n:integer); { b(k+1,n/2);
begin if (n%2) cout<<V[k]<<endl;
if n<>0 then }
begin }
b(k+1,n div 2);
if n mod 2=1 then main()
writeln(V[k]);; { int i,n;
end cin>>n;
end; for (i=1;i<=n;i++)
cin>>V[i];
begin cin>>m;
readln (n); b(1,m);
for i:=1 to n do readln(V[i]); }
readln(m);b(1,m);
end.

34. Dacă n=4, V=(1,2,3,4) şi m=2, ce afişează programul?

a) 4; b) 3; c) 2; d) 1.
Manual de informatică pentru clasa a XI-a 165

35. Dacă n=4, pentru care dintre valorile de mai jos ale lui m se afişează două
valori?

a) 4; b) 3; c) 2; d) 1.

36. Dacă n=4, pentru care valoare a lui m se afişează toate cele 4 valori ale
vectorului?

a) 15; b) 16; c) 0; d) Nu există o astfel de valoare.

37. În funcţia de mai jos înlocuiţi linia ”...” cu una dintre instrucţiunile următoare,
astfel încât funcţia să-şi încheie execuţia fără eroare pentru orice valoare admisibilă
a argumentului:

Varianta Pascal Varianta C++


function x(n:integer):integer; int x(int n)
begin {
if n<>0 then if (n)
begin {
writeln(n); cout<<n;
... ...
end }
end; }

a) x:=x(n-2); a) return x(n-2);


b) x:=x(n mod 2); b) return x(n%2);
c) x:=x(n-1); c) return x(n-1);
d) x:=x(n div 2). d) return x(n/2).

Testele 38 şi 39 se referă la funcţia de mai jos:

Varianta Pascal Varianta C++


function an(n:integer):integer; int an(int n)
begin {
if n=0 then an:=1 if (n==0) return 1;
else an:=3*an(n-1)+7 else return 3*an(n-1)+7;
end; }

38. Dacă funcţia este apelată prin an(4), de câte ori se autoapelează?

a) de 4 ori; b) de 3 ori; c) de 5 ori; d) depinde de stivă.

39. Pentru care dintre valorile de mai jos, care sunt parametri de intrare pentru
funcţia an, executarea funcţiei se termină cu eroare?

a) 0; b) -1; c) 1; d) nici una dintre valorile de mai sus.


166 Capitolul 6. Introducere în recursivitate

40. Pentru programul de mai jos, de câte ori se autoapelează funcţia an?

Varianta Pascal Varianta C++


function an(n:integer):integer; #include <iostream.h>
begin int an(int n)
if n=0 then an:=2 { if (n==0) return 2;
else else
if n=1 then an:=1 if (n==1) return 1;
else an:=an(n-1)-an(n-2)+1 else
end; return an(n-1)-an(n-2)+1;
}
begin
writeln(an(4)); main()
end. { cout<<an(4); }

a) de 8 ori; b) de 4 ori; c) de 9 ori; d) de 2 ori.

Indicaţii / Rezolvări

1. Sn=1+2+....n-1+n; Sn⇒Sn-1+n;

0, daca n = 0
Sn = 
S n−1 + n, altfel

7. Există (2n)! permutări ale literelor. Întrucât pentru fiecare permutare nu


contează dacă s-a inversat A cu A, (2n)! se împarte la n!. Pentru că nu
contează dacă s-a inversat B cu B (2n)!, se împarte din nou la n!. Se obţine:

(2n )! = C n
n!⋅n!
2n

8.
Varianta Pascal Varianta C++
function int Maxim(int n)
Maxim(n:integer):integer; { int max;
var max:integer; if (n==1) return V[1];
begin else
if n=1 then Maxim:=V[1] { max=Maxim(n-1);
else begin if (max<V[n]) return V[n];
max:=Maxim(n-1); else return max;
if max<V[n] then }
Maxim:=V[n] }
else Maxim:=max;
end;
end;
Manual de informatică pentru clasa a XI-a 167

9. Se calculează cifra maximă a numărului şi se adună 1.

10. De exemplu, funcţia de mai jos se apelează Prim(n,2):

Varianta Pascal Varianta C++


function int Prim(int n,int i)
Prim(n,i:integer):boolean; { if (i>(int)(sqrt(n)+1))
begin return 1;
if i>trunc(sqrt(n))+1 else
then Prim:=true if (n%i==0) return 0;
else else return Prim(n,i+1);
if n mod i=0 }
then Prim:=false
else Prim:=Prim(n,i+1);
end;

11.

Varianta Pascal Varianta C++


function int Suma(int n)
Suma(n:integer):integer; { if (n==0) return 0;
begin else return Suma(n-1)+
if n=0 (V[n]%2==0)*V[n];
then Suma:=0 }
else Suma:=Suma(n-1)+
ord(V[n] mod 2=0)*V[n];
end;

12.

Varianta Pascal Varianta C++


function int Apartine(int n, int x)
Apartine(n,x:integer):boolean; { if (n==0) return 0;
begin else
if n=0 if (V[n]==x) return 1;
then else
Apartine:=false return Apartine(n-1,x);
else }
if V[n]=x
then Apartine:=true
else
Apartine:=Apartine(n-1,x);
end;
168 Capitolul 6. Introducere în recursivitate

13. Este clasicul algoritm de căutare binară.

Varianta Pascal Varianta C++


function int PInt(int i,int j,float x)
PInt(i,j:integer;x:real):integer; { int Mijloc;
var Mijloc:integer; if (j-i>1)
begin { Mijloc=(i+j)/2;
if j-i>1 if (x==Mijloc)
then return Mijloc;
begin else
Mijloc:=(i+j) div 2; if (x<Mijloc)
if x=Mijloc return
then Pint:=Mijloc PInt(i,Mijloc,x);
else else
if x<Mijloc then PInt(Mijloc,j,x);
Pint:=Pint(i,Mijloc,x) }
else Pint:=Pint(Mijloc,j,x) else return i;
end }
else Pint:=i
end;

14. Iniţial, se apelează cu Palindrom(1,n):

Varianta Pascal Varianta C++


function int Palindrom(int i, int j)
Palindrom(i,j:integer):boolean; {
begin if (i>=j) return 1;
if i>=j else
then Palindrom:=true if (V[i]!=V[j]) return 0;
else else return
if V[i]<>V[j] Palindrom(i+1,j-1);
then Palindrom:=false }
else Palindrom:=
Palindrom(i+1,j-1);
end;

15.

Varianta Pascal Varianta C++


function Nrcif (n:longint): int Nrcif(long n)
integer; { if (n<10) return 1;
begin else return Nrcif(n/10)+1;
if n<10 }
then Nrcif:=1
else Nrcif:=NrCif(n div 10)+1;
end;
Manual de informatică pentru clasa a XI-a 169

16.

Varianta Pascal Varianta C++


procedure Oglinda(n:integer); void Oglinda(int n)
begin { if (n)
if n<>0 then { cout<<n%10;
begin Oglinda (n/10);
write(n mod 10); }
Oglinda(n div 10); }
end;
end;

17. Variabila ninv, transmisă prin referinţă, trebuie să reţină, iniţial, 0.

Varianta Pascal Varianta C++


procedure Oglinda(n:integer; void Oglinda(int n, int& ninv)
var ninv:integer); { if (n)
begin { ninv=ninv*10+n%10;
if n<>0 then Oglinda (n/10,ninv);
begin }
ninv:=ninv*10+n mod 10; }
Oglinda(n div 10, ninv);
end;
end;

18. Funcţia se apelează cu Distincte(1,n):

Varianta Pascal Varianta C++


function int Distincte(int i,int n)
Distincte(i,n:integer):boolean; { int gasit,j;
var gasit:boolean; if (i==n) return 1;
j:integer; else
begin { gasit=0;
if i=n for (j=i+1;j<=n;j++)
then Distincte:=true if (V[i]==V[j]) gasit=1;
else if (gasit) return 0;
begin else return
gasit:=false; Distincte(i+1,n);
for j:=i+1 to n do }
if V[i]=V[j] }
then gasit:=true;
if gasit
then Distincte:=false
else Distincte:=
Distincte(i+1,n);
end
end;
170 Capitolul 6. Introducere în recursivitate

19. Apelaţi cu Descompun(1,n):

Varianta Pascal Varianta C++


procedure Descompun(i,n:integer); void Descompun(int i,int n)
begin { if (i<=n/2)
if i<= n div 2 then { cout<<i<<" "<<n-i<<endl;
begin Descompun(i+1,n);
writeln (i, ' ', n-i); }
Descompun(i+1,n); }
end
end;

20. Apelaţi cu Refac(n,n):

Varianta Pascal Varianta C++


function int Refac(int i,int n)
Refac(i,n:integer):integer; { if (i==1) return V[1];
begin else
if i=1 then Refac:=V[1] return 2*Refac(i-1,n)+V[i];
else }
Refac:=2*Refac(i-1,n)+V[i];
end;

21. Apelaţi cu Valoare(n,n,a):

Varianta Pascal Varianta C++


function int Valoare(int i,int n,int a)
Valoare(i,n,a:integer):integer; { if (i==1) return V[i];
begin else return a*Valoare(i-1,
if i=1 then Valoare:=V[1] n,a)+V[i];
else Valoare:=a*Valoare(i-1, }
n,a)+V[i];
end;

22. Este ineficient să calculăm recursiv.

23. Să presupunem că avem de calculat S(8,5). Mai întâi calculăm:

S(1,1)=1, S(1,2)=0, S(1,3)=0, ..., S(1,8)=0. Urmează:

S(2,1)=1, S(2,2)=1,S(2,3)=0, ..., S(2,8)=0


S(3,1)=1, S(3,2)=S(2,1)+2S(2,2)=3, S(3,3)=1, S(3,4)=0, ...
S(4,1)=1, S(4,2)=S(3,1)+2S(3,2)=7, ...

Cu excepţia primei linii S(1,x), pentru calculul elementelor de pe linia i, S(i,k),


se folosesc rezultatele aflate pe linia i-1, adică S(i-1,x).
Manual de informatică pentru clasa a XI-a 171

25. În cazul în care k<n, avem:


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

Varianta Pascal Varianta C++


function int P(int n,int k)
P(n,k:integer):integer; { int i,s=0;
var i,s:integer; if (k>n) return 0;
begin else
if k>n then P:=0 if (k==1 || k==n) return 1;
else else
if (k=1) or (k=n) { for (i=1;i<=n-k;i++)
then P:=1 s+=P(n-k,i);
else return s;
begin }
s:=0; }
for i:=1 to n-k do
s:=s+P(n-k,i);
P:=s;
end
end;

27. a); 28. d); 29. b); 30. a); 31. a), b); 32. a); 33. d);

34. c); 35. b); 36. a); 37. d);

38. a); 39. b);


40. a) Autoapelurile se efectuează după schema de mai jos:

3 2

2 1 1 0

1 0

Figura 6.2. Exemplu

Acum, vedeţi “pe viu” motivul pentru care, în cazul unor astfel de formule de
recurenţă, în care, în expresie, intervin mai mulţi operanzi ce se calculează
recursiv, este de preferat metoda iterativă.
172

Capitolul 7

Metoda DIVIDE ET IMPERA

7.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.

7.2. Aplicaţii

7.2.1. Valoarea maximă dintr-un vector

 Problema 7.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 173

 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.
174 Capitolul 7. Metoda DIVIDE ET IMPERA

7.2.2. Sortarea prin interclasare

 Problema 7.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 175

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.
176 Capitolul 7. 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

7.2.3. Sortarea rapidă

 Problema 7.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 177

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; }
178 Capitolul 7. 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++)
procedure quick(li,ls:integer); cout<<a[i]<<endl;
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 179

7.2.4. Turnurile din Hanoi

 Problema 7.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.
180 Capitolul 7. 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.

7.2.5. Problema tăieturilor

 Problema 7.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 181

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; }
182 Capitolul 7. 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 183

7.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ă. Aceştia 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. Acestea vor fi prezentate în continuare.

7.3.1. Elemente de grafică

7.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.
184 Capitolul 7. 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 185

7.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.


186 Capitolul 7. 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.

7.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 187

 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.
188 Capitolul 7. Metoda DIVIDE ET IMPERA

7.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 7.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 189

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ă trasă 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);
190 Capitolul 7. 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 7.2. Exemple de fractali formaţi cu ajutorul curbei lui Koch (triunghi echilateral)
Manual de informatică pentru clasa a XI-a 191

7.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 7.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(xc,yc,x1, int gdriver,gmode,ls,L;
y1:integer;
var x,y:integer; void init()
unghi:real); { ...
... }
192 Capitolul 7. Metoda DIVIDE ET IMPERA

procedure desen(x1,y1,x2, void rotplan(int xc,int yc,


y2,n,ls:integer); int x1,int y1,int &x,int &y,
var x3,x4,x5,x6,x7,x8,xc, float unghi)
y3,y4,y5,y6,y7,y8,yc:integer; { ...
begin }
if n<=ls then void desen(int x1,int y1,int
begin x2,int y2,int n,int ls)
x3:=(3*x1+x2) div 4; { int x3,x4,x5,x6,x7,x8,
y3:=(3*y1+y2) div 4; xc,y3, y4,y5,y6,y7,y8,yc;
rotplan(x3,y3,x1,y1,x4,y4, if (n<=ls)
-pi/2); { x3=div(3*x1+x2,4).quot;
xc:=(x1+x2) div 2; y3=div(3*y1+y2, 4).quot;
yc:=(y1+y2) div 2; rotplan(x3,y3,x1,y1,x4,y4,
rotplan(xc,yc,x3,y3,x5,y5, -M_PI/2);
-pi/2); xc=div(x1+x2,2).quot;
rotplan(xc,yc,x3,y3, yc=div(y1+y2,2).quot;
x6,y6,pi/2); rotplan(xc,yc,x3,y3,x5,y5,
x8:=(x1+3*x2) div 4; -M_PI/2);
y8:=(y1+3*y2) div 4; rotplan(xc,yc,x3,y3,x6,y6,
rotplan(x8,y8,xc,yc,x7,y7, M_PI/2);
pi/2); x8=div(x1+3*x2, 4).quot;
desen(x1,y1,x3,y3,n+1,ls); y8=div(y1+3*y2,4).quot;
desen(x3,y3,x4,y4,n+1,ls); rotplan(x8,y8,xc,yc,x7,y7,
desen(x4,y4,x5,y5,n+1,ls); M_PI/2);
desen(x5,y5,xc,yc,n+1,ls); desen(x1,y1,x3,y3,n+1,ls);
desen(xc,yc,x6,y6,n+1,ls); desen(x3,y3,x4,y4,n+1,ls);
desen(x4,y4,x5,y5,n+1,ls);
desen(x6,y6,x7,y7,n+1,ls);
desen(x5,y5,xc,yc,n+1,ls);
desen(x7,y7,x8,y8,n+1,ls);
desen(xc,yc,x6,y6,n+1,ls);
desen(x8,y8,x2,y2,n+1,ls); desen(x6,y6,x7,y7,n+1,ls);
if n = ls then begin desen(x7,y7,x8,y8,n+1,ls);
moveto(x1,y1); desen(x8,y8,x2,y2,n+1,ls);
lineto(x3,y3); if (n == ls)
lineto(x4,y4); { moveto(x1,y1);
lineto(x5,y5); lineto(x3,y3);
lineto(x6,y6); lineto(x4,y4);
lineto(x7,y7); lineto(x5,y5);
lineto(x8,y8); lineto(x6,y6);
lineto(x2,y2); lineto(x7,y7);
end lineto(x8,y8);
end lineto(x2,y2); }
end; }
}
begin
write('ls= '); readln(ls); main()
{ cout<<"ls= "; cin>>ls;
initg; setcolor(red);
init(); setcolor(6);
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,blue); setfillstyle(1,3);
floodfill(getmaxx div 2, floodfill(div(getmaxx(),2)
getmaxy div 2, red); .quot,div(getmaxy(),2).quot,6);
readln getch(); closegraph();
end. }
Manual de informatică pentru clasa a XI-a 193

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 7.4. Exemple de fractali formaţi cu ajutorul curbei lui Koch (pătrat)

7.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 7.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 .
194 Capitolul 7. Metoda DIVIDE ET IMPERA

Î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"
#include <iostream.h>
var gdriver,gmode,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(...)
... { ... }
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);
rotplan(x2,y2,div(3*x2-
desenez(x2,y2,x,y,n+1,ls);
x1,2).quot,div(3*y2-y1,
rotplan(x2,y2,(3*x2-x1) div
2).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); }
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. }
Manual de informatică pentru clasa a XI-a 195

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

ls = 3 ls = 5 ls = 7

Figura 7.6. Exemple de fractali de tip arbore

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.
196 Capitolul 7. Metoda DIVIDE ET IMPERA

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).

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;
}
Manual de informatică pentru clasa a XI-a 197

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 #include <iostream.h>
integer;
int n,i,v[10];
var v:vector; int Suma(int li, int ls)
n,i:integer; { if (li==ls) return v[li];
else return
function
Suma(li, (li+ls)/2)+
Suma(li,ls:integer):integer;
Suma((li+ls)/2+1,ls);
begin }
if li=ls then Suma:=v[li]
main()
else
{ cout<<"n=";
Suma:=Suma(li,(li+ls) div 2)
cin>>n;
+ Suma((li+ls) div 2+1,ls);
for (i=1;i<=n;i++)
end;
cin>>v[i];
begin cout<<Suma(1,n);
write('n='); }
readln(n);
for i:=1 to n do
readln(v[i]);
writeln(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 ] .
198 Capitolul 7. Metoda DIVIDE ET IMPERA

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; li=1;
ls:=n; 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.
199

Capitolul 8
Metoda BACKTRACKING

8.1. Prezentarea metodei

8.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

8.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}.
200 Capitolul 8. Metoda backtracking

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.
Manual de informatică pentru clasa a XI-a 201

 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.

8.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;
202 Capitolul 8. Metoda backtracking

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.


Manual de informatică pentru clasa a XI-a 203

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.
204 Capitolul 8. Metoda backtracking

8.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 8.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 8.1. Exemplu pentru n=4


Manual de informatică pentru clasa a XI-a 205

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 8.2.
206 Capitolul 8. Metoda backtracking

b)
sol(1) = 3 i = 1
sol(3) = 1 j = 3
|sol(i) - sol(j)| = |3 - 1| = 2
|i - j| = |1 - 3| = 2

Figura 8.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.
Manual de informatică pentru clasa a XI-a 207

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?

8.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.
208 Capitolul 8. Metoda backtracking

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:
Manual de informatică pentru clasa a XI-a 209

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.
210 Capitolul 8. Metoda backtracking

8.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 8.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 8.4.
• ţara 5 - culoarea 4.
Manual de informatică pentru clasa a XI-a 211

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.
212 Capitolul 8. Metoda backtracking

 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 8.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?

8.4. Aplicaţii ale metodei backtracking în


combinatorică

8.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;
...
Manual de informatică pentru clasa a XI-a 213

Î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.

8.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.
214 Capitolul 8. Metoda backtracking

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.
Manual de informatică pentru clasa a XI-a 215

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.

8.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, pentru i ∈ A
V[i] = 
0, pentru 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:
216 Capitolul 8. Metoda backtracking

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 cifre 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.
Manual de informatică pentru clasa a XI-a 217

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...

8.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].
218 Capitolul 8. Metoda backtracking

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.
Manual de informatică pentru clasa a XI-a 219

 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.

8.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.
220 Capitolul 8. Metoda backtracking

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}.
Manual de informatică pentru clasa a XI-a 221

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).

8.4.6. Generarea tuturor partiţiilor mulţimii {1, 2, ..., n}

Definiţia 8.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]}.
222 Capitolul 8. Metoda backtracking

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]; }
Manual de informatică pentru clasa a XI-a 223

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.

8.5. Alte tipuri de probleme care se rezolvă prin


utilizarea metodei backtracking

8.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.
224 Capitolul 8. Metoda backtracking

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.

8.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].
Manual de informatică pentru clasa a XI-a 225

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.
226 Capitolul 8. Metoda backtracking

 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.

8.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?
Manual de informatică pentru clasa a XI-a 227

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;
228 Capitolul 8. Metoda backtracking

begin for (i=1;i<=n;i++)


write ('suma='); readln(suma); { cout<<"a["<<i<<"]=";
write('n='); readln(n); cin>>a[i];
for i:=1 to n do }
begin back(1);
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.

8.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).
Manual de informatică pentru clasa a XI-a 229

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;
230 Capitolul 8. Metoda backtracking

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. }
Manual de informatică pentru clasa a XI-a 231

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.

8.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.
232 Capitolul 8. Metoda backtracking

Î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;
t:array [0..10,0..10] int sol[100][3],t[10][10],
of integer; m,n,i,j,lin,col;
m,n,i,j,lin,col:integer;
procedure tipar(k:integer); void tipar(int k)
begin { cout<<"Solutie "<<endl;
for(i=1;i<=k-1;i++)
writeln('Solutie');
cout<<sol[i][1]<<" "
for i:=1 to k-1 do
<<sol[i][2]<<endl;
writeln(sol[i,2],' ',
}
sol[i,3]);
end; void back(int k, int lin, int col)
procedure { if (lin==0 || lin==m+1 ||
back(k,lin,col:integer); col==0 || col==n+1)
begin tipar(k);
if (lin=0) or (lin=m+1) or else
(col=0) or (col=n+1) { sol[k][0]=0;
then tipar(k) sol[k][1]=lin;
else begin sol[k][2]=col;
sol[k,1]:=0; while (sol[k][0]<4)
sol[k,2]:=lin; {
sol[k,3]:=col; sol[k][0]++;
while sol[k,1]<4 do switch(sol[k][0])
begin {
sol[k,1]:=sol[k,1]+1; case 1:
case sol[k,1] of if(t[lin-1][col]<t[lin][col])
1:if t[lin-1,col]< back(k+1,lin-1,col); break;
t[lin,col] then case 2:
back(k+1,lin-1,col); if(t[lin][col+1]<t[lin][col])
2:if t[lin,col+1]< back(k+1,lin,col+1); break;
t[lin,col] then case 3:
back(k+1,lin,col+1); if(t[lin+1][col]<t[lin][col])
3:if t[lin+1,col]< back(k+1,lin+1,col); break;
t[lin,col] then case 4:
back(k+1,lin+1,col); if(t[lin][col-1]<t[lin][col])
4:if t[lin,col-1]< back(k+1,lin,col-1); break;
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;
Manual de informatică pentru clasa a XI-a 233

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.

8.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); #include <stdlib.h>
y:array[1..8] of
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 const int y[8]={2,2,1,-1,-2,-2,
integer; -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;
234 Capitolul 8. Metoda backtracking

begin if (k==n*n)
if k=n*n { for (i=1;i<=k-1;i++)
then cout<<sol[i][0]<<" "
begin <<sol[i][1]<<endl;
for i:=1 to k-1 do cout<<lin<<" "<<col;
writeln(st[i,1],' ', exit(EXIT_SUCCESS);
st[i,2]); }
writeln(lin,' ',col); else
halt; { sol[k][0]=lin;
end sol[k][1]=col;
else for (i=0;i<=7;i++)
begin { linie=lin+x[i];
st[k,1]:=lin; coloana=col+y[i];
st[k,2]:=col; if (linie<=n && linie>=1
for i:=1 to 8 do && coloana<=n &&
begin coloana>=1 &&
linie:=lin+x[i]; t[linie][coloana]==0)
coloana:=col+y[i]; {
if (linie<=n) and t[linie][coloana]=1;
(linie>=1) and back(k+1,linie,coloana);
(coloana<=n) and t[linie][coloana]=0;
(coloana>=1) and
}
(t[linie,coloana]=0)
}
then
}
begin
}
t[linie,coloana]:=1;
back(k+1,linie, main()
coloana);
{ cout<<"n=";
t[linie,coloana]:=0;
cin>>n;
end;
back(1,1,1);
end
}
end
end;

begin
write ('n=');
readln(n);
back(1,1,1);
end.
Manual de informatică pentru clasa a XI-a 235

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.

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.
236 Capitolul 8. Metoda backtracking

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;
 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.
Manual de informatică pentru clasa a XI-a 237

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.

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.
238 Capitolul 8. Metoda backtracking

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.
239

Capitolul 9

Introducere în teoria grafurilor

9.1. Grafuri neorientate

9.1.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 9.1 şi convenim ca o astfel
de reprezentare s-o numim graf neorientat. Figura 9.1.
Exemplu de graf neorientat
240 Capitolul 9. Introducere în teoria grafurilor

Desigur, această abordare este intuitivă. Teoria grafurilor este fundamentată


matematic şi în cele ce urmează ne propunem s-o prezentăm sistematic.

9.1.2. Definiţia grafului neorientat


1
Definiţia 9.1. . Un graf neorientat este o pereche ordonată G=(V,E),
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 9.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
4
Notaţie: în graful G=(V,E), vom nota
cu n numărul nodurilor şi cu m numărul
muchiilor. Figura 9.2.
Alt exemplu de graf neorientat

Observaţii

 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 9.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 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ă.
Manual de informatică pentru clasa a XI-a 241

Definiţia 9.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, iar d(6)=0 (6 este nod izolat).

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 9.1.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.

 Exerciţiu

Daţi un exemplu inspirat din viaţa reală, pentru care să găsiţi graful asociat.
Astfel, veţi răspunde la întrebările: ce semnificaţie au nodurile sau muchiile şi ce
înseamnă gradul unui nod.
242 Capitolul 9. Introducere în teoria grafurilor

9.1.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
2
structură sau alta în funcţie de :
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
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.

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
4 5
3 Figura 9.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

2
Modul de alegere a structurii îl veţi înţelege pe parcursul studiului acestui capitol.
Manual de informatică pentru clasa a XI-a 243

Pentru graful din figura 9.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} .

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:
244 Capitolul 9. Introducere în teoria grafurilor

Varianta Pascal Varianta C++

type mat_ad=array[1..50,1..50] of void CitireN


integer; (char Nume_fis[20],
procedure CitireN int A[50][50], int& n)
(Nume_Fis:string; {
var A:Mat_ad; var n:integer); int i,j;
var f:text; fstreamf (Nume_fis,ios::in);
i,j:byte; f>>n;
begin while (f>>i>>j)
Assign(f,Nume_Fis); A[i][j]=A[j][i]=1;
Reset(f); Readln(f,n); f.close();
while(not eof(f)) do }
begin
readln(f,i,j);
A[i,j]:=1; A[j,i]:=1;
end;
close(f);
end;

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ţă


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 9.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 ->
4
Figura 9.4.
Manual de informatică pentru clasa a XI-a 245

Î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) – reprezintă un nod al listei nodurilor adiacente.
 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

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ţă.


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) va reţine j, pentru că succesorul lui i este j;
3. 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.
246 Capitolul 9. Introducere în teoria grafurilor

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 void Citire_LA_Astatic
lista=array[0..1,1..50]of integer; (char Nume_fis[20],
pornire=array[1..50] of integer; int T[2][50],
... int Start[50], int& n)
procedure Citire_LA_Astatic {
(Nume_fis:string;var T:Lista; int i,j,k=0;
var Start:pornire;var n:integer); fstream
var i,j,k:integer; f(Nume_fis,ios::in);
f:text; f>>n;
begin while (f>>i>>j)
k:=0; { k++;
Assign(f,Nume_Fis); Reset(f); T[0][k]=j;
Readln(f,n); T[1][k]=Start[i];
while(not eof(f)) do Start[i]=k;
begin k++;
readln(f,i,j); k:=k+1; T[0][k]=i;
T[0,k]:=j; T[1,k]:=Start[i]; T[1][k]=Start[j];
Start[i]:=k; k:=k+1; Start[j]=k;
T[0,k]:=i; T[1,k]:=Start[j]; }
Start[j]:=k; f.close();
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; T:lista; int T[2][50],Start[50],
n,i,man:integer; n,i,man;
begin main()
Citire_LA_Astatic {Citire_LA_Astatic
('Graf.txt',T,Start,n); ("Graf.txt",T,Start,n);
for i:=1 to n do for (int i=1;i<=n;i++)
begin {
writeln ('Noduri adiac. cu ',i); cout<<"Noduri adiac. cu "<<
man:=Start[i]; i<<endl;
while (man<>0)do man=Start[i];
begin while (man)
write(T[0,man],' '); { cout<<T[0][man]<<" ";
man:=T[1,man]; man=T[1][man]; }
end; cout<<endl;
writeln }
end; }
end.
Manual de informatică pentru clasa a XI-a 247

C. 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 arbori parţiali). Uneori, va fi necesar să sortăm
muchiile, fie după nodul de pornire, fie după altă informaţie asociată lor.

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 memorat prin lista muchiilor.

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, 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.

9.1.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 grafului. 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 9.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.
248 Capitolul 9. Introducere în teoria grafurilor

Alăturat, aveţi un graf complet cu 4 noduri (K4):


1

2 3

Figura 9.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:

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.

9.1.5. Graf parţial, subgraf

Definiţia 9.5. Un graf parţial al unui graf neorientat dat G=(V,E) este un
graf G1=(V,E1), unde E1⊆E.
Manual de informatică pentru clasa a XI-a 249

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 9.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 G'.

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 9.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 9.7. 1
Obţinerea unui 1
subgraf

2 3 3
rezultă

4 4

G=(V,E) G1=(V1,E1)
250 Capitolul 9. Introducere în teoria grafurilor

 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ă.

9.1.6. Parcurgerea grafurilor neorientate


3
Prin parcurgerea grafurilor înţelegem o modalitate de vizitare a nodurilor
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.

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.

9.1.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.

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 251

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 9.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ă.

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ţă:
252 Capitolul 9. Introducere în teoria grafurilor

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 sf_c=1,T[2][50],Start[50],p;
integer;
T:lista; void bf(int nod)
Start:pornire; { coada[i_c]=nod;
s[nod]=1;
procedure bf(nod:integer); while (i_c<=sf_c)
begin { p=Start[coada[i_c]];
i_c:=1;sf_c:=1; while (p)
coada[i_c]:=nod; { if (s[T[0][p]]==0)
s[nod]:=1; {sf_c++;
while i_c<=sf_c do coada[sf_c]=T[0][p];
begin s[T[0][p]]=1;
p:=Start[coada[i_c]]; }
while p<>0 do p=T[1][p];
begin }
if s[T[0,p]]=0 then cout<<coada[i_c]<<endl;
begin i_c++;
sf_c:=sf_c+1; }
coada[sf_c]:=T[0,p]; }
s[T[0,p]]:=1;
end; main()
p:=T[1,p]; {
end; Citire_LA_Astatic("Graf.txt",T
writeln(coada[i_c]); ,Start,n);
i_c:=i_c+1; bf(6);
end }
end;
begin
Citire_LA_Astatic('Graf.txt',T,
Start,n);
bf(3);
end.

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;
coada,s:array[1..50] of int n, coada[50],s[50],i_c=1,
integer; sf_c=1,A[50][50],i;
A:mat_ad;
void bf(int nod)
procedure bf(nod:integer); {
begin coada[i_c]=nod;
i_c:=1; sf_c:=1; s[nod]=1;
Manual de informatică pentru clasa a XI-a 253

coada[i_c]:=nod; while (i_c<=sf_c)


s[nod]:=1; { i=1;
while (i_c<=sf_c) do while (i<=n)
begin {if (A[coada[i_c]][i]==1
i:=1; && s[i]==0)
while i<=n do { sf_c++;
begin coada[sf_c]=i;
if ((A[coada[i_c],i]=1) s[i]=1;
and (s[i]=0)) }
then i++;
begin }
sf_c:=sf_c+1;; cout<<coada[i_c]<<endl;
coada[sf_c]:=i; i_c++;
s[i]:=1; }
end; }
i:=i+1;
main()
end;
{ CitireN("Graf.txt",A,n);
writeln(coada[i_c]);
bf(1);
i_c:=i_c+1;
}
end
end;
begin
CitireN('Graf.txt',A,n);
bf(1);
end.

9.1.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.

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 9.9.
254 Capitolul 9. Introducere în teoria grafurilor

Exemple de implementări ale parcurgerii DF

1. Programul următor parcurge DF un graf memorat prin liste de adiacenţă:

Varianta Pascal Varianta 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;
procedure df(nod:integer); cout<<nod<<" ";
var p:integer; p=Start[nod];
begin s[nod]=1;
writeln(nod,' '); while (p)
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]);
p:=T[1,p]; main()
end {
end; Citire_LA_Astatic("Graf.txt",
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ţă:

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.
Manual de informatică pentru clasa a XI-a 255

9.1.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.

9.1.7. Lanţuri

Reluăm exemplele de la paragraful 9.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?
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 9.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.
256 Capitolul 9. Introducere în teoria grafurilor

Definiţia 9.8. Se numeşte lanţ elementar un lanţ care conţine numai noduri
distincte.
1
Exemple: pentru graful din figura alăturată:
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 9.10.

 Problema 9.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 nu a fost selectat
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ă nu 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"
var n,a1,b:integer; int
s,T:array[1..50] of integer; s[50],A[50][50],n,T[50],a,b;
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;
Manual de informatică pentru clasa a XI-a 257

procedure df_r(nod:integer); void df_r(int nod)


var k:integer; {
begin int k;
s[nod]:=1; s[nod]=1;
for k:=1 to n do for (k=1;k<=n;k++)
if (A[nod,k]=1) and (s[k]=0) if(A[nod][k]==1 && s[k]==0)
then {
begin T[k]=nod;
T[k]:=nod; df_r(k);
df_r(k); }
end }
end;
main()
begin {
CitireN('Graf.txt',A,n); CitireN("Graf.txt",A,n);
write('a='); readln(a1); cout<<"a="; cin>>a;
write('b='); readln(b); cout<<"b="; cin>>b;
df_r(a1); df_r(a);
if (T[b]<>0) then refac(b); if (T[b]!=0) 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.
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 int n,coada[50],s[50],
n,a1,b,i_c,sf_c,i:integer; i_c=1, sf_c=1,
s,T,coada:array[1..50] of A[50][50],i,T[50],a,b;
integer;
void refac (int nod)
A:mat_ad;
{ if (nod!=0)
procedure refac (nod:integer); { refac (T[nod]);
begin cout<<nod<<" ";
if nod<>0 then }
begin }
refac (T[nod]);
write(nod,' ');
end
end;
258 Capitolul 9. Introducere în teoria grafurilor

procedure bf(nod:integer); void bf(int nod)


begin { coada[i_c]=nod;
i_c:=1; s[nod]=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 T[i]=coada[i_c];
(s[i]=0) }
then i++;
begin }
sf_c:=sf_c+1; i_c++;
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
end; cout<<"b=";
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.

Matricea lanţurilor. Întrebări referitoare la situaţiile prezentate la 9.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ă există lanţ de la i la j


L(i, j) = 
0, în caz contrar
Manual de informatică pentru clasa a XI-a 259

 Problema 9.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?

9.1.8. Graf conex

Revenind la exemplele de la 9.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 9.9. Un graf neorientat G=(V,E) este conex, dacă pentru orice
pereche de noduri x,y∈V, există un lanţ în care extremitatea iniţială este x
şi extremitatea finală este y.

Exemple Figura 9.11.


1
1. Graful alăturat este conex. De exemplu,
între 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ă 2 3
lanţul [3,4,5], ş.a.m.d.

Oricum am alege două noduri, există lanţul cerut de


4 5
definiţie.

2. Graful alăturat nu este conex. De exemplu, între Figura 9.12.


nodurile 1 şi 4 nu există nici un lanţ. 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ţ.
260 Capitolul 9. Introducere în teoria grafurilor

 Problema 9.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. Puteţi
scrie acest program?

9.1.9. Componente conexe


Analizăm, din nou, exemplele de la 9.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.
Fie graful asociat unuia dintre cazurile prezentate. În termeni din teoria
grafurilor, problema se reduce la determinarea nodurilor unei componente conexe.

Definiţia 9.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).
Figura 9.13.
Exemple 4
1
1. Graful alăturat este alcătuit din două componente
conexe. Prima este alcătuită din nodurile 1, 2, 3 şi
muchiile care le unesc pe acestea, a doua este 2 3 5
formată din nodurile 4 şi 5 şi muchia care le uneşte.
Figura 9.14.
2. Graful din figura 9.14. conţine 3 componente 4
conexe. Aşa cum un graf, cu un singur nod, este 1
conex, tot aşa un nod izolat alcătuieşte el singur o
componentă conexă.
2 3 5
Manual de informatică pentru clasa a XI-a 261

Observaţii

1. Câte componente conexe poate avea un graf 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 (de programare).9.4. Fiind dat un graf neorientat, 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;
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[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; main()
{ CitireN("Graf.txt",A,n);
begin for (i=1;i<=n;i++)
CitireN('Graf.txt',A,n); if (s[i]==0)
for i:=1 to n do { cout <<"Comp
if s[i]=0 then conexa"<<endl;
begin df_r(i);
writeln('Comp conexa'); cout<<endl;
df_r(i); }
writeln; }
end
end.
262 Capitolul 9. Introducere în teoria grafurilor

9.1.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 9.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).

Exemple: pentru graful neorientat din Figura 9.15.


figura 9.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 9.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
(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
să se încerce vizitarea nodului 3 de două ori? Pentru că nodul 2 3
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 9.16.
Manual de informatică pentru clasa a XI-a 263

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)
main()
else gasit:=true;
{
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,

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…


264 Capitolul 9. Introducere în teoria grafurilor

9.1.11. Arbori

9.1.11.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
proprietăţile de mai sus, numit arbore.

Definiţia 9.12. Se numeşte arbore un graf


neorientat care este conex şi nu conţine 1 2
cicluri.

În figura 9.17. aveţi un exemplu de arbore cu 5


noduri. 3 4
5
 Problema 9.6. Se citeşte un graf. Să se scrie un
Figura 9.17.
program care verifică dacă este arbore.

 Rezolvare. Conexitatea ştim să o verificăm. Dacă într-o parcurgere, DF (BF) 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(BF). 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 (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;
{
procedure df(nod:integer); int k;
var k:integer; s[nod]=1;
begin for(k=1;k<=n;k++)
s[nod]:=1; if (A[nod][k]==1)
for k:=1 to n do {
if A[nod,k]=1 then A[k][nod]=0;
begin if (s[k]==0) df(k);
A[k,nod]:=0; else gasit=1;
if s[k]=0 then df(k) }
else gasit:=true; }
end
end;
Manual de informatică pentru clasa a XI-a 265

begin main()
CitireN('Graf.txt',A,n); { CitireN("Graf.txt",A,n);
df(1); df(1);
suma:=0; suma=0;
for i:=1 to n do for (i=1;i<=n;i++)
suma:=suma+s[i]; suma+=s[i];
if suma<>n if (suma!=n)
then writeln('Nu este conex') cout<<"Nu este conex";
else else
if gasit if (gasit)
then writeln('Are ciclu') cout<<"Are ciclu";
else writeln('Arbore'); else cout<<"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. Fie G1, G2, …, Gp componentele conexe ale grafului. 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.
266 Capitolul 9. Introducere în teoria grafurilor

9.1.11.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 recondiţionate. Desigur, se doreşte ca repararea şoselelor să se
facă cât mai repede, dar, în acelaşi timp, trebuie ca în timpul 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 9.1. (dată în paragraful anterior), un arbore.
 Problema 9.7. 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ă
posibilitatea ca dintr-un graf conex să se 2 3 2 3
poată obţine mai mulţi arbori parţiali.
Alăturat, puteţi observa un graf
conex (stânga) şi un arbore parţial al său. 4 5 4 5

 Pentru a rezolva problema, vom folosi, Figura 9.18.


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;
void df_r(int nod)
A:mat_ad;
{int k;
procedure df_r(nod:integer);
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.
Manual de informatică pentru clasa a XI-a 267

9.2. Grafuri orientate

9.2.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. În agenda telefonului mobil al fiecărei persoane se găsesc numerele 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 de carte doreşte să prezinte 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 prezentate.
3. Un program este alcătuit din n instrucţiuni, atribuiri, apeluri de subprograme,
afişări, citiri sau 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ă anumite 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 pe agenda telefonică 1
numărul lui j. Pentru exemplul 2, arcul (i,j)
are semnificaţia că este necesară cunoaşterea 6
noţiunii i pentru a putea înţelege noţiunea j. 2
Pentru exemplul 3, arcul (i,j) are semnificaţia
că după executarea instrucţiunii i este posibil să 5
se efectueze instrucţiunea j. 3 4

Procedând aşa cum am arătat, obţinem un Figura 9.19.


Exemplu de graf orientat
graf orientat aşa cum este cel alăturat.
Definiţia 9.13. 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.
268 Capitolul 9. Introducere în teoria grafurilor

De exemplu, pentru graful din figura 9.19., 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 al mulţimii V cu ea însăşi.
4. Grafurile neorientate sunt cazuri particulare
de grafuri orientate, mai precis acele grafuri 1 1
orientate în care pentru orice arc (vi,vj) există
arcul (vj,vi). Alăturat, puteţi observa un graf 2 2
neorientat (stânga) reprezentat ca un graf
orientat (dreapta).
Figura 9.20.
5. Grafurile orientate se mai numesc şi
digrafuri.

Definiţia 9.14. Î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 9.15. Î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).

Definiţia 9.16. Î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: i

d+(i)=3 şi
d-(i)=2. Figura 9.21.
Manual de informatică pentru clasa a XI-a 269

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 pe agenda telefonică numerele a k persoane din grup, iar dacă gradul
interior al nodului i este l înseamnă că numărul persoanei i este pe 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ă 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)
2
O relaţie utilă: avem 4 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 1.
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
grafuri orientate. Adăugăm vârful n+1. Acest vârf 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 .

Definiţia 9.17. Un graf orientat este complet dacă oricare două vârfuri i
şi j (i≠j) sunt adiacente.
270 Capitolul 9. Introducere în teoria grafurilor

n ( n −1)
2
O relaţie utilă: avem 3 grafuri complete. Demonstraţia se face prin inducţie!
Exerciţiu!

9.2.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 9.22.

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) ∈ A


a i, j = 
0, pentru (i, j) ∉ A
0 1 1 0 1 0
0 0 1 0 0 0
Pentru graful din figura 9.22., matricea de adiacenţă
este prezentată alăturat. 
0 0 0 1 0 0
Observaţii  
0 0 0 0 1 0
0 0 0 0 0 0
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 0 0 0 0 0 0
Manual de informatică pentru clasa a XI-a 271

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); {
var f:text; int i,j;
i,j:byte; fstream f(Nume_fis,ios::in);
begin f>>n;
Assign(f,Nume_Fis); while (f>>i>>j) A[i][j]=1;
Reset(f); Readln(f,n); f.close();
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ţă

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)).

Mai jos, puteţi observa subprogramele care construiesc listele de adiacenţă:

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],
var n:integer); int Start[50], int& n)
var i,j,k:integer; {
f:text; int i,j,k=0;
272 Capitolul 9. Introducere în teoria grafurilor

begin fstream f(Nume_fis,ios::in);


k:=0; f>>n;
Assign(f,Nume_Fis); Reset(f); while (f>>i>>j)
Readln(f,n); { k++;
while(not eof(f)) do T[0][k]=j;
begin T[1][k]=Start[i];
readln(f,i,j); Start[i]=k;
k:=k+1; }
T[0,k]:=j; f.close();
T[1,k]:=Start[i]; }
Start[i]:=k;
end;
close(f);
end;

C. Memorarea grafului prin lista arcelor se face la fel ca în cazul grafurilor


neorientate.

9.2.3. Graf parţial, subgraf

Definiţia 9.18. Un graf parţial al unui graf orientat dat 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 9.23.
Obţinerea unui
graf parţial G=(V,A) G1=(V,A1)

Referitor la exemplul 1 din 9.2.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.

 Exerciţiu! Câte grafuri parţiale are un graf cu m arce?

Definiţia 9.19. 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.
Manual de informatică pentru clasa a XI-a 273

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 9.24.
Obţinerea unui G1=(V1,A1)
subgraf G=(V,A)

Referitor la exemplele din paragraful 9.2.1, avem:


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?

9.2.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ă feluri, î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.
 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.
274 Capitolul 9. Introducere în teoria grafurilor

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.

Reluăm exemplele din paragraful 9.2.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ă ajungă 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ă execuţia 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 6
asemenea, dacă după ce se execută instrucţiunea
i, se ajunge să se execute din nou instrucţiunea i,
atunci programul conţine structuri repetitive, iar
7
graful asociat conţine circuite.
Aşa cum am definit pentru grafuri neorientate
matricea lanţurilor, similar, se poate forma pentru Figura 9.25.
grafuri orientate matricea drumurilor:
1, dacă există drum de la i la j
D(i, j) = 
0, în caz contrar
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.
Manual de informatică pentru clasa a XI-a 275

9.2.5. Graf tare conex. Componente tare conexe

Reluăm exemplul 1 din paragraful 9.2.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ânire ş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 j la i.
Un graf cu această proprietate se numeşte graf tare conex.

Definiţia 9.20. 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 9.21. Subgraful G1=(V1,A1) al grafului G=(V,A) este 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 componente


5 tare conexe:
1 4 - subgraful care conţine vârfurile:
1 2 3
- subgraful care conţine vârfurile:
5 7
7
2 3
- subgraful care conţine vârful 4
6 - subgraful care conţine vârful 6

Figura 9.26.

 Problema 9.8. 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.
276 Capitolul 9. Introducere în teoria grafurilor

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

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
Manual de informatică pentru clasa a XI-a 277

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;
n,nrc,i,j:integer; void df_r1(int nod)
{ 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)
procedure df_r2(nod:byte); { int k;
var k:byte; pred[nod]=nrc;
begin for (k=1;k<=n;k++)
pred[nod]:=nrc; if (A[k][nod]==1 &&
for k:=1 to n do pred[k]==0)
if (A[k,nod]=1) and (pred[k]=0) df_r2(k);
then df_r2(k); }
end;
main()
begin {
CitireO('Graf.txt',A,n); CitireO("Graf.txt",A,n);
nrc:=1; nrc=1;
for i:=1 to n do for (i=1;i<=n;i++)
if suc[i]=0 if (suc[i]==0)
then { suc[i]=nrc;
begin df_r1(i);
suc[i]:=nrc; df_r2(i);
df_r1(i); for (j=1;j<=n;j++)
df_r2(i); if(suc[j]!=pred[j])
for j:=1 to n do suc[j]=pred[j]=0;
if suc[j]<>pred[j] then nrc++;
begin }
suc[j]:=0; for (i=1;i<nrc;i++)
pred[j]:=0; {
end; cout<<"Componenta"<<i<<endl;
nrc:=nrc+1 for (j=1;j<=n;j++)
end; if (suc[j]==i)
for i:=1 to nrc-1 do cout<<j<<" ";
begin cout<<endl;
writeln ('Componenta ',i); }
for j:=1 to n do }
if suc[j]=i
then write (j,' ');
writeln;
end;
end.
278 Capitolul 9. Introducere în teoria grafurilor

Şi în cazul grafurilor orientate se păstrează noţiunea de graf conex şi noţiunea


de componentă conexă.

Definiţia 9.22. Graful orientat G=(V,A) este conex dacă ∀x,y∈V,


∃ lanţ de la x la y.

Definiţia 9.23. 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 îndepli-
neşte condiţia 1.

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.
Manual de informatică pentru clasa a XI-a 279

1. Orice firmă colaborează cu toate a. Există un lanţ de la i la j.


celelalte.
b. Graful asociat are un nod de
2. Anumite firme îşi întrerup relaţia grad n-1.
de colaborare.
c. Graful asociat se transformă
3. Anumite firme dau faliment şi nu într-un graf parţial al său.
mai funcţionează.
d. Graful asociat este complet.
4. O firmă colaborează cu toate
e. Graful asociat se transformă
celelalte.
într-un subgraf al său.
5. Cu ajutorul unor firme interme-
diare dintre cel n firme, firma i poate
face afaceri cu firma j.

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 se 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 1
2
alte persoane din grup.

Exerciţiile 8 şi 9 se referă la graful din 3


5
figura 9.27. 4
6
8. Care este matricea de adiacenţă a 7
grafului? Figura 9.27.

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)
280 Capitolul 9. Introducere în teoria grafurilor

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.


9.2. [1,7,4] este un lanţ.
1
9.3. [2,1,4,3,1] este un ciclu. 2

9.4. Nodul 2 este izolat.


3
9.5. Graful din problemă are ca graf parţial graful 4
din figura 9.28. 6
9.6. Graful din problemă are ca subgraf graful din 7
figura 9.28. Figura 9.28.

9.7. Nodul 1 are gradul 2.


9.8. Nodul 4 are gradul 3. 1 2 3 4
2 1
9.9. Graful din problemă conţine un
ciclu de lungime 3. 3 1 4
9.10. Graful alăturat, reprezentat cu 4 1 3
ajutorul listelor de adiacenţă, este
Figura 9.29.
subgraf al grafului din problemă.

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 9.30.

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)
Manual de informatică pentru clasa a XI-a 281

12. Câte muchii are graful neorientat reprezentat de matricea de 0 1 0 1 1


 
adiacenţă alăturată? 1 0 0 1 1
0 0 0 1 1
13. Care este numărul maxim de componente conexe pe care le  
1 1 1 0 1
poate avea un graf neorientat cu 5 noduri şi 4 muchii? 1
 1 1 1 0 
14. Care dintre matricele de mai jos este matricea de
adiacenţă a unui subgraf al grafului din figura 9.31?

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 
Figura 9.31.
a) b) c) d)

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 false. 1 3
Toate afirmaţiile se referă la graful din 2
figura 9.32.
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 9.32.

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 minim de muchii care trebuie eliminate pentru a obţine un arbore
parţial este 2.
19.7. Numărul maxim de muchii care pot fi eliminate astfel încât graful să rămână
conex este 3.
19.8. Numărul minim de muchii care pot fi eliminate pentru ca graful să nu conţină
cicluri este 3.
19.9. Un arbore parţial al grafului dat are 7 muchii.
282 Capitolul 9. Introducere în teoria grafurilor

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. Orice graf neorientat are un graf parţial care este arbore.
20.3. Cu ajutorul parcurgerii în lăţime se poate determina dacă un graf este conex.
20.4. Orice graf neorientat cu 10 noduri şi 9 muchii este un arbore.

20.5. Un arbore conţine exact o componentă conexă.


20.6. Un graf este alcătuit din două componente conexe. Pentru ca graful să devină
conex, este suficient să eliminăm o anumită muchie.
20.7. Un graf este alcătuit din două componente conexe. Fiecare dintre ele
alcătuieşte un graf parţial al grafului dat.
20.8. Un graf este alcătuit din două componente conexe. Fiecare dintre ele
alcătuieşte un subgraf al grafului dat.
20.9. Cu ajutorul parcurgerii în lăţime se poate determina, dacă există, un lanţ între
două noduri ale grafului.
20.10. Cu ajutorul parcurgerii în adâncime se poate determina, dacă există, un lanţ
între două noduri ale grafului.
20.11. Există graf complet cu n>2 noduri care nu conţine cicluri.

20.12. Orice graf complet este alcătuit dintr-o singură componentă conexă.
20.13. Din orice graf complet, prin eliminarea anumitor muchii se poate obţine un
arbore.
20.14. Orice graf complet are un subgraf care este arbore.
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 scrie un subprogram care transformă listele de adiacenţă î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ă static.
Manual de informatică pentru clasa a XI-a 283

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∈ mulţimii 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 prin citirea 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ţă 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.
33. Într-un grup de n persoane, anumite persoane, împrumută altor persoane 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 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.
284 Capitolul 9. Introducere în teoria grafurilor

34. 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.

35. Refaceţi problema anterioară în cazul în care se consideră n


numere naturale şi relaţia de divizibilitate. De asemenea, încercaţi 1
să adăugaţi noi situaţii în care se cere corespondenţa.
36. Câte componente conexe şi câte componente tare conexe
conţine graful din figura 9.33? 3
a) 1 1; b) 1 3; c) 1 0; d) 0 0.
2

Figura 9.33.

37. În graful din figura 9.34, 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

2
Figura 9.34.

Problemele de la 38 la 41 se referă la graful alăturat:


5
38. Câte circuite conţine? 1
a) 3; b) 2; c) 1; d) 4.
39. Câte componente tare conexe conţine? 4 3
a) 4; b) 3; c) 2; d) 1.
40. Care este nodul cu grad interior maxim şi care 2
este nodul cu grad exterior minim?
a) 1 1; b) 1 2; c) 2 2; d) 2 5. Figura 9.35.
Manual de informatică pentru clasa a XI-a 285

41. 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.
42. 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 este dat prin
matricea de adiacenţă şi în cazul în care el este dat prin liste de adiacenţă.
43. 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 este dat prin matricea de
adiacenţă şi în cazul în care el este dat prin liste de adiacenţă.
44. La fel ca mai sus, dar se cere să se determine dacă succesiunea respectivă este
sau nu lanţ (lanţ elementar).
45. Se dă un graf prin lista muchiilor. Programul va decide dacă graful este
neorientat.
46. Se dau listele de adiacenţe ale unui graf orientat. Programul va afişa matricea de
adiacenţă.
47. Se dă matricea de adiacenţă a unui graf orientat. Programul va afişa listele de
adiacenţe ale acestuia.
48. Se dă matricea de adiacenţă a unui graf orientat. Se cere să se listeze toate
circuitele de lungime 3.
49. Se dă matricea de adiacenţă a unui graf orientat. Se cere să se listeze toate
ciclurile de lungime 3.
*
50 . 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ă.
*
51 . 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ă.
52 . 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 perechile de coordonate ale obstacolelor.
*
53 . 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
286 Capitolul 9. Introducere în teoria grafurilor

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.

Exemplu:
3 Programul va afişa:
3 1 2 1 2
2 2 3
2 3
1 2
*
54 . 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. Aceasta înseamnă 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.
Manual de informatică pentru clasa a XI-a 287

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ă.
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 doagonala 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, să-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 1 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. F, 19.7. A,
19.8. A, 19.9. F.
20. 20.1. A, 20.2. F, 20.3. A, 20.4. F, 20.5. A, 20.6. F, 20.7. F,
20.8. A, 20.9. A, 20.10. A, 20.11. F, 20.12. A, 20.13. A, 20.14. A
(lăsăm un singur nod).
29., 30. Descompunerea unui graf în componente conexe.
31. Backtracking. O soluţie are lungimea k.
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
288 Capitolul 9. Introducere în teoria grafurilor

Astfel, în matricea de adiacenţă se formează un dreptunghi. Trebuie


identificate toate dreptunghiurile astfel formate.
33. 1-b, 2-a.
34. 1-d, 2-b, 3-a, 4-c.
36. b).
37. a).
38. c).
39. b).
40. c).
41. a).

50. Se poate lucra direct pe matricea L. Idee: pentru camera iniţială vom avea
L(i,j)=1. Apoi, 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.

51. 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ă.

52. Algoritmul lui Lee.

53. 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.
289

Anexa 1
Memento

A.1. Limbajul Pascal

A.1.1. Tipuri standard

A) Tipuri întregi

Ocupă
Nume tip Semnificaţie Valori admise
(biţi)
shortint întreg scurt 8 de la -128 la 127
integer întreg 16 de la -32768 la 32767

longint 32 de la -2147483648 la
întreg lung
2147483647
byte număr natural scurt 8 de la 0 la 255
word cuvânt 16 de la 0 la 65535

B) Tipul caracter - un caracter se notează între apostrofuri.


Exemple: 'a','A','1' (caracterul '1' nu trebuie confundat cu numărul 1).

C) Tipuri reale

Ocupă
Nume tip Semnificaţie Valori admise (în modul)
(biţi)
virgulă mobilă, [-1,7×1038, -2,9×10-29] ∪
real 48
simplă precizie [2,9×10-29, 1,7×1038]

virgulă mobilă, [-3,4×1038, -1,5×10-45] ∪


single 32
simplă precizie [1.5×10-45, 3,4×1038]

double virgulă mobilă, [-1,7×10308, -5×10-324] ∪


64
dublă precizie [5×10-324, 1,7×10308]

virgulă mobilă, [-1,1×104932, -3,4×10-4932]


extended 80
format lung ∪ [3,4×10-4932, 1,1×104932]
comp virgulă mobilă 64 [-9,2×1018, 9,2×1018]

D) Tipul logic – boolean – poate reţine doar două valori true şi false.
290 Anexa 1. Memento

A.1.2. Constante

A) Constante întregi. Sunt alcătuite dintr−o submulţime a numerelor întregi care


pot fi reprezentate în memoria calculatorului. Se pot reprezenta numere întregi
cuprinse în intervalul: [−2.147.483.648, 2.147.483.647].

B) Constante reale. Sunt alcătuite dintr−o submulţime a numerelor reale (mai


precis, a numerelor raţionale) care pot fi reprezentate în calculator. Modulul
numerelor reale se găseşte în intervalul [3.4∗10 ,1.1∗10 ]. În locul virgulei se
-4352 4932

foloseşte punctul.
Exemple: 2.34, −45.26, 512E+23, −45.1E−3.

Ultimele două numere folosesc o scriere neîntâlnită în matematică. Ele reprezintă


numerele 512∗10 şi −45.1∗10 .
23 -3

C) Constante şir de caractere. După cum reiese şi din denumire, cu ajutorul lor
se reprezintă şiruri de caractere. Caracterele din şir pot fi specificate enumerându-le
între apostrofuri.
Exemplu: 'abc'.

D) Constantele simbolice. Acestea sunt constante care au în program un anumit


nume. Definirea constantelor simbolice precede definirea tipurilor utilizator şi se
realizează astfel:
const identificator = expresie;
...
identificator = expresie;

Expresiile care intervin în definirea constantelor trebuie să poată fi evaluate


la momentul compilării programului.
Exemplu:
const NrMaxValori = 30;
Dim = NrMaxValori*2-1;
Mesaj = 'Nu exista solutie'#10#13;
PI = 3.14;

A.1.3. Operatori
A.1.3.1. Prioritatea operatorilor

Iată cele 4 grupe de prioritate.

Grupa 1 (prioritate maximă):


NOT, + (operator unar), - (operator unar).
Grupa 2 (operatorii din grupa doi se mai numesc şi operatori multiplicativi):
AND, *, /, DIV, MOD.
Manual de informatică pentru clasa a XI-a 291

Grupa 3 (operatorii din această grupă se numesc şi operatori aditivi):


OR, XOR, +, -.

Grupa 4 (operatori cu cea mai mică prioritate - cuprinde şi operatorii relaţionali):


<, <=, >, >=, =,<>.

A.1.3.2. Operatori aritmetici

Operatorii aritmetici sunt de două feluri:

• operatori unari (+ şi -);


• operatori binari ( +, -, *, /, DIV, MOD).

 Operatorul +. Are semnificaţia de adunare. Operanzii săi sunt de tip întreg


sau real. Se poate ca unul să fie de tip întreg şi celălalt de tip real. Dacă cel
puţin unul din operanzi este, real rezultatul este de tip real, altfel este de
tip întreg.

Operatorul + apare şi ca operator unar. De asemenea, apare ca sumă de


şiruri (caz pe care nu-l discutăm acum).

 Operatorul -. Are semnificaţia de scădere. Operanzii sunt de tip întreg sau


real. Dacă cel puţin un operand este real, rezultatul este de tip real, altfel
este de tip întreg.

 Operatorul *. Are semnificaţia de înmulţire. Operanzii sunt de tip întreg sau


real. Dacă cel puţin unul din operanzi este de tip real, rezultatul este de tip
real, altfel rezultatul este de tip întreg.

 Operatorul /. Are semnificaţia de împărţire. Operanzii pot fi de tip întreg sau


real dar, întotdeauna rezultatul este de tip real.

 Operatorul DIV. Are semnificaţia de împărţire întreagă. Operanzii sunt în


mod obligatoriu de tip întreg. Este obligatoriu ca fiecare operand să fie
separat cu cel puţin un spaţiu de operator. Rezultatul este de tip întreg.

Operatorul DIV furnizează rezultat corect numai dacă ambele valori sunt
numere întregi pozitive.

Rezultatul pentru operatorul DIV se obţine astfel:

• se face împărţirea întreagă a celor două numere care sunt considerate


pozitive (de exemplu, 13 div 4=3);

• semnul câtului se stabileşte după regula semnelor (+ cu + rezultat +, +


cu - rezultat -, - cu - rezultat + şi - cu + rezultat -).
292 Anexa 1. Memento

 Operatorul MOD. Are semnificaţia de rest al împărţirii pentru numere întregi.


Operanzii sunt în mod obligatoriu de tip întreg, iar rezultatul va fi întotdeauna
de tip întreg. Operanzii trebuie separaţi de operator prin cel puţin un spaţiu.

Ca şi în cazul operatorului DIV, rezultatul este corect numai dacă ambii


operanzi sunt pozitivi. Fie a MOD b. Rezultatul se obţine astfel:
a - a DIV b (R=D-Î×C).

A.1.3.3. Operatori relaţionali

 Operatorul < (mai mic). Fiind daţi doi operanzi a şi b, operatorul < arată
dacă este adevărată sau nu relaţia a<b. Dacă relaţia este adevărată
rezultatul va fi TRUE, altfel rezultatul va fi FALSE.

 Operatorul <= (mai mic sau egal). Fiind daţi doi operanzi a şi b, operatorul
<= arată dacă este adevărată sau nu relaţia a<=b. Dacă relaţia este
adevărată rezultatul va fi TRUE, altfel rezultatul va fi FALSE.

 Operatorul > (mai mare). Fiind daţi doi operanzi a şi b, operatorul > arată
dacă este adevărată sau nu relaţia a>b. Dacă relaţia este adevărată
rezultatul va fi TRUE, altfel rezultatul va fi FALSE.

 Operatorul >= (mai mare sau egal). Fiind daţi doi operanzi a şi b, operatorul
>= arată dacă este adevărată sau nu relaţia a>=b. Dacă relaţia este
adevărată rezultatul va fi TRUE, altfel rezultatul va fi FALSE.

 Operatorul = (egal). Fiind daţi doi operanzi a şi b, operatorul = arată dacă


este adevărată sau nu relaţia a=b. Dacă relaţia este adevărată rezultatul va
fi TRUE, altfel rezultatul va fi FALSE.

A.1.3.4. Operatori logici

 Operatorul NOT (negare). Modul de acţiune se poate observa mai jos:


NOT (TRUE)=FALSE;
NOT (FALSE)=TRUE.

 Operatorul AND (şi). Regula de obţinere a rezultatului este foarte simplă:


rezultatul este TRUE numai dacă ambii operanzi au valoarea TRUE (altfel
rezultatul este FALSE).

 Operatorul OR (sau). Regula este simplă: dacă unul dintre operanzi este
TRUE, rezultatul este TRUE, altfel rezultatul este FALSE.

 Operatorul XOR (sau exclusiv). Şi aici, regula este foarte simplă: dacă
argumentele sunt diferite rezultatul este TRUE, contrar el este FALSE.
Manual de informatică pentru clasa a XI-a 293

Dacă operanzii sunt de tip întreg, se pleacă de la reprezentarea binară a


numerelor.

 Operatorul NOT (negare) este unar. Transformă toţi biţii 1 în 0 şi invers.

 Operatorul AND (ŞI) este binar. Se face ŞI logic pentru toate perechile de
biţi aflaţi pe aceeaşi poziţie a celor doi operatori. Dacă ambii biţi sunt 1,
rezultatul este 1, iar în orice alt caz, rezultatul este 0.

 Operatorul OR (SAU) este binar. Se face SAU logic pentru toate perechile de
biţi aflaţi pe aceeaşi poziţie a celor doi operatori. Dacă cel puţin un bit este 1
rezultatul este 1, altfel, rezultatul este 0.

 Operatorul XOR (SAU EXCLUSIV) este binar. Se face XOR pentru toate
perechile de biţi aflaţi pe aceeaşi poziţie a celor doi operatori. Dacă biţii sunt
diferiţi rezultatul este 1, altfel, rezultatul este 0.

A.1.4. Instrucţiuni

1. Instrucţiunea vidă. Nu se trece nimic, dar totuşi există. O succesiune de


separatori ';' indică prezenţa mai multor instrucţiuni vide.

2. Instrucţiunea de atribuire este de forma: v:=expresie, unde v este numele


unei variabile.

Principiul de executare este următorul:


• se evaluează expresia;
• variabila v ia această valoare.

Regula fundamentală este următoarea: tipul expresiei trebuie să coincidă cu


tipul variabilei v.

3. Instrucţiunea IF. Există două forme ale acestei instrucţiuni:

Forma 1.

IF expresie logică THEN instrucţiune1


ELSE instrucţiune2

Principiul de executare este următorul:

• se evaluează expresia logică;


• dacă aceasta ia valoarea TRUE, se execută instrucţiunea plasată după
THEN, în caz contrar se execută instrucţiunea plasată după ELSE.
294 Anexa 1. Memento

Forma 2.

IF expresie logică THEN instrucţiune.

Principiul de executare este următorul:

• se evaluează expresia logică;


• în situaţia în care aceasta are valoarea TRUE, se execută instrucţiunea
aflată după THEN, în caz contrar se trece la instrucţiunea următoare.

4. Instrucţiunea compusă. Se utilizează pentru a putea scrie mai multe


instrucţiuni care vor fi interpretate de compilator ca una singură. Instrucţiunile se
scriu între begin şi end.

5. Instrucţiunea CASE. Corespunde structurii alternative multiple.

Forma generală a instrucţiunii CASE este:


CASE expresie ordinală OF
c1,[c2,...,cn1]: instrucţiune1;
p1,[p2,...,pn2]: instrucţiune2;
z1,[z2,...,znp]: instrucţiunep
[ELSE instrucţiune]
END

Aici, c1,...,cn1,...,znp reprezintă constante de acelaşi tip ca şi expresia ordinală.

Principiul de executare este următorul:

• se evaluează expresia ordinală;


• se execută acea instrucţiune care are în faţă constanta obţinută în
evaluarea expresiei;
• în situaţia în care nici una din instrucţiunile 1...p nu este precedată
de acea constantă, se execută instrucţiunea plasată după ELSE;
• dacă şi clauza ELSE este absentă, se trece la instrucţiunea următoare.

6. Instrucţiunea WHILE. Reproduce structura Cât timp... execută.

Forma generală este:


WHILE expresie logică DO instrucţiune

Principiul de executare este următorul:

• se evaluează expresia logică şi în caz că aceasta are valoarea TRUE


se execută instrucţiunea, se evaluează din nou expresia, dacă aceasta
are valoarea TRUE se execută din nou instrucţiunea; procedeul
continuă până când, la evaluarea expresiei, se obţine FALSE.
Manual de informatică pentru clasa a XI-a 295

7. Instrucţiunea REPEAT. Această instrucţiune reproduce structura REPEAT


UNTIL şi are forma generală:
REPEAT
i1;
i2;
...
in
UNTIL expresie logică
Aici, i1, i2, ..., in reprezintă instrucţiuni.

Principiul de executare este următorul:


• se execută secvenţa de instrucţiuni;
• se evaluează expresia logică;
• dacă aceasta ia valoarea FALSE se execută din nou secvenţa de
instrucţiuni, contrar se trece mai departe.

8. Instrucţiunea FOR. Atunci când cunoaştem de câte ori se execută o secvenţă


este bine să se utilizeze instrucţiunea FOR. Ea are două forme, prezentate în
continuare.

Forma 1.
FOR variabilă := expresie1 TO expresie2 DO instrucţiune
unde:
− variabila poate fi de orice tip ordinal (de exemplu, de tip integer, char
sau boolean, dar în nici un caz de tipul real);
− expresie1, expresie2 sunt expresii de acelaşi tip cu variabila.

Principiul de executare este următorul:


Pasul 1. Se evaluează cele două expresii.
Pasul 2.
Pasul 2.1.
• Dacă valoarea obţinută în urma evaluării expresiei 1 este strict mai
mare decât valoarea obţinută în urma evaluării expresiei 2,
executarea FOR este încheiată;
• Dacă valoarea obţinută în urma evaluării expresiei 1 este egală cu
valoarea obţinută în urma evaluării expresiei 2, se atribuie variabilei
de ciclare valoarea obţinută în urma evaluării expresiei 1, se execută
instrucţiunea subordonată şi executarea FOR este încheiată;
• Dacă valoarea obţinută în urma evaluării expresiei 1 este strict mai
mică decât valoarea obţinută în urma evaluării expresiei 2, se
atribuie variabilei de ciclare valoarea obţinută în urma evaluării
expresiei 1 şi se trece la 2.2.
296 Anexa 1. Memento

Pasul 2.2. Se execută instrucţiunea subordonată.


Pasul 2.3.
• Dacă valoarea reţinută de variabila de ciclare este strict mai mică
decât valoarea obţinută în urma evaluării expresiei 2 (evaluare
efectuată la început) se adună 1 variabilei de ciclare şi se trece
la 2.2.
• Dacă valoarea reţinută de variabila de ciclare este egală cu valoarea
obţinută în urma evaluării expresiei 2, executarea instrucţiunii FOR
se încheie.

Forma 2.

FOR variabilă := expresie1 DOWNTO expresie2 DO instrucţiune

În acest caz, variabila de ciclare scade la fiecare pas.

A.1.5. Câteva funcţii utile

 sinus:ℜ→[-1,1]; are forma generală:

function Sin(X: Real): Real;

 cosinus:ℜ→[-1,1]; are forma generală:

function Cos(X: Real): Real; {cosinus (x)}

 arctangent:ℜ→(-π/2,π/2); are forma generală:

function ArcTan(X: Real): Real; {arctangent (x)}

Atenţie: argumentul trebuie exprimat în radiani. Celelalte funcţii uzuale


se obţin prin aplicarea formulelor trigonometrice.

 Funcţia exponenţială f:ℜ→ℜ+, unde f(x)=ex (e este un număr iraţional,


e≈2.71) are forma generală:

function Exp(X: Real): Real;

 Funcţia logaritmică f:ℜ+→ℜ, unde f(x)=ln(x) este funcţia inversă funcţiei


exponenţiale şi are forma generală:
function Ln(X: Real): Real;

Funcţia logaritmică are următoarele proprietăţi:

pentru A, B > 0, avem : ln(A ⋅ B) = ln(A) + ln(B); ln(AB ) = B ⋅ ln(A).


Manual de informatică pentru clasa a XI-a 297

Din faptul că funcţia logaritmică este inversa funcţiei exponenţiale şi din a


doua relaţie de mai sus, deducem pentru x>0, y oarecare:

x y = eln(x ) = e y⋅ln(x) .
y

Aceasta înseamnă că putem calcula xy - pentru că funcţia putere nu există


în Pascal: exp(y*ln(x)).

 Funcţia pătratică f:ℜ→ℜ+, unde f(x)=x2:

function Sqr(X: Real): Real;

unde x este o valoare întreagă sau reală.

 Funcţia radical f:ℜ+→ℜ+, unde: f(x) = x , are forma generală:

function Sqrt(X: Real): Real;

 Funcţia „parte întreagă”


function Int(X: Real): Real;

returnează partea întreagă a lui x.

Atenţie: dacă X este negativ, rezultatul este eronat.


Exemple: int(2.75) returnează 2.0, int(-2.75) returnează -2.0.

 Funcţia de trunchiere

function Trunc(X: Real): Longint;

returnează valoarea trunchiată a argumentului.

 Funcţia „parte fracţionară”

function Frac(X: Real): Real;

returnează X-Int(X).

 Funcţia de rotunjire
function Round(X: Real): Longint;

returnează valoarea rotunjită a lui x.

 Funcţia „valoare absolută”


function Abs(X);

returnează x .
298 Anexa 1. Memento

A.2. Limbajul C++


A.2.1. Tipuri standard
Limbajul C++, în varianta Borland C++, admite tipurile de mai jos:

A) Tipuri întregi

Ocupă
Nume tip Semnificaţie Valori admise
(biţi)
unsigned
char caracter fără semn 8 de la 0 la 255

char caracter 8 de la -128 la 127


unsigned
int întreg fără semn 16 de la 0 la 65535

int întreg 16 de la -32768 la 32767


unsigned întreg lung fără 32 de la 0 la 4.294.967.295
long semn

32 de la -2.147.483.648 la
long întreg lung cu semn 2.147.483.647

B) Tipuri reale

Ocupă
Nume tip Semnificaţie Valori admise (în modul)
(biţi)
virgulă mobilă,
float 32 [3.4 ×10-38,3.4×1038]
simplă precizie
virgulă mobilă,
double 64 [1.7×10-308 ,1.7×10+308]
dublă precizie
long virgulă mobilă,
dublă precizie [3.4×10-493 , 1.1 ×10+4932]
2
double 80
format lung

A.2.2. Constante
1. Constante întregi. Acestea se clasifică astfel:
• zecimale (în baza 10). Exemple: 23, 1239, 56.
• octale (în baza 8). O constantă în baza 8 se declară precedată de un 0
nesemnificativ. Exemplu: 0123. Se reţine numărul întreg 123(8).
• hexazecimale (în baza 16). Acestea sunt precedate de 0X sau 0x.
Exemplu: pentru 0X1A2 adică 1A2(16) sau 0x1a2, adică 1A2(16).
Manual de informatică pentru clasa a XI-a 299

2. Constante caracter. Acestea se trec între două caractere apostrof (').

Exemple: 'A', '1', 'a'.

3. Secvenţe escape. O secvenţă escape începe prin caracterul '\' (backslash).


Să considerăm o constantă caracter 'a'. Codul său este 97(10)=141(8)=61(16).
Printr-o secvenţă escape, constanta se introduce prin codul său într-una din bazele
8 sau 16. De exemplu, constanta 'a' poate fi scrisă (echivalent) astfel: '\141'
sau '\x61'. În cazul când se foloseşte codul scris în baza 16, acesta este
precedat de caracterul 'x'. Uneori, pentru anumite caractere, se pot utiliza şi semne
speciale, aşa cum rezultă din exemplele următoare.
• backslash: '\\','\134','\x5c';
• newline: '\n','\12','\xa';
• apostrof: '\'','\47','\x27';
• bel: '\a','\7','\x7';
• cr: '\r','\15','\xd'.

4. Caractere albe (whitespaces). Au un rol special în cadrul operaţiilor de


citire/scriere. Acestea sunt:
• blank (' ');
• tab orizontal ('\t');
• newline ('\n');
• tab vertical ('\v');
• cr ('\r').

5. Constante reale

Exemple: -45.66, 1., .2, 0.3, -2.5E-12, adică, 2.5×10-12.

6. Constante şir de caractere


Exemplu: "'acesta este un text".

Pentru a da un nume constantelor folosim const. Forma generală a unei


astfel de declaraţii este (construcţia dintre paranteze drepte este opţională):

const [tip] nume=valoare;


unde:

• tip - reprezintă tipul constantei (dacă este absent, tipul este int);
• nume - reprezintă numele constantei;
• valoare - reprezintă valoarea constantei.
300 Anexa 1. Memento

A.2.3. Operatori

A.2.3.1. Prioritatea operatorilor (în ordine descrescătoare)

Priviţi următorul tabel:

prioritate operator asociativitate

1 ()[]− >::. s →d
2 !~ +−+ +− −*(typecast )sizeof newdelete d →s
3 . *− > * s →d
4 */ % s →d
5 +− s →d
6 <<>> s →d
7 <<=>>= s →d
8 ==! = s →d
9 & s →d
10 ^ s →d
11 | s →d
12 && s →d
13 || s →d
14 ?: d →s
15 =* =/ =+ =− =& =^ =|=<<=>>= d →s
16 , s →d

A.2.3.2. Operatori aritmetici

În C++ există următorii operatori aritmetici:

• - minus (unar, adică acţionează asupra unui singur operand);


• + plus (unar);
• + (binar), pentru adunare;
• - (binar), pentru scădere;
• * (binar), are semnificaţia de înmulţire;
• / (binar), pentru împărţire;
• % (binar), restul împărţirii întregi.
Manual de informatică pentru clasa a XI-a 301

Observaţii

1. Operatorul ”/” (împărţire) acţionează în mod diferit în funcţie de operanzi:

a) dacă ambii sunt de tip întreg, rezultatul este întreg şi are semnificaţia de
împărţire întreagă. Cu toate acestea, rezultatul este corect (din punct de
vedere matematic) numai dacă valorile care se împart sunt pozitive.

b) dacă cel puţin un operand este de unul din tipurile reale, rezultatul este
real (se efectuează împărţirea obişnuită).
2. Operatorul ”%” acţionează numai asupra operanzilor de tip întreg. Rezultatul
obţinut este corect din punct de vedere matematic numai dacă ambii
operanzi sunt numere naturale.
3. În cazul în care se împart două valori întregi, se procedează astfel:
a) se face împărţirea întreagă a celor două valori care sunt considerate în
modul;
b) semnul câtului se stabileşte după regula semnelor (+ cu + rezultat +,
+ cu -, rezultat -), etc.

A.2.3.3. Operatori relaţionali

În C++ există următorii operatori relaţionali:

• < (mai mic);


• <= (mai mic sau egal);
• > (mai mare);
• >= (mai mare sau egal).
Rezultatul unei operaţii logice este 1, în cazul în care inegalitatea este
respectată şi 0, în caz contrar.

A.2.3.4. Operatori de egalitate

Aceştia sunt:

• == pentru egalitate;

• != pentru inegalitate.

În cazul în care relaţia indicată de operator este respectată, expresia


returnează 1, altfel returnează 0.
302 Anexa 1. Memento

A.2.3.5. Operatori de incrementare şi decrementare

Aceşti operatori sunt unari şi au rolul de a incrementa (adună 1) sau


decrementa (scad 1) conţinutul unei variabile. Operatorii sunt:
• ++ pentru incrementare;
• -- pentru decrementare.

Operatorii pot fi prefixaţi (aplicaţi în faţa operandului) sau postfixaţi (aplicaţi după
operand).

 Dacă operatorul este prefixat, variabila este incrementată (decrementată)


înainte ca valoarea reţinută de ea să intre în calcul.
 Dacă operatorul este postfixat, variabila este incrementată (decrementată)
după ce valoarea reţinută de ea intră în calcul.

A.2.3.6. Operatori logici

Există trei operatori logici:


• ! - negare logică;
• && - şi logic;
• || - sau logic.
Operatorul negare logică acţionează astfel: dacă operandul este o valoare
diferită de 0, rezultatul este 0, altfel rezultatul este 1.

Operatorul şi logic (binar) acţionează astfel: dacă ambii operanzi sunt diferiţi
de 0, rezultatul este 1, altfel el este 0.

Operatorul sau logic (binar) acţionează astfel: dacă cel puţin unul din
operanzi este o valoare diferită de 0, rezultatul este 1, altfel rezultatul este 0.

A.2.3.7. Operatori logici pe biţi

Limbajul C++ este dotat cu un set de operatori care permit accesul la bit.
Aceştia sunt:
• <<, >> operatori de deplasare;
• & şi pe biţi;
• | sau pe biţi;
• ^ sau exclusiv pe biţi;
• ~ negare pe biţi (operator unar).

Aceşti operatori acţionează numai asupra operanzilor de tip întreg.


Manual de informatică pentru clasa a XI-a 303

Operatorul ”<<” este binar. El are rolul de a deplasa către stânga conţinutul
tuturor biţilor operandului din stânga sa, cu un număr de poziţii egal cu valoarea
reţinută de al doilea operand. Poziţiile rămase libere (în dreapta) vor reţine valoarea 0.

Operatorul ”>> ” este binar. El are rolul de a deplasa către dreapta conţinutul
tuturor biţilor operandului din stânga cu un număr de poziţii egal cu valoarea reţinută
de al doilea operand. Dacă operandul din stânga este de un tip întreg fără semn,
poziţiile rămase libere (în stânga) vor reţine valoarea 0. Dacă al doilea operand
reţine valoarea m, o astfel de deplasare este echivalentă cu împărţirea întreagă cu
2m. În cazul în care primul operand este un întreg cu semn, fiecare poziţie din
stânga rămasă liberă se completează cu valoarea reţinută de bitul de semn.
În cazul operatorilor binari ”& ”, ”|” , ”^”, rezultatul se obţine aplicând pentru
fiecare pereche de biţi aflaţi pe aceeaşi poziţie regulile din tabelul următor. Atunci
când cei doi operanzi nu au aceeaşi lungime (dar numai atunci - de exemplu, dacă
ambii operanzi sunt de tip char şi rezultatul este de tip char), se aplică regulile de
conversie pentru expresii aritmetice.

OP1 OP2 OP1&OP2 OP1^OP2 OP1|OP2


0 0 0 0 0
1 0 0 1 1
0 1 0 1 1
1 1 1 0 1

Operatorul ”~” (negare pe biţi) are rolul de a inversa conţinutul biţilor (dacă
un bit conţine 0, va conţine 1 şi invers).

A.2.3.8. Operatori de atribuire

În C++ atribuirea este operator. În plus, în C++ avem mai mulţi operatori de
atribuire. Operatorul ”=” se foloseşte într-o expresie de forma:
v=expresie
Aici, v este o variabilă.

Principiul de executare este următorul:

• se evaluează expresia;

• variabilei v i se atribuie valoarea obţinută (dacă este cazul, se


efectuează conversia respectivă).
Se pot efectua şi atribuiri multiple de forma:
v=v1=v2=...=vn=expresie
unde v, v1, …, vn sunt variabile.
304 Anexa 1. Memento

În acest caz, principiul de executare este următorul:


• se evaluează expresia;
• valoarea obţinută este atribuită variabilei vn (eventual convertită - dacă
este cazul);
• conţinutul variabilei vn este atribuit variabilei vn-1 (eventual, se
efectuează conversia necesară);
• ...
• conţinutul variabilei v1 este atribuit variabilei v (eventual, se efectuează
conversia necesară).
Pentru atribuiri se mai pot utiliza şi operatorii: ”*=”, ”/=”, ”%=”, ”+=”, ”-=”,
”<<=”, ”>>=”, ”&>”, ”^=” sau ”|=”.
O atribuire de forma: v op expresie, are acelaşi rezultat ca
v=v op expresie
(diferenţa este că, în primul caz, se generează un cod maşină eficient).

A.2.3.9. Operatorul ',' (virgulă)

C++ permite programatorilor să scrie mai multe expresii separate prin


virgulă, ca mai jos:
exp1,exp2, ..., expn;

Întrucât, după cum rezultă din tabel, operatorul virgulă se asociază de la


stânga la dreapta, expresiile se evaluează în ordinea exp1, exp2,..., expn. S-a
convenit ca întreaga expresie (care cuprinde cele n expresii separate prin virgulă)
să producă ca rezultat valoarea obţinută în urma evaluării ultimei expresii (evident,
tipul acestei valori este şi tipul expresiei).

A.2.3.10. Operatorul condiţional

Se foloseşte în expresii de genul:


exp1?exp2:exp3
Principiul de executare este următorul:
• se evaluează exp1;
• dacă aceasta produce o valoare diferită de 0, se evaluează exp2 şi
exp3 este ignorată (nu se evaluează);
• altfel, se evaluează exp3 şi exp2 este ignorată.

În ansamblu, expresia este de tipul lui exp2 sau exp3 şi produce valoarea
exp2 sau exp3 (în funcţie de cea care se evaluează).
Manual de informatică pentru clasa a XI-a 305

A.2.3.11. Operatorul sizeof

Are rolul de a returna numărul de octeţi utilizaţi pentru memorarea unei


valori. Operatorul sizeof poate fi utilizat într-una din cele două forme prezentate
în continuare:
sizeof (expresie)
sizeof (tip)

A.2.3.12. Operatorul de conversie explicită

De multe ori, dorim ca unul sau mai mulţi operanzi să intre în calcul convertiţi
aşa cum dorim (nu implicit). Pentru aceasta, înaintea operandului se trece între
paranteze tipul său.
Exemplu: fie declaraţia: float x= -1.9;. Atunci: (int)x=-1 (se face
conversia din float în int prin trunchiere).

A.2.4. Instrucţiuni
1. Instrucţiunea expresie este de forma:
expresie;.
La întâlnirea unei astfel de instrucţiuni, se evaluează expresia. În limbajul
C++ şi o atribuire este o expresie.

2. Instrucţiunea ”if” se poate utiliza în următoarele două forme:

Forma 1.
if (expresie) instrucţiune1 else instrucţiune2

Principiul de executare este următorul:


• se evaluează expresia;
• dacă valoarea produsă de aceasta este diferită de 0, se execută
instrucţiune1;
• dacă valoarea produsă este 0 se execută instrucţiune2.

Forma 2.
if (expresie) instrucţiune
Principiul de executare este următorul:
• se evaluează expresia;
• dacă valoarea produsă de aceasta este diferită de 0, se execută
instrucţiunea subordonată.
306 Anexa 1. Memento

3. Instrucţiunea compusă. Se utilizează în cazul în care se doreşte ca mai multe


instrucţiuni să fie tratate de compilator ca o singură instrucţiune. Este de forma de
mai jos, unde i1, i2, ..., in sunt instrucţiuni:
{ i1;
i2;
:
:
in;
}

4. Instrucţiunea ”switch” are forma generală:

switch (expresie)
{ case exp1: secvenţă instrucţiuni1; break;
case exp2: secvenţă instrucţiuni2; break;
.................................
case expn: secvenţă instrucţiunin; break;
[default: secvenţă instrucţiunin+1];
}
unde:
− expresie are semnificaţia: expresie de tip întreg;
− expi sunt expresii constante de tip întreg;
− instrucţiunii reprezintă o secvenţă oarecare de instrucţiuni.

Principiul de executare:

• se evaluează expresia;
• dacă aceasta produce o valoare egală cu cea produsă de expi, se
execută, în ordine, instrucţiunii şi se trece la instrucţiunea următoare,
altfel se execută numai secvenţa instrucţiunin+1.

Alternativa default este facultativă. În absenţă, în cazul în care nu există


coincidenţă de valori, se trece la instrucţiunea următoare.

5. Instrucţiunea ”while”

Această instrucţiune reproduce structura de tip Cât timp ... execută.

Forma generală este:


while (expresie) instrucţiune

Principiul de executare este următorul:


• pasul 1: se evaluează expresia;
• pasul 2: dacă valoarea produsă de aceasta este diferită de 0, se
execută instrucţiunea subordonată, apoi se revine la pasul 1, altfel se
trece la instrucţiunea următoare.
Manual de informatică pentru clasa a XI-a 307

6. Instrucţiunea ”do while”. Traduce în limbaj structura Execută...cât


timp. Forma generală a acestei instrucţiuni este următoarea:
do
instrucţiune
while(expresie);

Principiul de executare este următorul:


• pasul 1: se execută instrucţiunea subordonată;
• pasul 2: se evaluează expresia. În cazul în care valoarea produsă la
evaluare este 0, execuţia instrucţiunii do se termină, altfel se trece
la pasul 1.

7. Instrucţiunea ”for” are forma generală:

for (expresieiniţializare; expresietest; expresieincrementare) instrucţiune

După cum se observă, între paranteze se găsesc trei expresii:


− expresieinitializare se foloseşte de regulă, pentru iniţializarea variabilei de
ciclare. Este de remarcat faptul că în cadrul acestei expresii (cu rol special)
este posibil chiar să declarăm variabila de ciclare (cu valoare iniţială).
− expresietest se foloseşte pentru a testa dacă se execută instrucţiunea
subordonată - dacă expresia produce la evaluare o valoare diferită de 0,
instrucţiunea subordonată for se execută.
− expresieincrementare se foloseşte pentru incrementarea variabilei de ciclare.

Principiul de executare:

• pasul 1: se evaluează expresieiniţializare (un caz special este acela în


care aceasta conţine şi declaraţia variabilei de ciclare);
• pasul 2: se evaluează expresiatest. În cazul în care aceasta produce o
valoare diferită de 0, se execută instrucţiunea subordonată for; apoi se
trece la pasul 3, altfel se trece la instrucţiunea următoare (se termină
execuţia instrucţiunii for).
• pasul 3: se evaluează expresia de incrementare şi se revine la pasul 2.

A.2.5. Câteva funcţii utile

Pentru a le utiliza, includeţi fişierul math.h: #include <math.h>.

 Funcţia abs are forma generală: int abs(int x); Rolul ei este de a
întoarce x (modulul lui x).
 Funcţia fabs are forma generală double fabs(double x); are acelaşi
rol cu abs, numai că întoarce valoarea unui număr real (chiar double).
308 Anexa 1. Memento

 Funcţia labs are forma generală long int labs(long int x); şi
acelaşi rol cu abs, numai că întoarce valoarea unui întreg lung.
 Funcţia acos are forma generală: double acos(double x); şi
calculează valoarea funcţiei arccos( x) : [−1,1] → [0, π ].

 Funcţia asin are forma generală: double asin(double x); şi


π π
calculează valoarea funcţiei arcsin( x) : [−1,1] → [− , ].
2 2
 Funcţia atan are forma generală: double atan(double x); şi
π π
calculează valoarea funcţiei arctg ( x) : ℜ → (− , ).
2 2
 Funcţia atan2 are forma generală: double atan2(double y,double x)
y
şi calculează arctg ( ). Rezultatul este în intervalul (−π , π ). Motivul?
x
 Funcţia floor are forma generală double floor(double x); şi
calculează valoarea rotunjită a lui x (rotunjirea se face în minus).
Exemple: floor (123.78)=123, floor (-23,34)=-24.
 Funcţia ceil are forma generală double ceil(double x); şi calculează
valoarea rotunjită a lui x (rotunjirea se face în plus).
Exemple: ceil(123.78)=124, ceil(-23,34)=-23.
 Funcţia cos are forma generală double cos(double x); şi calculează
valoarea funcţiei cos( x) : ℜ → [−1,1].

 Funcţia sin are forma generală double sin(double x); şi calculează


valoarea funcţiei sin( x) : ℜ → [−1,1].

 Funcţia tan are forma generală double tan(double x); şi calculează


 π 
valoarea funcţiei tg ( x) : ℜ − k ⋅ π + k ∈ Ζ → ℜ.
 2 

 Funcţia exp are forma generală double exp(double x); şi calculează


funcţia e x : ℜ → ℜ *+ .

 Funcţia log are forma generală double log(double x); şi calculează


funcţia ln( x) : ℜ *+ → ℜ, unde ln( x) = log e ( x) .

 Funcţia log10 are forma generală double log10(double x); şi


calculează funcţia lg( x) : ℜ *+ → ℜ, unde lg( x) = log 10 ( x).

 Funcţia pow are forma generală double pow(double x, double y);


şi calculează x y .
309

Anexa 2
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


310 Anexa 2 - Aplicaţii practice ale grafurilor

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
Manual de informatică pentru clasa a XI-a 311

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,
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.
312 Anexa 2 - Aplicaţii practice ale grafurilor

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.
Î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


Manual de informatică pentru clasa a XI-a 313

Î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.

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).
314 Anexa 2 - Aplicaţii practice ale grafurilor

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“.

A.3. Chimie molecularǎ

Ştiinţa care se ocup ǎ cu studiul moleculelor se


numeşte chimie molecularǎ. Presupunându-se cunoscute
elementele teoretice de baz ǎ, considerǎm cǎ o moleculǎ
reprezintǎ cea mai micǎ particulǎ a unei substanţe chimice
ce reţine toate proprietǎţile sale chimice şi de compoziţie. O
moleculǎ este formatǎ din cel puţin doi atomi şi este neutrǎ
din punct de vedere electric. Formula chimicǎ şi structura unei molecule reprezintǎ
cei mai importanţi factori care-i determinǎ proprietǎţile.

ǎ se numesc
În chimie, grafurile ce descriu topologia molecular grafuri
moleculare. Dupǎ cum era de aşteptat, nodurile rep rezintǎ atomii, iar arcele
semnificǎ legǎturile dintre atomi.

Mai jos, este prezentat un exemplu de graf molecular neorientat pentru o


hidrocarburǎ (lipC4):

5 7

2 1

3 4

Figura A.4. Exemplu de graf molecular asociat

Pentru graful neorientat prezentat anterior, se pot asocia urmǎtoarele trei matrice:
Manual de informatică pentru clasa a XI-a 315

0 1 0 1 1 0 0 0 1 2 1 1 2 2 0 3 2 3 1 2 2
     
1 0 1 0 0 0 0 1 0 1 2 2 3 3 3 0 3 2 4 5 5
0 1 0 1 0 0 0 2 1 0 1 3 4 4 2 3 0 3 3 4 4
     
1 0 1 0 0 0 0 1 2 1 0 2 3 3 3 2 3 0 4 5 5
1
 0 0 0 0 1 1  1
 2 3 2 0 1 1 
1
 4 3 4 0 1 1 
0 0 0 0 1 0 0 2 3 4 3 1 0 2 2 5 4 5 1 0 2
     
0 0 0 0 1 0 0 2 3 4 3 1 2 0 2 5 4 5 1 2 0

matricea de adiacenţǎ matricea drumurilor matricea drumurilor


minime (distanţa) maxime (Detour)

Observăm faptul că acest graf asociat conţine cicluri, lucru obişnuit în


structurile moleculare. Drumurile maxime sunt totuşi determinate, cu
precizarea că în programele specializate se evită ciclarea algoritmului prin
utilizarea unei condiţii de stop.
Dupǎ ce au fost determinate aceste matrice, mai multe caracteristici
topologice importante pot fi obţinute direct. Prezentǎm doar douǎ dintre ele:

- indicele de drum (Detour) – se obţine din matricea drumurilor maxime:


n n

∑∑ (∆)
1
ω= ⋅ ij ,
2 i =1 j =1

unde (∆) ij este un element al matricei Detour.

- indicele Weiner – introdus în anul 1947 de chimistul Harry Weiner pentru


a studia structura molecularǎ:
n n

∑∑ (d )
1
W (G ) = ⋅ ij ,
2 i =1 j =1

unde (d ) ij reprezintǎ un element al matricei drumurilor minime.

Existǎ mai mult de 400 de astfel de indici topologici şi sunt folosiţi în


determinarea similaritǎţ ilor structurale între molecule, extrem de utile în analiza
moleculară.
Exemplul prezentat anterior este foarte simplu, dar imaginaţi-vǎ cât de uşor
poate fi pentru un chimistǎ sanalizeze aceste date, pentru o formulǎ chimicǎ
complexǎ, simulând totul direct pe calculator… Teoria grafurilor este folositǎ cu
succes în chimie şi genetic
ǎ, iar simularea experimentelor cu aj utorul unui PC
diminueazǎ considerabil timpul necesar de lucru.
316

Anexa 3
Tabela codurilor ASCII
Cod Caracter Cod Caracter Cod Caracter Cod Caracter Cod Caracter Cod Caracter
000 (nul) 022 ▀ (syn) 044 , 066 B 088 X 110 n
001 ☺ (soh) 023 ¥ (etb) 045 - 067 C 089 Y 111 o
002 ☻ (stx) 024 ↑ (can) 046 . 068 D 090 Z 112 p
003 ♥ (etx) 025 ↓ (em) 047 / 069 E 091 [ 113 q
004 ♦ (eot) 026 → (eof) 048 0 070 F 092 \ 114 r
005 ♣ (enq) 027 ← (esc) 049 1 071 G 093 ] 115 s
006 ♠ (ack) 028 ⌐ (fs) 050 2 072 H 094 ^ 116 t
007 • (bel) 029 ↔ (gs) 051 3 073 I 095 _ 117 u
008 _ (bs) 030 ▲ (rs) 052 4 074 J 096 ` 118 v
009 □ (tab) 031 ▼ (us) 053 5 075 K 097 a 119 w
010 ◙ (lf) 032 (spaţiu) 054 6 076 L 098 b 120 x
011 ♂ (vt) 033 ! 055 7 077 M 099 c 121 y
012 ♀ (np) 034 " 056 8 078 N 100 d 122 z
013 ♪ (cr) 035 # 057 9 079 O 101 e 123 {
014 ♫ (so) 036 $ 058 : 080 P 102 f 124 |
015 ☼ (si) 037 % 059 ; 081 Q 103 g 125 }
016 ► (dle) 038 & 060 < 082 R 104 h 126 ~
017 ◄ (dc1) 039 ' 061 = 083 S 105 i 127
018 ↨ (dc2) 040 ( 062 > 084 T 106 j
019 ‼ (dc3) 041 ) 063 ? 085 U 107 k
020 ¶ (dc4) 042 * 064 @ 086 V 108 l
021 § (nak) 043 + 065 A 087 W 109 m

Codul ASCII extins


Cod Caracter Cod Caracter Cod Caracter Cod Caracter Cod Caracter Cod Caracter
128 Ç 149 ò 170 ¬ 191 ┐ 212 ╘ 233 Θ
129 ü 150 û 171 ½ 197 └ 213 ╒ 234 Ω
130 é 151 ù 172 ¼ 193 ┴ 214 ╓ 235 δ
131 â 152 _ 173 ¡ 194 ┬ 215 ╫ 236 ∞
132 ä 153 Ö 174 « 195 ├ 216 ╪ 237 ∅
133 à 154 Ü 175 » 196 ─ 217 ┘ 238 ∈
134 å 155 ¢ 176 ░ 197 ┼ 218 ┌ 239 ∩
135 ç 156 £ 177 ▒ 198 ╞ 219 █ 240 ≡
136 ê 157 ¥ 178 ▓ 199 ╟ 220 ▄ 241 ±
137 ë 158 _ 179 │ 200 ╚ 221 ▌ 242 ≥
138 è 159  180 ┤ 201 ╔ 222 ▐ 243 ≤
139 ï 160 á 181 ╡ 202 ╩ 223 ▀ 244 ⌠
140 î 161 í 182 ╢ 203 ╦ 224 α 245 ⌡
141 ì 162 ó 183 ╖ 204 ╠ 225 ß 246 ÷
142 Ä 163 ú 184 ╕ 205 ═ 226 Γ 247 ≈
143 Å 164 ñ 185 ╣ 206 ╬ 227 π 248 °
144 É 165 Ñ 186 ║ 207 ╧ 228 Σ 249 •
145 æ 166 ª 187 ╗ 208 ╨ 229 σ 250 −
146 Æ 167 º 188 ╝ 209 ╤ 230 µ 251 √
147 ô 168 ¿ 189 ╜ 210 ╥ 231 τ 252 ⁿ
148 ö 169 _ 190 ╛ 211 ╙ 232 φ 253 ²
254  255

S-ar putea să vă placă și