This shows you the differences between two versions of the page.
sda-ab:laboratoare:10 [2020/12/24 13:04] ruben_gilian.udroiu |
sda-ab:laboratoare:10 [2021/04/26 09:58] (current) ruben_gilian.udroiu [1. Obiectivele laboratorului] |
||
---|---|---|---|
Line 1: | Line 1: | ||
- | ===== Laboratorul 10: Grafuri - Drumuri de cost minim ===== | + | ===== Laboratorul 9: Grafuri - Drumuri de cost minim ===== |
+ | |||
+ | ====== 1. Obiectivele laboratorului ====== | ||
+ | |||
+ | *Înțelegerea ideii de cost și de drum minim într-un graf | ||
+ | *Prezentarea algoritmilor care calculează drumul de cost minim | ||
+ | *Înțelegerea aplicațiilor practice ( de exemplu: gasirea drumului minim intre doua locatii, rutarea in cazul retelelor de calculatoare) | ||
+ | |||
+ | Structura laboratorului se gaseste in **[[https://github.com/sda-ab/lab-09-tasks|acest link.]]** | ||
+ | |||
+ | ====== 2. Notiuni teoretice ====== | ||
+ | |||
+ | === 2.1. Costul unei muchii === | ||
+ | |||
+ | La fel ca la arbori de acoperire, presupunem că fiecare muchie are un cost de **parcurgere**. | ||
+ | |||
+ | === 2.2. Costul unei drum (drumul de cost minim) === | ||
+ | |||
+ | Într-un graf, orice drum este definit de o succesiune de muchii (cu proprietatea că, pentru oricare două muchii consecutive din succesiune, nodul destinaţie/de sosire al primei muchii este acelaşi cu nodul sursa/de plecare al celei de-a doua muchii). | ||
+ | |||
+ | Costul unui drum va fi definit ca **suma costurilor muchiilor** ce compun acel drum. | ||
+ | |||
+ | Fie un nod **sursă**(S) şi un nod **destinaţie**(D). Pot exista mai multe drumuri de la S la D(drumuri care au S = primul nod, D = ultimul nod), iar drumul de cost minim de la S la D va fi cel mai **ieftin**(cu **costul cel mai mic**) dintre acestea. (**OBS** - pot exista mai multe drumuri de cost minim de la S la D). | ||
+ | |||
+ | === 2.3. Legatura muchii-arce == | ||
+ | |||
+ | <note tip>Orice **graf neorientat** este echivalent cu un **graf orientat** dacă înlocuim fiecare **muchie** cu **două arce**(de acelaşi cost, câte un arc pentru fiecare sens). | ||
+ | Algoritmii următori pot fi folosiţi(în limita observaţiilor finale) pe grafuri orientate la fel ca pe grafuri neorientate.</note> | ||
+ | |||
+ | ====== 3. Drumuri de cost minim cu sursa unica ====== | ||
+ | |||
+ | Următorii algoritmi caută drumurile de cost minim de la un singur nod(sursă) la toate celelalte noduri. Rezultatul acestor algoritmi este un arbore cu drumuri de cost minim, unde: | ||
+ | *nodul sursă(S) este rădăcina arborelui; | ||
+ | *toate nodurile din graf sunt în arbore; | ||
+ | *pentru orice nod destinaţie(D), costul drumului din arbore de la rădăcina S la D este drum de cost minim(de la S la D) în graf. | ||
+ | |||
+ | Algoritmi: | ||
+ | *Algoritmul lui **Dijkstra** | ||
+ | *Algoritmul **Bellman-Ford** | ||
+ | |||
+ | === 3.1 Algoritmul lui Dijkstra === | ||
+ | |||
+ | Un algoritm pentru determinarea celei mai scurte cai de la un varf la oricare alt varf e algoritmul lui Dijkstra. (**OBS** - acesta nu functioneaza pentru grafuri cu circuite negative(in cazul in care exista circuite de cost negativ, costum minim pentru orice varf care apartine circuitului, ar putea fi considerat -INF, iar algoritmul nu detecteaza aceste circuite) | ||
+ | |||
+ | Algoritm: | ||
+ | *iniţial, toate nodurile sunt neexplorate şi vom construi arborele, începând de la nodul S; | ||
+ | atribuim un posibil cost(o estimare a distanţei) pentru fiecare nod. (iniţial, S are costul 0, toate celelalte noduri au costul infinit); | ||
+ | *la fiecare pas, alegem cel mai bun candidat dintre nodurile neexplorate, urmând să îl explorăm(să îi evaluăm vecinii), iar acel candidat va rămâne în arbore; | ||
+ | *la fiecare explorare(evaluare a vecinilor), dacă găsim o nouă estimare de cost mai bună decât cea precedentă, folosim, mai departe, noua estimare. Dacă dorim să ţinem evidenţa muchiilor folosite, actualizăm şi nodul părinte al vecinului respectiv. | ||
+ | <note important>OBS: atunci cand evaluam vecinii unui nod - pentru fiecare nod vecin(V) al lui C, noul cost posibil va fi costul drumului S-V(de la S la V) care trece prin C, mai exact - suma dintre **costul drumului S-C** şi **costul muchiei (C,V)**.</note> | ||
+ | |||
+ | {{ :sda-ab:laboratoare:dijkstra_animation.gif?400 |}} | ||
+ | |||
+ | <note tip>Construind astfel algoritmul, este **garantat** că, în momentul în care explorăm un nod(C), estimarea pentru costul drumului S-C este chiar **costul minim**.</note> | ||
+ | |||
+ | Pasii: | ||
+ | |||
+ | <code c> | ||
+ | 1. Declarăm două mulţimi: | ||
+ | mulţimea nodurilor neexplorate(MN), iniţial MN conţine toate nodurile; | ||
+ | mulţimea nodurilor explorate(ME) ce compun arborele, iniţial ME = vidă; | ||
+ | 2. Atribuim fiecărui nod o estimare iniţială a costului: | ||
+ | 0 pentru nodul sursă(S); | ||
+ | infinit pentru toate celelalte; | ||
+ | 3. Cât timp există noduri în MN | ||
+ | 1. Alegem, din MN(nodurile neexplorate), nodul cu cel mai mic cost estimat | ||
+ | îl numim C(nodul curent) | ||
+ | 2. pentru fiecare din vecinii lui C care se află în MN | ||
+ | 3. calculăm noua estimare de cost = cost(drumul S-C) + cost(muchia (C,V)); | ||
+ | 4. comparăm noua estimare cu vechiul cost(drumul S-V): | ||
+ | dacă noul cost e mai bun | ||
+ | 1. actualizăm cost(drumul S-V) = noul cost; | ||
+ | 2. actualizăm parinte(V) = C; (pentru păstrarea muchiei folosite) | ||
+ | altfel păstrăm vechiul cost | ||
+ | 5. Marcăm nodul C ca explorat: îl eliminăm din MN şi îl adăugăm în ME. | ||
+ | (Nu va mai fi verificat) | ||
+ | </code> | ||
+ | <note tip>**OBS** - algoritmul nu salveaza si calea de la nodul sursa spre celalalte varfuri. Pentru a putea face acest lucru, se poate introduce un vector pred (predecesor) care stocheaza predecesorul pentru fiecare varf adaugat in ME.</note> | ||
+ | |||
+ | === 3.2 Algoritmul Bellman-Ford === | ||
+ | |||
+ | Algoritmul Bellman-Ford determina drumurile de cost minim de la un varf **sursa** la fiecare dintre celalalte varfuri, identificand eventualele circuite de cost negativ. | ||
+ | |||
+ | Pasii sunt similari ca cei ai algoritmului Dijkstra: | ||
+ | *vom construi arborele, începând de la nodul S; | ||
+ | *atribuim un posibil cost(o estimare a distanţei) pentru fiecare nod. (iniţial, S are costul 0, toate celelalte noduri au costul infinit); | ||
+ | *la fiecare evaluare, dacă găsim o nouă estimare de cost mai bună decât cea precedentă, folosim, mai departe, noua estimare. Dacă dorim să ţinem evidenţa muchiilor folosite, actualizăm şi nodul părinte. | ||
+ | *funcţia de estimare a costului este definită la fel ca la algoritmul lui Dijkstra(costul drumului de la S la nodul respectiv) | ||
+ | <note tip>Diferenta care apare este cea la alegerea nodurilor pentru care facem evaluarea : | ||
+ | *Algoritmul nu are preferinţe pentru anumite noduri şi nu extrage, la fiecare pas, cel mai bun candidat. | ||
+ | *În schimb, acest algoritm evaluează toate muchiile la un pas. (presupunand ca nu se repeta muchii lungimea oricărui drum de la S la alt nod va fi maxim **N-1**, unde N = numarul de noduri din graf) | ||
+ | </note> | ||
+ | <code c> | ||
+ | 1. Atribuim fiecărui nod o estimare iniţială a costului: | ||
+ | 0 pentru nodul sursă(S); | ||
+ | infinit pentru toate celelalte; | ||
+ | 2. Executăm de N-1 ori: | ||
+ | 1. Pentru fiecare pereche (u, v) a.i. există muchie de la u la v | ||
+ | 1. calculăm noua estimare de cost = cost(drumul S-u) + cost(muchia (u,v)); | ||
+ | 2. comparăm noua estimare cu vechiul cost(drumul S-v): | ||
+ | dacă noul cost e mai bun | ||
+ | 1. actualizăm cost(drumul S-v) = noul cost; | ||
+ | 2. actualizăm parinte(v) = u; (pentru păstrarea muchiei folosite) | ||
+ | altfel păstrăm vechiul cost | ||
+ | 3. Dupa executare, pentru identificarea circuitelor de cost negativ, se verifica fiecare pereche (u,v) a.i exista muchie de la u la v cu conditia ca noul cost sa fie mai bun. In cazul in care exista, se va returna valoarea 0 si 1, in caz contrar. | ||
+ | </code> | ||
+ | |||
+ | ====== 4. Drumul de cost minim intre oricare 2 noduri ====== | ||
+ | |||
+ | Algoritmul **Floyd-Warshall**, caută cel mai scurt drum de la **orice nod sursă(S)** la **fiecare nod destinaţie(D)**. | ||
+ | |||
+ | Rezultatul algoritmului este o matrice **N x N**(unde N = |V|, numărul de noduri), iar valorea din matrice de la poziţia [i][j] va fi costul minim pentru drumul i-j. Fie această matrice numită **dist**. | ||
+ | <code c> | ||
+ | 1. Declarăm matricele: | ||
+ | dist, matrice N x N şi o iniţializăm dist[i][j] = infinit, pentru orice i şi j | ||
+ | next, matrice N x N în care vom salva prima muchie din drumul i-j de cost minim | ||
+ | //next este necesar doar în cazul în care ne interesează muchiile folosite | ||
+ | //pasul k = 0 | ||
+ | 2. Pentru fiecare nod v | ||
+ | 1. dist[v][v] = 0; | ||
+ | 3. Pentru fiecare pereche (u,v) a.i. există muchie de la u la v | ||
+ | 1. dist[u][v] = costMuchie(u,v); | ||
+ | 2. next[u][v] = v; //pentru urmărirea muchiilor ce compun drumul | ||
+ | //am terminat pasul k = 0 | ||
+ | 4. Pentru fiecare pas k (de la 1 la N) | ||
+ | 1. Pentru fiecare nod i (de la 1 la N) | ||
+ | 1. Pentru fiecare nod j (de la 1 la N) | ||
+ | 1. calculăm costul nou = dist[i][k] + dist[k][j]; | ||
+ | 2. comparăm costul nou cu costul vechi = dist[i][j] | ||
+ | dacă e mai bun costul nou: | ||
+ | 1. dist[i][j] = costul nou; | ||
+ | 2. next[i][j] = next[i][k]; //pentru urmărirea muchiilor | ||
+ | altfel păstrăm costul vechi | ||
+ | </code> | ||
+ | |||
+ | Pentru simplitate, algoritmul numerotează nodurile de la 1 la N şi foloseşte următoarea construcţie: | ||
+ | **costMinim(i,j,k)** = cel mai ieftin drum care pleaca din i, ajunge in j, iar nodurile parcurse intermediar fac parte din {1,2,3...k} (primele k noduri). | ||
+ | |||
+ | <note tip> | ||
+ | ***costMin(i,j,0)** = costMuchie(i,j) (dacă există muchie de la i la j) sau infinit(altfel); | ||
+ | ***costMin(i,j,N)** = drumul de cost minim de la i la j | ||
+ | </note> | ||
+ | |||
+ | **OBS** - Pentru obţinerea nodurilor ce formează drumul de cost minim de la u la v, putem folosi următoarea secvenţă: | ||
+ | |||
+ | <code c> | ||
+ | funcţie afişareDrum(u, v) | ||
+ | 1. dacă u == v | ||
+ | 1. print(v); STOP | ||
+ | 2. print(u); | ||
+ | 3. afişareDrum(next[u][v], v); | ||
+ | </code> | ||
+ | <note tip>OBS: Algoritmul Floyd-Warshall poate detecta cicluri cu cost negativ: putem verifica, la fiecare pas, dacă avem o valoare negativă pe diagonala matricei **dist**. Dacă găsim **cel puţin** o valoare negativă, graful conţine măcar un ciclu cu cost negativ</note> | ||
+ | |||
+ | ====== 5. Observatii finale ====== | ||
+ | |||
+ | <note important>Cei trei algoritmi functioneaza bine atunci cand **toate muchiile** au **cost pozitiv**</note> | ||
+ | |||
+ | <note warning> | ||
+ | Drumul de cost minim **NU** este **bine definit** atunci când există cicluri cu cost negativ. | ||
+ | *putem ajunge de la un nod la acelaşi nod, folosind acelaşi ciclu de oricâte ori ⇒ (costMinim = -infinit); | ||
+ | *în aceste cazuri, nu putem pune problema găsirii drumurilor de cost minim. | ||
+ | </note> | ||
+ | |||
+ | <note important>O muchie cu cost negativ este echivalentă cu 2 arce cu cost negativ şi implică existenţa unui ciclu cu cost negativ.</note> | ||
+ | |||
+ | <note warning>Algoritmii **Bellman-Ford** şi **Floyd-Warshall** funcţionează pe grafurile cu **arce cu cost negativ**, atâta timp cât drumurile de cost minim sunt **bine definite**(fără cicluri cu cost negativ)</note> | ||
+ | |||
+ | ====== 6. Exercitii propuse ====== | ||
+ | |||
+ | - Daţi un exemplu de graf orientat cu un singur arc cu cost negativ pentru care algoritmul lui Dijkstra dă rezultate greşite. | ||
+ | - Cum putem folosi algoritmul lui Dijkstra sau algoritmul Bellman-Ford pentru a obţine aceleaşi rezultate ca algoritmul Floyd-Warshall(drumul de cost minim pentru toate perechile de noduri)? (**OBS ** -Ne limităm la grafurile pe care merg toţi cei trei algoritmi. | ||
+ | - Implementaţi unul din algoritmi pentru a calcula drumurile de cost minim de la un nod sursă la toate celelalte noduri într-un graf cu toate muchiile/arcele cu cost pozitiv. | ||
+ | - Verificaţi dacă un graf conţine cicluri negative. | ||
+ | - Cum găsiţi(mai rapid decât cu cei 3 algoritmi prezentaţi) drumul de cost minim de la S la D într-un graf în care toate muchiile au acelaşi cost(1)? Cum adaptaţi soluţia în cazul în care toate muchiile au costul 1 sau 2? | ||