У задачі потрібно обробляти запити двох типів: присвоєння значення одному елементу масиву та обчислення суми елементів на відрізку [l; r]. Якщо зберігати лише сам масив, присвоєння виконується за , але кожен запит суми потребує операцій. Якщо зберігати префіксні суми, сума обчислюється за , але кожне присвоєння вимагає перерахунку префіксних сум. При великій кількості запитів обидва підходи є занадто повільними.
Використаємо дерево відрізків для сум. Кожна його вершина зберігає суму елементів свого відрізка, причому значення у внутрішній вершині дорівнює сумі значень її двох дітей. Дерево відрізків виконує обидві операції за O(\log n). При обчисленні суми порожня частина відрізка відповідає — нейтральному елементу для додавання.
Спочатку всі елементи масиву дорівнюють нулю, тому суми в усіх вершинах дерева також дорівнюють нулю. Достатньо заповнити масив дерева нулями; окрема побудова дерева не потрібна.
Значення елементів досягають 10^9, тому сума на відрізку може досягати 10^5 \cdot 10^9 = 10^{14} і не вміщується в тип int. Значення в дереві зберігаються в типі long long.
Розглянемо розв'язок із використанням дерева Фенвіка. Воно підтримує дві операції, кожна з яких виконується за O(\log n):
додавання числа до елемента a_i;
обчислення префіксної суми sum(i) = a_0 + a_1 + \ldots + a_i.
Спочатку всі елементи масиву дорівнюють нулю, тому дерево Фенвіка, заповнене нулями, вже відповідає початковому масиву. Індекси в запитах змінюються від до ; елемент a_0 не використовується і завжди дорівнює нулю.
Присвоєння. Дерево Фенвіка може лише додавати число до елемента, але не може присвоїти йому нове значення. Однак поточне значення елемента можна обчислити через префіксні суми:
Тому присвоєння a_i = x замінюється на додавання до a_i різниці між новим і старим значеннями:
Сума на відрізку. Сума елементів на відрізку [l; r] дорівнює різниці двох префіксних сум:
Значення елементів досягають 10^9, тому суми можуть досягати 10^{14} і зберігаються в типі long long.
Приклад
Спочатку масив складається з нулів. Після запитів , і масив стає (0, 2, 1, 2, 0). Дерево відрізків для нього показано нижче. У кожній вершині показано її відрізок і суму елементів на ньому.
При виконанні запиту перераховуються лише вершини на шляху від листка до кореня: листок отримує значення , вершина — значення 2 + 1 = 3, а корінь — значення 3 + 2 = 5.
Запити , , , і повертають значення листків: 0, 2, 1, 2, 0. Відрізок запиту збігається з відрізком кореня, тому відповідь береться безпосередньо з кореня і дорівнює (виділена вершина).
Просимулюємо запити з прикладу за допомогою дерева Фенвіка, n = 5. Спочатку масив (0, 0, 0, 0, 0). Для кожного запиту присвоєння в таблиці показано, як обчислюється старе значення елемента і яке число додається; для кожного запиту суми показано різницю префіксних сум.
запит | обчислення | результат |
старе значення sum(2) - sum(1) = 0 - 0 = 0, додаємо 2 - 0 = 2 | (0, 2, 0, 0, 0) | |
старе значення sum(3) - sum(2) = 2 - 2 = 0, додаємо 1 - 0 = 1 | (0, 2, 1, 0, 0) | |
старе значення sum(4) - sum(3) = 3 - 3 = 0, додаємо 2 - 0 = 2 | (0, 2, 1, 2, 0) | |
sum(1) - sum(0) = 0 - 0 | ||
sum(2) - sum(1) = 2 - 0 | ||
sum(3) - sum(2) = 3 - 2 | ||
sum(4) - sum(3) = 5 - 3 | ||
sum(5) - sum(4) = 5 - 5 | ||
sum(5) - sum(0) = 5 - 0 |
Наприклад, перед запитом масив (0, 2, 1, 2, 0), тому sum(4) = 0 + 2 + 1 + 2 = 5, sum(3) = 0 + 2 + 1 = 3, і відповідь дорівнює 5 - 3 = 2.
У прикладі всі присвоєння застосовуються до нульових елементів. Подивимось, що відбудеться при повторному присвоєнні, наприклад наприкінці. Старе значення дорівнює sum(3) - sum(2) = 3 - 2 = 1, тому до a_3 додається 5 - 1 = 4, і елемент стає рівним .
Реалізація алгоритму
Дерево відрізків зберігається в масиві SegTree довжиною 4 \cdot MAX, де — максимальна кількість елементів масиву. Сума на відрізку може перевищувати межі типу int, тому значення в дереві мають тип long long.
#define MAX 100010
long long SegTree[4 * MAX];Функція Summa повертає суму елементів на відрізку [left; right]. Вершина відповідає відрізку [lpos; rpos].
long long Summa(int v, int lpos, int rpos, int left, int right)
{Якщо запитуваний відрізок порожній (left > right), повертаємо — нейтральний елемент для додавання.
if (left > right) return 0;Якщо запитуваний відрізок збігається з відрізком [lpos; rpos] вершини , повертаємо суму, що зберігається в ній.
if ((left == lpos) && (right == rpos))
return SegTree[v];Інакше знаходимо середину відрізка mid = (lpos + rpos) / 2.
int mid = (lpos + rpos) / 2;Розбиваємо запитуваний відрізок на дві частини: [left; \min(right, mid)] лежить у відрізку лівого нащадка, а [\max(left, mid + 1); right] — у відрізку правого нащадка. Одна з частин може бути порожньою. Рекурсивно обчислюємо суми на обох частинах і повертаємо їхню суму.
return Summa(2*v, lpos, mid, left, min(right, mid)) +
Summa(2*v+1, mid + 1, rpos, max(left, mid + 1), right);
}Функція update присвоює значення елементу з індексом . Вершина відповідає відрізку [lpos; rpos].
void update(int v, int lpos, int rpos, int pos, int val)
{Якщо відрізок вершини складається з одного елемента (lpos = rpos), то — це листок, що відповідає позиції . Записуємо в нього значення .
if (lpos == rpos)
SegTree[v] = val;
else
{Інакше визначаємо, яка половина відрізка містить позицію , і рекурсивно спускаємось у лівого або правого нащадка.
int mid = (lpos + rpos) / 2;
if (pos <= mid) update(2*v, lpos, mid, pos, val);
else update(2*v+1, mid + 1, rpos, pos, val);Після повернення з рекурсії перераховуємо суму у вершині . Таким чином оновлюються всі вершини на шляху від листка до кореня.
SegTree[v] = SegTree[2*v] + SegTree[2*v+1];
}
}Основна частина програми. Спочатку всі елементи масиву дорівнюють нулю, тому всі суми в дереві відрізків також дорівнюють нулю. Заповнюємо масив SegTree нулями; окрема побудова дерева не потрібна.
memset(SegTree, 0, sizeof(SegTree));Зчитуємо розмір масиву і кількість запитів . Символ \n у форматі пропускає перехід на новий рядок, щоб наступним символом, який зчитується, була буква запиту.
scanf("%d %d\n", &n, &k);Обробляємо запитів по черзі. Спочатку зчитуємо букву запиту . Якщо вона дорівнює 'A', присвоюємо значення елементу з індексом . Інакше виводимо суму на відрізку [l; r]. Символ \n у форматі "%lld %lld\n" пропускає перехід на новий рядок після запиту.
for (j = 0; j < k; j++)
{
scanf("%c", &command);
if (command == 'A')
{
scanf("%lld %lld\n", &i, &x);
update(1, 1, n, i, x);
}
else
{
scanf("%lld %lld\n", &l, &r);
printf("%lld\n", Summa(1, 1, n, l, r));
}
}Реалізація алгоритму — дерево Фенвіка
Масив Fenwick розміру зберігає дерево Фенвіка. Глобальний масив спочатку заповнений нулями, що відповідає початковому нульовому масиву.
#define MAX 100010
long long Fenwick[MAX];Функція Summma0_i повертає префіксну суму sum(i) = a_0 + a_1 + \ldots + a_i.
// Fenwick[0] + Fenwick[1] + ... + Fenwick[i]
long long Summma0_i(long long i)
{
long long result = 0;
for (; i >= 0; i = (i & (i + 1)) - 1)
result += Fenwick[i];
return result;
}Функція IncElement додає до елемента a_i. Індекси елементів змінюються від до , тому цикл виконується, поки i \le n.
// Fenwick[i] = Fenwick[i] + delta
void IncElement(long long i, long long delta)
{
for (; i <= n; i = (i | (i+1)))
Fenwick[i] += delta;
}Основна частина програми. Зчитуємо розмір масиву і кількість запитів . Символ \n у форматі пропускає перехід на новий рядок, щоб наступним символом, який зчитується, була буква запиту.
scanf("%d %d\n",&n,&k);Обробляємо запитів по черзі. Спочатку зчитуємо букву запиту .
for(j = 0; j < k; j++)
{
scanf("%c",&command);Запит . Поточне значення елемента a_i дорівнює sum(i) - sum(i - 1). Щоб присвоїти йому значення , замінюємо на різницю x - sum(i) + sum(i - 1) і додаємо її до a_i за допомогою функції IncElement.
if (command == 'A')
{
scanf("%lld %lld\n",&i,&x);
x = x - Summma0_i(i) + Summma0_i(i-1);
IncElement(i,x);Запит . Сума на відрізку [l; r] дорівнює sum(r) - sum(l - 1).
} else
{
scanf("%lld %lld\n",&l,&r);
printf("%lld\n",Summma0_i(r) - Summma0_i(l-1));
}
}