Sunteți pe pagina 1din 19

Lectia nr. 3.

– Elemente de combinatorica si jocuri combinatoriale

Elemente de combinatorica si jocuri combinatoriale

Analiza combinatorică este un domeniu al matematicii care studiază modalitățile


de numărare ale elementelor mulțimilor finite, precum și de alegere și aranjare a lor.
Numeroase concepte specifice acestui domeniu intervin și în probleme de algoritmică.

Produsul cartezian
Fie n∈N și mulțimile A1, A2,…, An. Produsul cartezian A1×A2×…× An este mulțimea
n-uplurilor (x1,x2,…,xn) cu proprietatea că x1∈A1, x2∈A2, …, xn∈An.
Regula produsului: Dacă mulțimile A1, A2., …, An au respective k1,k2., …,kn atunci
numărul de elemente al produsului cartezian A1×A2×…×An este k1⋅k2⋅…⋅kn.

Submulțimile unei mulțimi


Fie A o mulțimi cu n elemente. Atunci ea are 2n submulțimi:
Exemplu: Mulțimea A=1,2,3 are 23=8 submulțimi:

1. ∅
2. 1 ; 2 ; 3
3. 1,2 ;1,3 ; 2,3
4. 1,2,3

Permutări
Se numește permutare o corespondență biunivocă (bijecție) între o mulțime finită și ea
însăși.

Altfel spus, se numește permutare a unei mulțimi finite o aranjare a elementelor sale într-
o anumită ordine.

Exemplu: permutările mulțimii {1,2,3} sunt: (1,2,3), (1,3,2), (2,1,3), (2,3,1),


(3,1,2) și (3,2,1). Într-o altă reprezentare, acestea sunt:

(123123),(123132),(123213),(123231),(123312),(123321)
Dacă σ=(123312), atunci σ(1)=3,σ(2)=1 și σ(3)=2.
Numărul de permutări al unei mulțimi cu n elemente este Pn=n!=1⋅2⋅⋅…⋅n.
Acest articol conține mai multe despre factorialul unui număr.
Numărul permutărilor unei mulțimi crește foarte repede. Pentru valori mici ale lui n,
numărul permutărilor depășește domeniul de valori al datelor întregi, fiind necesară
implementarea operațiilor pe numere mari.

Problema
Fie un număr natural n. Să se afișeze, în ordine lexicografică, permutările
mulțimii {1,2,,⋯,n}{1,2,,⋯,n}.
Lectia nr. 3. – Elemente de combinatorica si jocuri combinatoriale

Exemplu
Pentru n=3, se va afișa:

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

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

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


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

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


backtracking, într-un mod mai formal:

• condiții externe: x[k]∈{1,2,⋯,n}x[k]∈{1,2,⋯,n};


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

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

#include <iostream>
using namespace std;
int x[10] ,n;
void Afis()
{
for( int j=1;j<=n;j++)
cout<<x[j]<<" ";
cout<<endl;
Lectia nr. 3. – Elemente de combinatorica si jocuri combinatoriale

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

Varianta iterativă
În general, algoritmii nerecursivi sunt mai buni decât cei recursivi, deși uneori sunt mai dificil
de urmărit. Următorul program iterativ afișează și el permutările pe ecran:

#include <iostream>
using namespace std;
int n , x[100];
void afisare(int k){
Lectia nr. 3. – Elemente de combinatorica si jocuri combinatoriale

for(int i = 1 ; i <= k ; i ++)


cout << x[i] << " ";
cout << endl;
}
bool OK(int k){
for(int i = 1 ; i < k ; i ++)
if(x[i] == x[k])
return 0;
return 1;
}
void back(){
int k = 1;x[1] = 0;
while(k > 0)
{
bool gasit = false;
do{
x[k] ++;
if(x[k] <= n && OK(k))
gasit = true;
}while(! gasit && x[k] <= n);
if(! gasit) k --;
else
if(k < n){
k ++;
x[k] = 0;
}
else afisare(k);
}
}
int main(){
cin >> n;
back();
return 0;
}

Probleme pbinfo: 123,124,125,1327,194,202,205,3331


Lectia nr. 3. – Elemente de combinatorica si jocuri combinatoriale

Permutări cu repetiție
Considerăm un set de n obiecte de k.tipuri. Avem n1 obiecte de tipul 1, n2 obiecte de tipul
2, …, nk obiecte de tipul k și n1+n2+…+nk=n. Numărul de permutări distincte ale celor n
obiecte este:
PR(n)=n! / (n1!⋅n2!⋅…⋅nk!)
Exemplu: Numărul de anagrame distincte ale cuvântului ABABA este:

PR(2+3)=(2+3)! / (2!⋅3!)=1⋅2⋅3⋅4⋅5 / (1⋅2⋅1⋅2⋅3)=10


Acestea sunt: AAABB, AABAB, AABBA, ABAAB, ABABA, ABBAA, BAAAB, BAABA, BABAA, BBAAA.

Probleme pbinfo: 2205

Aranjamente
Dacă A este o mulțime cu n elemente, submulțimile ordonate cu k elemente ale lui A, 0
≤ k ≤ n se numesc aranjamente a n elemente luate câte k.

Numărul de aranjamente de n luate câte k se notează


k
An =n⋅(n−1)⋅(n−2)⋅…⋅(n−k+1)⋅(n−k)=n! / (n−k)!.
La fel ca în cazul permutărilor, pentru determinarea numărului de aranjamente poate fi
necesară implementarea operațiilor pe numere mari.

Probleme pbinfo: 196,3159,1128

Combinări
Pentru o mulțime dată o combinare reprezintă o modalitate de alegere a unor elemente,
fără a ține cont de ordinea lor. Se numesc combinări de k elemente submulțimile cu k
elemente ale mulțimii, presupuse cu n elemente. Numărul de asemenea submulțimi se
numește combinări de n luate câte k și se notează Cnk.
Formule:

• Cnk=Ank / Pk
• Cnk=n⋅(n−1)⋅…⋅(n−k+1). / (1⋅2⋅…⋅k)
• Cnk=n! / (k!⋅(n−k)!)
• Cnk={ 1 dacă k=0,

(n–k+1)/k⋅Cnk altfel.

• Cnk={ 1 dacă n=k sau k=0,

Cn−1k−1+Cn−1k altfel.; această formulă se regăsește în Triunghiul lui


Pascal

Probleme pbinfo: 197,3152,3160,3330

Probleme infoarena
Lectia nr. 3. – Elemente de combinatorica si jocuri combinatoriale

https://www.infoarena.ro/problema/superbec

Triunghiul lui Pascal


este un tablou triunghiular cu numere naturale, în care fiecare element aflat pe laturile
triunghiului are valoarea 1, iar celelalte elemente sunt egale cu suma celor două elemente
vecine, situate pe linia de deasupra.

Proprietăți
Putem rearanja elementele tabloului astfel:

Considerând liniile și coloanele numerotate ca mai sus atunci:


Ai,j={1 dacă i=j sau j=0,
Ai−1,j−1+Ai−1,j altfel..
De fapt, această relație este cunoscută în calculul combinărilor:
Cnk={1 dacă n=k sau k=0,
k−1
Cn−1 +Cn−1 altfel., unde Cnk înseamnă “combinări den luate câte k”.
k
Lectia nr. 3. – Elemente de combinatorica si jocuri combinatoriale

De fapt, un element Ai,j al tabloului de mai sus este egal cu Cij.


Elementele de pe linia n sunt coeficienți binomiali ai dezvoltării
n 0 n 1 n−1 1 2 n−2 k n−k k n−1 1 n−1
(a+b) =Cn ⋅a +Cn ⋅a ⋅b +Cn ⋅a ⋅b2+⋯+Cn ⋅a ⋅b +⋯+Cn ⋅a ⋅b +Cnn⋅bn –
binomul lui Newton.
Suma elementelor de pe linia n este egală cu 2n: Cn0+Cn1+Cn2+⋯+Cnk+⋯+Cnn=2n.

Problemă
Se citește un număr natural n. Afișați linia n din triunghiul lui Pascal.

Soluție cu combinări
O primă soluție constă în calcularea valorii Cnk pentru fiecare k∈{0,1,2,⋯, n}.

#include <iostream>
using namespace std;
int comb(int n , int k)
{
int p = 1;
for(int i = 1 ; i <= k ; i ++)
p = p * (n-i+1) / i;
return p;
}
int main()
{
int n;
cin >> n;
for(int i = 0 ; i <= n ; i ++)
cout << comb(n,i) << " ";
Lectia nr. 3. – Elemente de combinatorica si jocuri combinatoriale

return 0;
}

O îmbunătățire a variantei de mai sus este:


#include <iostream>
using namespace std;
int main()
{
int n;
cin >> n;
int p = 1;
cout << "1 ";
for(int i = 1 ; i <= n ; i ++)
{
p = p * (n-i+1) / i;
cout << p << " ";
}
return 0;
}

Soluție cu matrice
În soluția următoare aplicăm formula Ai,j=Ai−1,j−1+Ai−1,j, folosind un tablou
unidimensional:
#include <iostream>
using namespace std;
int main()
{
int n, A[31][31];
cin >> n;
for(int i = 0 ; i <= n ; i ++)
{
A[i][0] = A[i][i] = 1;
for(int j = 1 ; j < i ; j ++)
A[i][j] = A[i-1][j-1] + A[i-1][j];
}
for(int j = 0 ; j <= n ; j ++)
cout << A[n][j] << " ";
Lectia nr. 3. – Elemente de combinatorica si jocuri combinatoriale

return 0;
}

Soluție cu doi vectori


Soluția de mai sus poate fi îmbunătățită, observând că pentru a calcula linia curentă folosim
doar linia precedentă. Vom folosi doar doi vectori, unul pentru linia curentă, celălalt pentru
linia precedentă:
#include <iostream>
using namespace std;

int main()
{
int n, v[31],u[31];
cin >> n;
v[0] = 0;
for(int i = 1 ; i <= n ; i ++)
{
u[0] = u[i] = 1;
for(int j = 1 ; j < i ; j ++)
u[j] = v[j-1] + v[j];
for(int j = 0 ; j <= i ; j ++)
v[j] = u[j];
}
for(int j = 0 ; j <= n ; j ++)
cout << v[j] << " ";
return 0;
}

Soluție cu doi vectori și doi pointeri


În soluția de mai sus putem evita copierea elementelor din u în v, folosind doi pointeri
suplimentari. Aceștia vor memora adresa de început a celor două tablouri, iar copierea
elementelor este înlocuită de interschimbarea valorilor celor doi pointeri:
#include <iostream>
using namespace std;

int main()
{
Lectia nr. 3. – Elemente de combinatorica si jocuri combinatoriale

int n, A[31], B[31];


int * u = A, * v = B;
cin >> n;
v[0] = 0;
for(int i = 1 ; i <= n ; i ++)
{
u[0] = u[i] = 1;
for(int j = 1 ; j < i ; j ++)
u[j] = v[j-1] + v[j];
swap(u,v);
}
for(int j = 0 ; j <= n ; j ++)
cout << v[j] << " ";
return 0;
}

Soluție cu un singur vector


Putem folosi un singur vector. Acesta conține linia anterioară și se modifică pas cu pas, pentru
a deveni linia curentă a triunghiului:
#include <iostream>
using namespace std;
int main()
{
int n, v[31];
cin >> n;
v[0] = 0;
for(int i = 1 ; i <= n ; i ++)
{
v[i] = 1;
for(int j = i - 1 ; j > 0 ; j --)
v[j] = v[j] + v[j-1];
v[0] = 1;
}
for(int j = 0 ; j <= n ; j ++)
cout << v[j] << " ";
return 0;
}
Lectia nr. 3. – Elemente de combinatorica si jocuri combinatoriale

Probleme pbinfo: 702,1718,1110,2449,1297

Probleme infoarena:

https://www.infoarena.ro/problema/magic4

https://www.infoarena.ro/problema/xor3

Numerele lui Catalan


Termenul general al acestui șir este:

𝑛+𝑘
Cn=C2nn–C2nn+1=1/(n+1)⋅C2nn=∏𝑛𝑘=2 ,pentru n≥0
𝑘
O formulă recursivă:
𝑛

𝐶𝑛+1 = ∑ 𝐶𝑖 ∙ 𝐶𝑛−𝑖
𝑖=0

Pentru n=0,1,2,3,… numere Catalan sunt: 1,1,2,5,14,42,132,429,1430,4862,….


Există numeroase probleme care au ca rezultat numerele lui Catalan. Amintim:

• Cn este egal cu numărul de șiruri de n paranteze deschise și n paranteze închise


care se închid corect. Pentru n=3, avem C3=5 variante: ((())), (())(), ()(()),
()()() și (()()).
• numărul de cuvinte Dyck de lungime 2*n: formate din n caractere X și n
caractere Y și în orice prefix umărul de X este mai mare sau egal cu numărul de
Y. Pentru n=3: XXXYYY, XXYYXY, XYXXYY, XYXYXY, XXYXYY.
• numărul de șiruri formate din n de 1 și n de -1 astfel încât toate sumele parțiale
să fie nenegative.
• Cn reprezintă numărul de modalități de a paranteza o expresie formată din n+1
operanzi ai unei operatii asociative: ((ab)c)d, (a(bc))d, (ab)(cd), a((bc)d),
a(b(cd)).
• Cn reprezintă numărul de modalităti de a desena “șiruri de munți” cu n linii
crescătoare și n linii descrescătoare. Munții încep și se termină la același nivel și
nu coboară sub acel nivel:

/\
/\ /\ /\/\ / \
/\/\/\ / \/\ /\/ \ / \ / \

• numărul de drumuri laticiale de la punctul (0,0) la (2*n,0) cu pași din


{(1,1),(1,-1) care nu coboară sub axa Ox.
• Cn este numărul de modalități de a triangula un poligon cu n+2 laturi:
Lectia nr. 3. – Elemente de combinatorica si jocuri combinatoriale

• Cn este umărul de drumuri laticiale de la (0,0) la (n,n) cu pași din


{(0,1),(1,0)} care nu depășesc prima diagonală y=x. Următoarea imagine
conține drumurile pentru n=4:

• numărul de siruri 1⩽a1⩽a2⩽…⩽an cu proprietatea că ak⩽k este Cn.


• Cn numărul de modalități de a acoperi o scară cu n trepte folosind n
dreptunghiuri. Pentru n=4:

• pe un cerc sunt 2*n puncte. Numărul posibilități de a desena n coarde care nu se


intersectează este Cn.

Alte probleme care au care rezultat numărul lui Catalan sunt pe Wikipedia sau în
documentul Exercises on Catalan and Related Numbers, Cambridge University Press,
Richard P_ Stanley, June 1998.

Probleme pbinfo: 2917,1257

Probleme inforena :

https://www.infoarena.ro/problema/permutare4

https://www.infoarena.ro/problema/colors

Soluție Catalan cu numere mari

#include<iostream>
Lectia nr. 3. – Elemente de combinatorica si jocuri combinatoriale

#define NMax 1000


using namespace std;
typedef short Huge[NMax+3];
void AtribValue(Huge H,short X) {
H[0] = 0;
while (X) {
++H[0];
H[H[0]] = X % 10;
X /= 10;
}
}
void Mult(Huge H,short X)
/* H <- H*X */
{short i,T=0;
for (i=1;i<=H[0];i++)
{ H[i]=H[i]*X+T;
T=H[i]/10;
H[i]=H[i]%10;
}
while (T) /* Cat timp exista transport */
{ H[++H[0]]=T%10;
T/=10;
}
}

void Divide(Huge A,short X)


/* A <- A/X si intoarce A%X */
{ short i,R=0;
for (i=A[0];i;i--)
{ A[i]=(R=10*R+A[i])/X;
R%=X;
}
while (!A[A[0]] && A[0]>1) A[0]--;
}

void Afisez(Huge H){


Lectia nr. 3. – Elemente de combinatorica si jocuri combinatoriale

for(short i=H[0];i>0;--i) cout<<H[i];


cout<<'\n';
}

void catalan(unsigned int n,unsigned int k)


/// Folosesc coeficientii binomiali
{
Huge res;
AtribValue(res,1);
if(k>n-k) /// C(n, k) = C(n, n-k)
k=n-k;
for(unsigned int i=0;i<k;++i)
/// Calculez valoarea [n*(n-1)*---*(n-k+1)]/[k*(k-1)*---*1]
{
Mult(res,n-i); /// res*=n-i
Divide(res,i+1); /// res/=(i+1)
}
Divide(res,n/2+1); /// res/(n/2+1)
Afisez(res);
}
int main(){
int n;
cin>>n;
catalan(2*n,n);
return 0;
}

Soluție PD pentru Catalan :

#include <iostream>
using namespace std;
unsigned long int catalanDP(unsigned int n)
{
unsigned long int catalan[n + 1];
catalan[0] = catalan[1] = 1;
for (int i = 2; i <= n; i++) {
catalan[i] = 0;
Lectia nr. 3. – Elemente de combinatorica si jocuri combinatoriale

for (int j = 0; j < i; j++)


catalan[i] += catalan[j] * catalan[i - j - 1];
}
return catalan[n];
}
int main() {
for (int i = 0; i < 10; i++)
cout << catalanDP(i) << " ";
return 0;
}

Partițiile unei mulțimi


Fie A=a1,a2,…,an o mulțime (finită). Se numește partiție a mulțimii A o familie de
submulțimi A1,A2,…,Ak ale lui A cu proprietățile:

1. Ai⊆A,∀i=1,k
2. A1∪A2∪…∪Ak=A
3. Ai∩Aj=∅, ∀i≠j

De exemplu, A=1,2,3 are următoarele partiții:

• 1,2,3
• 1,2,3, 1,3,2, 1,2,3
• 1,2,3

❖ Numărul de partiții cu k submulțimi ale unei mulțimi cu n elemente se


numește numărul lui Stirling de speța a doua, notat S(n,k). El respectă
următoarea relație de recurență:
S(0,0)=1
S(0,n)=S(n,0)=0
S(n+1,k)=k⋅S(n,k)+S(n,k−1)pentru k>0

Sursă C++ pentru calcularea S(n,k)


Putem folosi metoda programării dinamice pentru calcularea S(n,k): Vom reține într-o
matrice rezultatele subproblemelor, mai exact stir[i][j]=S(i,j).

#include <bits/stdc++.h>
using namespace std;
int main() {
int n, k; cin >> n >> k;
vector<vector<int>> stir(n + 1, vector<int>(k + 1));
stir[0][0] = 1;
for (int i = 1; i <= n; i++)
Lectia nr. 3. – Elemente de combinatorica si jocuri combinatoriale

for (int j = 1; j <= min(i, k); j++)


stir[i][j]=stir[i-1][j-1]+j*stir[i-1][j];
cout << stir[n][k] << '\n';
return 0;
}
❖ Numerele lui Stirling de speța I numără câte permutări de ordin n cu k cicluri
există, și se notează cu s(n,k).

Cazurile particulare sunt: s(0,0)=1, s(n,0)=0 (n>0) și s(0,k)=0 (k>0). Motivele sunt
similare cu cele de la numerele de speța a II-a.
Recurența este: s(n,k)=s(n−1,k−1)+(n−1)⋅s(n−1,k)

Sursă C++ pentru calcularea s(n,k)


Iată mai jos o sursă care calculează s(n,k). Este aproape identică cu cea pentru numere
lui Stirling de speța a II-a, singura diferență fiind relația de recurență (linia 10).
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, k; cin >> n >> k;
vector<vector<int>> stir(n + 1, vector<int>(k + 1));
stir[0][0] = 1;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= min(i, k); j++)
stir[i][j]=stir[i-1][j-1]+(i-1)*stir[i-1][j];
cout << stir[n][k] << '\n';
return 0;
}

Pentru mai multe informații accesați https://infogenius.ro/numerele-stirling-bell/

Soluție pentru problema de pe pbinfo: Numerele lui Stirling

#include<fstream>
#define MOD 98999
using namespace std;
ifstream fin("stirling.in");
ofstream fout("stirling.out");
int s[202][202],S[202][202];
int main(){
int t,p,n,m,i,j;
s[0][0]=1; S[0][0]=1;
for(i=1;i<=200;i++)
Lectia nr. 3. – Elemente de combinatorica si jocuri combinatoriale

for(j=1;j<=i;j++){
s[i][j]=s[i-1][j]*(1-i)+s[i-1][j-1];
s[i][j]%=MOD;
S[i][j]=S[i-1][j]*j+S[i-1][j-1];
S[i][j]%=MOD;
}
fin>>t;
for(i=1;i<=t;i++) {
fin>>p>>n>>m;
if(p==1) fout<<s[n][m];
else fout<<S[n][m];
fout<<'\n';
}
return 0;
}

• Probleme infoarena:, ex, xnumere, trineq, 100m, Permutări, Sistem, Permutări4

Numărul total de partiții ale unei mulțimi cu n elemente este numărul lui Bell:
𝑛

𝐵𝑛 = ∑ 𝑆(𝑛, 𝑘)
𝑘=0
Începând cu B0=B1=1, primele numere Bell sunt: 1,1,2,5,15,52,203,877,4140,….
Numerele Bell pot fi construite cu ajutorul așa-numitului triunghi Bell. Vom construi
(parțial) un tablou A[][]:

• A[0][1] = 1
• primul element de pe liniile următoare este egal cu ultimul de pe linia
precedentă: A[i][1] = A[i-1][i]
• celelalte elemente ale fiecărei linii sunt egale cu suma celor după elemente din
stânga (linia curentă și linia precedentă): A[i][j] = A[i][j-1] + A[i-1][j-
1], pentru j=2,..., i+1
• primul element de pe fiecare linie este numărul Bell corespunzător: Bn=A[n][1].
Lectia nr. 3. – Elemente de combinatorica si jocuri combinatoriale

Soluție:

#include<iostream>
using namespace std;
int bellNumber(int n) {
int bell[n+1][n+1];
bell[0][0] = 1;
for (int i=1; i<=n; i++) {
bell[i][0] = bell[i-1][i-1];
for (int j=1; j<=i; j++)
bell[i][j] = bell[i-1][j-1] + bell[i][j-1];
}
return bell[n][0];
}
int main() {
for (int n=0; n<=5; n++)
cout<<"Bell Number "<<n<<" este "<< bellNumber(n)<<endl;
return 0;
}

Numerele Narayana

Vezi paper20.pdf

Soluție

#include<bits/stdc++.h>
using namespace std;
int Nara(int n, int k)
{
int C[n + 1][k + 1];
for (int i = 0; i <= n; i++)
for (int j = 0; j <= min(i, k); j++)
if (j == 0 || j == i) C[i][j] = 1;
else C[i][j] = C[i - 1][j - 1] + C[i - 1][j];
return C[n][k] * C[n][k - 1];
}
int Solve(int n, int k){return (Nara(n, k)) / n;}
int main(){
Lectia nr. 3. – Elemente de combinatorica si jocuri combinatoriale

int n = 8, k = 5;
cout << Solve(n, k) << endl;
return 0;
}

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