Consideratii teoretice
Aplicabilitatea algoritmilor de tip Backtracking
Algoritmii de tip backtracking se aplică la problemele unde spaţiul soluţiilor S este de forma unui produs cartezian S = S0 × S1 × ... × Sn-1. Orice element x din spaţiul soluţiilor S va fi un vector de forma x = (x0, x1, ..., xn-1), cu xi ∈Si, 0<i<n. Nu toate elementele x∈S sunt soluţii valide ale problemei. Doar acele elemente x care satisfac anumite condiţii impuse de problemă vor fi soluţii valide. Definim condiţiile care trebuie satisfăcute sub forma unei funcţii booleene Solutie(x0,x1,...,xn-1). Un element x=(x0, x1, ..., xn-1) ∈ S este soluţie a problemei dacă funcţia Solutie aplicată componentelor lui x va returna valoarea true. Scopul este de a găsi acei vectori x∈S pentru care funcţia Solutie returnează true.Implementare
k = 0;
while (k >= 0)
{
do
{
* alege urmatorul x[k] din multimea S[k]
* evalueaza Continuare(x[1], x[2], ..., x[k])
}
while ( !Continuare(x[1], x[2], ..., x[k]) &&
(* mai sunt elemente de ales din multimea S[k]) )
if (Continuare(x[1], x[2], ..., x[k]))
{
if (k == n-1)
{
if (Solutie(x[1], x[2], ..., x[n]))
* afiseaza solutie
}
else
{
k = k + 1;
}
}
else
{
k = k - 1;
}
}
Dacă analizăm modul de funcţionare al algoritmului backtracking, vom vedea ca la orice moment de timp ne este suficient un tablou de n elemente pentru a memora elementele x0, x1, ..., xn-1 alese. Ca urmare în implementare declarăm un tablou x de dimensiune n. Tipul de date al tabloului depinde de problemă, de tipul mulţimilor S. Variabila k ne indică indicele mulţimii din care urmează să alegem un element. La început de tot trebuie să alegem un element din mulţimea S0, de aceea îl iniţializăm pe k cu valoarea 0. Pe urmă k se modifică în funcţie de modul în care avansăm sau revenim pe calea de căutare. Pentru alegerea următorului xk din mulţimea Sk, ne bazăm pe faptul că între elementele fiecărei mulţimi Sk există o relaţie de ordine. Dacă încă nu s-a ales nici un element din mulţimea Sk atunci îl alegem pe primul conform relaţiei de ordine. Dacă deja s-a ales cel puţin un element, atunci îl alegem pe următorul neales, conform relaţiei de ordine.
Exemple
Săritura calului pe tabla de şah
Funcţia Continuare trebuie să facă exact aceleaşi verificări ca şi funcţia Solutie, dar nu pentru toate 64 de căsuţe ci pentru cele k căsuţe care au fost alese până la un moment dat.
Cod sursă
Optimizare
Putem face o optimizare la programul prezentat, pornind de la faptul că se cunosc regulile după care se poate deplasa calul pe tabla de şah. La alegerea din mulţimea Ck, în loc să alegem pe rând fiecare căsută şi apoi să verificăm dacă s-ar putea face mutare în căsuţa respectivă, e mai eficient să alegem direct dintre căsuţele în care se poate face mutare. Pentru a putea determina aceste căsuţe, folosim doi vectori care definesc mutările posibile ale calului (numărul de căsuţe cu care se poate deplasa pe orizontală şi pe verticală). Prezentăm mai jos codul sursă care implementează această optimizare. Implementarea este una recursivă, pentru a arăta că modul de lucru al algoritmului backtracking se pretează în mod natural la implementări recursive. #include <stdio.h>#define N 5
int dx[8] = {-1, -2, -2, -1, 1, 2, 2, 1};
int dy[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
int c[N*N][2];
int count = 0;
void back(int pas)
{
int i, j, continuare;
if (pas == N*N)
{
for (i=0; i<pas; i++)
printf("(%d,%d) ", c[i][0], c[i][1]);
printf("\n");
count++;
}
else
{
for (i=0; i<8; i++)
{
c[pas][0] = c[pas-1][0] + dy[i];
c[pas][1] = c[pas-1][1] + dx[i];
if ((c[pas][0]>=0) && (c[pas][0]<N) &&
(c[pas][1]>=0) && (c[pas][1]<N))
{
continuare = 1;
for (j=0; continuare && (j<pas); j++)
{
if ((c[j][0] == c[pas][0]) &&
(c[j][1] == c[pas][1]))
continuare = 0;
}
if (continuare)
back(pas+1);
}
}
}
}
int main(void)
{
int i,j;
for (i=0; i<N; i++)
for (j=0; j<N; j++)
{
c[0][0] = i;
c[0][1] = j;
back(1);
}
printf("%d solutii\n", count);
return 0;
}