Сума на відрізку#4255

Сума на відрізку (Розбір)

У задачі потрібно обробляти запити двох типів: присвоєння значення одному елементу масиву та обчислення суми елементів на відрізку [l; r]. Якщо зберігати лише сам масив, присвоєння виконується за , але кожен запит суми потребує операцій. Якщо зберігати префіксні суми, сума обчислюється за , але кожне присвоєння вимагає перерахунку префіксних сум. При великій кількості запитів обидва підходи є занадто повільними.

Використаємо дерево відрізків для сум. Кожна його вершина зберігає суму елементів свого відрізка, причому значення у внутрішній вершині дорівнює сумі значень її двох дітей. Дерево відрізків виконує обидві операції за O(\log n). При обчисленні суми порожня частина відрізка відповідає — нейтральному елементу для додавання.

Спочатку всі елементи масиву дорівнюють нулю, тому суми в усіх вершинах дерева також дорівнюють нулю. Достатньо заповнити масив дерева нулями; окрема побудова дерева не потрібна.

Значення елементів досягають 10^9, тому сума на відрізку може досягати 10^5 \cdot 10^9 = 10^{14} і не вміщується в тип int. Значення в дереві зберігаються в типі long long.

Розглянемо розв'язок із використанням дерева Фенвіка. Воно підтримує дві операції, кожна з яких виконується за O(\log n):

Спочатку всі елементи масиву дорівнюють нулю, тому дерево Фенвіка, заповнене нулями, вже відповідає початковому масиву. Індекси в запитах змінюються від до ; елемент a_0 не використовується і завжди дорівнює нулю.

Присвоєння. Дерево Фенвіка може лише додавати число до елемента, але не може присвоїти йому нове значення. Однак поточне значення елемента можна обчислити через префіксні суми:

a_i = sum(i) - sum(i - 1)

Тому присвоєння a_i = x замінюється на додавання до a_i різниці між новим і старим значеннями:

delta = x - (sum(i) - sum(i - 1)) = x - sum(i) + sum(i - 1)

Сума на відрізку. Сума елементів на відрізку [l; r] дорівнює різниці двох префіксних сум:

a_l + a_{l+1} + \ldots + a_r = sum(r) - sum(l - 1)

Значення елементів досягають 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));
  }
}