Skip to Content

Introducción a las sumas de prefijos

Recursos

Recursos
FuenteRecursoNotas
IUSACO11 - Prefix Sums

Fuente de este módulo

CPH9.1 - Sum Queries

Descripción breve

PAPS11.2.1 - Prefix Precomputation

Motivación e implementación

Video de YouTube (f0bfiuDjq9A)

Introducción

Las sumas de prefijos (prefix sums) son una técnica para calcular rápidamente la suma de cualquier subarreglo. Al precomputar las sumas acumuladas en tiempo O(N)\mathcal{O}(N), las consultas posteriores se pueden resolver en tiempo O(1)\mathcal{O}(1). Esta técnica se puede generalizar a operaciones invertibles, como el XOR bit a bit .

Ejemplo - Static Range Sum

HechoFuenteNombreDificultadTagsSolución
YSStatic Range SumMuy fácilen el módulo

Explicación

Supongamos que tenemos un arreglo de enteros arr\texttt{arr} de tamaño NN indexado desde 1 y queremos calcular el valor de

arr[a]+arr[a+1]++arr[b] \texttt{arr}[a]+\texttt{arr}[a+1]+\cdots+\texttt{arr}[b]

para QQ pares distintos (a,b)(a,b) que cumplen 1abN1\le a\le b\le N. Usaremos el siguiente ejemplo con N=6N = 6:

Índice ii123456
arr[i]\texttt{arr}[i]164253

De forma ingenua, para cada consulta podemos recorrer todas las entradas desde el índice aa hasta el índice bb y sumarlas. Como hay QQ consultas y cada una requiere como máximo O(N)\mathcal{O}(N) operaciones para calcular la suma, la complejidad temporal total es O(NQ)\mathcal{O}(NQ). En la mayoría de los problemas de este tipo, las restricciones serán N,Q105N, Q \leq 10^5, así que NQNQ es del orden de 101010^{10}. Esto no es aceptable; casi con certeza superará el límite de tiempo.

En su lugar, podemos usar sumas de prefijos para procesar estas consultas de suma sobre el arreglo. Designamos un arreglo de sumas de prefijos prefix\texttt{prefix}. Primero, como indexamos el arreglo desde 1, asignamos prefix[0]=0\texttt{prefix}[0]=0; luego, para los índices kk tales que 1kN1 \leq k \leq N, definimos el arreglo de sumas de prefijos así:

prefix[k]=i=1karr[i] \texttt{prefix}[k]=\sum_{i=1}^{k} \texttt{arr}[i]

En esencia, el elemento en el índice kk del arreglo de sumas de prefijos almacena la suma de todos los elementos del arreglo original desde el índice 11 hasta kk. Esto se puede calcular fácilmente en O(N)\mathcal{O}(N) con la siguiente fórmula para cada 1kN1\le k\le N:

prefix[k]=prefix[k1]+arr[k] \texttt{prefix}[k]=\texttt{prefix}[k-1]+\texttt{arr}[k]

Para el caso de ejemplo, el arreglo de sumas de prefijos queda así:

Índice ii0123456
prefix[i]\texttt{prefix}[i]01711131821

Ahora, cuando queremos consultar la suma de los elementos de arr\texttt{arr} entre los índices aa y bb inclusive (indexados desde 1), podemos usar la siguiente fórmula:

i=LRarr[i]=i=1Rarr[i]i=1L1arr[i] \sum_{i=L}^{R} \texttt{arr}[i] = \sum_{i=1}^{R} \texttt{arr}[i] - \sum_{i=1}^{L-1} \texttt{arr}[i]

Usando la definición de los elementos del arreglo de sumas de prefijos, tenemos

i=LRarr[i]=prefix[R]prefix[L1] \sum_{i=L}^{R} \texttt{arr}[i]= \texttt{prefix}[R]-\texttt{prefix}[L-1]

Como solo consultamos dos elementos del arreglo de sumas de prefijos, podemos calcular sumas de subarreglos en O(1)\mathcal{O}(1) por consulta, lo cual es mucho mejor que el O(N)\mathcal{O}(N) por consulta que teníamos antes. Ahora, después de un preprocesamiento de O(N)\mathcal{O}(N) para calcular el arreglo de sumas de prefijos, cada una de las QQ consultas toma tiempo O(1)\mathcal{O}(1). Así, la complejidad temporal total es O(N+Q)\mathcal{O}(N+Q), que ya debería pasar el límite de tiempo.

Hagamos una consulta de ejemplo y calculemos la suma del subarreglo entre los índices a=2a = 2 y b=5b = 5, inclusive, en el arr\texttt{arr} indexado desde 1. Mirando el arreglo original, vemos que esto es

i=25arr[i]=6+4+2+5=17. \sum_{i=2}^{5} \texttt{arr}[i] = 6 + 4 + 2 + 5 = 17.
Índice ii123456
arr[i]\texttt{arr}[i]164253

Usando sumas de prefijos:

prefix[5]prefix[1]=181=17. \texttt{prefix}[5] - \texttt{prefix}[1] = 18 - 1 = 17.

Clic en un índice para L. La suma es prefix[R] − prefix[L−1].

arr
pref

L=2, R=5 → prefix[5] − prefix[1] = 181 = 17

Índice ii0123456
prefix[i]\texttt{prefix}[i]01711131821

También se conocen como sumas parciales  (partial sums).

Implementación

Complejidad temporal: O(N+Q)\mathcal{O}(N + Q)

En C++ podemos usar std::partial_sum, aunque no acorta mucho el código.

#include <bits/stdc++.h> using namespace std; vector<long long> psum(const vector<int> &arr) { vector<long long> psums(arr.size() + 1); for (int i = 0; i < arr.size(); i++) { psums[i + 1] = psums[i] + arr[i]; } // or partial_sum(begin(a), end(a), begin(psums) + 1); return psums; } int main() { int N, Q; cin >> N >> Q; vector<int> nums(N); for (int i = 0; i < N; i++) { cin >> nums[i]; } vector<long long> prefix_arr = psum(nums); for (int i = 0; i < Q; ++i) { int l, r; cin >> l >> r; cout << prefix_arr[r] - prefix_arr[l] << "\n"; } }
import java.io.*; import java.util.*; public class Main { static int N, Q; public static void main(String[] args) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); PrintWriter writer = new PrintWriter(System.out); StringTokenizer st = new StringTokenizer(reader.readLine()); N = Integer.parseInt(st.nextToken()); Q = Integer.parseInt(st.nextToken()); st = new StringTokenizer(reader.readLine()); int[] nums = new int[N]; for (int i = 0; i < N; i++) { nums[i] = Integer.parseInt(st.nextToken()); } long[] prefix_arr = psum(nums); for (int i = 0; i < Q; i++) { int l, r; st = new StringTokenizer(reader.readLine()); l = Integer.parseInt(st.nextToken()); r = Integer.parseInt(st.nextToken()); writer.println(prefix_arr[r] - prefix_arr[l]); } reader.close(); writer.close(); } public static long[] psum(int[] arr) { long[] psums = new long[N + 1]; for (int i = 0; i < N; i++) { psums[i + 1] = psums[i] + arr[i]; } return psums; } }
def psum(arr): psums = [0] for i in arr: psums.append(psum[-1] + i) return psums N, Q = map(int, input().split()) nums = list(map(int, input().split())) prefix_arr = psum(nums) for i in range(Q): l, r = map(int, input().split()) print(prefix_arr[r] - prefix_arr[l])

Un enfoque alternativo es usar itertools.accumulate. Nótese que hay que agregar un 00 al inicio del arreglo. Si se usa una versión más reciente, también se puede usar el parámetro opcional initial=0.

import itertools def psum(arr): return [0] + list(itertools.accumulate(arr)) # BeginCodeSnip{Same code as above} N, Q = map(int, input().split()) nums = list(map(int, input().split())) prefix_arr = psum(nums) for i in range(Q): l, r = map(int, input().split()) print(prefix_arr[r] - prefix_arr[l]) # EndCodeSnip

Problemas

HechoFuenteNombreDificultadTagsSolución
SilverBreed CountingMuy fácilPrefix SumsSolución
SilverHoof Paper ScissorsMuy fácilPrefix SumsSolución
ACGCD on BlackboardFácilPrefix SumsSolución
CFGood SubarraysFácilPrefix Sums, MathSolución
CSESSubarray DivisibilityFácilPrefix SumsSolución
CSESSubarray Sums IIFácilPrefix SumsSolución
SilverSubsequences Summing to SevensFácilPrefix SumsSolución
CFAlternating StringNormalPrefix SumsSolución
ACMultiple of 2019NormalPrefix SumsSolución
CFRunning MilesNormalPrefix SumsSolución
SilverFarmer John's Favorite OperationNormalPrefix SumsSolución
CFIrreducible AnagramsDifícilPrefix SumsSolución
CFSum of SegmentsDifícilPrefix Sums

Quiz

Pregunta 1/4

¿Cuál es la complejidad temporal óptima de calcular el arreglo de sumas de prefijos de un arreglo de longitud nn?