www.agora.ro

Noduri, muchii, arce ... (3)

Introducere în teoria GRAFURILOR

Cristian Cadar

Arborii parțiali de cost minim reprezintă un domeniu de maximă importanță a teoriei grafurilor, cu numeroase aplicații practice în toate domeniile tehnice. De aceea, în acest articol vă prezentăm doi dintre cei mai cunoscuți algoritmi de aflare a unui arbore parțial de cost minim: algoritmul lui Kruskal și algoritmul lui Prim.

Înainte de abordarea acestui subiect, de mare importanță pentru numeroase domenii de activitate, vom familiariza cititorul cu câteva noțiuni legate de arbori, necesare înțelegerii acestui articol.

Definiție

Un graf neorientat, conex și aciclic se numește arbore.

Definiție

Fie G un graf. Numim arbore parțial al grafului G un graf parțial al lui G care este arbore.

Principalele proprietăți ale arborilor pot fi sintetizate prin următoarele afirmații echivalente:

  • - G este un arbore cu n noduri;
  • - G este un graf conex cu n-1 muchii;
  • - G este un graf aciclic cu n-1 muchii;
  • - G este un graf conex minimal (prin suprimarea unei muchii, graful obținut devine neconex);
  • - G este un graf aciclic maximal (prin adăugarea unei muchii, graful obținut va conține cicluri);
  • - oricare două vârfuri din G sunt unite printr-un lanț unic.

Demonstrarea acestor proprietăți este foarte simplă și o lăsăm pe seama cititorului.

În majoritatea problemelor, arborii sunt folosiți pentru ierarhizarea informațiilor ce corespund vârfurilor arborelui. De aceea au fost introduși arborii cu rădăcină.

Definiție

Un arbore cu rădăcină este o mulțime finită A de noduri astfel încât:

  1. - există un nod numit rădăcină;
  2. - toate celelalte noduri sunt repartizate în m mulțimi disjuncte A1, A2,.... Am, fiecare mulțime fiind la rândul său arbore. Între rădăcina arborelui și rădăcinile arborilor A1, A2,.... Am există câte o muchie.

Observăm că un arbore oarecare poate fi transformat într-un arbore cu rădăcină prin alegerea arbitrară a unui nod din arbore drept rădăcină.

În ceea ce privește arborii, majoritatea termenilor folosiți aici derivă din vocabularul arborelui genealogic. Astfel afirmăm că rădăcina este tatăl rădăcinilor arborilor A1, A2, ..., Am, iar acestea din urmă sunt fii ai rădăcinii arborelui. Nodurile cu același tată (părinte) sunt numite frați. În mod similar sunt folosiți termenii de bunic, nepot, descendent, ascendent etc.

Un arbore cu rădăcină poate fi etajat (ierarhizat). Considerăm rădăcina ca aflându-se pe nivelul 0, descendenții direcți ai acesteia (fiii) pe nivelul 1, descendenții direcți ai acestora din urmă pe nivelul 2 etc. De exemplu, pentru arborele din figura 2, nodul 5 se află pe nivelul 0, nodurile 1 și 7 pe nivelul 1, nodurile 3, 8, 4, 2 și 9 pe nivelul 2, iar nodul 6 pe nivelul 3.

Cu aceste notații, putem aborda subiectul acestui articol și anume generarea arborilor parțiali de cost minim. Arborii parțiali de cost minim (pe scurt A.P.M.) au numeroase aplicații în diverse domenii. Exemplul cel mai des folosit este unul din domeniul transporturilor: dându-se n orașe ale unei țări, se cere interconectarea lor, astfel încât lungimea totală a șoselelor ce trebuie construite să fie minimă. Interconectarea orașelor presupune ca între oricare două orașe să existe cel puțin un drum (nu neapărat direct).

Această problemă, ca de altfel multe altele de acest tip, poate fi modelată folosind teoria grafurilor. Structura de orașe poate fi codificată printr-un graf neorientat, ale cărui noduri reprezintă orașele, iar muchiile reprezintă șoselele care ar putea fi construite. Lungimile muchiilor sunt date bineînțeles de lungimile șoselelor corespunzătoare. Problema se reduce astfel la găsirea unui graf parțial conex, pentru care suma lungimilor muchiilor să fie minimă. Se observă cu ușurință faptul că pentru a fi conex, graful parțial nu trebuie să conțină decât n-1 muchii, deci avem de a face cu un arbore.

Există doi algoritmi foarte cunoscuți de determinare a unui A.P.M. al unui graf: algoritmul lui Kruskal, de complexitate O(mŚlog n) și algoritmul lui Prim, de complexitate O(n2) (pentru acest algoritm se pot obține și complexități mai bune, așa cum va fi explicat ulterior). Ambii algoritmi folosesc tehnica greedy. Mai precis se pornește cu o mulțime de arbori A (inițial A conține doar arbori cu un singur nod), care reprezintă subarbori ai unui A.P.M. La fiecare pas al algoritmului, se adaugă o muchie w în A, astfel încât AÈ{w} să constituie de asemenea un set de subarbori ai unui A.P.M. Astfel, după introducerea a n-1 muchii, mulțimea A va reține chiar un A.P.M. al grafului dat.

Deși tehnica de abordare este aceeași pentru ambii algoritmi, modul în care se realizează alegerea muchiilor diferă. Vom începe prin prezentarea algoritmului Kruskal, algoritm ce are o complexitate de O(mŚlog n). Mulțimea A de mai sus este inițializată cu n arbori A1, A2,.... An, fiecare arbore Ai (i=1..n) conținând doar nodul i. La fiecare din cei n-1 pași, algoritmul unește doi subarbori din mulțimea A, rezultând în final un A.P.M. al grafului dat. Folosind o tehnică greedy, algoritmul Kruskal alege din toate muchiile ce unesc doi arbori din A, muchia de lungime minimă. Muchiile selectate nu trebuie memorate în cadrul mulțimii A, ele putând fi afișate la fiecare din cei n-1 pași ai algoritmului.

Un prim șablon al algoritmului Kruskal este următorul:

Kruskal (graf G)

APM={}

A1={1}, A2={2}, ..., An={n}

pentru pas = 1,n-1

Alege muchia minimă (i,j)

care unește doi

arbori Ap și Aq

APM=APM+{(i,j)}

Unește arborii Ap și A

q

 

O primă optimizare a algoritmului de mai sus constă în a sorta inițial muchiile grafului, descrescător în funcție de lungimea acestora. Algoritmul devine:

Kruskal(graf G)

APM={}

A1={1}, A2={2}, ..., An={n}

Sortează descrescător muchiile grafului în funcție de cost

pentru k = 1,m

dacă muchia k unește doi arbori Ap și A

q

atunci APM=APM+{k}

Unește arborii Ap și A

q

Observăm că mulțimile Ai sunt mulțimi de noduri. Condiția ca o muchie k să fie o muchie care să unească doi arbori Ap și Aq, este echivalentă cu condiția ca cele două extremități ale muchiei să nu se găsească într-un același arbore Ai. O primă variantă de memorare a arborilor Ai este aceea de a reține pentru fiecare nod i din graf, arborele căruia îi aparține. De exemplu, dacă pentru un graf cu 11 vârfuri avem arborii A1={2, 5, 8}, A2={1}, A3={3, 9, 10}, A4={4, 6, 7, 11}, atunci structura va fi memorată prin vectorul A=(2, 1, 3, 4, 1, 4, 4, 1, 3, 3, 4). Folosind această reprezentare, complexitatea algoritmului va fi O(mŚn), deoarece determinarea submulțimii din care face parte un nod se face în O(1), iar unirea a doi arbori în O(n). Pentru a obține complexitatea dorită O(mŚlog n), vom folosi o modalitate eficientă de memorare a acestor mulțimi disjuncte. Pentru aceasta, fiecare mulțime va fi structurată sub forma unui arbore cu rădăcină. Astfel, mulțimile din exemplul anterior ar putea fi memorate astfel:

Pentru fiecare nod al grafului, reținem tatăl său. Prin definiție, un nod fără tată, are tatăl 0. Deci, în cazul de mai sus vectorul t al taților va fi: (0, 0, 0, 0, 2, 4, 4, 2, 3, 3, 7). Având această structură, unirea a doi arbori se face foarte ușor: tatăl rădăcinii unuia dintre arbori devine rădăcina celuilalt arbore:

union (nod a,b) // unește subarborii de

// rădăcini a și b

t(a)=b

Presupunând că arborii sunt echilibrați (cel mai mare nivel atins în arbore este élog2 nù), rezultă că determinarea rădăcinii arborelui din care face parte un anumit nod se face în complexitatea O(log n):

root (nod a)

dacă t(a)=0

atunci root=a

altfel root=root(t(a))

Problema care apare constă în faptul că în urma operației union, arborele nou format s-ar putea să nu mai fie echilibrat. Pentru a ne asigura că vom forma tot un arbore echilibrat, tatăl rădăcinii unui arbore va reține în loc de valoarea 0, numărul nodurilor arborelui respectiv. Pentru a putea diferenția în continuare rădăcina de celelalte noduri ale arborelui, acest număr va fi reținut cu semn negativ. În cazul arborilor din figura 1, vectorul t va fi (-3, -1, -3, -4, 2, 4, 4, 2, 3, 3, 7). Unirea a doi arbori va putea fi făcută astfel încât în orice moment arborii Ai să fie echilibrați:

union (nod a,b)

k=t(a)+t(b)

dacă t(a)>t(b)

atunci t(b)=a

t(a)=k

altfel t(a)=b

t(b)=k

Având implementate aceste proceduri, algoritmul Kruskal devine:

Kruskal (graf G)

APM={}

t(1)=-1, t(2)=-1, ..., t(n)=-1

Sortează descrescător muchiile grafului în funcție de cost

pentru k = 1,m

dacă root(i)čroot(j) // k=(i,j)

atunci APM=APM+{k}

union(root(i),root(j))

Se observă ușor că algoritmul are complexitatea dorită O(mŚlog n).

În cele ce urmează vom demonstra corectitudinea algoritmului Kruskal. În primul rând se observă ușor faptul că algoritmul generează un arbore parțial, deoarece el selectează n-1 muchii ale grafului care nu formează cicluri. Să arătăm că acest arbore parțial este și de cost minim. Fie (a1, a2, ..., an-1) arborele parțial generat de algoritmul Kruskal, unde a1 Ł a2 Ł ... Ł an-1 sunt muchiile arborelui. Fie B=(b1, b2, ..., bn-1) un A.P.M. al grafului respectiv, cu b1 Ł b2 Ł ... Ł bn-1. Fie k primul indice pentru care ak č bk. Bineînțeles ak < bk, altfel algoritmul Kruskal ar fi ales muchia bk, fiind de cost mai mic și neformând cicluri cu muchiile a1, a2, ..., ak-1. În continuare, adăugăm la A.P.M.-ul B muchia ak. Se va obține graful G'= (a1, a2, ..., ak-1, bk, ..., bn-1, ak), care va conține un ciclu generat de introducerea muchiei ak. Cum ak nu formează cicluri cu muchiile a1, a2, ..., ak-1, înseamnă că aceasta va forma cicluri cu muchiile bk, ..., bn-1. Dar ak<bk, ..., bn-1, deci eliminând din ciclul format o muchie diferită de ak, vom obține un arbore parțial al cărui cost este mai mic decât cel al arborelui B. Contradicție cu faptul că B este A.P.M., deci nu există un indice k pentru care ak č bk.

Această demonstrație este deosebit de importantă, ea conferindu-ne o informație foarte utilă despre A.P.M-urile unui graf. Ea arată că două A.P.M.-uri nu pot diferi prin lungimile muchiilor, ci doar prin muchiile în sine. O consecință imediată a acestui fapt este că pentru un graf în care oricare două muchii au lungimi diferite, există un A.P.M. unic. Aceste observații sunt foarte importante deoarece ne permit să aflăm cu ușurință toți A.P.M. ai unui graf.

În cele ce urmează, vom descrie algoritmul lui Prim. Vom insista mai puțin asupra acestui algoritm, deoarece el folosește ideea algoritmului Dijkstra, algoritm care a fost descris detaliat în cadrul episodului anterior. În acest caz mulțimea A este mulțimea S din cadrul algoritmului Dijkstra. La fiecare din cei n-1 pași, în S este introdus un nou nod, care se află la distanță minimă de nodurile deja introduse.

Prim(graf G, nod s)

Inițializare S, DIST // ca la Dijkstra

pentru pas=1,n-1

(1) selectează nodul vÏS pentru care

DIST(v)

este minim

(2) S=S+{v}

(3) pentru wÏS și există muchia (v,w)

dacă DIST(w) > cost(v,w)

(4) atunci DIST(w) = cost(v,w)

Analiza complexității acestui algoritm este identică cu cea făcută în cazul algoritmului Dijkstra. Implementând pe S sub formă de vector, vom avea complexitatea O(n2), implementându-l sub formă de heap, vom avea O(mŚlog n), iar implementându-l sub formă de heap Fibonacci, vom avea O(nŚlog n+m).

Cristian Cadar este student în anul II la Universitatea Politehnică București. Poate fi contactat prin e-mail la adresa ccadar@pcnet.pcnet.ro.

[cuprins]