Dintre mulți algoritmi pentru găsirea celor mai scurte rute pe un grafic, pe Habré am găsit doar o descriere a algoritmului Floyd-Warshall. Acest algoritm găsește cele mai scurte căi între toate vârfurile graficului și lungimea lor. În acest articol, voi descrie principiul de funcționare al algoritmului lui Dijkstra, care găsește rutele optime și lungimile acestora între un anumit vârf (sursă) și toate celelalte vârfuri ale graficului. Dezavantajul acestui algoritm este că nu va funcționa corect dacă graficul are arce de greutate negativă.
De exemplu, luați următorul grafic direcționat G:
Putem reprezenta acest grafic ca o matrice C:
Să luăm vârful 1 ca sursă. Aceasta înseamnă că vom căuta cele mai scurte rute de la vârful 1 la vârfurile 2, 3, 4 și 5.
Acest algoritm parcurge toate vârfurile graficului și le atribuie etichete, care reprezintă distanța minimă cunoscută de la vârful sursă la un anumit vârf. Să ne uităm la acest algoritm folosind un exemplu.
Să atribuim o etichetă egală cu 0 primului vârf, deoarece acest vârf este sursa. Vom atribui etichete egale cu infinit nodurilor rămase.
Apoi, selectăm un vârf W care are o etichetă minimă (în prezent este vârful 1) și luăm în considerare toate vârfurile la care există o cale de la vârful W care nu conține vârfuri intermediare. Fiecăruia dintre vârfurile considerate atribuim o etichetă egală cu suma etichetei W și lungimea drumului de la W la vârful considerat, dar numai dacă suma rezultată este mai mică decât valoarea anterioară a etichetei. Dacă suma nu este mai mică, atunci lăsăm marca anterioară neschimbată.
După ce am luat în considerare toate nodurile către care există o cale directă de la W, marchem vârful W ca fiind vizitat și îl selectăm dintre cele ne vizitate încă pe cel care are valoarea minimă a etichetei, acesta va fi următorul vârf al lui W. În în acest caz, acesta este vârful 2 sau 5. Dacă există mai multe vârfuri cu aceleași etichete, atunci nu contează pe care o alegem ca W.
Vom selecta vârful 2. Dar nu există o cale de ieșire din el, așa că marchem imediat acest vârf ca vizitat și trecem la următorul vârf cu eticheta minimă. De data aceasta, doar vârful 5 are eticheta minimă. Să luăm în considerare toate vârfurile la care există căi directe din 5, dar care nu au fost încă marcate ca vizitate. Din nou găsim suma etichetei vârfului W și greutatea muchiei de la W la vârful curent, iar dacă această sumă este mai mică decât eticheta anterioară, atunci înlocuim valoarea etichetei cu suma rezultată.
Pe baza imaginii, putem observa că etichetele vârfurilor 3 și 4 au devenit mai mici, adică s-a găsit un traseu mai scurt către aceste vârfuri de la vârful sursă. Apoi, marcați al 5-lea vârf ca vizitat și selectați următorul vârf care are eticheta minimă. Repetăm toți pașii de mai sus atâta timp cât există vârfuri nevizitate.
După parcurgerea tuturor pașilor, obținem următorul rezultat:
Există și un vector P, pe baza căruia puteți construi cele mai scurte rute. Numărul de elemente ale acestui vector este egal cu numărul de vârfuri din grafic. Fiecare element conține ultimul vârf intermediar pe calea cea mai scurtă dintre vârful sursă și vârful destinație. La începutul algoritmului, toate elementele vectorului P sunt egale cu vârful sursă (în cazul nostru, P = (1, 1, 1, 1, 1)). Apoi, în etapa de recalculare a valorii etichetei pentru vârful în cauză, dacă eticheta vârfului în cauză se schimbă într-una mai mică, scriem valoarea vârfului curent W în tabloul P. De exemplu: al 3-lea vârf avea o etichetă cu valoarea „30”, cu W = 1. Apoi, la W=5, eticheta celui de-al 3-lea vârf s-a schimbat în „20”, prin urmare vom scrie valoarea în vectorul P - P=5. De asemenea, când W=5, valoarea etichetei la al 4-lea vârf s-a schimbat (a fost „50”, a devenit „40”), ceea ce înseamnă că trebuie să atribuiți valoarea W - P=5 celui de-al 4-lea element al vectorul P. Ca rezultat, obținem vectorul P = (1, 1, 5, 5, 1).
Știind că fiecare element al vectorului P conține ultimul vârf intermediar pe calea dintre sursă și vârful final, putem obține chiar cel mai scurt traseu.
Algoritmul lui Dijkstra este un algoritm grafic inventat de omul de știință olandez Edsger Dijkstra în 1959. Găsește cele mai scurte căi de la unul dintre vârfurile graficului la toate celelalte. Algoritmul funcționează numai pentru grafice fără muchii de greutate negativă.
Să luăm în considerare execuția algoritmului folosind exemplul graficului prezentat în figură.
Să presupunem că trebuie să găsiți cele mai scurte distanțe de la primul vârf la toate celelalte.
Cercurile indică vârfurile, liniile indică căile dintre ele (marginile graficului). Numerele vârfurilor sunt indicate în cercuri, iar „prețul” lor este indicat deasupra marginilor - lungimea căii. Lângă fiecare vârf există un semn roșu - lungimea celei mai scurte căi către acest vârf de la vârful 1.
Primul pas. Să ne uităm la pasul algoritmului lui Dijkstra pentru exemplul nostru. Vârful 1 are eticheta minimă. Vecinii săi sunt vârfurile 2, 3 și 6.
Primul vecin al vârfului 1 este vârful 2, deoarece lungimea căii către acesta este minimă. Lungimea căii în el prin vârful 1 este egală cu suma valorii etichetei vârfului 1 și lungimea muchiei care merge de la 1 la 2, adică 0 + 7 = 7. Aceasta este mai mică decât eticheta curentă a vârfului 2, infinit, deci noua etichetă este al 2-lea vârf este 7.
Efectuăm o operație similară cu alți doi vecini ai primului vârf - al 3-lea și al 6-lea.
Toți vecinii vârfului 1 sunt verificați. Distanța minimă actuală până la vârful 1 este considerată finală și nu poate fi revizuită (că acesta este într-adevăr cazul a fost dovedit prima dată de E. Dijkstra). Să o tăiem din grafic pentru a marca faptul că acest vârf a fost vizitat.
Al doilea pas. Se repetă pasul algoritmului. Din nou găsim „cel mai apropiat” dintre vârfurile nevizitate. Acesta este vârful 2 cu eticheta 7.
Din nou încercăm să reducem etichetele vecinilor vârfului selectat, încercând să trecem în ele prin al 2-lea vârf. Vecinii vârfului 2 sunt vârfurile 1, 3 și 4.
Primul (în ordine) vecin al vârfului 2 este vârful 1. Dar acesta a fost deja vizitat, așa că nu facem nimic cu primul vârf.
Următorul vecin al vârfului 2 este vârful 3, deoarece are eticheta minimă a vârfurilor marcate ca nevizitate. Dacă treceți la el prin 2, atunci lungimea unei astfel de căi va fi egală cu 17 (7 + 10 = 17). Dar eticheta curentă a celui de-al treilea vârf este 9, care este mai mică de 17, deci eticheta nu se schimbă.
Un alt vecin al vârfului 2 este vârful 4. Dacă mergeți la el prin al 2-lea, atunci lungimea unei astfel de căi va fi egală cu suma celei mai scurte distanțe până la al 2-lea vârf și distanța dintre vârfurile 2 și 4, adică , 22 (7 + 15 = 22) . Din 22<, устанавливаем метку вершины 4 равной 22.
Toți vecinii vârfului 2 au fost vizualizați, înghețăm distanța până la acesta și îl marchem ca vizitat.
Al treilea pas. Repetăm pasul algoritmului, selectând vârful 3. După „procesarea” acestuia, obținem următoarele rezultate:
Următorii pași. Repetăm pasul algoritmului pentru vârfurile rămase. Acestea vor fi vârfurile 6, 4 și 5, conform ordinii.
Finalizarea execuției algoritmului. Algoritmul se termină când nu mai pot fi procesate vârfuri. În acest exemplu, toate nodurile sunt tăiate, dar este o greșeală să presupunem că acesta va fi cazul în orice exemplu - unele vârfuri pot rămâne netălate dacă nu pot fi atinse, adică dacă graficul este deconectat. Rezultatul algoritmului este vizibil în ultima figură: cea mai scurtă cale de la vârful 1 la 2 este 7, la 3 este 9, la 4 este 20, la 5 este 20, la 6 este 11.
Implementarea algoritmului în diferite limbaje de programare:
tokenizer privat StringTokenizer;
//procedura de lansare a algoritmului lui Dijkstra de la vârful de pornire private void dejkstra(int s) ( dist[s] = 0; //cea mai mică distanță până la vârful de pornire este 0 pentru (int iter = 0; iter)
întoarcere nd; ) /* --- Dijkstra chestii; nodurile inaccesibile nu vor intra niciodată în coadă --- */ void calc_all(node_t *start) ( node_t *lead; edge_t *e; set_dist(start, start, 0); while ((lead = pop_queue())) pentru ( e = lead->edge e = e->frate) set_dist(e->nd, lead, lead->dist + e->len ) void show_path(node_t *nd) ( if (nd-> via == nd) ) printf("%s", nd->nume else if (!nd->via) printf("%s(neatins)", nd->name else ( show_path(nd->); via); printf ("-> %s(%g) ", nd->name, nd->dist int main(void) ( #ifndef BIG_EXAMPLE int i; # define N_NODES ("f" - "a" + 1) node_t); *noduri = calloc(sizeof(node_t), N_NODES pentru (i = 0; i
Algoritmul lui Dijkstra rezolvă problema căii celei mai scurte cu un singur vârf pentru un grafic direcționat ponderat G = (V, E) cu vârful sursă s, în care ponderile tuturor muchiilor sunt nenegative ((u, v) ? 0 pentru toate (u) , v) E). În cazul în care marginile graficului nu sunt egale, este recomandabil să folosiți acest algoritm.
Formularea problemei. Există un grafic. Unele dintre vârfurile sale sunt desemnate ca vârf 1. Este necesar să se găsească căile minime de la vârful 1 la fiecare dintre vârfurile graficului. O cale minimă este o cale cu suma minimă a prețurilor de-a lungul căii. Să numim prețul un număr nenegativ care este greutatea marginii.
Ideea algoritmului. Ideea se bazează pe următoarea afirmație evidentă: Să se construiască calea minimă de la vârf O la vârful B. Și să fie conectat vârful B la un anumit număr de vârfuri i. Să notăm cu C i costul drumului de la vârful B la vârful i. Să alegem valoarea minimă din C i. Apoi continuarea minimă a căii din punctul B va trece prin valoarea selectată.
Această afirmație chiar nu necesită dovezi. Și de aici rezultă o consecință foarte gravă. Să existe un set de vârfuri prin care trec deja căi minime. Este garantat existența unei astfel de mulțimi, acesta este vârful 1. Declarația formulată mai sus face posibilă adăugarea unui alt vârf la o mulțime deja existentă de vârfuri (le vom numi în continuare selectate), și deoarece există un număr finit de vârfuri în graficul, apoi într-un număr finit de pași vor fi selectate toate vârfurile graficului și aceasta va fi soluția.
Esența algoritmului lui Dijkstra constă în procedura de a adăuga încă un vârf la setul de cele selectate. Această procedură constă din doi pași:
1. Construim un set de vârfuri incidente celui selectat și găsim printre acestea vârful cu cel mai mic preț. Vârful găsit este adăugat la setul selectat.
2. Construim un set de vârfuri incidente celor selectate și stabilim noi prețuri pentru acestea. Noul preț al unui vârf este prețul minim al căii de la setul de vârfuri selectate la acest vârf. Noul preț este construit astfel:
o. Pentru un vârf neselectat din mulțimea celor selectate, se determină un subset de vârfuri incidente celui dat.
b. Pentru fiecare vârf al submulțimii selectate, se determină costul căii către acesta.
c. Se stabileste pretul minim. Acest preț devine prețul de top.
Algoritmul funcționează cu două tipuri de prețuri: prețul marginii și prețul vârfului. Prețurile coastelor sunt constante. Prețurile vârfurilor sunt constant recalculate. Sensul acestor prețuri este diferit. Costul unei muchii este costul deplasării de la vârf la vârf conectat prin această muchie. Iar prețul vârfului este prețul căii minime. O altă notă importantă se referă la recalcularea prețurilor preliminare. De fapt, are sens să se recalculeze prețurile preliminare numai pentru acele vârfuri care sunt asociate cu vârful adăugat la mulțimea selectată la ultimul pas, deoarece pentru alte vârfuri nu există niciun motiv pentru a modifica prețul preliminar.
Se știe că toate prețurile (de exemplu, de așezare a unei căi sau de călătorie) sunt nenegative. Găsiți cel mai mic cost al căii 1->i pentru tot i=1. n în timp O(n2).
În timpul funcționării algoritmului, unele orașe vor fi evidențiate (la început - doar orașul 1, la sfârșit - toate). În acest caz:
pentru fiecare oraș i selectat, este stocat cel mai mic cost al căii 1->i; Mai mult, se știe că minimul se realizează pe o potecă care trece doar prin orașele selectate;
pentru fiecare oraș i nealocat se stochează cel mai mic cost al căii 1->i, în care doar orașele alocate sunt folosite ca intermediare.
Setul de orașe alocate este extins pe baza următoarei observații: dacă dintre toate orașele nealocate îl luăm pe cel pentru care numărul stocat este minim, atunci acest număr este cel mai mic cost adevărat. Într-adevăr, poate exista o cale mai scurtă. Să ne uităm la primul oraș nemarcat de pe această cale - calea către el este deja mai lungă! (Nenegativitatea prețurilor este esențială aici.)
După ce a adăugat orașul selectat la cele selectate, trebuie să ajustam informațiile stocate pentru orașele neselectate. În acest caz, este suficient să luăm în considerare doar căile în care noul oraș este ultimul punct de transfer, iar acest lucru este ușor de făcut, deoarece știm deja costul minim al căii către noul oraș.
Cu alte cuvinte, fiecărui vârf de la V i se atribuie o etichetă - distanța minimă cunoscută de la acest vârf la a. Algoritmul funcționează pas cu pas - la fiecare pas „vizitează” un vârf și încearcă să reducă etichetele. Algoritmul se termină când toate nodurile au fost vizitate.
Inițializare. Notă superioară o presupus a fi egal 0 , etichetele vârfurilor rămase sunt infinite. Acest lucru reflectă faptul că distanțele de la o la alte vârfuri sunt încă necunoscute. Toate vârfurile graficului sunt marcate ca nevizitate.
Pasul algoritmului. Dacă toate nodurile sunt vizitate, algoritmul se termină. În caz contrar, un vârf este selectat dintre nodurile care nu au fost încă vizitate u, având o etichetă minimă. Avem în vedere tot felul de trasee în care u este penultimul punct. Vârfurile conectate la un vârf u margini, să le numim vecine ale acestui vârf. Pentru fiecare vecin, luați în considerare o nouă lungime de cale egală cu suma etichetei curente uși lungimea marginii de legătură u cu acest vecin. Dacă lungimea rezultată este mai mică decât eticheta vecinului, înlocuiți eticheta cu această lungime. Luând în considerare toți vecinii, marchem vârful u așa cum a fost vizitat și repetați pasul.
Deoarece algoritmul lui Dijkstra alege întotdeauna să proceseze vârfurile cu cel mai mic scor de calea cea mai scurtă, putem spune că este un algoritm lacom.
Să descriem mai detaliat schema de funcționare a algoritmului lui Dijkstra.
Algoritmul folosește trei matrice de N (= numărul de vârfuri ale rețelei) numere fiecare. Prima matrice A conține etichete cu două valori: 0 (vârful nu a fost încă luat în considerare) și 1 (vârful a fost deja luat în considerare); a doua matrice B conține distanțe - cele mai scurte distanțe curente de la vârful corespunzător; a treia matrice c conține numerele de vârfuri - k-lea element C [k] este numărul penultimului vârf de pe calea cea mai scurtă curentă de la Vi la Vk. Matricea distanțelor D specifică lungimea arcului D; dacă nu există un astfel de arc, lui D i se atribuie un număr mare B, egal cu „infinitul mașinii”.
Acum putem descrie:
1. (inițializare). Într-o buclă de la 1 la N, umpleți tabloul A cu zerouri; umpleți tabloul C cu numărul i; mutați al-lea rând al matricei D în tabloul B, A [i]: =1; C [i]: =0 (i este numărul vârfului de pornire)
2. (treaptă generală). Aflați minimul dintre cele nemarcate (adică acele k pentru care A [k] =0); să fie atins minimul la indicele j, adică. B[j]<=B [k] Затем выполняются следующие операции: A [j]: =1; если B [k] >B [j] +D , apoi (B [k]: =B [j] +D ; C [k]: =j) (Condiția înseamnă că calea Vi. Vk este mai lungă decât calea Vi. Vj Vk) . (Dacă toate A [k] sunt marcate, atunci lungimea căii de la Vi la Vk este egală cu B [k]. Acum trebuie să) enumerați vârfurile incluse în calea cea mai scurtă).
3. (oferirea unui răspuns). (Calea de la Vi la Vk este dată în ordine inversă prin următoarea procedură:)
2. Problema z;
3. z: =C [z]. Dacă z = O, atunci sfârșitul, în caz contrar, treceți la 3.2.
Pentru a executa algoritmul, trebuie să căutați prin matricea B de N elemente de N ori, adică. Algoritmul lui Dijkstra are complexitate pătratică: O(n2).
Mai jos este o diagramă bloc a algoritmului lui Dijkstra (vezi Fig. 2).
Fig.2. Organigrama algoritmului lui Dijkstra
La începutul algoritmului, distanța pentru vârful inițial este setată la zero și toate celelalte distanțe sunt umplute cu un număr pozitiv mare (mai mare decât calea maximă posibilă din grafic). Matricea flags este umplută cu zerouri. Apoi începe bucla principală.
La fiecare pas al buclei, căutăm un vârf cu o distanță minimă și un steag egal cu zero. Apoi îi setăm pavilionul la 1 și verificăm toate vârfurile adiacente. Dacă distanța din ea este mai mare decât suma distanței până la vârful curent și lungimea muchiei, atunci o reducem. Bucla se termină când steagurile tuturor vârfurilor devin egale cu 1.
Dat un grafic ponderat direcționat sau nedirecționat cu vârfuri și muchii. Greutățile tuturor marginilor nu sunt negative. Este indicat un anumit vârf de început. Trebuie să găsim lungimile celor mai scurte căi de la un vârf la toate celelalte vârfuri și, de asemenea, să oferim o modalitate de a afișa în sine cele mai scurte căi.
Această problemă se numește problema căilor cele mai scurte cu o singură sursă.
Iată un algoritm care a fost propus de un cercetător olandez. Dijkstra(Dijkstra) în 1959
Să creăm o matrice în care pentru fiecare vârf vom stoca lungimea curentă a celei mai scurte căi de la până la . Inițial, și pentru toate celelalte vârfuri, această lungime este egală cu infinitul (atunci când este implementată pe un computer, de obicei, un număr suficient de mare este ales ca infinit, evident mai mare decât lungimea posibilă a căii):
În plus, pentru fiecare vârf vom stoca dacă este sau nu marcat încă, adică. Să creăm o matrice booleană. Inițial, toate nodurile nu sunt marcate, adică.
Algoritmul lui Dijkstra în sine constă din iterații. La următoarea iterație, este selectat vârful cu cea mai mică valoare dintre cele nemarcate încă, adică:
(Este clar că la prima iterație va fi selectat vârful de pornire.)
Vârful selectat în acest fel este marcat. Mai departe, la iterația curentă, de la vârf relaxare: Toate muchiile care emană dintr-un vârf sunt examinate, iar pentru fiecare astfel de vârf algoritmul încearcă să îmbunătățească valoarea. Fie lungimea muchiei curente egală cu , apoi sub formă de cod relaxarea arată astfel:
Aceasta încheie iterația curentă, algoritmul trece la următoarea iterație (se selectează din nou vârful cu cea mai mică valoare, se fac relaxări din acesta etc.). În acest caz, în final, după iterații, toate vârfurile graficului vor deveni marcate, iar algoritmul își finalizează munca. Se afirmă că valorile găsite sunt lungimile necesare ale celor mai scurte căi de la la .
Este demn de remarcat faptul că, dacă nu toate vârfurile graficului sunt accesibile de la vârf, atunci valorile lor vor rămâne infinite. Este clar că ultimele câteva iterații ale algoritmului vor selecta doar aceste vârfuri, dar aceste iterații nu vor produce nicio muncă utilă (deoarece o distanță infinită nu va putea relaxa alte distanțe, chiar infinite). Prin urmare, algoritmul poate fi oprit imediat de îndată ce un vârf cu o distanță infinită este luat ca vârf selectat.
Refacerea căilor. Desigur, de obicei trebuie să cunoașteți nu numai lungimile celor mai scurte căi, ci și să obțineți căile în sine. Vom arăta cum să stocăm informații suficiente pentru reconstrucția ulterioară a drumului cel mai scurt de la orice vârf. Pentru aceasta, așa-numitul serie de strămoși: o matrice în care, pentru fiecare vârf, este stocat numărul vârfului care este penultimul în calea cea mai scurtă către vârf. Acest lucru folosește faptul că, dacă luăm calea cea mai scurtă către un vârf și apoi eliminăm ultimul vârf din această cale, vom obține o cale care se termină la un vârf, iar această cale va fi cea mai scurtă pentru vârf. Deci, dacă avem această matrice de strămoși, atunci cea mai scurtă cale poate fi restabilită din ea, pur și simplu luând strămoșul din vârful curent de fiecare dată până când ajungem la vârful de pornire - în acest fel vom obține calea cea mai scurtă dorită, dar scrisă în ordine inversă. Deci, cea mai scurtă cale către vârf este:
Rămâne să înțelegem cum să construim această serie de strămoși. Cu toate acestea, acest lucru se face foarte simplu: cu fiecare relaxare reușită, de exemplu. când de la un vârf selectat există o îmbunătățire a distanței până la un vârf, scriem că strămoșul vârfului este vârf:
Declarația principală, pe care se bazează corectitudinea algoritmului lui Dijkstra, este următorul. Se afirmă că, după ce orice vârf devine marcat, distanța actuală până la acesta este deja cea mai scurtă și, în consecință, nu se va mai schimba.
Dovada Vom produce prin inducție. Pentru prima iterație, validitatea sa este evidentă - pentru vârful avem , care este lungimea celei mai scurte căi către acesta. Să fie acum această afirmație adevărată pentru toate iterațiile anterioare, de exemplu. toate nodurile deja marcate; Să demonstrăm că nu este încălcat după finalizarea iterației curente. Fie vârful selectat la iterația curentă, adică. vârful pe care algoritmul îl va eticheta. Să demonstrăm că este într-adevăr egală cu lungimea drumului cel mai scurt către ea (notăm această lungime cu ).
Luați în considerare calea cea mai scurtă către vârf. Este clar că această cale poate fi împărțită în două căi: constând numai din vârfuri marcate (cel puțin vârful de pornire va fi în acest drum) și restul căii (poate include și vârfuri marcate, dar trebuie să înceapă cu unul nemarcat). Să notăm cu primul vârf al căii și cu ultimul vârf al căii.
Să demonstrăm mai întâi afirmația noastră pentru vârf, i.e. hai sa dovedim egalitatea. Cu toate acestea, acest lucru este aproape evident: la urma urmei, într-una dintre iterațiile anterioare am selectat un vârf și am efectuat relaxarea din acesta. Deoarece (în virtutea alegerii vârfului) cea mai scurtă cale către este egală cu cea mai scurtă cale către plus muchia, atunci când vă relaxați de la, valoarea va fi setată de fapt la valoarea necesară.
Datorită nenegativității costurilor marginii, lungimea căii celei mai scurte (și după ce tocmai s-a dovedit, este egală cu ) nu depășește lungimea căii celei mai scurte până la vârf . Având în vedere că (la urma urmei, algoritmul lui Dijkstra nu a putut găsi o cale mai scurtă decât este posibilă în general), obținem în cele din urmă următoarele relații:
Pe de altă parte, deoarece ambele și sunt vârfuri neetichetate, atunci deoarece la iterația curentă a fost ales vârful, și nu vârful, obținem o altă inegalitate:
Din aceste două inegalități concluzionăm egalitatea , iar apoi din relațiile găsite anterior obținem:
Q.E.D.
Deci, algoritmul lui Dijkstra constă din iterații, în fiecare dintre care este selectat vârful neetichetat cu cea mai mică valoare, acel vârf este marcat și apoi sunt examinate toate muchiile care emană din acel vârf, iar de-a lungul fiecărei muchii se încearcă îmbunătățirea valoare la celălalt capăt al muchiei.
Timpul de rulare al algoritmului este format din:
Cu cea mai simplă implementare a acestor operații, căutarea unui vârf va necesita operații, iar o relaxare va necesita operații, iar finalul asimptotice algoritmul este:
Implementarea:
const int INF = 1000000000 ;< vector < pair< int ,int >int main() ( int n; ... citește n ... vector > > g (n);...citirea graficului... int s = ...;< int >// vârful de pornire< char >vector< n; ++ i) { int v = - 1 ; for (int j= 0 ; j< n; ++ j) if (! u[ j] && (v == - 1 || d[ j] < d[ v] ) ) v = j; if (d[ v] == INF) break ; u[ v] = true ; for (size_t j= 0 ; j< g[ v] .size () ; ++ j) { int to = g[ v] [ j] .first , len = g[ v] [ j] .second ; if (d[ v] + len < d[ to] ) { d[ to] = d[ v] + len; p[ to] = v; } } } }d(n, INF), p(n);
d[ s] = 0;
Fie dat un grafic G = (V, E) cu ponderile muchiilor f(e) și un vârf de sursă selectat u. Să notăm cu d(v) distanța cea mai scurtă de la sursa u până la vârful v.
Fie că toate distanțele care nu depășesc un anumit număr r au fost deja calculate, adică distanțele până la vârfuri din mulțime V_r = \( v \in V \mid d(v) \le r \). Lasă
(v, w) \in \arg\min \( d(v) + f(e) \mid v \in V, e = (v, w) \in E \).Apoi d(w) = d(v) + f(e), iar v se află pe calea cea mai scurtă de la u la w.
Cantitati d^+(w) = d(v) + f(e), unde v \in V_r , e = (v, w) \in E , sunt numite distante estimateși sunt o estimare superioară pentru distanțe reale: d(w) \le d^+(w) .
La fiecare pas, algoritmul lui Dijkstra găsește vârful cu cea mai mică distanță estimată, îl marchează ca vizitat și actualizează distanțele estimate pentru toate capetele marginilor care provin din acesta.
Calculele de bază implică operații pe o coadă prioritară:
Pseudocod algoritm:
Date de intrare: grafic cu vârfuri V, coaste E cu solzi f(e); u. vârful sursă Imprima : distante(d v d ∈ V) la fiecare vârf u. de sus := Q nou coada de prioritate d ∈ V: pentru fiecare d = u dacă : distante(d) := 0 apoi : distante(d) := ∞ de sus altfel d, : distante(d)) .introduce( de sus ≠ ∅: d := de susîn timp ce coada de prioritate e = (d, .delete_min()) ∈ E: pentru fiecare : distante(.delete_min()) > : distante(d) + f(e): : distante(.delete_min()) := : distante(d) + f(e) de sus w .delete_min(), : distante(.delete_min()))1.5 Schema de implementare a algoritmului secvenţial
Implementarea specifică a algoritmului lui Dijkstra este determinată de alegerea algoritmului de așteptare cu prioritate utilizat. În cel mai simplu caz, poate fi o matrice sau o listă, căutarea minimului în care necesită scanarea tuturor nodurilor. Este mai eficient să folosiți grămada; Cea mai cunoscută estimare a complexității este folosirea unui morman Fibonacci.
1.6 Complexitatea secvenţială a algoritmului
C_2 – numărul de operații pentru calcularea minimului.
Algoritmul original al lui Dijkstra a folosit liste ca structură internă de date, pentru care C_1 = O(1) , C_2 = O(n) , deci complexitatea totală a fost O(n^2) .
Un grafic de algoritm este dat pentru o implementare de bază a algoritmului lui Dijkstra pe liste sau tablouri.
Figura 1. Graficul algoritmului fără afișarea datelor de intrare și de ieșire. n=3. Galben indică operațiuni de comparare, verde indică operațiuni de schimbare a etichetelor vârfurilor, iar albastrul indică etichetarea vârfurilor.
Algoritmul lui Dijkstra permite o paralelizare eficientă, timpul mediu de rulare este O(n^(1/3)\ln n) cu cantitatea de calcul O(n\ln n + m) .
Prima estimare se bazează pe caracteristica daps, care estimează numărul de accesări la memorie (citiri și scrieri) efectuate pe secundă. Această caracteristică este un analog al estimării flops în legătură cu lucrul cu memoria și este mai mult o evaluare a performanței interacțiunii cu memoria decât o evaluare a localității. Cu toate acestea, servește ca o sursă bună de informații, inclusiv compararea cu rezultatele pentru următoarea caracteristică cvg.
Figura 6 prezintă valorile daps pentru implementările algoritmilor obișnuiți, sortate în ordine crescătoare (cu cât sunt mai mari daps, cu atât performanța este în general mai mare). Puteți vedea că performanța memoriei este destul de scăzută. Acest lucru nu este surprinzător, deoarece implementările de algoritmi peste grafice au aproape întotdeauna o eficiență scăzută din cauza accesului neregulat la date, lucru pe care l-am văzut când am analizat profilul solicitărilor.
Figura 6. Comparația valorilor scorului daps
A doua caracteristică, cvg, este menită să ofere o estimare a localității mai independentă de mașină. Acesta determină cât de des trebuie programul să extragă date în memoria cache. În consecință, cu cât valoarea cvg este mai mică, cu atât mai rar trebuie făcut acest lucru, cu atât localitatea este mai bună.
Figura 7 prezintă valorile cvg pentru același set de implementări, sortate în ordine descrescătoare (cu cât este mai mic cvg, cu atât localitatea în general este mai mare). Se poate observa că în acest caz valoarea cvg se corelează bine cu scorul de performanță și reflectă localitatea scăzută, ceea ce este în concordanță cu concluziile desprinse din evaluarea calitativă a localității.
Figura 7. Comparația valorilor scorului cvg
Să realizăm un studiu al scalabilității implementării paralele a algoritmului conform metodologiei. Cercetarea a fost efectuată pe supercomputerul Lomonosov al Complexului de Supercomputer al Universității din Moscova. Setul și limitele valorilor parametrilor modificabili pentru lansarea implementării algoritmului:
Următoarea figură prezintă un grafic al performanței implementării selectate a algoritmului în funcție de parametrii variabili de lansare.
Figura 8. Implementarea în paralel a algoritmului. Performanța se modifică în funcție de numărul de procesoare și de dimensiunea zonei.
Datorită particularităților implementării paralele a algoritmului, performanța în ansamblu este destul de scăzută și crește lent pe măsură ce numărul de procese crește, iar când se apropie de numărul de procese 128, acesta începe să scadă. Acest lucru se explică prin utilizarea operațiilor colective la fiecare iterație a algoritmului și prin faptul că costurile schimburilor de comunicații cresc semnificativ odată cu creșterea numărului de procese utilizate. Calculele pe fiecare proces sunt destul de rapide și, prin urmare, descompunerea graficului compensează slab efectul costurilor schimburilor de comunicații.
Pentru a efectua experimentele, a fost folosită o implementare a algoritmului lui Dijkstra. Toate rezultatele au fost obținute pe supercalculatorul Lomonosov. Au fost folosite procesoare Intel Xeon X5570 cu performanțe de vârf de 94 Gflops, precum și compilatorul Intel 13.1.0. Cifrele arată eficiența implementării algoritmului lui Dijkstra pe 32 de procese.
Figura 9. Graficul de încărcare a CPU la executarea algoritmului lui Dijkstra
Graficul de încărcare a procesorului arată că aproape tot timpul rulează programul, nivelul de încărcare este de aproximativ 50%. Acest lucru indică o încărcare uniformă a procesoarelor cu calcule, folosind 8 procese per nod de calcul și fără a utiliza Hyper Threading.
Figura 10. Graficul operațiilor cu virgulă mobilă pe secundă la executarea algoritmului lui Dijkstra
Figura 10 prezintă un grafic al numărului de operații în virgulă mobilă pe secundă. Graficul arată o performanță generală foarte scăzută de calcul de aproximativ 250 Kflops la vârf și aproximativ 150 Kflops în medie pe toate nodurile. Acest lucru indică faptul că aproape toate calculele din program sunt efectuate cu numere întregi.
Graficul scrierilor de memorie arată o imagine similară a neuniformității computaționale, în care doar câteva procese scriu activ în același timp. Acest lucru se corelează cu alte programe de execuție. Este demn de remarcat numărul destul de scăzut de accesări la scriere în memorie. Acest lucru indică o bună organizare a calculelor și o gestionare destul de eficientă a memoriei.
Figura 15. Graficul vitezei de transmisie prin rețeaua Infiniband în octeți/sec când se execută algoritmul lui Dijkstra
Graficul vitezei de transfer de date a rețelei Infiniband arată o rată de transfer de date destul de ridicată în octeți pe secundă. Acest lucru sugerează că procesele schimbă intens între ele și, probabil, bucăți destul de mici de date, deoarece performanța de calcul este ridicată. Este de remarcat faptul că viteza de transfer variază între procese, indicând un dezechilibru de calcul.
Figura 16. Graficul vitezei de transmisie prin rețeaua Infiniband în pachete/sec atunci când algoritmul lui Dijkstra rulează
Graficul ratei de transfer de date în pachete pe secundă arată rate de transfer de date extrem de mari. Acest lucru sugerează că procesele schimbă probabil cantități nu foarte semnificative de date, dar foarte intens. Operațiunile colective sunt folosite la fiecare pas cu bucăți mici de date, ceea ce explică această imagine. Există, de asemenea, mai puțin dezechilibru între procese decât se vede în graficele de memorie, calcul și octeți/sec. Acest lucru indică faptul că procesele schimbă același număr de pachete conform algoritmului, dar primesc cantități diferite de date și efectuează calcule inegale.
Figura 17. Graficul numărului de procese care așteaptă să intre în etapa de numărare (Loadavg) când algoritmul lui Dijkstra rulează
Graficul numărului de procese care așteaptă să intre în etapa de calcul (Loadavg) arată că pe toată durata funcționării programului, valoarea acestui parametru este constantă și aproximativ egală cu 8. Aceasta indică funcționarea stabilă a programului cu toate nodurile încărcate. cu calcule. Acest lucru indică o încărcare foarte rațională și statică a resurselor hardware de către procese. Și arată o eficiență destul de bună a implementării în curs de realizare. În general, conform monitorizării sistemului a programului, putem concluziona că programul a funcționat destul de eficient și stabil. Utilizarea memoriei este foarte intensă, iar utilizarea mijloacelor de comunicare este extrem de intensivă, dar volumele de date transferate nu sunt mari. Acest lucru indică latența solicitantă a mediului de comunicare a părții algoritmice a programului. Eficiența scăzută pare să se datoreze volumului destul de mare de transferuri pe proces și schimburilor intensive de mesaje.