Объяснение
Дерево отрезков — это структура данных, которая быстро отвечает на запросы по отрезку.
запрос: сумма на [2, 4]
ответ: 2 + 3 + 4 = 9
Без дерева отрезков нужно проходить весь отрезок, это может быть долго. Дерево отрезков делает запросы за O(log n).
Каждая вершина отвечает за свой отрезок.
[1, 3] и [4, 5]
[1, 2] и [3, 3] и [4, 4] и [5, 5]
Что храним в вершине?
struct Node {
int l, r;
long long sum;
};
- l — левая граница отрезка
- r — правая граница отрезка
- sum — сумма на этом отрезке
Задача 1: обычное дерево отрезков
Нужно поддерживать массив и делать два типа запросов.
| Команда | Что делает |
|---|---|
| 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
Полная реализация обычного ДО
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
struct Node {
int l, r;
long long sum;
} tr[N * 4];
int a[N];
void pushup(int u) {
tr[u].sum = tr[u * 2].sum + tr[u * 2 + 1].sum;
}
void build(int u, int l, int r) {
tr[u].l = l;
tr[u].r = r;
if (l == r) {
tr[u].sum = a[l];
return;
}
int mid = (l + r) / 2;
build(u * 2, l, mid);
build(u * 2 + 1, mid + 1, r);
pushup(u);
}
void change(int u, int pos, int x) {
if (tr[u].l == tr[u].r) {
tr[u].sum = x;
return;
}
int mid = (tr[u].l + tr[u].r) / 2;
if (pos <= mid) {
change(u * 2, pos, x);
} else {
change(u * 2 + 1, pos, x);
}
pushup(u);
}
long long query(int u, int l, int r) {
if (l <= tr[u].l && tr[u].r <= r) {
return tr[u].sum;
}
int mid = (tr[u].l + tr[u].r) / 2;
long long ans = 0;
if (l <= mid) {
ans += query(u * 2, l, r);
}
if (r > mid) {
ans += query(u * 2 + 1, l, r);
}
return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, q;
cin >> n >> q;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
build(1, 1, n);
while (q--) {
char op;
cin >> op;
if (op == 'Q') {
int l, r;
cin >> l >> r;
cout << query(1, l, r) << '\n';
} else if (op == 'C') {
int pos, x;
cin >> pos >> x;
change(1, pos, x);
}
}
return 0;
}
Объяснение функций обычного ДО
1. pushup
void pushup(int u) {
tr[u].sum = tr[u * 2].sum + tr[u * 2 + 1].sum;
}
pushup пересчитывает сумму вершины через двух детей.
правый сын sum = 9
тогда вершина sum = 6 + 9 = 15
2. build
void build(int u, int l, int r)
build строит дерево отрезков. Вершина u отвечает за отрезок [l, r].
build(1, 1, 4)
вершина 1: [1, 4]
левый сын: [1, 2]
правый сын: [3, 4]
Если l == r, значит это один элемент массива. Тогда сумма равна a[l].
if (l == r) {
tr[u].sum = a[l];
return;
}
3. change
void change(int u, int pos, int x)
change меняет один элемент массива. Например, change(1, 3, 10) означает: поставить a[3] = 10.
change(pos = 3, x = 10)
стало: 1 2 10 4 5
Функция идет вниз до нужной позиции. Потом пересчитывает суммы обратно через pushup.
4. query
long long query(int u, int l, int r)
query возвращает сумму на отрезке [l, r].
query(2, 4)
ответ: 2 + 3 + 4 = 9
Если текущий отрезок полностью лежит внутри запроса, мы сразу берем его сумму.
if (l <= tr[u].l && tr[u].r <= r) {
return tr[u].sum;
}
Если запрос пересекает левую часть, идем в левого сына. Если пересекает правую часть, идем в правого сына.
Задача 2: ДО с обновлениями на отрезке
Теперь нужно уметь прибавлять число ко всему отрезку.
| Команда | Что делает |
|---|---|
| A l r x | Прибавить x ко всем элементам на отрезке [l, r] |
| Q l r | Найти сумму на отрезке [l, r] |
Input
5 5
1 2 3 4 5
Q 1 5
A 2 4 10
Q 1 5
Q 2 4
Q 3 3
Output
15
45
39
13
Полная реализация ДО с pushdown
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
struct Node {
int l, r;
long long sum;
long long add;
} tr[N * 4];
int a[N];
void pushup(int u) {
tr[u].sum = tr[u * 2].sum + tr[u * 2 + 1].sum;
}
void apply(int u, long long x) {
int len = tr[u].r - tr[u].l + 1;
tr[u].sum += x * len;
tr[u].add += x;
}
void pushdown(int u) {
if (tr[u].add == 0) {
return;
}
apply(u * 2, tr[u].add);
apply(u * 2 + 1, tr[u].add);
tr[u].add = 0;
}
void build(int u, int l, int r) {
tr[u].l = l;
tr[u].r = r;
tr[u].add = 0;
if (l == r) {
tr[u].sum = a[l];
return;
}
int mid = (l + r) / 2;
build(u * 2, l, mid);
build(u * 2 + 1, mid + 1, r);
pushup(u);
}
void modify(int u, int l, int r, long long x) {
if (l <= tr[u].l && tr[u].r <= r) {
apply(u, x);
return;
}
pushdown(u);
int mid = (tr[u].l + tr[u].r) / 2;
if (l <= mid) {
modify(u * 2, l, r, x);
}
if (r > mid) {
modify(u * 2 + 1, l, r, x);
}
pushup(u);
}
long long query(int u, int l, int r) {
if (l <= tr[u].l && tr[u].r <= r) {
return tr[u].sum;
}
pushdown(u);
int mid = (tr[u].l + tr[u].r) / 2;
long long ans = 0;
if (l <= mid) {
ans += query(u * 2, l, r);
}
if (r > mid) {
ans += query(u * 2 + 1, l, r);
}
return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, q;
cin >> n >> q;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
build(1, 1, n);
while (q--) {
char op;
cin >> op;
if (op == 'A') {
int l, r;
long long x;
cin >> l >> r >> x;
modify(1, l, r, x);
} else if (op == 'Q') {
int l, r;
cin >> l >> r;
cout << query(1, l, r) << '\n';
}
}
return 0;
}
Объяснение функций с pushdown
1. pushup
void pushup(int u) {
tr[u].sum = tr[u * 2].sum + tr[u * 2 + 1].sum;
}
Работает так же, как в обычном дереве отрезков. Берем сумму левого и правого сына.
right.sum = 25
u.sum = 45
2. apply
void apply(int u, long long x) {
int len = tr[u].r - tr[u].l + 1;
tr[u].sum += x * len;
tr[u].add += x;
}
apply применяет прибавление ко всей вершине.
длина = 3
прибавляем x = 10
сумма увеличится на 10 * 3 = 30
add запоминает, что это прибавление потом нужно передать детям.
3. pushdown
void pushdown(int u) {
if (tr[u].add == 0) {
return;
}
apply(u * 2, tr[u].add);
apply(u * 2 + 1, tr[u].add);
tr[u].add = 0;
}
pushdown передает отложенное прибавление детям.
передаем +5 в [1, 2]
передаем +5 в [3, 4]
у [1, 4] add становится 0
Это нужно перед тем, как идти вниз в детей.
4. build
void build(int u, int l, int r)
Строит дерево. В этой версии еще нужно поставить add = 0, потому что сначала нет отложенных обновлений.
tr[1].l = 1
tr[1].r = 5
tr[1].add = 0
5. modify
void modify(int u, int l, int r, long long x)
modify прибавляет x ко всем элементам на отрезке [l, r].
modify(2, 4, +10)
стало: 1 12 13 14 5
Если текущая вершина полностью лежит внутри запроса, мы просто вызываем apply.
if (l <= tr[u].l && tr[u].r <= r) {
apply(u, x);
return;
}
Если нужно идти глубже, сначала вызываем pushdown, чтобы дети получили старые отложенные обновления.
6. query
long long query(int u, int l, int r)
Возвращает сумму на отрезке [l, r].
query(2, 4)
ответ: 12 + 13 + 14 = 39
Если текущий отрезок полностью внутри запроса, возвращаем tr[u].sum. Иначе перед спуском вызываем pushdown.
Асимптотика
| Операция | Время |
|---|---|
| build | O(n) |
| query | O(log n) |
| change | O(log n) |
| modify | O(log n) |