Дерево Фенвика
Шесть строк вместо восьмидесяти: суммы, разности, дерево по значениям, спуск по битам и лишние измерения — и чего оно не умеет.
5 мин
Дерево Фенвика — структура для префиксных сумм с изменениями. Она не упрощённое дерево отрезков, а другой инструмент: короче, быстрее по константе, занимает ровно чисел — и умеет заметно меньше.
| дерево отрезков | дерево Фенвика | |
|---|---|---|
| память | ровно | |
| код | 40–80 строк | 6 строк |
| операции | любые ассоциативные | обратимые и префиксные |
| отложенные пометки | есть | нет |
| многомерность | тяжело | цикл на измерение |
Устройство
Ячейка хранит сумму отрезка длины i & -i, кончающегося в позиции . Здесь i & -i — младший единичный бит числа.
void add(int at, Long delta) {
for (int i = at; i <= n; i += i & -i) tree[i] += delta;
}
Long prefix(int at) {
Long total = 0;
for (int i = at; i > 0; i -= i & -i) total += tree[i];
return total;
}
Подъём i += i & -i идёт к ячейке, которая накрывает текущую; спуск i -= i & -i отрезает уже учтённый кусок и переходит к соседнему слева. Обе цепочки имеют длину .
Сумма на отрезке — разность префиксов: prefix(r) - prefix(l - 1).
Индексы начинаются с единицы. При получается
0 & -0 == 0, и цикл прибавления зацикливается — это не соглашение, а требование структуры.
Построение за линию
прибавлений дают . Быстрее — заполнить дерево значениями и «протолкнуть» каждое в накрывающую ячейку:
for (int i = 1; i <= n; i++) tree[i] += values[i];
for (int i = 1; i <= n; i++) { int up = i + (i & -i); if (up <= n) tree[up] += tree[i]; }
Приём 1: массив разностей
Если в дереве держать не значения, а разности соседей , операции меняются местами:
| что хотим | что делаем |
|---|---|
| прибавить на | add(l, v); add(r + 1, -v); |
| узнать | prefix(i) |
Если нужны и прибавление на отрезке, и сумма на отрезке, хватает двух деревьев. Из
видно, что второе дерево хранит . Прибавление пишет в оба дерева по две ячейки со сдвигами и .
Приём 2: дерево по значениям
Индекс — не позиция, а само число (после сжатия координат). Тогда префиксная сумма отвечает на «сколько добавленных меньше », и этим решаются:
- количество инверсий: идём слева направо, спрашиваем «сколько уже больших»;
- порядковые статистики в изменяющемся мультимножестве;
- «сколько точек левее и ниже» после сортировки по одной координате.
Приём 3: спуск по битам
-е по счёту находится за один логарифм, без двоичного поиска по префиксам:
int kth(Long k) {
int pos = 0;
for (int step = highestPower; step > 0; step >>= 1)
if (pos + step <= n && tree[pos + step] < k) { pos += step; k -= tree[pos]; }
return pos + 1;
}
Ячейка pos + step накрывает отрезок длины step. Если её суммы не хватает — весь отрезок левее ответа, сдвигаемся; иначе ответ внутри, уменьшаем шаг.
Приём 4: лишние измерения
Двумерное дерево Фенвика — два вложенных цикла по битам и массив :
for (int i = x; i <= n; i += i & -i)
for (int j = y; j <= m; j += j & -j)
tree[i][j] += value;
Запрос по прямоугольнику — четыре префикса со знаками включений-исключений, по параллелепипеду — восемь. В измерениях: циклов и слагаемых.
Ограничение тут не время, а память: чисел при — шестнадцать мегабайт.
Чего оно не умеет
| задача | дерево Фенвика |
|---|---|
| сумма на отрезке | умеет: разность префиксов |
| минимум на префиксе | умеет, если значения только уменьшаются |
| минимум на произвольном отрезке | нет |
| присвоение на отрезке | нет |
| любые отложенные пометки | нет |
Причина одна: отрезок получается вычитанием, а вычитать можно только обратимое. Минимум и максимум необратимы — для них нужно дерево отрезков.
Где ошибаются
Индексация с нуля. Зацикливание в add. Самая частая ошибка и самая быстрая в поиске, если про неё помнить.
Тип суммы. При и значениях до сумма доходит до : 32 бита молча переполняются.
Граница в разностях. Заведите дерево на ячейку, и проверка исчезнет сама.
Сравнение < против <= в спуске. От него зависит, какой из равных элементов найдётся; проявляется только на повторяющихся значениях.
Смежное: дерево отрезков, массив разностей, дерево по значениям, сжатие координат.