kz-maxx

Дерево Фенвика

Fenwick как умная префиксная сумма: int tr[N] и int tr[N][N]

Объяснение через префиксные суммы

Дерево Фенвика работает почти как префиксная сумма, но умнее.

В обычной префиксной сумме мы храним:

pref[i] = a[1] + a[2] + ... + a[i]

Тогда сумму на отрезке можно найти быстро:

sum(l, r) = pref[r] - pref[l - 1]

Например:

a = 1 2 3 4 5
pref = 1 3 6 10 15
сумма на [2, 4] = pref[4] - pref[1]
ответ = 10 - 1 = 9
Проблема обычного pref в том, что если поменять один элемент массива, нужно пересчитывать много значений. Это долго.

Дерево Фенвика решает эту проблему. Оно тоже хранит суммы, но не все префиксы полностью, а маленькие куски префиксов.

Fenwick = префиксная сумма, которая умеет быстро обновляться

Что такое tr?

В Fenwick есть массив:

int tr[N];

tr — это массив дерева Фенвика. Каждая ячейка tr[i] хранит сумму маленького отрезка.

Размер этого отрезка зависит от функции lowbit.

int lowbit(int x) {
    return x & -x;
}
tr[i] хранит сумму от:
i - lowbit(i) + 1
до i
Главное правило: tr[i] хранит сумму последних lowbit(i) элементов до позиции i.

Как строится tr?

Пусть есть массив:

a = 1 2 3 4 5 6 7 8

Тогда tr хранит такие суммы:

i lowbit(i) Какой отрезок хранит tr[i] tr[i]
1 1 [1, 1] 1
2 2 [1, 2] 1 + 2 = 3
3 1 [3, 3] 3
4 4 [1, 4] 1 + 2 + 3 + 4 = 10
5 1 [5, 5] 5
6 2 [5, 6] 5 + 6 = 11
7 1 [7, 7] 7
8 8 [1, 8] 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 = 36

То есть tr не хранит сам массив. Он хранит суммы разных маленьких отрезков.

Как найти сумму?

Функция sum(pos) находит сумму от 1 до pos.

int sum(int pos) {
    int ans = 0;

    for (int i = pos; i > 0; i -= lowbit(i)) {
        ans += tr[i];
    }

    return ans;
}

Например, хотим найти:

sum(7) = a[1] + a[2] + ... + a[7]

Fenwick берет не каждый элемент отдельно, а готовые куски из tr.

sum(7)
берем tr[7] → это [7, 7]
потом tr[6] → это [5, 6]
потом tr[4] → это [1, 4]

Получается:

sum(7) = tr[7] + tr[6] + tr[4]
sum(7) = 7 + 11 + 10 = 28

Мы быстро собрали весь отрезок:

[1, 4] + [5, 6] + [7, 7]
это весь отрезок [1, 7]

Сумма на отрезке

Чтобы найти сумму на отрезке [l, r], используем две префиксные суммы:

int query(int l, int r) {
    return sum(r) - sum(l - 1);
}

Например:

a = 1 2 3 4 5 6 7 8
query(3, 7)
это 3 + 4 + 5 + 6 + 7 = 25

Через Fenwick:

query(3, 7) = sum(7) - sum(2)
sum(7) = 28
sum(2) = 3
ответ = 28 - 3 = 25

Как обновлять значение?

Fenwick умеет быстро прибавлять число к одному элементу.

void add(int pos, int x) {
    for (int i = pos; i <= n; i += lowbit(i)) {
        tr[i] += x;
    }
}

Например:

a = 1 2 3 4 5 6 7 8
add(3, 5)

Это значит: к a[3] прибавить 5.

было: a[3] = 3
стало: a[3] = 8

Теперь надо обновить все tr[i], которые содержат позицию 3.

add(3, 5)
обновляем tr[3]
потом tr[4]
потом tr[8]
tr[i] Какой отрезок хранит Содержит позицию 3?
tr[3] [3, 3] да
tr[4] [1, 4] да
tr[8] [1, 8] да

Поэтому к этим ячейкам прибавляем 5.

tr[3] += 5
tr[4] += 5
tr[8] += 5

Если нужно поменять значение

Если нужно не прибавить, а именно поменять a[pos] на новое значение, считаем разницу.

void change(int pos, int x) {
    int diff = x - a[pos];

    a[pos] = x;
    add(pos, diff);
}

Например:

было: a[3] = 3
хотим: a[3] = 10
diff = 10 - 3 = 7
делаем add(3, 7)

То есть Fenwick не меняет значение напрямую. Он прибавляет разницу.

Задача 1: одномерный Fenwick

Дан массив из n чисел. Нужно обработать запросы:

Команда Что делает
Q l r Найти сумму на отрезке [l, r]
C pos x Поменять a[pos] на x

Input

5 5
1 2 3 4 5
Q 1 5
Q 2 4
C 3 10
Q 1 5
Q 3 3

Output

15
9
22
10

Полная реализация одномерного Fenwick

#include <bits/stdc++.h>
using namespace std;

const int N = 100010;

int n;
int a[N];
int tr[N];

int lowbit(int x) {
    return x & -x;
}

void add(int pos, int x) {
    for (int i = pos; i <= n; i += lowbit(i)) {
        tr[i] += x;
    }
}

int sum(int pos) {
    int ans = 0;

    for (int i = pos; i > 0; i -= lowbit(i)) {
        ans += tr[i];
    }

    return ans;
}

int query(int l, int r) {
    return sum(r) - sum(l - 1);
}

void change(int pos, int x) {
    int diff = x - a[pos];

    a[pos] = x;
    add(pos, diff);
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int q;
    cin >> n >> q;

    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        add(i, a[i]);
    }

    while (q--) {
        char op;
        cin >> op;

        if (op == 'Q') {
            int l, r;
            cin >> l >> r;

            cout << query(l, r) << '\n';
        } else if (op == 'C') {
            int pos, x;
            cin >> pos >> x;

            change(pos, x);
        }
    }

    return 0;
}

Двумерный Fenwick

Двумерный Fenwick работает так же, как префиксная сумма в матрице. Только вместо одномерного массива используется таблица.

int tr[N][N];

В обычной двумерной префиксной сумме:

pref[x][y] = сумма прямоугольника от (1, 1) до (x, y)

В двумерном Fenwick tr[x][y] тоже хранит сумму прямоугольника, но не всегда от (1, 1), а маленький кусок.

tr[x][y] хранит прямоугольник:
строки: x - lowbit(x) + 1 ... x
столбцы: y - lowbit(y) + 1 ... y
Двумерный Fenwick — это тот же одномерный Fenwick, только внутри одного цикла стоит второй цикл.

Как находится сумма в 2D?

Пусть есть матрица:

1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16

Найдем sum(3, 3). Это сумма прямоугольника от (1, 1) до (3, 3).

1 + 2 + 3
5 + 6 + 7
9 + 10 + 11
ответ = 54

Fenwick берет несколько готовых прямоугольников:

sum(3, 3)
берем tr[3][3] → клетка (3, 3) = 11
берем tr[3][2] → строка 3, столбцы 1..2 = 9 + 10 = 19
берем tr[2][3] → строки 1..2, столбец 3 = 3 + 7 = 10
берем tr[2][2] → строки 1..2, столбцы 1..2 = 1 + 2 + 5 + 6 = 14

Итого:

sum(3, 3) = 11 + 19 + 10 + 14 = 54

Сумма в прямоугольнике

Чтобы найти сумму в прямоугольнике от (x1, y1) до (x2, y2), используем четыре префиксные суммы.

int query(int x1, int y1, int x2, int y2) {
    return sum(x2, y2)
         - sum(x1 - 1, y2)
         - sum(x2, y1 - 1)
         + sum(x1 - 1, y1 - 1);
}
берем большой прямоугольник
вычитаем верхнюю лишнюю часть
вычитаем левую лишнюю часть
добавляем угол, который вычли два раза

Как обновлять в 2D?

Если делаем:

add(3, 2, 5)

Это значит: к клетке (3, 2) прибавить 5. Тогда обновятся все tr[i][j], прямоугольники которых содержат клетку (3, 2).

по x: 3 → 4
по y: 2 → 4
обновятся:
tr[3][2], tr[3][4], tr[4][2], tr[4][4]
void add(int x, int y, int val) {
    for (int i = x; i <= n; i += lowbit(i)) {
        for (int j = y; j <= m; j += lowbit(j)) {
            tr[i][j] += val;
        }
    }
}

Если нужно поменять значение клетки, снова считаем разницу.

void change(int x, int y, int val) {
    int diff = val - a[x][y];

    a[x][y] = val;
    add(x, y, diff);
}

Задача 2: двумерный Fenwick

Дана матрица n × m. Нужно обрабатывать запросы:

Команда Что делает
Q x1 y1 x2 y2 Найти сумму в прямоугольнике от (x1, y1) до (x2, y2)
C x y val Поменять a[x][y] на val

Input

3 3 5
1 2 3
4 5 6
7 8 9
Q 1 1 3 3
Q 2 2 3 3
C 2 2 10
Q 1 1 3 3
Q 2 2 2 2

Output

45
28
50
10
После команды C 2 2 10 значение в центре меняется с 5 на 10.

Полная реализация двумерного Fenwick

#include <bits/stdc++.h>
using namespace std;

const int N = 1010;

int n, m;
int a[N][N];
int tr[N][N];

int lowbit(int x) {
    return x & -x;
}

void add(int x, int y, int val) {
    for (int i = x; i <= n; i += lowbit(i)) {
        for (int j = y; j <= m; j += lowbit(j)) {
            tr[i][j] += val;
        }
    }
}

int sum(int x, int y) {
    int ans = 0;

    for (int i = x; i > 0; i -= lowbit(i)) {
        for (int j = y; j > 0; j -= lowbit(j)) {
            ans += tr[i][j];
        }
    }

    return ans;
}

int query(int x1, int y1, int x2, int y2) {
    return sum(x2, y2)
         - sum(x1 - 1, y2)
         - sum(x2, y1 - 1)
         + sum(x1 - 1, y1 - 1);
}

void change(int x, int y, int val) {
    int diff = val - a[x][y];

    a[x][y] = val;
    add(x, y, diff);
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int q;
    cin >> n >> m >> q;

    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            cin >> a[i][j];
            add(i, j, a[i][j]);
        }
    }

    while (q--) {
        char op;
        cin >> op;

        if (op == 'Q') {
            int x1, y1, x2, y2;
            cin >> x1 >> y1 >> x2 >> y2;

            cout << query(x1, y1, x2, y2) << '\n';
        } else if (op == 'C') {
            int x, y, val;
            cin >> x >> y >> val;

            change(x, y, val);
        }
    }

    return 0;
}

Асимптотика

Версия Операция Время
1D Fenwick add / sum / query / change O(log n)
2D Fenwick add / sum / query / change O(log n · log m)