Sunteți pe pagina 1din 38

SUPORT DE CURS – Semestrul al II -lea

DISCIPLINA: INFORMATICA

CLASA: a XII-a Df (2022-2023)

CONȚINUTURI
1. Metoda de programare „Divide et Impera”
2. Metoda de programare “Backtracking”
3. Liste
4. Grafuri

Prof. Zaharciuc Viorica


1. METODA DE PROGRAMARE „DIVIDE ET IMPERA”

Divide et Impera este o metodă de programare bazată pe un principiu simplu:


 problema dată se descompune în două (sau mai multe) subprobleme (de același tip ca
problema inițială, dar de dimensiuni mai mici);
 se rezolvă independent fiecare subproblemă;
 se combină rezultatele obținute pentru subprobleme, obținând rezultatul problemei
inițiale.
Subproblemele trebuie să fie de același tip cu problema inițială, ele urmând a fi rezolvate prin
aceeași tehnică.
Subproblemele în care se descompun problema dată trebuie să fie:
 de același tip cu problema dată;
 de dimensiuni mai mici (mai “ușoare”);
 independente (să nu se suprapună, prelucrează seturi de date distincte).

În tehnica Divide et Impera, în urma împărțirilor succesive în subprobleme, se ajunge în


situația că problema curentă nu mai poate fi împărțită în subprobleme. O asemenea problemă
se numește problemă elementară și se rezolvă în alt mod – de regulă foarte simplu.
Divide et Impera admite de regulă o implementare recursivă – rezolvarea problemei
constă în rezolvarea unor subprobleme de același tip. Un algoritm pseudocod care descrie
metoda este:
Algoritm DivImp(P)
Dacă P este problemă elementară
R <- RezolvăDirect(P)
Altfel
[P1,P2] <- Descompune(P)
R1 <- DivImp(P1)
R2 <- DivImp(P2)
R <- Combină(R1,R2)
SfârșitDacă
SfârșitAlgoritm
Aplicații
Suma elementelor dintr-un vector
Fie un vector V cu n elemente întregi, indexate de la 1 la n. Să se determine suma lor.
Problema este binecunoscută. Cum o rezolvăm prin metoda Divide et Impera? Care sunt
subproblemele?
A împărți problema în subprobleme constă de fapt în a împărți vectorul în doi subvectori, cu
număr (aproape) egal de elemente. Primul subvector ar fi V cu elementele indexate de
la 1 la n/2 (prima jumătate a lui V), iar al doilea ar fi a doua jumătate – elementele indexate de
la n/2+1 la n.
Prima jumătate este un vector, dar a jumătate nu mai este un vector, elementele nu mai sunt
indexate de la 1 la ..., deci cele două subprobleme nu mai sunt de același tip (sau cel puțin nu
în mod direct).
Putem reformula problema inițială astfel:
Fie un vector V cu n elemente întregi, indexate de la 1 la n. Determinați suma elementelor din
secvență delimitată de indicii 1 și n.
Vom realiza o funcție care să determine pentru vectorul V suma elementelor din secvența
delimitată de indicii st și dr. Pentru a rezolva problema dată vom apela funcția cu
parametrii st=1 și dr=n. Această abordare are două avantaje:
 putem rezolva problema prin metoda divide et impera – o secvență poate fi împărțită în
alte după secvențe, de dimensiuni mai mici;
 putem folosi funcția realizată pentru a determina suma elementelor din orice secvența a
vectorului.
Pentru secvența delimitată de st și dr, procedăm astfel:
 dacă st == dr, atunci suma este V[st];
 altfel:
o împărțim problema în subprobleme: determinăm mijlocul secvenței, m = (st + dr) /
2; astfel, obținem două secvențe: prima conține elementele cu indici între st și m,
iar a doua conține elementele cu indici între m+1 și dr (observăm că cele două
secvențe nu au elemente comune – subproblemele sunt independente);
o rezolvăm cele două subprobleme în același mod:
 determinăm S1 = suma din prima secvență
 determinăm S2 = suma din a doua secvență;
o combinăm rezultatele: suma pe secvența inițială este egală cu S1 + S2.

Secvență C++:
int Suma(int V[], int st, int dr)
{
if(st == dr)
return V[st]; // problemă elementară
else
{
int m = (st + dr) / 2; // împărțim problema în subprobleme
int s1 = Suma(V, st, m); // rezolvăm prima subproblemă
int s2 = Suma(V, m + 1, dr); // rezolvăm a doua subproblemă
return s1 + s2; // combinăm rezultatele
}
}
int main()
{
int V[101], n;
//citire n si V
cout << Suma(V,1,n);
return 0;
}

Cmmdc al elementelor dintr-un vector

Fie un vector V cu n elemente naturale nenule, indexate de la 1 la n. Să se determine cel mai


mare divizor comun al lor.
La fel în cazul problemei precedente, o transformă într-una cu secvențe. Vom determina cel
mai mare divizor comun al elementelor dintr-o secvență delimitată de indicii st și dr.
CMMDC(V,st,dr):
 dacă secvența este formată dintr-un singur element (st == dr), atunci rezultatul este
chiar V[st];
 altfel:
o determinăm indicele de la mijloc, m = (st + dr) / 2;
o determinăm recursiv a = CMMDC(V, st, m);
o determinăm recursiv b = CMMDC(V, m + 1, dr);
o rezultatul este Cmmdc2(a,b), unde Cmmdc2(x,y) este cel mai mare divizor comun
a lui x și y, și poate fi determinat cu algoritmul lui Euclid.

2. METODA DE PROGRAMARE “BACKTRACKING”


Metoda backtracking poate fi folosită în rezolvarea a diverse probleme. Este o metodă
lentă, dar de multe ori este singura pe care o avem la dispoziție!
Introducere
Metoda backtracking poate fi aplicată în rezolvarea problemelor care respectă următoarele
condiții:
 soluția poate fi reprezentată printr-un tablou x[]=(x[1], x[2], ..., x[n]), fiecare
element x[i] aparținând unei mulțimi cunoscute Ai;
 fiecare mulțime Ai este finită, iar elementele ei se află într-o relație de ordine precizată –
de multe ori cele n mulțimi sunt identice;
 se cer toate soluțiile problemei sau se cere o anumită soluție care nu poate fi
determinată într-un alt mod (de regulă mai rapid).
Algoritmul de tip backtracking construiește vectorul x[] (numit vector soluție) astfel:
Fiecare pas k, începând (de regulă) de la pasul 1, se prelucrează elementul curent x[k] al
vectorului soluție:
 x[k] primește pe rând valori din mulțimea corespunzătoare Ak;
 la fiecare pas se verifică dacă configurația curentă a vectorului soluție poate duce la o
soluție finală – dacă valoarea lui x[k] este corectă în raport cu x[1], x[2], … x[k-1]:
o dacă valoarea nu este corectă, elementul curent X[k] primește următoarea
valoare din Ak sau revenim la elementul anterior x[k-1], dacă X[k] a primit toate
valorile din Ak – pas înapoi;
o dacă valoarea lui x[k] este corectă (avem o soluție parțială), se verifică existența
unei soluții finale a problemei:
dacă configurația curentă a vectorului soluție x reprezintă soluție finală
(de regulă) o afișăm;
 dacă nu am identificat o soluție finală trecem la următorul element, x[k+1],
și reluăm procesul pentru acest element – pas înainte.
Pe măsură ce se construiește, vectorul soluție x[] reprezintă o soluție parțială a problemei.
Când vectorul soluție este complet construit, avem o soluție finală a problemei.
Exemplu
Să rezolvăm următoarea problemă folosind metoda backtracking, “cu creionul pe hârtie”: Să
se afișeze permutările mulțimii {1, 2, 3}.
Ne amintim că un șir de numere reprezintă o permutare a unei mulțimi M dacă și numai dacă
conține fiecare element al mulțimii M o singură dată. Altfel spus, în cazul nostru:
 are exact 3 elemente;
 fiecare element este cuprins între 1 și 3;
 elementele nu se repetă.
Pentru a rezolva problemă vom scrie pe rând valori din mulțimea dată și vom verifica la fiecare
pas dacă valorile scrise duc la o permutare corectă:
k x[] Observații

1 1 – – corect, pas înainte

2 1 1 – greșit

2 1 2 – corect, pas înainte

3 1 2 1 greșit

3 1 2 2 greșit

3 1 2 3 soluție finală 1

3 1 2 ! am terminat valorile posibile pentru x[ 3 ], pas înapoi

2 1 3 – corect, pas înainte

3 1 3 1 greșit

3 1 3 2 soluție finală 2

3 1 3 3 greșit

3 1 3 ! am terminat valorile posibile pentru x[ 3 ], pas înapoi

2 1 ! – am terminat valorile posibile pentru x[ 2 ], pas înapoi

1 2 – – corect, pas înainte


2 2 1 – corect, pas înainte

3 2 1 1 greșit

3 2 1 2 greșit

3 2 1 3 soluție finală 3

3 2 1 ! am terminat valorile posibile pentru x[ 3 ], pas înapoi

2 2 2 – greșit

2 2 3 – corect, pas înainte

3 2 3 1 soluție finală 4

3 2 3 2 greșit

3 2 3 3 greșit

3 2 3 ! am terminat valorile posibile pentru x[ 3 ], pas înapoi

2 2 ! – am terminat valorile posibile pentru x[ 2 ], pas înapoi

1 3 – – corect, pas înainte

2 3 1 – corect, pas înainte

3 3 1 1 greșit

3 3 1 2 soluție finală 5

3 3 1 3 greșit

3 3 1 ! am terminat valorile posibile pentru x[ 3 ], pas înapoi

2 3 2 – corect, pas înainte

3 3 2 1 soluție finală 6

3 3 2 2 greșit
3 3 2 3 greșit

3 3 2 ! am terminat valorile posibile pentru x[ 3 ], pas înapoi

2 3 3 – greșit

2 3 ! – am terminat valorile posibile pentru x[ 2 ], pas înapoi

1 ! – – am terminat valorile posibile pentru x[ 1 ], pas înapoi

Formalizare
Pentru a putea realiza un algoritm backtracking pentru rezolvarea unei probleme trebuie să
răspundem la următoarele întrebări:
1. Ce memorăm în vectorul soluție x[]? Uneori răspunsul este direct; de exemplu, la
generarea permutărilor vectorul soluție reprezintă o permutare a mulțimii A={1,2,...,n}. În
alte situații, vectorul soluție este o reprezentare mai puțin directă a soluției; de
exemplu, generarea submulțimilor unei mulțimi folosind vectori caracteristici sau
Problema reginelor.
2. Ce valori poate lua fiecare element x[k] vectorului soluție și câte elemente poate
avea x[]? Altfel spus, care sunt mulțimile Ak. Vom numi aceste restricții condiții
externe. Cu cât condițiile externe sunt mai restrictive (cu cât mulțimile Ak au mai puține
elemente), cu atât va fi mai rapid algoritmul!
3. Ce condiții trebuie să îndeplinească x[k] ca să fie considerat
corect? Elementul x[k] a primit o anumită valoare, în conformitate ce condițiile externe.
Este ea corectă? Poate conduce la o soluție finală? Aceste condiții se numesc condiții
interne și în ele pot să intervină doar x[k] și elementele x[1], x[2], …, x[k-1].
Elementele x[k+1], …, x[n] nu poti apărea în condițiile interne deoarece încă nu au fost
generate!!!
4. Am găsit o soluție finală? Elementul x[k] a primit o valoare conformă cu condițiile
externe, care respectă condițiile interne. Am ajuns la soluție finală sau continuăm
cu x[k+1]?
Exemplu. Pentru problema generării permutărilor mulțimii A={1,2,3,…,n}A={1,2,3,…,n},
condițiile de mai sus sunt:
1. Vectorul soluție conține o permutare a mulțimii AA;
2. Condiții externe: x[k]∈{1,2,3,…,n}x[k]∈{1,2,3,…,n} sau x[k]=1,n¯¯¯¯¯¯¯¯x[k]=1,n¯,
pentru k=1,n¯¯¯¯¯¯¯¯k=1,n¯
3. Condiții interne: x[k]≠x[i]x[k]≠x[i], pentru i=1,k−1¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯i=1,k−1¯
4. Condiții de existență a soluției: k=nk=n
Algoritmul general
Metoda backtracking poate fi implementată iterativ sau recursiv. În ambele situații se se
folosește o structură de deate de tip stivă. În cazul implementării iterative, stiva trebuie
gestionată intern în algoritm – ceea ce poate duce la dificulăți în implementăre. În cazul
implementării recursive se folosește spațiu de memorie de tip stivă – STACK alocat
programului; implementarea recursivă este de regulă mai scurtă și mai ușor de înțeles. Acest
articol prezintă implementări recursive ale metodei.
Următorul subprogram recursiv prezintă algoritmul la modul general:
 la fiecare apel BACK(k) se generează valori pentru elementul x[k] al vectorului soluție;
 instrucțiunea Pentru modelează condițiile externe;
 subprogramul OK(k) verifică condițiile interne
 subprogramul Solutie(k) verifică dacă configurația curentă a vectorului soluție reprezintă
o soluție finală
 subprogramul Afisare(k) tratează soluția curentă a problemei – de exemplu o afișează!

subprogram BACK(k)
┌ pentru fiecare element i din A[k] executa
│ x[k] ← i
│ ┌ daca OK(k) atunci
│ │ ┌ daca Solutie(k) atunci
│ │ │ Afisare(k)
│ │ │ altfel
│ │ │ BACK(k+1)
│ │ └■
│ └■
└■
sfarsit_subprogram

Observații:
 de cele mai multe ori mulțimile AA sunt de
forma A={1,2,3,….,n}A={1,2,3,….,n} sau A={1,2,3,….,m}A={1,2,3,….,m} sau A={
a,a+1,a+2,….,b}A={a,a+1,a+2,….,b} sau o altă formă astfel încât să putem scrie
instrucțiunea Pentru conform specificului limbajului de programare folosit – eventual
folosind o structură repetitivă de alt tip! Dacă este necesar, trebuie realizate unele
transformări încât mulțimile să ajungă la această formă!
 elementele mulțimii AA pot fi in orice ordine. Contează însă ordinea în care le vom
parcurge în instrucțiunea Pentru, deoarece în probleme este precizată de obicei o
anumită ordine în care trebuie generate soluțiile:
o dacă parcurgem elementele lui AA în ordine crescătoare vom obține soluții în
ordine lexicografică;
o dacă parcurgem elementele lui AA în ordine descrescătoare vom obține soluții
în ordine invers lexicografică.
 în anumite probleme determinarea unei soluții finale nu conduce la întreruperea
apelurilor recursive. Un exemplu este generarea submulțimilor unei mulțimi. În acest
caz algoritmul de mai sus poate fi modificat astfel:
┌ daca OK(k) atunci
│ ┌ daca Solutie(k) atunci
│ │ Afisare(k)
│ └■
│ BACK(k+1)
└■

Bineînțeles, trebuie să ne asigurăm că apelurile recursive se opresc!


Un șablon C++
Următoarea secvență C++ oferă un șablon pentru rezolvarea unei probleme oarecare folosind
metoda backtracking. Vom considera în continuare următoarele condiții
externe: x[k]=A,B¯¯¯¯¯¯¯¯¯¯x[k]=A,B¯, pentru k=1,n¯¯¯¯¯¯¯¯k=1,n¯.
În practică AA și BB vor avea valori specifice problemei:

#include <fstream>
using namespace std;
int x[10] ,n;
int Solutie(int k){
// x[k] verifică condițiile interne
// verificare dacă x[] reprezintă o soluție finală
return 1; // sau 0
}
int OK(int k){
// verificare conditii interne
return 1; // sau 0
}
void Afisare(int k)
{
// afișare/prelucrare soluția finală curentă
}
void Back(int k){
for(int i = A ; i <= B ; ++i)
{
x[k]=i;
if( OK(k) )
if(Solutie(k))
Afisare(k);
else
Back(k+1);
}
}
int main(){
//citire date de intrare
Back(1);
return 0;
}

De multe ori condițiile de existență a soluției sunt simple și nu se justifică scrierea unei funcții
pentru verificarea lor, ele putând fi verificate direct în funcția Back().

De cele mai multe ori, rezolvarea unei probleme folosind metoda backtracking constă în
următoarele:
1. stabilirea semnificației vectorului soluție;
2. stabilirea condițiilor externe;
3. stabilirea condițiilor interne;
4. stabilirea condițiilor de existența a soluției finale;
5. completarea adecvată a șablonului de mai sus!
Complexitatea algoritmului
Algoritmii Backtracking sunt exponențiali. Complexitatea depinde de la problemă la
problemă dar este de tipul O(an)O(an). De exemplu:
 generarea permutărilor unei mulțimi cu n elemente are complexitatea O(n!)O(n!);
 generarea submulțimilor unei mulțimi cu n elemente are complexitatea O(2n)O(2n)
 produsul cartezian AnAn unde mulțimea A={1,2,3,…,m}A={1,2,3,…,m} are
complexitatea O(mn)O(mn)
 etc.
3. LISTE
Liste liniare - noțiuni introductive
Lista liniară este o structură de date logică, cu date omogene, în care fiecare element
are exact un element predecesor și exact un element succesor, cu excepția primului și al
ultimului element.
Elementele unei liste se mai numesc noduri. Lungimea unei liste reprezintă numărul
de noduri din listă. O lista care nu are niciun element se numește listă vidă.
Asupra listei se pot executa anumite operații:
 inițializarea listei – crearea unei liste vide;
 crearea listei – adăugarea repetată a unor elemente, pornind de la o listă vidă;
 inserarea unui element în listă – la început, la sfârșit, în interior;
 ștergerea unui element din listă – de la început, de la sfârșit, din interior;
 parcurgerea listei – analizarea elementelor listei, pentru a obține anumite informații
despre ele;
 căutarea unui element într-o listă;
 concatenarea a două liste, divizarea unei liste.
O modalitate deja cunoscută de implementare a listelor este prin intermediul tablourilor
statice, obținându-se astfel liste liniare secvențiale. Tablourile au fost studiate deja, precum
și operațiile amintite mai sus; ele au o serie de avantaje care trebuie reținute:
 accesul la un anumit element se face prin indicele acestuia și este foarte rapid;
 tablourile sunt zone contigue de memorie – elementele sunt alocate în memorie în
zone învecinate;
 elementele listei conțin numai informațiile utile.
Ele au și o serie de dezavantaje:
 operațiile de inserare și ștergere a elementelor presupun parcurgerea tabloului, ceea
ce duce la algoritmi lenți;
 dimensiunea tablourilor (numărul de elemente) este precizat la compilare, iar la
execuție pot să apară următoarele situații:
o spațiul alocat pentru tablou poate fi insuficient;
o spațiul alocat pentru tablou poate fi mult mai mare decât este necesar.
O altă modalitate de implementare a listelor este sub forma listelor liniare alocate
dinamic. În acest caz, fiecare element al listei este o variabilă dinamică; aceasta va conține,
pe lângă informația utilă și informația de legătură, adică adresa elementului succesor și,
eventual, adresa elementului precedent. Sigur, aceste adrese vor fi memorate prin
intermediul pointerilor.
Dacă un nod al listei conține, alături de informația utilă, doar adresa următorului
element, avem o listă alocată dinamic simplu înlănțuită; pentru a gestiona o asemenea listă
trebuie să cunoaștem adresa primului element al listei. Următoarele elementele vor fi
descoperite succesiv, elementul curent conținând ca informație de legătură adresa următorului
element.
Dacă un nod al listei conține, alături de informația utilă, atât adresa elementului următor, cât și
a celui precedent, avem o listă liniară dublu înlănțuită; de regulă cunoaștem adresa primului
și/sau a ultimului element, dar pentru a gestiona o asemenea listă este suficient să cunoaștem
adresa unui element oarecare a acesteia. Pornind de la acesta pot fi determinate primul și
ultimul element al listei.
1. Aspecte teoretice
1.1.Definitia si operatii asupra listelor
O lista L e o secventa de zero sau mai multe elemente, numite noduri, toate fiind de
acelasi tip de baza T.
L=a1,a2,...,an (n>=0)
Daca n>=1, a1 se spune ca este primul nod al listei, iar an, ultimul nod. Daca n=0, lista
este vida.
O proprietate importanta a unei liste este aceea ca nodurile sale pot fi ordonate liniar
functie de pozitia lor in cadrul listei. Se spune ca ai precede pe ai+1 (i=1,2,...,n-1),iar ai
succede pe ai-1 (i=2,3,...,n), ai aflindu-se pe pozitia i.
Se postuleaza existenta pozitiei urmatoare ultimului element al listei si se introduce functia
FIN(L) ce va returna pozitia urmatoare pozitiei n din lista L de n elemente.
Folosind notatiile anterioare si notind x:T un nod al lis- tei, iar p fiind de tip pozitie, se
introduce urmatorul set reprezentativ de operatori aplicabili obiectelor de tip lista:
INSEREAZA(L,x,p)-insereaza in lista L nodul x, in pozitia p;
daca L=a1,a2,...,an, in urma insertiei:
p < FIN(L),L=a1,...,ap-1,x,ap+1,...,an
p=FIN(L),L=a1,...,an,x
p > FIN(L),rezultatul insertiei este imprevi-
zibil.
1.2.Tehnici de implementare a listelor
1.2.a.Implementarea listelor cu ajutorul tipului tablou
O lista se asimileaza cu un tablou, nodurile fiind elementele tabloului memorate in
locatii succesive de memorie. In aceasta reprezentare, lista poate fi usor traversata, noile
noduri pot fi inserate la sfirsitul listei. Insertia unui nod in interiorul listei presupune
deplasarea tuturor nodurilor urmatoare cu o pozitie spre sfirsitul listei, iar suprimarea,
deplasarea tuturor nodurilor urmatoare cu o pozitie spre inceputul listei.
Tipul Lista se defineste ca un articol cu doua cimpuri:
-primul e un tablou cu elementele de tip Nod, cu lungimea aleasa astfel incit sa poata
acoperi cea mai mare dimensiune de lista ce apare in respectiva aplicatie;
-al doilea e pozitia ultimului nod al listei
Functia FIN(l) va returna valoarea ultim+1.
Declaratiile cele mai importante intr-o astfel de implementare sunt:

const LungMax=100;
type Lista=record noduri:array [1..LungMax] of Nod;
ultim:integer {0..lungmax}
end;
Pozitie=integer;
function FIN(var l:Lista):Pozitie;
begin
FIN:=l.ultim+1
end;

Cu declaratiile de mai sus, implementarea operatorilor prezentati anterior devine


simpla.
1.2.b.I mplementarea listelor cu ajutorul tipului pointer. Structuri recursive de
tip lista
Cu ajutorul tipului pointer, se defineste structura unui nod al listei liniare care apare
ca o structura recursiva, avind o componenta de tip identic cu al structurii complete.
type PointerNod=^Nod;
Nod=record cheie:TipCheie;
urmator:PointerNod;
info:TipInfo
end;

var inceput:PointerNod;
Caracteristica unei astfel de structuri consta in prezenta unei singure inlantuiri.
Cimpul cheie serveste la identificarea nodului, cimpul urmator e pointer de inlantuire la
nodul urmator, iar cel info contine informatia utila.
Variabila inceput indica spre primul nod al listei; in unele situatii in locul lui inceput
se utilizeaza un nod fictiv, adica o variabila de tip nod cu cimpurile cheie si info
neprecizate, dar cimpul urmator indicind spre primul nod al listei.
De asemenea uneori e util a se pastra pointerul spre ultimul nod al listei.
O varianta este a listelor circulare la care dispare notiunea de prim, ultim nod, lista fiind un
pointer ce se plimba pe lista.
1.2.b.1.Tehnici de insertie a nodurilor si de creare a listelor inlantuite
a)insertia unui nod la inceputul listei
Daca inceput e variabila pointer ce indica spre primul nod al listei, iar q o variabila auxiliara
de tip pointer, secventa urmatoare realizeaza insertia la inceputul listei si actualizeaza
pointerul inceput:
new(q); {creaza spatiu pentru un nou nod}
q^.urmator:=inceput;
{asignarea cimpurilor cheie si info}
inceput:=q;
Secventa e corecta si pentru insertia intr-o lista vida, caz in care inceput=nil (nil fiind
pointerul vid, care nu se refera la nici o variabila indicata).
b)insertia unui nod la sfirsitul listei
Devine mai simpla daca se pastreaza o variabila sfirsit indicind spre ultimul nod al listei:
new(q); {creaza spatiu pentru noul nod ultim al listei}
sfirsit^.urmator:=q;
q^.urmator:=nil;
{asignarea cimpurilor cheie si info}
sfirsit:=q;
Pentru insertia la sfirsitul listei e necesara existenta a cel putin un nod, care se creaza prin
procedura de la paragraful anterior.
c)insertia unui nod dupa unul indicat
E simpla pentru ca se cunoaste pointerul spre nodul anterior si spre cel urmator celui ce
se insereaza (pointerul spre nodul urmator e valoarea cimpului urmator al nodului indicat).
d)insertia unui nod in fata unui nod indicat
Printr-un artificiu, se reduce acest caz la cel anterior: se insereaza un nod dupa cel indicat,
cheia si informatia din nodul indicat fiind atribuite noului nod inserat si fiind inlocuite cu
valorile nodului ce trebuia inserat.
1.2.b.2.Tehnici de suprimare
Pentru suprimarea nodului urmator celui indicat de o variabila pointer q, prin atribuirea:
q^.urmator:=q^.urmator^.urmator;
se exclud din lista legaturile la si de la nodul de suprimat.
Pentru suprimarea nodului anterior unuia precizat, se aduce nodul precizat in cel de
suprimat si se suprima succesorul lui, deci cel indicat initial de variabila pointer q:
q^:=q^.urmator^.
1.2.b.3.Traversarea unei liste inlantuite
Daca nodul de inceput al listei e indicat de variabila inceput, o variabila auxiliara q, care
parcurge toate nodurile listei pina cind valoarea ei devine nil, permite accesul la fiecare
nod si efectuarea operatiei specifice traversarii.
1.2.c.Implementarea listelor cu ajutorul cursorilor
Implementarea e specifica limbajelor ce nu definesc tipul pointer (Fortran, Algol, Basic).
Pointerii pot fi simulati cu ajutorul cursorilor, care sint valori intregi ce indica pozitia in
cadrul tablourilor.
Mai multe liste ce contin acelasi tip Nod de elemente se vor grupa intr-un tablou cu
elemente articole cu doua cimpuri: primul de tip Nod, iar al doilea de tip intreg, folosit ca si
cursor.
const LMax=1000; {valoare mai mare decit suma dimensiunilor maxime
ale listelor}
type Cursor=0..LMax;
Articol=record nodl:Nod;
urmator:Cursor
end;
Lista=cursor;
var zona:array [1..LMax] of articol;
l,disponibil:Lista;

Pentru fiecare lista existenta in tabloul zona se declara o variabila intrega l (de tip
Lista), reprezentind cursorul spre intrarea din tablou ce contine primul nod al listei
(zona[l].nodl este primul nod al listei).Nodul succesor e indicat de cursorul prezent in
cimpul urmator al nodului curent. Ultimul nod al listei e in elementul de tablou ce contine in
cimpul urmator o valoare invalida (0, tabloul avind primul indice 1).
Analog se defineste o lista a liberilor, continind intrarile libere din tablou inlantuite, o
variabila disponibil indicind spre prima intrare libera.
1.3.Aplicatii ale listelor inlantuite
1.3.1.Liste ordonate si reorganizarea listelor
a)cautarea intr-o lista neordonata; tehnica fanionului
Se considera o lista simplu inlantuita, cu nodurile de tip Nod. Daca inceput indica spre
primul nod al listei, iar ordinea cheilor in lista este aleatoare, cautarea unei chei implica
traversarea listei. Functia booleana gasit returneaza valoarea true si pointerul spre nodul
cu cheia egala cu cea cautata, daca un astfel de nod exista si valoarea false in caz
contrar:

function gasit(val:TipCheie;var poz:PointerNod):boolean;


var found:boolean;
begin
poz:=inceput;found:=false;
while (poz<>nil) and not found do
if poz^.cheie=val then found:=true
else poz:=poz^.urmator;
gasit:=found
end;

Cautarea se poate perfectiona prin utilizarea metodei fanionului, lista prelungindu-


se cu un nod fictiv numit fanion, la creare lista continind acest unic nod. In functia gasit,
inainte de baleierea listei, informatia cautata se introduce in cheia nodului fanion, astfel
incit va exista cel putin un nod cu cheia cautata:

var fanion:PointerNod;
...
function gasit(val:TipCheie;var poz:PointerNod):boolean;
begin
poz:=inceput;fanion^.cheie:=val;
while poz^.cheie<>val do poz:=poz^.urmator;
gasit:=poz<>fanion
end;

b)crearea unei liste ordonate; tehnica celor doi pointeri


In continuare se prezinta o metoda foarte simpla pentru crearea unei liste ordonate,
tipurile PointerNod si Nod fiind cele definite anterior. Lista se initializeaza cu doua noduri
fictive pointate de doua variabile pointer, inceput si fanion:
var inceput, fanion:PointerNod;
procedure init;
begin
new(inceput);
new(fanion);
inceput^.urmator:=fanion
end;

Pentru introducerea unei noi chei in lista, pastrind ordonarea, se va scrie o functie
gasit, care daca gaseste cheia in lista returneaza valoarea true si pointerii p1 spre nodul
gasit si p2 spre cel anterior, respectiv in cazul negasirii cheii, valoarea false si pointerii p1
si p2 spre nodurile intre care trebuie facuta insertia:

function gasit(val:TipCheie;var p1,p2:TipPointer):boolean;


begin
p2:=inceput;
p1:=p2^.urmator;
fanion^.cheie:=val;
while p1^.cheie<="">fanion)
end;

Fragmentul de program ce insereaza o noua cheie este:

var p1,p2,p3:PointerNod;val:TipCheie;
...
if not gasit(val,p1,p2) then
begin
new(p3);
p2^.urmator:=p3;
with p3^ do
begin
cheie:=val;
{info}
urmator:=p1
end
end;

Pentru o tiparire a cheilor dintr-o lista ordonata astfel creata, pointerul ce parcurge
nodurile trebuie sa fie initializat cu valoarea pointerului spre primul nod efectiv al listei,
urmator celui inceput, iar parcurgerea listei se face pina la intilnirea nodului fanion:

var p:PointerNod;
...
p:=inceput^.urmator;
while p<>fanion do
begin
{prelucrare nod}
p:=p^.urmator
end;
c) tehnica de cautare in lista cu reordonare
In compilatoare, structurile de date de tip lista liniara sint foarte avantajoase in
crearea si exploatarea listei identificatorilor. Conform principiului localizarii, aparitia unui
identificator oarecare in textul sursa, poate fi urmata cu mare probabilitate de una sau mai
multe reaparitii.
Pornind de la acest principiu, s-a conceput metoda de cautare cu reordonare, care
consta in aceea ca ori de cite ori un identificator se cauta si se gaseste in lista, el se aduce
la inceputul listei, astfel incit la proxima aparitie, deci cautare, el se va gasi la inceputul
listei, iar daca nu s-a gasit se insereaza la inceputul listei.

Stiva
Stiva (stack) este o structură de date liniară abstractă, pentru care sunt definite
operațiile de adăugare a unui element și eliminare a unui element și aceste operații se
realizează la un singur capăt al structurii, numit vârful stivei.
În timpul operațiilor cu stiva avem acces numai la elementul din vârful stivei.
Operații cu stiva
Cu o stivă se pot face următoarele operații:
 inițializarea stivei – crearea unei stive vide;
 verificarea faptului că o stivă este sau nu vidă;
 adăugarea unui nou element pe stivă – elementul devine vârful stivei. Operația se
numește push;
 eliminarea unui element de pe stivă – se va elimina vârful stivei. Un nou element
devine vârf al stivei, sau ea devine vidă. Operația se numește pop;
 identificarea valorii elementului din vârful stivei – accesul la acel element Operația se
numește top.
Imaginați-vă o stivă de lăzi într-un depozit. Dacă adăugăm încă o ladă, o vom plasa în
vârful stivei. Dacă luăm o ladă, o vom lua pe cea din vârful stivei – altfel s-ar răsturna stiva!!
Deoarece operațiile cu elementele stivei se fac la același capăt, spunem că stiva este o
structură de date de tip LIFO – Last In First Out (ultimul intrat, primul ieșit).
Când folosim stiva?
Programele au la dispoziție memorie, iar o parte a ei se numește de tip stivă – STACK, tocmai
pentru că operațiile cu această memorie se fac pe principiul stivei. Apelul functiilor, deci și
recursivitatea, folosesc memoria de tip stivă, iar înțelegerea acestor concept cere înțelegerea
modului în care funcționează o stivă.
În programe putem folosi stiva atunci când vrem să amânăm efectuarea unor operații până la
obținerea unor rezultate. De exemplu, conversia unui număr din baza 10 în baza 2 constă în
efectuarea succesivă a unor împărțiri la 2. Cifrele reprezentării în baza 2 sunt resturile
împărțirii în ordine inversă. Ne putem imagina că la fiecare împărțire plasăm restul pe o stivă.
În final golim stiva și afișăm valorile întâlnite.
Modalități de implementare a stivei
La fel ca în cazul cozii, stiva poate fi implementată în limbajul C++ în mai multe moduri:
 implementare statică, prin intermediul tablourilor;
 implementare dinamică, prin intermediul listelor alocate dinamic;
 folosirea containerului stack din STL.
Acest articol prezintă implementarea statică și folosirea STL.
Implementarea statică a stivei
Vom folosi un tablou alocat static și o variabilă simplă prin care identificăm vârful stivei. În
continuare considerăm o stivă cu elemente întregi.
Declarații
const int DIM = 100;
int S[DIM], vf;

Inițializarea stivei – crearea unei stive vide

vf = 0;

Verificarea faptului că stiva este vidă

vf == 0 // stivă vidă
vf > 0 // stivă nevidă

Adăugarea unui element – PUSH

S[vf++] = _VALOARE ;

Eliminarea unui element – POP

vf --;

Identificarea valorii din vârful stivei – TOP

S[vf-1]

Folosirea containerului STL stack


Standard Template Library pune la dispoziție un container adaptor specializat pentru
gestionarea unei stive, numit stack. Un obiect de tip stack încapsulează toate operațiile
specifice stivei:
Declarații

#include <stack>

Inițializarea stivei – crearea unei stive vide

stack<int> S;

Verificarea faptului că stiva este vidă

S.empty() // true dacă stiva este vidă, false în caz contrar

Adăugarea unui element – PUSH

S.push( _VALOARE );

Eliminarea unui element – POP

S.pop();
Identificarea valorii din vârful stivei – TOP

S.top()

Coada
Coada (queue) este o structură de date abstractă în care operația de adăugare se realizează
la un capăt, iar cea de eliminare se realizează la celălalt capăt.
În timpul operațiilor cu coada avem acces la un singur element, cel aflat la începutul cozii –
elementul care urmează să se elimine.
Operații cu coada
Cu o coadă se pot face următoarele operații:
 inițializarea cozii – crearea unei cozi vide;
 verificarea faptului că o coadă este sau nu vidă;
 adăugarea unui nou element în coadă. Operația se numește push;
 eliminarea unui element din coadă. Operația se numește pop;
 identificarea valorii elementului de la începutul cozii – accesul la acel element Operația
se numește front.
Operațiile cu coada sunt similare cu modul în care funcționează coada la casa de bilete
a unui cinematograf. Spectatorii vin și se așează în ordine la coadă, ordinea în care cumpără
biletele este aceea în care au sosit.
Deoarece operațiile de eliminare se fac în aceeași ordine ca cele de adăugare, coada
este o structură de date de tip FIFO – First In First Out.
Cum folosim coada?
Vom folosi coada atunci când trebuie să prelucrăm informații într-o ordine precisă, pe
măsură ce acestea sunt identificate. Uneori, informațiile noi sunt determinate pe baza celor
vechi, deja existente în coadă. Mecanismul se numește expandare a cozii și constă în
următoarele:
 adăugăm în coadă informații inițiale
 cât timp coada este nevidă (sau până când am determinat rezultatul căutat)
o scoatem din coadă un element
o cu ajutorul său identificăm noi informații pe care le adăugăm în coadă
În informatică se folosește coada în numeroase situații:
 tipărirea documentelor la imprimantă se face printr-o coadă de așteptare – fiecare
document se adaugă în coadă imprimantei, iar tipărirea propriu-zisă se face în
momentul eliminări din coadă
 evaluarea soluțiilor la probleme pe pbinfo.ro se face pe principiul cozii, în ordinea în
care au fost postate
Modalități de implementare a cozii
La fel ca stiva, și coada poate fi implementată în limbajul C++ în mai multe moduri:
 implementare statică, prin intermediul tablourilor;
 implementare dinamică, prin intermediul listelor alocate dinamic;
 folosirea containerului queue din STL.
Acest articol prezintă implementarea statică și folosirea STL.
Implementarea statică a cozii
Vom folosi un tablou unidimensional alocat static Q și două variabile simple prin care
identificăm începutul (st) și sfârșitul stivei (dr). Numele variabilelor provine de
la stânga și dreapta, deoarece adăugarea unui element în coadă se face adăugând un
element în tabloul suport Q, iar eliminare se face mărind variabila st – ignorând elementele din
față, fără a le șterge efectiv.
În continuare considerăm o coadă cu elemente întregi.
Declarații
const int DIM = 1000;
int Q[DIM], st, dr;

Inițializarea cozii – crearea unei cozi vide


st = 1 , dr = 0;

Verificarea faptului că este vidă coada


st > dr // stivă vidă
st <= dr // stivă nevidă

Adăugarea unui element – PUSH


Q[++dr] = _VALOARE ;

Eliminarea unui element – POP


st ++;

Identificarea valorii de la începutul cozii – TOP


Q[st]

Folosirea containerului STL stack


Standard Template Library pune la dispoziție un container adaptor specializat pentru
gestionarea unei cozi, numit queue. Un obiect de tip queue încapsulează toate operațiile
specifice cozii:
Declarații
#include <queue>

Inițializarea cozii – crearea unei cozi vide


queue<int> Q;

Verificarea faptului că este vidă coada


Q.empty() // true dacă este vidă, false în caz contrar

Adăugarea unui element – PUSH


Q.push( _VALOARE );

Eliminarea unui element – POP


Q.pop();

Identificarea valorii de la începutul cozii – TOP


Q.top()

Liste liniare simplu înlănțuite alocate dinamic


O listă liniară simplu înlănțuită conține elemente (noduri) a căror valori constau din două părți:
informația utilă și informația de legătură. Informația utilă reprezintă informația propriu-zisă
memorată în elementul liste (numere, șiruri de caractere, etc.), iar informația de legătură
precizează adresa următorului element al listei. În C/C++ putem folosi următorul tip de date
pentru a memora elementele unei liste liniare simplu înlănțuite alocate dinamic:
struct nod{
int info;
nod * urm;
};

Câmpul info al tipului nod reprezintă informația utilă – în acest caz un număr întreg, iar
câmpul urm este de tip pointer la nod și reprezintă informația de legătură.
În program vom folosi o variabilă de tip pointer (de exemplu prim) pentru a memora
adresa primului element al listei și fiecare element al listei, începând cu primul, va memora în
câmpul urm adresa elementului următor. Excepție face ultimul element al listei care va
memora în câmpul urm valoarea NULL.
Observații:
 La început prim va avea valoarea NULL, cu semnificația că lista este vidă. Dacă la un
moment dat lista redevine vidă (de exemplu se șterg toate elementele ei)
variabila prim va avea valoarea NULL.
 Elementele listei sunt variabile dinamice, create cu ajutorul operatorului C++ new și
gestionate prin intermediul pointerilor. Variabila prim este de tip pointer, dar este (în
cele ce urmează) statică.
 Fiind variabile dinamice, pentru elementele listei se alocă memorie în HEAP.
 Informațiile de legătură ocupă memorie. Spațiul de memorie ocupat de un pointer
depinde de versiunea compilatorului folosit; în general este de 4 octeți. Astfel, fiecare
element al unei liste de tipul de mai sus va ocupa în memorie 4+4=8 octeți.
 Accesul la un nod al listei se face prin parcurgerea nodurilor care îl preced.
O secvență C++ care conține declarațiile corespunzătoare poate fi:

struct nod{
int info;
nod * urm;
};

nod * prim = NULL;

În continuare vom prezenta secvențe/funcții C++ pentru principalele operații:


 crearea unui element nou,
 adăugarea unui element la sfârșitul listei,
 adăugarea unui element la începutul listei,
 parcurgerea listei,
 ștergere unui element din listă,
 inserarea unui element în listă.
Funcțiile care urmează vor avea ca parametru adresa primului element al listei și
eventual alți parametri. În funcție de situație, parametrul care reprezintă adresa primului
element ale listei va fi transmis prin valoare sau prin referință.
Crearea unui element nou
Numeroase operații cu liste solicită crearea unui nou element/nod. Pentru aceasta
trebuie să ținem cont de următoarele:
 Nodurile sunt variabile dinamice. Crearea unui nou nod înseamnă crearea unei
variabile dinamice. Acest lucru se face cu ajutorul operatorului C++ new, care are ca
rezultat adresa variabilei nou create. Aceasta va fi memorată într-un pointer de tip nod
*. Să-l numim p: nod * p = new nod;
 Nodurile sunt variabile de tip structură, cu câmpurile info și urm. Accesul la câmpuri se
va face prin intermediul pointerilor, cu ajutorul operatorului ->, astfel: p->info și p-
>urm. Accesul la câmpuri se poate face și după dereferențierea pointer-ului: (*
p).info și (* p).urm.
 Nodul nou creat va fi inclus într-o listă. p->urm va memora adresa următorului element,
sau NULL dacă nu există următorul element!
 Rezumat:
o p este pointer la nod; este de tip nod *;
o *p este variabila de tip nod – este nod din listă
o p->info este informația utilă din nodul listei, de tip int
o p->urm este pointer. Memorează adresa elementului următor!

Secvența C++:

nod * p = new nod;


p->info = ..... ; // cin >> p->info;
p->urm = NULL;

Ne imaginăm lista în felul următor; săgețile simbolizează legăturile dintre nodurile listei. Vârful
săgeții reprezintă elementul următor. Ultimul element nu are săgeată. Valoarea
corespunzătoare din câmpul urm este NULL.

În exemplul de mai sus au loc următoarele relații:


 valoarea pointerului prim este adresa elementului cu valoarea 12;
 prim->info==12
 prim->urm->info==46
 prim->urm este adresa elemenului cu valoarea 46
 prim->urm->urm==p
 p->info==59
 p->urm->urm==t
 t->info==17
 t->urm->info==25
 t->urm->urm->info==18
 t->urm->urm->urm==NULL
 t->urm->urm->urm->info nu există. Rezultatul acestei expresii este impredictibil!

Adăugarea unui element la finalul listei


Un antet posibil pentru funcția care adaugă un element la finalul liste ar putea fi:

void AdaugaFinal(nod * & prim , int val);

Parametru prim este transmis prin referință pentru a trata corespunzător situația când lista
este vidă. În acesta caz, valoare de intrare a lui prim este NULL, iar valoarea de ieșire este
adresa primului element al listei – element nou creat.
Practic, vom trata două situații:
 dacă prim este NULL, creăm un nod nou, care va fi primul și totodată ultimul element al
listei, memorăm în el valoarea dorită și prim devine adresa acestui nod;
 în caz contrar, identificăm ultimul nod al listei și nodul nou creat devine succesor al
ultimului element și totodată ultimul element al listei.
void AdaugaFinal(nod * & prim , int x)
{
// creăm nod nou
nod * q = new nod;
q -> info = x;
q -> urm = NULL;
// adăugă noul nod la listă
if(prim == NULL)
{ // lista este vidă
prim = q;
}
else
{ // lista nu este vidă
nod * t = prim;
while(t -> urm != NULL)
t = t -> urm;
t -> urm = q;
}
}
Adăugarea unui element la începutul listei
Un antet posibil pentru funcția care adaugă element la începutul liste ar putea fi:

void AdaugaInceput(nod * & prim , int val);

Parametru prim este transmis prin referință deoarece la fiecare apel al funcției primul element
se modifică; se creează un element nou care devine prim element al listei. Astfel, adresa
primului element se modifică.
Procedăm astfel:
 prim memorează adresa primului element
 creăm un element nou: nod * t = new nod;
 memorăm în el informația utilă: t->info = ....
 îl plasăm în listă înaintea primului element: t->urm = prim;
 elementul nou creat este reținut ca prim element al listei: prim = t;
Obs: Nu este necesară tratarea diferențiată a situațiilor când lista este vidă ( prim==NULL),
respectiv când lista conține elemente (prim memorează adresa primului element). În ambele
situații atribuirea prim = t; are efectul dorit!
void AdaugaFinal(nod * & prim , int x)
{
// creăm nod nou
nod * t = new nod;
t -> info = x;

// legam nodul de lista


t -> urm = prim;

// valoarea lui prim se modifică, pentru a ieși din funcție cu valoarea corectă
prim = t;
}

Parcurgerea listei
Parcurgerea listei reprezintă vizitarea succesivă a elementelor pentru a realiza diverse operații
cu valorile lor. Un antet posibil pentru o funcție care parcurge lista poate fi:
void Parcurgere(nod * prim);

Parcurgerea se realizează secvențial, element cu element:


 folosim un pointer nod * p în care vom memora, pe rând, adresele elementelor din
listă;
 începem de la primul element al listei: p = prim;
 cât timp nu am trecut de ultimul element:
o prelucrăm elementul curent (p->info)
o trecem la următorul element: p = p->urm;

void Parcurgere(nod * prim)


{
nod * p = prim;
while(p != NULL)
{
//prelucrăm nodul curent

// trecem la următorul nod


p = p->urm;
}
}

Ștergerea unui element


Ștergerea unui element al listei constă în două etape: ștergerea propriu-zisă a variabilei
dinamice în care este stoca nodul de șters și refacerea legăturilor, astfel încât lista să fie
consistentă. Tehnic, modul de ștergere diferă după cum nodul de șters este primul din listă
sau nu.
Dacă ștergem primul element al listei vom proceda astfel:
 memorăm adresa primului nod într-un pointer auxiliar: nod * t = prim;
 nodul de după prim devine primul nod al listei: prim = prim->urm;
 ștergem variabila adresată de t: delete t;
Dac ștergem un element oarecare al listei, trebuie să cunoaștem într-un pointer oarecare, să
spunem p, adresa elementului din fața nodului de șters. Acest lucru este necesar pentru
refacerea corectă a legăturilor dintre elementele listei:
 vom șterge elementul situat în listă după cel cu adresa memorată în p, adică vom
șterge p->urm;
 memorăm adresa nodului de șters înt-un pointer auxiliar: nod * t = p->urm;
 corectăm adresa elementului de după p: p->urm = t->urm;
 ștergem variabila adresată de t: delete t;

Inserarea unui nou element


Și inserarea se face diferit, în funcție de poziția noului nod în listă; inserarea unui nod nou
înaintea primului nod al listei (adresa sa este memorată în pointer-ul prim) se face astfel:
 creăm un nod nou: nod * t = new nod;
 memorăm valoarea dorită în acest nod: t->info = ...;
 îl legăm de primul nod al listei: t->urm = prim;
 nodul nou creat devine primul din listă: prim = t;
Dacă nodul nou creat nu va fi primul din listă, îl vom insera după un nod cu adresa cunoscută,
memorată în pointer-ul p:
 creăm un nod nou: nod * t = new nod;
 memorăm valoarea dorită în acest nod: t->info = ...;
 în inserăm în listă:
o nodul nou creat va fi înaintea nodului de după p: t->urm = p->urm;
o nodul nou creat va fi plasat după nodul p: p-urm = t;
4. GRAFURI
Grafurile au numeroase aplicații în diverse domenii: proiectarea circuitelor electrice,
determinarea celui mai scurt drum dintre două localități, rețelele sociale (ex. Facebook), etc.
Primele rezultate legate de teoria grafurilor au fost obținute de matematicianul Leonard
Euler. A demonstrat că problema nu are soluție, iar în onoarea lui o categorie specială de
grafuri au fost numite grafuri euleriene.
Terminologie
1. Grafuri neorientate
1.1 Definiție
Un graf neorientat este o pereche ordonată de multimi(X,U),unde:
 X este o mulțime finită și nevidă de elemente numite noduri sau vârfuri
 U este o mulțime de perechi neordonate din X,numite muchii
1.2 Structură
Un graf are următoarele elemente:
 Mulțimea nodurilor X - Mulțimea tuturor nodurilor grafului
 Multimea muchiilor U - Mulțimea tuturor muchiilor grafului
 Gradul nodului - numărul de muchii formate cu ajutorul nodului respectiv
 Nod izolat - Un nod ce nu formează nici-o muchie
 Noduri terminale - Un nod ce formează o singură muchie
 Noduri adiacente - Noduri intre care există o muchie
 Nod si muchie incidente - Nodul face parte dintr-o muchie
Ce relaţie există între suma gradelor tuturor vârfurilor dintr-un graf neorientat şi numărul de muchii?
Într-un graf complet, oricare două noduri sunt adiacente. Câte muchii are un astfel de graf?

2.3 Reprezentare
 Matricea de adiacență - este o matrice a cu n linii și n coloane,în care elementele a[i,j] se
definesc astfel:
 a[i,j] = 1 ,dacă ∃ muchia [i,j] cu i≠j
 a[i,j] = 0 ,în caz contrar
 Lista de adiacență - este un tablou de liste egal cu numarul de varfuri;dacă există o muchie intre
nodul curent si un alt nod,atunci acesta se trece în listă
 Vectorul de muchii - mulțime ce conține toate muchiile grafului
Având lista de adiacentă:

 A:B→C→D
 B:A→D→E
 C:A→D
 D:A→B→C→D→E
 E:B→D

Un graf parțial este un graf obținut din graful inițial prin eliminarea uneia sau mai multor muchii
Un subgraf este un graf obținut din graful inițial prin eliminarea unui număr de noduri impreună cu
muchiile formate cu acele noduri
Se numește lanț într-un graf,o succesiune de vârfuri L={v1,v2,…,vk},cu proprietatea că oricare
două vârfuri consecutive sunt adiacente,adică există o muchie între acestea.
Se numeşte lanţ elementar un lanţ în care nu se repetă vârfuri.
Se numeşte lanţ simplu un lanţ în care nu se repetă muchii.
Se numeşte ciclu un lanţ simplu pentru care primul şi ultimul vârf coincid.
Se numeşte ciclu elementar un ciclu în care nu se repetă vârfuri(excepţie primul şi ultimul).

2. Grafuri orientate
2.1 Definiție

Un graf orientat este o pereche ordonată de mulțimi G={X,U},unde:

 X este o mulțime finită și nevidă numită mulțimea nodurilor(vârfurilor)


 U este o mulțime formată din perechi ordonate de elemente ale lui X,numită mulțimea arcelor
2.2 Structură

Într-un graf orientat, distingem:

 gradul interior/intern al unui nod: numărul de arce care intră în nod


 gradul exterior/extern al unui nod: numărul de arce care ies din nod
Ce relaţie există între suma gradelor exterioare, suma gradelor interioare şi numărul de arce?

2.3 Reprezentare

Matricea de adiacență - este o matrice a cu n linii și n coloane,în care elementele a[i,j] se definesc
astfel:

 a[i,j] = 1 ,dacă ∃ arcul (i,j) în mulțimea U


 a[i,j] = 0 ,în caz contrar
Matricea vârfuri-arce este o matrice B cu n = |X| linii și m = |U| coloane,în care fiecare
element b[i,j] este:

 1 ,dacă nodul i este o extremitate inițială a arcului


 -1 ,dacă nodul i este o extremitate finală a arcului
 0 ,dacă nodul i nu este o extremitate a arcului

3. Parcurgerea grafurilor
3.1 Parcurgerea în lățime
Parcurgerea în lățime (Breadth-First-Search -BFS) este o metodă ce presupune vizitarea nodurilor în
următoarea ordine:
 nodul sursă (considerat a fi pe nivelul 0)
 vecinii nodului sursă (reprezentând nivelul 1)
 vecinii încă nevizitați ai nodurilor de pe nivelul 1 (reprezentând nivelul 2)
 ș.a.m.d
3.1.1 Implementare
Pe masură ce algoritmul avansează,se colorează nodurile în felul următor:
 alb - nodul este nedescoperit încă
 gri - nodul a fost descoperit și este în curs de procesare
 negru - procesarea nodului s-a încheiat
Se păstrează informațiile despre distanța până la nodul sursă. Se obține arborele BFS
Pentru implementarea BFS se utilizează o coadă (Q) în care inițial se află doar nodul sursă.Se
vizitează pe rând vecinii acestui nod și se pun și ei în coadă.În momentul în care nu mai există vecini
nevizitați,nodul sursă este scos din coadă.
Pentru fiecare nod u din graf

culoare[u]=alb

d[u] = infinit //in d se retine distanta pana la nodul sursa

p[u] = null //
}

culoare[sursă]=gri

d[sursă]=0

enqueue(Q,sursă) //punem nodul sursă în coada Q

Cât timp coada Q nu este vidă

v=dequeue(Q) //extragem nodul v din coadă

pentru fiecare u dintre vecinii lui v

dacă culoare[u] == alb

culoare[u] = gri

p[u] = v

d[u] = d[v] + 1

enqueue(Q,u) //adăugăm nodul u în coadă

culoare[v] = negru //am terminat explorarea vecinilor lui v

}
Exemplu

3.2 Parcurgerea în adâncime

Parcurgea în adâncime (Depth-First-Search -DFS) presupune explorarea nodurilor în următoarea


ordine:

 Nodul sursă
 Primul vecin nevizitat al nodului sursă (îl numim V1)
 Primul vecin nevizitat al lui V1 (îl numim V2)
 ș.a.m.d
 În momentul în care am epuizat vecinii unui nod Vn, continuăm cu următorul vecin nevizitat al
nodului anterior,Vn-1
Această metoda de parcurgere pune prioritate pe explorarea în adâncime (pe distanțe tot mai mari față
de nodul sursă).

3.2.1 Implementare
Spre deosebire de BFS, DFS utilizează o stivă în loc de o coadă. Putem defini o stivă sau ne putem
folosi de stiva compilatorului, prin apeluri recursive.
funcţie pasDFS(curent)
{
pentru fiecare u dintre vecinii nodului curent
dacă culoare[u] == alb
{
culoare[u] = gri
p[u] = curent
d[u] = d[curent] + 1
pasDFS(u); //adăugăm nodul u în "stivă" şi începem explorarea
}
culoare[curent] = negru //am terminat explorarea vecinilor nodului curent
//ieşirea din funcţie este echivalentă cu eliminarea unui element din stivă
}
Pentru fiecare nod u din graf
{
culoare[u]=alb
d[u] = infinit //in d se retine distanta pana la nodul sursa
p[u] = null
}
culoare[sursă] = gri;
d[sursă] = 0;

//se apelează iniţial pasDFS(sursă)

Definiții
Definiție. Se numeşte graf orientat sau digraf o pereche ordonată de mulțimi notată G=(V,
U), unde:

 V este o mulțime finită şi nevidă ale cărei elemente se numesc noduri sau vârfuri;
 U este o mulțime de perechi ordonate de elemente distincte din V ale cărei elemente se
numesc arce.

Exemplu:
V={1,2,3,4,5,6}
U={(1,6),(2,1),(2,4),(3,2),(4,2),(5,4),(6,1),(6,2),(6,4)}

Observăm că arcele (1,6) și (6,1) sunt distincte.

Noțiuni
 extremități ale unui arc: pentru arcul u=(x,y), se
numesc extremități ale sale nodurile x şi y;
o x se numeşte extremitate inițială;
o y se numeşte extremitate finală;
o y se numește succesor al lui x;
o x se numește predecesor al lui y.
 vârfuri adiacente: dacă într-un graf există arcul u=(x,y) (sau u=(y,x), sau
amândouă), se spune despre nodurile x şi y că sunt adiacente;
 incidență:
o dacă u1 şi u2 sunt două arce ale aceluiaşi graf, se numesc incidente dacă au o
extremitate comună. Exemplu: u1=(x,y) şi u2=(y,z) sunt incidente;
o dacă u1=(x,y) este un arc într-un graf, se spune despre el şi nodul x, sau
nodul y, că sunt incidente.
Definiții alternative
Definiție. Se numeşte graf orientat o pereche ordonată de mulțimi notată G=(V, U), unde:
 V este o mulțime, finită şi nevidă, ale cărei elemente se numesc noduri sau vârfuri;
 U este o mulțime, de perechi ordonate de elemente din V, ale cărei elemente se
numesc arce.
Această definiție diferă de prima definiție prin faptul ca acum nu se mai spune despre
extremitățile unui arc
ca trebuie să fie distincte. În baza acestei definiții, sunt permise şi arce de
genul: u=(x,x) unde x∈V; aceste arce se numesc bucle.
Exemplu:

Definiție. Se numeşte graf orientat o pereche ordonată de mulțimi notată G=(V, U), unde:

 V este o mulțime, finită şi nevidă, ale cărei elemente se numesc noduri sau vârfuri;
 U este o familie de perechi ordonate de elemente din V, numită familia de arce.
Această definiție diferă de cea anterioară prin faptul ca acum nu numai că se admit bucle, dar
se admit şi mai multe arce identice.

Exemplu:

Observăm că există trei arce (6,2).

Observație. Dacă într-un graf orientat numărul arcelor identice nu depăşeşte numărul p,
atunci se numeşte p-graf. Graful de mai sus este un 3-graf.

Grade
Definiție. Fie G=(V, U) un graf orientat și x un nod al său.

 Se numeşte grad exterior al nodului x, numărul arcelor de forma (x,y) (adică numărul
arcelor care ies din x), notat d+(x).
 Se numeşte grad interior al nodului x, numărul arcelor de forma (y,x) (adică numărul
arcelor care intră în x), notat d-(x).

Exemplu:

Pentru graful alăturat:

 d+(2)=2
 d-(2)=3

Teoremă: Într-un graf orientat, suma gradelor exterioare a


tuturor nodurilor este egală cu suma gradelor interioare a
tuturor nodurilor și cu numărul de arce.

Un nod x se numește izolat dacă d+(x)=d-(x)=0 (are gradul


interior și gradul exterior egal cu 0).

Reprezentarea grafurilor orientate


Matricea de adiacență
Fie G=(V,U) un graf orientat cu n noduri, în care nu există mai multe arce de la un nod la altul.
Matricea de adiacență a grafului este o matrice cu n linii și n coloane și elemente 0 sau 1,
astfel:

 Ai,j=1 dacă există arcul (i,j)


 Ai,j=0 dacă există nu arcul (i,j)
Pentru graful alăturat, matricea de adiacență este:

0 0 0 0 0 1
1 0 0 1 0 0
0 1 0 0 0 0
0 1 0 0 0 0
0 0 0 1 0 0
1 1 0 1 0 0

Observăm că matricea de adiacență:

 are zerouri pe diagonală (dacă în graf nu avem bucle)


 nu trebuie să fie simetrică față de diagonala principală

Pentru reprezentarea în memorie vom folosi un tablou bidimensional ale cărui dimensiuni sunt
în concordanță cu numărul de noduri din graf.

Considerăm un graf cu maxim 50 de noduri. În C/C++ vom avea declarația:

int A[51][51];

Lista de arce
Lista de arce a unui graf orientat reprezintă o mulțime (familie, dacă arcele se pot repeta)
ce conține toate arcele din graf.
Pentru graful alăturat, lista de arce este:

U={(1,6),(2,1),(2,4),(3,2),(4,2),(5,4),(6,1),(6,4)}

Pentru reprezentarea în memorie putem folosi:

 un tablou unidimensional cu elemente de tip struct


{int I,J;}
 două tablouri unidimensionale cu elemente de tip int
 o listă alocată dinamic
 etc.
Listele de adiacență (succesori)
Pentru un graf orientat cu G=(V,U) se va memora numărul de noduri n și apoi, pentru fiecare
nod x, lista succesorilor lui x, adică nodurilor y cu proprietatea că există arcul (x,y).

Pentru graful alăturat, acestea sunt:

1: 6
2: 1 4
3: 2
4: 2
5: 4
6: 1 2 4

La reprezentarea în memorie trebui avut în vedere că dimensiunile listelor de succesori sunt


variabile. De aceea, este neeficientă utilizarea unor tablouri alocate static. Astfel, putem folosi:

 un șir de n tablouri unidimensionale alocate dinamic;


 un șir de n vectori din STL;
 un șir de n liste simplu (dublu) înlănțuite alocate dinamic.

Graf parțial, subgraf


Definiție. Fie G=(V, U) un graf orientat. Se numeşte graf parțial al grafului G, graful
orientat G1=(V, U1), unde U1 ⊆ U.

Din definiție rezultă:

 Un graf parțial al unui graf orientat G=(V,U), are aceeaşi mulțime de vârfuri ca şi G, iar
mulțimea arcelor este o submulțime a lui U sau chiar U.
 Fie G=(V, U) un graf orientat. Un graf parțial al grafului G, se obține păstrând vârfurile şi
eliminând eventual nişte arce (se pot elimina şi toate arcele sau chiar nici unul).

Definiție. Fie G=(V, U) un graf orientat. Se numeşte subgraf al grafului G graful


orientat G1=(V1,U1) unde V1 ⊆ V iar U1 conține toate arcele din U care au extremitățile în V1.

Din definiție rezultă:


 Fie G=(V,U) un graf orientat. Un subgraf al grafului G,
se obține ştergând eventual anumite
vârfuri şi odată cu acestea şi arcele care le admit ca
extremitate (nu se pot şterge toate vârfurile deoarece
s-ar obține un graf cu mulțimea vârfurilor vidă).

Exemplu:

Graful inițial Graf parțial Subgraf

S-au eliminat S-a eliminat nodul 6 și toate


arcele (1,6), (3,2), (6,4)
arcele incidente cu el.

Graf complet. Graf turneu.


Definiție. Fie G=(V, U) un graf orientat. Graful G se numește graf complet dacă oricare
două vârfuri
distincte ale sale sunt adiacente.
Două vârfuri x și y sunt adiacente dacă:
 între ele există arcul (x,y), sau
 între ele există arcul (y,x), sau
 între ele există arcele (x,y) şi (y,x).
Exemplu:
Teoremă: Numărul de grafuri orientate complete cu n noduri este 3n*(n-1)/2.

Definiție: Un graf orientat este turneu, dacă oricare ar fi două vârfuri i şi j, i≠j, între ele
există un singur arc: arcul (i,j) sau arcul (j,i).

Exemplu:

Proprietăți:

1. Orice graf turneu este graf complet.


2. Avem 2n*(n-1)/2 grafuri turneu cu n noduri.
3. În orice graf turneu există un drum elementar care trece prin toate vârfurile grafului.

Conexitate
Lanț. Drum
Definiție: Fie G=(V, U) un graf orientat. Se numește lanț, în graful G, o succesiune de
arce, notată
L = (u1 , u2 ,..., uk) cu proprietatea ca oricare două arce consecutive au o extremitate comună (nu
are importanță orientarea arcelor).
sau
Definiție: Fie G=(V, U) un graf orientat. Se numește lanț, în graful G, o succesiune de
noduri, notată
L = (x1 , x2 ,..., xp) cu proprietatea ca oricare două noduri consecutive sunt adiacente.
Lungimea unui lanț este egală cu numărul de arce din care este alcătuit.
Primul nod și ultimul nod dintr-un lanț formează extremitățile lanțului.
Definiție. Fie G=(V, U) un graf orientat. Se numește drum în graful G o succesiune de
noduri, notată
D = (x1 , x2 ,..., xk), cu proprietatea că pentru orice 1≤i<k, (xi,xi+1) este arc în G.
Lungimea unui drum este egală cu numărul de arce din care este alcătuit.
Pentru un drum D = (x1 , x2 ,..., xk), nodurile x1 și xk reprezintă extremitățile – inițială, respectiv
finală.
Un lanț (drum) se numește elementar dacă în el nu se repetă noduri. Un lanț (drum) se
numește simplu dacă în el nu se repetă arce.
Exemple În graful alăturat:
L=(5,4,2,6,1) este un lanț elementar, dar nu este drum.
D=(3,2,1,6,4) este drum elementar.
D=(3,2,1,6,2,4) este drum neelementar, dar simplu.

Circuit
Definiție: Se numește circuit un drum simplu în care extremitatea inițială și finală sunt
egale. Se numește circuit elementar un circuit în care, cu excepția extremităților, nu se
repetă noduri.
Lungimea unui circuit este reprezentată de numărul de arce din care acesta este
alcătuit.

Exemple În graful alăturat:


(1,6,2,1) și (1,6,4,2,1) sunt circuite elementare.

Conexitate. Tare conexitate


Definiții: Fie G=(V,U) un graf orientat.
Graful se numește tare conex dacă între oricare două noduri distincte există cel puțin
un drum.
Se numește componentă tare conexă un subgraf tare conex și maximal cu această
calitate – dacă am mai adauga un nod, n-ar mai fi tare conex.
Exemplu:

Graful de mai sus nu este tare conex. El conține trei componente tare conexe:
 134
 2
 5678
Observație: Un nod al grafului face parte dintr-o singură componentă tare conexă. Dacă ar
face parte din două compoennte tare conexe, ele s-ar “reuni” prin intermediul acelui nod.
Acest articol conține mai multe detalii despre tare-conexitate (algoritmi de verificare a tare
conexității, de determinare a componentelor tare-conexe, etc.).

Graf hamiltonian. Graf eulerian


Definiții: Fie un graf orientat G=(V,U).
Un drum elementar care conține toate nodurile grafului se numește drum hamiltonian.
Un circuit elementar care conține toate nodurile grafului se numește circuit hamiltonian.
Un graf care conține un circuit hamiltonian se numește graf hamiltonian.

Exemplu: Graful orientat desenat mai jos este hamiltonian, deoarece con ține circuitul
hamiltonian (2, 1, 5 , 6, 4, 3, 2).

Definiții: Fie un graf orientat G=(V,U).


Un drum care conține toate arcele grafului se numește drum
eulerian.
Un circuit care conține toate arcele grafului se numește circuit
eulerian.

Un graf care conține un circuit eulerian se numește graf eulerian.


Teoremă: Un graf fără noduri izolate este eulerian dacă și numai dacă este conex și
pentru fiecare nod, gradul interior este egal cu cel exterior.
Exemplu: Graful orientat de mai jos este eulerian.

S-ar putea să vă placă și