Корневая декомпозиция (sqrt decomposition): как отвечать на запросы суммы на отрезке за O(sqrt n)?
Хочу разобраться с sqrt-декомпозицией на простом примере: запросы «сумма на отрезке [l, r]» и «изменить a[i]». Знаю, что есть дерево отрезков, но хочу понять корневую: почему именно блоки размера sqrt(n) и откуда O(sqrt n)?
2 ответа
Идея: разбиваем массив на блоки длины примерно B = sqrt(n). Для каждого блока заранее храним его сумму. Тогда:
- Точечное обновление
a[i] += d: меняемa[i]и сумму его блока — O(1). - Запрос суммы [l, r]: края обрабатываем поэлементно (не больше двух неполных блоков, каждый длины < B → O(B)), а целые блоки между ними берём уже посчитанными суммами (их не больше n/B штук → O(n/B)). Итого O(B + n/B), минимум при B = sqrt(n) → O(sqrt n) на запрос.
int n, B;
vector<long long> a, blockSum;
void build() {
B = max(1, (int)sqrt((double)n));
blockSum.assign((n + B - 1) / B, 0);
for (int i = 0; i < n; i++) blockSum[i / B] += a[i];
}
void update(int i, long long val) {
blockSum[i / B] += val - a[i];
a[i] = val;
}
long long query(int l, int r) { // включительно
long long res = 0;
int bl = l / B, br = r / B;
if (bl == br) { // один блок целиком в крае
for (int i = l; i <= r; i++) res += a[i];
} else {
for (int i = l; i < (bl + 1) * B; i++) res += a[i]; // левый край
for (int b = bl + 1; b < br; b++) res += blockSum[b]; // целые блоки
for (int i = br * B; i <= r; i++) res += a[i]; // правый край
}
return res;
}
Построение O(n), запрос/обновление O(sqrt n), память O(n). Выбор B = sqrt(n) — это минимум суммы B + n/B по AM-GM. Корневая проще дерева отрезков в реализации и легко обобщается на «отложенные» обновления на блок (пометки на блок + ленивое пересчитывание края).
Когда корневая лучше дерева отрезков: когда операция не ассоциативна красиво или сложно влезает в lazy-дерево, но легко агрегируется по блоку (например, «сколько чисел в [l,r] больше x» — храни отсортированный блок и делай бинпоиск внутри блока, O(sqrt n log n)). Минусы корневой: константа O(sqrt n) на запрос против O(log n) у дерева — при n = 1e5 это ~316 против ~17, разница в ~18 раз. Поэтому при большом числе запросов и стандартной агрегации (сумма/мин/макс) дерево отрезков предпочтительнее. Корневую берут за простоту и универсальность, когда дерево писать дольше или операция нестандартная.