Задача вимагає обробки операцій двох типів:
присвоєння значення одному елементу масиву;
обчислення суми елементів на відрізку .
Нам потрібна структура даних, яка швидко виконує обидві операції. Такою структурою є дерево відрізків для сум. Кожна його вершина зберігає суму елементів свого відрізка, причому значення у внутрішній вершині дорівнює сумі значень її двох дітей.
Дерево відрізків дозволяє як обчислювати суму на відрізку, так і змінювати елемент масиву за O(\log n).
Приклад
Змоделюємо запити з прикладу. Спочатку масив дорівнює (1, 2, 3).
: сума елементів на відрізку дорівнює 1 + 2 + 3 = 6;
: елементу a_3 присвоюється значення , і масив стає (1, 2, 2);
: сума елементів на відрізку дорівнює 1 + 2 + 2 = 5.
Подивимось, як змінюється дерево відрізків. На лівому малюнку зображено дерево для початкового масиву, а на правому — після присвоєння a_3 = 2. Для кожної вершини показано її відрізок та суму елементів на ньому; змінені вершини виділено.
Відрізок в обох запитах збігається з відрізком кореня, тому відповідь береться безпосередньо з кореня: до присвоєння та після нього. Під час присвоєння a_3 = 2 перераховуються лише вершини на шляху від листа до кореня: лист отримує значення , а корінь отримує значення 3 + 2 = 5.
Реалізація алгоритму
Зберігатимемо дерево відрізків у векторі SegTree довжиною 4 \cdot MAX, де MAX = 5 \cdot 10^5 — максимальна кількість елементів масиву. Кожна вершина дерева зберігає суму елементів свого відрізка.
#define MAX 500000
vector<int> SegTree(4*MAX,0);Функція BuildTree будує дерево відрізків з масиву . Вона отримує номер поточної вершини та межі і відрізка, що відповідає цій вершині.
void BuildTree(vector<int>&a, int v, int lpos, int rpos)
{Якщо відрізок вершини складається з одного елемента (lpos = rpos), то — лист дерева. Записуємо в неї значення .
if (lpos == rpos) SegTree[v] = a[lpos];
else
{Інакше знаходимо середину відрізка mid = (lpos + rpos) / 2.
int mid = (lpos + rpos) / 2;Відрізок [lpos; rpos] розбивається на два підвідрізки [lpos; mid] та [mid + 1; rpos]. Рекурсивно будуємо дерево для лівого нащадка та правого нащадка 2v + 1.
BuildTree(a, 2*v, lpos, mid);
BuildTree(a, 2*v+1, mid + 1, rpos);Значення у вершині дорівнює сумі значень її нащадків.
SegTree[v] = SegTree[2*v] + SegTree[2*v+1];
}
}Функція 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];
}
}Функція Summa повертає суму елементів на відрізку [left; right]. Вершина відповідає відрізку [lpos; rpos]. Якщо запитаний відрізок порожній (left > right), повертаємо — нейтральний елемент для додавання.
int Summa(int v, int lpos, int rpos, int left, int right)
{
if (left > right) return 0;Якщо запитаний відрізок збігається з відрізком вершини, відповідь зберігається в SegTree[v].
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);
}Основна частина програми. Зчитуємо розмір масиву , кількість запитів та елементи масиву у вектор , починаючи з індексу . Виклик scanf("\n") пропускає перехід на новий рядок після масиву, щоб наступним зчитаним символом був знак операції.
scanf("%d %d",&n,&q);
v.resize(n+1);
for(i = 1; i <= n; i++)
scanf("%d",&v[i]);
scanf("\n");Будуємо дерево відрізків з елементів v[1 \ldots n].
BuildTree(v,1,1,n);Обробляємо запитів по черзі. Спочатку зчитуємо символ операції ; пробіл у форматі "%c " пропускає пробіл після нього. Якщо = '=', присвоюємо значення елементу з індексом . Інакше обчислюємо та виводимо суму на відрізку . Символ \n у форматі "%d %d\n" пропускає перехід на новий рядок, щоб наступне зчитування в отримало знак наступної операції.
for(j = 0; j < q; j++)
{
scanf("%c ",&c);
if (c == '=')
{
scanf("%d %d\n",&i,&d);
Update(1,1,n,i,d);
} else
{
scanf("%d %d\n",&f,&t);
printf("%d\n",Summa(1,1,n,f,t));
}
}