Sunteți pe pagina 1din 56

Cuprins

Lista tabele ........................................................................................................................ 4


Lista figuri......................................................................................................................... 4
Prefaţă ............................................................................................................................... 5
Lucrarea de laborator nr.1. Tablouri ................................................................................. 7
1.1.Tablouri unidimensionale .................................................................................... 7
1.2.Tablouri bidimensionale ...................................................................................... 8
Lucrarea de laborator nr.2. Funcţii definite de utilizatori ............................................... 12
2.1.Apelul subprogramelor ...................................................................................... 13
2.2.Transmiterea parametrilor ................................................................................. 14
2.2.1.Transmiterea parametrilor prin valoare ..................................................... 14
2.2.2.Transmiterea parametrilor prin referinţă ................................................... 15
Lucrarea de laborator nr.3. Căutarea şi sortare ............................................................... 18
3.1.Căutarea............................................................................................................. 18
3.1.1.Căutare secvenţială ................................................................................... 18
3.1.2.Căutare binară ........................................................................................... 19
3.2.Sortare ............................................................................................................... 20
3.2.1.Sortarea prin selecţie directă ..................................................................... 20
3.2.2.Sortarea prin metoda bulelor – Geniul rău al permutării........................... 21
3.2.3.Sortarea prin inserare ................................................................................ 22
Lucrarea de laborator nr.4. Metoda Greedy folosind tabele de verificare ...................... 23
Lucrarea de laborator nr.5. Metoda Greedy folosind schema logică .............................. 27
Evaluare cunoştinţe I....................................................................................................... 32
Lucrarea de laborator nr.6. Recursivitatea ...................................................................... 33
Lucrarea de laborator nr.7. Divide et impera .................................................................. 39
Lucrarea de laborator nr.8. Backtracking liniar de lungime fixă .................................... 41
Lucrarea de laborator nr.9. Backtracking în plan............................................................ 45
Lucrarea de laborator nr.10. Backtracking recursiv ........................................................ 49

3
Evaluare cunoştinţe II ..................................................................................................... 51
Lucrarea de laborator nr.11. Algoritmi genetici I ........................................................... 52
Lucrarea de laborator nr.12. Algoritmi genetici II .......................................................... 55
Bibliografie ..................................................................................................................... 58

Lista tabele

Tabel 1. Reprezentare grafică a valorilor unei matrici 3x5 ............................................... 9


Tabel 2. Comparaţie transmitere parametrii prin valoare şi referinţă ............................. 16
Tabel 3. Evoluţie variabile problema „Bancomatului” pentru val_extras =
231 - tabel de verificare .................................................................................................. 25
Tabel 4. Stiva aferentă cuvântului "vara"........................................................................ 37

Lista figuri

Figura 1. Schema logică algoritm Rucsacul .................................................................... 28


Figura 2. Blocul aferent ordonării elementelor ............................................................... 29
Figura 3. Blocul aferent algoritmului Greedy ................................................................. 30
Figura 4. Arbore de căutare............................................................................................. 42
Figura 5. Arborele de căutare pentru 4 regine................................................................. 43
Figura 6. Posibilităţi de mutare a calului pe tabla de şah ................................................ 45
Figura 7. Etapele algoritmilor genetici............................................................................ 52
Figura 8. Structura unui cromozon.................................................................................. 53
Figura 9. Cromozomul înainte de mutaţie....................................................................... 56
Figura 10. Cromozomul după mutaţie ............................................................................ 56
Figura 11. Cromozomii înainte de încrucişare ................................................................ 56
Figura 12. Cromozomii după încrucişare ........................................................................ 56

4
Prefaţă

Atât în matematică, cât şi în informatică, un algortim reprezintă o metodă sau o


procedură alcătuită dintr-o succesiune de paşi care ajută la rezolvarea unei unei clase de
probleme. Conceptul de algoritm a există de secole, utilizarea lui putând fi atribuit
matematicienilor greci, reprezentaţi prin Eratostene (prin ciurul lui Eratostene- un
algoritm simplu şi vechi de descoperire a numerelor prime până la un număr întreg
specificat) şi Euclid (prin algoritmul lui Euclid- considerat cel mai simplu algoritm utilizat
în determinarea celui mai mare divizor comun a două numere).

Totuşi, termenul de algoritm derivă din numele unui matematician din secolul IX,
numit Muḥammad ibn Mūsā al'Khwārizmī (prescurtat Al-Horezmi, cu numele latinizat de
„Algoritmi”), personaj considerat un părinte al Algebrei.

Îndrumarul de laborator prezintă o serie de metode şi tehnici arhicunoscute


utilizate în programare şi este destinat studenţilor de la specializarea Informatică
Economică, anul II, dar altor persoane ce ar dori să descopere din secretele acestor tehnici.
Pentru o parcurgere uşoară şi înţelegere a acestor metode, se recomandă cunoaşterea
elementelor de bază din cadrul unui limbaj de programare (în cazul nostru vom folosi C
sau C++).

Deoarece unii dintre studenţii prezenţi în cadrul unei astfel de specializări nu


posedă cunoştinţe avansate de programare, primele laboratoare vor fi recapitulative,
primul algoritm fiind descris tocmai în laboratorul nr.3. În cadrul laboratoarelor 4 şi 5 am
prezentat tehnica Greedy, folosind două instrumente ajutătoare în înţelegerea algoritmilor:
tabele de verificare şi schema logică. Începând cu laboratorul 6, vom prezenta
recursivitatea, divide et impera şi backtrackingul, în timp ce în cadrul laboratoarelor 11 şi
12 vom avea descrişi algoritmii genetici, larg utilizaţi în rezolvarea anumitor probleme
pentru care metodele clasice nu ar putea să ofere rezultate satisfăcătoare.

5
În cadrul acestor laboratoare se observă prezenţa a unui număr ridicat de exemple
(cod C++, pseudocod), dar şi exerciţii. Se recomandă parcurgerea exemplelor din cadrul
laboratoarelor (pentru o înţelegere mai bună a tehnicilor), după care se poate trece la
rezolvarea de exerciţii. În cadrul anumitor lucrări de laborator, anumite exerciţii vor fi
continuate, individual, acasă, urmând să fie dezbătute în cadrul laboratorului viitor.

Printre lucrările de laborator vor exista şi două testări, aceste părţi componente
putând ajuta, după o dezbatere ulterioară, la fixarea anumitor cunoştinţe.

Observaţiile şi propunerile existente la sfârşitul fiecărei aplicaţii ajută viitorul


specialist în informatică să înţeleagă pe deplin soluţia prezentată, având un punct de
pornire solid pentru a concepe modificări ale metodelor conform noilor situaţii ce pot să
apară.

6
Lucrarea de laborator nr.1. Tablouri

Un tablou este un ansamblu de variabile de acelaşi tip la care se face referire


folosindu-se un acelaşi nume. Un anume element dintr-un tablou este indicat prin
intermediul unui indice (index). În cele mai multe cazuri, tablourile sunt alcătuite din
locaţii de memorie învecinate. Adresa de memorie cea mai mică corespunde primului
element, iar adresa cea mai mare corespunde ultimului element. Tablourile pot avea de la
una la mai multe dimensiuni.

1.1.Tablouri unidimensionale

Forma generală de declarare a unui tablou unidimensional este:

tip variabila_nume[dimensiune]

Ca şi alte variabile, tablourile trebuie declarate în mod explicit, pentru că în acest


mod se alocă spaţiu în memorie. Aici, tip declară tipul de bază al tabloului, care este tipul
fiecărui element inclus în tablou, iar dimensiune defineşte numărul de elemente conţinute
în tablou. De exemplu, pentru a declara un tablou de 100 de elemente denumit vector,
toate elementele tabloului fiind de tip double, se va folosi următoarea instrucţiune:

double vector[100];

În cadrul tuturor laboratoarelor ce urmează vom considera indicele primului


element din orice tablou fiind 0. Ca atare, în momentul în care avem:

int in[100];

Am declarat un tablou de numere întregi cu 100 de elemente, de la in[0] la in[99].

7
Exemplu iniţializare tablou:

void main(){
int x[100];//declarare tablou 100 elemente
int t;
for(t=0;t<100;t++)
x[t]=t;// ce se stochează în tablou?
}

1.2.Tablouri bidimensionale

O mare parte a limbajelor de programare acceptă tablouri multidimensionale.


Forma cea mai simplă a unui tablou multidimensional este tabloul bidimensional. În
esenţă, un tablou bidimensional este un tablou de tablouri unidimensionale. Pentru a
declara un tablou de întregi tabl, de dimensiune 5 x 10, se va scrie instrucţiunea:

int tabl[5][10];

În mod analog, pentru a accesa punctul de coordonate 1,3 din tabloul declarat mai
sus, vom folosi instrucţiunea:

tabl[1][3];

Exemplul următor încarcă în memorie un tablou bidimensional, care cuprinde


numere de la 1 la 15 şi le scrie pe rânduri succesive.

void main(){
int num[3][5];
for(int i=0;i<3;i++)
for(int j=0;j<5;j++)
num[i][j]=(i*5)+j+1;
//afişarea
for(int i=0;i<3;i++){
for(int j=0;j<5;j++){
cout<<num[i][j];
}
cout<<”\n”;
}
}

8
În acest exemplu, elementul num[0][0] are valoarea 1, elementul num[0][1] are
valoarea 2, elementul num[0][2] are valoarea 3, etc. Valoarea elementului num[2][4] va fi
(2*5)+4+1, adică 15. Tabloul num apare după cum se vede mai jos:

Tabel 1. Reprezentare grafică a valorilor unei matrici 3x5

num 0 1 2 3 4
0 1 2 3 4 5
1 6 7 8 9 10
2 11 12 13 14 15

Tablourile bidimensionale sunt stocate într-o matrice al cărei prim indice arată
rândul, iar al doilea indică coloana. Aceasta înseamnă că indicele din dreapta se modifică
mai repede decât cel din stânga la accesarea elementelor din tablou în ordinea în care
acestea se află stocate în memorie.

Pentru a înţelege mai bine tablourile vă prezentăm nişte exemple şi vă propunem


să rezolvaţi nişte exerciţii.

Exemple:

1. Se citeşte de la tastatură un număr natural n (2<n<16). Să se construiască şi să


se afişeze pe ecran o matrice cu n linii şi n coloane, în care elementele de pe
cele două diagonale sunt egale cu 4, iar restul elementelor sunt egale cu 3.
Exemplu: pentru n=5 se va afişa matricea:
43334
34343
33433
34343
43334

9
#include <iostream>
using namespace std;
int main(){
int n, a[10][10],i ,j;
cout<<" dati n:";
cin>>n;
for(i=0; i<n; i++){// coloana principală şi secundară
a[i][i]=4;
a[i][n-1-i]=4;
}
for(i=0; i<n; i++)// restul elementelor din matrice
for(j=0; j<n;j++){
if(a[i][j]!=4)
a[i][j]=3;
}
for(i=0; i<n; i++){// afişarea matricii
for(j=0; j<n;j++)
cout<< a[i][j]<<" ";
cout<<endl;
}
return 0;
}

Exerciţii:

1. Se dă un tablou unidimensional cu n numere naturale. Să se determine:


a. Suma numerelor pare.
b. Câte numere din tablou sunt mai mici decât ultimul.
c. Cele mai mari două numere din tablou.
d. Suma numerelor formate din 2 cifre.
e. Câte numere au ultimele două cifre egale.

10
2. Scrieţi un program C++ care citeşte de la tastatură două numere naturale
nenule n şi m (2≤m≤10;2≤n≤10) şi care construieşte în memorie şi apoi
afişează o matrice A cu n linii (numerotate de la 1 la n) şi m coloane
(numerotate de la 1 la m) cu proprietatea că fiecare element A [i][j]
memorează cea mai mică dintre valorile indicilor i şi j (1≤i≤n, 1≤j≤m).
Matricea se va afişa pe ecran, câte o linie a matricei pe câte o linie a ecranului,
elementele fiecărei linii fiind separate prin câte un spaţiu. Exemplu: pentru
n=4 şi m=5 se va afişa matricea de mai jos.

11111
12222
12333
12344

3. Scrieţi un program C++ care citeşte de la tastatură un număr natural n (2≤n≤


24) şi construieşte în memorie o matrice cu n linii şi n coloane ale cărei
elemente vor primi valori după cum urmează:
- elementele aflate pe diagonala principală a matricei vor primi valoarea 0
- elementele de pe prima coloană, cu excepţia celui aflat pe diagonala
principală vor primi valoarea 1
- elementele de pe a doua coloană, cu excepţia celui aflat pe diagonala
principală vor primi valoarea 2
...
- elementele de pe ultima coloană, cu excepţia celui aflat pe diagonala
principală vor primi valoarea n
Programul va afişa matricea astfel construită pe ecran, câte o linie a matricei
pe câte o linie a ecranului, cu câte un spaţiu între elementele fiecărei linii (ca
în exemplu). Exemplu: pentru n=5 se va afişa matricea de mai jos.

02345
10345
12045
12305
12340

11
Lucrarea de laborator nr.2. Funcţii definite de utilizatori

O funcţie (subprogram) reprezintă un ansamblu de instrucţiuni folosite în vederea


executării unei anumite prelucrări dorite. Acest ansamblu de instrucţiuni este identificat
printr-un nume şi, în cele mai multe cazuri, este implementat separat (în exteriorul oricărei
alte funcţii).

Forma generală a unei funcţii este:

specificator_de_tip nume_functie(lista_parametri){
corpul functiei
}

specificator_de_tip indică tipul de date returnate de funcţie. lista_parametri este o


listă de nume de variabile şi tipurile asociate acestora, separate prin virgulă, care primesc
valorile argumentelor la apelul funcţiei. Există şi situaţii în care unei funcţii îi pot lipsi
parametrii. Totuşi, chiar dacă funcţia nu primeşte nici un parametru, parantezele sunt
necesare.

În limbajul C/C++ se utilizează declaraţii şi definiţii de funcţii.

Declaraţia conţine antetul funcţiei şi informează compilatorul asupra tipului,


numelui funcţiei şi a listei parametrilor formali (în care se poate indica tipul parametrilor
formali şi numele acestora). Declaraţiile de funcţii se numesc prototipuri şi sunt constituite
din antetul funcţiei.

tip_returnat nume_funcţie (lista tipuri parametri formali); //prototip

Definiţia conţine antetul funcţiei şi corpul acesteia.

tip_returnat nume_funcţie (lista parametrilor formali) //antet


{
instrucţiune compusă; // corpul funcţiei
}

12
Exemplu:

#include <iostream>
using namespace std;
void mesaj();
//prototipul poate să lipsească, deoarece mesaj() este înainte //main()
void mesaj(){
cout<<”Subprogram”;
}
int main(){
mesaj();
}

2.1.Apelul subprogramelor

O funcţie poate fi apelată printr-o instrucţiune de apel, de forma:

nume_functie(lista_parametri_efectivi)

Parametrii efectivi trebuie să corespundă cu cei formali ca ordine şi tip, în caz


contrar vom obţine erori.

Exemplu de apel de funcţie ce nu returnează o valoare:

#include <iostream>
using namespace std;
int n;
void suma();//prototip-obligatoriu aici
int main(){
cout<<”n=”; cin>>n;
suma();//apel
return 0;
}
void suma(){
int i, s=0;
for (i=0;i<n;i++){
cout<<s;
}
}

13
Exemplu de apel de funcţie ce returnează o valoare:

#include <iostream>
using namespace std;

int n;
int suma();//prototip-obligatoriu aici
int main(){
cout<<”n=”; cin>>n;
cout<<”suma=”<<suma();//apel
return 0;
}
void suma(){
int i, s=0;
for (i=0;i<n;i++){
s+=i;
}
return s;
}

2.2.Transmiterea parametrilor

Datele care “circulă” între modulul apelant şi cel apelat se introduc în paranteze.
Aceste date, care se numesc parametrii funcţiei, pot fi transmise prin valoare sau prin
referinţă.

2.2.1.Transmiterea parametrilor prin valoare

Acest tip de transmitere a parametrilor se utilizează în momentul în care suntem


interesaţi ca subprogramul să lucreze cu valoarea transmisă, dar, în prelucrare, nu ne
interesează ca parametrul efectiv (cel din blocul apelant) să reţină valoarea modificată în
subprogram.

14
Exemplu:

#include<iostream>
using namespace std;
void functie(int n) {
cout<<n<<endl;
}
int main() {
functie(3); //afiseaza 3
functie(3+4*5); //afiseaza 23
return 0;
}

Exemplu transmitere parametrilor prin valoare în cazul tablourilor:

#include<iostream>
using namespace std;
int n; //variabila globala
void citeste (int x[10]) {
cin>>n; //citeste dimensiunea unui tablou
for (int i=0; i<n;i++)
cin>>x[i]; //citeste fiecare element al tabloului
}
int main() {
int a[10]; // tablou ce va fi afisat
citeste(a); //apeleaza functia declarata mai sus prin //transmiterea, ca
argument,
//a unui tablou
for(int i=0; i<n;i++)
cout<<a[i]<<" "; //afiseaza tabloul
return 0;
}

2.2.2.Transmiterea parametrilor prin referinţă

Acest tip de transmitere a parametrilor apare în momentul în care ne interesează


ca la revenirea din subprogram variabila transmisă să reţină valoarea stabilită în timpul
execuţiei programului şi nu valoarea de la apel.

Un exemplu comparativ între coduri în care se realizează transmiterea


parametrilor prin valori şi referinţe, este prezentat în Tabel 2 :

15
Tabel 2. Comparaţie transmitere parametrii prin valoare şi referinţă

Transmitere prin Comentarii Transmitere prin Comentarii


valoare referinţă
#include<iostream> #include<iostream>
using namespace using namespace
std; std;
void f(int a); void f(int &a);
int main() { int main() {
int x; int x;
cout <<„X=”; cout <<„X=”;
cin>>x; 1.Introducem 4 cin>>x; 1.Introducem 4
cout <<„X=”<<x; 2.Afiseaza X=4 cout <<„X=”<<x; 2.Afiseaza X=4
f(x); 3.Apelul functiei f(x); 3.Apelul functiei
cout <<„X=”<<x; 5.Afiseaza X=4 cout <<„X=”<<x; 5.Afiseaza X=16
return 0; return 0;
} }
void f(int a){ void f(int &a){
a=a*a; a=a*a;
cout<<”A=”<<a; 4.Afiseaza A=16 cout<<”A=”<<a; 4.Afiseaza A=16
} }

Exemplu transmitere parametrilor prin referinţă în cazul tablourilor:

#include <iostream>
using namespace std;
void citeste (int x[], int &n) {
cin>>n;
for (int i=1; i<=n;i++)
cin>>x[i];
}
int main() {
int a[10],n;
citeste(a,n);
for(int i=1; i<=n;i++)
cout<<a[i]<<" ";
return 0;
}

16
Exerciţii:

1. Se citesc două numere naturale a şi b. Verificaţi dacă numerele a şi b au


aceeaşi sumă a cifrelor. Scrieţi o funcţie care determină suma cifrelor unui
număr natural transmis ca parametru.
2. Folosind subprograme să se afişeze cifra maximă a unui număr.
3. Se citeşte de la tastatură un număr natural n. Să se verifice dacă numărul
natural n începe şi se termină cu aceeaşi cifră.
4. Să se scrie o funcție care primește ca parametru un număr natural n şi afișează
descompunerea lui n în factori primi. Ex: n=165 se returnează 3, 5,11.
5. Folosind subprograme, să se realizeze citirea şi afişarea unei matrici.

17
Lucrarea de laborator nr.3. Căutarea şi sortare

3.1.Căutarea

Localizarea informaţiei într-o listă neordonată obligă la o cercetare secvenţială


începând cu primul element şi sfârşindu-se fie la detectarea elementului dorit, fie la finalul
listei de elemente (tabloului). Această cercetare este specifică listelor neordonate, dar
poate fi folosită şi-n cazul listelor ordonate, cu precizarea că, în acest caz, metoda nu este
tocmai eficientă.

3.1.1.Căutare secvenţială

Căutarea secvenţială:

• un algoritm simplu care poate duce la rezultate satisfăcătoare;


• verifică dacă un element aparţine unui tablou de elemente de aceeaşi
natură (de exemplu un număr într-un şir de numere);
• presupune parcurgea şirul de la un capăt la celălalt şi compararea
numărului de căutat cu fiecare număr din şir. În cazul în care s-a găsit
corespondenţă, acest lucru este semnalizat într-un anumit mod (fie prin
afişarea unui text, sau chiar chiar prin utilizarea unui contor pentru
numărarea apariţiei acestui număr în şir).

18
Exemplu: Să se afişeze dacă un număr aparţine sau nu unui tablou de numere
întregi.

#include<iostream>
using namespace std;
void main() {
int n,x,i,gasit,a[50];
cout<<"Dati numarul de elemente ale tabloului: ";
cin>>n; //n-dimensiunea vectorului
cout<<"Dati numarul de cautat : ";
cin>>x; //x-elementul ce se doreste a fi cautat
cout<<"Dati elementele tabloului"<<endl;
gasit=0;
for(i=0; i<n;i++){
cout<<"a["<<i<<"]= ";
cin>>a[i];
if (a[i]==x)
gasit=1;
}
if (gasit==0) //gasit=0, inseamna ca nu a fost gasit //elementul in tablou
cout<<"Numarul cautat nu apartine tabloului !"<<endl;
else
cout<<"Numarul apartine tabloului!"<<endl;

3.1.2.Căutare binară

Acest algoritm:
• ne oferă performanţe superioare algoritmului de căutare secvenţială
prezentat mai sus;
• presupune compararea numărului de căutat cu elementul aflat la mijlocul
tabloului (element care se mai numeşte şi pivot). În cazul în care cele
două elemente coincid căutarea s-a încheiat cu succes. Dacă numărul
căutat este mai mare decât pivotul stabilit, se continuă căutarea în aceeaşi
manieră în partea de tablou delimitat de pivot şi capătul tabloului, iar în
momentul în care numărul căutat este mai mic decât pivotul, căutarea are
loc între primul element al tabloului şi pivot. Algoritmul prezentat se
încadrează în clasa algoritmilor elaboraţi conform tehnicii de programare
Divide et Impera, tehnică pe care o vom studia într-un laborator viitor;

19
• printre dezavantajele unui astfel de algoritm ar fi obligativitatea ca şirul
în care se face căutarea să fie sortat chiar de la bun început.

3.2.Sortare

Sortarea reprezintă procesul de aranjare a unui set de date similare într-o anumită
ordine (crescătoare sau descrescătoare). Pentru sortarea tablourilor de date se folosesc
următoarele trei metode generale:

• selecţie;
• permutare;
• inserare.

În continuare vom prezenta câte un algoritm pentru fiecare metodă de sortare.

3.2.1.Sortarea prin selecţie directă

Luând în considerare un tablou ce se doreşte a fi ordonat crescător, se compară


primul element cu toate elementele care urmează după el. Dacă se va găsi un element mai
mic decât primul, atunci se face o interschimbare între cele două. Identic, se continuă cu al
doilea element al şirului, pe care, de asemenea îl vom compara cu toate elementele care
urmează după el şi în cazul în care se găseşte unul mai mic vom realiza o interschimbare
între cele două elemente. Asemănător se va proceda cu toate celelalte elemente până la
penultimul, procesul încheindu-se prin compararea ultimelor două elemente din tablou. O
abordare asemănătoare va avea loc şi-n cazul ordonării descrescătoare (se vor schimba
semnele utilizate în comparaţie).

20
#include <iostream>
using namespace std;
void main() {
int n, a[50],temp;
cout<<"Introduceti numarul de elemente din tablou : ";
cin>>n;
cout<<"Introduceti numerele"<<endl;
for(int i=0;i<n;i++){
cout<<"a["<<i<<"]=";
cin>>a[i];
}
// sortarea
for(int i=0;i<n-1;i++){//primul element din comparatie
for(int j=i+1;j<n;j++){//al doilea element din //comparatie
if (a[j]<a[i]){
temp=a[i];
a[i]=a[j];
a[j]=temp;
}
}
}
//prin afisarea tabloului se va observa ca elementele lui //sunt ordonate
crescator
}

3.2.2.Sortarea prin metoda bulelor – Geniul rău al permutării

Cel mai cunoscut algoritm de sortare este cel al bulelor(considerat şi cel mai
periculos). Popularitatea sa se bazează pe numele atrăgător şi pe simplitatea sa. În orice
caz, este unul dintre cele mai neadecvate principii de ordonare concepute vreodată.

Algoritmul bulelor realizează o sortare de tip permutare. Se realizează comparaţii


repetate şi, unde este cazul, se realizează o permutare a elementelor adiacente. Elementele
se comportă asemenea bulelor de aer dintr-un pahar cu apă: fiecare caută nivelul propriu.
Mai jos este prezentată o formă a acestui algoritm.

21
#include <iostream>
using namespace std;
void main() {
int n,temp,gata,a[50];
cout<<"Introduceti dimensiunea vectorului : ";
cin>>n;
for(int i=0;i<=n-1;i++) {
cout<<"a["<<i<<"]=";
cin>>a[i];
}
gata=0;
while(!gata) {
gata=1;
for(int i=0;i<n-1;i++){
if(a[i]>a[i+1]) {
temp=a[i];
a[i]=a[i+1];
a[i+1]=temp;
gata=0;
}
}
}
//se poate afisa vectorul ordonat
}

3.2.3.Sortarea prin inserare

Această metodă este a treia şi ultima din categoria metodelor simple de sortare.
Principiul de lucru este următorul: în prima fază sunt ordonate primele două numere din
cadrul unui eventual tablou; după acest prim pas se ia în vizor cel de-al treilea element,
care urmează să fie introdus în poziţia corectă în raport cu precedentele două. Procedeul
continuă până când întreaga listă de elemente este ordonată.

Exerciţii:

1. Să se modifice exemplul din subcapitolul 3.1.1, astfel încât să se afişeze


poziţia pe care se află numărul căutat.
2. Să se implementeze algoritmul prezentat în cadrul subcapitolului 3.2.3.

22
Lucrarea de laborator nr.4. Metoda Greedy folosind tabele de
verificare

“greedy” = “lacom”

Metoda se aplică problemelor ce presupun o organizare crescătoare (sau


descrescătoare) a datelor după anumite criterii pentru ca, mai apoi, să se aleagă dintre ele
anumite elemente (cele mai mici/mari) care sunt soluţii ale problemei.

De ce „lacom”? Pentru că la fiecare pas se alege cel mai bun candidat de la


momentul respectiv, fără a privi în profunzime. Dacă un candidat este inclus în mulţimea
soluţiilor, el va rămâne acolo, fără a putea fi modificat, chiar dacă ne-am putea da seama,
la un moment dat (după parcurgerea unui subset din datele problemei) că nu a fost
alegerea perfectă. Acest lucru ne spune că o astfel de metodă duce, în anumite cazuri, la
obţinerea unui optim care poate fi diferit de optimul global aferent problemei.

Exemple de probleme ce se pot rezolva folosind Greedy:

1. Fie un șir de N numere pentru care se cere determinarea unui subșir de


numere cu suma maximă. Un subșir al unui șir este format din caractere
(nu neapărat consecutive) ale șirului respectiv, în ordinea în care acestea
apar în șir.
• Pentru numerele 1 7 -6 2 -2 4 răspunsul este 1 7 2 4 (suma 14).
• Se observă că tot ce avem de făcut este să verificăm fiecare număr
dacă este pozitiv sau nu. În cazul pozitiv, îl introducem în subșirul
soluție. Această problemă nu obligă la ordonarea datelor de
intrare, ducând, cu toate acestea, la obţinerea soluţiei optime.
2. Bancomatul. Avem bancnote de 1, 5, 10, 50, 100, 200 şi 500 u.m. Când
vine un client să retragă o sumă de bani vom încerca să-i oferim cât mai
puţine bancnote.
• Pentru ca această problemă să aibă rezolvare folosind metoda
Greedy vom presupune că numărul de bancote este nelimitat.

23
Exerciţii:

1. Să se implementeze prima problemă dată ca exemplu în acest laborator.


2. Să se implementeze problema Bancomatului.
• Datele problemei:
i. Candidaţii: mulţimea iniţială de bancnote va fi formată
din 1, 5, 10, 100, 200 şi 500 lei.
ii. O soluţie posibilă: valoarea totală a unei astfel de mulţimi
de bancnote selectate trebuie să fie exact valoarea pe care
ar trebui să o dea bancomatul.
iii. O mulţime fezabilă: valoarea totală a unei astfel de
mulţimi nu este mai mare decât valoarea introdusă de
client.
iv. Funcţia de selecţie (funcţie care indică, la orice moment,
care este cel mai promiţător dintre candidaţii nefolosiţi):
se alege cea mai mare bancnotă dn mulţimea de candidaţi,
astfel încât suma dintre noul candidat şi valoarea
existentă să nu depăşească valoarea maximă admisă.
v. Funcţia obiectiv (funcţia pe care dorim s-o optimizăm): se
doreşte minimizarea numărului de bancnote ce vor
aparţine soluţiei.
• Abordare:
i. Iniţial, mulţimea candidaţilor selectaţi este vidă;
ii. Construim soluţia pas cu pas;
iii. La fiecare pas încercăm să adăugăm la muţimea
candidaţilor cel mai promiţător candidat astfel:
1. Dacă mulţimea candidaţilor nu mai este fezabilă,
eliminăm ultimul candidat introdus (de exemplu,
dacă la pasul i avem suma candidaţilor selectaţi
400, iar prin adăugarea unui nou candidat, adică
200, depăşim valoare ce se doreşte a se obţine,
vom renunţa la bancnota de 200, trecând să facem
verificările cu bancnotele cu o valoare inferioară
acesteia);

24
2. Dacă, după adăugare, mulţimea de candidaţi
selectaţi este fezabilă, ultimul candidat adăugat
va rămâne de acum încolo în ea (în exemplul
anterior, dacă acel 200 ar duce la o mulţime
fezabilă, el ar rămâne în mulţimea candidaţilor
selectaţi).
iv. La fiecare pas se verifică dacă nu s-a atins soluţia optimă
(în cazul nostru valoarea ce se vrea a fi scoasă din
bancomat).
v. Prima soluţie găsită va fi soluţia optimă a problemei.

Problema bancomatului folosind metoda Greedy poate fi înţeleasă mai bine în


urma parcurgerii unui tabel de verificare, ca cel exemplificat în Tabel 3:

Tabel 3. Evoluţie variabile problema „Bancomatului” pentru val_extras = 231 - tabel de


verificare

Instrucţiuni-paşi Pas
Citeste val_extras val_extras=231
val_i=0 val_i=0
mult_sol={} mult_sol={}
Pas 1:
val_candidat=500 val_candidat=500
val_i + val_candidat <=val_extras ? 0+500 <=231 (F)
val_candidat=200 val_candidat=200
val_i + val_candidat <=val_extras ? 0+200 <=231 (A)
adauga val_candidat la mult_sol mult_sol={200}
val_i=val_i+val_candidat val_i=0+200
val_i=val_extras ? (F) Sari la Pas 2
Pas 2:
val_i + val_candidat <=val_extras ? 200+200 <=231 (F)
val_candidat=100 val_candidat=100
val_i + val_candidat <=val_extras ? 200+100 <=231 (F)
val_candidat=10 val_candidat=10
val_i + val_candidat <=val_extras ? 200+10 <=231 (A)
adauga val_candidat la mult_sol mult_sol={200,10}
val_i=val_i+val_candidat val_i=200+10
val_i=val_extras ? (F) Sari la Pas 3
Pas 3:
val_i + val_candidat <=val_extras ? 210+10 <=231 (A)
adauga val_candidat la mult_sol mult_sol={200,10,10}

25
val_i=val_i+val_candidat val_i=210+10
val_i=val_extras ? (F) Sari la Pas 4
Pas 4:
val_i + val_candidat <=val_extras ? 220+10 <=231 (A)
adauga val_candidat la mult_sol mult_sol={200,10,10,10}
val_i=val_i+val_candidat val_i=220+10
val_i=val_extras ? (F) Sari la Pas 5
Pas 5:
val_i + val_candidat <=val_extras ? 230+10 <=231 (F)
val_candidat=1 val_candidat=1
val_i + val_candidat <=val_extras ? 230+1 <=231 (A)
adauga val_candidat la mult_sol mult_sol={200,10,10,10,1}
val_i=val_i+val_candidat val_i=230+1
Val_i=val_extras ? (A) STOP

Unde: val_extras –valoarea ce se doreşte a fi


extrasă
val_i – variabilă ce ţine suma intermediară
calculată prin extragerea a fiecărei bancnote
mult_sol – mulţimea de bancote obţinute la
final
val_candidat – valoarea ce se doreşte a se
adăuga la valoarea intermediară la fiecare
pas

26
Lucrarea de laborator nr.5. Metoda Greedy folosind schema
logică

Fiind date n obiecte, fiecare cu greutatea g[i] şi valoarea v[i] şi un rucsac cu


capacitatea totală Gt, să se determine ce obiecte trebuie selectate pentru a fi luate în rucsac
astfel încât greutatea lor totală să nu depăşească Gt şi valoarea lor să fie maximă.

Problema poate avea două variante de rezolvare:

• varianta fracţionară, dacă se pot lua părţi din fiecare obiect;


• varianta 0/1, dacă nu se pot lua decât obiecte întregi.

Pentru problema fracţionară metoda greedy conduce sigur la soluţia optimă, în


timp ce pentru varianta 0/1 nu este garantată găsirea soluţiei optime.

În Figura 1 avem reprezentarea, folosind o schemă logică, a variantei 0/1 a


problemei rucsacului.

27
Figura 1. Schema logică algoritm Rucsacul

28
sch:=0

i:=0

Nu
v[i]/g[i]
<v[i+1]/g[i+1]

Da

sch:=1

aux:=g[i]
g[i]:=g[i+1]
g[i+1]:=aux

aux:=v[i]
v[i]:=v[i+1]
v[i+1]:=aux

i:=i+1

Da
i<n-1

Nu

Nu
sch=0

Da
Figura 2. Blocul aferent ordonării elementelor
29
i:=0

Nu
Gt>=g[i]

Da

Gt:=Gt-g[i]

val:=val+v[i]

i:=i+1

Da
Gt>g[i] &&
i<n

Nu

Figura 3. Blocul aferent algoritmului Greedy

30
Exerciţii:

1. Să se implementeze algoritmul prezentat în diagramele acestui laborator.


2. Să se implementeze varianta fracţionară a algoritmului (prin modificarea
codului aferent exerciţiului 1).

31
Evaluare cunoştinţe I

În cadrul acestui laborator încercăm să prezentăm, pe scurt, noțiunile ce se


doresc a fi recapitulate, pentru o analiză a gradului de ”stăpânire” a materiei,
precum și pentru o consolidare în acest sens:

• Tablouri unidimensionale și bidimensionale- prelucrare:


- Declararea tablourilor;
- Parcurgerea elementelor unui tablou;
- Căutarea unui element în tabloul de memorie (secvențială,
binară);
- Ștergerea unui sau a mai multor elemente din cadrul
tabloului;
- Inserarea unui element într-un tablou unidimensional;
- Sortarea elementelor unui vector;
• Subprograme:
- Definirea subprogramului, identificare elemente
subprogram;
- Funcția procedurală și funcția operand;
- Transferul de parametri între subprograme (transfer prin
valoare și transfer prin referință);
• Metoda Greedy (aplicabilitate, implementare).
Aceste elemente prezentate vor putea fi considerate puncte de pornire în prima
evaluare aferentă semestrului.

32
Lucrarea de laborator nr.6. Recursivitatea

Procesul recursiv este procesul care, în timpul execuției, generează apariția


unor procese similare lui, aflate în legătură directă cu procesul care le generează.

Cu alte cuvinte, putem spune despre un proces că este recursiv, dacă în


definirea acestuia apare însăşi noțiunea care se definește.

Dacă ar fi să luăm un exemplu concret, din natură, ar putea fi cel al unui


arbore; o ramură a acestuia este baza următoarelor ramuri, acest lucru întâmplându-se
până se ajunge la ramurile mici, formate doar din tulpină și frunze, fără a mai fi baza
altor ramuri.

Adâncimea recursivității este dată de numărul de apeluri succesive ale unui


suprogram recursiv.

Structura unei funcții recursive:

tip nume_funcție (lista de parametri)


{......
if( condiție de continuare sau de oprire)
.......
nume_funcție (lista de parametri);
.......
}

Exemplu:

Să se calculeze suma primelor n numere naturale, valoarea lui n fiind introdusă


de la tastatură.

33
Folosind funcția iterativă, utilizăm doar valori cunoscute. Implementarea
iterativă va fi următoarea:

#include<iostream>
int suma(int n){
int i, s=0;
if(n==0)
return 0;
else {
for(i=1;i<=n;i++)
s=s+i;
return s;
}
}
void main(){
int n;
cout<<”n= ”;
cin>>n;
cout<<”suma este “<<suma(n);
}

Pe de cealaltă parte, funcția recursivă utilizează o expresie care conține însăși


funcția. Funcția matematică este următoarea:

Suma(n) va fi:
• 0, pentru n=0;
• n+ Suma(n-1), pentru n≠0.
Implementarea recursivă va fi următoarea:

#include<iostream>
int suma(int n){
if(n==0)
return 0;
else
return suma(n-1)+n;
}
void main(){
int n;
cout<<”n= ”;
cin>>n;
cout<<”suma este “<<suma(n);
}

34
Putem observa că recurența este dată de autoapelul funcției utilizate, suma(n),
nefiind necesară o variare inițială, cunoscută, spre deosebire de varianta iterativă, unde
valoarea inițială era 0.

Reguli pentru construirea unui subprogram recursiv:

1. Întotdeauna există atât un caz general al soluției, care va conține absolut


toate prelucrările necesare pentru a aduce problema în stadiul de subproblemă cu
rezolvare imediată, cât și un caz de bază. În exemplul dat, cea mai importantă operație
este cea a autoapelului, realizat prin linia de cod return suma(n-1)+n;. Cazul de bază
rezolvă un caz special al problemei, fără a utiliza autoapelul, prin instrucțiunea return
0;.

2. Având în vedere că recursivitatea este un proces repetitiv, este obligatorie


o condiție de oprire, pentru a nu duce procesul spre o repetare teoretic infinită, pentru că
practic, acest lucru nu se va întâmpla, fiind umplută stiva de memorie alocată, iar
programul va fi întrerupt cu o eroare. În general, se recurge la utilizarea structurii
alternative if…else. Oprirea recursivității trebuie să se realizeze cu ajutorul unei expresii
logice, depinzând de parametrii funcției, care de fapt face trecerea de la cazul general al
problemei, la cazul de bază. În exemplul dat, instrucțiunea logică este (n==0;).

3. Orice definiție recursivă trebuie să asigure o condiție de consistență. Cu


alte cuvinte, valoarea funcției trebuie să fie direct calculabilă, sau posibil calculabilă cu
o valoare direct calculabilă.
În cazul în care funcția utilizată are parametrii, aceștia se memorează ca și
variabile locale pe stivă, după cum urmează:

• Parametrii ce se transmit prin valoare vor fi memorați în stivă cu


valoarea din momentul curent;
• Parametrii transmiși prin referință vor fi memorați cu ajutorul adresei
lor.

35
Exemplu:

Se citește de la tastatură un cuvânt. Terminarea cuvântului se realizează prin


utilizarea spațiului. Să se afișeze același cuvânt, dar inversat.

#include<iostream>
using namespace std;
void inv(){
char litera;
cin.get(litera);
if(litera!=' ')
inv();
cout<<litera;
}
int main(){
cout<< "Introduceti cuvantul dorit a fi inversat ";
inv();
return 0;
}

Pentru a putea înțelege mai bine alocarea pe stivă, presupunem citirea de la


tastatură a cuvântului ”vara”, urmat de un spațiu.

La primul apel al funcției inv(), din void main, se creează primul nivel al stivei.
Îl vom denumi nivelul 1. În acest moment, tot controlul programului aparține funcției
inv(). Am declarat variabila locală litera, în care citim prima literă, ”v”. aceasta va fi
memorată pe stivă, în nivelul 1.

Urmează testul de continuare, verificarea existanței spațiului, adică dacă citim


un caracter diferit de spațiu. Deoarece caracterul citit a fost ”v”, reapelăm funcția inv().
Se creează nivelul 2 pe stivă și se citește următorul caracter, ”a”. Se memorează
caracterul pe nivelul 2, în variabila litera.

Se continuă testarea, se decide reapelarea funcției inv(), deoarece caracterul


citit nu este spațiu. Se creează nivelul 3, se memoreaza în variabilă litera ”r”, se
reapelează funcția. Similar se întâmplă și cu ultima literă, ”a”, care va ocupa nivelul 4.

Se creează nivelul 5 al stivei. În variabila litera se va memora, de această dată,


caracterul spațiu. În urma testului, se decide că funcția nu va mai fi reapelată.

În acest moment, se revine la instrucţiunea aflată imediat după reapelul


funcţiei, se afişează conţinutul variabilei lit, adică “ “. Se închide nivelul 5 al stivei, se
revine la instrucţiunea următoare şi se afişează “a”. Se închide nivelul 4 al stivei, se
afişează “r”. Se închide nivelul 3 şi se afişează “a”, se închide şi nivelul 2, se afișează

36
”v”. se închide și nivelul 1 şi se revine în funcţia main(), care în acest moment se încheie
şi astfel se termină programul.

Tabel 4. Stiva aferentă cuvântului "vara"

Stiva
litera=” ” Nivelul 5
litera=”a” Nivelul 4
litera=”r” Nivelul 3
litera=”a” Nivelul 2
litera=”v” Nivelul 1

În funcție de procesele care se autoapelează, există două tipuri de recursivitate:

1. Recursivitatea directă, care presupune existența unui singur proces, P, care


își generează singur apariția, pe baza anumitor condiții. Cu alte cuvinte,
dacă în corpul unui subprogram se întâlnește un apel al aceluiași
subprogram, înseamnă că recursivitatea este directă.

void P(){
…….
P();
……
}

2. Recursivitatea indirectă presupune existența a două procese, P1 și P2, care


își generează apariția reciproc.

void P2;
void P1;
void main()
{……….
P2();
………..
};
void P2()
{……….
P1();
………..
};

Soluțiile recursive sunt, de obicei, mai clare, mai scurte. Prezintă un avantaj
evident dacă soluțiile problemei abordate sunt definite recursiv și dacă cerințele
problemei sunt definite, de asemenea, recursiv.

37
Totuși, dacă există o adâncime mare a recursivității, aceasta nu își mai justifică
utilizarea, deoarece timpul de execuție crește, din cauza timpilor necesari pentru
mecanismul de apel și pentru administrarea stivei.

Exerciţii:

1. Pentru un vector cu n elemente numere întregi, calculați recursiv:


a. Suma elementelor vectorului;
b. Media aritmetică a elementelor vectorului.
2. Să se calculeze numărul de apariții ale unui element citit de la tastatură,
într-un vector cu n elemente. Atât n, cât și elementele vectorului se citesc,
de asemenea, de la tastatură.
3. Să se genereze toate numerele din intervalul [a,b]. Valorile lui a și b se
citesc de la tastatură.

38
Lucrarea de laborator nr.7. Divide et impera

Vom încerca să înţelegem această tehnică folosind un algoritm de căutare pomenit


în cadrul unui laborator anterior.

Pentru seturile de date sortate, putem folosi pentru căutare o metodă cu mult
superioară celei secvenţiale. Aceasta este căutarea binară, care foloseşte o abordare gen
dezbină şi cucereşte. Utilizarea acestei metode obligă la o ordonare a elementelor din şirul
iniţial. Pentru început în cadrul aceastei metode se testează elementul din mijloc. Dacă
acesta e mai mare decât cel căutat, se va testa elementul de mijloc al primei jumătăţi; în
caz contrar va fi testat elemetul de mijloc al celeilate jumătăţi. Procesul se repetă până
când este descoperit elementul căutat sau nu mai sunt elemente de testat.

De exemplu, pentru a localiza elementul 17 în următorul şir:

3 6 9 10 15 17 23 39 54 73 88 91 99

o căutare binară va căuta mai întâi elementul din mijloc, adică locul unde se află
valoare 23. Deoarece acest element este mai mare decât 17, cercetarea va continua în
prima jumătate a şirului iniţial, adică în

3 6 9 10 15 17 23

Elementul din mijloc este 10, adică o valoare mai mică decât cea căutată, ceea ce
ne obligă să continuăm căutarea în a doua jumătate a acestui subşir, adică în

15 17 23

Verificând acest subşir, vom observa că elementul de mijloc e chiar cel căutat.

39
În continuare avem o funcţie recursivă de căutare:

funcţie recursiv (st,dr)


mij=(st+dr)/2
dacă v[mij]=x atunci
returnează mij //mij este poziţia lui x în vector
altfel
dacă st<dr atunci
dacă v[mij]>x atunci
dr=mij
recursiv(st,dr)
altfel
st=mij
recursiv(st,dr)
sf. dacă
altfel
afişează “x nu este vector”
sf. dacă
sf. dacă
sf. funcţie

Exerciţii:

1. Să se implementeze algoritmul descris în acest laborator, folosind


pseudocodul dat ca exemplu.
2. Să se încerce rezolvarea problemei anterioare folosind un vector
neordonat. Comentaţi rezolvarea.
3. Se citeşte un vector cu n elemente numere naturale. Să se calculeze suma
elementelor vectorului folosind divide et impera.

40
Lucrarea de laborator nr.8. Backtracking liniar de lungime
fixă

Este o strategie de căutare sistematică în spaţiul soluţiilor unei probleme. Se


utilizează pentru rezolvarea anumitor probleme a căror cerinţă este de a determina
configuraţii care satisfac anumite restricţii. De obicei, în rezolvarea acestor probleme, se
încearcă găsirea unei submulţimi S a produsului cartezian A1xA2x…xAn, având
proprietatea că fiecare element s=(s1,s2,…sn) satisfice anumite restricţii.

Soluţiile sunt construite pas cu pas, prin găsirea succesivă a valorilor potrivite
pentru componente (în fiecare etapă se adaugă o componentă, generându-se o soluţie
parţială). Fiecare din aceste soluţii parţiale este evaluată cu scopul de a stabili dacă este
validă. O astfel de soluţie care este validă poate conduce la o soluţie finală, în timp ce o
soluţie care încalcă restricţiile prevăzute iniţial în cadrul problemei sunt invalidate. Dacă
nici una dintre valorile unei componente nu conduce la o soluţie validă, atunci se revine la
componenta anterioară şi se încearcă o nouă valoare pentru aceasta.

Principiul acestei tehnici poate fi vizualizat folosind un arbore de căutare, care


ilustrează parcurgerea spaţiului soluţiilor. Desenarea acestui arbore se realizează
respectând următoarele:

• Rădăcina arborelui corespunde stării iniţiale, cea de dinaintea completării


componentelor;
• Un nod intern corespunde unei soluţii parţiale valide;
• Un nod final (frunză) corespunde unui soluţii finale a problemei sau unei
soluţii parţiale invalide.

Un exemplu de problemă ce ar putea fi rezolvată cu o astfel de metodă este:

Se citeşte un număr natural n. Generaţi şi afisaţi toate combinaţiile de câte n cifre


binare care nu au două cifre de 0 alăturate. În Figura 4 vom reprezenta arborele de căutare
corespunzător acestei probleme. Vom considera n=3.

41
Figura 4. Arbore de căutare

O altă problemă ce poate fi rezolvată într-un mod asemănător cu cea prezentată


mai sus ar fi „Problema celor n regine”. Ea presupune determinarea modului de aşezare a
n regine pe o tablă de şah nxn, astfel încât acestea să nu se poată elimina(„mânca”)
reciproc. Pentru cei care nu cunosc arta şahului, două regine se elimină reciproc în
momentul în care se află pe aceeaşi linie, aceeaşi coloană sau diagonală.

Pentru o înţelegere mai uşoară a modului de rezolvare, vom considera o tablă de


şah 4x4. Pentru această dimensiune vom încerca amplasarea a 4 regine pe această tablă.
De ce numai 4? Am precizat mai sus că pe o linie putem pune o singură regină, această
afirmaţie ducând la concluzia că în cazul în care numărul de regine este 5 sau mai mare,
cel puţin două regine se vor ataca. Acelaşi lucru putem să-l spunem şi legat de coloane.

Punctul de plecare în amplasarea acestor regine va fi colţul din stânga sus al


tablei, adică prima celulă(cu cordonatele 0,0). Urmează, pe rând, încercarea de plasare a
celorlaltor regine (3 la număr). Condiţia de neeliminare reciprocă trebuie să fie respectată
în cazul acestor amplasări. În momentul în care se ajunge la o regină prin a cărei
amplasare se încalcă condiţia de neeliminare, vom reveni la precedenta regină şi vom
încerca o reamplasare a acesteia într-o poziţie validă.

Recurgând la arborele de căutare, în Figura 5 prezentăm primii paşi în rezolvarea


acestei probleme.

42
Figura 5. Arborele de căutare pentru 4 regine

Problema celor n regine este reprezentată în continuare folosind pseudocodul:

funcţie regina(n) // x(1.....n)- vectorul de soluţii


x(1)=0; k=1;
cât timp (k>0) execută
x(k)=x(k)+1;
cît timp x(k)<=n şi valid(k)=0
x(k)=x(k)+1;
sf. cât timp
dacă( x(k)<=n )
dacă (k=n) // sol. completa
afişează x
//ATENŢIE x este vector
altfel
k=k+1;
x(k)=0
sf. dacă
altfel //nu este o sol. bună
k=k-1;
sf. dacă
sf. cât timp
sf. funcţie

43
/*………………………………………………*/

funcţie valid(k)
i=1;
cât timp (i<k) execută
dacă (x(i)=x(k) sau |x(i)-x(k)|=|i-k|)
returnează 0
sf. dacă
i=i+1
sf. cât timp
returnează 1
sf. funcţie

Exerciţii:

1. Să se explice de ce s-a recurs la un tablou unidimensional în rezolvarea acestei


probleme, chiar dacă o tablă de şah poate fi reprezentată folosind un tablou
bidimensional.
2. Să se implementeze „Problema celor n regine” şi să se testeze rezolvarea
luând diferite valori pentru n. Ce aţi observat?
3. Fie n un număr natural nenul (n<30). Scrieţi un program de generare
a permutărilor de ordin n a elementelor mulţimii {1, 2, 3,..., n}.

44
Lucrarea de laborator nr.9. Backtracking în plan

O altă problemă ce se poate rezolva prin metoda backtracking ar fi „Săritura


calului pe o tablă de şah de dimensiuni nxn”. Această piesă poate fi mutată doar în forma
de L, cum se poate vedea în Figura 6.

Figura 6. Posibilităţi de mutare a calului pe tabla de şah

În cazul acestei probleme se doreşte determinarea modalităţilor ca un cal să


parcurgă toate pătratele unei table de şah de o dimensiune dată, o singură dată, pornind
dintr-un pătrat dat. Datorită modului în care poate fi mutat un cal pe tabla de şah această
problemă poate fi rezolvată doar în „plan”, adică folosind o structură specifică (un tablou
bidimensional).

Considerând x şi y coordonatele poziţiei curente a unui cal pe tabla de şah,


următoarea mutare poate fi efectuată în una din noile coordonate:

• (x-2,y-1);
• (x-2,y+1);
• (x-1,y+2);
• (x+1,y+2);

45
• (x+2,y+1);
• (x+2,y-1);
• (x+1,y-2);
• (x-1,y-2).

Pentru rezolvarea acestei probleme am avea nevoie de:

o un vector cu poziţii POZ;

o un vector cu direcţii DIR.

Logica:

o DIR[i] poate lua valori de la 0 la 7, în funcţie de deplasare;

o Pentru DIR[k] vom încerca toate numerele de la 0 la 7 (în dx vom


stoca deplasarea pe x, în dy vom stoca deplasarea pe y pe tabla de
şah);

o Dacă ne putem deplasa pe un pătrat liber atunci vom înainta, k-ul


se va incrementa, iar DIR[k] primeşte valoarea -1.

Vom avea nevoie de doi vectori:

const int dx={-2,-2,-1,1,2,2,1,-1}


const int dy={-1,1,2,2,1,-1,-2,-2}, vectori care ne vor ajuta în deplasarea pe tabla
de şah.

Un rol important în rezolarea problemei îl constituie verificarea apartenenţei unei


poziţii luate în calcul tablei de şah

int PeTabla(int x,int y){


if (0<=x && x<n && 0<=y && y<n)
return 1;
else
return 0;
}

46
Funcţia principală:

//citire date de intrare si alte initializari


int eSuccesor,eCandidat;
int k;
cin>>n;//dimensiunea tablei
cin>>li>>ci;//linia si coloana initiala
k=0;
POZ[k].l=li-1;//o structura cu l-linie si c-coloana
POZ[k].c=ci-1;
k=1;
DIR[k]=-1;//sa putem incepe verificarea succesorilor cu 0
while (k>0){
do {
if(DIR[k]<7){
DIR[k]++;//porneste de la 0
eSuccesor=1;
POZ[k].l=POZ[k-1].l+dx[DIR[k]];
POZ[k].c=POZ[k-1].c+dy[DIR[k]];
//determinam daca punctul este candidat la solutie
//determinand daca pozitia gasita este pe tabla si
//nu se suprapune cu o pozitie deja ocupata
}else{
eSuccesor=0;
}
}while (eSuccesor && !eCandidat);
if(eSuccesor){
if(k==n*n-1)
Afisare();
else {
k++;
DIR[k]=-1;
}//sfarsit else
}else{
k--;
}
}//sfarsit while

47
Determinarea apartenenţei unui punct la soluţie, verificându-se ca poziţia sa să fie
pe tablă şi să nu se suprapună cu o poziţie existentă deja în soluţie, se face folosind
funcţia:

int EsteCandidat(int k){


int i;
for(i=0;i<k;i++){
if(POZ[i].l==POZ[k].l && POZ[i].c== POZ[k].c ){
return 0;
}
}
return 1;
}

Pentru afişarea rezultatului final putem folosi o funcţie de forma:

void Afisare(){
for(int i=0;i<n*n-1;i++){
cout <<“(” <<POZ[i].l+1 <<“,” <<POZ[i].c+1 <<“)”;
}
cout<<endl;
cout<<endl;
}

Exerciţii:

1. Să se implementeze săritura calului pe tabla de şah. Să se ruleze de mai


multe ori soluţia obţinută, modificându-se dimensiunea tablei de şah. Ce
aţi observat?
2. Să se implementeze problema celor n regine folosind backtracking în
plan.

48
Lucrarea de laborator nr.10. Backtracking recursiv

Exerciţii:

1. Folosind cunoştinţele acumulate până în acest moment să se rezolve


problema “Săritura calului pe tabla de şah” recursiv.
• Sugestii:
i. Completăm toate pătrăţelele cu valoare -1;
ii. Pătratul de start va lua valoarea 0 (sau 1);
iii. Vom apela la o funcţie back cu 3 parametri:
• Coordonatele pătratului de start;
• Pasul curent al algoritmului (calului).
iv. Pentru parametrii l şi c, ce reprezintă poziţia calului vom
încerca toate poziţiile din jur;
v. Noua poziţie va fi xnou şi ynou;
vi. apelăm back(......,k+1).
• Variabile utilizate:
• int n;
• int Tabla[21][21];
• int nSol=0;
• ofstream f(“fisier.out”);
• Funcţia de afişare traseu:

AfisareTraseu(){
f<<“Solutia: ”<<++nSol<<endl;
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
f<<Tabla[i][j]+1<<“ ”;
}
f<<endl;
}
return 0;
}

49
• Funcţia back:

void back(int l, int c, int pas){


if(pas==n*n-1){
AfisareTraseu();
return;
}
int dx,dy,lnou,cnou;
for(dx=-2;dx<3;dx++){
for(dy=-2;dy<3;dy++){
if(abs(dx*dy)==2){
lnou=l+dx;
cnou=c+dy;
if(0<=lnou && lnou<n &&

0<=cnou && cnou<n


&& Tabla[lnou][cnou]==-1){

Tabla[lnou][cnou]=pas+1;
back(l+dx,c+dy,pas+1);
Tabla[lnou][cnou]=-1;
}
}
}
}
}//sfarsit back

• Funcţia main:

void main(){
int l, c;
cin>>n;
for(l=0;l<n;l++){
for(c=0;c<n;c++){
Tabla[l][c]=-1;
}
}
cin>>l>>c;
Tabla[l-1][c-1]=0;
back(l-1,c-1,0);
f.close();
}
2. Dat un număr natural n, 1<=n<=500, să se determine toate modurile
diferite de a-l scrie pe n ca suma de numere naturale.

50
Evaluare cunoştinţe II

În cadrul acestui laborator încercăm să prezentăm, pe scurt, noțiunile ce se


doresc a fi recapitulate, pentru o analiză a gradului de ”stăpânire” a materiei,
precum și pentru o consolidare în acest sens:

• Recursivitatea:
- Descriere, structură;
- Implementare;
- Algoritmizare.
• Divide et Impera:
- Descriere metodă;
- Implementare;
- Aplicabilitate.
• Backtracking (liniar, în plan, recursiv):
- Descrierea metodei;
- Recunoasterea aplicabilității;
- Implementare.
Prezenta structură este strict orientativă. Spor!

51
Lucrarea de laborator nr.11. Algoritmi genetici I

Algoritmii genetici reprezintă tehnici de căutare şi optimizare , având ca punct de


pornire metafora biologică a „moştenirii genetice şi evoluţiei naturale”- John Henry
Holland(1960).

În cadrul acestor algoritmi populaţia iniţială evoluează prin mecanisme de


inspiraţie biologică: selecţie, încrucişare, mutaţie.

În diagrama de mai jos avem o reprezentare a paşilor acestui tip de algoritm.

Figura 7. Etapele algoritmilor genetici

În cadrul acestui tip de algoritm fiecare individ component al unei populaţii se


descrie printr-un singur cromozom, acesta din urmă fiind o colecţie de gene, care în cazul
nostru vor fi nişte variabile. Deci, practic, populaţia va fi o mulţime de indivizi, fiecare
individ fiind caracterizat prin intermediul unor variabile.

52
var1 var2 var3 ..... varn
Figura 8. Structura unui cromozon

Scopul evoluţiei ar fi atingerea unor soluţii optime prin păstrarea unor indivizi
superiori calitativi din populaţia anterioară, dar şi prin generarea unor noi indivizi care au
la bază acei indivizi superiori calitativ din generaţiile anterioare.

Variabilele utilizate în cadrul acestor algoritmi pot avea diverse reprezentări:


binară, reală, numere întregi. Aceste reprezentări se aleg în funcţie de necesităţi, nefiind o
regulă dinainte stabilită.

Algoritmul debutează prin generarea unei mulţimi iniţiale (mulţime de cromozomi


iniţiali). Această generare poate fi făcută într-un mod aleatoriu. Totuşi, în cazul unor
probleme, această generare aleatorie poate duce la o încetinire a vitezei de găsire a unei
soluţii optime. De aceea, în anumite cazuri este recomandat să utilizăm, poate chiar, şi
ceva calcule în cazul acestor generări, sau chiar să impunem anumite restricţii.

Numărul de gene din cadrul unui cromozon, precum şi numărul de cromozomi din
cadrul populaţiei iniţiale se stabileşte în funcţie de problemă, de puterea de procesare al
calculatorului, dar şi de viteza cu care dorim să obţinem un rezultat optim.

Exerciţii:

1. Să se creeze un fişier „distante.in” în care avem:


a. Un n – care reprezintă un număr de oraşe. Pentru început să
considerăm acest număr fiind egal cu 10.
b. O matrice de nxn, adică în cazul nostru, 10x10, care reprezintă
matricea distanţelor(adică la intersecţia dintre i şi j se află distanţa
dintre i şi j). Distanţele vor fi numere întregi cu valori sub 100.
Matricea construită trebuie să fie simetrică, adică A[i][j]=A[j][i]. Se
consideră că există legătură directă între fiecare două oraşe x şi z
diferite, deci A[x][z]!=0. Elementele de pe diagonala principală vor fi
egale cu 0.
2. Folosind C++, să se citească numărul de oraşe, precum şi distanţele dintre ele.
(Distanţele să fie memorate într-un tablou bidimensional)
3. Să se genereze populaţia iniţială aferentă algoritmului genetic ţinând cont de
următoarele:
a. Soluţia finală pe care dorim să o obţinem va reprezenta drumul minim
care leagă cele 10 oraşe (problema comis-voiajorului), drum ce trece
o singură dată prin toate oraşele, cu excepţia primului oraş, care va fi
şi ultimul.

53
b. În rezolvarea de faţă, primul oras va fi oraşul 0.
c. Se generează aleatoriu un număr de la 1 la 9 (0 fiind luat deja în
calcul), iar dacă acest număr generat nu se află în cromozomul curent
va fi introdus în acesta, pe poziţia curentă.
d. Generarea de la pasul c va continua până când dimensiunea
cromozomului curent va avea dimensiunea egală 10.
e. Deoarece, avem de-a face cu o populaţie de cromozomi, vom salva
fiecare cromozom într-o matrice n+1xm, unde n reprezintă lungimea
cromozomului (care în cazul nostru este 10), iar m reprezintă
dimensiunea populaţiei (care în cazul nostru va fi 30). Acea coloană
în plus (de la n+1) va fi folosită pentru stocarea lungimii drumului
aferent fiecărui cromozom generat.(Matricea populaţiei iniţiale va fi
11x30).

Ierarhizarea cromozomilor în cadrul unei populaţii ocupă un loc de bază în cadrul


algoritmilor genetici. Rezultatele acestui process sunt utilizate în cadrul tuturor celorlaltor
paşi descrişi în laborator. Inexistenţa unei astfel de etape ar putea duce la găsirea unor
soluţii departe de valorile dorite.

Exerciţii :

4. Să se ierarhizeze cromozomii din populaţia iniţială. Ordonarea să se facă de la


valoarea cea mai mică aferentă ultimei coloane a matricii iniţiale la valoarea
cea mai mare.

54
Lucrarea de laborator nr.12. Algoritmi genetici II

O etapă importantă în evoluţia algoritmilor genetici o reprezintă verificarea


îndeplinirii criteriilor de optimizarea (sau oprire) aferent algoritmului. Putem întâlni 4
situaţii:

• Se atinge valoarea minimă pentru funcţia obiectiv(distanţa


minimă)-lucru greu de aflat în cazul unor probleme de genul celei
de la laboratorul 11;
• Se stabileşte un număr maxim de generaţii, număr ce este atins la
un moment dat;
• Se stabileşte un timp alocat execuţiei programului – în funcţie de
dimensiunea problemei, acesta poate duce la rezultate mai bune,
sau mai puţin bune;
• Are loc o stagnare a îmbunătăţirii soluţiilor-practic, timp de mai
multe generaţii valoarea de pe coloana n+1, aferentă celui mai
bun cromozom nu se modifică.

Fiecare individ din cadrul unei populaţii poate primi o probabilitate de selecţie în
vederea recombinării, probabilitate ce depinde de rezultatul obţinut după ierarhizare.
Astfel, cromozomii aflaţi în partea superioară a matricii vor avea o probabilitate mai mare
pentru a fi selectaţi în vederea utilizării lor în următoarele etape ale algoritmului, sau chiar
pentru următoarele generaţii.

Vom avea două tipuri de selecţie:

• una a părinţilor, care contribuie la generarea populaţiei viitoare;


• una în care indivizii din populaţia viitoare sunt aleşi dintre
descendenţii obţinuţi şi indivizii din populaţia curentă.

Aceste două tipuri de selecţie influenţează modul în care creşte calitatea generală
a soluţiilor.

55
Operaţia de selecţie are rolul de a concentra căutările pe regiunile cele mai
promiţătoare din spaţiul de căutare al problemei.

Pentru crearea unor noi soluţii candidat din cele vechi, cu scopul final de a mări
diversitatea populaţiei curente, se folosesc operatori de variaţie. Cei mai cunoscuţi
operatori de variaţie sunt: mutaţia şi încrucişarea.

Mutaţia acţionează asupra unui singur individ şi produce unul nou. Rezultatul
obţinut în urmă aplicării operatorului de mutaţie conţine mici modificări faţă de individual
inţial, dar poate să ducă la o valoare mult îmbunătăţită(în caz optimist) a funcţiei ce se
doreşte a fi optimizată. Un exemplu de mutaţie avem în Figura 9 şi Figura 10.

0 7 3 8 2 9 6 5 4 1
Figura 9. Cromozomul înainte de mutaţie

0 7 3 5 2 9 6 8 4 1
Figura 10. Cromozomul după mutaţie

Recombinarea (sau încrucişarea) implică, în cele mai multe cazuri, doi indivizi.
Scopul acestei operaţii este de a genera unul, doi, sau poate chiar mai mulţi indivizi prin
combinarea genelor părinţilor. Pentru problema care se doreşte a fi rezolvată în cadrul
acestui laborator recombinarea poate fi realizată în felul următor (Figura 11 şi Figura 12):

• se alege în mod aleator un punct de încrucişare (care trebuie să fie mai


mare decât 0 şi mai mic decât numărul de oraşe luate în calcul);
• se copiază primele două părţi în cei doi descendenţi;
• restul cromozomului se completează prin inserarea de valori de la celălalt
părinte:
o în ordinea în care apar acolo, începând cu punctul de încrucişare,
dar sărind peste valorile care deja se găsesc în descendent.

0 7 3 8 2 9 6 5 4 1
0 5 2 8 1 3 4 6 7 9
Figura 11. Cromozomii înainte de încrucişare

0 7 3 8 1 4 6 9 5 2
0 5 2 8 9 6 4 1 7 3
Figura 12. Cromozomii după încrucişare

56
Exerciţii (continuarea problemei începute în cadrul laboratorului 11):

1. Considerând că vom avea 200 de generaţii, în fiecare dintre ele să se aplice


următoarele:
a. Operatorul de mutaţie pentru 5 cromozomi, luaţi aleator, din primii 10
ai populaţiei (putându-se folosi logica descrisă în figurile aferente);
b. Operatorul de recombinare, de 5 ori, luând aleatoriu, din primii 20 de
cromozomi, câte 2 cromozomi (a se vedea exemplul din figurile
aferente acestui operator);
c. După obţinerea a celor 15 noi cromozomi (5 de la mutaţie şi 10 de la
recombinare), să se introducă aceştia în populaţia curentă, alături de
cromozomii existenţi (se poate folosi un alt tablou) şi să se realizeze o
nouă ierarhizare.
d. După ierarhizarea celor 45 de cromozomi se vor selecta primii 30 care
au o valoare cât mai mică. Aceşti 30 de indivizi vor forma populaţia
generaţiei următoare;
e. După 200 de paşi se va afişa cromozomul cu valoarea funcţiei căutate
minimă.
2. Încercaţi acelaşi algoritm folosind diferite valori pentru numărul de oraşe,
numărul de generaţii, numărul de mutaţii, numărul de încrucişări, dar şi modul
de selectare a cromozomilor pentru aceste operaţii. Comentaţi rezultatele
obţinute.

57
Bibliografie

• Balan, I., Utilizarea calculului de înaltă performanţă în optimizări, Ed.


Didactică şi Pedagogică, 2016
• Braharu, G., Curs de programare. Nivel mediu. Recursivitate,
Backtracking, Pointeri, Liste, Bucureşti, 2017
• Logofătu, D., Algoritmi fundamentali în C++. Aplicaţii, Editura Polirom,
2007
• Morariu, N., Incursiune în teoria şi practica programării calculatoarelor,
Ed. Didactică şi Pedagogică, 2010
• Oltean, G., Inteligenţă computaţională în sisteme decizionale complexe,
note de curs 2017-2018, Univ. Tehnică Cluj-Napoca
[http://www.bel.utcluj.ro/dce/didactic/icsdc/]
• Schildt, H., C manual complet, Traducere de Mihai Mănăstireanu, Ed.
Teora, 2001

58

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