Atlasingeniería

Estructuras de datosEstructuras para consultas por rangoTema 1

Sumas acumuladas y consultas por rango

Preguntar cuánto suma un tramo de un arreglo cuesta recorrerlo. Si la pregunta se va a repetir miles de veces, conviene pagar una pasada al principio y que cada consulta salga en tiempo constante. Es la primera forma de cambiar preprocesamiento por velocidad.

Para este tema conviene tener claro:Arreglos, listas y matrices

«¿Cuánto se vendió entre el día 100 y el día 250?» Con el arreglo de ventas a mano, la respuesta es recorrer 150 posiciones y sumar. Rápido, si la pregunta se hace una vez.

El problema aparece cuando la pregunta se hace cien mil veces con rangos distintos: son cien mil recorridos sobre los mismos números. Una pasada de preprocesamiento convierte cada una de esas consultas en una resta.

Guardar los totales hasta cada posición

La idea es un arreglo auxiliar donde cada posición guarda la suma de todo lo que hay antes:

P[i]=j<iA[j],P[0]=0.P[i] = \sum_{j<i} A[j], \qquad P[0] = 0.

Con eso, la suma del tramo [desde,hasta)[desde, hasta) es una resta:

j=desdehasta1A[j]=P[hasta]P[desde].\sum_{j=desde}^{hasta-1} A[j] = P[hasta] - P[desde].

La construcción y una consulta

El arreglo original, A.

1 / 8
A = [3, 1, 4, 1, 5]. La consulta de las posiciones 1 a 3 inclusive es P[4] − P[1] = 9 − 3 = 6, que es 1 + 4 + 1.
const prefixSums = (values: readonly number[]): number[] => {
  const sums = new Array<number>(values.length + 1).fill(0);
  for (let index = 0; index < values.length; index += 1) {
    sums[index + 1] = sums[index] + values[index];
  }
  return sums;
};

/** Suma de values[from..to], con los dos extremos incluidos. */
const rangeSum = (sums: readonly number[], from: number, to: number): number =>
  sums[to + 1] - sums[from];

Cuándo conviene pagar el preprocesamiento

Sin preprocesarCon sumas acumuladas
ConstrucciónNadaO(n), una sola pasada
Memoria extraNadaO(n)
Cada consultaO(largo del rango)O(1): dos lecturas y una resta
Modificar un elementoO(1)O(n): hay que rehacer todo lo que sigue
La última fila es la que decide: las sumas acumuladas son para datos que no cambian, o que cambian mucho menos de lo que se consultan.

Antes de seguir, predecí

Tenés un arreglo que cambia todo el tiempo y también se consulta por rangos todo el tiempo. ¿Sirven las sumas acumuladas?

El truco inverso: el arreglo de diferencias

Hay un caso simétrico y muy útil: muchas modificaciones por rango y una sola lectura al final. «Sumale 5 a todos los días entre el 100 y el 250», repetido cien mil veces.

La técnica es la inversa: en vez de acumular, se guardan diferencias. Para sumar vv al rango [desde,hasta][desde, hasta] alcanzan dos escrituras:

D[desde]+=v,D[hasta+1]=v.D[desde] \mathrel{+}= v, \qquad D[hasta+1] \mathrel{-}= v.

Al final, la suma acumulada de DD reconstruye el arreglo con todas las modificaciones aplicadas.

Seis días en cero. D tiene una posición extra para el cierre.

1 / 6
Dos modificaciones por rango cuestan dos escrituras cada una. La acumulada final, una sola pasada, deja el arreglo con todo aplicado.

En dos dimensiones

La misma idea sirve para submatrices, con un detalle: la esquina que se resta dos veces hay que devolverla.

S(x1,y1,x2,y2)=P[x2][y2]P[x11][y2]P[x2][y11]+P[x11][y11].S(x_1,y_1,x_2,y_2) = P[x_2][y_2] - P[x_1-1][y_2] - P[x_2][y_1-1] + P[x_1-1][y_1-1].

La matriz original A, de 3 por 3.

1 / 3
Cada celda de P guarda la suma del rectángulo desde el origen hasta ella. Consultar cualquier submatriz son cuatro lecturas: se restan dos rectángulos y se devuelve la intersección, que quedó restada dos veces.

Cierre

Autoevaluación

¿Lo entendiste?

¿Por qué conviene que P tenga un elemento más que A, con P[0] = 0?
¿Cuánto cuesta consultar la suma de un rango de un millón de elementos?
¿Cuál es la limitación principal de las sumas acumuladas?
Necesitás aplicar cien mil sumas por rango y leer el arreglo una sola vez al final. ¿Qué usás?
En dos dimensiones, ¿por qué la fórmula suma de vuelta una esquina?