Dima e l'array#2941

Dima e l'array (Editoriale)

Il problema richiede di elaborare operazioni di due tipi:

Abbiamo bisogno di una struttura dati che esegua entrambe le operazioni velocemente. Una struttura di questo tipo è un segment tree per le somme. Ciascuno dei suoi vertici memorizza la somma degli elementi del proprio segmento, e il valore in un vertice interno è uguale alla somma dei valori dei suoi due figli.

Il segment tree permette sia di calcolare la somma su un segmento sia di modificare un elemento dell'array in O(\log n).

Esempio

Simuliamo le query dell'esempio. Inizialmente, l'array è (1, 2, 3).

Vediamo come cambia il segment tree. La figura a sinistra mostra l'albero per l'array iniziale, e la figura a destra lo mostra dopo l'assegnazione a_3 = 2. Ogni vertice mostra il proprio segmento e la somma degli elementi su di esso; i vertici modificati sono evidenziati.

Il segmento di entrambe le query coincide con il segmento della radice, quindi la risposta viene presa direttamente dalla radice: prima dell'assegnazione e dopo di essa. Durante l'assegnazione a_3 = 2, vengono ricalcolati solo i vertici sul percorso dalla foglia alla radice: la foglia riceve il valore , e la radice riceve il valore 3 + 2 = 5.

Implementazione dell'algoritmo

Memorizziamo il segment tree nel vettore SegTree di lunghezza 4 \cdot MAX, dove MAX = 5 \cdot 10^5 è il numero massimo di elementi dell'array. Ogni vertice dell'albero memorizza la somma degli elementi del proprio segmento.

#define MAX 500000
vector<int> SegTree(4*MAX,0);

La funzione BuildTree costruisce il segment tree a partire dall'array . Riceve il numero del vertice corrente e gli estremi e del segmento corrispondente a questo vertice.

void BuildTree(vector<int>&a, int v, int lpos, int rpos)
{

Se il segmento del vertice è costituito da un solo elemento (lpos = rpos), allora è una foglia dell'albero. Scriviamo in esso il valore .

  if (lpos == rpos) SegTree[v] = a[lpos];
  else
  {

Altrimenti, troviamo il punto medio del segmento mid = (lpos + rpos) / 2.

    int mid = (lpos + rpos) / 2;

Il segmento [lpos; rpos] viene suddiviso in due sottosegmenti [lpos; mid] e [mid + 1; rpos]. Costruiamo ricorsivamente l'albero per il figlio sinistro e il figlio destro 2v + 1.

    BuildTree(a, 2*v, lpos, mid);
    BuildTree(a, 2*v+1, mid + 1, rpos);

Il valore nel vertice è uguale alla somma dei valori dei suoi figli.

    SegTree[v] = SegTree[2*v] + SegTree[2*v+1];
  }
}

La funzione Update assegna il valore all'elemento con indice . Il vertice corrisponde al segmento [lpos; rpos].

void Update(int v, int lpos, int rpos, int pos, int val)
{

Se il vertice è una foglia (lpos = rpos), esso corrisponde alla posizione . Scriviamo in esso il valore .

  if (lpos == rpos) SegTree[v] = val;
  else
  {

Altrimenti, determiniamo quale metà del segmento contiene la posizione , e scendiamo ricorsivamente nel figlio sinistro o destro.

    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);

Dopo essere tornati dalla ricorsione, ricalcoliamo la somma nel vertice . In questo modo, vengono aggiornati tutti i vertici sul percorso dalla foglia alla radice.

    SegTree[v] = SegTree[2*v] + SegTree[2*v+1];
  }
}

La funzione Summa restituisce la somma degli elementi sul segmento [left; right]. Il vertice corrisponde al segmento [lpos; rpos]. Se il segmento di query è vuoto (left > right), restituiamo — l'elemento neutro per l'addizione.

int Summa(int v, int lpos, int rpos, int left, int right)
{
  if (left > right) return 0;

Se il segmento di query coincide con il segmento del vertice, la risposta è memorizzata in SegTree[v].

  if ((left == lpos) && (right == rpos))
    return SegTree[v];

Altrimenti, troviamo il punto medio del segmento mid = (lpos + rpos) / 2.

  int mid = (lpos + rpos) / 2;

Suddividiamo il segmento di query in due parti: [left; \min(right, mid)] si trova nel segmento del figlio sinistro, e [\max(left, mid + 1); right] si trova nel segmento del figlio destro. Una delle parti può essere vuota. Restituiamo la somma dei risultati per entrambi i figli.

  return Summa(2*v, lpos, mid, left, min(right,mid)) +
         Summa(2*v+1, mid+1, rpos, max(left,mid+1), right);
}

La parte principale del programma. Leggiamo la dimensione dell'array , il numero di query , e gli elementi dell'array nel vettore , a partire dall'indice . La chiamata scanf("\n") salta il ritorno a capo dopo l'array, in modo che il carattere successivo letto sia il segno dell'operazione.

scanf("%d %d",&n,&q);
v.resize(n+1);
for(i = 1; i <= n; i++)
  scanf("%d",&v[i]);
scanf("\n");

Costruiamo il segment tree a partire dagli elementi v[1 \ldots n].

BuildTree(v,1,1,n);

Elaboriamo le query una per una. Per prima cosa, leggiamo il carattere dell'operazione ; lo spazio nel formato "%c " salta lo spazio dopo di esso. Se = '=', assegniamo il valore all'elemento con indice . Altrimenti, calcoliamo e stampiamo la somma sul segmento . Il carattere \n nel formato "%d %d\n" salta il ritorno a capo, in modo che la lettura successiva in ottenga il segno dell'operazione successiva.

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));
  }
}