Sunteți pe pagina 1din 20

Metoda “ Divide et Impera”

Introducere

Expresia “Divide et Impera” provine din limba latina si a


constituit unul dintre principiile de guvernare ale imparatului
roman Iulius Caesar. In traducere inseamna „dezbina si
stapaneste” si exprima un adevar, din pacate valabil si in ziua de
astazi: o masa de oameni poate fi mai usor stapanita, atunci cand
este dezbinata!

Ceea ce este trist pentru o societate, poate fi benefic atunci


cand un informatician vrea sa rezolve o problema, asa cum se va
vedea in continuare. Numele acestei tehnici de programare arata
foarte clar in ce consta ea: prin analogie cu exemplul din istorie, o
problema poate fi mai usor „stapanita” si astfel rezolvata, daca este
„despicata” in mai multe parti.
Principiul metodei

In programare, aplicarea acestui principiu se face astfel:

• Se descompune problema data in doua sau mai multe


probleme de aceeasi natura. Acestea pot fi de doua tipuri:
elementare sau ne-elementare.

• Cele elementare se rezolva direct, iar cele ne-elementare se


descompun in continuare in alte subprobleme elementare sin
e-elementare. Procesul de descompunere continua pana cand
s-au obtinut numai probleme elementare.

• Solutia problemei initiale se obtine prin reconstituirea si


recombinarea solutiilor subproblemelor, in ordine inversa.
Datorita acestui principiu de functionare, algoritmii Divide et
Impera au un caracter recursiv.

Algoritmii Divide et Impera sunt in general rapizi, deoarece


prin descompunere, de cele mai multe ori, se obtin probleme
pentru care rezolvarea si combinarea solutiilor au un grad de
complexitate mai mic decat problema initiala.

Algoritmii Divide et Impera se implementeaza, de obicei,


intr-un subprogram recursiv.
Modelul metodei

Cel mai potrivit model pentru explicarea metodei il constituie


prelucrarea unui vector (sir) de forma fara: (x0,x1,…,xn-1).

Aplicarea metodei consta in impartirea sirului dat in doua


subsiruri (x0,…xm), (xm+1,…,xn-1) si prelucrarea fiecaruia dintre
ele. Astfel, am descompus problema in doua subprobleme de
acelasi tip.

In continuare, pentru fiecare dintre subsirurile obtinute sunt


posibile doua situatii:

 Se poate prelucra direct;


 Nu poate fi prelucrat direct, caz in care subsirul trebuie
descompus in acelasi fel.

Lantul de descompuneri va continua pana cand obtinem


numai subsiruri prelucrabile direct.
Putin mai general, putem modela procedeul considerand ca la
fiecare pas al algoritmului va fi analizat un subsir de forma (xp,…
xq), care contine elementele sirului situate intre indicii p si q.
Presupunem ca oricare ar fi p,q ∈ {0,1,…n-1}, exista m ∈ {p,
…,q} asa incat prelucrarea subsirului {xp,…,xq} sa se faca
prelucrand subsirurile (xp,…,xm) si (xm+1,…,xq).
Este natural sa consideram ca prelucrarea subsirului (xp,…xq)
se face printr-o functie recursive, notata generic Div_Imp (int p, int
q). O structura posibila a acesteia ar fi:

 Daca subsirul (xp,…xq) poate fi prelucrat direct, atunci


il prelucram;
 In caz contrar:
 Impartim subsirul (xp,…xq) in alte 2 subsiruri: (xp,
…xm) si (xm+1,…xq)
 Prelucram subsirurile obtinute: intrucat subsirul
(xp,…xm) trebuie descompus mai departe in
aceeasi maniera, rezulta ca prelucrarea acestuia se
va face printr-un auto-apel recursive de forma
Div_Imp (p,m); analog, sirul (xm+1,…xq) se va
prelucra prin auto-apelul Div_Imp (m+1,q)

Observatii:

 De obicei elementul xm care determina partitionarea


subsirului (xp,…xq), este cel aflat pe pozitia de mijloc:
 p +q
m=
 2 
 (“parte intreaga din (p+q)/2”).
Astfel, subsirurile obtinute vor avea dimensiuni apropiate si
in consecinta subproblemele determinate de catre aceste
subsiruri vor fi apropiate din punct de vedere al complexitatii.
 In procesul de descompunere, subsirurile pot fi alese si altfel:
de exemplu, (xp,…xm-1) si (xm+1,…xq), adica elementul de
referinta xm nu a fost inclus in nici unul dintre subsiruri;
 In general, un subsir poate fi prelucrat direct daca nu are nici
un element sau contine un singur element.
Aplicatii

1.Maximul dintr-un sir

Sa se determine elementul maxim dintr-un sir de n numere


intregi, folosind metoda Divide et Impera.

Reprezentare

Urmam modelul general si consideram va valorile sunt


memorate intr-un vector (x[0],x[1],…,x[n-1]), Descompunem
problema in doua subprobleme de acelasi tip, impartind vectorul
in doua “jumatati de vector”. Primul subvector va contine
elemente aflate pana in pozitia de mijloc inclusive, iar al doilea
va fi alcatuit din elemente de dupa pozitia de mijloc.
Determinam elementul maxim al fiecaruia dintre cei doi
subvectori, apoi maximul intregului vector va fi cea mai mare
dintre cele doua valori maxime.

Daca primul subvector nu s-a redus la un singur element, el


trebuie descompus la randu-I in doua jumatati; la fel si al doilea
subvector. Procedeul continua pana cand obtinem numai
subvectori cu un element. Maximul unui subvector cu un singur
element este, evident, chiar elementul respectiv. Un vector cu un
singur element va fi considerat problema elementara.

Analizam procedeul mai in profunzime considerand ca la un


anumit pas prelucram subvectorul (x[p]…x[q]), ce contine
elementele cuprinse intre pozitiile p si q. Pornim de la vectorul
dat (x[0]…x[n-1]), deci initial “capetele” intervalului sunt p=0
si q=n-1. Pozitia de mijloc care determina subvectorii
 p +q
descompunerii este mij = 
 2 
(“partea intreaga din (p+q)/2”),
iar subvectorii vor fi x1=(x[p]…x[mij]) si x2=(x[mij+1]…x[q]).
Exemplificam pentru vectorul x=(-1,2,8,-3,1,9,6), avand n=7
elemente. Procesul de descompunere incepe cu vectorul initial :

X: p=0 1 2 3 4 5 q=6
-1 2 8 -3 1 9 6
 p +q
mij = 
 2 
= [(0+6)/2]=3

(x1) cuprins intre pozitiile p si mij, iar mij devine “noul q”.

p=0 1 3 q=3
-1 2 8 -3

(x2) cuprins intre pozitiile mij +1 si q, iar mij+1 devine “noul


p”.

p=4 5 q=6
1 9 6

Fiecare vector se descompune la randul sau, in mod similar:

Pentru (x1), mij=[(0+3)/2]=1, si obtinem subvectorii:

(x11): p=0 q=1 (x12): p=2 q=3


-1 2 8 -3

Pentru (x2), mij=6 si obtinem subvectorii:

(x21): p=4 q=5


1 9
(x22): p=q=6
6
In sfarsit, urmatoarele descompuneri ale vectorilor conduc la
probleme elementare:

(x11) ⇒ (x111): -1 si (x112): 2


(x12) ⇒ (x121): 8 si (x122): -3
(x21) ⇒ (x211): 1 si (x212): 9
(x22) este elementar: 6

Determinarea maximului se face combinand problemele, astfel:


 Valoarea maxima a lui x11 este maximul dintre x111 si
x112, adica 2;
 Valoarea maxima a lui x12 este maximul dintre x121 si
x122, adica 8;
 Valoarea maxima a lui x21 este maximul dintre x211 si
x212, adica 9;
 Valoarea maxina a lui x22, care este elementar este 6.

Si in continuare:
 Maximul dintre x11 si x12 este 8, adica maximul lui x1 este
8.
 Maximul dintre x21 si x22 este 9. adica maximul lui x2 este
9.

In sfarsit, maximul lui x se obtine din maximul lui x1 si x2, adica


9.

Rezolvare

Definim o functie recursive {int div_imp (int p, int q)} care


returneaza valoarea maxima a subvectorului (x[p], x[p+1],…
x[p]).
 Daca subvectorul contine un singur element , adica p=q,
atunci acel element va fi maximul si va fi returnat de catre
functiile: {return x[p];}
 In caz contrar:
- determinam pozitia de mijloc {mij=(p+q)/2;};
- determinam valoarea maxima a vectorului {x[p],…,x[mij]};
pentru aceasta, se va relua acelasi algoritm cu singura
deosebire ca in loc de q vom avea mij, (adica mij devine
“noul q”). Prin urmare, are loc auto-apelul div_imp(p, mij),
memorandu-se valoarea intoarsa in variabila max1:
{max1=div_imp(p,mij);};
- analog, maximul din subvectorul (x[mij+1],…,x[q]) se obtine
prin apelul recursive max2=div_imp(mij+1,q);
- re-combinam cele doua probleme: maximul din tot sirul va fi
cea mai mare dintre cele doua valori maxime max1 si max2
aferente subvectorilor. Totul se reduce la o simpla
instructiune if-else:

if (max1>max2)
return max1;
else
return max2;

In functia principala, citim numarul de elemente, n precum si


elementele vectorilor, apoi apelam div_imp(0,n-1), ceea ce
reprezinta, desigur, determinarea valorii maxime a vectorului
(x[0],…,x[n-1]):

Cout << ”\n Valoarea maxima este: “ << div_imp(0,n-1);

Programul complet este prezentat in continuare:


#include <iostream.h>
#include<conio.h>
int x[20],n,i;
int div_imp (int p, int q)
{
int mij, max1, max2;
if (p= =q)
return x[p];
else
{
mij=(p+q/2);
max1=div_imp(p,mij);
max2=div_imp(mij+1,q);
if (max1>max2)
return max1;
else
return max2;
}
}
void main ()
{
cout<< “\n n=”;
cin >>n;
for(i=0;i<=n-1;i++)
{
cout << “x[“<<i<<”]=”;
cin >>x[i];
}
cout <<”\n Valoarea maxima este:” << div_imp(0,n-1);
getch();
}

2.Cautarea binara intr-un sir

Sa se verifice daca o valoare data a exista intr-un sir de


numere ordonat crescator. Se va folosi un algoritm bazat pe
metoda Divide et Impera.
Reprezentare

Presupunem ca sirul este memorat intr-un vector (x[0], x[1],


…,x[n-1]), unde n este numarul de elemente. Descompunem
problema in trei subprobleme de acelasi tip:

- o problema reprezentata de o singura valoare, si anume aceea


 p + q
din mijloc: x[mij], unde mij este pozitia de mijloc, mij = 
 2 
- Doua probleme reprezentate in subvectorul stang, respective
drept: (x[0],x[1],…,x[mij-1]) si (x[mij+1],…,x[n-1]).

Implementarea propriu-zisa va analiza problemele in aceasta


ordine. Se observa ca am sugerat aplicarea unui algoritm Divide
et Impera in care prima problema este elementara, iar
urmatoarele doua sunt de acelasi tip cu cea initiala. Deoarece
problema noastra este o problema de cautare putem descrie
complet algoritmul astfel:

- Daca valoarea cautata se afla pe pozitia de mijloc (a=x[mij]),


atunci algoritmul se incheie;
- Daca valoarea cautata este mai mica decat elementul de pe
pozitia de mijloc (a<x[mij]), atunci cautarea continua in
subvectorul stang (x[0],x[1],…,x[mij-1]), deoarece toate
elementele mai mici decat cel din mijloc se gasesc in stanga
acestuia (vectorul este sortat crescator!);
- Daca valoarea cautata este mai mare decat elementul de pe
pozitia de mijloc (a>x[mij]), atunci cautarea continua in
subvectorul drept (x[mij+1],…,x[n-1]);
- Cautarile in fiecare dintre cei doi subvectori se fac prin acelasi
procedeu de descompunere a problemei, pana cand gasim
valoarea cautata pe o pozitie de mijloc, sau pana cand ajungem
la un subvector care nu contine nici un element.
Consideram vectorul ordonat crescator x=(-6,-5,-1,5,8,9, cu
n=6 elemente), in care cautam valoarea a=8. Notam cu p si q
pozitiile capetelor subvectorului pe care-l analizam la fiecare
pas al algoritmului; initial p=1 si q=n=6

p=0 1 2 3 4 q=5
x: -6 -5 -1 5 8 9

 p +q
mij : =  ⇒ mij = 2
 2 

Problema se descompune in urmatoarele subprobleme:


subvectorul (x1) cuprins intre pozitiile p si mij-1, subvectorul cu
un singur element x[mij], si subvectorul (x2), cuprins intre
pozitiile mij+1 si q.

p=0 1 x[2] p=3 4 q=5


(x1): -6 -5 -1 (x2): 5 8 9

Cautarea continua in (x2) deoarece a>[mij] (8>-1), deci p=3,q=5


iar mij=4. Descompunerea lui (x2) se face la fel ca la pasul
anterior.

p=q=3 x[4]
(x21) 5 8

p=q=5
(x22) 9
Sa analizam problema elementara: x[mij]=a, adica x[4]=8,
moment in care algoritmul se incheie deoarece valoarea cautata
a fost gasita!

Presupunem acum ca valoarea cautata ar fi a=7. Algoritmul ar fi


identic cu cel de sus pana la obtinerea subproblemelor date de
elementul x[4] si subvectorii (x21),(x22). Mai departe insa,
problema elementara “x[mij]” nu mai furnizeaza solutia.
Intrucat a<x[mij] (7<8), trebuie continuat cu descompunerea
subvectorului (x21). Avem:

 p + q
p=q=3, mij = 
 2 
= [(3 + 3) / 2] = 3 ; primul subvector ar fi cuprins
intre pozitiile p=3 si q=mij-1=2, iar al doilea intre pozitiile
p=mij+1=4 si q=3. Acesti noi subvectori nu se mai pot
descompune in continuare, pentru ca nu sunt “valizi”. Am ajuns
la subvectori care nu contin nici un element, si se observa cu
usurinta contidia care defineste aceasta situatie: q<p!

Rezolvare

Consideram o functie recursive {int div_imp (intp, intq)}


care returneaza : 0 daca valoarea a nu exista in subvectorul
(x[p],…,x[q]), respective 1 daca aceasta exista in subvector.

La un anumit pas al algoritmului recursive, cautam valoarea a


intr-un subvector (x[p],…,x[q]) cuprins intre pozitiile p si q.

• Daca subvectorul nu contine nici un element (q<p, asa


cum am aratat mai sus), atunci abandonam cautarea,
functia returnand valoarea 0:
if (q<p) return 0;
• In caz contrar: determinam pozitia de mijloc:
{mij=(p+q)/2;}. Apoi analizam cele trei subprobleme, in
ordine:
 Verificam daca valoarea a coincide cu x[mij]; in caz
afirmativ, cautarea se opreste; {if (x[maj]= = a)
return 1;}
 In caz contrar:
- Daca a este mai mic decat x[mij], atunci valoarea a trebuie
cautata in subvectorul din stanga (x[p],…,x[mij-1); avand in
vedere ca algoritmul va fi acelasi cu singura deosebire ca in
loc de q vom avea mij-1, rezulta ca este sufficient sa apelam
div_imp (p,mij-1);
- Daca a este mai mare decat x[mij], atunci valoarea a trebuie
cautata in subvectorul din dreapta (x[mij+1],…x[q]); pentru
aceasta apelam div_imp(mij+1,q):
if (a<x[mij])
return div_imp (p,mij-1);
else
return div_imp(mij+1,q);

In functia principala citim n si apoi elementele vectorului x, care


trebuie introduce de la inceput in ordine crescatoare. Fiecare
element x[i] incepand cu al doilea, trebuie sa fie mai mare decat
elementul anterior x[i-1]. Vom citi separate primul element x[0],
apoi pe celalalte intr-un ciclu in care parcurgem pozitiile i=1,2,
…,n-1 si pentru fiecare valoare a lui i, citim elementul x[i] atata
timp cat valoarea introdusa este mai mica decat elementul
precedent x[i-1].

cut << “\n n=”; cin n;


cut <<”\n x[0]=”; cin >>x[0];
for (i=1;i<=n-1;i++)
do
{
cout << “x[“<<i<<”]=”; cin >> x[i];
}
while (x[i]<=x[i-1]);
De exemplu: pentru i=1, intra in ciclul do-while, face o prima
citire a lui x[1], dupa care in linia while verifica daca x[i]<=x[i-
1], adica daca x[1]<=x[0] (valoarea lui x[0] a fost citita anterior,
inaintea ciclului!). In caz afirmativ, se va relua ciclul do-while
cu o noua citire a lui x[1]. Doar atunci cand introducem o
valoare pentru x[1] mai mare decat x[0], va iesi din ciclul do-
while (conditia din linia while este falsa, fiind asigurata ordinea
crescatoare) si va trece la urmatorul pas al ciclului for, cu citirea
urmatorului element (i=2).

In continuare citim valoarea a care va fi cautata in vector.


Cautarea incepe cu vectorul dat (x[0],…,x[n-1]), deci initial
p=0, q=n-1, primul apel fiind div_imp (0,n-1), cu afisarea
valorii returnate.

#include<iostream.h>
int x[20],n,a,i;
int div_imp (int p, int q)
{
int mij;
if (q<p)
return 0;
else
{
mij=(p+q)/2;
if(x[mij]= =a)
return 1;
else
if(a<x[mij])
return div_imp (p, mij-1);
else
return div_imp (mij+1,q);
}
}
void main()
{
cout <<”\n n=”; cin n;
cout <<”\n x[0]=”; cin >> x[0];
for(i=1;i<=n-1;i++)
do
{
cout << “x[“<<i<<”]=”; cin >> x[i];
} while (x[i]<=x[i-1]);
cout << “\n a=”; cin >>a;
if(div_imp (o,n-1))
cout <<”\n Valoarea cautata exista in vector”;
else
cout <<”\n Valoarea cautata nu exista in vector”;
}

3.Turnurile din Hanoi

Problema cunoscuta sub acest nume este urmatoarea: se dau


trei tije, simbolizate prin X,Y,Z si se dau n discuri de diameter
diferite stivuite pe tija X, in ordinea descrescatoare a
diametrelor, formand astfel un “turn”. Se cere sa se mute cele n
discuri pe tija Y, folosind tija intermediara Z, si respectand
urmatoarele reguli:
-in fiecare miscare se muta un singur disc;
-un disc nu poate fi asezat peste un diametru mai mic.

Reprezentare

Simbolizam prin XY, operatia de mutare de pe tija X pe tija


Y. Pentru inceput sa reprezentam solutia problemei pentru valori
mici ale lui n.
Pentru n=1, se muta unicul disc de pe tija X pe tija Y (mutarea
XY).
Pentru n=2:
Notam cu D1 discul de la baza tijei X si cu D2 discul aflat
deasupra lui D1. Conform ipotezei, diametrul lui D2 este mai mic
decat diametrul lui D1. Nu putem sa mutam direct discul D2 si
apoi discul D1 pe tija Y (am pune astfel discul D1 peste discul D2
de diametru mai mic). Trebuie sa folosim tija intermediara Z,
facand urmatoarele mutari:
-discul D2 de pe tija X, pe tija intermediara Z (mutarea XZ);
-discul D1 de pe tija X, pe tija Y (XY);
-discul D2 de pe tija Z, pe tija Y (ZY).

Sa observam ca, in generall, adica pentru n>=2, procedam


astfel:
-mutam n-1 discuri de pe tija X pe tija Z, utilizand Y ca tija
intermediara;
-mutam pe tija Y, unicul disc ramas pe tija X;
-mutam cele n-1 discuri de pe tija Z pe tija Y, utilizand acum ca
tija intermediara pe cea care a ramas goala, adica X.
In realitate, prin cazul general am descris un procedeu
specific metodei Divide et Impera, adica nu am facut nimic altceva
decat sa descompunem problema initiala in trei subprobleme mai
simple, de aceeasi natura ca cea initiala. Ordinea de rezolvare a
acestora este esentiala, tinand cont de chiar enuntul problemei date,
asa cum am vazut anterior.
Prima problema este neelementara si comporta mutarea a n-1
discuri, a doua este elementara fiind vorba de mutarea unui singur
disc, iar a treia este iarasii neelementara implicand mutarea a n-1
discuri.
Daca notam symbolic prin H(n,X,Y,Z) operatia de mutare a n
discuri de pe tija X pe tija Y folosind tija intermediara Z, aceasta
operatie ar putea fi descrisa recursive astfel:

 X ,Yd a nc = a1
H ( n, X , Y , Z ) = 
 H (n − 1, X , Z ,Y ) ,X ,YH (n − 1, Z ,Y , X ) d, a nc > a1
Rezolvare

Definim o functie HANOI(n,a,b,c), care muta n discuri de pe


X pe Y, folosind tija de manevra Z. Aceasta functie va lucra astfel:
• Daca n=1, muta unicul disc de pe tija X pe tija Y, afisand
mutarea: cout <<”\n”<<X<<Y;
• In caz contrar:
-se auto-apeleaza functia sub forma HANOI(n-1,X,Y,Z) pentru
a muta n-1 discuri de pe tija X pe tija Z, folosind Y ca manevra;
-se tipareste mutarea XY a discului ramas, de pe X pe Y;
Cout <<”\n”<<X<<Y;
-se auto-apeleaza HANOI(n-1,X,Y,Z) pentru a muta n-1 discuri
de pe tija Z pe tija Y, folosind X ca tija de manevra.

In functia principala: citim numarul de discuri n. Pentru afisarea


mutarilor, vom asocial tijele X,Y,Z variabile de tip caracter care
contin litere de desemneaza tija X=’X’;Y=’Y’;Z=’Z’; Cu aceste
notatii, programul apeleaza functia in forma HANOI(n,X,Y,Z).

Programul complet este prezentat in continuare:


#include<iostream.h>
#include<conio.h>
#include<stdio.h>
char X,Y,Z;
int n;
void HANOI(int n, char X, char Y, char Z)
{
if (n= =1)
cout <<”\n”<<X<<Y;
else
{
HANOI(n-1,X,Z,Y);
cout <<”\n”<<X<<Y;
HANOI(n-1,Z,Y,X);
}
}
void main()
{
cout <<”\n Numarul de discuri n=”; cin >>n;
HANOI(n-1,’X’,’Y’,’Z’);
getch();
}

Probleme suplimentare
1. Consideram un sir de n numere intregi, ordonat crescator si
un numar intreg x. Scrieti un algoritm care imparte sirul dat
in doua subsiruri in asa fel incat toate elementele primului
subsir sa fie mai mici sau cel mult egale cu x, iar toate
elementele celui de-al doilea subsir sa fie strict mai mari
decat x.
2. Se citeste de la tastatura un sir cu n elemente numere intregi.
Sa se gaseasca elementul aflat pe o pozitie data k in sirul
ordonat crescator, fara a efectua ordonarea.
3. Folosind Divide et Impera, sa se calculeze n!, unde valoarea
lui n (1<=n<=20) se citeste de la intrare.
4. Cunoscandu-se numarul n al elevilor unei clase, precum si
mediile generale ale celor n elevi la finele unui an scolar,
realizati un program care, folosind un algoritm Divide et
Impera, testeaza daca in clasa respective exista doi elevi cu
aceeasi medie generala. Programul va tipari pe ecran mesajul
“da” sau “nu”, in functie de situatie.