Documente Academic
Documente Profesional
Documente Cultură
DISCIPLINA: INFORMATICA
CONȚINUTURI
1. Metoda de programare „Divide et Impera”
2. Metoda de programare “Backtracking”
3. Liste
4. Grafuri
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;
}
2 1 1 – greșit
3 1 2 1 greșit
3 1 2 2 greșit
3 1 2 3 soluție finală 1
3 1 3 1 greșit
3 1 3 2 soluție finală 2
3 1 3 3 greșit
3 2 1 1 greșit
3 2 1 2 greșit
3 2 1 3 soluție finală 3
2 2 2 – greșit
3 2 3 1 soluție finală 4
3 2 3 2 greșit
3 2 3 3 greșit
3 3 1 1 greșit
3 3 1 2 soluție finală 5
3 3 1 3 greșit
3 3 2 1 soluție finală 6
3 3 2 2 greșit
3 3 2 3 greșit
2 3 3 – greșit
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)
└■
#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;
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:
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;
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:
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;
vf = 0;
vf == 0 // stivă vidă
vf > 0 // stivă nevidă
S[vf++] = _VALOARE ;
vf --;
S[vf-1]
#include <stack>
stack<int> S;
S.push( _VALOARE );
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;
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;
};
Secvența C++:
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.
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:
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;
// 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);
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
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:
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
p[u] = null //
}
culoare[sursă]=gri
d[sursă]=0
culoare[u] = gri
p[u] = v
d[u] = d[v] + 1
}
Exemplu
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;
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)}
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:
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:
d+(2)=2
d-(2)=3
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
Pentru reprezentarea în memorie vom folosi un tablou bidimensional ale cărui dimensiuni sunt
în concordanță cu numărul de noduri din graf.
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)}
1: 6
2: 1 4
3: 2
4: 2
5: 4
6: 1 2 4
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).
Exemplu:
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:
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.
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.).
Exemplu: Graful orientat desenat mai jos este hamiltonian, deoarece con ține circuitul
hamiltonian (2, 1, 5 , 6, 4, 3, 2).