Діма і масив#2941

Діма і масив (Розбір)

Задача вимагає обробки операцій двох типів:

Нам потрібна структура даних, яка швидко виконує обидві операції. Такою структурою є дерево відрізків для сум. Кожна його вершина зберігає суму елементів свого відрізка, причому значення у внутрішній вершині дорівнює сумі значень її двох дітей.

Дерево відрізків дозволяє як обчислювати суму на відрізку, так і змінювати елемент масиву за O(\log n).

Приклад

Змоделюємо запити з прикладу. Спочатку масив дорівнює (1, 2, 3).

Подивимось, як змінюється дерево відрізків. На лівому малюнку зображено дерево для початкового масиву, а на правому — після присвоєння 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));
  }
}