Professional Documents
Culture Documents
PREFAŢĂ.................................................................................Pag. 2
LISTE SIMPLU ÎNLĂNŢUITE.............................................Pag. 5
LISTE CIRCULARE SIMPLU ÎNLĂNŢUITE....................Pag. 15
LISTE DUBLU ÎNLĂNŢUITE..............................................Pag. 22
ARBORI...................................................................................Pag. 30
ARBORI BINARI DE CĂUTARE..........................................Pag. 39
REPREZENTAREA ŞI TRAVERSAREA
GRAFURILOR........................................................................Pag. 58
ALGORITMI PENTRU PRELUCRAREA
GRAFURILOR........................................................................Pag. 63
TABELE DE DISPERSIE...................................................... Pag. 69
METODE GENERALE DE ELABORARE A
ALGORITMILOR (I).............................................................Pag. 75
METODE GENERALE DE ELABORARE A
ALGORITMILOR (II)............................................................Pag. 87
METODE GENERALE DE ELABORARE A
ALGORITMILOR (III)..........................................................Pag. 95
ALGORITMI FUNDAMENTALI DE SORTARE...............Pag. 104
BIBLIOGRAFIE......................................................................Pag. 115
PREFAŢĂ
3
4
Lucrarea de laborator nr. 1.
1. Conţinutul lucrării
2. Consideraţii teoretice
5
Fig. 2.1. Model de listă simplu înlănţuită
Pointerii prim şi ultim vor fi declaraţi astfel:
TIP_NOD *prim, *ultim;
prim = 0; ultim = 0;
n=sizeof(TIP_NOD);
p=(TIP_NOD *)malloc(n); /* rezervare spaţiu de
memorie în heap*/
citire date în nodul de adresă p;
6
2.2 Accesul la un nod al unei liste simplu înlănţuite
if (prim == 0) {
prim=p; ultim=p; p->urm=0;
}
7
b) după ultimul nod:
if (ultim != 0) {
p -> urm = 0; ultim -> urm = p; ultim = p;
}
c) înaintea unui nod precizat printr-o cheie “key”:
- se caută nodul de cheie “key”:
TIP_NOD *q, *q1;
q1=0; q=prim;
while(q!=0)
{
if(q->cheie==key) break;
q1=q; q=q->urm;
}
- se inserează nodul de pointer p, făcând legăturile
corespunzătoare:
TIP_NOD *q;
q=prim;
while(q!=0) {
if(q->cheie==key) break;
q=q->urm;
}
8
- se inserează nodul de adresă p, făcând legăturile
corespunzătoare:
9
b) Ştergerea ultimului nod
TIP_NOD *q, *q1;
q1=0; q=prim;
if(q!=0) { /* lista nu este vidă */
while(q!=ultim)
{
q1=q; q=q->urm;
}
if(q==prim) {
prim=0; ultim=0;
}
else {
q1->urm=0; ultim=q1;
}
elib_nod(q);
}
c) Ştergerea unui nod de cheie “key”
TIP_NOD *q, *q1;
/* căutare nod */
q1=0; q=prim;
while (q!=0)
{
if(q->cheie == key) break; /* s-a găsit nodul */
q1=q; q=q->urm;
}
if(q != 0) { /* există un nod de cheie “key” */
if (q == prim) {
prim=prim_>urm;
elib_nod(q);
/*eliberare spaţiu */
if( prim==0) ultim=0;
}
else {
q1->urm=q->urm;
if(q==ultim) ultim=q1;
elib_nod(q); /* eliberare spaţiu */
}
10
2.5 Ştergerea unei liste simplu înlănţuite
În acest caz, se şterge în mod secvenţial fiecare nod:
TIP_NOD *p;
while( prim != 0) {
p=prim; prim=prim->ultim;
elib_nod(p);
/*eliberare spaţiu de memorie */
}
ultim=0;
2.6 Stive
Stiva este o listă simplu înlănţuită bazată pe algoritmul LIFO (Last In
First Out), adică ultimul nod introdus este primul scos. Modelul stivei, care
va fi avut în vedere în continuare, este prezentat în fig.2.6.1.
11
2.7. Cozi
Coada este o listă simplu înlănţuită, bazată pe algoritmul FIFO (First In
First Out), adică primul element introdus este primul scos. Modelul cozii
care va fi avut în vedere în consideraţiile
următoare, este prezentat în fig.2.7.1.
Se introduce
un element
nou
3. Mersul lucrării
3.1.Să se definească şi să se implementeze funcţiile pentru structura de
date
typedef stuct {
int lungime;
struct TIP_NOD *inceput, *curent, *sfarşit;
} LISTA;
având modelul din fig.3.1.
12
Fig.3.1.Modelul listei pentru problema 3.1
13
că elementul x precede elementul y. Să se afişeze elementele mulţimii M
într-o anumită ordine, încât pentru orice elemente x, y cu proprietatea că x
precede pe y, elementul x să fie afişat înaintea lui y.
14
Lucrarea de laborator nr. 2.
1. Conţinutul lucrării
În lucrare sunt prezentate principalele operaţii asupra listelor circulare
simplu înlănţuite: crearea, inserarea unui nod, ştergerea unui nod şi
ştergerea listei.
2. Consideraţii teoretice
Lista circulară simplu înlănţuită este lista simplu înlănţuită a cărei
ultim element este legat de primul element; adică ultim -> urm = prim.
În cadrul listei circulare simplu înlănţuite nu există capete. Pentru
gestionarea ei se va folosi un pointer ptr_nod, care adresează un nod
oarecare al listei, mai precis ultimul introdus(fig.2.1.).
15
Structura unui nod este următoarea:
ptr_nod = 0;
/* crearea nodului */
n = sizeof(TIP_NOD); /* dimensiunea nodului */
p = (TIP_NOD *)malloc(n); /* rezervarea memorie în
heap */
citire date în nod la adresa p;
if (ptr_nod = = 0) { /* lista este vidă */
ptr_nod = p;
ptr_nod -> urm = p;
}
else { /* lista nu este vidă */
p -> urm = ptr_nod -> urm;
ptr_nod -> urm = p;
ptr_nod=p; /* ptr_nod pointează la
ultimul nod inserat */
}
16
2.2. Accesul la un nod
Nodurile pot fi accesate secvenţial plecând de la nodul de pointer
ptr_nod:
p=ptr_nod;
if(p! = 0) /* lista nu este vidă */
do {
acceseaază nodul şi preia informaţia;
p = p -> urm;
}
while (p! = ptr_nod);
sau căutând un nod de cheie dată key; în acest caz o funcţie care va returna
pointerul la nodul găsit va conţine următoarea secvenţă de program:
p = ptr_nod;
if (p! = 0) /* lista nu este vidă */
do {
if ( p -> cheie == key)
{
/* s-a găsit nodul */
/* nodul are adresa p */
return p;
}
p = p -> urm;
}
while (p! = ptr_nod);
return 0;
17
În ambele cazuri se caută nodul de cheie dată având adresa q; dacă
există un astfel de nod ,se creează nodul de inserat de adresă p şi se fac
legăturile corespunzătoare.
a) Inserarea înaintea unui nod de cheie dată
se caută nodul de cheie dată (adresa sa va fi q):
TIP_NOD *p,*q,*q1;
q = ptr_nod;
do {
q1 = q; q = q -> urm;
if (q -> cheie = =key ) break; /* s-a găsit nodul */
}
while (q! = ptr_nod);
TIP_NOD *p,*q;
q = ptr_nod;
do {
if (q -> cheie == key ) break;
q = q -> urm;
}
while(q!=ptr_nod);
18
se inserează nodul de adresă p :
if (q -> cheie == key) {
p -> urm =q -> urm;
q -> urm = p;
}
2.4. Ştergerea unui nod de cheie dată
Ştergerea unui nod de cheie dată key se va face astfel:
se caută nodul de cheie dată:
q = ptr_nod;
do {
q1 = q; q = q -> urm;
if (q -> cheie == key ) break;
/* s-a găsit nodul */
}
while (q! = ptr_nod);
se şterge nodul, cu menţiunea că dacă se
şterge nodul de pointer ptr_nod, atunci ptr_nod va pointa spre nodul
precedent q1:
if (q-> cheie == key)
{
if (q==q -> urm) ptr_nod==0;
/* lista a devenit vidă */
else {
q1 -> urm = q -> urm;
if (q == ptr_nod) ptr_nod = q1;
}
elib_nod(q);
}
19
2.5. Ştergerea listei
Ştergerea listei circulare simplu înlănţuite se va face astfel:
p = ptr_nod;
do {
p1 =p; p = p -> urm;
elib_nod(p1);
}
while (p! = ptr_nod);
ptr_nod = 0;
3. Mersul lucrării
3.1. Să se definească şi să se implementeze funcţiile pentru structura de
date:
struct LISTA_CIRC {
int lungime;
struct TIP_NOD *început; *curent;
}
având modelul din fig. 3.1.
20
copil, se numără copiii în sensul acelor de ceasornic. Fiecare al n-lea copil
iese din cerc .Câştigă ultimul copil rămas în joc.
3.3. Să se implementeze un buffer circular, care conţine înregistrări cu
datele unui student şi asupra căruia acţionează principiul de sincronizare
producător-consumator, care constă în următoarele:
a) înregistrările sunt preluate în ordinea producerii lor;
b) dacă bufferul nu conţine înregistrări, consumatorul este
întârziat până când producătorul depune o înregistrare;
c) dacă bufferul este plin, producătorul este întârziat până
când consumatorul a preluat o înregistrare.
21
Lucrarea de laborator nr. 3.
1. Conţinutul lucrării
2. Consideraţii teoretice
Lista dublu înlănţuită este lista dinamică între nodurile căreia s-a
definit o dublă relaţie: de succesor si de predecesor.
Modelul listei dublu înlănţuite, pentru care se vor da explicaţiile în
22
Ca şi la lista simplu înlănţuită, principalele operaţii sunt:
• crearea;
• accesul la un nod;
• inserarea unui nod;
• ştergerea unui nod,
• ştergerea listei.
Lista dublu înlănţuită va fi gestionată prin pointerii prim şi ultim:
TIP_NOD *prim, *ultim;
prim -> prec = 0;
ultim -> urm = 0;
23
2.2. Accesul la un nod
Accesul la un nod se poate face:
secvenţial înainte (de la „prim” spre „ultim”):
p = prim;
while (p != 0) {
vizitare nod de pointer p;
p = p -> urm;
}
secvenţial înapoi ( de la „ultim” spre „prim”):
p = ultim;
while (p != 0) {
vizitare nod de pointer p;
p = p -> prec;
}
• pe baza unei chei. Căutarea unui nod de cheie dată
key se va face identic ca la lista simplu înlănţuită (lucrarea 1, par. 2.2.).
24
) după ultimul nod:
if (prim == 0) { /* lista este vidă */
prim = p; ultim = p;
p -> urm = 0; p -> prec = 0;
}
else { /* lista nu este vidă /*
p -> urm =0; p -> prec = ultim; utim -> urm =
p;
ultim = p;
}
) înaintea unui nod de cheie dată key:
După căutarea nodului de cheie key, presupunând că acesta
există şi are adresa q, nodul de adresă p va fi inserat astfel:
25
2.4. Ştergerea unui nod
Există următoarele cazuri de ştergere a unui nod din listă:
a) ştergerea primului nod; acest lucru se poate face cu
secvenţa de program:
p = prim;
prim = prim -> urm; /* se consideră listă nevidă */
elib_nod(p); /* eliberarea nodului */
if (prim == 0) ultim = 0; /* lista a devenit vidă */
else prim -> prec = 0;
}
else if(p == prim) { /* se şterge primul nod */
prim = prim -> urm; prim -> prec =0;
elib_nod(p);
}
else if (p == ultim) { /* se şterge ultimul nod */
ultim = ultim -> prec;
ultim -> urm = 0;
elib_nod(p)
}
else { /* nodul de şters este diferit de capete */
26
p -> urm -> prec = p -> prec;
p -> prec -> urm = p -> urm;
elib_nod(p);
}
3. Mersul lucrării
3.1. Să se definească şi să se implementeze funcţiile pentru
structura de date:
struct LISTA {
int lungime;
struct TIP_NOD *început, *curent, *sfârşit;
}
având modelul din fig.3.1.
27
3.2. Să se scrie funcţiile pentru realizarea operaţiilor de creare,
inserare, ştergere pentru o listă dublu înlănţuită circulară având modelele din
fig.3.2.
28
Lucrarea de laborator nr. 4.
ARBORI
1. Conţinutul lucrării
În lucrare sunt prezentate operaţiile de bază asupra arborilor binari,
binari total echilibraţi şi arborilor oarecare.
2. Consideraţii teoretice
Arborele binar, foarte des întâlnit în aplicaţii, este arborele în care
orice nod are cel mult doi descendenţi: fiul stâng şi fiul drept.
ABD*G***CE**F*H**
Fig. 2.1.1. Arbore binar.
29
typedef struct tip_nod {
char ch; /* identificatorul nodului */
informaţie;
struct tip_nod *stg, *dr;
} TIP_NOD;
TIP_NOD *construire( )
{
TIP_NOD *p;
int n;
char c;
n=sizeof(TIP_NOD);
/* citire caracter de identificare nod */
scanf(“%c”, c);
if(c==’*’) return 0;
else {
/* construire nod de adresă p */
p=(TIP_NOD *)malloc(n);
/* introducere de informaţie în nod */
p->ch=c;
p->stg=construire( );
p->dr=construire( );
}
return p;
}
Apelul funcţiei se va face astfel:
rădăcina = construire ( )
Traversarea unui arbore binar se poate face în cele 3 moduri
cunoscute: preordine, inordine, postordine.
În programul următor sunt implementate operaţiile de construcţie şi
traversare a unui arbore binar. Nodul conţine numai identificatorul său.
Afişarea nodurilor vizitate se face cu indentare.
30
/* Program de construire şi afişare a arborilor binari */
#include <stdio.h>
#include <conio.h>
#include <alloc.h>
typedef struct tip_nod{
int nr.; /*informaţie */
struct tip_nod *stg,*dr;
} TIP_NOD;
TIP_NOD *rad;
void preordine(TIP_NOD *p, int nivel)
{
int i;
if (p!=0){
for(i=0;i<=nivel;i++) printf(" ");
printf("%2d\n",p->nr);
preordine(p->stg,nivel+1);
preordine(p->dr,nivel+1);
}
}
void inordine(TIP_NOD *p, int nivel)
{
int i;
if (p!=0){
inordine(p->stg,nivel+1);
for(i=0;i<=nivel;i++) printf(" ");
printf("%2d\n",p->nr);
inordine(p->dr,nivel+1);
}
}
void postordine(TIP_NOD *p, int nivel)
{
int i;
if (p!=0){
postordine(p->stg,nivel+1);
postordine(p->dr,nivel+1);
for(i=0;i<=nivel;i++) printf(" ");
printf("%2d\n",p->nr);
}
}
31
TIP_NOD *constructie()
{
TIP_NOD *p;
int inf,n;
n=sizeof(TIP_NOD);
printf("\nIntroduceti Inf.din nod inf=");
scanf("%d",&inf);
if(inf==0) return 0;
else {
p=(TIP_NOD *)malloc(n);
p->nr=inf;
p->stg=constructie();
p->dr=constructie();
}
return p;
}
void main(void)
{
rad=constructie();
printf("\nVIZITAREA IN PREORDINE\n");
preordine(rad,0);
getch();
printf("\nVIZITAREA IN INORDINE\n");
inordine(rad,0);
getch();
printf("VIZITAREA IN POSTORDINE\n");
postordine(rad,0);
getch();
}
2.7 Arbori binari total echilibraţi
Un arbore binar total echilibrat este un arbore binar care îndeplineşte
următoarea condiţie: numărul nodurilor unui oricare subarbore stâng diferă
cu cel mult 1 în plus faţă de numărul nodurilor subarborelui corespunzător
drept. Rezultă că frunzele sale se află pe ultimele două niveluri.
Algoritmul de construire a unui arbore binar total echilibrat cu n
noduri, este următorul:
a) un nod este rădăcină;
32
b) se iau nstg = [n/2] noduri pentru arborele stâng şi se trece la
construirea lui (pasul a);
c) se iau cele ndr=n-nstg-1 noduri rămase pentru subarborele
drept şi se trece la construirea lui (pasul a).
Pentru oricare nod există relaţia:
ndr <= nstg <= ndr + 1
În programul următor este implementat acest algoritm pentru
construirea unui arbore binar total echilibrat, citirea informaţiei în
noduri făcându-se în preordine.
#include <stdio.h>
#include <conio.h>
#include <alloc.h>
/* ARBORI BINARI TOTAL ECHILIBRATI */
33
}
void postordine(TIP_NOD *p, int nivel)
{
int i;
if (p!=0){
postordine(p->stg,nivel+1);
postordine(p->dr,nivel+1);
for(i=0;i<=nivel;i++) printf(" ");
printf("%2d\n",p->nr);
}
}
TIP_NOD *constructie(int nr_noduri)
{
TIP_NOD *p;
int n_stg,n_dr;
int inf,n;
n=sizeof(TIP_NOD);
if(nr_noduri==0) return 0;
else {
n_stg=nr_noduri/2;
/*nr_noduri din subarborele stang */
n_dr=nr_noduri-n_stg-1;
/*nr.noduri din subarborele drept */
p=(TIP_NOD *)malloc(n);
printf("\nIntroduceti informatia din nod in
preordine ");
scanf("%d",&(p->nr));
p->stg=constructie(n_stg);
p->dr=constructie(n_dr);
}
return p;
}
void main(void)
{
int nr_total_noduri;
printf("\nNumarul total de noduri =");
scanf("%d",&nr_total_noduri);\
rad=constructie(nr_total_noduri);
34
printf("\nVIZITAREA IN PREORDINE\n");
preordine(rad,0);
getch();
printf("\nVIZITAREA IN INORDINE\n");
inordine(rad,0);
getch();
printf("VIZITAREA IN POSTORDINE\n");
postordine(rad,0);
getch();
}
2.8 Arbori oarecare
35
− se scoate pe rând din coadă adresa a câte unui nod, se prelucrează
informaţia din nod, iar apoi se introduc adresele fiilor nodului respectiv. Se
repetă acest pas până când coada devine vidă.
3. Mersul lucrării
36
3.12 Să se scrie o funcţie care determină dacă doi arbori binari sunt
echivalenţi (arborii binari sunt echivalenţi dacă sunt structural echivalenţi
şi datele corespunzătoare nodurilor sunt aceleaşi).
37
Lucrarea de laborator nr. 5.
1. Conţinutul lucrării
În lucrare sunt prezentate principalele operaţii asupra arborilor binari
de căutare: inserare, căutare, ştergere, traversare. De asemenea sunt
prezentaţi arborii binari de căutare optimali.
2. Consideraţii teoretice
Arborii binari de căutare sunt des folosiţi pentru memorarea şi
regăsirea rapidă a unor informaţii, pe baza unei chei. Fiecare nod al
arborelui trebuie să conţină o cheie distinctă.
Structura unui nod al unui arbore binar de căutare este următoarea:
38
b) Dacă cheia rădăcinii este egală cu key atunci inserarea nu se
poate face întrucât există deja un nod cu această cheie.
c) Dacă cheia key este mai mică decât cheia rădăcinii, se reia
algoritmul pentru subarborele stâng (pasul a).
d) Dacă cheia key este mai mare decât cheia rădăcinii, se reia
algoritmul pentru subarborele drept (pasul a).
39
return;
}
else q=q->dr;
}
else { /* cheie dublă */
/* scriere mesaj */
free (p);
return;
}
}
}
2.2 Căutarea unui nod de cheie dată key într-un arbore binar de
căutare.
40
else if (key<p->cheie) p=p->stg; /*căutarea
se face în subarb.stâng */
else p=p->dr; /* căutarea se face în
subarborele drept */
}
return 0; /* nu există nod de cheie key */
}
41
2.4 Ştergerea unui arbore binar de căutare
Ştergerea unui arbore binar de căutare constă în parcurgerea în
postordine a arborelui şi ştergerea nod cu nod, conform funcţiei următoare:
42
if (p!=0) {
postordine (p->stg);
postordine (p->dr);
extragere informaţie din nodul p;
}
}
cu condiţia:
43
n n
∑ p + ∑q
i =1
i
j =0
j
=1
q ij
= ∑ p + ∑q
k =i +1
k
k =i
k
Fie rij valoarea lui k pentru care se obţine minimul din relaţia lui cij.
Nodul cu cheia c[rij] va fi rădăcina subarborelui optimal Aij, iar subarborii
săi vor fi Ai,k-1 şi Akj.
Calculul valorilor matricei C este de ordinul O(n3). S-a demonstrat că
se poate reduce ordinul timpului de calcul la O(n2).
Construirea se face cu ajutorul funcţiei următoare:
44
p->dr=constr_arbore_optimal(r[i][j], j);
}
return p;
}
2.7 Exemple
Primul program prezintă toate funcţiile descrise în lucrare asupra
unui arbore de căutare. Un nod conţine drept informaţie utilă numai cheia,
care este un număr întreg.
Al doilea program conţine funcţiile principale asupra unui arbore
#include <stdio.h>
#include <conio.h>
#include <alloc.h>
typedef struct tip_nod{
int cheie;/*informatie */
struct tip_nod *stg,*dr;
} TIP_NOD;
TIP_NOD *rad;
void preordine(TIP_NOD *p, int nivel)
{
int i;
if (p!=0){
for(i=0;i<=nivel;i++) printf(" ");
printf("%2d\n",p->cheie);
preordine(p->stg,nivel+1);
preordine(p->dr,nivel+1);
}
}
void inordine(TIP_NOD *p, int nivel)
{
int i;
if (p!=0){
inordine(p->stg,nivel+1);
45
for(i=0;i<=nivel;i++) printf(" ");
printf("%2d\n",p->cheie);
inordine(p->dr,nivel+1);
}
}
void postordine(TIP_NOD *p, int nivel)
{
int i;
if (p!=0){
postordine(p->stg,nivel+1);
postordine(p->dr,nivel+1);
for(i=0;i<=nivel;i++) printf(" ");
printf("%2d\n",p->cheie);
}
}
46
if(q->dr == 0) {
q->dr=p;
return;
}
else q=q->dr;
}
else { /* chei egale */
printf("\n Exista un nod de cheie = %d\n",key);
/* eventuala prelucrare a nodului */
free(p);
return;
}
}
}
47
TIP_NOD *p;
48
/* s-a gasit nodul p de cheie key */
if(p->stg==0) nod_inlocuire=p->dr; /* nodul de sters p nu are fiu
sting */
else if(p->dr==0) nod_inlocuire=p->stg; /*nodul de sters p nu are
fiu drept*/
else { /* nodul de sters p are fiu stang si fiu drept */
tata_nod_inlocuire=p;
nod_inlocuire=p->dr; /* se cauta in subarborele drept*/
while(nod_inlocuire->stg!=0)
{
tata_nod_inlocuire=nod_inlocuire;
nod_inlocuire=nod_inlocuire->stg;
}
if(tata_nod_inlocuire!=p)
{
tata_nod_inlocuire->stg=nod_inlocuire->dr;
nod_inlocuire->dr=p->dr;
}
nod_inlocuire->stg=p->stg;
}
free(p);
printf("\nNodul de cheie=%d a fost sters!\n",key);
if(tata_p==0) return nod_inlocuire; /*s-a sters chiar radacina
initiala */
else {
if (directie==1) tata_p->stg=nod_inlocuire;
else tata_p->dr=nod_inlocuire;
return rad;
}
}
49
void main(void)
{
TIP_NOD *p;
int i, n,key;
char ch;
printf("ALEGETI Inserare recursiva r/R sau nerecursiva alt
caracter");
scanf("%c",&ch);
printf("\nNumarul total de noduri=");
scanf("%d",&n);
rad=0;
for(i=1;i<=n;i++)
{
printf("\nCheia nodului=");
scanf("%d",&key);
if((ch=='R')||(ch=='r')) rad=inserare_rec(rad,key);
else inserare(key);
}
printf("\nVIZITAREA IN PREORDINE\n");
preordine(rad,0);
getch();
printf("\nVIZITAREA IN INORDINE\n");
inordine(rad,0);
getch();
printf("VIZITAREA IN POSTORDINE\n");
postordine(rad,0);
getch();
fflush(stdin);
printf("\n Doriti sa cautati un nod DA=D/d Nu= alt caracter :");
scanf("%c",&ch);
while((ch=='D')||(ch=='d'))
{
printf("Cheia nodului cautat=");
scanf("%d",&key);
p=cautare(rad,key);
if(p!=0) printf("Nodul exista si are adresa p\n");
else printf("Nu exista un nod de cheie data\n");
fflush(stdin);
50
printf("\n Doriti sa cautati un nod DA=D/d Nu= alt
caracter : ");
scanf("%c",&ch);
}
fflush(stdin);
printf("\n Doriti sa sterget un nod DA=D/d Nu= alt caracter :");
scanf("%c",&ch);
while((ch=='D')||(ch=='d'))
{
printf("Cheia nodului de sters=");
scanf("%d",&key);
rad=stergere_nod(rad,key);
inordine(rad,0);
fflush(stdin);
printf("\n Doriti sa stergeti un nod DA=D/d Nu= alt
caracter : ");
scanf("%c",&ch);
}
printf("stergeti arborele creat ? da=d/D nu=alt caracter ");
fflush(stdin);
scanf("%c",&ch);
if((ch=='D')||(ch=='d')) {
stergere_arbore(rad);
rad=0;
printf("\nARBORELE ESTE STERS!!\n");
}
getch();
}
#include <stdio.h>
#include <conio.h>
#include <alloc.h>
#define nmax 25
51
typedef struct tip_nod {
char cheie;
tip_nod *stg,*dr;
} TIP_NOD;
52
for(k=i+1;k<=l+i;k++)
{
x=c[i][k-1]+c[k][l+i];
if(x<min) {
min=x;
m=k;
}
}
c[i][l+i]=min+g[i][l+i];
r[i][l+i]=m;
}
printf("\nMATRICEA G\n");
for(i=0;i<=nr;i++)
{ for(j=i;j<=nr;j++)
printf("%d ",g[i][j]);
printf("\n");
}
getch();
printf("\nMATRICEA C\n");
for(i=0;i<=nr;i++)
{ for(j=i;j<=nr;j++)
printf("%d ",c[i][j]);
printf("\n");
}
getch();
printf("\nMATRICEA R\n");
for(i=0;i<=nr;i++)
{ for(j=i;j<=nr;j++)
printf("%d ",r[i][j]);
printf("\n");
}
getch();
printf("c[0][nr.]=%ld g[0][nr.]=%ld\n",c[0]
[nr.],g[0][nr.]);
getch();
*dr_med=c[0][nr.]/(float)g[0][nr.];
}
53
/* construirea arborelui optimal */
{
int n;
TIP_NOD *p;
if (i==j) p=0;
else {
n=sizeof(TIP_NOD);
p=(TIP_NOD*)malloc(n);
p->stg=constr_arbore(i,r[i][j]-1);
p->cheie=chei[r[i][j]];
p->dr=constr_arbore(r[i][j],j);
}
return p;
}
54
chei[i]=getche();
printf(" frecventa=");
scanf("%d",&p[i]);
}
/*Citirea frecventelor de cautare intre chei */
for(i=0;i<=n;i++)
{
printf("q[%d]=",i);
scanf("%d",&q[i]);
}
calcul(n,&drum_mediu);
printf("Drumul mediu=%6f\n",drum_mediu);
getch();
radacina=constr_arbore(0,n);
inordine(radacina,0);
getch();
}
4. Mersul lucrării
3.1 De la tastatură se citesc cuvinte ( şiruri de caractere ). Să se scrie un
program care creează un arbore de căutare, care conţine în noduri cuvintele
şi frecvenţa lor de apariţie. Să se afişeze apoi cuvintele în ordine
lexicografică crescătoare şi frecvenţa lor de apariţie.
55
3.5 Se consideră un arbore de căutare care conţine elemente cu
informaţia utilă de tip şir de caractere. Să se scrie o funcţie de căutare,
inserare şi ştergere a şirului de caractere permiţându-se folosirea
şabloanelor, spre exemplu * pentru orice subşir sau ? pentru orice caracter.
56
Lucrarea de laborator nr. 6
REPREZENTAREA ŞI TRAVERSAREA
GRAFURILOR
1. Conţinutul lucrării
În lucrare sunt prezentate câteva noţiuni legate de grafuri, modurile de
reprezentare şi traversare a lor.
2. Consideraţii teoretice
Noţiuni de bază
Graful orientat sau digraful G =(V, E) este perechea formată din
mulţimea V de vârfuri şi mulţimea E ⊂ V V de arce. Un arc este o
pereche ordonată de vârfuri (v, w), unde v este baza arcului, iar w este
vârful arcului. In alţi termeni se spune că w este adiacent lui v.
O cale este o succesiune de vârfuri v[1],v[2],…,v[k], astfel că există
arcele (v[1],v[2]), (v[2],v[3]),…,(v[k-1],v[k]) în mulţimea arcelor E.
Lungimea căii este numărul de arce din cale. Prin convenţie, calea de la un
nod la el însuşi are lungimea 0.
O cale este simplă, dacă toate vârfurile, cu excepţia primului şi
ultimului sunt distincte între ele.
Un ciclu este o cale de la un vârf la el însuşi.
Un graf orientat etichetat este un graf orientat în care fiecare arc şi
/sau vârf are o etichetă asociată, care poate fi un nume, un cost sau o valoare
de un tip oarecare.
Un graf orientat este tare conex, daca oricare ar fi vârfurile v şi w
există o cale de la v la w şi una de la w la v.
Un graf G’ =(V’, E’) este subgraf al lui G daca V’⊂ V şi E’⊂ E. Se
spune că subgraful indus de V’⊂ V este G’ =(V’, E ∧(V’V’)).
Un graf neorientat sau prescurtat graf G =(N, R) este perechea
formată din mulţimea N de noduri şi mulţimea R de muchii. O muchie este
o pereche neordonată (v, w)=(w, v) de noduri.
Definiţiile prezentate anterior rămân valabile şi în cazul grafurilor
neorientate.
57
Moduri de reprezentare
Atât grafurile orientate, cât şi cele neorientate se reprezintă frecvent
sub două forme: matricea de adiacenţe şi listele de adiacenţe.
Astfel, pentru graful orientat G =(V, E), unde V este mulţimea
vârfurilor V ={1,2,…,n},matricea de adiacenţe A va fi definită astfel :
58
LISTA
2.3.Explorarea în lărgime
Explorarea în lărgime constă în următoarele acţiuni:
− se trece într-o coadă vidă nodul de pornire;
− se trece extrage din coadă câte un nod care este prelucrat şi
se adaugă toate nodurile adiacente lui neprelucrate. Se repetă acest pas până
când coada devine vidă.
59
Algoritmul este următorul:
void explorare_largime(int s)
/* s este nodul de pornire */
{
int vizitate[NrMaxNoduri];
coada Q;
int i,NrNoduri,v,w;
for(i=0;i<NrNoduri;i++)
vizitate[i]=0; /*iniţializare vector cu zero*/
vizitate[s]=1;/*se vizitează nodul s */
prelucrează informaţia din s;
introducere nod s in coada Q;
while(coada Q nu este vidă)
{
extrage următorul nod v din coada Q;
for(fiecare nod w adiacent lui v)
if(nodul w nu a fost vizitat)
{
vizitate[w]=1;
prelucrează informaţia din w;
introducere nod w în coada Q;
}
}
}
2.4.Explorarea in adâncime
La explorarea în adâncime se marchează vizitarea nodului iniţial, după
care se vizitează în adâncime, recursiv, fiecare nod adiacent. După vizitarea
tuturor nodurilor ce pot fi atinse din nodul de start, parcurgerea se consideră
încheiată. Dacă rămân noduri nevizitate, se alege un nou nod şi se repetă
procedeul de mai sus.
60
Algoritmul este următorul:
void explorare_adancime(int s)
/* s este nodul de pornire* /
{
int vizitate[NrMaxNoduri];
stiva ST;
int NrNoduri,i,v,w;
for(i=0;i<NrNoduri;i++)
vizitate[i]=0; /*initializare vector cu zero*/
vizitate[s]=1; /*se incepe cu s */
prelucrare informatia din s;
introducere s in stiva ST;
while(stiva ST nu este vidă)
{
v=conţinutul nodului din vârful stivei;
w=următorul nod adiacent lui v nevizitat;
if(există w)
{
vizitate[w]=1;
prelucrează informaţia din w;
pune pe stiva ST nodul w;
}
else pop(ST); /* se şterge nodul v din vârful stivei ST */
}
}
3.Mersul lucrării
3.1. Pentru un graf orientat G =(V, E) şi V’⊂V să se
găsească subgraful indus G’ =(V’, E’).Elementele din V şi V’ se citesc.
61
3.4. Să se scrie o funcţie care să verifice dacă există o cale
între două noduri date (v, w) ale unui graf orientat G =(V, E).
62
Lucrarea de laborator nr.7
1.Conţinutul lucrării
2.Consideraţii teoretice
2.1.Căile de cost minim dintr-un vârf
Se consideră un graf orientat G =(V, E) etichetat, în care fiecare arc
are ca etichetă un număr nenegativ numit cost. Graful se reprezintă în
memorie prin matricea de adiacenţe etichetată, care se mai numeşte matricea
costurilor.
Fiind date două vârfuri, unul sursă şi unul destinaţie, se cere găsirea
drumului de cost minim de la sursă la destinaţie. Algoritmul lui Dijkstra de
rezolvare a acestei probleme constă in următoarele:
- se păstrează o mulţime S de vârfuri j∈V, pentru care există cel
puţin un drum de la sursă la j. Iniţial S ={sursa}.
- la fiecare pas, se adaugă la S un vârf a cărui distanţă faţă de un vârf
din S este minimă.
Pentru a înregistra căile minime de la sursă la fiecare vârf se
utilizează un tablou TATA, în care TATA[k] păstrează vârful anterior lui k
pe calea cea mai scurtă.
In descrierea algoritmului se fac următoarele notaţii:
- n numărul vârfurilor mulţimii V;
- mulţimea S se reprezintă prin vectorul caracteristic ( elementele sale sunt
S[i]=1, dacă i∈S şi S[i]=0, dacă i∉S;
- vectorul DISTANTE de n elemente al distanţelor minime de la sursă la
fiecare vârf;
63
- matricea de costuri COST de nxn elemente: COST[ i ][j]=c>0 dacă ( i
,j)∈E, COST[i][j]=0 dacă i =j si COST[i][j]=+∞ dacă (i, j)∉E;
- vectorul TATA.
Algoritmul lui Dijkstra este următorul:
#define NMAX …
#define INFINIT …
float DISTANTE[NMAX];
float COST[NMAX][NMAX];
int TATA[NMAX];
int S[NMAX];
void DIJKSTRA(int n,int sursa)
{
/* n este numărul de vârfuri;sursa este vârful sursă */
int i ,j,k,pas;
/*iniţializări*/
for (i=1;i<=n;i++)
{
S[i]=0;
DISTANTE[i]=COST[sursa][i];
if (DISTANTE[i]<INFINIT) TATA[i]=sursa;
else TATA[i]=0;
}
/*introducere sursa in S*/
S[sursa]=1;
TATA[sursa]=0;
DISTANTE[sursa]=0;
/*construire vectori DISTANTE si TATA */
for (pas=1;pas<=n-1;pas++)
{
găseşte vârful k neselectat cu DISTANTE[k] minim;
if (minimul anterior găsit==INFINIT) return;
S[k]=1; /* se adaugă k la mulţimea S */
for (j=1;j<=n;j++)
if ((S[j]=0) && (DISTANTE[k]+COST[k]
[j]<DISTANTE[j]))
{
64
DISTANTE[j]=DISTANTE[k]+COST[k]
[j];
TATA[j]=k;
}
}
}
#define NMAX …
float C[NMAX][NMAX]; /*matricea costurilor*/
float A[NMAX][NMAX];
void FLOYD(int n)
{
int i,j,k;
for (i=1;i<=n;i++)
for (j=1;j<=n;j++)
A[i][j]=C[i][j];/*iniţializare A*/
65
for (i=1;i<=n;i++) A[i][i]=0;
/*iteraţiile*/
for (k=1;k<=n;k++)
for (i=1;i<=n;i++) //pentru toate liniile
for (j=1;j<=n;j++) //pentru toate coloanele
if (A[i][k]+A[k][j]<A[i][j])
A[i][j]=A[i][k]+A[k][j];
}
66
void algoritm_PRIM(int n)
{
W={1}; //se pleacă din nodul 1
T={ }; //mulţimea vidă
while (W!=N)
{
selectează muchia (w,u) de cost minim cu w∈W şi
u∈N-W;
adaugă u la W;
adaugă (u,w) la T;
}
}
void algoritm_Kruskal(int n)
{
T={};
while (T nu este arbore de acoperire)
{
selectează muchia (w,u) de cost minim din R;
şterge (w,u) din R;
if ( (w,u) nu creează un ciclu in T)
adaugă (w,u) la T;
}
}
3.Mersul lucrării
67
arborele având ca rădăcină vârful sursă. Care este performanta algoritmului
în ceea ce priveşte timpul de calcul?
68
Lucrarea de laborator nr. 8
TABELE DE DISPERSIE
1. Conţinutul lucrării
2. Consideraţii teoretice
2.1.Tipuri de tabele
69
f: K -> H
unde K este mulţimea cheilor, iar H este o mulţime de numere naturale.
Funcţia f nu este injectivă .Două chei pentru care f(k1)=f(k2) se
spune că intră în coliziune, iar înregistrările respective se numesc sinonime.
Asupra lui f se pun două condiţii:
- valoarea ei pentru un kε K să rezulte cât mai simplu şi rapid;
- să minimizeze numărul de coliziuni.
Un exemplu de funcţie de dispersie este următoarea:
f(k)=γ (k) mod M
unde γ (k) este o funcţie care transformă cheia într-un număr natural, iar M
este un număr
natural recomandat a fi prim.
Funcţia γ (k) se alege în funcţie de natura cheilor. Dacă ele sunt
numerice, atunci γ (k)=k. În cazul cheilor alfanumerice, cea mai simplă
funcţie γ (k) este suma codurilor ASCII ale caracterelor din componenţa
lor; ca urmare funcţia f de calcul a dispersiei este următoarea:
#define M…
int f(char *key)
{
int i,suma;
suma=0;
for(i=0;i<length(key);i++)
suma=suma+*(key+i);
return suma%M;
}
70
2.3.Tabela de dispersie
71
Iniţial el conţine pointerii nuli:
for(i=0;i<M;i++)
HT[i]=0;
Căutarea într-o tabelă de dispersie a unei înregistrări având pointerul key la
cheia sa, se face astfel:
- se calculează codul de dispersie:
h=f(key);
- se caută înregistrarea având pointerul key la cheia sa, din lista având
pointerul spre primul nod HT[h].
Căutarea este liniară:
p=HT(h);
while(p!=0)
{
if(strcmp(key,p->cheie)==0) return p;
p=p->urm;
}
return 0;
Inserarea unei înregistrări într-o tabelă de dispersie se face astfel:
- se construieşte nodul de pointer p, care va conţine informaţia utilă şi
pointerul la cheia înregistrării:
p=(TIP_NOD*)malloc(sizeof(TIP_NOD));
citire_nod(p);
- se determină codul de dispersie al înregistrării:
h=f(p->cheie);
if(HT[h]==0){
HT[h]=p;
p->urm=0;
}
72
în caz contrar se verifică dacă nu cumva mai există o înregistrare cu cheia
respectivă. În caz afirmativ se face o prelucrare a înregistrării existente
( ştergere, actualizare) sau este o eroare (cheie dublă ). Dacă nu există o
înregistrare de cheia respectivă, se inserează în listă ca prim element nodul
de adresă p:
q=cautare(p->cheie);
if(q==o){ /* nu exista o înregistrare de cheia respectiva */
p->urm=HT[h];
HT[h]=p;
}
else prelucrare(p,q);/* cheie dubla */
Construirea tabelei de dispersie se face prin inserarea repetată a nodurilor.
for(i=0;i<M;i++)
{
if(HT[i]!=0)
{
printf(“\nInregistrări avand codul de dispersie=%d\n”,i);
p=HT[i];
while(p!=0){
afisare(p);
p=p->urm;
}
}
3.Mersul lucrării
73
3.2. Să se implementeze algoritmii prezentaţi aferenţi unei tabele de
dispersie. Înregistrarea va conţine datele aferente unui student. Cheia va fi
numele şi prenumele studentului. Scrieţi în plus
faţă de cele prezentate o funcţie de ştergere a unei înregistrări de cheie dată.
74
Lucrarea de laborator nr. 9
1.Conţinutul lucrării
În lucrare sunt prezentate principiile metodelor Greedy şi
backtracking, variantele lor de aplicare şi exemple.
2 2.Consideraţii teoretice
2.1.Metoda Greedy.
Metoda Greedy se aplică următoarelor tipuri de probleme:
Dintr-o mulţime A de n elemente se cere determinarea unei
submulţimi B care să îndeplinească anumite condiţii pentru a fi acceptată.
Numele metodei vine de la următorul fapt: se alege pe rând câte un
element din mulţimea A şi eventual se introduce în soluţie.
Se menţionează faptul că o dată ce un element a fost ales el rămâne
în soluţia finală, iar dacă un element a fost exclus, el nu va mai putea fi
reconsiderat pentru includere în soluţie.
Metoda determină o singură soluţie.
Există două variante de rezolvare a unei probleme cu ajutorul
metodei Greedy:
a) Varianta I
Se pleacă de la mulţimea B vidă. Se alege din mulţimea A un
element neales în paşii precedenţi. Se cercetează dacă adăugarea la soluţia
parţială B conduce la o soluţie posibilă. În caz afirmativ se adaugă
elementul respectiv la B.
75
Descrierea variantei este următoarea:
#define max ...
GREEDY1(int A[max], int n, int B[max], int *k)
/* A este mulţimea de n elemente date;
B este mulţimea extrasă de k elemente */
{
int x, v, i;
*k = 0; /* Mulţimea B este vidă */
for(i = 0; i<n; i++)
{
ALEGE (A, n, i, x);
/* se alege elementul x dintre elementele A[i], A[i+1], ...
A[n-1] şi se aduce pe poziţia i prin interschimbare */
POSIBIL (B, x, v);
/* v=1 dacă x prin adăugare la B conduce la soluţie posibilă
şi
v=0 în caz contrar */
if(v==1) ADAUGA(B, x, *k);
/* se adaugă x la B, k indicând numărul de elemente din B
*/
}
}
76
Descrierea variantei este următoarea:
#define max ...
GREEDY2(int A[max], int n, int B[max], int *k)
/* A este mulţimea de n elemente date;
B este mulţimea extrasă de k elemente */
{
int x, v, i;
*k = 0; /* soluţia vidă */
PRELUCRARE(A, n); /* rearanjare vector A */
for(i = 0; i<n; i++)
{
x=A[i];
POSIBIL (B, x, v);
/* v=1 dacă prin adăugarea lui x la B se ajunge la o soluţie
posibilă şi
v=0 în caz contrar */
if(v==1) then ADAUGA(B, x, *k);
/* se adaugă x la mulţimea B */
}
}
77
Pentru evitarea parcurgerii tuturor arcelor grafului la fiecare pas, se
ia vectorul v având n componente definit astfel:
0 dacă vârful i aparţine arborelui deja construit
U[i] =
k dacă vârful i nu aparţine arborelui deja construit; k
este vârful arborelui deja construit a. î. muchia (i, k) este de
cost minim.
Iniţial v[1]=0 şi v[2]=v[3]=...=v[n]=1, adică iniţial arborele este A
=({1}, Ø).
78
minim=c[k][v[k]];
}
muchii[i][0]=v[j];
muchii[i][1]=j;
*cost=*cost+c[j][v[j]];
/*reactualizare vector v */
v[j]=0;
for(k=1;k<=n;k++)
if(v[k]!=0)
if(c[k][v[k]]>c[k][j]) v[k]=j;
}
}
void main()
{
int n;/*nr. nodurilor */
int c[nmax][nmax]; /*matricea costurilor */
int muchii[nmax][2];/*muchiile arborelui*/
int i,j,k,cost;
printf("\nNr. nodurilor=");
scanf("%d",&n);
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
c[i][j]=MAX;
/*citirea costurilor(numere întregi) */
for(i=1;i<n;i++)
{
do
{
printf("\nIntroduceţi 0 pentru terminare sau nodul
adiacent\n");
printf(" cu %2d > ca el =",i);
scanf("%d",&j);
if(j>0)
{
printf("\nCostul c[%d][%d]=",i,j);
scanf("%d",&c[i][j]);
c[j][i]=c[i][j]; /*matricea este simetrica */
}
}
79
while(j>0);
};
prim(n,c,muchii,&cost);
printf("\nCOSTUL ARBORELUI = %d",cost);
printf("\nMUCHIILE ARBORELUI COSTUL
MUCHIEI\n");
for(i=1;i<=n-1;i++)
printf(" %2d - %2d %10d\n",muchii[i][0],muchii[i][1],
c[muchii[i][0]][muchii[i][1]]);
getch();
}
80
poate avansa se urmăreşte înapoi secvenţa curentă din soluţia posibilă. Între
condiţia internă şi cea de continuare există o strânsă legătură. Stabilirea
optimă a condiţiilor de continuare reduce mult numărul de calcule.
Algoritmul backtracking este redat sub formă nerecursivă astfel:
#define nmax ...
backtracking_nerecursiv(int n)
/* se consideră globale mulţimile Si şi numărul lor de elemente
nrsi */
{
int x[nmax];
int k, v;
k=1;
while(k>0)
{
v=0;
while ((mai există o valoare α Є Sk netestată) & (v==0))
{
x[k]=α;
if (φ(x[1], x[2], ..., x[k]) este îndeplinită) v=1;
}
if (v==0) k=k-1;
else if (k==n) afişare (x, n); /* afişarea sau eventual
prelucrarea soluţiei */
else k=k+1;
}
}
81
x[k]=Sk[j]; /* al j-lea element din mullţimea Sk */
if (φ(x[1], x[2], ..., x[k]) este îndeplinită)
if (k<n) backtracking_recursiv(k+1);
else afişare (x, n); /* afişarea sau eventual
prelucrarea soluţiei */
}
}
Apelul se face:
backtracking_recursiv(1);
Exemplu: Problema damelor de şah.
Se cere găsirea tuturor soluţiilor de aşezare pe tabla de şah de n linii
şi n coloane a n dame, astfel încât ele să nu se atace. Se consideră că ele se
atacă dacă sunt pe aceeaşi linie, coloană sau diagonală.
Întrucât pe o linie se găseşte o singură damă, soluţia se prezintă sub
formă de vector
x =(x1, x2, ... ,xn), unde xi reprezintă coloana pe care se află dama în linia i.
82
nr_solutie=0;
k=1;x[k]=0;
while(k>0)
{
/*găsirea unei aşezări corecte în linia k */
v=0;
while((v==0)&(x[k]<=n-1))
{
x[k]++;
v=1;i=1;
while((i<=k-1)&(v==1))
if((x[k]==x[i])|(abs(k-i)==abs(x[k]-x[i]))) v=0;
else i++;
}
if(v==0) k=k-1;
else {
if(k==n){
/*afişarea tablei */
nr_solutie++;
printf("\nSOLUTIA nr. %d\n",nr_solutie);
for(i=1;i<=n;i++)
{
for(j=1;j<=n;j++)
if (x[i]==j) printf("1");
else printf("0");
printf("\n");
};
getch();
}
else {
k++;
x[k]=0;
};
}
}
}
void main(void)
{
int n;
83
printf("\nOrdinul tablei de sah=");
scanf("%d",&n);
dame_nerecursiv(n);
printf("\nSFARSIT");
}
84
if(x[i]==p) printf("1");
else printf("0");
printf("\n");
};
getch();
}
}
}
void main(void)
{
printf("\nOrdinul tablei de sah n=");
scanf("%d",&n);
nr_solutie=0;
back_recursiv(1);
printf("\nSFARSIT\n");
}
3. Mersul lucrării
Utilizând metoda Greedy să se rezolve următoarele probleme:
3.1 Se dau m vectori V1, V2, ... Vm, care conţin n1, n2, ... nm elemente,
ordonate crescător după o cheie. Se interclasează vectorii daţi, obţinându-se
un vector de lungime n1+n2+...+nm elemente, ordonate crescător. Se ştie că
interclasarea a doi vectori care conţin n1, respectiv n2 elemente necesită un
timp proporţional cu suma lungimilor lor. Să se determine ordinea optimă în
care trebuie efectuată interclasarea tuturor vectorilor daţi.
85
terminare tfi (tpi < tfi). Dacă este selectată activitatea i, ea se desfăşoară pe
durata [tpi, tfi). Spunem că activităţile i şi j sunt compatibile dacă duratele
lor nu se intersectează. Să se selecteze o mulţime maximală de activităţi
mutual compatibile.
86
3.10.Se dau două mulţimi de numere întregi A şi B. Să se genereze
toate funcţiile f : A → B .
Lucrarea de laborator nr. 10.
1. Conţinutul lucrării
În lucrare se prezintă esenţa metodelor “Branch and Bound”
(ramifică şi mărgineşte) şi a metodei “Divide et Impera”.
2 2. Consideraţii teoretice
2.1. Metoda “Branch and Bound”
Metoda “Branch and Bound” este înrudită cu metoda backtracking,
diferind ordinea de parcurgere a spaţiului stărilor şi a modului de eliminare
a subarborilor ce nu pot conduce la rezultat.
Metoda “Branch and Bound” se aplică următoarelor tipuri de
probleme: Se cunoaşte starea iniţială s0 şi starea finală sf (starea rezultat).
Din starea s0 se ajunge în starea sf printr-o serie de decizii. Ne interesează ca
numărul stărilor intermediare să fie minim.
Presupunem că stările reprezintă nodurile unui graf, iar arcele
indică faptul că o decizie a transformat starea si în starea si+1. Se introduc
restricţii ca să nu existe mai multe noduri în graf cu aceeaşi stare, deci graful
să nu fie infinit. Astfel graful se reduce la un arbore. Arborele se generează
până la prima apariţie a nodului final.
Există posibilitatea parcurgerii grafului în adâncime sau în lăţime,
însă timpul de ajungere la rezultat este mare. O strategie superioară din
punct de vedere al timpului de calcul se obţine alegând dintre descendenţii
vârfului curent pe cel mai aproape de starea finală. Pentru a putea aprecia
depărtarea faţă de starea finală, se va folosi o funcţie de cost c definită pe
mulţimea vârfurilor din arbore. Având valorile acestei funcţii, vom alege
dintre vârfurile descendente ale vârfului curent pe cel cu cost minim. O
astfel de parcurgere a grafului se numeşte “Least Cost” sau prescurtat “LC”.
87
Funcţia c ideală pentru a măsura distanţa de la vârf la vârful final
este:
rezultat;
niv x − 1
c ( x ) = + ∞
min c(y) y este vârf terminal
∈ subarborelui de rădăcinăx}
88
TIP_NOD *RAD;
RAD=(TIP_NOD *)malloc(sizeof(TIP_NOD));
/* se depune configuraţia iniţială la adresa RAD */
void Branch_and_Bound()
{
TIP_NOD *i, *j;
LISTA L;
int iesire;
i=RAD;
LISTA_VIDA(L);
for(;;)
{
while(mai există vecini j ai lui i neprelucraţi)
if(j==vârf_rezultat)
{ afisare_drumul_de_la RAD la j;
return;
}
else {
ADAUG(j,L);
TATA[j]=i;
};
if(ESTE_VIDA(L))
{
printf("Nu exista solutie\n");
return;
}
else i=ELEMENT(L);
}
}
Funcţiile apelate au următoarea semnificaţie:
LISTAVIDĂ(L) – iniţializează lista L ca fiind vidă;
ESTEVIDĂ(L) –– returnează 1 dacă lista L este vidă sau 0 în
caz contrar;
ELEMENT(L) –– este funcţia ce returnează un element al
listei care are cel mai mic cost ĉ , pentru a ne afla în cazul pacurgerii LC a
arborelui stărilor;
ADAUG(j, L) ––– adaugă nodul j la lista L. Din descrierea
funcţiei se observă că atunci când un vârf i din lista L devine vârf curent,
89
sunt generaţi toţi descendenţii săi, aceştia fiind puşi în lista L. Unul din
aceşti descendenţi va deveni la rândul său pe baza costului ĉ vârf curent,
până când se ajunge la vârful rezultat (cel ce conţine starea finală).
{a , a
p p +1
,..., a m } şi {a , a
m +1 m+2
,..., a q } şi apoi prin combinarea
rezultatelor se obţine prelucrarea dorită.
Metoda “Divide et Impera” poate fi descrisă astfel:
void DIVIDE_IMPERA(int p,int q,rezultat:alfa)
/* p, q reprezinta indicii secvenţei care se
prelucrează;
alfa reprezintă rezultatul */
{
int eps, m;
rezultat beta,gama;
if(abs(q-p)<=eps) PRELUCRARE(p,q,alfa);
else {
DIVIDE(p,q,m);
DIVIDE_IMPERA(p,m,beta);
DIVIDE_IMPERA(m+1,q,gama);
COMBINA(beta,gama,alfa);
}
}
DIVIDE_IMPERA(1, n, alfa);
90
Variabilele şi funcţiile din funcţia DIVIDE_IMPERA au următoarele
semnificaţii:
eps – este lungimea maximă a unei secvenţe ap, ap+1, ...,aq
pentru care prelucrarea se poate face direct;
m – este indicele intermediar în care secvenţa ap, ap+1, ...,aq
este împărţită în două subsecvenţe de funcţia DIVIDE;
beta şi gama – reprezintă rezultatele intermediare obţinute în
urma prelucrării subsecvenţelor (ap, ap+1, ...,am) şi respectiv (am+1, am+2, ...,aq);
alfa – reprezintă rezultatul combinării rezultatelor
intermediare beta şi gama;
DIVIDE – împarte secvenţa (ap, ap+1, ...,aq) în două
subsecvenţe (ap, ap+1, ...,am) şi
(am+1, am+2, ...,aq);
COMBINĂ – combină rezultatele beta şi gama ale
prelucrării subsecvenţelor returnate de procedura DIVIDE, obţinând
rezultatul alfa a prelucrării secvenţei iniţiale.
91
int i,j,k,l;
int b[nmax];
i=inf;j=med+1;k=inf;
while((i<=med)&(j<=sup))
{
if(a[i]<=a[j]) {
b[k]=a[i];
i++;
}
else{
b[k]=a[j];
j++;
}
k++;
}
for(l=i;l<=med;l++)
{
/* au ramas elemente in stanga */
b[k]=a[l];
k++;
}
for(l=j;l<=sup;l++)
{
/* au ramas elemente in dreapta */
b[k]=a[l];
k++;
}
/* secventa intre inf si sup este sortata */
for(l=inf;l<=sup;l++) a[l]=b[l];
}
void divide_impera(int inf,int sup)
{
int med;
if(inf<sup) {
med=(inf+sup) / 2;
divide_impera(inf,med);
divide_impera(med+1,sup);
comb(inf,med,sup);
}
92
}
void main(void)
{
int i,n;
printf("\nIntroduceti nr.elementelor n=");
scanf("%d",&n);
printf("\nIntroduceti elementele vectorului\n");
for(i=0;i<n;i++)
{
printf("a[%d]=",i);
scanf("%d",&a[i]);
}
printf("\nVECTORUL NESORTAT\n");
afisare(n);
divide_impera(0,n-1);
printf("\nVECTORUL SORTAT\n");
0 afisare(n);
getch();
}
3. Mersul lucrării
Se vor rezolva următoarele probleme prin metoda “Branch and Bound”:
3.1 Jocul PERSPICO. 15 plăcuţe pătrate sunt încadrate într-un cadru de
dimensiune 4x4, o poziţie fiind liberă. Orice plăcuţă vecină cu această
poziţie liberă poate fi mutată în locul ei. Cele 15 plăcuţe sunt numerotate de
la 1 la 15. Se începe dintr-o stare iniţială, care corespunde unei distribuţii
oarecare a celor 15 plăcuţe şi a locului liber în cele 16 poziţii posibile.
Problema constă în a trece, folosind mutări posibile, din starea iniţială în
starea finală (fig. 3.1.1).
1 3 4 1 2 3 4
5 2 7 8 5 6 7 8
9 6 1 11 9 1 1 12
0 0 1
1 1 1 12 1 1 1
3 4 5 3 4 5
93
Exemplu de configuraţie iniţială Configuraţie finală
Figura 3.1.1
3.3 Pe malul unui râu se află 2n băştinaşi din care n sunt canibali.
Aceştia doresc să traverseze râul utilizând o barcă care poate transporta cel
mult k persoane. Dacă pe un mal sau în barcă sunt mai mulţi canibali decât
ceilalţi, atunci canibalii îi vor mânca. Cum vor reuşi să treacă toţi pe malul
opus fără să se mănânce şi fără a apela la alte persoane.
3.4 Pe un pod se află n capre care vin dintr-un sens, cu n capre care vin
din sens opus. Acestea nu se pot ocoli, însă fiecare capră poate sări peste o
singură capră din grupul opus şi desigur poate avansa dacă în faţa sa este un
spaţiu liber.
Cum reuşesc aceste capre să traverseze podul doar prin cele două
mişcări posibile (avans şi săritură).
3.6 Problema turnurilor din Hanoi. Se dau trei tije. Pe una dintre ele
sunt aşezate n discuri de mărimi diferite, discurile de diametre mai mici
fiind aşezate peste discurile cu diametre mai mari. Se cere să se mute aceste
discuri pe o altă tijă, utilizând tija a treia ca intermediar, cu condiţia mutării
94
a câte unui singur disc şi fără a pune un disc de diametru mai mare peste
unul cu diametru mai mic.
1. Conţinutul lucrării
În lucrare sunt prezentate metoda programării dinamice şi metodele
euristice.
2 2. Consideraţii teoretice
Dacă di, di+1, ..., dj (1 ≤ i < j ≤ n) este un şir optim de decizii care
transformă starea si-1 în starea sj, trecând prin stările intermediare si, si+1, ...,
sj-1 şi dacă pentru oricare k ∈[i, j-1] rezultă că di, di+1, ..., dk şi dk+1 dk+2, ..., dj
sunt ambele şiruri optime de decizii de trecere din starea si-1 în starea sk,
respectiv din starea sk în starea si-1, atunci este satisfăcut principiul
optimalităţii.
95
Aplicarea metodei programării dinamice se face astfel:
se verifică principiul optimalităţii;
se scriu relaţiile de recurenţă obţinute din regulile de
trecere dintr-o stare în alta şi se rezolvă.
Drept exemplu se ia înmulţirea optimă a unui şir de matrici.
R=A1*A2*...*An
în care Ai (i=1,n) este de dimensiunile di*di+1. Matricea rezultat R va fi de
dimensiunile di*dn+1.
Se ştie că la înmulţirea matricelor Ai şi Ai+1 se efectuează
di*di+1*di+2 operaţii de înmulţire. Dacă matricele au dimensiuni diferite,
numărul operaţiilor de înmulţire necesare obţinerii matricei rezultat R
depinde de ordinea efectuării produselor a câte două matrice. Se cere găsirea
ordinii de asociere pentru care numărul înmulţirilor să fie minim.
Rezolvarea acestei probleme se va face astfel:
Fie Cij numărul minim de înmulţiri de elemente pentru calculul
produsului Ai*Ai+1*...*Aj pentru 1 ≤ i < j ≤ n.
Se observă că:
a) Cii=0
b) Ci,i+1= di*di+1*di+2
c) C1n este valoarea minimă căutată.
d) este verificat principiul optimalităţii.
Cij = min {Ci,k+Ck+1,j+di*dk+1*dj+1 1 ≤ k ≤ j } asocierile
fiind de forma (Ai* Ai+1*…* Ak) * (Ak+1* Ak+2*…* Aj).
Se calculează valorile Ci, i+d pentru fiecare nivel d, până se
ajunge la C1,n. Pentru a construi arborele binar care va descrie ordinea
efectuării operaţiilor, se va reţine la fiecare pas indicele k care realizează
minimul, adică modul de asociere a matricelor. Vârfurile arborelui vor
conţine limitele subşirului de matrice care se asociază; rădăcina va conţine
(1,n), iar un subarbore care conţine în rădăcină (i, j) va avea descendenţi pe
(i, k) şi (k+1, j), unde k este valoarea pentru care se realizează optimul cerut.
În continuare este prezentat programul comentat de
rezolvare a acestei probleme.
#include <stdio.h>
#include <conio.h>
#include <alloc.h>
#define nmax 10
typedef struct tip_nod{
96
long ind1,ind2;
struct tip_nod *stg,*dr;
} TIP_NOD;
/*Inmultirea optima a unui sir de matrici A1*A2*...*An */
/* de dimensiuni d1*d2,d2*d3,...,dn*d(n+1) */
void prod_matr(int n,long c[nmax][nmax],int d[nmax+1])
{
int i,j,k,l,poz;
long min,val;
for(i=1;i<=n;i++)
c[i][i]=0;
for(l=1;l<=n-1;l++)
for(i=1;i<=n-l;i++)
{
j=i+l;
min=0x7fffffff;
for(k=i;k<=j-1;k++)
{
val=c[i][k]+c[k+1][j]+
(long)d[i]*d[k+1]*d[j+1];
if(val<min) {
min=val;poz=k;
};
};
c[i][j]=min;
c[j][i]=poz;
}
}
TIP_NOD *constr_arbore(TIP_NOD *p,int i,int j,long c[nmax]
[nmax])
{
p=(TIP_NOD *)malloc(sizeof(TIP_NOD));
p->ind1=i;p->ind2=j;
if(i<j) {
p->stg=constr_arbore(p->stg,i,c[j][i],c);
p->dr=constr_arbore(p->dr,c[j][i]+1,j,c);
}
else {
p->stg=0; p->dr=0;
97
};
return p;
}
void postordine(TIP_NOD *p,int nivel)
{
int i;
if(p!=0) {
postordine(p->stg,nivel+1);
postordine(p->dr,nivel+1);
for(i=0;i<=nivel;i++) printf(" ");
printf("(%ld,%ld)\n",p->ind1,p->ind2);
}
}
void main(void)
{
int i,j,n;
long c[nmax][nmax];
int d[nmax+1]; /* dimensiunile matricelor */
TIP_NOD *rad;
printf("\nIntroduceti nr.matricelor n=");
scanf("%d",&n);
printf("\nIntroduceti dimensiunile matricelor\n");
for(i=1;i<=n+1;i++)
{
printf("\nDimensiunea d[%d]=",i);
scanf("%d",&d[i]);
};
prod_matr(n,c,d);
/* Matricea c rezultata in urma calculelor */
printf("\nMATRICEA C\n");
for(i=1;i<=n;i++)
{
for(j=1;j<=n;j++)
printf("%6ld",c[i][j]);
printf("\n");
}
printf("\nNR.MINIM DE INMULTIRI = %ld",c[1]
[n]);
98
getch();
rad=0;
rad=constr_arbore(rad,1,n,c);
printf("\nARBORELE IN POSTORDINE\n");
postordine(rad,0);
getch();
}
99
Pentru că ciclul este o cale închisă, putem considera ca punct de
plecare oricare nod. De aceea se pot alege nişte noduri de start, se determină
pentru fiecare ciclul corespunzător după strategia descrisă şi se reţine ciclul
de cost minim dintre ele.
În continuare este prezentat programul corespunzător.
#include <stdio.h>
#include <conio.h>
#define nmax 10
/*Problema comis_voiajorului */
void comis_voiajor(int n,int c[nmax][nmax],
int i,int ciclu[nmax+1],int *cost)
/* n este nr.nodurilor;c este matricea costurilor;
i este nodul de start;ciclu contine nodurile din ciclu;
cost este costul ciclului */
{
int p[nmax];
int k,v,j,vmin;
int costmin;
for(k=1;k<=n;k++)
p[k]=0;
*cost=0;
p[i]=1;ciclu[1]=i;
v=i; /*nodul curent */
for(k=1;k<n;k++) /* se adauga pe rand n-1 muchii */
{
costmin=0x7fff;
/*gasirea muchiei de cost minim care are
nodul de origine v*/
for(j=1;j<=n;j++)
if((p[j]==0)&&(c[v][j]<costmin))
{
costmin=c[v][j];
vmin=j;
}
*cost=*cost+costmin;
ciclu[k+1]=vmin;
p[vmin]=1;
v=vmin;
}
100
ciclu[n+1]=i;
*cost=*cost+c[v][i];
}
void main(void)
{
int i,j,n;
int cost_ciclu;
int ciclu[nmax+1];
int c[nmax][nmax];
printf("\nNr.nodurilor grafului = ");
scanf("%d",&n);
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
c[i][j]=0x7fff;
printf("\nIntroduceti costurile muchiilor care au ca origine");
printf("\n nodul i si celalalt nod mai mare ca i;daca nu mai
sunt");
printf("\n astfel de noduri se introduce 0 \n");
for(i=1;i<=n-1;i++)
{
while(1)
{
printf("Nodul adiacent lui %d =",i);
scanf("%d",&j);
if(j!=0){
printf("Costul muchiei (%d,
%d)=",i,j);
scanf("%d",&c[i][j]);
c[j][i]=c[i][j];
}
else break;
}
}
i=1;
comis_voiajor(n,c,i,ciclu,&cost_ciclu);
printf("\nCOSTUL CICLULUI =%d\n",cost_ciclu);
printf("\nCICLUL=");
for(i=1;i<=n+1;i++)
printf("%3d",ciclu[i]);
101
getch();
}
4. Mersul lucrării
Se vor rezolva prin metoda programării dinamice următoarele probleme:
3.1 Determinarea căilor de cost minim între oricare două vârfuri ale
unui graf orientat prin algoritmul lui Floyd. (problema 3.2 din lucrarea 7).
102
determine greutatea maximă de mere care poate fi culeasă şi ordinea în care
sunt culese merele.
Se citesc: n, x, d şi (gi, hi) i=1...n.
Să se rezolve prin metode euristice următoarele probleme:
3.6 Să se găsească maximul unei funcţii f(x) în intervalul [a, b].
103
Lucrarea de laborator nr. 12.
ALGORITMI FUNDAMENTALI DE
SORTARE
1. Conţinutul lucrării
În lucrare sunt prezentaţi algoritmii de sortare prin numărare, prin
inserare (directă şi shellsort), prin interschimbare (metoda bulelor şi
quicksort), prin selecţie şi interclasare.
2. Consideraţii teoretice
Sortarea constă în ordonarea crescătoare sau descrescătoare a
elementelor unui vector A =(a0, a1, ..., an-1). În practică, problema se
întâlneşte sub forma sortării unor articole după cheie, cheia fiind un câmp
din articol.
104
Metoda inserării binare constă în căutarea binară a locului unde
trebuie inserat elementul aj, având în vedere că secvenţa a0, a1, ..., aj-1 este
ordonată crescător.
Tot din categoria inserării face parte şi metoda shell. Pentru a explica
metoda se introduce noţiunea de h-sortare. Numim h-sortare, sortarea prin
inserare directă a următoarelor secvenţe:
a0, ah, a2h, ....
a1, a1+h, a1+2h, ....
.
.
.
ah, a2h, a3h, ....
h este numit increment.
Metoda shell constă în alegerea unui număr de k incremenţi
h1 > h2 > h3 > ... > hk = 1
şi de a face o h1 – sortare, urmată de o h2 – sortare ş. a. m. d., în final o 1 –
sortare.
Performaţele metodei sunt strâns legate de alegerea incremenţilor.
În exemplul din paragraful 2.6., funcţia sort_inserare_directă redă
algoritmul de inserare directă, iar funcţia shell_sort redă algoritmul metodei
shell.
Timpul de prelucrare în cadrul metodei de inserare directă este de
ordinul O(n2), iar al metodei shell de ordinul O(n *lnn). De asemenea
timpul de prelucrare în cadrul metodei de inserare prin căutare binară este
de ordinul O(n *lnn).
105
Principiul metodei este următorul: se selectează un element din tablou
numit pivot şi se rearanjează vectorul în doi subvectori, astfel încât cel din
stânga are toate elementele mai mici decât pivotul, iar cel din dreapta mai
mare ca pivotul. Procedeul se reia în subtabloul din stânga şi apoi în cel din
dreapta ş. a. m. d. Procedeul se termină când se ajunge cu pivotul în
extremitatea stângă şi respectiv dreaptă a tabloului iniţial.
Timpul de prelucrare a metodei quicksort este de ordinul O(n* lnn).
În exemplul de la paragraful 2.6., funcţia sort_metoda_bulelor redă
algoritmul de sortări prin metoda bulelor, iar funcţiile quicksort şi quick
redau algoritmul sortării prin metoda quicksort.
106
2.5. Sortarea prin interclasare
Metoda sortării prin interclasare a fost prezentată în cadrul lucrării nr.
10, drept exemplificare a metodei de elaborare a algoritmilor „Divide et
Impera”
2.6. Exemplu
În programul de mai jos sunt implementaţi algoritmii de sortare
prezentaţi în paragrafele 2.1. –2.5.
#include <stdio.h>
#include <conio.h>
#define nmax 100
/* ALGORITMI DE SORTARE */
107
void afisare(int n,float a[nmax])
/* AFISARE VECTOR */
{
int i;
for(i=0;i<n;i++)
{
printf("%8.2f",a[i]);
if(((i+1) % 10)==0) printf("\n");
}
}
108
x=a[j]; i=j-1;
while((i>=0) && (x<a[i]))
{
a[i+1]=a[i];
i=i-1;
}
a[i+1]=x;
}
}
109
int i,j,gata;
float x;
j=0;
do{
gata=1;
j=j+1;
for(i=0;i<n-j;i++)
if(a[i]>a[i+1]){
gata=0;
x=a[i];a[i]=a[i+1];a[i+1]=x;
};
}while(gata==0);
}
110
/* SORTAREA PRIN SELECTIE */
{
int i,j,poz;
float x;
for(i=0;i<n-1;i++)
{
x=a[i];poz=i;
for(j=i+1;j<n;j++)
if(a[j]<x) {
x=a[j];
poz=j;
}
a[poz]=a[i];a[i]=x;
}
}
void main(void)
{
int i,n;
float a[nmax],b[nmax];
clrscr();
printf("\nIntroduceti nr.elementelor n=");
scanf("%d",&n);
citire_vector(n,a,b);
printf("\nVECTORUL NESORTAT\n");
afisare(n,a);
printf("\nVECTORUL SORTAT PRIN METODA
NUMARARII\n");
sort_numarare(n,a);
afisare(n,a);
getch();
printf("\nVECTORUL SORTAT PRIN METODA
INSERARII DIRECTE\n");
reconstituire(n,a,b); /* a devine vectorul nesortat
initial */
sort_inserare_directa(n,a);
afisare(n,a);
getch();
111
printf("\nVECTORUL SORTAT PRIN METODA
SHELL\n");
reconstituire(n,a,b); /* a devine vectorul nesortat
initial */
shell_sort(n,a);
afisare(n,a);
getch();
printf("\nVECTORUL SORTAT PRIN METODA
BULELOR\n");
reconstituire(n,a,b); /* a devine vectorul nesortat
initial */
sort_metoda_bulelor(n,a);
afisare(n,a);
getch();
printf("\nVECTORUL SORTAT PRIN METODA
QUICKSORT\n");
reconstituire(n,a,b); /* a devine vectorul nesortat
initial */
quicksort(n,a);
afisare(n,a);
getch();
printf("\nVECTORUL SORTAT PRIN METODA
SELECTIEI DIRECTE\n");
reconstituire(n,a,b); /* a devine vectorul nesortat
initial */
sort_selectie(n,a);
afisare(n,a);
getch();
}
3. Mersul lucrării
3.1. Fie tabloul de întregi:
59174320
Ordonaţi, fără program, acest şir în ordinea crescătoare a
elementelor, folosind cele trei metode ‘elementare’ de sortare: inserţia,
metoda bulelor şi selecţia, arătând la fiecare pas al metodei care este noua
configuraţie a tabloului. Câte comparaţii şi câte mutări de elemente au avut
112
loc pentru fiecare metodă? Care metodă este cea mai potrivită pentru acest
tablou de întregi? Care aranjare a tabloului ar fi fost cea mai defavorabilă?
113
3.9. Să se descrie algoritmul care realizează următoarele acţiuni
specifice unui examen de admitere:
efectuarea calculului mediilor candidaţilor la examenul de
admitere
repartizarea celor admişi după opţiuni (se consideră că există m
opţiuni, fiecare cu un număr dat de candidaţi admişi) şi afişarea listei.
afişarea în ordinea descrescătoare a mediilor a tuturor
candidaţilor neadmişi.
Se consideră că examenul constă din două probe, la medii egale
(calculate cu două zecimale, prin trunchiere), departajarea se face după nota
la prima probă (dacă egalitatea se menţine, se ia în considerare a doua),
admiţându-se depăşirea numărului de locuri în caz de egalitate şi după acest
criteriu.
114
Bibliografie:
115