Algoritmul lui Mo
Algoritmul lui Mo (Mo’s Algorithm) este o tehnică pentru a răspunde eficient la interogări offline pe intervale. Se bazează pe aceeași idee de blocuri de √n ca și Șmenul lui Batog, dar abordarea e diferită: în loc să precalculăm pe blocuri, sortăm inteligent interogările și menținem un răspuns curent pe care îl actualizăm incremental.
Complexitate: O((n + q) * √n).
Când folosim algoritmul lui Mo?
Avem un vector cu n elemente și q
interogări de forma [st, dr]. Pentru fiecare,
trebuie să calculăm ceva pe acel interval (numărare elemente
distincte, frecvența maximă, sumă cu condiție, etc.).
Condiții:
- Nu avem actualizări pe vector (vectorul nu se modifică)
- Putem procesa interogările în orice ordine (offline)
- Putem adăuga și elimina un element din răspunsul curent în O(1) sau O(log n)
Ideea
Imaginează-ți că ai o fereastră [curSt, curDr]
pe vector și cunoști răspunsul pentru ea. Poți
extinde sau micșora fereastra
cu câte un element la fiecare pas:
- Adaugă element la dreapta:
curDr++ - Adaugă element la stânga:
curSt-- - Elimină element din dreapta:
curDr-- - Elimină element din stânga:
curSt++
Dacă procesăm interogările într-o ordine aleatoare, fereastra se mișcă mult. Dar dacă le sortăm inteligent, minimizăm numărul total de mișcări.
Sortarea interogărilor
Împărțim pozițiile în blocuri de √n. Sortăm interogările astfel:
- Primar: după blocul lui
st(blocul =st / B) - Secundar: după
dr(crescător)
B = 3, interogări: [1,5], [2,8], [4,7], [1,3], [5,9], [3,6]
Bloc 0 (st=1..3): [1,3], [1,5], [2,8], [3,6] ← sortate după dr
Bloc 1 (st=4..6): [4,7], [5,9] ← sortate după dr
Interogările cu st în același bloc sunt grupate,
iar dr crește monoton în cadrul grupului. Asta face
ca curDr să se miște cel mult n pași
per bloc, iar curSt cel mult B pași
per interogare.
Complexitate
- Avem
n / Bblocuri curDrparcurge cel multnpași per bloc → totaln * (n/B)=n * √ncurStse mișcă cel multBpași per interogare → totalq * B=q * √n- Total: O((n + q) * √n)
Problema clasică: numărul de elemente distincte pe interval
Avem un vector cu n elemente și q
interogări [st, dr]. Pentru fiecare, câte valori
distincte sunt pe acel interval?
Exemplu
Vector: 1 2 1 3 2 1 4 1
Interogări:
[1, 4] → valorile: 1, 2, 1, 3 → distincte: {1, 2, 3} → 3
[3, 7] → valorile: 1, 3, 2, 1, 4 → distincte: {1, 2, 3, 4} → 4
[5, 8] → valorile: 2, 1, 4, 1 → distincte: {1, 2, 4} → 3
Codul complet
#include <fstream>
#include <algorithm>
#include <cmath>
using namespace std;
ifstream fin("date.in");
ofstream fout("date.out");
int v[100001];
int fr[100001];
int rez[100001];
int n, q, B;
int distincte = 0;
struct Query {
int st, dr, idx;
};
Query qr[100001];
bool cmp(Query a, Query b) {
int blocA = a.st / B;
int blocB = b.st / B;
if (blocA != blocB)
return blocA < blocB;
return a.dr < b.dr;
}
void adauga(int poz) {
fr[v[poz]]++;
if (fr[v[poz]] == 1)
distincte++;
}
void elimina(int poz) {
fr[v[poz]]--;
if (fr[v[poz]] == 0)
distincte--;
}
int main()
{
fin >> n >> q;
B = max(1, (int)sqrt(n));
for (int i = 1; i <= n; i++)
fin >> v[i];
for (int i = 0; i < q; i++) {
fin >> qr[i].st >> qr[i].dr;
qr[i].idx = i;
}
sort(qr, qr + q, cmp);
int curSt = 1, curDr = 0;
for (int i = 0; i < q; i++) {
int st = qr[i].st;
int dr = qr[i].dr;
while (curDr < dr) adauga(++curDr);
while (curDr > dr) elimina(curDr--);
while (curSt < st) elimina(curSt++);
while (curSt > st) adauga(--curSt);
rez[qr[i].idx] = distincte;
}
for (int i = 0; i < q; i++)
fout << rez[i] << "\n";
return 0;
}date.in:
8 3
1 2 1 3 2 1 4 1
1 4
3 7
5 8date.out:
3
4
3Pas cu pas
Vector: 1 2 1 3 2 1 4 1, B = 2
Interogări sortate (bloc lui st, apoi dr): -
[1, 4] (bloc 0, dr=4) - [3, 7] (bloc
1, dr=7) - [5, 8] (bloc 2, dr=8)
Interogarea 1:
[1, 4]
Fereastra: curSt=1, curDr=0 (goală)
| Pas | Operație | Element | fr | distincte |
|---|---|---|---|---|
| 1 | adauga(1) | v[1]=1 | fr[1]=1 | 1 |
| 2 | adauga(2) | v[2]=2 | fr[2]=1 | 2 |
| 3 | adauga(3) | v[3]=1 | fr[1]=2 | 2 |
| 4 | adauga(4) | v[4]=3 | fr[3]=1 | 3 |
Răspuns: 3
Interogarea 2:
[3, 7]
Fereastra curentă: [1, 4], trebuie
[3, 7]
| Pas | Operație | Element | fr | distincte |
|---|---|---|---|---|
| 1 | adauga(5) | v[5]=2 | fr[2]=2 | 3 |
| 2 | adauga(6) | v[6]=1 | fr[1]=3 | 3 |
| 3 | adauga(7) | v[7]=4 | fr[4]=1 | 4 |
| 4 | elimina(1) | v[1]=1 | fr[1]=2 | 4 |
| 5 | elimina(2) | v[2]=2 | fr[2]=1 | 4 |
Răspuns: 4
Interogarea 3:
[5, 8]
Fereastra curentă: [3, 7], trebuie
[5, 8]
| Pas | Operație | Element | fr | distincte |
|---|---|---|---|---|
| 1 | adauga(8) | v[8]=1 | fr[1]=3 | 4 |
| 2 | elimina(3) | v[3]=1 | fr[1]=2 | 4 |
| 3 | elimina(4) | v[4]=3 | fr[3]=0 | 3 |
Răspuns: 3
Total: 12 operații. Fără Mo (brute force): 4+5+4 = 13. Diferența crește masiv pe input-uri mari.
Structura generală
Algoritmul lui Mo urmează mereu aceeași structură:
1. Citim vectorul și interogările
2. Alegem B = √n
3. Sortăm interogările (bloc st, apoi dr)
4. Pornim cu fereastra goală [curSt=1, curDr=0]
5. Pentru fiecare interogare:
a. Extindem/micșorăm fereastra cu adauga/elimina
b. Salvăm răspunsul pe poziția originală
6. Afișăm răspunsurile în ordinea originală
Ce se schimbă de la problemă la problemă sunt funcțiile
adauga și elimina.
Alt exemplu: suma valorilor distincte pe interval
Schimbăm doar adauga și
elimina:
long long sumaDistincte = 0;
void adauga(int poz) {
fr[v[poz]]++;
if (fr[v[poz]] == 1)
sumaDistincte += v[poz];
}
void elimina(int poz) {
fr[v[poz]]--;
if (fr[v[poz]] == 0)
sumaDistincte -= v[poz];
}Restul codului rămâne identic.
Alt exemplu: frecvența maximă pe interval
int frMax = 0;
int cntFr[100001]; // cntFr[f] = câte valori au frecvența f
void adauga(int poz) {
cntFr[fr[v[poz]]]--;
fr[v[poz]]++;
cntFr[fr[v[poz]]]++;
if (fr[v[poz]] > frMax)
frMax = fr[v[poz]];
}
void elimina(int poz) {
cntFr[fr[v[poz]]]--;
if (fr[v[poz]] == frMax && cntFr[fr[v[poz]]] == 0)
frMax--;
fr[v[poz]]--;
cntFr[fr[v[poz]]]++;
}De ce e mai complex la eliminare?
La adăugare, frecvența maximă poate doar crește - ușor de actualizat.
La eliminare, dacă scoatem ultimul element cu frecvența
maximă, trebuie să scădem frMax. Verificăm cu
cntFr dacă mai există elemente cu acea
frecvență.
Optimizare: ordinea alternantă
O îmbunătățire practică: pentru blocuri impare, sortăm
dr descrescător în loc de
crescător. Asta face ca curDr să “meargă și să
vină” în loc să se reseteze la fiecare bloc.
bool cmp(Query a, Query b) {
int blocA = a.st / B;
int blocB = b.st / B;
if (blocA != blocB)
return blocA < blocB;
if (blocA % 2 == 0)
return a.dr < b.dr;
return a.dr > b.dr;
}În practică, reduce timpul cu 20-40%.
Când NU funcționează Mo?
- Cu actualizări pe vector (vectorul se modifică între interogări). Există varianta “Mo with updates” dar e mult mai complexă.
- Online - când trebuie să răspunzi la fiecare interogare înainte de a o citi pe următoarea.
- Când
adauga/eliminanu se pot face eficient (O(1) sau O(log n)).
Comparație
| Tehnică | Complexitate | Actualizări | Dificultate |
|---|---|---|---|
| Brute force | O(q * n) | Da | Ușor |
| Sume parțiale | O(n + q) | Nu | Ușor |
| Sqrt decomposition | O((n + q) * √n) | Da | Mediu |
| Mo’s Algorithm | O((n + q) * √n) | Nu | Mediu |
| Segment Tree | O((n + q) * log n) | Da | Greu |
Mo e ideal pentru probleme fără actualizări unde informația pe interval e greu de combinat (elemente distincte, frecvențe).
Greșeli frecvente
1. Uitarea salvării indexului original
qr[i].idx = i; // ESENȚIAL - altfel nu știm ordinea de afișareSortăm interogările, dar răspunsurile trebuie afișate în ordinea originală.
2. Ordinea operațiilor la extindere/micșorare
// CORECT:
while (curDr < dr) adauga(++curDr);
while (curDr > dr) elimina(curDr--);
while (curSt < st) elimina(curSt++);
while (curSt > st) adauga(--curSt);Ordinea contează: extindem mai întâi, apoi micșorăm. Altfel putem avea fereastra goală momentan.
3. Vectorul de frecvențe nu e resetat
Frecvențele pornesc de la 0. Dacă reutilizăm vectorul fără resetare, rezultatele sunt greșite.
4. B prea mic sau prea mare
B = max(1, (int)sqrt(n)); // CORECTDacă B = 0, avem împărțire la zero. Dacă
B = 1, complexitatea degenerează.
Ce să reții
- Mo’s Algorithm sortează interogările offline pentru a minimiza mișcările ferestrei.
- Sortare: primar după blocul lui
st, secundar dupădr. - Complexitate: O((n + q) * √n).
- Schimbăm doar
adauga/eliminade la problemă la problemă. - Funcționează fără actualizări pe vector.
- Salvăm indexul original pentru a afișa răspunsurile în ordine.
- Optimizare: ordinea alternantă a lui
drpe blocuri impare.