Algoritmi de tip succesor
Algoritmii de tip succesor generează toate configurațiile unei probleme combinatoriale (submulțimi, produs cartezian, combinări, permutări) iterativ, în ordine lexicografică, fără recursivitate.
Ideea: pornim de la prima configurație (cea mai mică), și la fiecare pas calculăm succesorul - configurația imediat următoare din punct de vedere lexicografic. Când nu mai există succesor, ne oprim.
Ce înseamnă “ordine lexicografică”?
Două configurații reprezentate ca vectori
x = (x1, x2, ..., xn) și
y = (y1, y2, ..., ym) se compară ca la
dicționar:
x precede pe y dacă există
k astfel încât xi = yi pentru orice
i < k și xk < yk, sau
n < m și x e prefix al lui
y.
(1, 2, 3) < (1, 2, 4) // difera la poz 3: 3 < 4
(1, 2) < (1, 2, 5) // primul e prefix
(2, 1) > (1, 9, 9) // difera la poz 1: 2 > 1
Structura generală a unui algoritm de tip succesor
1. Inițializăm cu prima configurație (cea mai mică lexicografic)
2. Cât timp mai există succesor:
- Afișăm configurația curentă
- Generăm succesorul
Pentru a genera succesorul, căutăm cea mai din dreapta poziție care poate fi mărită, o mărim cu 1, și resetăm pozițiile din dreapta ei la valoarea minimă.
1. Produs cartezian
Problema
Fie n și lungimile
L = (L[0], L[1], ..., L[n-1]). Generăm toate
elementele produsului cartezian:
{1, 2, ..., L[0]} × {1, 2, ..., L[1]} × ... × {1, 2, ..., L[n-1]}
Exemplu
n = 3, L = (2, 3, 2). Avem
2*3*2 = 12 elemente:
(1,1,1), (1,1,2), (1,2,1), (1,2,2), (1,3,1), (1,3,2),
(2,1,1), (2,1,2), (2,2,1), (2,2,2), (2,3,1), (2,3,2)
Algoritmul
- Pornim cu
E = (1, 1, ..., 1). - Pentru succesor, găsim cea mai din dreapta poziție
icuE[i] < L[i]:- Creștem
E[i]cu 1 - Toate pozițiile din dreapta lui
i→ 1
- Creștem
- Dacă nicio poziție nu poate fi mărită, STOP.
Cod
#include <fstream>
using namespace std;
ifstream fin("date.in");
ofstream fout("date.out");
int L[101], E[101];
int n;
int main()
{
fin >> n;
for (int i = 0; i < n; i++) {
fin >> L[i];
E[i] = 1;
}
bool gata = false;
while (!gata) {
// Afisam configuratia curenta
for (int i = 0; i < n; i++)
fout << E[i] << ' ';
fout << '\n';
// Generam succesorul: cea mai din dreapta pozitie care poate creste
int i = n - 1;
while (i >= 0 && E[i] == L[i]) {
E[i] = 1;
i--;
}
if (i < 0) gata = true;
else E[i]++;
}
return 0;
}date.in:
3
2 3 2date.out (primele linii):
1 1 1
1 1 2
1 2 1
1 2 2
...Pas cu pas pentru
(2, 3, 2)
| Pas | E | Acțiune |
|---|---|---|
| 0 | 1 1 1 | Start |
| 1 | 1 1 2 | Creștem E[2] |
| 2 | 1 2 1 | E[2]=L[2], resetăm, creștem E[1] |
| 3 | 1 2 2 | Creștem E[2] |
| 4 | 1 3 1 | Resetăm E[2], creștem E[1] |
| 5 | 1 3 2 | Creștem E[2] |
| 6 | 2 1 1 | E[2]=L[2] și E[1]=L[1], resetăm, creștem E[0] |
| … | … | … |
2. Submulțimi
Problema
Generăm toate submulțimile lui
{1, 2, ..., n}.
Observație
O submulțime poate fi reprezentată cu un vector de n
biți (caracteristic): E[i] = 1 dacă
elementul i e în submulțime, 0
altfel.
Asta e echivalent cu un produs cartezian cu
L = (2, 2, ..., 2) (fiecare poziție ia valori 0 sau
1).
Cod
#include <fstream>
using namespace std;
ifstream fin("date.in");
ofstream fout("date.out");
int E[101];
int n;
int main()
{
fin >> n;
// Pornim cu submultimea vida (toti 0)
for (int i = 0; i < n; i++) E[i] = 0;
bool gata = false;
while (!gata) {
// Afisam submultimea curenta
fout << "{ ";
for (int i = 0; i < n; i++)
if (E[i] == 1)
fout << (i + 1) << ' ';
fout << "}\n";
// Succesor: cresc cea mai din dreapta pozitie cu valoarea 0
int i = n - 1;
while (i >= 0 && E[i] == 1) {
E[i] = 0;
i--;
}
if (i < 0) gata = true;
else E[i] = 1;
}
return 0;
}date.in:
3date.out:
{ }
{ 3 }
{ 2 }
{ 2 3 }
{ 1 }
{ 1 3 }
{ 1 2 }
{ 1 2 3 }Total: 2^3 = 8 submulțimi.
3. Combinări
Problema
Generăm toate submulțimile de exact k
elemente din {1, 2, ..., n}. În total,
C(n, k) configurații.
Exemplu
n = 4, k = 2. Combinări:
(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)
Reprezentăm fiecare combinare ca vector
crescător
E[0] < E[1] < ... < E[k-1].
Algoritmul
- Inițializăm
E = (1, 2, 3, ..., k)(prima combinare). - Pentru succesor:
- Găsim cea mai din dreapta poziție
icuE[i] < n - k + 1 + i(adică mai poate crește) E[i]++- Pentru
j > i:E[j] = E[j-1] + 1
- Găsim cea mai din dreapta poziție
- Dacă nicio poziție nu poate crește, STOP.
De ce n - k + 1 + i?
Pozițiile din dreapta lui i trebuie să fie
strict mai mari decât E[i]. Ultima poziție
(k-1) poate ajunge maxim la n, deci
penultima la n-1, etc. Poziția i poate
ajunge maxim la
n - (k - 1 - i) = n - k + 1 + i.
Cod
#include <fstream>
using namespace std;
ifstream fin("date.in");
ofstream fout("date.out");
int E[101];
int n, k;
int main()
{
fin >> n >> k;
// Prima combinare: 1, 2, 3, ..., k
for (int i = 0; i < k; i++)
E[i] = i + 1;
bool gata = false;
while (!gata) {
for (int i = 0; i < k; i++)
fout << E[i] << ' ';
fout << '\n';
// Succesor: gasim cea mai din dreapta pozitie care mai poate creste
int i = k - 1;
while (i >= 0 && E[i] == n - k + 1 + i)
i--;
if (i < 0) gata = true;
else {
E[i]++;
for (int j = i + 1; j < k; j++)
E[j] = E[j - 1] + 1;
}
}
return 0;
}date.in:
4 2date.out:
1 2
1 3
1 4
2 3
2 4
3 4Pas cu pas pentru
n=4, k=2
| Pas | E | Acțiune |
|---|---|---|
| 0 | 1 2 | Start |
| 1 | 1 3 | E[1]<3, creștem E[1] |
| 2 | 1 4 | E[1]<4, creștem E[1] |
| 3 | 2 3 | E[1]=4, max, scădem la E[0]. E[0]=2, E[1]=3 |
| 4 | 2 4 | Creștem E[1] |
| 5 | 3 4 | E[1]=4, max. E[0]=3, E[1]=4 |
| 6 | STOP | E[0]=3=n-k+1, E[1]=4=max |
4. Permutări
Problema
Generăm toate permutările lui {1, 2, ..., n}.
Sunt n! configurații.
Exemplu
n = 3. Permutări:
(1,2,3), (1,3,2), (2,1,3), (2,3,1), (3,1,2), (3,2,1)
Algoritmul clasic (lexicografic)
- Inițializăm
E = (1, 2, 3, ..., n). - Pentru succesor:
- Găsim cea mai din dreapta poziție
icuE[i] < E[i+1] - Găsim cea mai din dreapta poziție
j > icuE[j] > E[i] - Interschimbăm
E[i]șiE[j] - Inversăm secvența de la
i+1lan-1
- Găsim cea mai din dreapta poziție
- Dacă nu există
i, STOP.
Cod
#include <fstream>
using namespace std;
ifstream fin("date.in");
ofstream fout("date.out");
int E[101];
int n;
int main()
{
fin >> n;
for (int i = 0; i < n; i++)
E[i] = i + 1;
bool gata = false;
while (!gata) {
for (int i = 0; i < n; i++)
fout << E[i] << ' ';
fout << '\n';
// Pas 1: gasim cel mai din dreapta i cu E[i] < E[i+1]
int i = n - 2;
while (i >= 0 && E[i] >= E[i + 1])
i--;
if (i < 0) { gata = true; continue; }
// Pas 2: gasim cel mai din dreapta j > i cu E[j] > E[i]
int j = n - 1;
while (E[j] <= E[i]) j--;
// Pas 3: swap E[i] si E[j]
int tmp = E[i]; E[i] = E[j]; E[j] = tmp;
// Pas 4: inversam E[i+1..n-1]
int st = i + 1, dr = n - 1;
while (st < dr) {
tmp = E[st]; E[st] = E[dr]; E[dr] = tmp;
st++; dr--;
}
}
return 0;
}date.in:
3date.out:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1Abordarea succesor vs backtracking
Ambele generează aceleași configurații. Diferențele:
| Succesor | Backtracking | |
|---|---|---|
| Structură | iterativă (while) | recursivă |
| Ordine | lexicografică directă | lexicografică natural |
| Memorie | O(n) | O(n) stivă |
| Generalitate | specifică problemei | foarte generală |
| Simplitate | medie | mai ușor de înțeles inițial |
Succesor e util când:
- Vrei o soluție iterativă (fără stivă de apeluri)
- Problema se pretează la “cel mai din dreapta…”
- Vrei să salvezi memorie
Backtracking e util când condițiile sunt mai complexe.
Șablonul universal
Pentru orice problemă generativă de tip succesor:
// Inițializare: prima configurație
// ...
while (!gata) {
// Afișăm configurația curentă
// ...
// Generăm succesorul:
// 1. Găsim cea mai din dreapta poziție care "poate crește"
// 2. Dacă nu există, gata = true
// 3. Altfel: creștem acea poziție și resetăm/recalculăm pozițiile din dreapta
}Greșeli frecvente
1. Inițializarea greșită
// Produs cartezian: trebuie sa incepem cu (1, 1, ..., 1)
for (int i = 0; i < n; i++) E[i] = 1;
// NU 0! pentru ca multimile sunt {1, 2, ..., L[i]}2. Uitarea resetării
// Cand gasim pozitia i care poate creste, trebuie sa resetam din dreapta
while (i >= 0 && E[i] == L[i]) {
E[i] = 1; // uitarea asta duce la configuratii gresite
i--;
}3. Condiția pentru combinări
// GRESIT: if (E[i] < n) - nu tine cont de restrictiile pe urmatoarele pozitii
// CORECT: if (E[i] < n - k + 1 + i)4. Ordinea afișare/generare succesor
while (!gata) {
afiseaza(); // intai afisam
calculeaza_succesor(); // apoi succesorul
}Dacă inversezi ordinea, pierzi prima configurație sau o afișezi de două ori.
5. Confuzia E[i] vs E[i]-1 la indici
Dacă reprezentezi submulțimea cu vector binar, E[i] e
1 sau 0. Dar când afișezi elementul, e
i+1 (pentru că mulțimea e {1, ..., n},
iar indicii vectorului încep de la 0).
Ce să reții
- Algoritmii de tip succesor generează toate configurațiile unei probleme iterativ, în ordine lexicografică.
- Ideea: pornim cu prima, la fiecare pas găsim cea mai din dreapta poziție care poate crește, o creștem și resetăm pozițiile din dreapta ei.
- Aplicații clasice:
- Produs cartezian:
{1,...,L1} × ... × {1,...,Ln}-L1*L2*...*Lnelemente - Submulțimi: 2^n (caz particular cu L=(2,2,…,2))
- Combinări: C(n, k) (vector strict crescător)
- Permutări: n! (algoritm cu swap + reverse)
- Produs cartezian:
- Alternativă la backtracking - iterativ, fără recursivitate.
- Atenție la inițializare, reset și condiția de oprire.