Sunteți pe pagina 1din 15

METODA BACKTRACKING

Metoda backtracking poate fi folosită în rezolvarea a diverse probleme. Este o metodă


lentă, dar de multe ori este singura pe care o avem la dispoziție!

Introducere
Metoda backtracking poate fi aplicată în rezolvarea problemelor care respectă
următoarele condiții:

 soluția poate fi reprezentată printr-un tablou x[]=(x[1], x[2], ...,


x[n]), fiecare element x[i] aparținând unei mulțimi cunoscute Ai;
 fiecare mulțime Ai este finită, iar elementele ei se află într-o relație de ordine
precizată – de multe ori cele n mulțimi sunt identice;
 se cer toate soluțiile problemei sau se cere o anumită soluție care nu poate fi
determinată într-un alt mod (de regulă mai rapid).

Algoritmul de tip backtracking construiește vectorul x[] (numit vector soluție) astfel:

Fiecare pas k, începând (de regulă) de la pasul 1, se prelucrează elementul


curent x[k] al vectorului soluție:

 x[k] primește pe rând valori din mulțimea corespunzătoare Ak;


 la fiecare pas se verifică dacă configurația curentă a vectorului soluție poate
duce la o soluție finală – dacă valoarea lui x[k] este corectă în raport
cu x[1], x[2], … x[k-1]:
o dacă valoarea nu este corectă, elementul curent X[k] primește
următoarea valoare din Ak sau revenim la elementul anterior x[k-1],
dacă X[k] a primit toate valorile din Ak – pas înapoi;
o dacă valoarea lui x[k] este corectă (avem o soluție parțială), se verifică
existența unei soluții finale a problemei:
 dacă configurația curentă a vectorului soluție x reprezintă soluție
finală (de regulă) o afișăm;
 dacă nu am identificat o soluție finală trecem la următorul
element, x[k+1], și reluăm procesul pentru acest element – pas
înainte.

Pe măsură ce se construiește, vectorul soluție x[] reprezintă o soluție parțială a


problemei. Când vectorul soluție este complet construit, avem o soluție finală a
problemei.

Exemplu
Să rezolvăm următoarea problemă folosind metoda backtracking. Să se afișeze
permutările mulțimii {1, 2, 3}.
o Un șir de numere reprezintă o permutare a unei mulțimi M dacă și numai dacă
conține fiecare element al mulțimii M o singură dată. Altfel spus, în cazul nostru:

 are exact 3 elemente;
 fiecare element este cuprins între 1 și 3;
 elementele nu se repetă.

Formalizare
Pentru a putea realiza un algoritm backtracking pentru rezolvarea unei probleme trebuie
să răspundem la următoarele întrebări:

1. Ce memorăm în vectorul soluție x[]? Uneori răspunsul este direct; de


exemplu, la generarea permutărilor vectorul soluție reprezintă o permutare a
mulțimii A={1,2,...,n}. În alte situații, vectorul soluție este o reprezentare mai
puțin directă a soluției; de exemplu, generarea submulțimilor unei
mulțimi folosind vectori caracteristici sau Problema reginelor.
2. Ce valori poate lua fiecare element x[k] vectorului soluție și câte elemente
poate avea x[]? Altfel spus, care sunt mulțimile Ak. Vom numi aceste
restricții condiții externe. Cu cât condițiile externe sunt mai restrictive (cu cât
mulțimile Ak au mai puține elemente), cu atât va fi mai rapid algoritmul!
3. Ce condiții trebuie să îndeplinească x[k] ca să fie considerat
corect? Elementul x[k] a primit o anumită valoare, în conformitate ce condițiile
externe. Este ea corectă? Poate conduce la o soluție finală? Aceste condiții se
numesc condiții interne și în ele pot să intervină doar x[k] și
elementele x[1], x[2], …, x[k-1]. Elementele x[k+1], …, x[n] nu poti
apărea în condițiile interne deoarece încă nu au fost generate!!!
4. Am găsit o soluție finală? Elementul x[k] a primit o valoare conformă cu
condițiile externe, care respectă condițiile interne. Am ajuns la soluție finală sau
continuăm cu x[k+1]?

Exemplu. Pentru problema generării permutărilor mulțimii A={1,2,3,…,n}, condițiile de


mai sus sunt:

1. Vectorul soluție conține o permutare a mulțimii A;


2. Condiții externe: x[k]∈{1,2,3,…,n}
3. Condiții interne: x[k]≠x[i], pentru i=1,k−1
4. Condiții de existență a soluției: k=n

Algoritmul general
Metoda backtracking poate fi implementată iterativ sau recursiv. În ambele situații se se
folosește o structură de deate de tip stivă.

Următorul subprogram recursiv prezintă algoritmul la modul general:


 la fiecare apel BACK(k) se generează valori pentru elementul x[k] al vectorului
soluție;
 instrucțiunea Pentru modelează condițiile externe;
 subprogramul OK(k) verifică condițiile interne
 subprogramul Solutie(k) verifică dacă configurația curentă a vectorului soluție
reprezintă o soluție finală
 subprogramul Afisare(k) tratează soluția curentă a problemei – de exemplu o
afișează!

subprogram BACK(k)
┌ pentru fiecare element i din A[k] executa
│ x[k] ← i
│ ┌ daca OK(k) atunci
│ │ ┌ daca Solutie(k) atunci
│ │ │ Afisare(k)
│ │ │ altfel
│ │ │ BACK(k+1)
│ │ └■
│ └■
└■
sfarsit_subprogram

Observații:

 de cele mai multe ori mulțimile A sunt de forma  A={1,2,3,….,n}  sau  A={1,2,3,


….,m}  sau A={a,a+1,a+2,….,b} sau o altă formă astfel încât să putem scrie
instrucțiunea Pentru conform specificului limbajului de programare folosit –
eventual folosind o structură repetitivă de alt tip! Dacă este necesar, trebuie
realizate unele transformări încât mulțimile să ajungă la această formă!
 elementele mulțimii A pot fi in orice ordine. Contează însă ordinea în care le vom
parcurge în instrucțiunea Pentru, deoarece în probleme este precizată de obicei
o anumită ordine în care trebuie generate soluțiile:

o dacă parcurgem elementele lui A în ordine crescătoare vom obține soluții în ordine
lexicografică;
o dacă parcurgem elementele lui A în ordine descrescătoare vom obține soluții în ordine
invers lexicografică.
o

Un șablon C++ pentru rezolvarea unei probleme oarecare folosind metoda


backtracking. Vom considera în continuare următoarele condiții externe: x[k]=A,B
pentru k=1,n. În practică A și B vor avea valori specifice problemei:

#include <fstream>
using namespace std;

int x[10] ,n;


int Solutie(int k){
// x[k] verifică condițiile interne
// verificare dacă x[] reprezintă o soluție finală
return 1; // sau 0
}
int OK(int k){
// verificare conditii interne
return 1; // sau 0
}
void Afisare(int k)
{
// afișare/prelucrare soluția finală curentă
}
void Back(int k){
for(int i = A ; i <= B ; ++i)
{
x[k]=i;
if( OK(k) )
if(Solutie(k))
Afisare(k);
else
Back(k+1);
}
}
int main(){
//citire date de intrare
Back(1);
return 0;
}

De multe ori condițiile de existență a soluției sunt simple și nu se justifică scrierea unei
funcții pentru verificarea lor, ele putând fi verificate direct în funcția Back().

De cele mai multe ori, rezolvarea unei probleme folosind metoda backtracking constă în
următoarele:

1. stabilirea semnificației vectorului soluție;


2. stabilirea condițiilor externe;
3. stabilirea condițiilor interne;
4. stabilirea condițiilor de existența a soluției finale;
5. completarea adecvată a șablonului de mai sus!

GENERAREA PERMUTARILOR

Prin permutare a unei mulțimi înțelegem o aranjare a elementelor sale, într-o


anumită ordine. Este cunoscut, printre altele, faptul că numărul de permutări ale unei
mulțimi cu n elemente este Pn=n!=1⋅2⋅⋅⋯⋅n

Problema
Fie un număr natural n. Să se afișeze, în ordine lexicografică, permutările mulțimii {1,2,,
⋯,n}
Exemplu
Pentru n=3, se va afișa:

1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1

Rezolvare
Bineînțeles, vom rezolva problema prin metoda backtracking. Vectorul soluție x[] va
reprezenta o permutare candidat. Să ne gândim care sunt proprietățile unei permutări,
pe care le va respecta și vectorul x[]:

 elementele sunt numere naturale cuprinse între 1 și n;


 elementele nu se repetă;
 vectorul x[] se construiește pas cu pas, element cu element. El va conține o
permutare validă când va conține n elemente, desigur corecte.

Cele observate mai sus ne permit să precizăm condițiile specifice algoritmului


backtracking, într-un mod mai formal:

 condiții externe: x[k]∈{1,2,⋯,n}
 condiții interne: x[k]∉{x[1],x[2],⋯,x[k−1]} pentru k∈{2,3,⋯,n}
 condiții de existență a soluției: k=n

Următorul program afișează pe ecran permutările, folosind un algoritm recursiv:

#include <iostream>
using namespace std;
int x[10] ,n;
void Afis()
{
for( int j=1;j<=n;j++)
cout<<x[j]<<" ";
cout<<endl;
}
int OK(int k){
for(int i=1;i<k;i++)
if(x[k]==x[i])
return 0;
return 1;
}
int Solutie(int k)
{ return k == n;}

void back(int k){


for(int i=1 ; i<=n ; i++)
{
x[k]=i;
if( OK(k) )
if(Solutie(k))
Afis();
else
back(k+1);
}
}
int main(){
cin>>n;
back(1);
return 0;
}

Semnificația funcțiilor
 void Afis(); afișează soluția curentă. Când se apelează, vectorul
soluție x are n elemente, reprezentând o permutare completă;
 int OK(int k); verifică condițiile interne. La apel, x[k] tocmai a primit o
valoare conform condițiilor externe. Prin funcția OK() se va verifica dacă această
valoare este validă;
 int Solutie(int k); verifică dacă avem o soluție completă. Acest lucru se
întâmplă când permutarea este completă – am dat o valoare corectă ultimului
element al tabloului, x[n], adică atunci când k=n;
 void back(int k); – apelul acestei funcții dă valori posibile
elementului x[k] al vectorului soluție și le verifică:
o se parcurg valorile pe care le pot lua elementele vectorului, conform
condițiilor externe (în acest caz, 1..n);
 se memorează în x[k] valoarea curentă;
 dacă valoarea lui x[k] este corectă, conform condițiilor interne, se
verifică dacă avem o soluție completă. În caz afirmativ se afișează
această soluție, în caz contrar se trece la următorul element, prin
apelul recursiv;
o la finalul parcurgerii, se revine la elementul anterior al vectorului x, prin
revenirea din apelul recursiv.

Observații

 generarea valorilor din vectorul soluție începe cu primul element al


acestuia, x[ 1 ]; în consecință, apelul principal al
funcției back() este back(1);
 generarea permutărilor în ordine lexicografică se obține datorită faptului că, în
funcția back() valorile posibile pe care le primește x[k] sunt parcurse în ordine
crescătoare (for(int i=1 ; i<=n ; i++)....). Dacă am fi parcurs valorile
de la n la 1, s-ar fi generat permutările în ordine invers lexicografică;
 algoritmul este exponențial și poate fi folosit numai pentru valori mici ale lui n. O
soluție ceva mai bună se poate obține dacă, pentru a stabili corectitudinea
condițiilor interne, evităm parcurgerea elementelor deja memorate în vectorul
soluție. Acest lucru poate fi realizat prin intermediul unui vector caracteristic, cu
semnificația: p[v] = 1 dacă valoarea p face deja parte din permutare,
și p[v]=0 dacă p nu face parte din permutare.

O variantă (puțin) mai bună


Algoritmul de generarea a permutărilor este unul exponențial, deci lent. Totuși, poate fi
ușor îmbunățit în ceea ce privește verificarea condițiilor interne. Acestea cer ca
valoarea curentă a lui x[k] (elementul care se generează) să nu se repete. În varianta
anterioară am parcurs elementele care îl preced și le-am comparat cu x[k].

Această parcurgere poate fi evitată dacă folosim un vector caracteristic, p[], cu


următorul înțeles:
p[v]={ 1 dacă valoarea v a fost plasată deja în vectorul soluție,
0 dacă valoarea v nu a fost plasată încă în vectorul soluție

Următorul program foloseste această idee, verificarea condițiilor interne și a celor de


existență a soluției făcându-se în în funcția back(), fără a mai scrie funcții de sine
stătătoare:

#include <iostream>
using namespace std;

int x[10] , n , p[10];


void Afis(int k)
{
for(int j = 1 ; j <= k ; j ++)
cout << x[j] << " ";
cout << endl;
}
void back(int k){
for(int i = 1 ; i <= n ; i++)
if(p[i] == 0)
{
x[k] = i;
p[i] = 1;
if(k == n)
Afis(k);
else
back(k + 1);
p[i] = 0;
}
}
int main(){
cin >> n;
back(1);
return 0;
}

Permutari, aranjamente, ORDINEA CONTEAZA


Combinari, submultimile unei multimi NU CONTEAZA ORDINEA (VALORILE DIN SOLUTII
TREBUIE SA FIE IN ORDINE STRICT CRESCATOARE SAU DESCRESCATOARE )

COMBINARI
4 2 => C(4,2)=4!/(2!*2!)=6
12 13 14 23 24 34
#include <iostream>
using namespace std;
int x[10] , n ,cnt=0,k;
void Afis()
{ cnt++;
for(int j = 1 ; j <= k ; j ++)
cout << x[j] << " ";
cout << endl;
}
void back(int pas){
for(int i = x[pas-1]+1 ; i <= n ; ++ i)
{
x[pas] = i;
if(pas < k)
back(pas + 1);
else
Afis();
}
}
int main(){
cin >> n>>k;
back(1);cout<<cnt<<endl;
return 0;
}

ARANJAMENTE
4 2 => A(4,2)=4!/2!=12
12 13 14 21 23 24
31 32 34 41 42 43

#include <iostream>
using namespace std;
int x[10] , n ,k,p[10];
void Afis()
{ for(int j = 1 ; j <= k ; j ++)
cout << x[j] << " ";
cout << endl;
}
void back(int pas){
for(int i = 1 ; i <= n ; ++ i)
{ if(p[i]==0)
{ p[i]=1;
x[pas] = i;
if(pas < k)
back(pas + 1);
else // pas==k
Afis();
p[i]=0;
}
}
int main(){
cin >> n>>k;
back(1);
return 0;
}
Probleme.
1. Se citesc 2 nr naturale n si m . Sa se afiseze in ordine lexicografica
toate submultimile de m elem din mult {1,2,....n} in care nu exista
doua elemnte pare alaturate.
#include <iostream>
using namespace std;
int x[10] , n ,k;

void Afis()
{ for(int i = 1 ; i <= k ; i ++)
cout << x[i] << " ";
cout << endl;
}

void back(int pas){


for(int i = x[pas-1]+1 ; i <= n; i++)
{ if(x[pas-1]%2!=0 || i%2!=0)
{
x[pas] = i;
if(pas < k)
back(pas + 1);
else
Afis();

}}
}
int main(){
cin >> n>>k;
for(int i=1;i<=n;++i)
x[1]=i, back(2);
return 0;
}

2. Toate Submultimile unei multimi {1,2..n}

ex 1 2 3
 1
 12
 123
 13
 2
 23
 3

#include <iostream>
using namespace std;
int x[10] , n;

void Afis(int l)
{ for(int i = 1 ; i <= l ; i ++)
cout << x[i] << " ";
cout << endl;
}

void back(int k){


for(int i = x[k-1]+1 ; i <= n; i++)
{ x[k] = i;
Afis(k);
if(k < n)
back(k + 1);

}
}

int main(){
cin >> n;
back(1);
return 0;
}

S-ar putea să vă placă și