Programarea algoritmilor pentru calcularea drumului min (Dijkstra, Prim, Kruskal)
Cel mai eficient algoritm cunoscut pentru problema
drumurilor optime cu o singură sursă este algoritmul lui Dijkstra, care poate fi descris în mai multe moduri: ca algoritm de
tip “greedy” cu o coadă cu priorităti, ca algoritm ce foloseste operatia de
“relaxare” (comună si altor algoritmi), ca algoritm cu multimi  de vârfuri sau ca algoritm cu vectori. Diferentele
de prezentare provin din structurile de date utilizate. Se foloseste un vector
D astfel că d[i] este distanta  minimă de
la 1 la i, dintre drumurile care  trec
prin noduri deja selectate. O variabilă S de tip multime memorează numerele nodurilor
cu distantă   minimă fată de nodul 1,
găsite până la un   moment dat.
Initial   S={1} si d[i]=cost[1][i], adică
se consideră arcul direct de la 1 la i ca drum minim între 1 si i. Pe măsură ce
algoritmul evoluează, se actualizează D si S.
S ={1} // S =multime
noduri ptr care s-a determinat dist. minima fata de 1
repetă cât timp S
contine mai putin de n noduri {
gaseste muchia (x,y)
cu x în S si y nu în S care face minim d[x]+cost(x,y)
           adauga y la S
           d[y] = d[x] + cost(x,y)
       }
 La fiecare pas din algoritmul Dijkstra:
 -  Se
găseste dintre nodurile j care nu apartin lui S acel nod "jmin" care
are distanta minimă fată de nodurile din S;
 -  Se
adaugă nodul "jmin" la multimea S;
 -  Se
recalculează distantele de la nodul 1 la nodurile care  nu fac parte din S, pentru că distantele la nodurile
din S rămân neschimbate;
 -  Se
retine în p[j] numărul nodului precedent cel mai apropiat de nodul j (de pe
drumul minim de la 1 la j).
Pentru a ilustra modul
de lucru al algoritmului Dijkstra considerăm un graf orientat cu următoarele costuri
de arce:
(1,2)=5; (1,4)=2;
(1,5)=6;
(2,3)=3;
(3,2)=4; (3,5)=4;
(4,2)=2; (4,3)=7;
(4,5)=3;
(5,3)=3
Drumurile posibile
intre 1 si 3 si costul lor:
1-2-3 = 8; 1-4-3 = 9;
1-4-2-3 = 7;  1-4-5-3 = 8;  1-5-3 = 9;
Drumurile minime de la
1 la celelalte noduri sunt în acest graf:
1-4-2 cost 4
1-4-2-3 cost 7
1-4 cost 2
1-4-5 cost 5
 De observat că într-un drum minim fiecare drum
partial este minim; astfel în drumul 1-4-2-3, drumurile partiale 1-4-2 si 1-4
sunt si ele minime.
Evolutia vectorilor D
si S pentru acest graf  în cazul
algoritmului Dijkstra:
S 
 | 
  
D[2] 
 | 
  
D[3] 
 | 
  
D[4] 
 | 
  
D[5] 
 | 
  
Nod sel 
 | 
 
1 
 | 
  
5 
 | 
  
M 
 | 
  
2 
 | 
  
6 
 | 
  
4 
 | 
 
1,4 
 | 
  
4 
 | 
  
9 
 | 
  
2 
 | 
  
5 
 | 
  
2 
 | 
 
1,4,2 
 | 
  
4 
 | 
  
7 
 | 
  
2 
 | 
  
5 
 | 
  
5 
 | 
 
1,4,2,5 
 | 
  
4 
 | 
  
7 
 | 
  
2 
 | 
  
5 
 | 
  
3 
 | 
 
P[2] 
 | 
  
P[3] 
 | 
  
P[4] 
 | 
  
P[5] 
 | 
 
4 
 | 
  
2 
 | 
  
1 
 | 
  
4 
 | 
 
C[2] 
 | 
  
P[2] 
 | 
  
C[3] 
 | 
  
P[3] 
 | 
  
C[4] 
 | 
  
P[4] 
 | 
  
C[5] 
 | 
  
P[5] 
 | 
  
C[6] 
 | 
  
P[6] 
 | 
  
k 
 | 
 
6 
 | 
  
1 
 | 
  
1 
 | 
  
1 
 | 
  
5 
 | 
  
1 
 | 
  
M1 
 | 
  
1 
 | 
  
M1 
 | 
  
1 
 | 
  
3 
 | 
 
5 
 | 
  
3 
 | 
  
M2 
 | 
  
1 
 | 
  
5 
 | 
  
1 
 | 
  
6 
 | 
  
3 
 | 
  
4 
 | 
  
3 
 | 
  
6 
 | 
 
5 
 | 
  
3 
 | 
  
M2 
 | 
  
1 
 | 
  
2 
 | 
  
6 
 | 
  
6 
 | 
  
3 
 | 
  
M2 
 | 
  
3 
 | 
  
4 
 | 
 
5 
 | 
  
3 
 | 
  
M2 
 | 
  
1 
 | 
  
M2 
 | 
  
6 
 | 
  
6 
 | 
  
3 
 | 
  
M2 
 | 
  
3 
 | 
  
2 
 | 
 
M2 
 | 
  
3 
 | 
  
M2 
 | 
  
1 
 | 
  
M2 
 | 
  
6 
 | 
  
3 
 | 
  
2 
 | 
  
M2 
 | 
  
3 
 | 
  
5 
 | 
 
M2 
 | 
  
3 
 | 
  
M2 
 | 
  
1 
 | 
  
M2 
 | 
  
6 
 | 
  
M2 
 | 
  
2 
 | 
  
M2 
 | 
  
3 
 | 
  
Pas 
 | 
  
Arc (Cost) 
 | 
  
Acceptabil 
 | 
  
Cost total 
 | 
  
Afisare 
 | 
 
1 
 | 
  
1,3 (1) 
 | 
  
da 
 | 
  
1 
 | 
  
1 – 3 
 | 
 
2 
 | 
  
4,6 (2) 
 | 
  
da 
 | 
  
3 
 | 
  
4 – 6 
 | 
 
3 
 | 
  
2,5 (3) 
 | 
  
da 
 | 
  
6 
 | 
  
2 – 5  
 | 
 
4 
 | 
  
3,6 (4) 
 | 
  
da 
 | 
  
10 
 | 
  
3 – 6 
 | 
 
5 
 | 
  
1,4 (5) 
 | 
  
nu 
 | 
  
10 
 | 
  |
6 
 | 
  
3,4 (5) 
 | 
  
nu 
 | 
  
10 
 | 
  |
7 
 | 
  
2,3 (5) 
 | 
  
da 
 | 
  
15 
 | 
  
2 – 3 
 | 
 
Vectorul P va arăta în
final astfel:
Exemplu de functie
pentru algoritmul Dijkstra:
void dijkstra (Net
g,int p[]) { // Net este tipul abstract “graf cu costuri”
 int d[M],s[M]; // s= noduri ptr care se stie
distanta minima
 int dmin; int jmin,i,j;
  for (i=2;i<=g.n;i++) {
    p[i]=1; d[i]=carc(g,1,i); // distante
initiale de la 1 la alte noduri
  }
  s[1]=1;
 for (i=2;i<=g.n;i++) { // repeta de n-1 ori
    // caută nodul j ptr care d[j] este minim 
    dmin =MARE;
    for (j=2;j<=g.n;j++) // determina
minimul dintre distantele d[j]
      if (s[j]==0 && dmin > d[j]) {
// daca j nu e in S si este mai aproape de S
   dmin =d[j]; jmin=j;
      }
    s[jmin]=1; // adauga nodul jmin la S
    for (j=2;j<=g.n;j++) // recalculare
distante noduri fata de 1
      if
( d[j] >d[jmin] + carc(g,jmin,j) ) {
   d[j] =d[jmin] + carc(g,jmin,j);
   p[j] =jmin; // predecesorul lui j pe drumul
minim
      }
  }
}
In programul principal
se apelează repetat functia "drum":
for(j=2;j<=n;j++)
     drum (p,1, j); // afisare drum minim de la
1 la j
Afisarea drumului
minim pe baza vectorului "p" se poate face recursiv sau iterativ:
// drum minim intre i
si j - recursiv
void drum (int p[],
int i,int j) {
  if (j != i)
drum (p,i,p[j]);
  printf ("%d ",j);
}
    // drum minim intre i si j - iterativ   
void drum (int p[],
int i,int j){
  int 
s[M], sp=0;            // s este o
stiva vector cu varful in sp
  printf ("%d ",i); // primul nod de
pe calea i~j
  while (j != i) { // pune pe stiva nodurile
precedente lui j
     s[++sp]=j;
     j=p[j]; // precesorul lui j
  }
  for( ; sp>=1;sp--) // afisare continut
stiva
     printf("%d ",s[sp]);
}
Metoda de ajustare
treptată a lungimii drumurilor din vectorul D este o metodă de
"relaxare", 
folosită si în alti
algoritmi pentru drumuri minime sau maxime: algoritmul Bellmann-Ford pentru drumuri
minime cu o singură sursă în grafuri cu costuri negative, algoritmul Floyd
pentru drumuri minime între oricare pereche de noduri s.a.
Prin relaxarea unei
muchii (v,w) se întelege ajustarea costului anterior al drumului către nodul w tinându-se
seama si de costul muchiei v-w, deci considerând si un drum către w care trece
prin v. 
Complexitatea
algoritmului Dijkstra este O(n*n) si poate fi redusă la O(m*lg(n)) prin
folosirea unei cozi ordonate (min-heap) cu operatie de diminuare a cheii.
Pentru determinarea
unui arbore de acoperire de cost minim se cunosc doi algoritmi eficienti 
având ca autori pe Kruskal
si Prim. Problema   este   de  
a   găsi   pentru  
un   graf   dat  
arborele   de   acoperire  
cu   cost   total  
minim (MST=Minimum Spanning Tree) sau unul dintre ei, dacă sunt mai
multi.
Exemplu: graful
neorientat cu 6 noduri si următoarele arce si costuri:
(1,2)=6; (1,3)=1;
(1,4)=5;
(2,3)=5; (2,5)=3;
(3,4)=5; (3,5)=6;
(3,6)=4;
(4,6)=2;
(5,6)=6;
Arborele minim de
acoperire este format din arcele: (1,3),(3,6),(6,4),(3,2),(2,5) si are costul
total 1+5+3+4+2=15.
Algoritmul lui Prim
seamănă cu algoritmul Dijkstra pentru drumuri minime si foloseste o coadă cu priorităti   de 
arce  care  leagă 
vârfuri   din  MST 
cu  alte  vârfuri  
(coada  se  modifică 
pe  măsură   ce algoritmul evoluează). Algoritmul lui
Prim se bazează pe observatia următoare: fie S o submultime a vârfurilor
grafului si R submultimea V-S (vârfuri care nu sunt în S); muchia de cost minim
care uneste vârfurile din S cu vârfurile din R face parte din MST. La fiecare
pas se face o nouă tăietură în graful rămas si se determină un alt arc din MST;
proces repetat de n-1 ori (sau până când S este vidă). Solutia problemei este o
multime de arce, deci un vector de perechi de noduri, sau doi vectori de întregi
X si Y, cu semnificatia că o pereche x[i]-y[i] reprezintă un arc din MST. Este
posibilă si folosirea unui vector de întregi pentru arborele MST. Implementarea
mai eficienta a algoritmului Prim folosind 2 vectori:
p [i] = numărul
nodului din S cel mai apropiat de 
nodul  i din R
c [i] = costul arcului
dintre i si p[i]
La fiecare pas se
caută în vectorul “c” pentru a găsi nodul 
k din R cel mai apropiat de nodul i din S. Pentru a nu mai folosi o
multime S, se atribuie lui c[k] o valoare 
foarte mare astfel ca nodul k să nu mai fie luat in considerare în pasii
următori. 
Multimea S este deci
implicit multimea nodurilor i cu c[i] foarte mare. Celelalte noduri formează multimea
R.
# define M 20           // nr maxim de noduri 
# define M1 10000    // un nr. foarte mare (cost arc absent)
# define M2  (M1+1) // alt numar foarte mare (cost arc
folosit)
   // alg. Prim pentru arbore minim de
acoperire 
void prim (Net g, int
x[ ], int y[ ]){
  int c[M], cmin; 
  int p[M], i,j,k;
  int n=g.n; // n = nr de varfuri
  for(i=2;i<=n;i++) {
    p[i]=1; 
c[i]=carc (g,1,i); // costuri initiale
  }
  for(i=2;i<=n;i++) {
 // cauta nodul k cel mai apropiat de un nod
din mst 
    cmin = c[2];  k=2;
    for(j=2;j<=n;j++)
     if ( c[j] < cmin) {
       cmin=c[j];  k=j;
     }
     x[i-1]=p[k]; y[i-1]= k; // retine muchie
de cost minim in x si y
     c[k]=M2;
      // ajustare costuri in U 
    for(j=2;j<=n;j++)
     if (carc(g,k,j) < c[j] && c[j]
< M2) {
       c[j]= carc(g,k,j);  p[j] =k;
     }
  }
}
 Evolutia vectorilor “c” si “p” pentru exemplul
dat este următoarea:
Au fost necesare două
constante mari: M1 arată că nu există un arc între două noduri, iar M2 arată că
acel arc a fost inclus în MST si că va fi ignorat în continuare.
Vectorul “p” folosit
în programul anterior corespunde reprezentării unui arbore printr-un singur vector,
de predecesori.
Complexitatea
algoritmului Prim cu vectori este O(n*n), dar poate fi redusă la O(m*lg(n))
prin folosirea unui heap pentru memorarea costurilor arcelor dintre U si V.
Ideea algoritmului Kruskal
este de a alege la fiecare pas arcul de cost minim dintre cele rămase (încă
neselectate), dacă el nu formează ciclu cu arcele deja incluse în MST
(selectate). Conditia ca un arc (x,y) să nu formeze ciclu cu celelalte arce
selectate se poate exprima astfel: 
nodurile x si y trebuie să se afle în componente conexe diferite.
Initial fiecare nod formează o componentă conexă, iar apoi o componentă   conexă 
contine  toate  nodurile 
acoperite  cu  arce 
din  MST,  iar 
nodurile  neacoperite formează
alte componente conexe.
Algoritmul Kruskal
pentru găsirea unui arbore de acoperire de cost minim foloseste două tipuri abstracte
de date: o coadă cu priorităti si o colectie de multimi disjuncte si poate fi descris
astfel:
 citire date si creare coada de arce
repetă {
      extrage arcul de cost minim din coada
         dacă arc acceptabil atunci {
      afisare arc
   actualizare componente conexe
         }
     } până când toate nodurile conectate
 Un arc care leagă două noduri dintr-o aceeasi
componentă conexă va forma un ciclu cu arcele selectate anterior si nu poate fi
acceptat. Va fi acceptat numai un arc care leagă între ele noduri aflate în
două componente conexe diferite.
Pentru reteaua cu 6
noduri si 10 arce (1,2)=6; (1,3)=1; 
(1,4)=5;  (2,3)=5; (2,5)=3;
(3,4)=5; (3,5)=6;  (3,6)=4; 
(4,6)=2; (5,6)=6
Evolutia algoritmului
Kruskal este următoarea:
Toate nodurile din
graf trebuie să se afle în componentele conexe. Initial sunt atâtea componente (multimi)
câte noduri există. Atunci când un arc este acceptat, se reunesc   cele două multimi (componente) care contin
extremitătile arcului în una singura; în felul acesta numărul de componente conexe
se reduce treptat până când ajunge egal cu 1 (toate nodurile legate într-un
graf conex care este chiar arborele de acoperire cu cost minim).
Evolutia componentelor
conexe pentru exemplul anterior:
   Pas                     Componente conexe
       1       
           {1},
{2},{3},{4},{5},{6}
       2                    {1,3}, {2},{4},{5},{6}
       3                     {1,3}, {2,5}, {4,6}
       4                      {1,3,4,6}, {2,5}
       7                         {1,2,3,4,5,6}
 In programul următor graful este un vector de
arce, ordonat crescător după costuri înainte de a fi folosit. Exemplu:
typedef struct { int
v,w,cost ;} Arc;
   // compara arce dupa cost (ptr qsort)
int cmparc (const void
* p, const void* q) {
  Arc * pp =(Arc*) p; Arc *qq=(Arc*) q;
  return pp->cost -qq->cost;
}
    // algoritmul Kruskal 
 void main ( ) {
  DS ds;  
Arc arce[M], a;
  int x,y,n,na,mx,my,nm,k;
  printf ("nr.noduri în graf:
");  scanf ("%d", &n);
  initDS (ds,n); // ds = colectie de multimi disjuncte
  printf ("Lista de arce cu costuri:
\n");
  nm=0; // nr de muchii in graf
  while ( scanf
("%d%d%d",&a.v,&a.w,&a.cost) > 0)
     arce[nm++]=a;
  qsort (arce, nm, sizeof(Arc), cmparc);    // ordonare lista arce
  k=0; // nr arc extras din coada
  for ( na=n-1; na > 0; na--) {
     a=arce[k++]; // următorul arc de cost
minim
     x=a.v; y=a.w; // x, y = extremitati arc
     mx= findDS (ds,x); my=findDS (ds,y);
     if (mx !=my ) { // daca x si y in
componente conexe diferite
       unifDS (ds,x,y); // atunci se reunesc
cele doua componente
       printf ("%d - %d \n",x,y); //
si se scrie arcul gasit ptr mst
     }
  }
}