Bun venit în lumea fascinantă a combinatoricii! Dacă matematica discretă și programarea s-ar întâlni la o cafea, acesta ar fi subiectul lor de discuție favorit. Să explorăm împreună aceste concepte fundamentale care stau la baza multor probleme de programare!
1. Introducere: “Magia Aranjării”
Imaginează-ți această analogie:
Ai 3 cărți pe masă: A (Albăstră), R (Roșie), V (Verde). În câte moduri diferite le poți aranja?
Răspuns:
- A R V
- A V R
- R A V
- R V A
- V A R
- V R A
Aceasta este esența combinatoricii! Studiază modurile în care putem aranja, selecta și combina obiecte.
2. Ce sunt Permutările? “Orchestrația Perfectă”
Definiție:
O permutare este un aranjament al TUTUROR elementelor unei mulțimi într-o anumită ordine.
Formula matematică:
P(n) = n! = n × (n-1) × (n-2) × ... × 2 × 1
Unde n! se citește “n factorial”.
Exemple:
- 3 cărți: 3! = 3 × 2 × 1 = 6 permutări
- 4 studenți într-o bancă: 4! = 24 de așezări diferite
- 5 melodii într-un playlist: 5! = 120 de ordini diferite
Cum gândește calculatorul:
Pentru {A, B, C}:
Alege primul: A, B sau C
│
├── Dacă aleg A: rămân {B, C}
│ ├── Alege B: rămâne {C} → ABC
│ └── Alege C: rămâne {B} → ACB
│
├── Dacă aleg B: rămân {A, C}
│ ├── Alege A: rămâne {C} → BAC
│ └── Alege C: rămâne {A} → BCA
│
└── Dacă aleg C: rămân {A, B}
├── Alege A: rămâne {B} → CAB
└── Alege B: rămâne {A} → CBA
Implementare în C++:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void afisarePermutare(vector<int>& permutare) {
for(int num : permutare) {
cout << num << " ";
}
cout << endl;
}
// Metoda 1: Folosind backtracking clasic
void permutariBacktracking(vector<int>& elemente,
vector<bool>& folosit,
vector<int>& permutareCurenta) {
if(permutareCurenta.size() == elemente.size()) {
afisarePermutare(permutareCurenta);
return;
}
for(int i = 0; i < elemente.size(); i++) {
if(!folosit[i]) {
// ADAUGĂ elementul
folosit[i] = true;
permutareCurenta.push_back(elemente[i]);
// CONTINUĂ recursiv
permutariBacktracking(elemente, folosit, permutareCurenta);
// BACKTRACK
folosit[i] = false;
permutareCurenta.pop_back();
}
}
}
// Metoda 2: Folosind funcția next_permutation din STL
void permutariSTL(vector<int> elemente) {
// IMPORTANT: vectorul trebuie sortat inițial!
sort(elemente.begin(), elemente.end());
do {
afisarePermutare(elemente);
} while(next_permutation(elemente.begin(), elemente.end()));
}
int main() {
cout << "=== PERMUTĂRI - 3 ELEMENTE ===\n\n";
vector<int> elemente = {1, 2, 3};
cout << "Metoda Backtracking:\n";
vector<bool> folosit(3, false);
vector<int> permutareCurenta;
permutariBacktracking(elemente, folosit, permutareCurenta);
cout << "\nMetoda STL (next_permutation):\n";
permutariSTL(elemente);
cout << "\nNumar total de permutari: 3! = "
<< 3*2*1 << " = 6 permutari\n";
return 0;
}
3. Ce sunt Aranjamentele? “Selecția cu Ordine”
Definiție:
Un aranjament este o selecție de k elemente dintr-o mulțime de n elemente, unde ordinea contează.
Formula matematică:
A(n, k) = n! / (n-k)! = n × (n-1) × (n-2) × ... × (n-k+1)
Exemplu concret:
Ai 5 alergători (Ana, Bogdan, Cătălin, Dana, Eduard). Vrei să alegi:
- Primul loc (medalia de aur)
- Al doilea loc (medalia de argint)
Câte posibilități ai?
Primul loc: 5 posibilități
Al doilea loc: 4 posibilități (după ce am ales primul)
Total: 5 × 4 = 20 de aranjamente
Listă parțială a aranjamentelor:
- (Ana, Bogdan) ≠ (Bogdan, Ana) → ORDINEA CONTEAZĂ!
Implementare în C++:
#include <iostream>
#include <vector>
using namespace std;
void afisareAranjament(vector<int>& aranjament) {
for(int num : aranjament) {
cout << num << " ";
}
cout << endl;
}
void aranjamenteBacktracking(vector<int>& elemente,
vector<bool>& folosit,
vector<int>& aranjamentCurent,
int k) {
if(aranjamentCurent.size() == k) {
afisareAranjament(aranjamentCurent);
return;
}
for(int i = 0; i < elemente.size(); i++) {
if(!folosit[i]) {
// ADAUGĂ elementul
folosit[i] = true;
aranjamentCurent.push_back(elemente[i]);
// CONTINUĂ recursiv
aranjamenteBacktracking(elemente, folosit, aranjamentCurent, k);
// BACKTRACK
folosit[i] = false;
aranjamentCurent.pop_back();
}
}
}
int main() {
cout << "=== ARANJAMENTE ===\n\n";
cout << "Multime: {1, 2, 3, 4}\n";
cout << "Aranjamente de cate 2 elemente (A(4,2)):\n\n";
vector<int> elemente = {1, 2, 3, 4};
vector<bool> folosit(4, false);
vector<int> aranjamentCurent;
aranjamenteBacktracking(elemente, folosit, aranjamentCurent, 2);
cout << "\nFormula: A(4,2) = 4! / (4-2)! = 4×3 = 12 aranjamente\n";
cout << "Observatie: (1,2) ≠ (2,1) - ordinea conteaza!\n";
return 0;
}
4. Ce sunt Combinările? “Selecția fără Ordine”
Definiție:
O combinare este o selecție de k elemente dintr-o mulțime de n elemente, unde ordinea NU contează.
Formula matematică:
C(n, k) = n! / [k! × (n-k)!]
Notație: C(n, k) se citește “combinări de n luate câte k” sau “n alege k”
Exemplu concret:
Ai 5 prieteni și vrei să iei 2 cu tine la cinema.
- Alegi pe Maria și Ion = același lucru ca Ion și Maria
- ORDINEA NU CONTEAZĂ! Sunt aceeași combinație.
Diferența față de aranjamente:
- Aranjamente: (Maria, Ion) ≠ (Ion, Maria) → 2 aranjamente diferite
- Combinări: {Maria, Ion} = {Ion, Maria} → 1 singură combinație
Tabel comparativ Permutări vs Aranjamente vs Combinări:
| Concept | Ordinea contează? | Selectezi toate? | Formula | Exemplu cu {A,B,C} |
|---|---|---|---|---|
| Permutări | DA | DA (toate n) | P(n)=n! | ABC, ACB, BAC, BCA, CAB, CBA |
| Aranjamente de k | DA | NU (doar k) | A(n,k)=n!/(n-k)! | AB, AC, BA, BC, CA, CB |
| Combinări de k | NU | NU (doar k) | C(n,k)=n!/[k!(n-k)!] | {A,B}, {A,C}, {B,C} |
Implementare în C++:
#include <iostream>
#include <vector>
using namespace std;
void afisareCombinare(vector<int>& combinare) {
cout << "{ ";
for(int num : combinare) {
cout << num << " ";
}
cout << "}" << endl;
}
void combinariBacktracking(vector<int>& elemente,
vector<int>& combinareCurenta,
int start, int k) {
if(combinareCurenta.size() == k) {
afisareCombinare(combinareCurenta);
return;
}
// Start de la 'start' pentru a evita duplicatele
for(int i = start; i < elemente.size(); i++) {
// ADAUGĂ elementul
combinareCurenta.push_back(elemente[i]);
// CONTINUĂ cu următoarele elemente (i+1 pentru a evita repetiția)
combinariBacktracking(elemente, combinareCurenta, i + 1, k);
// BACKTRACK
combinareCurenta.pop_back();
}
}
int main() {
cout << "=== COMBINĂRI ===\n\n";
cout << "Multime: {1, 2, 3, 4}\n";
cout << "Combinari de cate 3 elemente (C(4,3)):\n\n";
vector<int> elemente = {1, 2, 3, 4};
vector<int> combinareCurenta;
combinariBacktracking(elemente, combinareCurenta, 0, 3);
cout << "\nFormula: C(4,3) = 4! / [3! × (4-3)!] = 4 combinari\n";
cout << "Observatie: {1,2,3} = {3,2,1} - ordinea NU conteaza!\n";
// Calculăm C(4,3) matematic
cout << "\nCalcul matematic:\n";
cout << "C(4,3) = 4! / (3! × 1!) = (4×3×2×1) / [(3×2×1) × 1] = 24 / 6 = 4\n";
return 0;
}
5. Triunghiul lui Pascal: “Harta Combinărilor”
Triunghiul lui Pascal este o modalitate ușoară de a calcula combinările:
C(0,0)
C(1,0) C(1,1)
C(2,0) C(2,1) C(2,2)
C(3,0) C(3,1) C(3,2) C(3,3)
C(4,0) C(4,1) C(4,2) C(4,3) C(4,4)
Care se traduce în:
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
Regula: Fiecare număr este suma celor două numere de deasupra lui.
Implementare triunghi Pascal:
#include <iostream>
#include <vector>
using namespace std;
void afisareTriunghiPascal(int n) {
vector<vector<int>> triunghi(n);
for(int i = 0; i < n; i++) {
triunghi[i].resize(i + 1);
triunghi[i][0] = triunghi[i][i] = 1;
for(int j = 1; j < i; j++) {
triunghi[i][j] = triunghi[i-1][j-1] + triunghi[i-1][j];
}
}
cout << "Triunghiul lui Pascal pentru n=" << n << ":\n\n";
for(int i = 0; i < n; i++) {
// Spații pentru centrare
for(int s = 0; s < n - i - 1; s++) {
cout << " ";
}
for(int j = 0; j <= i; j++) {
cout << triunghi[i][j] << " ";
}
cout << endl;
}
cout << "\nRelatia cu combinari: triunghi[i][j] = C("
<< "i" << ", " << "j" << ")\n";
cout << "Exemplu: C(4,2) = " << triunghi[4][2] << " (vezi linia 4)\n";
}
int combinariDinTriunghi(int n, int k) {
if(k > n) return 0;
if(k == 0 || k == n) return 1;
vector<int> randCurent(n + 1, 0);
vector<int> randAnterior(n + 1, 0);
randAnterior[0] = 1;
for(int i = 1; i <= n; i++) {
randCurent[0] = randCurent[i] = 1;
for(int j = 1; j < i; j++) {
randCurent[j] = randAnterior[j-1] + randAnterior[j];
}
randAnterior = randCurent;
}
return randCurent[k];
}
int main() {
cout << "=== TRIUNGHIUL LUI PASCAL ===\n\n";
afisareTriunghiPascal(6);
cout << "\nCalcul C(5,2) folosind triunghiul:\n";
cout << "C(5,2) = " << combinariDinTriunghi(5, 2) << endl;
return 0;
}
6. Formula Combinatorică: “Calculul Direct”
Funcție pentru factorial:
#include <iostream>
using namespace std;
// Funcție pentru factorial
long long factorial(int n) {
long long result = 1;
for(int i = 2; i <= n; i++) {
result *= i;
}
return result;
}
// Permutări directe
long long permutari(int n) {
return factorial(n);
}
// Aranjamente directe
long long aranjamente(int n, int k) {
if(k > n) return 0;
long long result = 1;
for(int i = 0; i < k; i++) {
result *= (n - i);
}
return result;
}
// Combinări directe
long long combinari(int n, int k) {
if(k > n) return 0;
if(k == 0 || k == n) return 1;
// Folosim proprietatea C(n,k) = C(n,n-k)
if(k > n - k) {
k = n - k;
}
long long result = 1;
for(int i = 0; i < k; i++) {
result *= (n - i);
result /= (i + 1);
}
return result;
}
int main() {
cout << "=== CALCUL DIRECT COMBINATORIC ===\n\n";
cout << "Factoriale:\n";
cout << "5! = " << factorial(5) << endl;
cout << "10! = " << factorial(10) << endl;
cout << "\nPermutari:\n";
cout << "P(4) = " << permutari(4) << endl;
cout << "\nAranjamente:\n";
cout << "A(6,2) = " << aranjamente(6, 2) << endl;
cout << "A(5,3) = " << aranjamente(5, 3) << endl;
cout << "\nCombinari:\n";
cout << "C(6,2) = " << combinari(6, 2) << endl;
cout << "C(10,3) = " << combinari(10, 3) << endl;
cout << "C(52,5) = combinatii la poker = " << combinari(52, 5) << endl;
return 0;
}
7. Aplicații Practice în Probleme Reale
Problema 1: Loto 6/49
Câte combinații există la Loto 6/49?
C(49,6) = 49! / (6! × 43!) = 13.983.816 combinații
Problema 2: Formarea Comisiilor
Avem 10 persoane. Câte comisii de 3 persoane putem forma?
C(10,3) = 120 de comisii
Problema 3: Handshake Problem
Într-o cameră cu n persoane, fiecare dă mâna cu fiecare. Câte strângeri de mână?
C(n,2) = n × (n-1) / 2
Pentru 10 persoane: C(10,2) = 45 de strângeri de mână
Problema 4: Coduri PIN
Un PIN are 4 cifre (0-9). Câte coduri posibile dacă:
a) Cifrele pot fi repetate? → 10^4 = 10.000
b) Cifrele sunt distincte? → A(10,4) = 5.040
Implementare probleme practice:
#include <iostream>
using namespace std;
long long combinari(int n, int k);
int main() {
cout << "=== APLICAȚII PRACTICE ===\n\n";
// 1. Loto 6/49
cout << "1. LOTO 6/49:\n";
cout << " C(49,6) = " << combinari(49, 6) << " combinatii\n";
cout << " Sansele de castig: 1/" << combinari(49, 6) << endl;
// 2. Comisii
cout << "\n2. COMISII:\n";
cout << " Din 10 persoane, comisii de 3: C(10,3) = "
<< combinari(10, 3) << endl;
cout << " Din 20 de studenti, comisii de 5: C(20,5) = "
<< combinari(20, 5) << endl;
// 3. Strângeri de mână
cout << "\n3. STRÂNGERI DE MÂNĂ:\n";
int persoane = 15;
cout << " La o petrecere cu " << persoane << " persoane:\n";
cout << " C(" << persoane << ",2) = " << combinari(persoane, 2)
<< " strangeri de mana\n";
// 4. Coduri PIN
cout << "\n4. CODURI PIN:\n";
cout << " PIN de 4 cifre (0-9):\n";
cout << " a) Cifrele pot fi repetate: 10^4 = "
<< 10*10*10*10 << " coduri\n";
cout << " b) Cifrele sunt distincte: A(10,4) = ";
long long aranj = 1;
for(int i = 0; i < 4; i++) {
aranj *= (10 - i);
}
cout << aranj << " coduri\n";
return 0;
}
8. Algoritmi Avansați pentru Generare
Generarea Combinărilor cu Gray Code:
Metodă care generează combinații astfel încât două combinații consecutive diferă prin exact un element.
#include <iostream>
#include <vector>
#include <bitset>
using namespace std;
void combinariGrayCode(int n, int k) {
if(k == 0 || k > n) return;
vector<int> combinare(k);
// Inițializare: primele k elemente
for(int i = 0; i < k; i++) {
combinare[i] = i;
}
int pozitie = k - 1;
while(pozitie >= 0) {
// Afișează combinația curentă
cout << "{ ";
for(int i = 0; i < k; i++) {
cout << combinare[i] << " ";
}
cout << "}" << endl;
if(combinare[k-1] == n-1) {
pozitie--;
} else {
pozitie = k - 1;
}
if(pozitie >= 0) {
for(int i = k-1; i >= pozitie; i--) {
combinare[i] = combinare[pozitie] + i - pozitie + 1;
}
}
}
}
int main() {
cout << "=== COMBINĂRI GRAY CODE ===\n\n";
cout << "Combinari de 3 luate cate 2 (ordine speciala):\n\n";
combinariGrayCode(3, 2);
return 0;
}
9. Probleme Clasice de Combinatorică
Problema 1: Numărul de Funcții
Câte funcții f: A → B există, unde |A|=m, |B|=n?
Răspuns: n^m
Exemplu: A={a,b}, B={1,2,3} → 3^2 = 9 funcții
Problema 2: Numărul de Funcții Injective
Câte funcții injective f: A → B există?
Răspuns: A(n,m) = n!/(n-m)!
Exemplu: A={a,b}, B={1,2,3,4} → A(4,2)=12 funcții injective
Problema 3: Numărul de Submulțimi
Câte submulțimi are o mulțime cu n elemente?
Răspuns: 2^n
Demonstrație: Fiecare element poate fi "în" sau "în afară"
Implementare probleme clasice:
#include <iostream>
#include <cmath>
using namespace std;
long long factorial(int n) {
long long result = 1;
for(int i = 2; i <= n; i++) result *= i;
return result;
}
long long aranjamente(int n, int m) {
if(m > n) return 0;
long long result = 1;
for(int i = 0; i < m; i++) result *= (n - i);
return result;
}
int main() {
cout << "=== PROBLEME CLASICE COMBINATORICE ===\n\n";
// Problema 1: Numărul de funcții
cout << "1. NUMARUL DE FUNCTII f: A→B\n";
int m = 3, n = 4;
cout << " |A|=" << m << ", |B|=" << n << endl;
cout << " Numar functii: " << n << "^" << m << " = "
<< (long long)pow(n, m) << endl;
// Problema 2: Funcții injective
cout << "\n2. FUNCTII INJECTIVE f: A→B\n";
m = 3; n = 5;
cout << " |A|=" << m << ", |B|=" << n << endl;
cout << " Numar functii injective: A(" << n << "," << m
<< ") = " << aranjamente(n, m) << endl;
// Problema 3: Submulțimi
cout << "\n3. SUBMULTIMILE UNEI MULTIMI\n";
n = 5;
cout << " O multime cu " << n << " elemente are:\n";
cout << " 2^" << n << " = " << (1LL << n) << " submultimi\n";
cout << " Inclusiv multimea vida si intreaga multime\n";
// Problema 4: Permutări cu repetiție
cout << "\n4. PERMUTARI CU REPETITIE\n";
cout << " Cuvinte cu literele 'M', 'A', 'T', 'E'\n";
cout << " M apare 2 ori, A apare 1, T apare 1, E apare 1\n";
cout << " Numar permutari: 5! / (2! × 1! × 1! × 1!) = "
<< factorial(5) / (factorial(2)*factorial(1)*factorial(1)*factorial(1))
<< " cuvinte\n";
return 0;
}
10. Optimizări și Limitări
Limitări numerice:
#include <iostream>
#include <limits>
using namespace std;
int main() {
cout << "=== LIMITE NUMERICE ÎN COMBINATORICĂ ===\n\n";
cout << "Factorialul crește FOARTE repede:\n";
cout << "10! = 3.628.800\n";
cout << "15! = 1.307.674.368.000 (depășește int32)\n";
cout << "20! = 2.432.902.008.176.640.000\n";
cout << "21! depășește 64-bit signed! (2^63-1 ≈ 9.22×10^18)\n";
cout << "\nConsecințe pentru programare:\n";
cout << "1. Folosește 'long long' pentru factoriale mici\n";
cout << "2. Pentru n>20, ai nevoie de aritmetică mare\n";
cout << "3. Combinările pot fi mari chiar dacă n e mic\n";
cout << " Exemplu: C(100,50) ≈ 1.0×10^29\n";
return 0;
}
Optimizări pentru combinări mari:
#include <iostream>
using namespace std;
// Combinări folosind formula recursivă C(n,k) = C(n-1,k-1) + C(n-1,k)
// Cu memoizare pentru eficiență
const int MAX_N = 100;
long long combMemo[MAX_N][MAX_N];
void initializareMemo() {
for(int i = 0; i < MAX_N; i++) {
combMemo[i][0] = combMemo[i][i] = 1;
for(int j = 1; j < i; j++) {
combMemo[i][j] = combMemo[i-1][j-1] + combMemo[i-1][j];
}
}
}
long long combinariMemo(int n, int k) {
if(k < 0 || k > n) return 0;
if(k == 0 || k == n) return 1;
if(combMemo[n][k] != 0) return combMemo[n][k];
combMemo[n][k] = combinariMemo(n-1, k-1) + combinariMemo(n-1, k);
return combMemo[n][k];
}
int main() {
initializareMemo();
cout << "=== COMBINĂRI CU MEMOIZARE ===\n\n";
cout << "C(10,5) = " << combinariMemo(10, 5) << endl;
cout << "C(20,10) = " << combinariMemo(20, 10) << endl;
cout << "C(30,15) = " << combinariMemo(30, 15) << endl;
return 0;
}
11. Tabel Sinoptic Final
| Concept | Formula | Exemplu | Aplicație |
|---|---|---|---|
| Permutări | P(n) = n! | P(3)=6 | Așezare pe scaune, ordine în playlist |
| Aranjamente | A(n,k)=n!/(n-k)! | A(4,2)=12 | Podium (locul 1 și 2), parole cu cifre distincte |
| Combinări | C(n,k)=n!/[k!(n-k)!] | C(4,2)=6 | Loterii, comisii, strângeri de mână |
| Permutări cu repetiție | n!/(n1!×n2!×…×nk!) | “BANANA”=60 | Anagrame, aranjare obiecte identice |
| Combinații cu repetiție | C(n+k-1,k) | C(3+2-1,2)=6 | Alegere fructe când poți lua același de mai multe ori |
12. Concluzie și Reguli Practice
Când folosești fiecare concept:
- Folosește PERMUTĂRI când:
- Ai toate elementele
- ORDINEA contează
- Exemplu: aranjare cărți pe raft
- Folosește ARANJAMENTE când:
- Selectezi doar unele elemente (k din n)
- ORDINEA contează
- Exemplu: alegere primul și al doilea premiu
- Folosește COMBINĂRI când:
- Selectezi doar unele elemente (k din n)
- ORDINEA NU contează
- Exemplu: formare echipă, extragere la loto
Reguli de aur pentru programare:
// 1. Pentru n mic (<10): poti genera toate permutarile
// 2. Pentru n mediu (10-20): combinațiile încă sunt gestionabile
// 3. Pentru n mare (>20): folosește formula directă, nu genera!
// 4. Atenție la overflow! Folosește long long pentru n>12
// 5. Pentru combinări: C(n,k) = C(n,n-k) → alege k mai mic
Exercițiu final de sinteză:
#include <iostream>
using namespace std;
int main() {
cout << "=== EXERCIȚIU FINAL - SINTEZĂ ===\n\n";
cout << "Problema: La o petrecere sunt 8 persoane.\n\n";
// 1. Strângeri de mână
cout << "1. Cate strangeri de mana au loc?\n";
cout << " R: C(8,2) = " << (8*7/2) << endl;
// 2. Fotografie de grup
cout << "\n2. In cate moduri pot sta in rand pentru o fotografie?\n";
cout << " R: P(8) = 8! = 40320 moduri\n";
// 3. Alegere comisie
cout << "\n3. Cate comisii de 3 persoane pot fi formate?\n";
cout << " R: C(8,3) = " << (8*7*6/(3*2*1)) << " comisii\n";
// 4. Alegere presedinte, vice, secretar
cout << "\n4. Cate variante pentru presedinte, vice, secretar?\n";
cout << " R: A(8,3) = 8×7×6 = 336 variante\n";
cout << "\n=== CONCLUZIE ===\n";
cout << "Aceeasi multime de 8 persoane,\n";
cout << "dar intrebari diferite → raspunsuri diferite!\n";
cout << "Contextul si conditiile fac diferenta.\n";
return 0;
}
Remember: Combinatorica este arta de a număra fără a număra efectiv fiecare caz! Învață să recunoști tipurile de probleme și formulele corespunzătoare, și vei rezolva cu ușurință o mulțime de probleme de programare! 🧮✨🔢
Leave a Reply