Sparse Table
Sparse Table este o structură de date care
răspunde la interogări de tip minim sau
maxim pe un interval [st, dr] în
O(1), după o precalculare de O(n log
n).
Este ideală pentru probleme fără actualizări (vectorul nu se modifică) - cunoscută ca problema RMQ (Range Minimum/Maximum Query).
Problema
Avem un vector cu n elemente și q
întrebări:
Care este minimul/maximul pe intervalul
[st, dr]?
| Tehnică | Precalculare | Interogare |
|---|---|---|
| Parcurgere | - | O(n) |
| Sqrt decomposition | O(n) | O(√n) |
| Segment Tree | O(n) | O(log n) |
| Sparse Table | O(n log n) | O(1) |
Sparse Table are cea mai rapidă interogare posibilă.
Ideea
Precalculăm răspunsul pentru toate intervalele cu lungime putere de 2: lungime 1, 2, 4, 8, 16, …
Definim sp[k][i] = minimul pe intervalul de
lungime 2^k care începe la poziția
i.
sp[0][i] = v[i](interval de lungime 1)sp[1][i] = min(v[i], v[i+1])(interval de lungime 2)sp[2][i] = min(v[i], v[i+1], v[i+2], v[i+3])(interval de lungime 4)- …
Exemplu
v: 3 1 4 1 5 9 2 6
sp[0]: 3 1 4 1 5 9 2 6 (lungime 1)
sp[1]: 1 1 1 1 5 2 2 - (lungime 2)
sp[2]: 1 1 1 1 2 2 - - (lungime 4)
sp[3]: 1 1 - - - - - - (lungime 8)
sp[2][3] = min(v[3], v[4], v[5], v[6]) = min(4, 1, 5, 9) = 1
Construirea
Formula de recurență:
sp[k][i] = min(sp[k-1][i], sp[k-1][i + 2^(k-1)])
Împărțim intervalul de lungime 2^k în
două jumătăți de lungime
2^(k-1):
Interval [i, i + 2^k - 1]:
[──── sp[k-1][i] ────][──── sp[k-1][i + 2^(k-1)] ────]
2^(k-1) 2^(k-1)
// sp[0][i] = v[i]
for (int i = 1; i <= n; i++)
sp[0][i] = v[i];
// Construim pentru k = 1, 2, ...
for (int k = 1; (1 << k) <= n; k++)
for (int i = 1; i + (1 << k) - 1 <= n; i++)
sp[k][i] = min(sp[k - 1][i], sp[k - 1][i + (1 << (k - 1))]);1 << k este operatorul de
shift pe biți. 1 << k = 2^k.
Deci 1 << 0 = 1, 1 << 1 =
2, 1 << 2 = 4, 1 << 3 =
8.
Interogarea în O(1)
Pentru intervalul [st, dr]:
- Calculăm
k= cel mai mare exponent astfel încât2^k <= dr - st + 1(lungimea intervalului). - Răspunsul =
min(sp[k][st], sp[k][dr - 2^k + 1]).
Cele două intervale de lungime 2^k se
suprapun, dar asta nu e o problemă pentru
min/max (minimul nu se schimbă dacă verificăm un element de două
ori).
Interogare [st, dr]:
[──── sp[k][st] ────────]
[──── sp[k][dr - 2^k + 1] ────]
Se suprapun la mijloc - OK pentru min/max!
Precalcularea lui
k
int lg[100001];
lg[1] = 0;
for (int i = 2; i <= n; i++)
lg[i] = lg[i / 2] + 1;lg[x] = floor(log2(x)).
Interogarea
int query(int st, int dr) {
int k = lg[dr - st + 1];
return min(sp[k][st], sp[k][dr - (1 << k) + 1]);
}Codul complet: RMQ (Range Minimum Query)
#include <fstream>
using namespace std;
ifstream fin("date.in");
ofstream fout("date.out");
int v[100001];
int sp[17][100001]; // log2(100000) ≈ 16
int lg[100001];
int n, q;
void construieste() {
for (int i = 1; i <= n; i++)
sp[0][i] = v[i];
for (int k = 1; (1 << k) <= n; k++)
for (int i = 1; i + (1 << k) - 1 <= n; i++)
sp[k][i] = min(sp[k - 1][i], sp[k - 1][i + (1 << (k - 1))]);
lg[1] = 0;
for (int i = 2; i <= n; i++)
lg[i] = lg[i / 2] + 1;
}
int query(int st, int dr) {
int k = lg[dr - st + 1];
return min(sp[k][st], sp[k][dr - (1 << k) + 1]);
}
int main()
{
fin >> n >> q;
for (int i = 1; i <= n; i++)
fin >> v[i];
construieste();
for (int i = 1; i <= q; i++) {
int st, dr;
fin >> st >> dr;
fout << query(st, dr) << "\n";
}
return 0;
}date.in:
8 4
3 1 4 1 5 9 2 6
1 8
3 6
5 8
2 4date.out:
1
1
2
1Verificare
[1,8]: min(3,1,4,1,5,9,2,6) = 1[3,6]: min(4,1,5,9) = 1[5,8]: min(5,9,2,6) = 2[2,4]: min(1,4,1) = 1
Pas cu pas: query(3, 6)
Lungime interval = 6 - 3 + 1 = 4
k = lg[4] = 2
2^k = 4
sp[2][3] = min(v[3], v[4], v[5], v[6]) = min(4, 1, 5, 9) = 1
sp[2][3] = sp[2][6 - 4 + 1] = sp[2][3] (se suprapun complet)
Răspuns: min(1, 1) = 1 ✓
Alt exemplu: query(2, 7):
Lungime = 6, k = lg[6] = 2, 2^k = 4
sp[2][2] = min(v[2], v[3], v[4], v[5]) = min(1, 4, 1, 5) = 1
sp[2][4] = min(v[4], v[5], v[6], v[7]) = min(1, 5, 9, 2) = 1
Răspuns: min(1, 1) = 1
Suprapunere: [2,5] și [4,7] se suprapun pe [4,5] - OK!
Varianta cu maxim
Schimbăm doar min cu max:
void construieste() {
for (int i = 1; i <= n; i++)
sp[0][i] = v[i];
for (int k = 1; (1 << k) <= n; k++)
for (int i = 1; i + (1 << k) - 1 <= n; i++)
sp[k][i] = max(sp[k - 1][i], sp[k - 1][i + (1 << (k - 1))]);
}
int query(int st, int dr) {
int k = lg[dr - st + 1];
return max(sp[k][st], sp[k][dr - (1 << k) + 1]);
}De ce NU funcționează pentru sumă?
La min/max, suprapunerea nu contează - minimul rămâne același chiar dacă verificăm un element de două ori.
La sumă, suprapunerea adaugă elementele din zona comună de două ori - rezultatul e greșit.
Pentru sumă pe interval, folosim sume parțiale (O(1)) sau Segment Tree (O(log n) cu actualizări).
Memorie
sp are log2(n) + 1 linii și
n coloane.
| n | Linii (log2 n) | Memorie |
|---|---|---|
| 1.000 | 10 | 10.000 int |
| 100.000 | 17 | 1.700.000 int |
| 1.000.000 | 20 | 20.000.000 int |
Pentru n = 100.000, declarăm
sp[17][100001] - aproximativ 6.5 MB.
Comparație completă
| Precalculare | Interogare | Actualizare | Memorie | |
|---|---|---|---|---|
| Parcurgere | - | O(n) | O(1) | O(n) |
| Sume parțiale | O(n) | O(1) | O(n) | O(n) |
| Sqrt decomp | O(n) | O(√n) | O(√n) | O(n) |
| Sparse Table | O(n log n) | O(1) | Nu suportă | O(n log n) |
| Segment Tree | O(n) | O(log n) | O(log n) | O(n) |
Sparse Table e cea mai rapidă la interogare dar nu suportă actualizări.
Când alegem Sparse Table?
- Vectorul nu se modifică (fără update)
- Avem foarte multe interogări (10^6+)
- Avem nevoie de min sau max (nu sumă)
- Vrem implementare relativ simplă
La OJI/ONI, Sparse Table apare la probleme RMQ statice. Dacă avem și actualizări, folosim Segment Tree.
Greșeli frecvente
1. Dimensiunea lui
sp greșită
int sp[20][100001]; // 2^20 > 100000, deci 20 e suficientDacă n poate fi 100.000, avem nevoie de maxim
log2(100000) ≈ 17 niveluri. Declaram
sp[18][...] ca să fim siguri.
2. Formula de query greșită
// GREȘIT:
sp[k][dr - (1 << k)] // lipsește +1
// CORECT:
sp[k][dr - (1 << k) + 1]3. Folosirea pentru sumă
Sparse Table cu suprapunere funcționează doar pentru min, max, gcd - operații idempotente (aplicarea de două ori pe același element nu schimbă rezultatul).
4. Uitarea precalculării
lui lg
// Fără lg precalculat, trebuie calculat la fiecare query
// Cu lg precalculat - O(1) per queryCe să reții
- Sparse Table precalculează min/max pe intervale de lungime putere de 2.
- Construire:
sp[k][i] = min(sp[k-1][i], sp[k-1][i + 2^(k-1)])- O(n log n). - Interogare:
min(sp[k][st], sp[k][dr - 2^k + 1])- O(1). - Funcționează doar pentru min, max, gcd (operații idempotente).
- Nu suportă actualizări.
- Precalculăm
lg[i] = floor(log2(i))pentru interogări rapide. 1 << k=2^k(shift pe biți).