Șmenul lui Batog (Square Root Decomposition)
Șmenul lui Batog (cunoscut și ca Square Root Decomposition sau descompunerea în radical) este o tehnică prin care împărțim un vector în blocuri de lungime √n și precalculăm informații pe fiecare bloc.
Astfel, putem răspunde la întrebări pe intervale în O(√n) în loc de O(n).
Problema
Avem un vector cu n elemente. Primim
q operații de două tipuri:
- Întrebare: care este suma/minimul/maximul
pe intervalul
[st, dr]? - Actualizare: modifică elementul de pe
poziția
pozla valoareaval.
Cu sume parțiale, răspundem la întrebări în O(1), dar actualizarea costă O(n) (trebuie recalculate).
Cu parcurgere, întrebarea costă O(n) iar actualizarea O(1).
Șmenul lui Batog oferă un compromis: ambele operații în O(√n).
Ideea
Împărțim vectorul în blocuri de lungime
B ≈ √n.
Vector cu n = 16 elemente, B = 4:
Bloc 0: Bloc 1: Bloc 2: Bloc 3:
[3 1 4 1] [5 9 2 6] [5 3 5 8] [9 7 9 3]
suma=9 suma=22 suma=21 suma=28
Pentru fiecare bloc, precalculăm suma (sau minimul, maximul - ce avem nevoie).
Cum răspundem la o întrebare?
Pentru intervalul [st, dr], avem 3 zone:
- Coada din stânga: elementele de la
stpână la sfârșitul blocului - parcurgem individual - Blocuri complete: blocurile care se încadrează complet în interval - folosim sumele precalculate
- Coada din dreapta: elementele de la
începutul ultimului bloc până la
dr- parcurgem individual
Interogare pe [2, 13]:
Bloc 0: Bloc 1: Bloc 2: Bloc 3:
[3 1 4 1] [5 9 2 6] [5 3 5 8] [9 7 9 3]
↑ ↑ complet complet ↑ ↑
coada bloc bloc coada
stânga complet complet dreapta
- Coada stânga:
v[2] + v[3]=4 + 1= 5 (2 elemente) - Blocuri complete:
suma[1] + suma[2]=22 + 21= 43 (2 blocuri) - Coada dreapta:
v[12] + v[13]=9 + 7= 16 (2 elemente) - Total: 5 + 43 + 16 = 64
Cel mult B elemente la coadă stânga +
n/B blocuri + B elemente la coadă
dreapta = O(√n).
Cum facem o actualizare?
Modificăm elementul v[poz] și recalculăm
doar suma blocului care conține acea
poziție.
v[5] = 100 (era 9)
Recalculăm doar suma blocului 1: 5 + 100 + 2 + 6 = 113
Celelalte blocuri rămân neschimbate.
Costul: O(B) = O(√n) - recalculăm un singur bloc.
Codul complet: sumă pe interval cu actualizări
#include <fstream>
using namespace std;
ifstream fin("date.in");
ofstream fout("date.out");
int v[100001];
long long bloc[320]; // √100000 ≈ 316
int n, q, B;
void construieste() {
for (int i = 1; i <= n; i++)
bloc[(i - 1) / B] = bloc[(i - 1) / B] + v[i];
}
void actualizeaza(int poz, int val) {
int b = (poz - 1) / B;
bloc[b] = bloc[b] - v[poz] + val;
v[poz] = val;
}
long long suma(int st, int dr) {
long long s = 0;
int bSt = (st - 1) / B;
int bDr = (dr - 1) / B;
if (bSt == bDr) {
// st și dr sunt în același bloc
for (int i = st; i <= dr; i++)
s = s + v[i];
return s;
}
// Coada stânga: de la st până la sfârșitul blocului bSt
for (int i = st; (i - 1) / B == bSt; i++)
s = s + v[i];
// Blocuri complete
for (int b = bSt + 1; b < bDr; b++)
s = s + bloc[b];
// Coada dreapta: de la începutul blocului bDr până la dr
for (int i = bDr * B + 1; i <= dr; i++)
s = s + v[i];
return s;
}
int main()
{
fin >> n >> q;
B = 1;
while ((B + 1) * (B + 1) <= n) B++;
for (int i = 1; i <= n; i++)
fin >> v[i];
construieste();
for (int i = 1; i <= q; i++) {
int tip;
fin >> tip;
if (tip == 1) {
int st, dr;
fin >> st >> dr;
fout << suma(st, dr) << "\n";
} else {
int poz, val;
fin >> poz >> val;
actualizeaza(poz, val);
}
}
return 0;
}date.in:
8 4
3 1 4 1 5 9 2 6
1 1 8
1 3 6
2 5 100
1 1 8date.out:
31
19
126Explicație
1 1 8- suma pe[1, 8]= 3+1+4+1+5+9+2+6 = 311 3 6- suma pe[3, 6]= 4+1+5+9 = 192 5 100-v[5]devine 100 (era 5)1 1 8- suma pe[1, 8]= 3+1+4+1+100+9+2+6 = 126
Pas
cu pas: funcția suma(3, 14) pe un vector cu n=16,
B=4
Indici: 1 2 3 4 | 5 6 7 8 | 9 10 11 12 | 13 14 15 16
Blocuri: bloc 0 | bloc 1 | bloc 2 | bloc 3
bSt = (3-1)/4 = 0,bDr = (14-1)/4 = 3- Coada stânga:
v[3] + v[4](restul blocului 0) - Blocuri complete:
bloc[1] + bloc[2] - Coada dreapta:
v[13] + v[14](începutul blocului 3)
Total: 4 operații pe elemente + 2 operații pe blocuri = 6 operații (în loc de 12 cu parcurgere).
Calculul lui B
Alegem B = √n pentru a echilibra costurile:
- Coada stânga: cel mult
Belemente - Coada dreapta: cel mult
Belemente - Blocuri complete: cel mult
n / Bblocuri - Total:
B + n/B
Minimul expresiei B + n/B se atinge când
B = √n.
| n | B = √n | Cost per operație |
|---|---|---|
| 100 | 10 | ~20 |
| 10.000 | 100 | ~200 |
| 100.000 | ~316 | ~632 |
| 1.000.000 | 1000 | ~2000 |
Comparație: cu parcurgere simplă, 100.000 de operații pe un vector de 100.000 = 10^10 (TLE). Cu sqrt decomposition = 100.000 × 632 ≈ 63.000.000 (OK).
Varianta cu minim pe interval
Aceeași idee, dar în loc de sumă, păstrăm minimul fiecărui bloc:
int bloc[320]; // minimul pe fiecare bloc
void construieste() {
for (int b = 0; b * B < n; b++) {
bloc[b] = v[b * B + 1];
for (int i = b * B + 2; i <= min(n, (b + 1) * B); i++)
if (v[i] < bloc[b])
bloc[b] = v[i];
}
}
int minim(int st, int dr) {
int mn = v[st];
int bSt = (st - 1) / B;
int bDr = (dr - 1) / B;
if (bSt == bDr) {
for (int i = st; i <= dr; i++)
if (v[i] < mn) mn = v[i];
return mn;
}
for (int i = st; (i - 1) / B == bSt; i++)
if (v[i] < mn) mn = v[i];
for (int b = bSt + 1; b < bDr; b++)
if (bloc[b] < mn) mn = bloc[b];
for (int i = bDr * B + 1; i <= dr; i++)
if (v[i] < mn) mn = v[i];
return mn;
}Comparație cu alte tehnici
| Tehnică | Întrebare | Actualizare | Precalculare |
|---|---|---|---|
| Parcurgere | O(n) | O(1) | - |
| Sume parțiale | O(1) | O(n) | O(n) |
| Sqrt Decomposition | O(√n) | O(√n) | O(n) |
| Arbore de intervale | O(log n) | O(log n) | O(n) |
Sqrt decomposition nu e cea mai rapidă, dar este simplu de implementat și suficient de eficientă pentru majoritatea problemelor.
Când alegem sqrt decomposition?
- Când avem actualizări și interogări amestecate (sume parțiale nu merg)
- Când nu vrem complexitatea implementării unui arbore de intervale
- Când limitele permit O(√n) per operație (~10^5 elemente, ~10^5 operații)
- La concursuri, când vrem o soluție rapidă de implementat
Arborele de intervale (Segment Tree) e mai rapid dar mult mai complex. Sqrt decomposition e compromisul ideal între simplitate și performanță.
Greșeli frecvente
1. Indicii blocurilor calculați greșit
Cu indexare de la 1: blocul elementului i este
(i - 1) / B. Cu indexare de la 0: blocul
elementului i este i / B.
Atenție la consistență.
2. Cazul
bSt == bDr (același bloc)
Când st și dr sunt în același bloc,
nu avem blocuri complete - parcurgem doar elementele. Dacă uiți
acest caz, calculul e greșit.
3. B calculat greșit
B = 1;
while ((B + 1) * (B + 1) <= n) B++;Sau mai simplu:
#include <cmath>
B = (int)sqrt(n);
if (B < 1) B = 1;4. Vectorul de blocuri prea mic
Dacă n = 100.000 și B = 316, avem
100.000 / 316 + 1 ≈ 317 blocuri. Declară vectorul
de blocuri cu cel puțin 320 elemente.
Ce să reții
- Sqrt decomposition împarte vectorul în blocuri de √n elemente.
- Precalculăm informația (sumă, minim, maxim) pe fiecare bloc.
- Întrebarea: parcurgem cozile + sumăm blocuri complete = O(√n).
- Actualizarea: modificăm elementul + recalculăm un bloc = O(√n).
- Cazul special: dacă
stșidrsunt în același bloc, parcurgem direct. - Este un compromis între simplitate și eficiență.
- Ideală când avem amestec de actualizări și interogări.