Дерево Фенвика
Определение
Дерево Фенвика - простая в реализация, быстрая в работе, но совершенно неочевидная в идее структура данных, позволяющая находить сумму на префиксе и изменять отдельные элементы за \(O(\log N)\). Несмотря на одинаковую сложность, дерево Фенвика работает значительно быстрее дерева отрезков.
В этой лекции не будет приводится точный принцип работы дерева Фенвика, будут приведены только реализация и способы использования.
Алгоритм
Дерево Фенвика, хотя и является по идее деревом, представляется в виде массива \(f\), в котором \(f[i]\) - сумма элементов от \(F(i)\) до \(i\).
Функция \(F(x)\) связана с битовым представлением числа \(x\). Её можно описать следующим образом: \(F(x)\) заменяет группу единичных битов, находящихся в конце числа (младших) на нули. Если \(x\) заканчивается на нулевой бит, то \(F(x) = x\). С помощью битовых операций \(F(x)\) записывается следующим образом:
\[F(x) = x\ and\ (x + 1)\]Значит, функцию нахождения суммы на промежутке \([0; x]\) можно реализовать следующим образом:
1
2
3
4
5
6
7
8
9
int sum(int x) {
int result = 0;
for (; x >= 0; x = (x & (x + 1)) - 1) {
result += f[x];
}
return result;
}
Осталось понять, как обновлять значения дерева Фенвика при изменении элемента. При изменении элемента \(i\) нужно обновить все \(f[j]\), где \(F(j) \le i \le j\). Оказывается, последовательность \(j\) выглядит следующим образом:
\[j_0 = i \\ j_i = j_{i - 1}\ or\ (j_{i - 1} + 1)\]Значит, обновление элемента реализуется следующим образом:
1
2
3
4
5
6
7
8
//Увеличить a[idx] на delta
void increase(int idx, int delta) {
a[idx] += delta;
for (; idx < n; idx |= idx + 1) {
f[idx] += delta;
}
}
Реализация
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <bits/stdc++.h>
using namespace std;
int n;
int a[100000]; //массив
int f[100000]; //дерево Фенвика
//сумма элементов от 0 до x
int sum(int x) {
int result = 0;
for (; x >= 0; x = (x & (x + 1)) - 1) {
result += f[x];
}
return result;
}
//сумма элементов от l до r
int sum(int l, int r) {
if (l) {
return sum(r) - sum(l - 1);
} else {
return sum(r);
}
}
//увеличение a[idx] на delta
void increase(int idx, int delta) {
a[idx] += delta;
for (; idx < n; idx |= idx + 1) {
f[idx] += delta;
}
}
int main() {
cin >> n;
for (int i = 0; i < n; i++) { //ввод массива и заполнение дерева Фенвика
int t;
cin >> t;
increase(i, t);
}
//Можем обрабатывать запросы
}
Дерево Фенвика для поиска минимума/максимума на префиксе
С помощью дерева Фенвика также можно поддерживать минимумы/максимумы на префиксах, но поддерживаемые операции такого дерева значительно ограничиваются:
-
По значениям минимума/максимума на префиксе невозможно установить значение на произвольном отрезке.
-
Возможно только уменьшение (дерево для поиска минимума)/увеличение (дерево для поиска максимума) элементов.
Реализация почти совпадает с реализацией для поиска суммы:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <bits/stdc++.h>
using namespace std;
int n;
int a[100000]; //массив
int f[100000]; //дерево Фенвика
//минимум среди элементов от 0 до x
int get_min(int x) {
int result = INT_MAX;
for (; x >= 0; x = (x & (x + 1)) - 1) {
result = min(result, f[x]);
}
return result;
}
//присваивание a[idx] = val (val <= a[idx])
void assign(int idx, int val) {
a[idx] = val;
for (; idx < n; idx |= idx + 1) {
f[idx] = min(f[idx], val);
}
}
int main() {
cin >> n;
for (int i = 0; i < n; i++) { //Изначально принимаем все значения в дереве
f[i] = INT_MAX; //равными бесконечности, так как далее их можно
} //только уменьшать.
for (int i = 0; i < n; i++) { //ввод массива и заполнение дерева Фенвика
int t;
cin >> t;
assign(i, t);
}
//Можем обрабатывать запросы.
}
Решение задачи о наибольшей возрастающей последовательности с помощью дерева Фенвика на максимумы
Эта задача уже разбиралась в лекции про динамическое программирование, и мы упоминали, что её можно решать за \(O(N \log N)\) с помощью дерева Фенвика. Разберём это решение.
Для решения важно ограничение на элементы последовательности. Если \(a_i \le 10^6\), то задачу можно решать с помощью дерева Фенвика, иначе, необходимо сначала “сжать” значения (заменить каждое значение на его индекс в порядке сортировки).
Идея решения достаточно простая: будем идти по последовательности слева направо, поддерживая массив \(f\), где \(f[i]\) - длина наибольшей возрастающей подпоследовательности, заканчивающейся на число \(i\). Изначально \(f[i] = 0\) для всех \(i\).
При обработке очередного элемента, пытаемся улучшить длину подпоследовательности, на него заканчивающейся. Для этого мы можем продолжить последовательность любой меньшей длины, или начать новую. В виде формулы это можно записать так (текущий элемент равен \(x\)):
\[f[x] = \max(f[x], \max\limits_{i = 0}^{x - 1} f[i] + 1)\]Заметим в формуле максимум на префиксе. Если находить его с помощью дерева Фенвика за \(O(\log N)\), можно решать задачу за \(O(N \log N)\).
Ответом на задачу будет максимум по всем \(f\).
Реализация на C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <bits/stdc++.h>
using namespace std;
int n;
int seq[100000]; //последовательность
int a[1000001]; //массив, соответствующий дереву Фенвика
int f[1000001]; //дерево Фенвика
int get_max(int x) {
int result = INT_MIN;
for (; x >= 0; x = (x & (x + 1)) - 1) {
result = max(result, f[x]);
}
return result;
}
//(val >= a[idx])
void assign(int idx, int val) {
a[idx] = val;
for (; idx <= 1000000; idx |= idx + 1) {
f[idx] = max(f[idx], val);
}
}
int main() {
cin >> n;
for (int i = 0; i < n; i++) {
cin >> seq[i];
}
for (int i = 0; i < n; i++) {
int x = seq[i];
int prev_max = get_max(x - 1); //максимальная длина, которую можно продолжить
if (prev_max + 1 > a[x]) {
assign(x, prev_max + 1);
}
}
int ans = get_max(1000000);
cout << ans << endl;
}
Обобщение дерева Фенвика на несколько измерений
Ещё одно преимущество дерева Фенвика перед деревом отрезков - абсолютно тривиальный способ обобщения на несколько измерений (сложность запроса для \(K\)-мерного дерева Фенвика равна \(O(2^k * \log^k N)\)). В качестве примера приведём реализацию двумерного дерева Фенвика для нахождения суммы на “префиксе” матрицы:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <bits/stdc++.h>
using namespace std;
int n, m;
int a[1000][1000]; //матрица
int f[1000][1000]; //дерево Фенвика
//сумма элементов от (0, 0) до (x, y)
int sum(int x, int y) {
if (x < 0 || y < 0) {
return 0;
}
int result = 0;
for (int i = x; i >= 0; i = (i & (i + 1)) - 1) {
for (int j = y; j >= 0; j = (j & (j + 1)) - 1) {
result += f[i][j];
}
}
return result;
}
//сумма элементов от (x1, y1) до (x2, y2)
//(разбирается в лекции про префиксные суммы)
int sum(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);
}
//увеличение a[x][y] на delta
void increase(int x, int y, int delta) {
a[x][y] += delta;
for (int i = x; i < n; i |= i + 1) {
for (int j = y; j < m; j |= j + 1) {
f[i][j] += delta;
}
}
}
int main() {
cin >> n >> m;
for (int i = 0; i < n; i++) { //ввод массива и заполнение дерева Фенвика
for (int j = 0; j < m; j++) {
int t;
cin >> t;
increase(i, j, t);
}
}
//Можем обрабатывать запросы
}