kz-maxx

Fenwick Tree

Fenwick — тез обновление жасай алатын умная префиксная сумма

Префиксная сумма арқылы түсіндіру

Fenwick Tree префиксная сумма сияқты жұмыс істейді, бірақ одан ақылдырақ.

Обычная префиксная суммада біз мынаны сақтаймыз:

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-тің проблемасы бар: егер бір элемент өзгерсе, көп pref мәндерін қайта санау керек. Бұл ұзақ.

Fenwick осы проблеманы шешеді. Ол да суммаларды сақтайды, бірақ толық префикстерді емес, кішкентай отрезоктарды сақтайды.

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

tr деген не?

Fenwick ішінде бір массив болады:

int tr[N];

tr — Fenwick массиві. Әр tr[i] кішкентай бір отрезоктың суммасын сақтайды.

Ол отрезоктың размері lowbit арқылы табылады.

int lowbit(int x) {
    return x & -x;
}
tr[i] сақтайды:
i - lowbit(i) + 1
ден i дейінгі сумманы
Негізгі правило: tr[i] соңғы lowbit(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 original массивті сақтамайды. Ол әртүрлі кішкентай отрезоктардың суммасын сақтайды.

Сумма қалай табылады?

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, 7] отрезогын тез жинадық:

[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

Енді позиция 3-ті сақтайтын барлық tr[i] жаңару керек.

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: 1D 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

1D 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;
}

2D Fenwick

2D Fenwick матрица үшін керек. Ол кәдімгі 2D prefix sum сияқты, бірақ обновление тез жасайды.

int tr[N][N];

Обычная 2D префиксная суммада:

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

2D Fenwick-та tr[x][y] да прямоугольник суммасын сақтайды, бірақ әрқашан (1, 1)-ден емес, кішкентай кусок сақтайды.

tr[x][y] сақтайды:
жолдар: x - lowbit(x) + 1 ... x
бағандар: y - lowbit(y) + 1 ... y
2D Fenwick — 1D 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] жаңарады.

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;
        }
    }
}

Егер клетканың мәнін ауыстыру керек болса, қайтадан diff табамыз.

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

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

Задача 2: 2D 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-ға өзгереді.

2D 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)