algoritmica grafurilor

April 5, 2018 | Author: Ana Zavate | Category: N/A
Share Embed Donate


Short Description

algoritmica...

Description

Mihai TALMACIU

Algoritmica grafurilor

Editura ALMA MATER

Conf. univ. dr. MIHAI TALMACIU

Recenzori: Prof. univ. dr. habilitat Dumitru Todoroi Conf. univ. dr. Elena Nechita Prof. univ. dr. Victor Blănuţă

Descrierea CIP a Bibliotecii Naţionale a României Talmaciu, Mihai Algoritmica grafurilor, Mihai Talmaciu – Bacău ALMA MATER, 2008 Bibliogr. ISBN

978 – 973 – 1833 – 76 – 7

I, Talmaciu M. Tehnoredactare computerizată: prep. univ. IOANA ALEXANDRA PANDELE asist. univ. drd. ALINA-MIHAELA PATRICIU

ISBN

978 – 973 – 1833 – 76 – 7

CUPRINS

Capitolul 1 Introducere 1.1. Definiţia unui graf 1. 2. Grade 1.3. Subgrafuri 1.4. Operaţii cu grafuri 1.5. Clase de grafuri 1.6. Drumuri şi circuite 1.7. Mulţimi separatoare, transversale şi mulţimi omogene 1.8. Algoritmi şi complexitate de calcul

1 1 3 3 3 4 5 6 7

Capitolul 2 Metode de căutare şi programare 2.1. Căutarea în lăţime 2.1.1. Algoritmul 2.1.2. Implementarea C++ 2.1.3. Complexitate şi optimalitate 2.1.4 Aplicaţii ale BFS 2.2. Căutarea în adâncime 2.3. Metoda Greedy 2.4. Metoda backtracking 2.5. Metoda divide et impera 2.6. Metoda branch and bound

9 9 9 9 10 11 11 13 14 16 17

Capitolul 3 Structuri de date 3.1. Liste 3.1.1. Liste simplu înlănţuite 3.1.2. Parcurgerea unei liste simplu înlănţuite 3.1.3. Liste dublu înlănţuite 3.1.4. Parcurgerea unei liste dublu înlănţuite 3.2. Arbori 3.2.1. Arbori liberi 3.2.2. Arbori cu rădăcină 3.2.3. Arbori binari 3.2.4. Parcurgerea arborilor binari

21 21 21 22 22 23 24 24 24 25 25

Capitolul 4 Parcurgeri de grafuri 4.1. Parcurgerea BF a grafurilor 4.2. Parcurgerea DF a grafurilor 4.3. Aplicaţii 4.3.1. Sortarea topologică 4.3.2. Componentele conexe ale unui graf

29 29 35 38 38 39

Capitolul 5 Probleme de drum în (di)grafuri 5.1. Problema celui mai scurt drum 5.1.1. Arborele Steiner 5.1.2. Algoritmul lui Dijkstra 5.1.3. Probleme similare şi algoritmi 5.1.4. Probleme legate de drum 5.1.5. Algoritmul Bellman-Ford

43 43 45 46 49 50 50 54

5.1.6. 5.1.7. 5.1.8. 5.2. 5.3. 5.4. 5.5. 5.6. 5.7. 5.8.

Algoritmul de căutare A∗ Algoritmul Floyd-Warshall Algoritmul lui Johnson Probleme de conexiune. Teorema lui Menger şi aplicaţii Structura grafurilor p-conexe Problema drumului Hamiltonian Problema ciclului Hamiltonian Arborele parţial de cost minim Algoritmul lui Prim Algoritmul lui Kruskal

57 60 61 62 63 64 69 71 77

Capitolul 6 Probleme de fluxuri în reţele 6.1. Problema fluxului maxim 6.2. Fluxuri de cost minim 6.3. Algoritmul Ford-Fulkerson

83 83 89 92

Capitolul 7 Numărul de stabilitate şi densitatea unui graf 7.1. Mulţimi stabile şi clici 7.2. Problema mulţimii independente 7.3. Problema clicii 7.4. Determinarea mulţimilor stabile maximale

95 95 96 98 100

Capitolul 8 Probleme de descompuneri în grafuri 8.1. Tipuri de descompuneri în grafuri 8.1.1. Descompunerea de tip 2 8.1.2. Descompunerea de tip 3 8.1.3. Descompunerea în funcţie de compoziţie 8.1.4. G-descompunerea 8.1.5. Descompunerea substituţie şi partiţionarea vârfurilor 8.2. Descompunerea slabă a grafurilor 8.2.1. Introducere 8.2.2. Descompunerea slabă a unui graf 8.3. Teorema celor patru culori 8.3.1. Colorarea grafurilor 8.3.2. Colorarea vârfurilor 8.3.3. Numărul cromatic 8.3.4. Aspecte algoritmice 8.3.5. Algoritmul Welsh – Powell 8.3.6. Polinomul cromatic

103 103 103 104 105 106 107 111 111 111 115 117 117 118 118 119 119

Bibliografie

121

Capitolul 1 INTRODUCERE

Pentru noţiunile din acest paragraf am consultat Behzad, Chartrand, Foster, Croitoru, Olaru, Tomescu. Alte completări bibliografice sunt precizate în momentul utilizării.

1.1.

Definiţia unui graf

Un graf este o pereche G = (V,E), unde V este o mulţime finită nevidă, iar E este o mulţime de submulţimi cu două elemente distincte ale lui V. V se numeşte mulţimea vârfurilor şi numărul său de elemente, |V| este ordinul grafului G. E este mulţimea muchiilor grafului G şi |E| este dimensiunea grafului G. Când facem referire la mulţimea de vârfuri şi muchii ale grafului G folosim V(G) şi E(G), respectiv. Un digraf (graf orientat) este o pereche D = (V(D), A(D)) unde V(D) este o mulţime finită nevidă (mulţimea vârfurilor digrafului D), iar A(D) ⊆ V(D) × V(D) este mulţimea arcelor digrafului D. Dacă e = {x,y} este o muchie a grafului G, vom nota, pe scurt, {x,y} = xy (yx) şi vom spune că: muchia e este incidentă cu vârfuri1e x şi y; vârfurile x şi y sunt adiacente în G; vârfurile x şi y sunt vecine în G; vârfurile x şi y sunt extremităţile muchiei e. Dacă v ∈ V(G) , atunci mulţimea NG (v) = {w | w ∈ V(G) − {v}, vw ∈ E(G)} ,

se numeşte vecinătatea vârfului v în G. NG (v) = {w | w ∈ V(G) − {v}, vw ∉ E(G)} se numeşte mulţimea nevecinilor vârfului v în G. Dacă A, B ⊆ V(G), A ∩ B = 0/ atunci : ¾ A ~ B (A este total adiacent cu B) dacă şi numai dacă: ∀a ∈ A, ∀b ∈ B : ab ∈ E(G) ; ¾ A ~/ B (A este total neadiacent cu B) dacă şi numai dacă: ∀a ∈ A, ∀b ∈ B : ab ∉ E(G) ; ¾ A ~ p B (A este parţial adiacent cu B) dacă şi numai dacă: ∃a ∈ A, ∃b ∈ B : ab ∈ E(G) ; a ~ B (vârful a este B – universal, adică a vede toate vârfurile din B) dacă şi numai dacă {a} ~ B . a ~/ B (vârful a este B – nul) dacă şi numai dacă {a} ~/ B .

1

Dacă S ⊆ V(G) atunci: S este mulţime stabilă (sau independentă) a lui G dacă şi numai dacă ∀x, y ∈ V(G), x ≠ y : xy ∉ E(G) . O stabilă S este maximală dacă nici o stabilă a lui G nu conţine propriu S; S(G) = {S | S este stabilă maximală în G} ; α(G) = max{S | S ∈ S(G)} este numărul de stabilitate a lui G. Sα (G) = {S | S ∈ S(G),| S | = α(G)} ; S este o mulţime α - stabilă dacă şi numai dacă S ∈ Sα (G) . Complementarul, G , al unui graf G este graful a cărui mulţime de vârfuri este V(G), iar două vârfuri sunt adiacente în G dacă şi numai dacă ele nu sunt adiacente în G. Dacă Q ⊆ V(G) atunci: Q este clică (său mulţime completă) a lui G dacă şi numai dacă Q este mulţime stabilă în G ; o clică Q este maximală dacă nici o clică a lui G nu conţine propriu Q; C(G) = {Q | Q este clică maximală în G}; , ω (G) = max{|Q| | Q ∈ C(G) } este numărul clică a lui G; Cω (G) ={Q | Q ∈ C(G) şi |Q| = ω (G)}; Q este o ω - clică dacă şi

numai dacă Q ∈ Cω (G) . Dacă p ∈ N∗ , se numeşte p – colorare a vârfurilor lui G o aplicaţie c : V(G) → {1,..., p} cu proprietatea că c−1 (i) este o mulţime stabilă în G. χ(G) este numărul cromatic a lui G, adică cel mai mic număr de culori necesare pentru a colora vârfurile de felul că două vârfuri adiacente distincte nu pot fi de aceeaşi culoare. θ(G) = χ(G) este numărul de acoperire cu clici a lui G. Două grafuri, G = (V(G),E(G)) şi H = (V(H),E(H)) se numesc izomorfe, şi notăm aceasta prin G ≈ H (sau G ≅ H ) dacă există o bijecţie ϕ : V(G) → V(H) cu proprietatea că aplicaţia ψ : E(G) → E(H) , definită pentru orice uv ∈ E(G) prin ψ (uv) = ϕ(u)ϕ(v) este o bijecţie.

2

1. 2. Grade Gradul unui vârf v dintr-un graf G este numărul de muchii ale lui G incidente cu v, care se notează cu d G (v) . Un vârf de grad zero se numeşte izolat. Un vârf de grad unu se numeşte pendant .

1.3.

Subgrafuri

Fie G un graf. Un graf H este un subgraf al lui G dacă V(H) ⊆ V(G) şi E(H) ⊆ E(G). Dacă A este o submulţime nevidă a lui V(G) atunci subgraful lui G indus de A este graful având mulţimea vârfurilor A, iar mulţimea muchiilor constă din acele muchii ale lui G incidente cu două elemente din A. Dacă A ⊆ V(G), v ∈ V, U ⊆ E(G),e ∈ E(G) atunci notăm: G A sau G(A) sau [A]G sau [A] subgraful lui G indus de A; G – A = G(V(G) – A); G – v = G – {v} ; G – U = (V(G),E(G) – U); G – e = G – {e}.

1.4.

Operaţii cu grafuri

Pe parcursul acestei cărţi vom folosi unele operaţii cu grafuri, pe care le reamintim mai jos. 1) Dacă G1 şi G 2 sunt două grafuri cu V( G1 ) ∩ V( G 2 ) = 0/ atunci reuniunea disjunctă a grafurilor G1 şi G 2 înseamnă graful G = G1 ∪ G 2 cu V(G) = V( G1 ) ∪ V( G 2 ) şi E(G) = E( G1 ) ∪ E( G 2 ).

2) Se numeşte graf adiacenţă (X - adiacenţă) (Sabidussi, Olaru, Antohe) a unei familii de grafuri (G x ) x∈V(X) , indexată de mulţimea de vârfuri a grafului X, graful notat U X x∈X G x , unde:

( ) E (UX x∈X G x ) = U x∈V(X) E(G x ) ∪ V UX x∈X G x = U x∈X V(X) × {x};

∪{[(a, x), (b, x ')] | x ≠ x ',[x, x '] ∈ E(X), a ∈ V(G x ), b ∈ V(G x ' )}.

3)

Produsul lexicografic (compoziţia) (Harary) a două grafuri G1 şi G 2 este graful notat G = G1 [ G 2 ], unde V(G) = V( G1 ) × V( G 2 ) şi două vârfuri

(u1 , u 2 ) şi (v1 , v 2 ) ale lui G sunt adiacente dacă şi numai dacă u1v1 ∈ E(G1 ) sau ( u1 = v1 şi u 2 v2 ∈ E(G 2 ) ). 4) Suma a două grafuri. Dacă luam X = K 2 , K 2 - adiacenţa grafurilor oarecare G1 şi G 2 se mai numeşte suma celor două grafuri şi se notează cu G1 + G 2 .

3

1.5.

Clase de grafuri

Se numeşte graf complet de ordin n, graful notat, K n , unde | V(K n ) | = n şi E(K n ) = P2 (V(K n )) , iar P2 (X) este mulţimea părţilor cu două elemente ale lui X. Se numeşte graf nul de ordin n, graful N n = K n . Se numeşte ciclu de lungime n ( n ≥ 3 ) graful notat, Cn , unde V(Cn ) = {1, 2,..., n} şi E(Cn ) = {12, 23,..., n − 1n, n1} . Deoarece vârfurile sunt

distincte, ciclul este elementar. Peste tot în carte vom folosi ciclu elementar. De asemenea îl vom numi şi circuit Se numeşte lanţ de ordin n, graful Pn = Cn − e (e ∈ E(Cn )) . Îl vom numi şi drum. Dacă a = i, b = i + l, pentru orice i = l, ..., n – l şi e = ab atunci spunem că avem un a – b drum (sau ab – drum). Un circuit (drum) al unui graf G este un subgraf indus al lui G care este el însuşi circuit (drum) în G. (Observăm că un circuit (drum) al unui graf nu are corzi. O coardă într-un circuit (drum) al unui graf este o muchie cu extremităţile vârfuri neconsecutive pe circuit (drum)). Un graf G = (V,E) se numeşte n – partit, n ≥ 1 , dacă V se partiţionează în n submulţimi V1 , V2 ,..., Vn nevide astfel încât [Vi ]G = (Vi , 0) / , ∀i = 1, n (adică, orice muchie din E este incidentă cu un vârf din Vi şi un vârf din Vj , unde i ≠ j ). Pentru n = 2, un astfel de graf se numeşte bipartit. Un graf n – partit G se numeşte n – partit complet dacă mulţimile partiţiei V1 , V2 ,..., Vn au proprietatea că pentru orice u din Vi şi orice v din Vj , i ≠ j , uv este muchie a lui G. Un graf bipartit complet cu mulţimile partiţiei V1 şi V2 , unde | V1 | = m şi | V2 | = n este notat K(m,n) ( K m,n ). Graful K(l,n) (sau K1,n ) se numeşte graf stea (star). Un graf se numeşte perfect (Berge) dacă numărul cromatic şi numărul clică sunt egale pentru orice subgraf indus al său. Un graf G este minimal imperfect dacă şi numai dacă G nu este perfect şi orice subgraf G – v (v din V(G)) este perfect. O muchie e a lui G se numeşte α – critică (Olaru) dacă α (G – e) = α (G) + 1. Notăm mulţimea muchiilor α – critice ale lui G prin E c şi numim G c = (V, E c ) scheletul α – critic a lui G. Dacă E c = E, graful se numeşte α - critic.

4

Un graf G se numeşte ( α , ω ) – partiţionabil (a se vedea Golumbic, Olaru) dacă pentru fiecare v ∈ V(G), G – v admite o partiţie de α ω – clici şi o partiţie de ω mulţimi α – stabile. Un graf G = (V,E) se numeşte α – partiţionabil ( α – decompozabil) (Olaru) dacă există o partiţie Z = {V1 ,..., Vm } a lui V, cu m > 1 astfel încât

∑ im=1 α(G i ) = α(G) , unde G i = G(Vi ), i = 1, m . Z se numeşte α – partiţie a lui G, iar G i sunt α – componente. Un graf G se numeşte ω - partiţionabil dacă G este α partiţionabil. Dacă graful G are o α – partiţie astfel încât α – componentele sunt clici atunci G este perfect α – partiţionabil ( α – decompozabil). G este partiţionabil dacă este α – partiţionabil şi ω – partiţionabil. Un graf G se numeşte tare stabil (Olaru, Antohe) dacă α(G − Q) = α(G) , pentru orice clică proprie Q a lui G. Un graf G se numeşte tare α – stabil (Olaru) dacă şi numai dacă ∀v ∈ V(G) şi orice clică Q a lui G, θ(G − v) = α(G) = α(G − Q) . Cografurile sau grafurile P4 – libere (sau P4 – free), descoperite de mai mulţi autori, printre care Lerchs sunt grafuri care nu conţin P4 indus. Un graf G se numeşte gem – free dacă pentru fiecare vârf v, [NG(v)] este graf P4 – free. Un graf se numeşte paw dacă este izomorf cu ({a,b,c,d},{ab,bc,cd,db}). Un graf izomorf cu P5 se numeşte house. Un graf se numeşte bull dacă este izomorf cu ({a,b,c,d,e},{ab,bc,ca,bd,ce}). Chvatal a introdus clasa grafurilor ordonabile perfect, care sunt caracterizate prin existenţa unei ordini lineare < pe mulţimea vârfurilor astfel încât nici un P4: abcd nu are a < b şi d < c. Fie F o familie de mulţimi nevide. Graful intersecţie al lui F este obţinut reprezentând fiecare mulţime din F printr-un vârf şi două astfel de vârfuri determină o muchie dacă şi numai dacă mulţimile lor corespunzătoare se intersectează.

1.6.

Drumuri şi circuite

Fie G un graf si a, b două vârfuri ale sale. Distanţa dintre a şi b, notată d(a,b) înseamnă minimum lungimilor a – b drumurilor din G. Pentru A ⊂ V(G) şi x ∈ V(G) – A, distanţa de la x la A înseamnă d(x, A) = min a∈A d(x, a) . 5

Un graf este conex dacă există un drum între orice două vârfuri ale sale. Un graf care nu este conex se numeşte neconex. Orice graf poate fi exprimat unic ca o reuniune disjunctă de subgrafuri induse conexe şi maximale cu această proprietate. Aceste subgrafuri se numesc componente conexe ale grafului. Un arbore este un graf conex şi fără circuite.

1.7.

Mulţimi separatoare, transversale şi mulţimi omogene

Fie G = (V,E) un graf conex. O submulţime S ⊂ V se numeşte vârf separator (a se vedea Golumbic) pentru vârfurile a şi b (sau a – b - separator) dacă în G – S, a şi b vor aparţine la componente conexe diferite. Dacă nici o submulţime proprie a lui S nu este a – b separator atunci S este vârf separator minimal pentru a şi b. Fie G = (V,E) un graf conex. O submulţime A ⊂ V se numeşte mulţime separatoare (cutset) dacă G – A este neconex. O mulţime separatoare A se numeşte minimală dacă nici o submulţime proprie a sa nu este mulţime separatoare. Fie G = (V,E) un graf. O mulţime nevidă T de vârfuri se numeşte star – cutset (Chvatal) dacă G – T este neconex şi există un vârf v din T care este adiacent la toate vârfurile rămase din T. Vârful v se numeşte centrul lui T. Fie M o mulţime şi F = {Mi }i∈I o familie de submulţimi a lui M şi

T ⊂ M.

Mulţimea T este o transversală a lui F dacă T ∩ Mi ≠ 0, / ∀i ∈ I . Transversala T este perfectă dacă | T ∩ Mi | = 1, ∀i ∈ I . Fie G = (V,E) un graf. Se numeşte cuplaj (Berge) o mulţime F ⊂ E astfel încât muchiile din F sunt neadiacente. Un cuplaj se numeşte perfect dacă orice vârf din V este extremitate a unei muchii din F. Un cuplaj se numeşte maximal dacă are cardinal maxim între toate cuplajele grafului G. O submulţime nevidă A, a lui V(G), se numeşte modul dacă ∀x ∈ V(G) − A , ori x ~ A ori x ~/ A . Dacă A este submulţime proprie a lui V(G) cu cel puţin două elemente, atunci A se numeşte mulţime omogenă (Babel, Olariu, Olaru). Summer numeşte mulţimea A, partitivă. Fie A o mulţime omogenă în G. G / A este graful obţinut din G, înlocuind A cu un nou vârf a şi conectând a prin muchii cu toate vârfurile x∈V(G) – A dacă şi numai dacă x ~ A.

6

1.8.

Algoritmi şi complexitate de calcul

Vom înţelege prin problemă algoritmică (Croitoru) o funcţie total definită P : I → F , unde I este mulţimea informaţiilor iniţiale (intrărilor problemei), iar F este mulţimea informaţiilor finale. Vom presupune că I şi F sunt cel mult numărabile. Dacă i ∈ I este precizat, atunci determinarea lui P(i) se numeşte instanţă a problemei P. Vom folosi pentru o instanţă notaţia p şi prin abuz de notaţie vom scrie p ∈ P . Pentru fiecare instanţă p ∈ P se poate asocia un număr natural g(p) numit dimensiunea problemei. Un algoritm care rezolvă problema P va porni de la o codificare a unei instanţe oarecare a problemei P si va oferi o codificare a rezultatului. Vom nota cu TA (p) timpul necesar algoritmului A pentru rezolvarea instanţei p a problemei P. Comportarea în cazul cel mai nefavorabil a algoritmului A pe o intrare de dimensiune n este TA (n) = sup{TA (p) | p ∈ P şi g(p) = n} . Acest tip de analiză a algoritmilor ne asigură că, oricare ar fi o intrare de dimensiune n, timpul de lucru este mărginit de TA (n) . De aceea, o abordare naturală este să se studieze comportarea în medie a unui algoritm, care presupune: • precizarea unei distribuţii de probabilitate pe mulţimea instanţelor p∈P; • determinarea mediei variabilei aleatoare TA (p) : TAmed (n) = M({TA (p) | p ∈ P şi g(p) = n}) .

Fie f : N → N . Atunci O(f ) = {g | g : N → N, ∃c ∈ R, c > 0, ∃n 0 ∈ N : g(n) ≤ cf (n), ∀n ≥ n 0 } . Un algoritm A cu proprietatea că TA(n) = O(p(n)), unde p este un polinom în n se numeşte polinomial. Un algoritm care nu este polinomial se numeşte exponenţial. O problema pentru care nu se cunosc algoritmi polinomiali se numeşte intractabilă.

7

8

Capitolul 2 METODE DE CĂUTARE ŞI PROGRAMARE

2.1.

Căutarea în lăţime

În teoria grafurilor, breadth-first search (BFS) este un algoritm de căutare în grafuri, care începe cu vârful rădăcină şi explorează toate nodurile vecine. Apoi, pentru fiecare dintre aceste noduri se explorează nodurile vecine încă necercetate, ş.a.m.d.., până când scopul a fost atins. BFS este o metodă de căutare, care ţinteşte extinderea şi examinarea tuturor nodurilor unui graf, cu scopul de a găsi soluţia. Din punct de vedere al algoritmului, toate nodurile „fii” obţinute prin expansiunea unui nod sunt adăugate într-o „coadă” de tipul FIFO (First In First Out). În implementările tipice, nodurile care nu au fost încă examinate de către vecinii corespunzători sunt plasate într-un „recipient” (asemănător unei cozi sau unei liste de legătură), numit „deschis”, iar odată examinaţi sunt plasaţi în „recipientul” „închis”.

2.1.1. Algoritmul 1. Introducerea nodului rădăcină în coadă. 2. Extragerea unui nod din capătul listei şi examinarea acestuia. ™ Dacă elementul căutat se identifică cu acest nod, se renunţă la căutare şi se returnează rezultatul. ™ Altfel, se plasează toţi succesorii (nodurile „fii”) (neexaminaţi încă) acestui nod la sfârşitul „cozii” (acesta în cazul în care există) 3. Dacă „coada” este goală, fiecare nod al grafului a fost examinat se renunţă la căutare şi se întoarce la „not found”. 4. Repetă începând cu Pasul 2.

2.1.2. Implementarea C++ În continuare este implementarea algoritmului de mai sus, unde „neexaminaţii până în momentul de faţă” sunt gestionaţi de către tabloul părinte. Fie structura struct şi structura de noduri struct Vertex { ... std::vector out; ... };

9

std::vector graph(vertices); bool BFS(const std::vector& graph, int start, int end) { std::queue next; std::vector parent(graph.size(), 0); parent[start] = -1; next.push(start); while (!next.empty()) { int u = next.front(); next.pop(); if (u == end) return true; for (std::vector::const_iteratorj=graph[u].out.begin(); j != graph[u].out.end(); ++j) { // Look through neighbors. int v = *j; if (parent[v] == 0) { // If v is unvisited. parent[v] = u; next.push(v); } } } return false; }

Sunt stocaţi părinţii fiecărui nod, de unde se poate deduce drumul.

2.1.3. Complexitate şi optimalitate Complexitate în spaţiu. Având în vedere faptul că toate nodurile descoperite până la momentul de faţă trebuiesc salvate, complexitatea în spaţiu a breadth-first search este O(V + E ) , unde V reprezintă numărul nodurilor, iar E numărul muchiilor grafului. Altă modalitate de a consemna

( )

acest lucru: complexitate O B M , unde B reprezintă cea mai lunga ramură, iar M lungimea maximă a drumului arborelui. Această cerere imensă de spaţiu este motivul pentru care breadth-first search nu este practică în cazul problemelor mai ample. Complexitatea în timp. Odată ce, în cel mai rău caz breadth-first search trebuie să ia în considerare toate drumurile către toate nodurile, complexitatea în timp a acestui tip de căutare este de O(| V | + | E |) . Cel mai bun caz în această căutare este conferit de complexitatea O(1) . Are loc atunci când nodul este găsit la prima parcurgere. Completitudine. Metoda breadth-first search este completă. Această înseamnă că dacă există o soluţie , metoda breadth-first search o va găsi, indiferent de tipul grafului. Cu toate acestea, dacă graful este infinit şi nu există nici o soluţie, breadth-first search va “eşua”.

10

Optimalitate. Pentru costul unitar pe muchii, bread-first search este o metodă optimă. În general, breadth-first search nu este o metodă optimă, şi aceasta deoarece returnează întotdeauna rezultatul cu cele mai puţine muchii între nodul de start şi nodul vizat. Dacă graful este un graf ponderat, şi drept urmare are costuri asociate fiecărei etape, această problemă se rezolvă îmbunătăţind metoda breadth-first search astfel încât să se uniformizeze costurile de căutare, identificate cu: costurile drumului. Totuşi, dacă graful nu este ponderat, şi prin urmare toate costurile etapelor sunt egale, breadth-first search va identifica cea mai apropiată şi optimă soluţie.

2.1.4 Aplicaţii ale BFS Breadth-first search poate fi folosită pentru rezolvarea unei game variate de probleme de teoria grafurilor, printre care: • Găsirea tuturor componentelor conexe dintr-un graf. • Identificarea tuturor nodurilor într-o componentă conexă. • Găsirea celui mai scurt drum între nodurile u şi v (într-un graf neponderat). • Testarea bipartiţiei unui graf. Găsirea Componentelor Conexe Mulţimea vârfurilor accesate prin metode BFS reprezintă cea mai mare componentă conexă care conţine vârful de start. Testarea bipartiţiei BFS poate fi folosită pentru testarea bipartiţiei, începând căutarea cu orice vârf şi atribuind etichete alternative vârfurilor vizitate în timpul căutării. Astfel, se atribuie eticheta 0 vârfului de start, 1 tuturor vecinilor săi, 0 vecinilor acelor vecini, şi aşa mai departe. Dacă într-un anumit moment al procesului un vârf are vecini vizitaţi cu aceeaşi etichetă, atunci graful nu este bipartit. Dacă parcurgerea se sfârşeşte fără a se produce o astfel de situaţie, atunci graful este bipartit.

2.2.

Căutarea în adâncime

Depth-first search (DFS) este un algoritm căutare a arborelui, structurii arborelui, sau a grafului. Formal, DFS reprezintă o căutare care evoluează prin expansiunea la primul vârf „fiu” a arborelui ce ia naştere pe măsură ce se coboară în adâncime, până în momentul în care vârful „ţintă” este descoperit sau până când se întâlneşte un vârf care nu are „fii”. La pasul următor, căutarea se reia (backtracking), revenind la nodul cel mai recent vizitat, însă pentru care explorarea nu este încheiată. Într-o implementare nerecursivă, toate vârfurile recent vizitate sunt adăugate într-o stivă de tipul LIFO (Last In First Out), în scopul explorării acestora. Complexitatea în spaţiu a DFS este cu mult mai mică decât cea a BFS (Breadth-First Search). De asemenea se pretează mult mai bine metodelor euristice de alegere a 11

ramurilor asemănătoare. Complexitatea în timp a ambilor algoritmi este proporţională cu numărul vârfurilor plus numărul muchiilor grafului corespunzător (O(V + E )) . Căutarea în adâncime se poate folosi şi la ordonarea liniară a vârfurilor grafului (sau arborelui). Există trei astfel de posibilităţi: ■ O preordine reprezintă o listare a vârfurilor în ordinea în care au fost vizitaţi prin intermediul algoritmului căutării în adâncime. Aceasta este o modalitate naturală şi compactă de descriere a progresului căutării. O preordine a unei expresii arbore este ceea ce numim expresie în notaţia Polonezǎ. ■ O postordine reprezintă o listare în care cel din urmă vârf vizitat este primul element al listei. O postordine a unui expresii arbore este de fapt expresia în oglindă a expresiei în notaţie Polonezǎ. ■ O postordine inversată (în oglindă) este, de fapt, reversul postordinii, i.e. o listare a vârfurilor în ordinea inversă a celei mai recente vizite a vârfurilor in cauză. În căutarea unui arbore, postordinea inversată coincide cu preordinea, însă, în general, diferă atunci când se caută un graf. Spre exemplu când se caută graful:

începând cu vârful A, preordinile posibile sunt A B D C, respectiv A C D B (în funcţie de alegerea algoritmului de a vizita mai întâi vârful B sau vârful C), în timp ce postordinile inversate (în oglindă) sunt: A B C D şi A C B D. Postordinea inversată produce o sortare topologică a oricărui graf orientat aciclic. Această ordonare este folositore şi în analiza fluxului de control, reprezentând adesea o liniarizare naturală a fluxului de control. Graful mai sus amintit poate reprezenta fluxul de control într-un fragment de cod ca cel de mai jos: if (A) then { B } else { C } D

şi este natural să considerăm că acest cod urmează ordinea A B C D sau A C B D, însă nu este normal să urmeze ordinea A B D C sau A C D B. 12

PSEUDOCOD (recursiv) dfs(v) process(v) mark v as visited for all vertices i adjacent to v not visited dfs(i)

O altă variantă dfs(graph G) { list L = empty tree T = empty choose a starting vertex x search(x) while(L is not empty) { remove edge (v, w) from beginning of L if w not yet visited { add (v, w) to T search(w) } } } search(vertex v) { visit v for each edge (v, w) add edge (v, w) to the beginning of L }

Aplicaţii Iată câţiva algoritmi în care se foloseşte DFS: ■ Găsirea componentelor conexe. ■ Sortarea topologică. ■ Găsirea componentelor tare conexe.

2.3.

Metoda Greedy Descrierea metodei Greedy Metoda Greedy (greedy = lacom) este aplicabilă problemelor de

optim. Considerăm mulţimea finită A = {a1 ,..., a n } şi o proprietate p definită pe mulţimea submulţimilor lui A: / =1 ⎧p(0) p : P(A) → {0,1} cu ⎨ ⎩p(X) = 1 ⇒ p(Y) = 1, ∀Y ⊂ X O submulţime S ⊂ A se numeşte soluţie dacă p(S) = 1.

13

Dintre soluţii va fi aleasă una care optimizează o funcţie de cost p : P(A) → R dată. Metoda urmăreşte evitarea căutării tuturor submulţimilor (ceea ce ar necesita un timp de calcul exponenţial), mergându-se "direct" spre soluţia optimă. Nu este însă garantată obţinerea unei soluţii optime; de aceea aplicarea metodei Greedy trebuie însoţită neapărat de o demonstraţie. Distingem doua variante generale de aplicare a metodei Greedy: Prima variantă alege în mod repetat câte un element oarecare al mulţimii A şi îl adaugă soluţiei curente S numai dacă în acest mod se obţine tot o soluţie. În a doua variantă procedura prel realizează o permutare a elementelor lui A, după care elementele lui A sunt analizate în ordine şi adăugate soluţiei curente S numai dacă în acest mod se obţine tot o soluţie. Exemplu. Se consideră mulţimea de valori reale A = {a1 ,..., a n } . Se caută submulţimea a cărei sumă a elementelor este maximă. Vom parcurge mulţimea şi vom selecta numai elementele pozitive, care vor fi plasate în vectorul soluţie s. k←0 for i = 1,n if ai > 0 then k ← k + 1; sk ← ai write(s)

2.4.

Metoda backtracking

Un algoritm este considerat "acceptabil" numai dacă timpul său de executare este polinomial, adică de ordinul O(nk) pentru un anumit k; n reprezintă numărul datelor de intrare. Pentru a ne convinge de acest lucru, vom considera un calculator capabil să efectueze un milion de operaţii pe secundă. în tabelul următor apar timpii necesari pentru a efectua n3, 2n şi 3n operaţii, pentru diferite valori mici ale lui n: n = 20 n3 2n 1 sec 3n 58 min

n = 40 12,7 zile 3855 secole

n = 60 0,2 sec 366 secole 1013 secole

Tabelul de mai sus arată că algoritmii exponenţiali nu sunt acceptabili.

14

Descrierea metodei Backtracking Fie produsul cartezian X = X1 × ... × X n . Căutam x ∈ X cu ϕ(x) = 1 ,

unde ϕ : X → {0,1} este o proprietate definită pe X. Din cele de mai sus rezultă că generarea tuturor elementelor produsului cartezian X nu este acceptabilă. Metoda backtracking încearcă micşorarea timpului de calcul. X este numit spaţiul soluţiilor posibile, iar ϕ sintetizează condiţiile interne. Vectorul X este construit progresiv, începând cu prima componentă. Nu se trece la atribuirea unei valori lui x, decât dacă am stabilit valori pentru x1 ,..., x k −1 şi ϕk −1 (x1 ,..., x k −1 ) = 1 . Funcţiile ϕk : X1 × ... × X n → {0,1} se

numesc condiţii de continuare şi sunt de obicei restricţiile lui ϕ la primele k variabile. Condiţiile de continuare sunt strict necesare, ideal fiind să fie şi suficiente. Distingem următoarele cazuri posibile la alegerea lui xk: 1) "Atribuie şi avansează": mai sunt valori neanalizate din Xk şi valoarea xk aleasă satisface ϕ k=> se măreşte k. 2) "Încercare eşuată": mai sunt valori neconsumate din Xk şi valoarea xk aleasă dintre acestea nu satisface ϕ k=> se va relua, încercându-se alegerea unei noi valori pentru xk. 3) "Revenire": nu mai există valori neconsumate din Xk (Xk epuizată) ⇒ întreaga Xk devine disponibilă şi k DRNIL) then { U:=U+1; ST[U]:=NOD->DR; write{NOD->DR->INF);} } }

27

28

Capitolul 4 PARCURGERI DE GRAFURI

Problema parcurgerii unui digraf G = (N,A), N = {1, ... , n}, A = {1, ... , m} are următoarea formulare: să se genereze mulţimea W ⊂ N a nodurilor y pentru care există drum de la un nod sursă dat s la nodul y în digraful G. Dacă există drum, în digraful G, de la nodul sursă s la nodul y atunci se spune că nodul y este accesibil din nodul s. Algoritmii pe care îi vom prezenta pentru rezolvarea problemei parcurgerii unui digraf G sunt metode sistematice de vizitare a nodurilor y accesibile din s. Fiecare iteraţie a execuţiei oricărui algoritm de parcurgere stabileşte pentru fiecare nod apartenenţa la una din următoarele trei stări: • nevizitat; • vizitat şi neanalizat, adică un nod vizitat ai cărui succesori au fost parţial vizitaţi; • vizitat si analizat, adică un nod vizitat ai cărui succesori au fost în totalitate vizitaţi. Dacă nodul x este vizitat si neanalizat, există arcul (x,y) şi nodul y este nevizitat, atunci se poate vizita nodul y. În acest caz se spune că arcul (x,y) este arc admisibil şi dacă nodul y este vizitat explorând arcul (x,y) se spune că nodul x este predecesorul parcurgere al nodului y. Se vor prezenta următorii algoritmi pentru parcurgerea unui digraf: algoritmul BF şi algoritmul DF. Aceşti algoritmi utilizează următoarele notaţii comune: U mulţimea nodurilor nevizitate; V mulţimea nodurilor vizitate şi neanalizate; W mulţimea nodurilor vizitate şi analizate; p tabloul predecesor, care este unidimensional cu n elemente.

4.1. Parcurgerea BF a grafurilor Parcurgerea se face "mai întâi în lăţime". În engleză "breadth first" (BF). Fie digraful G = (N,A) cu nodul sursă s şi Dx mulţimea drumurilor de la nodul sursa s la nodul x ∈ N. Numărul de arce ce compun un drum Dx ∈ Dx defineşte lungimea acestui drum pe care îl notăm l(Dx). Distanţa de la nodul s la nodul x se defineşte în modul următor: ⎪⎧min{l(D x ) D x ∈ Dx }, Dx ≠ 0/ d(x) = ⎨ ∞, Dx = 0/ ⎪⎩ 29

ˆ ∈ D cu l( D ˆ ) = d(x) se numeşte cel mai scurt drum Un drum D x x x de la nodul sursă s la nodul x. Observaţia 1. Pentru orice arc (x,y) ∈ A avem d(y) ≤ d(x) + 1. Într-adevăr, dacă Dx = 0/ atunci d(x) = ∞ şi inegalitatea se păstrează. ˆ un cel mai scurt drum de la s Dacă Dx ≠ 0/ atunci evident Dy ≠ 0/ . Fie D x ˆ ) = d(x). Mulţimea Dy poate conţine un drum Dy, astfel încât la x, deci l( D x

l(Dy) < d(x) + 1. Conform definiţiei distanţei avem d(y) ≤ l(Dy) şi rezultă că ˆ de la s la y d(y) < d(x) + 1. Avem egalitate când un drum cel mai scurt D y are lungimea ˆ ) = d(x) + 1, d(y) = l( D y

de exemplu ˆ ∪ {(x,y)}. ˆ = D D y x În algoritmul parcurgerii BF (algoritmul PBF) se utilizează tabloul lungime l care este unidimensional şi are n elemente. Mulţimea nodurilor vizitate şi neanalizate V este organizată, ca structură de date, ca o coadă. Algoritmul PBF este următorul: (1) (2) (3) (4) (5) (6) (7) (8) (9) (10) (11) (12) (13) (14) (15)

PROGRAM PBF; BEGIN; U := N - {s} ; V := {s} ; W := 0/ ; FOR toţi y ∈ N DO p(y) := 0; l(s) := 0; FOR toţi y ∈ U DO l(y) := ∞ ; WHILE V ≠ 0/ DO BEGIN se selectează cel mai vechi nod x introdus In V; FOR (x, y) ∈ A DO IF y ∈ U THEN U:=U–{y}; V:=V ∪ {y}; p(y):=x: l(y) := l(x) + 1; V:=V-{x}; W:=W ∪ {x}; END; END.

Teorema 1 . (1) Algoritmul PBF calculează elementele tabloului l astfel încât d(y) ≤ l(y), y ∈ N ; (2) Dacă la iteraţia k oarecare a algoritmului PBF avem V = (x1, ..., xr) în această ordine, atunci l(x r ) ≤ l(x1 ) + 1 şi l(x i ) ≤ l(x i +1 ), i = 1, r − 1 .

30

Demonstraţie. (1) Utilizăm inducţia după k, numărul de iteraţii ale ciclului WHILE. Iniţial l(s) := 0, l(y) := ∞ pentru y∈U şi evident d(y) ≤ l(y), y ∈ N . Presupunem că, la iteraţia k avem d(y) ≤ l(y) pentru y ∈ N . La iteraţia k + 1 pot exista cazurile: (Cl ) Există arc (x,y) admisibil ((x,y) ∈ A şi y ∈ U). În acest caz l(y) = l(x) + 1 şi d(y) ≤ d(x) + 1 ≤ l(x) + 1 = l(y) (l(x) pentru x ∈ V nu se modifică). Deci pentru toate arcele (x,y) ∈ A şi y ∈ U avem d(y) ≤ l(y) . Pentru celelalte noduri y, conform ipotezei inducţiei, avem d(y) ≤ l(y) , deoarece la iteraţia k + 1, l(y) nu se mai modifică. (C2) Nu există arc (x,y) admisibil ((x,y) ∉ A sau y ∉ U). În acest caz la iteraţia k + 1 nu se modifică nici un element l(y) şi conform ipotezei inducţiei d(y) ≤ l(y) pentru y ∈ N . (2) Utilizăm inducţia după k numărul de iteraţii ale ciclului WHILE. Iniţial V := {s}. Deci x1 = a, xr = a şi l(s) < l(s) + 1, l(s) = l(s). Presupunem că la iteraţia k avem l(xr) ≤ l(x1) + 1 şi l(xi) < l(xi+1), pentru i = 1, r − 1 . La iteraţia k + 1 pot exista cazurile: (Cl ) Există arc (x,y) admisibil ((x,y) ∈ A şi y ∈ U). În acest caz V={x1, ..., xr, xr+1}, x1 = x, xr+1 = y. Astfel, l(xr+1) = l(y) = l(x) + 1 = l(x1)+1. De asemenea, avem l(xr) ≤ l(xr) + 1 = l(x) + 1 = l(y) = l(xr+1) şi inegalităţile l(xi) ≤ l(xi+1), i = 1, r − 1 au rămas nemodificate. (C2) Nu există arc (x,y) admisibil ((x,y) ∉ A sau y ∉ U). În acest caz V = {x2, ..., xr}. Avem l(xr) ≤ l(x1) + 1 ≤ l(x2) + 1 şi inegalităţile l(xi) ≤ l(xi+1), i = 1, r − 1 au rămas nemodificate. Teorema 2. Algoritmul PBF este convergent şi la terminarea execuţiei determinăm mulţimea tuturor nodurilor care sunt accesibile din nodul sursă s în digraful G = (N,A). Demonstraţie. Din liniile (10), (11) şi (12) ale algoritmului rezultă că toate nodurile introduse în V sunt eliminate după ce sunt analizate. Deci după un număr finit de iteraţii se obţine V = 0/ şi execuţia algoritmului se opreşte. Pentru a arăta că la terminarea execuţiei, algoritmul determină mulţimea tuturor nodurilor care sunt accesibile din nodul sursă s în digraful G = (N,A), trebuie să arătăm că la terminarea execuţiei algoritmului mulţimea W este: W = {y y ∈ N şi există drum de la s la y}. Din liniile (10), (11) şi (12) ale algoritmului rezultă că în V sunt

31

introduse numai noduri y care sunt accesibile din s şi că după ce un nod x ∈ V a fost analizat el este eliminat din V şi introdus în W. Deoarece algoritmul se opreşte când V = 0/ rezultă că W conţine toate nodurile y ∈ N care sunt accesibile din s şi introduse în V. Să arătăm că W conţine toate nodurile y ∈ N care sunt accesibile din s. Prin reducere la absurd să presupunem că există un drum D = (yl,y2, ... ,yk-l,yk) cu yi = s, yk = y în G şi y∉ W. Rezultă că yk ∉ V. Deoarece yk ∉ V şi (yk-h,yk) ∈ A deducem că yk-1 ∉V, astfel yk ar fi fost introdus în V. Continuând procedeul vom deduce în final că s = y1 ∉ V. Aceasta contrazice faptul că în linia (3) a algoritmului iniţializăm V:={s}. Rezulta că y ∈ V, deci y ∈ W şi teorema este demonstrată. Teorema 3. Algoritmul PBF este convergent şi determină: (1) mulţimea tuturor nodurilor care sunt accesibile din nodul sursă s; (2) elementele tabloului l astfel încât l(y) = d(y) pentru y ∈ N. Demonstraţie. Convergenţa şi punctul (1) se demonstrează la fel ca Teorema 2. Punctul (2) se demonstrează prin inducţie după k numărul iteraţiilor ciclului WHILE. Fie mulţimea Nk = {y ∈ N |d(y) = k}. Pentru k = 0 avem N0 = {s} şi deci d(s) = l(s) = 0. Presupunem afirmaţia adevărată pentru k. Afirmaţia pentru k + 1 rezultă cu uşurinţă, deoarece, în conformitate cu Teorema 1 punctul (2), un nod y ∈ Nk+l este vizitat plecând de la un nod x ∈ Nk numai după ce toate nodurile din Nk sunt vizitate. Deci, dacă y ∈ Nk+l şi este vizitat explorând arcul (x,y), x ∈ Nk, atunci, l(y) = l(x) + 1 = d(x) + 1 = k + 1 = d(y). Teorema 4. Algoritmul PBF are complexitatea O(m). Demonstraţie. Din liniile (10) , (11) şi (12) ale algoritmului rezultă că fiecare nod al digrafului G este introdus şi eliminat din V cel mult o dată. Deoarece execuţia algoritmului se termină când V = 0/ deducem că algoritmul execută cel mult 2n iteraţii. Fiecare arc (x,y)∈A este explorat cel mult o dată pentru identificarea arcelor admisibile. Deci complexitatea algoritmului PBF este O(m + n) = O(m). În parcurgerea BF, dacă Np = W şi subgraful predecesor Gp = (Np,Ap) este o arborescenţă atunci Gp se numeşte arborescenţă parcurgere BPF. Teorema 5. Algoritmul PBF determină elementele tabloului p astfel încât subgraful predecesor Gp = (Np,Ap) este o arborescenţă parcurgere BPF. Demonstraţie. Din liniile (10), (11) şi (12) ale algoritmului rezultă că p(y) := x numai dacă y este accesibil din s. Evident că la terminarea execuţiei algoritmului avem Np = W. Din modul cum sunt definite Np şi Ap

32

rezultă că Gp este o arborescenţă. Deci subgraful predecesor Gp este o arborescenţă parcurgere a digrafului G = (N,A). Observaţia 2. Drumul unic de la nodul sursă s la un nod y din arborescenţa parcurgere BF este un cel mai scurt drum de la nodul sursă s la acelaşi nod y din digraful G, conform punctului (2) al Teoremei 3. Observaţia 3. Algoritmul PBF sau PTBF se poate aplica şi grafurilor neorientate. În acest caz, subgraful predecesor Gp = (Np,Ap) este o arborescenţă. sau mai multe arborescenţe. Observaţia 4. Un drum unic de la nodul sursă s la un nod y din arborescenta parcurgere BF se poate determina cu următoarea procedură. (1) (2) (3) (4) (5) (6) (7) (8) (9) (10) (11)

PROCEDURA DRUM (G,s,y); BEGIN; se tipăreşte u; WHILE p (y)≠i-0 DO BEGIN x := p(y); se tipăreşte x; IF x≠s THEN y := x ELSE EXIT; END; END.

Observaţia 5. Mulţimea W este în general submulţimea mulţimii nodurilor N. Pentru parcurgerea întregului digraf G = (N,A) se utilizează algoritmul parcurgerii totale generice (algoritmul PTG). Algoritmul PTG este următorul: (1) (2) (3) (4) (5) (6) (7) (8) (9) (10) (11) (12) (13) (14) (15) (16) (17) (18)

PROGRAM PTG; BEGIN; U := N - {s}; V :== {s}; W := 0/ ; FOR toţi y ∈ N DO p(y) := 0; k:= 1; o(s):= 1; FOR toţi y ∈ U DO o(y) := ∞ ; WHILE W≠N DO BEGIN WHILE V ≠ 0/ DO BEGIN se selectează un nod x din V; IF există arc (x, y) ∈ A şi y ∈ U THEN U:=U-{y}; V:=V ∪ {y};p(y):=x; k := k + 1; o(y) :=k ELSE V:=V-{x}; W:=W ∪ {x}; END; se selectează s ∈ U; U:= U - {s}; V:= {s}; k := k + 1: o(S) := k; END; END.

Este evident că algoritmul PTG are complexitatea tot O(m) şi că vectorul p determină una sau mai multe arborescente parcurgeri. 33

Exemplu 1. Se aplică algoritmul PBF digrafului din figura 1 .

Figura 1 Iniţializări: s = 1, U = {2,3,4,5,6}, V = {1}, W = 0/ , p = (0,0,0,0,0,0), l = (0, ∞ , ∞ , ∞ , ∞ , ∞ ). Iteraţia 1: x = 1, (1,2) ∈ A, 2 ∈ U: U = {3,4,5,6}, V = {1,2}, p = (0,1,0,0,0,0) , l = (0,1, ∞ , ∞ , ∞ , ∞ ); (1,3) ∈ A, 3 ∈ U: U = {4,5,6}, V = {1,2,3}, p= (0,1,1,0,0,0), l = (0,1, 1, ∞ , ∞ , ∞ ); V = {2, 3}; W = {1}. Iteraţia 2: x = 2, (2,4) ∈ A, 4 ∈ U: U = {5,6}, V = {2,3, 4}, p = (0,1,1,2,0,0), l = (0,1,1,2, ∞ , ∞ ); (2,5) ∈ A, 5 ∈ U : U = {6}, V = {2,3,4,5} , p = (0,1,1,2,2,0) , l = (0,1,1,2,2, ∞ ) ; V = {3, 4, 5} ; W = {1, 2}. Iteraţia 3: x = 3, V = {4, 5}, W = {1, 2, 3}. Iteraţia 4: x = 4, (4, 6) ∈ A, 6 ∈ U: U = 0/ , V = {4, 5,6}, p = (0,1,1,2,2,4), l = (0,1,1,2,2,3); V = {5,6}; W = {1,2,3,4}. Iteraţia 5: x =5, V={6}, W = {1,2,3,4,5}. Iteraţia 6: x = 6, V = 0/ , W = {1,2,3,4,5,6}. Np = {1,2,3,4,5,6} = W. Arborescenţa parcurgere BF este prezentată în figura 2.

34

Figura 2 Drumul unic de la nodul 1 la nodul 6 se obţine cu PROCEDURA DRUM în modul următor: y = 6 este ultimul nod al drumului; Iteraţia 1: x = p(6) = 4, y = 4. Iteraţia 2: x = p(4) = 2, y = 2. Iteraţia 3: x = p(2) = 1, y = l. Drumul este: 1,2,4,6.

4.2. Parcurgerea DF a grafurilor Parcurgerea se face "mai întâi în adâncime". În engleză "depth first" (DF). În algoritmul parcurgerii DF (algoritmul PDF) se folosesc aceleaşi notaţii ea în algoritmul PBF cu deosebirea că, în locul tabloului unidimensional de lungime l se utilizează tablourile timp unidimensionale t1 şi t2 care au fiecare n elemente. Mulţimea nodurilor vizitate şi neanalizate V este organizată ca structură de date, ca stivă. Algoritmul PDF este următorul: (1) (2) (3) (4) (5) (6) (7) (8) (9) (10) (11) (12) (13) (14)

PROGRAM PDF; BEGIN; U := N-{s}; V := {s}; W := 0/ ; FOR toţi y ∈ N DO p(y) := 0; t := 1; t1(s) := 1; t2(s) := ∞ ; FOR toţi y ∈ U DO t1(y) := ∞ ; t2(y) := ∞ ; WHILE V ≠ 0/ DO BEGIN se selectează cel mai nou nod x introdus in V; IF există arc (x,y) ∈ A şi y ∈ U THEN U := U - {y}; V := V ∪ {y}; p (y) := x; t := t + 1; t1(y) := t ELSE V := V-{x}; W := W ∪ {x}; t:=t+1;t2(x):=t; END; END.

35

Din liniile (10), (11), (12) ale algoritmului PDF rezultă că elementul t1(y) reprezintă momentul când y devine nod vizitat şi neanalizat şi elementul t2(x) reprezintă momentul când x devine nod vizitat şi analizat. Se va studia în continuare algoritmul parcurgerii totale DF (algoritmul PTDF). Algoritmul PTDF este următorul: (1) PROGRAM PTDF; (2) BEGIN; (3) U:=N-{s}; V:={s}; W:= 0/ ; (4) FOR toţi y ∈ N DO p(y) := 0; (5) t:=1; t1(s):= l; t2(s):= ∞ ; (6) FOR toţi y ∈ U DO t1(y) := ∞ ; t2(y) := ∞ ; (7) WHILE W≠N DO (8) BEGIN (9) WHILE V≠ 0/ DO (10) BEGIN (11) se selectează cel mai nou nod x introdus în V; (12) IF există arc (x,y) ∈ A şi y ∈ U (13) THEN U := U - {y}; V := V ∪ {y}; p (y) := x; t := t + 1; t1(y) := t; (14) ELSE V:= V - {x}; W:= W ∪ {x}; t:= t + 1; t2 (x) := t; (15) END; (16) se selectează s ∈ U; U := U - {s}; V := {s}; t:=t+l; t1(s):=t; (17) END; (18) END.

Fie mulţimea S = {s s ∈ N , s selectate în linia (3) şi linia (16)}.

Teorema 6. Algoritmul PTDF este convergent şi determină mulţimile nodurilor accesibile din s, s ∈ S. Demonstraţie. Se demonstrează la fel ca Teorema 2. Teorema 7. Algoritmul PTDF are complexitatea O(m). Demonstraţie. Evident că. algoritmul PTDF are complexitatea O(m). ˆ = (N, ˆ se numeşte pădure dacă este format din una ˆ A) Un digraf G ˆ = (N, ˆ se numeşte ˆ A) sau mai multe arborescenţe. Un graf neorientat G

pădure dacă este format din unul sau mai mulţi arbori. În parcurgerea totală DF, dacă subgraful predecesor Gp = (Np,Ap), N p = {y p(y) ≠ 0} ∪ S, A p = {(p(y), y) y ∈ N p − S} este o pădure şi Np = W, atunci Gp se numeşte pădure parcurgere DF. 36

Teorema 8. Algoritmul PTDF determină elementele tabloului p astfel încât subgraful predecesor Gp = (Np,Ap) este o pădure parcurgere DF. Demonstraţie. Se demonstrează analog ca Teorema 5. Exemplul 2. Se aplică algoritmul PTDF digrafului

Figura 3 Iniţializări: s = 1,U = {2,3,4,5,6, ,8}, V = {1}, W = 0/ , p p = (0,0,0,0,0,0,0,0), t = 1, t1 = (1, ∞ , ∞ , ∞ , ∞ , ∞ , ∞ , ∞ ), t2 = ( ∞ , ∞ , ∞ , ∞ , ∞ , ∞ , ∞ , ∞ ). Iteraţia 1: x = 1, (1,2) ∈ A, 2 ∈ U: U = {3,4,5,6,7,8}, V = {1,2}, p = (0,1,0,0,0,0,0,0), t = 2, tl = (1,2, ∞ , ∞ , ∞ , ∞ , ∞ , ∞ , ∞ ). Iteraţia 2: x = 2, (2,3) ∈ A,3 ∈ U: U = {4,5,6,7,8}, V = {1,2,3}, p = (0,1,2,0,0,0,0,0), t = 3, t1 = (1, 2, 3, ∞ , ∞ , ∞ , ∞ , ∞ ). Iteraţia 3: x = 3, (3,4) ∈ A,4 ∈ U: U = {5,6,7,8}, V = {1,2,3,4}, p = (0,1,2,3,0,0,0,0), t = 4, t1 = (1,2,3,4, ∞ , ∞ , ∞ , ∞ ). Iteraţia 4: x = 4: V = {1,2, 3}, W = {4}, t = 5, t2 = ( ∞ , ∞ , ∞ ,5, ∞ , ∞ , ∞ , ∞ ). Iteraţia 5: x = 3: V = {1,2}, W = {4,3}, t = 6, t2 = ( ∞ , ∞ ,6,5, ∞ , ∞ , ∞ , ∞ ). Iteraţia 6: x = 2, (2,5) ∈ A, 5 ∈ U: U = {6,7,8}, V = {1,2,5}, p = (0,1,2,3,2,0,0,0), t = 7, tl = (1,2,3,4,7, ∞ , ∞ , ∞ ). Iteraţia 7: x = 5: V = {1,2}, W = {4,3,5}, t = 8, t2 = ( ∞ , ∞ ,6,5,8, ∞ , ∞ , ∞ ). 37

Iteraţia 8: x = 2 : V = {1}, W = {4,3,5,2}, t = 9, t2 = ( ∞ ,9,6,5,8, ∞ , ∞ , ∞ ). Iteraţia 9: x = 1: V = 0, W = {4,3,5,2,l}, t = 10, t2 = (10,9,6,5,8, ∞ , ∞ , ∞ ). Actualizări: s = 6, U = {7,8}, V = {6}, t = 11, t1 = (1,2,3,4,7,11, ∞ , ∞ ) Iteraţia 10: x = 6, (6, 7)∈A, 7∈U : U = {8}, V = {6, 7}, p = (0,1,2,3,2,0,6,0), t = 12, t1 = (1,2,3,4,7,11,12, ∞ ). Iteraţia 11: x = 7 : V = {6}, W = {4, 3,5,2,1,7}, t = 13, t2 = (10,9,6,5,8,00,13, ∞ ). Iteraţia 12: x = 6, (6,8) ∈ A,8 ∈ U: U = 0/ , V = {6,8}, p = (0,1,2,3,2,0,6,6), t = 14, tl = (1,2,3,4,7,11,12,14). Iteraţia 13: x = 8 : V = {6}, W = {4, 3, 5, 2,1, 7,8}, t = 15, t2 = (10,9,6,5,8, ∞ ,13,15). Iteraţia 14: x = 6: V = 0/ , W = {4,3,5,2,1,7,8,6}, t = 16, t2 = (10,9,6,5,8,16,13,15).

4.3. Aplicaţii 4.3.1. Sortarea topologică Teorema 9. Un digraf G = (N,A) este fără circuite dacă şi numai dacă orice parcurgere totală DF a lui G nu produce arce de revenire. Demonstraţie. Presupunem că digraful G este fără circuite. Prin reducere la absurd presupunem ca o parcurgere totală DF a lui G produce arce de revenire ( R ≠ 0/ ). Fie arcul (z, x) ∈ R . În acest caz nodul z este un descendent al nodului x în pădurea parcurgere DF. Deci există un drum D de o

la x la z în G. Atunci D = D ∪ {z,x} este un circuit în G şi aceasta contrazice ipoteza că digraful G este fără circuite. Reciproc, presupunem că o parcurgere totală DF a digrafului G nu produce arce de revenire ( R ≠ 0/ ). Prin reducere la absurd presupunem că o

o

G conţine un circuit D . Fie x primul nod vizitat din D şi fie arcul o

o

(z,x) ∈ D . Deoarece nodurile x, z ∈ D rezultă că există un drum D de la x

38

o

la z. De asemenea x fiind primul nod vizitat din D rezultă că nodul z devine un descendent al nodului x la pădurea PDF. Deci (z,x) este un arc de revenire ce contrazice ipoteza R = 0/ . Sortarea topologică a unui digraf G = (N,A) fără circuite constă într-o ordonare liniară a nodurilor din N astfel încât dacă (x, y) ∈ A atunci x apare înaintea lui y în ordonare. Algoritmul sortare topologică (algoritmul ST) se obţine din algoritmul PTDF făcând următoarele două completări: (1) în partea de iniţializări (liniile (3)-(6)) se iniţializează o listă a nodurilor; (2) în linia (16) după calculul lui t2(X), nodul x se introduce la începutul listei. La terminarea algoritmului ST, lista furnizează sortarea topologică a digrafului G = (N,A) fără circuite şi nodurile x sunt plasate în listă în ordinea descrescătoare a timpilor t2(X).

4.3.2. Componentele conexe ale unui graf Definiţia 1. Un digraf G = (N,A) se numeşte conex dacă pentru oricare două noduri x, y există un lanţ care are aceste două noduri drept extremităţi. Noţiunea de conexitate are sens şi pentru grafuri neorientate. Definiţia 2. Se numeşte componentă conexă a unui digraf G = (N,A) un subgraf G' = (N',A') al lui G, care este conex şi care este maximal în raport cu incluziunea faţă de această proprietate (oricare ar fi x ∈ N ' = N − N ' , subgraful G 'x generat de N 'x = N '∪ {x} nu mai este

conex). O componentă conexă G' = (N',A') a unui digraf G = (N,A) se poate identifica cu mulţimea N' care generează subgraful G'. Deoarece în problema conexiunii sensul arcelor nu contează se va considera că grafurile sunt neorientate. Dacă G = (N,A) este digraf atunci i se ˆ = (N, ˆ , unde N ˆ = {[x, y] (x, y) ∈ A ˆ A) ˆ = N, A asociază graful neorientat G ˆ au aceleaşi componente conexe. şi / sau (y, x) ∈ A }. Este evident că G şi G Algoritmul componentelor conexe (algoritmul CC) este o adaptare a algoritmului PTDF aplicat unui graf neorientat G = (N,A). Nu se calculează tablourile timp t1, t2 şi prin p notăm o variabilă scalară a cărei valoare reprezintă numărul componentelor conexe. Algoritmul CC este următorul: (1) (2) (3) (4) (5)

PROGRAM CC; BEGIN U:= N - {s}; V:= {s}; W:= 0/ ; p:= l; N' = {s}; WHILE W ≠ N DO BEGIN

39

WHILE V≠ 0/ DO BEGIN se selectează cel mai nou nod x introdus în V; IF există muchie [x, y] ∈ A şi y ∈ V THEN U:= U-{y}; V:=V ∪ {y};N':=N' ∪ {y} ELSE V:= V-{x}; W:=W ∪ {x}; END; se tipăresc p şi N'; se selectează s ∈ U; U:=U-{s}; V:={s}; p:= p+1;N':={s};

(6) (7) (8) (9) (10) (11) (12) (13) (14) (15) (16)

END; END.

La terminarea algoritmului pot exista cazurile: (Cl) se tipăreşte o singură componentă conexă şi în acest caz graful G = (N,A) este conex; (C2) se tipăresc mai multe componente conexe şi în acest caz graful G = (N,A) nu este conex, obţinându-se toate componentele conexe ale lui G. Teorema 10. Algoritmul CC determină componentele conexe ale unui graf neorientat G = (N,A). Demonstraţie. La terminarea execuţiei ciclului WHILE se determină mulţimea N' a tuturor nodurilor accesibile printr-un lanţ cu aceeaşi extremitate, nodul s. Mulţimea N' generează evident o componentă conexă G' = (N',A'). Deoarece la terminarea execuţiei algoritmului avem W = N rezultă că algoritmul CC determină toate componentele conexe ale lui G = (N,A). Teorema 11. Algoritmul CC are complexitatea O(m). Demonstraţie. Algoritmul CC are aceeaşi complexitate cu a algoritmului PTDF, adică O(m). Exemplul 3. Fie digraful din figura 4. Pentru a determina componentele conexe ale acestui digraf se transformă într-un graf neorientat reprezentat în figura 5 căruia i se aplică algoritmul CC.

Figura 4

40

Figura 5 Iniţializări: s = 1, U = {2,3,4,5,6,7,8}, V = {1}, W = 0/ , p = 1, N'={1}. Iteraţia 1: x = 1, [1,2]∈A, 2∈U : U = {3,4,5,6,7,8}, V = {1,2}, N'={1,2}. Iteraţia 2: x = 2, [2,3]∈A, 3∈U: U={4,5,6,7,8}, V={1,2,3}, N'={1,2,3}. Iteraţia 3: x=3, [3,4]∈A, 4∈U: U={5,6,7,8}, V={1,2,3,4}, N'={1,2,3,4}. Iteraţia 4: x = 4: V = {1,2,3}, W = {4}. Iteraţia 5: x = 3: V = {1,2}, W = {4,3}. Iteraţia 6: x = 2, V = {1}, W = {4,3,2}. Iteraţia 7: x = 1 : V = 0/ , W = {4, 3, 2, 1}. Se tipăresc: p = 1 şi N' = {1,2,3,4} Actualizări: s = 5, U = {6,7,8}, V = {5}, p = 2, N' = {5}. Iteraţia 8: x = 5, [5,6] ∈ A, 6 ∈ U : U = {7,8}, V = {5,6}, N' = {5,6}. Iteraţia 9: x = 6, [6,8] ∈ A, 8 ∈ U : U = {7}, V = {5,6,8}, N' = {5,6,8}. Iteraţia 10: x = 6, [8, 7]∈A, 7∈U: U = 0/ , V = {5,6,8,7}, N'={5,6,8,7}. Iteraţia 11: x = 7 : V = {5,6,8}, W = {4,3,2,1,7}. După încă trei iteraţii se obţine V = 0/ , W = {4,3,2,1,7,8,6,5} Se tipăresc p = 2 şi N' = {5,6,8,7} şi execuţia algoritmului se opreşte.

41

42

Capitolul 5 PROBLEME DE DRUM ÎN (DI)GRAFURI

5.1.

Problema celui mai scurt drum

În teoria grafurilor, problema celui mai scurt drum constă în găsirea unui drum astfel încât suma “costurilor” muchiilor constituente să fie minimă. Un exemplu îl constituie găsirea celei mai rapide modalităţi de a trece de la o locaţie la alta pe o hartă; în acest caz nodurile sunt reprezentate de către locaţiile respective, iar muchiile reprezintă segmentele de drum, şi sunt ponderate, costurile constituind timpul necesar parcurgerii acelui segment. Formal, fiind dat un graf ponderat (adică, o mulţime de vârfuri V, o mulţime a muchiilor E, şi o funcţie de cost f :E →R cu valori reale) şi un element v al lui V, să se găsească un drum P de la v la fiecare v ′ din V astfel încât ∑ f ( p) p∈P

să fie minim între toate drumurile ce leagă v de v ′ . Uneori mai poate fi recunoscută sub numele de problema drumului cel mai scurt corespunzător perechii singulare, cu scopul deosebirii acesteia de următoarele generalizări: • problema drumului cel mai scurt corespunzător sursei unice, o problemă mai generală, în care trebuie să găsim cele mai scurte drumuri de la un nod sursă v la toate celelalte noduri ale grafului. • problema drumului cel mai scurt corespunzător tuturor perechilor reprezintă o problemă şi mai generală, în care trebuie să găsim cele mai scurte drumuri între oricare pereche de noduri (vârfuri) v, v ′ din graf. Ambele generalizări amintite au algoritmi mai performanţi în practică decât simpla rulare a algoritmului corespunzător drumului cel mai scurt în cazul perechii-unice (singulare) pentru toate perechile relevante de vârfuri. Algoritmi Cei mai importanţi algoritmi care rezolvă această problemă sunt: • Algoritmul lui Dijkstra – rezolvă problema sursei unice, dacă toate muchiile sunt ponderate pozitiv Acest algoritm poate

43

• • • •

genera cele mai scurte drumuri de la un anumit punct de placare s la toate celelalte noduri. Algoritmul Bellman-Ford – rezolvă problema sursei unice şi pentru costuri negative ale muchiilor. Algoritmul de căutare A* - rezolvă problema drumurilor cele mai scurte în cazul sursei unice, folosind euristica, în încercarea accelerării căutării. Algoritmul Floyd-Warshall – rezolvă problema celor mai scurte drumuri corespunzătoare tuturor perechilor. Algoritmul lui Johnson - rezolvă problema celor mai scurte drumuri corespunzătoare tuturor perechilor; poate fi mai rapid ca Algoritmul Floyd-Warshall, în cazul grafurilor rare.

Aplicaţii Algoritmii ce rezolvă problema celui mai scurt drum se aplică, în mod evident, pentru a găsi, în mod automat, adrese între diferite locaţii fizice, cum ar fi spre exemplu instrucţiuni legate de şofat oferite de GPS – uri sau programele web de mapare (Mapquest). Dacă reprezentăm, spre exemplu, o maşină abstractă nedeterministă sub forma unui graf, în care vârfurile descriu state, iar muchiile descriu posibile tranziţii, algoritmii de identificare a celui mai scurt drum pot fi folosiţi pentru a găsi o secvenţă optimală de alegeri, astfel încât să ajungă într-un stat prestabilit, sau pentru a minimiza timpul necesar pentru a ajunge în acel stat.

44

5.1.1. Arborele Steiner

Soluţia pentru 3 puncte; punctul Steiner este cel din mijloc – a se remarca faptul că nu există conexiuni directe între A, B, C

Soluţia pentru 4 puncte – a se remarca faptul că există 2 puncte Steiner Problema Arborelui Steiner este, aproximativ, similară problemei arborelui parţial de cost minim: fiind dată o mulţime V de vârfuri, interconectaţi aceste puncte prin intermediul unui graf de lungime minimă, unde lungimea reprezintă suma lungimilor tuturor muchiilor. Diferenţa între Problema Arborelui Steiner şi Problema Arborelui Parţial de Cost Minim constă în faptul că în cadrul Arborelui Steiner pot fi adăugate grafului iniţial vârfuri şi muchii intermediare, cu scopul reducerii lungimi arborelui parţial. Aceste vârfuri nou introduse, în scopul reducerii lungimii totale a conexiunii, sunt cunoscute sub numele de Puncte Steiner sau Vârfuri Steiner. S-a demonstrat că acea conexiune rezultantă este un arbore, numit şi Arborele Steiner. Pot exista mai mulţi arbori Steiner pentru o mulţime dată de vârfuri iniţiale. Problema originală a fost formulată în forma cunoscută sub numele de Problema Arborelui Euclidean Steiner: Fiind date N puncte în plan, se cere să se conecteze prin intermediul liniilor, valoarea rezultantă a acestora

45

fiind minimă, astfel încât oricare două puncte sunt interconectate, fie printrun segment de linie, fie via alte puncte, respectiv alte segmente de dreaptă. Pentru Problema Euclidean Steiner, punctele adăugate grafului (Punctele Steiner) trebuie să aibă gradul trei, iar cele trei muchii incidente corespunzătoare trebuie să formeze trei unghiuri de 120 de grade. Rezultă că numărul maxim de Puncte Steiner pe care le poate avea un Arbore Steiner este de N-2, unde N reprezintă numărul iniţial de puncte considerate. Se poate încă generaliza până la Problema Metrică a Arborelui Steiner. Fiind dat un graf ponderat G(S,E,w) ale cărui vârfuri corespund unor puncte în spaţiul metric, iar „costul” muchiilor este reprezentat de distanţele în spaţiu, se cere să se găsească un arbore de lungime totală minimă, ai cărui vârfuri constituie o supermulţime a mulţimii S, mulţime a vârfurilor grafului G. Versiunea cea mai generală o constituie Arborele Steiner în grafuri: Fiind dat un graf ponderat G(V,E,w) şi o submulţime de vârfuri S ⊆ V găsiţi un arbore de cost minim care include toate nodurile mulţimii S. Problema Metrică a Arborelui Steiner corespunde problemei Arborelui Steiner în grafuri, unde graful are un număr infinit de noduri, toate fiind puncte în spaţiul metric. Problema arborelui Steiner are aplicaţii în design-ului reţelelor. Majoritatea versiunilor Problemei Arborelui Steiner sunt NP – complete, i.e., gândite ca fiind computaţional-dificile. În realitate, una dintre acestea se număra printre cele 21 de probleme iniţiale ale lui Karp, NP – complete. Unele cazuri restrictive pot fi rezolvate într-un timp polinomial. În practică se folosesc algoritmii euristici. O aproximare comună a Problemei Arborelui Euclidian Steiner este reprezentată de calcularea arborelui parţial de cost minim Euclidian.

5.1.2. Algoritmul lui Dijkstra Algoritmul lui Dijkstra, după numele celui care l-a descoperit, expertul în calculatoare Edsger Dijkstra, este un algoritm greedy care rezolvă problema celui mai scurt drum cu o singură sursă pentru un graf orientat, care nu are muchii ponderate negativ. Spre exemplu, dacă vârfurile grafului reprezintă oraşe, iar costurile muchiilor reprezintă distanţele de parcurs între perechi de oraşe conectate printr-un drum direct, algoritmul lui Dijkstra poate fi folosit pentru depistarea celui mai scurt traseu între cele două oraşe. Datele de intrare necesare implementării algoritmului sunt: un graf orientat ponderat G şi un vârf sursă s în G. Vom nota cu V mulţimea tuturor vârfurilor grafului G. Fiecare muchie a grafului reprezintă o pereche ordonată de vârfuri (u, v), semnificaţia acesteia fiind legătura între u şi v. Mulţimea tuturor muchiilor este notată cu E. Costurile muchiilor sunt date de 46

funcţia de cost w : E → [0, ∞) ; astfel, w(u , v) reprezintă costul muchiei (u,v). Costul unei muchii poate fi închipuit ca o generalizare a distanţei între aceste două vârfuri. Costul unui drum între două vârfuri este dat de suma tuturor costurilor muchiilor componente. Pentru o pereche dată de vârfuri s şi t din V, algoritmul găseşte drumul de cost minim între s şi t (i.e. cel mai scurt drum). Algoritmul poate fi folosit, în aceeaşi măsură pentru depistarea drumurilor de cost minim între vârful sursă s şi toate celelalte vârfuri ale grafului. Descrierea algoritmului Algoritmul funcţionează reţinând, pentru fiecare vârf v, costul d [v ] al celui mai scurt drum găsit până în acel moment între s şi v. Iniţial, această valoare este 0, pentru vârful sursă s ( d [s ] = 0 ), respectiv infinit pentru restul vârfurilor, sugerând faptul că nu se cunoaşte nici un drum către aceste noduri (vârfuri) ( d [v ] = ∞ pentru fiecare v din V, exceptând s). La finalul algoritmului, d [v ] va reprezenta costul celui mai scurt drum de la s la v – sau infinit, dacă nu există un astfel de drum. Algoritmul presupune existenţa a două mulţimi de vârfuri S şi Q. Mulţimea S conţine toate vârfurile pentru care se cunoaşte valoarea d [v ] , valoare ce corespunde costului celui mai scurt drum, iar mulţimea Q conţine toate celelalte vârfuri . Mulţimea S este, iniţial,goală (nu are elemente), iar cu fiecare pas un vârf din mulţimea Q devine element al mulţimii S. Acest vârf este ales astfel încât d [v ] să corespundă celei mai mici valori. Odată cu „mutarea” vârfului u în mulţimea S, algoritmul „relaxează” fiecare muchie de forma (u,v). Aceasta înseamnă că, pentru fiecare vecin al lui v sau al lui u, algoritmul verifică dacă poate optimiza drumul (la v) cunoscut ca fiind cel mai scurt până la acel moment, urmând drumul cel mai scurt de la sursa s la u, traversând în cele din urmă muchie (u, v). Dacă acest nou drum este mai bun (în sensul unui cost mai mic), algoritmul actualizează d [v ], atribuindu-i valoarea mai mică.

Execuţia algoritmului Dijkstra asupra unui graf mic, demonstrând două operaţii de relaxare Pe măsură ce se găsesc drumuri mai scurte, costul estimat este redus, iar sursa se relaxează. Eventual, drumul cel mai scurt, dacă există, se relaxează la maximum.

47

Pseudocodul În algoritmul ce urmează, u := extract_min(Q) caută vârful u în mulţimea vârfurilor Q, care are cea mai mică valoare asociată dist[u]. Vârful este scos din mulţimea Q şi returnat utilizatorului. length(u, v) calculează distanţa între cele două vârfuri vecine u şi v alt de pe linia 10 reprezintă lungimea drumului de la rădăcină la v, dacă ar fi să treacă prin u. Dacă acest drum este mai scurt decât drumul considerat în momentul respectiv ca fiind cel mai scurt, acel drum curent este înlocuit cu acest alt drum. 1 2 3 4 5 6 7 8 9 10 11 12 13

function Dijkstra(Graph, source): for each vârf v in Graph: dist[v] := infinity previous[v] := undefined dist[source] := 0 Q := copy(Graph) while Q is not empty: u := extract_min(Q) for each vecin v of u: alt = dist[u] + length(u, v) if alt < dist[v] dist[v] := alt previous[v] := u

Dacă, însă ne interesează doar un drum mai scurt între vârfurile sursă şi ţintă, căutarea poate înceta la punctul 9 dacă u=target. Acum putem „citi” cel mai scurt drum de la sursă la ţintă prin iterare: 1 S := empty sequence 2 u := target 3 while este definit previous[u] 4 inserează u la începutul of S 5 u := previous[u]

Acum secvenţa S reprezintă lista vârfurilor ce constituie unul dintre cele mai scurte drumuri de la sursă la ţintă, sau secvenţa nulă dacă un astfel de drum nu există. O problemă mult mai generală ar fi aceea a determinării tuturor celor mai scurte drumuri între sursă şi ţintă (pot fi mai multe astfel de drumuri, de aceeaşi lungime). În acest caz, în locul memorării unui singur nod la fiecare „intrare” previous[], se vor păstra toate vârfurile ce satisfac condiţia de relaxare. Spre exemplu, dacă atât r cât şi sursa sunt conectate (sunt în legătură) cu ţinta şi ambele aparţin unor celor mai scurte drumuri distincte, ce ating ţinta (deoarece costul muchiilor este acelaşi în ambele cazuri), atunci vom adăuga ambele vârfuri – r şi sursă – valorii anterioare [target]. Când algoritmul este complet, structura de date previous[] va descrie un graf, care este subgraf al grafului iniţial din care au fost înlăturate unele muchii. Proprietatea esenţială va fi dată de faptul că dacă algoritmul a rulat cu un 48

anumit vârf de început, atunci fiecare drum de la acel vârf către oricare alt vârf, în noul graf, va fi cel mai scurt între nodurile respective în graful original, iar toate drumurile de aceiaşi lungime din garful original vor fi prezente în graful rezultant. Astfel, pentru a găsi aceste drumuri scurte între oricare două vârfuri date vom folosi algoritmul de găsire a drumului în noul graf, asemenea depth-first search (căutării în adâncime). Timpul de rulare Timpul de rulare al algoritmului lui Dijkstra într-un graf cu |E| muchii şi |V| noduri poate fi exprimat ca o funcţie de E şi V , folosind notaţia O. Cea mai simplă implementare a algoritmului lui Dijkstra stochează vârfurile mulţimii Q într-o listă de legătură ordinară sau într-un tablou, iar operaţia Extract-Min(Q) este o simplă căutare liniară a vârfurilor mulţimii Q. 2

În acest caz, timpul de rulare este O( V + E ) . Pentru cazul grafurilor rare, adică, grafuri cu un număr de muchii 2 mult mai mic decât V , algoritmul Dijkstra se poate implementa într-un mod mult mai eficient, prin stocarea grafului sub forma listelor de adiacenţă şi folosirea heap binar sau heap Fibonaci pe post de coadă cu priorităţi în implementarea funcţiei Extract-Min. Cu heap binar algoritmul necesită un timp de rulare de ordinul O(( E + V ) log V ) (dominat de către O( E log V ) presupunând că E ≥ V − 1 ), iar heap Fibonaci îmbunătăţeşte acest timp la O( E + V log V ) .

5.1.3. Probleme similare şi algoritmi Funcţionalitatea algoritmului original al lui Dijkstra poate fi extinsă dacă se efectuează anumite schimbări. De exemplu, în unele cazuri este de dorit a se prezenta unele soluţii ce nu sunt chiar optimale din punct de vedere matematic . Pentru a obţine o listă consistentă de astfel de soluţii mai puţin optimale, se calculează, totuşi, încă de la început, soluţia optimă. Se elimină, din graf, o singură muchie ce apare în soluţia optimă, iar soluţia optimă a acestui nou graf este calculată. La întoarcere, fiecare muchie a soluţiei originale este suprasaturată, iar drept urmare se calculează un nou cel mai scurt drum. Soluţiile secundare astfel obţinute sunt înşiruite imediat după prima soluţie optimă. OSPF (open shortest path first) reprezintă o implementare reală a algoritmului lui Dijkstra, în rout-area internet-ului. Spre deosebire de algoritmul lui Dijkstra, algoritmul Bellman-Ford poate fi folosit şi în cazul grafurilor ce au muchii cu costuri negative, atât timp cât graful nu conţine nici un ciclu negativ care se poate atinge din vârful sursă s. (Prezenţa unor astfel de cicluri sugerează faptul că nu există ceea ce 49

numim cel mai scurt drum, având în vedere că valoarea descreşte de fiecare dată când ciclul este traversat.) Algoritmul A* este o generalizare a algoritmului Dijkstra, care reduce mărimea subgrafului care urmează să fie explorat, aceasta în cazul în care sunt disponibile informaţii adiţionale, menite să micşoreze „distanţa” către ţintă. Procesul care stă la baza algoritmului lui Dijkstra este similar procesului greedy, folosit în cazul algoritmului lui Prim. Scopul algoritmului lui Prim îl constituie găsirea arborelui parţial de cost minim corespunzător unui graf.

5.1.4. Probleme legate de drum ■ Drumul Hamiltonian şi probleme legate de cicluri ■ Arborele parţial de cost minim ■ Problema inspecţiei drumului (cunoscută şi sub numele de „Problema Poştaşului Chinez”) ■ Cele Şapte Poduri din Königsberg ■ Problema celui mai scurt drum ■ Arborele Steiner ■ Problema Comisului Voiajor (NP - completă)

5.1.5. Algoritmul Bellman-Ford Algoritmul Bellman –Ford calculează cele mai scurte drumuri de la un vârf-sursă către celelalte vârfuri ale unui digraf ponderat (unde unele muchii pot avea costuri negative). Algoritmul lui Dijkstra rezolvă aceeaşi problemă, chiar cu un timp de execuţie mai mic, însă necesită muchii ale căror costuri să fie nenegative. Astfel, algoritmul Bellman – Ford se foloseşte doar atunci când există costuri negative ale muchiilor. Potrivit lui Robert Sedgewick, „Valorile negative intervin în mod natural în momentul în care se reduc alte probleme la probleme de studiu a drumului cel mai scurt”, şi oferă ca exemplu specific problema reducerii complexităţii -NP a drumului Hamiltonian. Dacă un graf conţine un ciclu având valoare negativă, atunci nu există soluţie; Bellman – Ford rezolvă acest caz. Algoritmul Bellman – Ford, în structura sa de bază, este similar algoritmului Dijkstra, dar în locul unei selecţii de tip greedy a nodului minim poderat, apelează la simpla relaxare a muchiilor, acest proces executându-se de V − 1 ori, unde V reprezintă numărul vârfurilor dintr-un graf. Aceste repetări permit propagarea distanţelor minime în graf, ţinând cont de faptul că, în absenţa ciclurilor negative, cel mai scurt drum poate vizita fiecare nod cel mult o dată. Spre deosebire de abordarea greedy, care depinde de anumite

50

consideraţii structurale derivate din costurile pozitive, această abordare directă se extinde la cazul general. Timpul de rulare al algoritmului Bellman – Ford este de ordinul O(|E|). procedure BellmanFord(list vertices, list edges, vertex source) // Pasul 1: Iniţializarea grafului for each vertex v in vertices: if v is source then v.distance := 0 else v.distance := infinity v.predecessor := null // Pasul 2: Relaxarea repetitivă a muchiilor for i from 1 to size(vertices): for each edge uv in edges: u := uv.source v := uv.destination //uv este muchia de la u la v if v.distance > u.distance + uv.weight: v.distance := u.distance + uv.weight v.predecessor := u // Depistarea ciclurilor negative for each edge uv in edges: u := uv.source v := uv.destination if v.distance > u.distance + uv.weight: error "Graful cinţine un ciclu negativ"

Demonstraţia corectitudinii Corectitudinea algoritmului poate fi arătată cu ajutorul inducţiei. Propoziţia care va fi demonstrată prin inducţie este dată de următoarea: Lemă. După i repetiţii ale buclei for: • Dacă Distance(u) nu este infinită, atunci este egală cu lungimea unui anumit drum de la s la u; • Dacă există un drum de la s la u cu cel mult i muchii, atunci Distance(u) corespunde cel mult lungimii celui mai scurt drum de la s la u cu cel mult i muchii. Demonstraţie. Pentru etapa I, considerăm i = 0 şi momentul apriori ciclului for considerându-l ca fiind executat pentru prima dată. Apoi, pentru vârful sursă, source.distance=0, ceea ce este corect. Pentru alte vârfuri u, u.distance=infinity, ceea este deopotrivă corect deoarece nu există nici un drum de la sursă la u cu 0 muchii. Pentru pasul inductiv, demonstrăm pentru început prima parte. Considerând un moment în care distanţa la un vârf este dată de: v.distance:=u.distance+uv.weight. Prin presupunere inductivă, u.distance este lungimea unui drum oarecare de la sursă la u. Astfel,

51

u.distance+uv.weight este lungimea drumului de la sursă la v, care nu părăseşte drumul de la sursă la u şi ajunge la v. Pentru cea de-a doua parte, considerăm cel mai scurt drum de la sursă la u cu cel mult i muchii. Fie v ultimul vârf înaintea lui u pe acest drum. Atunci, porţiunea de drum de la sursă la v este cel mai scurt drum de la sursă la v cu cel mult i-1 muchii. Prin presupunere inductivă, v.distance, acestui drum. De aceea, după i-1 cicluri, are cel mult lungimea uv.weight+v.distance are cel mult lungimea drumului de la s la u. La ciclul cu numărul i, u.distance este comparat cu uv.weight+v.distance, şi se egalează cu această cantitate dacă uv.weight+v.distance este mai mică. De aceea, după i cicluri, u.distance are cel mult lungimea celui mai scurt drum de la sursă la u, drum ce foloseşte cel mult i muchii. Când i egalează numărul vârfurilor grafului, fiecare drum va fi cel mai scurt între toate vârfurile, doar dacă nu există cicluri negative. Dacă există totuşi un ciclu ponderat negativ şi accesibil de la sursă, atunci dat fiind un drum oarecare, există unul mai scurt, deci nu există un cel mai scurt drum. Altfel, cel mai scurt drum nu va include nici un ciclu (deoarece ocolirea ciclului ar presupune scurtarea drumului), pentru ca fiecare drum mai scurt să viziteze fiecare nod cel mult o dată, iar numărul de muchii corespunzător să fie mai mic decât numărul vârfurilor grafului. Aplicaţii în rutare O variantă distribuită a algoritmului Bellman – Ford se foloseşte în protocoalele de rutare distanţă-vector, de exemplu Protocolul de Rutare a Informaţiei (RIP)(Routing Information Protocol). Algoritmul constă din următorii paşi: 1. Fiecare nod calculează distanţa între “sine” şi toate celelate noduri şi stochează această informaţie ca un tabel. 2. Fiecare nod îşi trimite tabelul corespunzător tuturor celorlalte noduri. 3. În momentul în care un nod primeşte un astfel de tabel de la vecinii săi, calculează cele mai scurte căi către toate celelalte noduri şi actualizează propriul tabel astfel încât să fie reflectate toate schimbările survenite. Marele dezavantaj al algoritmului Bellman-Ford în aceste condiţii constă în: • Măsurarea incorectă • Schimbările în topologia reţelei nu sunt reflectate în timp util, odată cu actualizarea succesivă a tabelelor nodurilor. • Numărarea la infinit (proces ce survine ca urmare a eşecului transmiterii tabelelor) Implementare Următorul program implementează algoritmul Bellman-Ford în C.

52

#include #include #include /* Să considerăm INFINIT-ul o valoare întreagă, pentru a nu interveni confuzia în valorarea reală, chiar şi cea negativă*/ #define INFINITY ((1 distance[edges[i].source] + edges[i].weight) { puts("S-au detectat cicluri cu muchii ponderate negativ (cu costuri negative)!"); free(distance); return; } } for (i=0; i < nodecount; i++) { printf("Cea mai scurtă distanţă dintre nodurile %d şi %d este %d\n", source, i, distance[i]); } free(distance); return; } int main(void) { /* Acest test ar trebui să genereze distanţele 2, 4, 7, -2, and 0. */ Edge edges[10] = {{0,1, 5}, {0,2, 8}, {0,3, -4}, {1,0, -2}, {2,1, -3}, {2,3, 9}, {3,1, 7}, {3,4, 2}, {4,0, 6}, {4,2, 7}}; BellmanFord(edges, 10, 5, 4); return 0; }

53

5.1.6. Algoritmul de căutare A∗ În ştiinţa calculatoarelor, A∗ este un algoritm de căutare a grafurilor de tipul “best-first”, care găseşte drumul de cost de minim de la un nod iniţial la un nod “ţintă” (din una sau mai multe ţinte posibile). Foloseşte o funcţie euristică distanţă-plus-cost (notată de regulă cu f ( x ) ) pentru a determina ordinea în care sunt vizitate nodurile arborelui. Euristic-ul distanţă-plus-cost reprezintă o sumă de două funcţii: funcţia cost-drum (notată de obicei cu g ( x ) , care poate fi, sau, nu euristică) şi o “estimare euristică” admisibilă a distanţei către ţintă (notată de regulă cu h( x ) ). Funcţia cost-drum g ( x ) determină costul de la nodul de start la nodul curent. Având în vedere faptul că h( x ) , parte a funcţiei f ( x ) , trebuie să fie euristic admisibilă, trebuie să se “subestimeze” distanţa către ţintă. Astfel, pentru o aplicaţie ca rout-area, h( x ) ar putea reprezenta distanţa în linie dreaptă la ţintă, ţinând cont şi de faptul că, din punct de vedere fizic, este cea mai mică distanţa posibilă între oricare două noduri. Algoritmul a fost descris pentru prima dată în anul 1968 de către Peter Hart, Nils Nilsson, respectiv Bertram Raphael. Algoritmul era numit algoritmul A. Având în vedere faptul că se face apel doar la comportamentul optimal pentru un anumit euristic, a fost numit A∗ . Descrierea algoritmului A∗ caută toate drumurile de la nodul de start, oprindu-se în momentul în care s-a găsit drumul cel mai scurt la nodul ţintă. Ca toţi algoritmii de căutare informaţionali, cercetează mai întâi drumurile ce par a conduce la ţintă. Ceea ce prezintă A∗ în plus faţă de căutarea greedy de tip best-first este reprezentat de faptul că ia în considerare distanţa deja parcursă Începând cu un anumit nod (iniţial), algoritmul extinde nodul cu cea mai mică valoare a lui f ( x ) - nodul care are cel mai mic cost-per-beneficiu. A∗ menţine o mulţime de soluţii parţiale - noduri frunză neextinse -, stocată într-o coadă cu priorităţi. Prioritatea asociată unui drum x este determinată de funcţia f ( x ) = g ( x ) + h( x ) . Funcţia “continuă” până când o ţintă are o valoare corespunzătoare f ( x ) mai mică decât a oricărui nod din coadă (sau până când arborele va fi fost parcurs în totalitate). Multe alte ţinte pot fi trecute cu vederea dacă există un drum care putea conduce la o „ţintă” având „costul” mai mic. Cu cât f ( x ) are o valoare mai mică, cu atât prioritatea este mai mare (astfel, s-ar putea folosi o min-heap pentru a implementa coada)

54

function A*(start,goal) var closed := the empty set var q := make_queue(path(start)) while q is not empty var p := remove_first(q) var x := the last node of p if x in closed continue if x = goal return p add x to closed for each y in successors(x) enqueue(q, p, y) return failure

Mulţimea închisă poate fi omisă (transformând algoritmul de căutare într-unul mai maleabil) dacă, fie existenţa soluţiei este garantată, fie membrul successors este adaptat ciclurilor (respinse). Proprietăţi Asemenea căutării „bredth-first”, A∗ este completă, în sensul că va găsi întotdeauna o soluţie, în cazul în care aceasta există. Dacă funcţia euristică h este admisibilă, adică nu supraestimează costul minim actual de „atingere a scopului”, atunci A∗ însuşi este admisibil (sau optimal) dacă nu se foloseşte o mulţime închisă. Dacă se foloseşte o astfel de mulţime închisă, h ar trebui să fie de asemenea monotonă (sau consistentă) pentru A∗ astfel încât să fie optimală. A fi admisibil înseamnă că funcţia euristică nu supraestimează ,niciodată , costul trecerii de la un nod la vecinii săi, în timp ce a fi monoton înseamnă că dacă există o conexiune de la nodul A la nodul C, respectiv o legătură de la nodul A la nodurile B şi C, costul estimat de la A la C va fi, întotdeauna, mai mic sau egal cu cel estimat de la A la B + costul estimat de la B la C. (Monotonia este cunoscută şi sub numele de inegalitate triunghiulară). Formal, pentru toate drumurile (x, y), unde y este un succesor al lui x: g ( x ) + h( x ) ≤ g ( y ) + h ( y ) .

A∗ este deopotrivă eficient pentru orice euristic h, aceasta însemnând că nici un alt algoritm ce foloseşte acelaşi euristic nu va extinde mai puţine noduri decât A∗ , exceptând doar cazul în care există câteva soluţii parţiale pentru care h prezice cu exactitate costul drumului optimal. Optimalitatea în grafurile arbitrare nu garantează performanţe mai mari ca algoritmii simpli de căutare, care deţin mai multe informaţii legate de acest domeniu. Spre exemplu, într-un mediu de tip „labirint”, singura posibilitate prin care se poate atinge scopul ar putea necesita o primă parcurgere (ce evită „ţinta”), întorcându-se ulterior la „ţintă”. Astfel, în acest

55

caz, probarea prioritară a nodurilor din imediata apropiere a „destinaţiei” ar putea implica un cost ridicat în ceea ce priveşte timpul implicat. Cazuri speciale În general vorbind, depth-first search şi bredth-first search reprezintă două cazuri speciale (particulare) ale algoritmului A∗ . Algoritmul lui Dijkstra, un alt exemplu de algoritm de tip best-first search (căutare prioritară), reprezintă un caz special al A∗ , unde h( x ) = 0 ∀x . Pentru depth-first search (parcurgerea în adâncime), putem considera că există un „contabilizator” C, iniţializat cu o valoare foarte mare. De fiecare dată când se procesează un nod îi ataşăm C corespunzător tuturor vecinilor săi astfel descoperiţi. După fiecare astfel de assign-are, micşorăm „contabilizatorul” C cu o unitate. Astfel, cu cât un nod este „descoperit” mai repede, cu atât valoarea h(x) corespunzătoare este mai mare. De ce A∗ este „admisibil” şi optimal din punct de vedere computaţional

A∗ este atât admisibil, iar, pe de altă parte, implică şi mai puţine noduri decât orice alt algoritm de căutare având acelaşi euristic, aceasta deoarece A∗ porneşte de la cost aproximativ „optim” al drumului ce parcurge toate nodurile, către „ţintă” („optim” însemnând că acel cost final va fi cel puţin la fel de mare cu cel estimat). Când A∗ finalizează căutarea, a găsit, prin definiţie, un drum al cărui cost actual este mai mic decât costul estimat al oricărui alt drum ce parcurge nodurile. Având în vedere, însă, faptul că aceste estimări sunt optimiste, A∗ poate ignora toate aceste noduri „deschise”. Cu alte cuvinte, A∗ nu va omite niciodată posibilitatea existenţei unui drum având un cost mai mic, fiind astfel admisibil. Să presupunem acum că un algoritm oarecare de căutare A finalizează căutarea găsind un drum al cărui cost nu este mai mic decât cel estimat. Algoritmul A nu poate exclude posibilitatea existenţei unui drum al cărui cost prin acel nod să fie mai scăzut, bazându-se pe informaţia euristică pe care o deţine. Astfel, atât timp cât A poate considera mai puţine noduri decât A* , nu poate fi admisibil. Deci, A∗ reprezintă algoritmul de căutare cu cele mai puţine noduri ce poate fi considerat ca fiind admisibil. Complexitate Complexitatea în timp a lui A∗ depinde de euristic. Potrivit celui mai sumbru scenariu, numărul nodurilor „extinse” este de ordin exponenţial, în ceea ce priveşte lungimea soluţiei (cel mai scurt drum), însă este de ordin polinomial atunci când funcţia euristică h satisface următoarea condiţie:

56

| h(x) − h∗ (x) |≤ O(log h∗ (x)) unde h∗ reprezintă euristicul optimal, i.e. costul exact ce-l implică drumul de la x la „ţintă”. Cu alte cuvinte, eroarea corespunzătoare lui h nu ar trebui să crească mai rapid decât logaritmul „euristicului perfect” h∗ , ce returnează distanţa reală de la x la „ţintă”. O chestiune şi mai problematică a A∗ decât cea legată de complexitatea în timp, o constituie uzul de memorie. În cel mai rău caz, ar trebui să memoreze un număr exponenţial de noduri. S-au elaborat mai multe variante ale algoritmului A∗ astfel încât să poată face faţă acestei probleme, printre care amintim: „adâncirea” iterativă A∗ (ID A∗ ), ∗

memoria–graniţă



(la limită) A (M A ), respectiv varianta simplificată a memoriei –graniţă (la limită) A∗ (SM A∗ ) şi best-first search varianta recursivă (RBFS).

5.1.7. Algoritmul Floyd-Warshall În ştiinţa calculatoarelor, algoritmul Floyd-Warshall (întâlnit uneori şi sub denumirea de algoritmul Roy-Floyd sau algoritmul WFI, încă din anul în care acest algoritm a fost descris de către Bernard Roy (1959)) reprezintă un algoritm de analiză a grafului, în vederea găsirii celor mai scurte drumuri într-un graf ponderat orientat. O singură execuţie a algoritmului va determina cel mai scurt drum între toate perechile de vârfuri. Algoritmul Floyd-Warshall reprezintă un exemplu de programare dinamică. Algoritm Algoritmul Floyd-Warshall compară toate drumurile posibile ale grafului între fiecare pereche de vârfuri. Poate realiza acest lucru prin intermediul a doar V

3

comparaţii (acest lucru este remarcabil, ţinând cont

de faptul că ar putea exista V

2

muchii în graf, fiecare combinaţie de astfel

de muchii fiind testată). Acest lucru este posibil prin îmbunătăţirea incrementală a estimării celui mai scurt drum între două vârfuri, până când estimarea este considerată a fi optimă. Considerăm un graf G, cu nodurile corespunzătoare V, fiecare dintre acestea fiind numerotat de la 1 la n. Mai mult, fie funcţia shortestPath(i,j,k) ce returnează cel mai scurt drum posibil de la i la j, folosind doar vârfurile de la 1 la k, pe post de puncte intermediare de-a lungul drumului. Acum, fiind dată această funcţie, scopul nostru îl constituie găsirea celui mai scurt drum de la fiecare i la fiecare j, folosind doar nodurile numerotate de la 1 la k+1.

57

Există două candidate la statutul de cel mai scurt drum, şi anume: fie adevăratul cel mai scurt drum, ce foloseşte doar noduri ale mulţimii (1…k), fie există un anume drum ce uneşte i de k+1, pe acest k+1 de j, ce este mai bun. Ştim că cel mai bun drum de la i la j, care foloseşte doar nodurile mulţimii (1…k) este definit de shortestPath(i,j,k), şi este evident faptul că dacă ar exista un drum mai bun de la i la k+1, respectiv la j, atunci lungimea acestui drum ar reprezenta concatenarea celui mai scurt drum de la i la k+1 (folosind vârfuri ale mulţimii (1…k)), respectiv a celui mai scurt drum de la k+1 la j (folosindu-se deopotrivă vârfurile mulţimii (1…k)). Astfel, putem defini shortestPath(i,j,k) în termenii următoarei formule recursive: shortestPath(i, j, k) = min(shortestPath(i, j, k − 1) + shortestPath(i, k, k − 1) + +shortestPath(k, j, k − 1)); shortestPath(i, j, 0) = edgeCost(i, j); Această formulă constituie „inima” lui Floyd Warshall. Algoritmul funcţionează calculând mai întâi shortestPath(i,j,1) pentru toate perechile de tipul (i,j), folosind acest rezultat, ulterior, pentru a calcula shortestPath(i,j,2) pentru toate perechile de tipul (i,j), etc. Acest proces continuă până când k = n, iar drumul cel mai scurt corespunzător tuturor perechilor (i,j), folosind nodurile intermediare, va fi fost găsit. Pseudocodul În mod convenabil, când se calculează cazul de ordinul k, se poate rescrie informaţia salvată la calculul corespunzător etapei k-1. Acesta înseamnă că algoritmul foloseşte memorie pătratică. (A se lua în considerare condiţiile de iniţializare!): 1

/* Fie o funcţie edgeCost(i,j) ce returnează costul muchiei ce uneşte vârfurile i şi j 2 (infinit dacă nu există). 3 Presupunem de asemenea că n reprezintă numărul nodurilor iar edgeCost(i,i)=0 4 */ 5 6 int path[][]; 7 /* O matrice 2-Dimensională. La fiecare pas (etapă) a algoritmului, path[i][j] constituie cel mai scurt drum 8 de la i la j folosind valorile intermediare ale mulţimii(1..k-1). Fiecare drum [i][j] este iniţializat la 9 edgeCost(i,j). 10 */ 11 12 procedure FloydWarshall () 13 for k: = 1 to n 14 begin 15 for each (i,j) in (1..n) 16 begin 17 path[i][j] = min ( path[i][j], path[i][k]+path[k][j] );

58

18 end 19 end 20 endproc

Comportamentul în cazul ciclurilor negative Pentru un rezultat numeric semnificativ, Floyd-Warshall presupun că nu există cicluri negative (de fapt, între oricare două perechi de vârfuri care reprezintă parte constituentă a unui ciclu negativ, drumul cel mai scurt nu poate fi definit în mod corect deoarece drumul poate fi infinit de mic). Totuşi, dacă există cicluri negative, Floyd-Warshall poate fi folosit pentru identificarea acestora. Dacă se rulează algoritmul încă odată, unele drumuri pot să scadă, însă nu se garantează că, între toate vârfurile, drumul corespunzător va fi afectat de aceeaşi manieră. Dacă numărul de pe diagonală matricei drumului este negativ, este necesar şi suficient ca acest vârf să aparţină unui ciclu negativ. Analiza Găsirea tuturor n 2 ai Wk din cei ai Wk −1 necesită 2n 2 operaţii. Ţinând cont de faptul că am considerat, iniţial, W0 = WR , respectiv am calculat secvenţele matricelor cu elemente 0 şi 1 de ordin n W1 , W2 ,K , Wn = M ∗ , R

numărul total de operaţii efectuate este n × 2 n 2 = 2n 3 . Deci, complexitatea algoritmului este de ordinul O(n 3 ) şi poate fi rezolvat cu ajutorul unei „maşini” deterministe într-un timp de ordin polinomial. Aplicaţii şi generalizări Algoritmul Floyd-Warshall poate fi folosit, printre altele, la rezolvarea următoarelor probleme: • Cele mai scurte drumuri în grafuri orientate (algoritmul Floyd) • „Închiderea” tranzitivă a grafurilor orientate (algoritmul Warshall). În formularea originală a algoritmului a lui Warshall, graful nu este ponderat şi este reprezentat cu ajutorul unei matrice de adiacenţă booleană. Mai mult, operaţia de adunare este înlocuită de conjuncţia logică (AND) iar operaţia de scădere de disjuncţia logică (OR). • Găsirea unei expresii regulare, indicând limbajul regulat, acceptat de către un automat finit (algoritmul lui Kleene) • Inversarea matricelor reale (algoritmul Gauss-Jordan) • Rout-area optimală. În cazul acestei aplicaţii preocuparea principală o constituie găsirea drumului caracterizat de flux

59

maxim între două vârfuri. Aceasta reprezintă că, în loc să considerăm minimul ca în cazul pseudocodului de mai sus, vom fi interesaţi de maxim. Costurile muchiilor constituie restricţii în ceea ce priveşte fluxul. Costurile drumului reprezintă „blocaje”. Astfel, operaţia de sumare de mai sus este înlocuită cu operaţia corespunzătoare minimului. • Testarea bipartiţiei unui graf neorientat.

5.1.8. Algoritmul lui Johnson Algoritmul lui Johnson reprezintă o modalitate de găsire a celor mai scurte drumuri între toate perechile de vârfuri ale unui graf rar orientat. El permite ca, costurile unor muchii să fie numere negative, însă nu permite existenţa ciclurilor ponderate negativ. Descrierea algoritmului Algoritmul lui Johnson constă în următorii paşi: 1. Pentru început, se adaugă un nod nou q mulţimii iniţiale a nodurilor, legat, prin muchii de ponderi nule, de toate celelalte noduri. 2. În cea de-a doua etapă, se foloseşte algoritmul Bellman-Ford, începând cu vârful nou introdus q, în vederea găsirii, pentru fiecare vârf în parte, cel mai puţin costisitor h(v) a unui drum de la q la v. Dacă în această etapă se găseşte un ciclu negativ, algoritmul se opreşte. 3. În continuare, muchiile grafului iniţial sunt re-ponderate, folosind valorile calculate de algoritmul Bellman-Ford: unei muchii ce leagă u şi v, având lungimea w(u, v), îi este ataşată noua lungime w (u, v)+h(u)-h (v). 4. În final, pentru fiecare nod s, se face apel la algoritmul lui Dijkstra cu scopul de a găsi cele mai scurte drumuri de la s la toate celelalte noduri ale grafului re-ponderat. În graful re-ponderat, toate drumurile între o pereche de noduri s respectiv t au o aceeaşi cantitate adăugată h(s)-h(t), astfel că un drum cel mai scurt în graful iniţial rămâne cel mai scurt în graful modificat şi vice versa. Totuşi, datorită modului de calcul al valorilor h(v), toate lungimile muchiilor modificate sunt nenegative, asigurând optimalitatea drumurilor găsite prin intermediul algoritmului lui Dijkstra. Distanţele în graful iniţial pot fi calculate cu ajutorul distanţelor calculate cu algoritmul lui Dijkstra, în graful re-ponderat, inversând transformarea de re-valorare. Analiza Complexitatea în timp a algoritmului, folosind heap Fibonacci în implementarea algoritmului lui Dijkstra, este de ordinul

60

O(| V |2 log | V | + | V || E |) : algoritmul foloseşte un timp de ordinul O(| V || E |) pentru etapa Bellman-Ford a algoritmului, respectiv de ordinul O (| V | log | V | + | E |) pentru fiecare din cele |V| apelări ale algoritmului lui Dijkstra. Astfel, când graful este rar, timpul total poate fi mai rapid decât cel corespunzător algoritmului Floyd-Warshall, care rezolvă aceeaşi problemă într-un timp de ordinul O(| V |3 ) .

5.2.

Probleme de conexiune. Teorema lui Menger şi aplicaţii

Definiţie. Fie G = (V,E) (di)graf şi X, Y ⊆ V . Numim XY – drum în G orice drum D în G de la un vârf x ∈ X la un vârf y ∈ Y , astfel încât V (D) ∩ X = {x} şi V (D) ∩ Y = {y}. Vom nota cu D(X,Y;G) mulţimea tuturor XY - drumurilor în G. Să observăm că dacă x ∈ X ∩ Y atunci drumul de lungime 0, D = {x} este XY drum. Vom spune că drumurile D1 şi D2 sunt disjuncte dacă V (D1) ∩ V (D2) = 0/ . Probleme practice din reţelele de comunicaţie, dar şi unele probleme legate de conexiunea grafurilor şi digrafurilor, necesită determinarea unor mulţimi de XY - drumuri disjuncte şi cu număr maxim de elemente. Vom nota cu p(X,Y;G) numărul maxim de XY – drumuri disjuncte în (di)graful G, număr ce a fost stabilit de Menger. Definiţie. Fie G = (V,E) un digraf şi X, Y ⊆ V . Numim mulţime XYseparatoare în G o mulţime Z ⊆ V astfel încât ∀D ∈ D(X, Y;G) ⇒

V (D) ∩ Z ≠ ∅. Notăm cu S(X,Y;G) = {Z | Z XY - separatoare în G}, k(X,Y;G) = min {|Z|;Z∈ S(X, Y ;G)}. Din definiţie, rezultă următoarele proprietăţi imediate ale mulţimilor XY - separatoare: (a) Dacă Z ∈ S(X,Y;G) atunci ∀ D ∈ D(X,Y;G), D nu este drum în G − Z. (b) X, Y ∈ S(X,Y;G). (c) Dacă Z ∈ S(X,Y;G) atunci ∀ A astfel încât Z ⊆ A ⊆ V avem A ∈ S(X,Y;G). (d) Dacă Z ∈ S(X,Y;G) şi T ∈ S(X,Z;G) sau T ∈ S(Z,Y;G) atunci T ∈ D(X,Y;G). Dăm fără demonstraţie următorul rezultat. Teoremă. Fie G = (V,E) (di)graf şi X, Y ⊆ V . Atunci p(X,Y;G) = k(X,Y;G). 61

Remarcăm: 1) Egalitatea min-max din enunţul teoremei este interesantă şi conduce la rezultate importante, în cazuri particulare. 2) Teorema se poate demonstra şi algoritmic ca o consecinţă a teoremei fluxului maxim - secţiunii minime. Forma echivalentă (a teoremei de mai sus) care a fost enunţată şi demonstrată iniţial de Menger este: Teoremă. Fie G = (V,E) un (di)graf şi s, t ∈ V, astfel încât s ≠ t, st ∉ E. Există k drumuri intern disjuncte de la s la t în graful G dacă şi numai dacă îndepărtând mai puţin de k vârfuri diferite de s şi t, în graful rămas există un drum de la s la t. Notăm că două drumuri sunt intern disjuncte dacă nu au vârfuri comune cu excepţia extremităţilor. Am definit un graf G p-conex ( p ∈ N* ) dacă G = Kp sau dacă |G| > p şi G nu poate fi deconectat prin îndepărtarea a mai puţin de p vârfuri. Avem şi rezultatul. Corolar. Un graf G este p-conex dacă G = Kp sau ∀st ∈ E(G) există p drumuri intern disjuncte de la s la t în G. Determinarea numărului k(G) de conexiune a grafului G (cea mai mare valoare a lui p pentru care G este p-conex) se reduce deci la determinarea lui max p({s},{t};G) st∈E(G)

problemă care se poate rezolva în timp polinomial. Un caz particular interesant al teoremei 1, se obţine atunci când G este un graf bipartit iar X şi Y sunt cele două clase ale bipartiţiei: Teoremă. (Konig) Dacă G = (S,R;E) este un graf bipartit, atunci cardinalul maxim al unui cuplaj (o mulţime independentă de muchii) este egal cu cardinalul minim al unei mulţimi de vârfuri incidente cu toate muchiile grafului.

5.3.

Structura grafurilor p-conexe

Lemă. Fie G = (V,E) p-conex, |V|≥p+1,U ⊆ V, |U| = p şi x ∈ V-U. Există în G p U – drumuri cu singurul vârf comun x. Lemă. Dacă G = (V,E) este un graf p - conex, p ≥ 2, atunci oricare ar fi două muchii e1 şi e2 şi p - 2 vârfuri x1, x2, . . . , xp−2 există un circuit în G care le conţine. 62

Teoremă. (Dirac) Dacă G = (V,E) este un graf p-conex, p≥2, atunci prin orice p vârfuri ale sale trece un circuit. Pe baza acestei teoreme, se poate demonstra o condiţie suficientă de hamiltonietate. Teoremă. Fie G p-conex. Dacă α(G)≤p atunci G este hamiltonian.

5.4.

Problema drumului Hamiltonian

În teoria grafurilor Problema drumului Hamiltonian, respectiv cea a Ciclului Hamiltonian reprezintă probleme de determinare a existenţei unui drum Hamiltonian, respectiv a unui ciclu Hamiltonian într-un graf dat (orientat sau nu). Ambele probleme sunt NP- complete. Există o relaţie simplă între cele două probleme. Problema Drumului Hamiltonian pentru un graf G este echivalentă cu problema Ciclului Hamiltonian într-un graf H obţinut din G prin adăugarea unui nou nod, ce va fi conectat cu toate nodurile grafului iniţial G. Problema Ciclului Hamiltonian este un caz special al problemei Comis Voiajorului, obţinută prin setarea distanţei între două oraşe la o anumită valoare finită, dacă acestea sunt adiacente, respectiv infinite dacă cele două oraşe nu sunt adiacente. Problemele Ciclului Hamiltonian orientat sau neorientat reprezintă două din cele 21 de probleme NP – complete ale lui Karp. Garey şi Johnson au arătat la scurt timp după aceasta, în anul 1974, că problema Ciclului Hamiltonian orientat rămâne NP – completă pentru grafurile planare, iar problema ciclului Hamiltonian neorientat rămâne NP – completă pentru grafurile planare cubice. Algoritmul aleatoriu Un algoritm aleatoriu pentru un Ciclu Hamiltonian, care este destul de rapid pentru ambele tipuri de grafuri, este următorul: Se începe într-un nod oarecare, şi se continuă dacă există un vecin nevizitat. Dacă nu mai există vecini nevizitaţi, iar drumul rezultat nu este Hamiltonian, se alege un vecin la întâmplare, urmând o rotaţie folosindu-se pe post de pivot vecinul în cauză. Are loc următorul rezultat: Teorema 1. Fie G un graf cu cel puţin trei vârfuri. Dacă, pentru un s, G este sconex şi conţine o mulţime neindependentă cu mai mult de s vârfuri, atunci G are un circuit Hamiltonian. Această teoremă ne arată că graful complet bipartit K(s,s+1) este sconex, conţine mulţimi neindependente cu mai mult de s+1 vârfuri şi nu are

63

circuit Hamiltonian. Similar, graful Petersen este 3-conex, conţine mulţimi neindependente cu mai mult de patru vârfuri şi nu are circuit Hamiltonian. Demonstraţie. Fie G ce satisface ipoteza Teoremei 1. Evident, G conţine un circuit; fie C cel mai lung circuit. Dacă G nu are circuit Hamiltonian, atunci există un vârf x, x ∉ C. Deoarece G este s-conex, există s drumuri începând din x şi terminând în C, care sunt perechi disjunctive despărţite din x şi partajează cu C chiar în vârfurile lor terminale x1,x2,…,xs. Pentru ∀ i=1,2,…,s , fie yi succesorul lui xi într-un ciclu ordonat fix al lui C. Nici un yi nu este adiacent cu x – altfel am putea înlocui muchiile xiyi în C prin drumul de la xi la yi în afara lui C (către x) şi am obţine un circuit mai lung. Cu toate acestea, G nu conţine mulţimi independente cu s+1 vârfuri şi deci există o muchie yiyj. Şterge muchiile xiyi, xjyj din C şi adăugă muchia yiyj împreună cu drumul de la xi la xj în afara lui C. În acest sens obţinem un circuit mai lung decât C, ceea ce este o contradicţie. Fie G un graf cu n vârfuri , n ≥ 3 . G nu conţine vârfuri cu grad mai 1 mic decât k unde k este un întreg astfel încât k ≥ ( n + 2 ) . Atunci G ori are 3 un circuit Hamiltonian, ori este separabil, ori are k+1 vârfuri independente. Ca o consecinţă simplă a teoremei 1 obţinem: Teorema 2. Fie G un graf s-conex fără mulţimi independente de s+2 vârfuri. Atunci G are un circuit Hamiltonian. Demonstraţie. Într-adevăr, dacă G satisface ipoteza Teoremei 2, atunci G+x (graful obţinut din G prin adăugarea lui x şi reunindu-l cu toate vârfurile lui G) satisface ipoteza Teoremei 1 cu s+1 în loc de s. Aşadar G+x are un circuit Hamiltonian şi G are un drum Hamiltonian. Graful bipartite complet K(s,s+2) arată că Teorema 2 este evidentă. Tehnica utilizată în demonstrarea Teoremei 1 ne dă de asemenea Teorema 3. Fie G un graf s-conex ce nu conţine s vârfuri independente. Atunci G este Hamiltonian – conex (i.e. fiecare pereche de vârfuri este unită printr-un drum Hamiltonian).

5.5.

Problema Ciclului Hamiltonian

Punerea problemei Problema Ciclului Hamiltonian (PCH) diferă de Problema ComisVoiajorului (PCV) prin faptul că graful nu este neapărat complet şi în plus nu se cere ca ciclul să aibă costul minim.

64

Fie G = (V,U) un graf conex neorientat. Fiecărei muchii i se ataşează un cost strict pozitiv. Ca urmare, graful va fi reprezentat prin matricea costurilor C, având drept componente: ⎧ ≠ 0, dacă muchia (i, j) există; ci, j = ⎨ ⎩0, dacă nu există muchia (i, j); Costul unui ciclu este definit ca sumă a costurilor ataşate muchiilor componente. Definiţie. Se numeşte ciclu hamiltonian un ciclu care trece exact o singura dată prin fiecare vârf. Pentru determinarea ciclurilor Hamiltoniene vom folosi metoda backtracking. Astfel, dacă N = card(V), atunci o soluţie oarecare a problemei se poate scrie sub forma unui vector X = (x1,x2,...,xN+1) . Condiţiile de continuitate ce trebuie satisfăcute în construcţia soluţiei sunt: - x1 = xN+1; - xi ≠ xj, ∀ (i,j) cu i ≠ j; - (xi, xi+1)∈U, ∀ i∈ {1,...,N}. Pentru a nu obţine de mai multe ori acelaşi ciclu, se poate fixa x1=1. Fie alese x1,..., xk-1 cu k∈{2,...,N}. Atunci, condiţiile de continuitate, care stabilesc dacă o valoare a lui xk poate conduce la o soluţie posibilă, sunt următoarele: - ∃ muchie între vârfurile xk-1 şi xk, cu xk ∉{x1,..., xk-1} - xN trebuie să îndeplinească şi condiţia ca (xN, x1)∈U. Procedura de calcul [1] Procedura PCH (N, C, X) /* i este varful din care incepe constructia ciclului */ x[1]=1 x[2]=1 k=2 while k>1 v=0 while x[k]
View more...

Comments

Copyright ©2017 KUPDF Inc.
SUPPORT KUPDF