diff --git a/README.md b/README.md index 620647f..2ef9197 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ A free, interactive web tool to learn algorithms through animated step-by-step v - **Variable tracking** — see the state of every variable in real time - **Contextual explanation** — understand the _why_ behind each operation -## 40+ algorithms across 8 categories +## 41+ algorithms across 8 categories @@ -86,7 +86,7 @@ Tower of Hanoi ### Concepts -Big O · Recursion · Two Pointers · Sliding Window · Memoization · Greedy vs DP · Space Complexity +Big O · Recursion · Two Pointers · Sliding Window · Prefix Sum Array · Memoization · Greedy vs DP · Space Complexity diff --git a/README_ES.md b/README_ES.md index c611006..b17d51b 100644 --- a/README_ES.md +++ b/README_ES.md @@ -27,7 +27,7 @@ Una herramienta web interactiva y gratuita para aprender algoritmos a través de - **Seguimiento de variables** — ve el estado de cada variable en tiempo real - **Explicación contextual** — entiende el _porqué_ de cada operación -## +40 algoritmos en 8 categorías +## +41 algoritmos en 8 categorías
@@ -86,7 +86,7 @@ Torre de Hanói ### Conceptos -Big O · Recursión · Two Pointers · Sliding Window · Memoización · Greedy vs DP · Space Complexity +Big O · Recursión · Two Pointers · Sliding Window · Prefix Sum Array · Memoización · Greedy vs DP · Space Complexity diff --git a/src/content/algorithms/prefix-sum-array.ts b/src/content/algorithms/prefix-sum-array.ts new file mode 100644 index 0000000..6995b85 --- /dev/null +++ b/src/content/algorithms/prefix-sum-array.ts @@ -0,0 +1,46 @@ +import type { Locale } from '@i18n/translations' + +const descriptions: Record = { + en: `Prefix Sum Array + +A Prefix Sum Array preprocesses a static array so range-sum queries become O(1). Each position stores the sum of all elements up to that index. + +How it works: +1. Build prefix[0] = arr[0] +2. For each next index, add the current value to the previous prefix +3. Answer sum(l, r) with prefix[r] - prefix[l - 1] +4. If l = 0, the answer is just prefix[r] + +Time Complexity: O(n) preprocessing, O(1) per query +Space Complexity: O(n) + +Best when: + - The array is static + - You need many range-sum queries + - You want to trade one preprocessing pass for instant lookups + +Limitation: + - Point updates are not handled efficiently here; for dynamic updates use other structures such as Fenwick Tree or Segment Tree.`, + es: `Prefix Sum Array + +Un Prefix Sum Array preprocesa un arreglo estático para que las consultas de suma por rango sean O(1). Cada posición guarda la suma de todos los elementos hasta ese índice. + +Cómo funciona: +1. Construir prefix[0] = arr[0] +2. Para cada índice siguiente, sumar el valor actual al prefijo anterior +3. Responder sum(l, r) con prefix[r] - prefix[l - 1] +4. Si l = 0, la respuesta es simplemente prefix[r] + +Complejidad Temporal: O(n) de preprocesamiento, O(1) por consulta +Complejidad Espacial: O(n) + +Conviene cuando: + - El arreglo es estático + - Necesitas muchas consultas de suma por rango + - Quieres cambiar una pasada de preprocesamiento por consultas instantáneas + +Limitación: + - Las actualizaciones puntuales no se manejan eficientemente aquí; para actualizaciones dinámicas hacen falta otras estructuras como Fenwick Tree o Segment Tree.`, +} + +export default descriptions diff --git a/src/lib/algorithms/catalog.ts b/src/lib/algorithms/catalog.ts index 419dc99..e0eba3e 100644 --- a/src/lib/algorithms/catalog.ts +++ b/src/lib/algorithms/catalog.ts @@ -35,6 +35,13 @@ export const algorithmCatalog: AlgorithmSummary[] = [ difficulty: 'intermediate', visualization: 'concept', }, + { + id: 'prefix-sum-array', + name: 'Prefix Sum Array', + category: 'Concepts', + difficulty: 'easy', + visualization: 'concept', + }, { id: 'space-complexity', name: 'Space Complexity', diff --git a/src/lib/algorithms/concepts.ts b/src/lib/algorithms/concepts.ts index f63d20b..e3e1fcd 100644 --- a/src/lib/algorithms/concepts.ts +++ b/src/lib/algorithms/concepts.ts @@ -1205,6 +1205,178 @@ export const slidingWindow: Algorithm = { }, } +// ============================================================ +// PREFIX SUM ARRAY +// ============================================================ + +export const prefixSumArray: Algorithm = { + id: 'prefix-sum-array', + name: 'Prefix Sum Array', + category: 'Concepts', + difficulty: 'easy', + visualization: 'concept', + code: `function buildPrefixSum(arr) { + const prefix = new Array(arr.length); + prefix[0] = arr[0]; + + for (let i = 1; i < arr.length; i++) { + prefix[i] = prefix[i - 1] + arr[i]; + } + return prefix; +} + +function rangeSum(prefix, left, right) { + if (left === 0) { + return prefix[right]; + } + return prefix[right] - prefix[left - 1]; +}`, + + generateSteps(locale = 'en') { + const steps: Step[] = [] + const array = [3, 1, 4, 2, 5] + const fullPrefix = [3, 4, 8, 10, 15] + + const partialPrefix = (filledThrough: number) => + array.map((_, index) => (index <= filledThrough ? fullPrefix[index] : null)) + + steps.push({ + concept: { + type: 'prefixSum', + array, + prefix: array.map(() => null), + phase: 'intro', + currentIndex: null, + range: null, + activePrefixIndices: [], + query: null, + operation: 'preprocess static array', + }, + description: d( + locale, + 'Prefix sums trade one O(n) preprocessing pass for O(1) range-sum queries on a static array.', + 'Los prefix sums cambian una pasada de preprocesamiento O(n) por consultas de suma por rango O(1) sobre un arreglo estático.', + ), + codeLine: 1, + variables: { array: '[3, 1, 4, 2, 5]', preprocessing: 'O(n)', 'per query': 'O(1)' }, + }) + + steps.push({ + concept: { + type: 'prefixSum', + array, + prefix: partialPrefix(0), + phase: 'build', + currentIndex: 0, + range: { start: 0, end: 0 }, + activePrefixIndices: [0], + query: null, + operation: 'build prefix[0]', + }, + description: d( + locale, + 'Initialize the prefix array with the first value. prefix[0] equals arr[0], so the sum from 0 to 0 is already known.', + 'Inicializa el arreglo prefix con el primer valor. prefix[0] es igual a arr[0], así que la suma de 0 a 0 ya queda conocida.', + ), + codeLine: 3, + variables: { i: 0, 'arr[i]': 3, 'prefix[0]': 3 }, + }) + + for (let i = 1; i < array.length; i++) { + steps.push({ + concept: { + type: 'prefixSum', + array, + prefix: partialPrefix(i), + phase: 'build', + currentIndex: i, + range: { start: 0, end: i }, + activePrefixIndices: [i - 1, i], + query: null, + operation: `build prefix[${i}]`, + }, + description: d( + locale, + `Add arr[${i}] = ${array[i]} to the previous prefix. Now prefix[${i}] stores the sum of the whole range [0..${i}] = ${fullPrefix[i]}.`, + `Suma arr[${i}] = ${array[i]} al prefijo anterior. Ahora prefix[${i}] guarda la suma de todo el rango [0..${i}] = ${fullPrefix[i]}.`, + ), + codeLine: i === 1 ? 5 : 6, + variables: { + i, + 'prefix[i - 1]': fullPrefix[i - 1], + 'arr[i]': array[i], + 'prefix[i]': fullPrefix[i], + }, + }) + } + + steps.push({ + concept: { + type: 'prefixSum', + array, + prefix: partialPrefix(array.length - 1), + phase: 'query', + currentIndex: null, + range: { start: 0, end: 2 }, + activePrefixIndices: [2], + query: { left: 0, right: 2, sum: 8, usesBaseCase: true }, + operation: 'query sum(0, 2)', + }, + description: d( + locale, + 'Base case: if the range starts at index 0, the answer is just prefix[right]. sum(0, 2) = prefix[2] = 8.', + 'Caso base: si el rango empieza en el índice 0, la respuesta es simplemente prefix[right]. sum(0, 2) = prefix[2] = 8.', + ), + codeLine: 12, + variables: { left: 0, right: 2, answer: 8, formula: 'prefix[2]' }, + }) + + steps.push({ + concept: { + type: 'prefixSum', + array, + prefix: partialPrefix(array.length - 1), + phase: 'query', + currentIndex: null, + range: { start: 1, end: 4 }, + activePrefixIndices: [0, 4], + query: { left: 1, right: 4, sum: 12, usesBaseCase: false }, + operation: 'query sum(1, 4)', + }, + description: d( + locale, + 'General case: subtract the prefix before the range. sum(1, 4) = prefix[4] - prefix[0] = 15 - 3 = 12.', + 'Caso general: resta el prefijo anterior al rango. sum(1, 4) = prefix[4] - prefix[0] = 15 - 3 = 12.', + ), + codeLine: 15, + variables: { left: 1, right: 4, 'prefix[4]': 15, 'prefix[0]': 3, answer: 12 }, + }) + + steps.push({ + concept: { + type: 'prefixSum', + array, + prefix: partialPrefix(array.length - 1), + phase: 'done', + currentIndex: null, + range: { start: 1, end: 4 }, + activePrefixIndices: [0, 2, 4], + query: { left: 1, right: 4, sum: 12, usesBaseCase: false }, + operation: 'ready for many queries', + }, + description: d( + locale, + 'After one linear build, every later query is O(1). That is why prefix sums shine when the array is static and queries are frequent.', + 'Después de una construcción lineal, cada consulta posterior es O(1). Por eso los prefix sums destacan cuando el arreglo es estático y las consultas son frecuentes.', + ), + codeLine: 11, + variables: { preprocessing: 'O(n)', 'per query': 'O(1)', limitation: 'no dynamic updates' }, + }) + + return steps + }, +} + // ============================================================ // SPACE COMPLEXITY (reuses BigO chart) // ============================================================ diff --git a/src/lib/algorithms/cpp/concepts.ts b/src/lib/algorithms/cpp/concepts.ts index e67c090..65006c6 100644 --- a/src/lib/algorithms/cpp/concepts.ts +++ b/src/lib/algorithms/cpp/concepts.ts @@ -106,6 +106,23 @@ vector mergeSort(vector arr) { return s.substr(bestStart, best); }`), + 'prefix-sum-array': annotated(`vector buildPrefixSum(const vector& arr) { //@1 + vector prefix(arr.size()); + prefix[0] = arr[0]; //@3 + + for (int i = 1; i < (int)arr.size(); i++) { //@5 + prefix[i] = prefix[i - 1] + arr[i]; //@6 + } + return prefix; //@8 +} + +int rangeSum(const vector& prefix, int left, int right) { //@11 + if (left == 0) { //@12 + return prefix[right]; //@13 + } + return prefix[right] - prefix[left - 1]; //@15 +}`), + 'space-complexity': annotated(`// O(1) space — fixed variables //@1 void swap(vector& arr, int i, int j) { //@2 int temp = arr[i]; diff --git a/src/lib/algorithms/index.ts b/src/lib/algorithms/index.ts index c646800..064b5d5 100644 --- a/src/lib/algorithms/index.ts +++ b/src/lib/algorithms/index.ts @@ -5,6 +5,7 @@ import { recursion, twoPointers, slidingWindow, + prefixSumArray, spaceComplexity, memoization, greedyVsDp, @@ -71,6 +72,7 @@ export const algorithms: Algorithm[] = [ recursion, twoPointers, slidingWindow, + prefixSumArray, spaceComplexity, memoization, greedyVsDp, diff --git a/src/lib/algorithms/java/concepts.ts b/src/lib/algorithms/java/concepts.ts index 49b5ecd..2e55a16 100644 --- a/src/lib/algorithms/java/concepts.ts +++ b/src/lib/algorithms/java/concepts.ts @@ -103,6 +103,23 @@ int[] mergeSort(int[] arr) { return s.substring(bestStart, bestStart + best); }`), + 'prefix-sum-array': annotated(`int[] buildPrefixSum(int[] arr) { //@1 + int[] prefix = new int[arr.length]; + prefix[0] = arr[0]; //@3 + + for (int i = 1; i < arr.length; i++) { //@5 + prefix[i] = prefix[i - 1] + arr[i]; //@6 + } + return prefix; //@8 +} + +int rangeSum(int[] prefix, int left, int right) { //@11 + if (left == 0) { //@12 + return prefix[right]; //@13 + } + return prefix[right] - prefix[left - 1]; //@15 +}`), + 'space-complexity': annotated(`// O(1) space — fixed variables //@1 void swap(int[] arr, int i, int j) { //@2 int temp = arr[i]; diff --git a/src/lib/algorithms/loaders.ts b/src/lib/algorithms/loaders.ts index 71f2027..563290a 100644 --- a/src/lib/algorithms/loaders.ts +++ b/src/lib/algorithms/loaders.ts @@ -38,6 +38,8 @@ const ALGORITHM_LOADERS: Record Promise> = { recursion: () => import('./concepts?algorithm=recursion').then(readDefaultAlgorithm), 'two-pointers': () => import('./concepts?algorithm=twoPointers').then(readDefaultAlgorithm), 'sliding-window': () => import('./concepts?algorithm=slidingWindow').then(readDefaultAlgorithm), + 'prefix-sum-array': () => + import('./concepts?algorithm=prefixSumArray').then(readDefaultAlgorithm), 'space-complexity': () => import('./concepts?algorithm=spaceComplexity').then(readDefaultAlgorithm), memoization: () => import('./concepts?algorithm=memoization').then(readDefaultAlgorithm), diff --git a/src/lib/algorithms/python/concepts.ts b/src/lib/algorithms/python/concepts.ts index 851c696..93cd5f5 100644 --- a/src/lib/algorithms/python/concepts.ts +++ b/src/lib/algorithms/python/concepts.ts @@ -96,6 +96,19 @@ def merge(left, right): best_start = start return s[best_start:best_start + best]`), + 'prefix-sum-array': annotated(`def build_prefix_sum(arr): #@1 + prefix = [0] * len(arr) + prefix[0] = arr[0] #@3 + + for i in range(1, len(arr)): #@5 + prefix[i] = prefix[i - 1] + arr[i] #@6 + return prefix #@8 + +def range_sum(prefix, left, right): #@11 + if left == 0: #@12 + return prefix[right] #@13 + return prefix[right] - prefix[left - 1] #@15`), + 'space-complexity': annotated(`# O(1) space — fixed variables #@1 def swap(arr, i, j): #@2 temp = arr[i] diff --git a/src/lib/algorithms/rust/concepts.ts b/src/lib/algorithms/rust/concepts.ts index 2b25915..50a1e3c 100644 --- a/src/lib/algorithms/rust/concepts.ts +++ b/src/lib/algorithms/rust/concepts.ts @@ -112,6 +112,23 @@ fn merge_sort(arr: &[i32]) -> Vec { chars[best_start..best_start + best].iter().collect() }`), + 'prefix-sum-array': annotated(`fn build_prefix_sum(arr: &[i32]) -> Vec { //@1 + let mut prefix = vec![0; arr.len()]; + prefix[0] = arr[0]; //@3 + + for i in 1..arr.len() { //@5 + prefix[i] = prefix[i - 1] + arr[i]; //@6 + } + prefix //@8 +} + +fn range_sum(prefix: &[i32], left: usize, right: usize) -> i32 { //@11 + if left == 0 { //@12 + return prefix[right]; //@13 + } + prefix[right] - prefix[left - 1] //@15 +}`), + 'space-complexity': annotated(`// O(1) space — fixed variables //@1 fn swap(arr: &mut [i32], i: usize, j: usize) { //@2 let temp = arr[i]; diff --git a/src/lib/types.ts b/src/lib/types.ts index b5ef125..55ee6fa 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -206,6 +206,23 @@ export interface SlidingWindowState { operation?: string } +export interface PrefixSumState { + type: 'prefixSum' + array: number[] + prefix: (number | null)[] + phase: 'intro' | 'build' | 'query' | 'done' + currentIndex?: number | null + range?: { start: number; end: number } | null + activePrefixIndices?: number[] + query?: { + left: number + right: number + sum: number + usesBaseCase: boolean + } | null + operation?: string +} + export interface MemoTableState { type: 'memoTable' entries: { key: number; value: number | null; state: 'empty' | 'computing' | 'cached' | 'hit' }[] @@ -424,6 +441,7 @@ export type ConceptState = | LruCacheState | TwoPointersState | SlidingWindowState + | PrefixSumState | MemoTableState | CoinChangeState | BucketsState diff --git a/src/lib/visualizers/concept/index.ts b/src/lib/visualizers/concept/index.ts index 2e68fe9..621a893 100644 --- a/src/lib/visualizers/concept/index.ts +++ b/src/lib/visualizers/concept/index.ts @@ -14,6 +14,7 @@ export type ConceptType = | 'lruCache' | 'twoPointers' | 'slidingWindow' + | 'prefixSum' | 'memoTable' | 'coinChange' | 'buckets' @@ -56,6 +57,8 @@ const loaders: Record Promise> = { import('@lib/visualizers/concept/sliding-window').then( (m) => m.renderSlidingWindow as ConceptRenderer, ), + prefixSum: () => + import('@lib/visualizers/concept/prefix-sum').then((m) => m.renderPrefixSum as ConceptRenderer), memoTable: () => import('@lib/visualizers/concept/memo-table').then((m) => m.renderMemoTable as ConceptRenderer), coinChange: () => diff --git a/src/lib/visualizers/concept/prefix-sum.ts b/src/lib/visualizers/concept/prefix-sum.ts new file mode 100644 index 0000000..70a2e66 --- /dev/null +++ b/src/lib/visualizers/concept/prefix-sum.ts @@ -0,0 +1,155 @@ +/** + * Concept visualizer: PrefixSum. + */ +import type { PrefixSumState } from '@lib/types' +import { applyStyles } from '@lib/visualizers/concept/dom' + +const ARRAY_COLORS = { + default: { bg: 'var(--subtle)', border: 'var(--viz-border)', text: 'var(--viz-label)' }, + current: { bg: 'rgba(96,165,250,0.15)', border: 'rgba(96,165,250,0.38)', text: '#60a5fa' }, + inRange: { bg: 'rgba(74,222,128,0.16)', border: 'rgba(74,222,128,0.4)', text: '#86efac' }, +} + +const PREFIX_COLORS = { + empty: { bg: 'rgba(255,255,255,0.03)', border: 'var(--viz-border)', text: 'var(--viz-muted)' }, + ready: { bg: 'rgba(250,204,21,0.12)', border: 'rgba(250,204,21,0.32)', text: '#fde047' }, + active: { bg: 'rgba(251,146,60,0.14)', border: 'rgba(251,146,60,0.4)', text: '#fb923c' }, +} + +function makeCell( + value: string, + colors: { bg: string; border: string; text: string }, + wide = false, +) { + const cell = document.createElement('div') + cell.className = `${wide ? 'w-16 md:w-18' : 'w-14 md:w-16'} h-12 rounded-lg border flex items-center justify-center font-mono text-sm md:text-base font-bold transition-all duration-300` + applyStyles(cell, { + backgroundColor: colors.bg, + borderColor: colors.border, + color: colors.text, + boxShadow: colors.border !== 'var(--viz-border)' ? `0 0 12px ${colors.border}` : 'none', + }) + cell.textContent = value + return cell +} + +function makeIndexRow(length: number, wide = false) { + const row = document.createElement('div') + row.className = 'flex gap-1' + for (let i = 0; i < length; i++) { + const idx = document.createElement('div') + idx.className = `${wide ? 'w-16 md:w-18' : 'w-14 md:w-16'} text-center text-[9px] font-mono text-neutral-600` + idx.textContent = String(i) + row.append(idx) + } + return row +} + +export function renderPrefixSum(state: PrefixSumState): HTMLElement { + const { + array, + prefix, + phase, + currentIndex, + range, + activePrefixIndices = [], + query, + operation, + } = state + + const wrap = document.createElement('div') + wrap.className = 'flex-1 flex flex-col items-center justify-center gap-4 w-full' + + const title = document.createElement('div') + title.className = 'text-neutral-500 font-mono text-[11px] uppercase tracking-widest' + title.textContent = 'Prefix Sum Array' + wrap.append(title) + + if (operation) { + const badge = document.createElement('div') + badge.className = + 'font-mono text-xs px-3 py-1 rounded-full bg-white/5 border border-white/10 text-neutral-300' + badge.textContent = operation + wrap.append(badge) + } + + const table = document.createElement('div') + table.className = 'flex flex-col gap-3 items-center' + + const arraySection = document.createElement('div') + arraySection.className = 'flex flex-col items-center gap-1' + const arrayLabel = document.createElement('div') + arrayLabel.className = 'text-[10px] font-mono uppercase tracking-[0.22em] text-neutral-500' + arrayLabel.textContent = 'Original Array' + arraySection.append(arrayLabel) + + const arrayCells = document.createElement('div') + arrayCells.className = 'flex gap-1' + array.forEach((value, index) => { + const inRange = range && index >= range.start && index <= range.end + const colors = + index === currentIndex + ? ARRAY_COLORS.current + : inRange + ? ARRAY_COLORS.inRange + : ARRAY_COLORS.default + arrayCells.append(makeCell(String(value), colors)) + }) + arraySection.append(arrayCells, makeIndexRow(array.length)) + table.append(arraySection) + + const prefixSection = document.createElement('div') + prefixSection.className = 'flex flex-col items-center gap-1' + const prefixLabel = document.createElement('div') + prefixLabel.className = 'text-[10px] font-mono uppercase tracking-[0.22em] text-neutral-500' + prefixLabel.textContent = 'Prefix Array' + prefixSection.append(prefixLabel) + + const prefixCells = document.createElement('div') + prefixCells.className = 'flex gap-1' + prefix.forEach((value, index) => { + const isActive = activePrefixIndices.includes(index) + const colors = + value == null ? PREFIX_COLORS.empty : isActive ? PREFIX_COLORS.active : PREFIX_COLORS.ready + prefixCells.append(makeCell(value == null ? '·' : String(value), colors, true)) + }) + prefixSection.append(prefixCells, makeIndexRow(prefix.length, true)) + table.append(prefixSection) + + wrap.append(table) + + if ( + phase === 'build' && + currentIndex != null && + currentIndex > 0 && + prefix[currentIndex] != null + ) { + const formula = document.createElement('div') + formula.className = 'font-mono text-xs text-neutral-400 text-center' + formula.innerHTML = `prefix[${currentIndex}] = prefix[${currentIndex - 1}] + arr[${currentIndex}] = ${prefix[currentIndex - 1]} + ${array[currentIndex]} = ${prefix[currentIndex]}` + wrap.append(formula) + } + + if (query && range) { + const line = document.createElement('div') + line.className = 'font-mono text-xs md:text-sm text-neutral-300 text-center' + if (query.usesBaseCase) { + line.innerHTML = `sum(${query.left}, ${query.right}) = prefix[${query.right}] = ${query.sum}` + } else { + const leftPrefix = prefix[query.left - 1] + const rightPrefix = prefix[query.right] + line.innerHTML = `sum(${query.left}, ${query.right}) = prefix[${query.right}] - prefix[${query.left - 1}] = ${rightPrefix} - ${leftPrefix} = ${query.sum}` + } + wrap.append(line) + } + + if (phase === 'done') { + const summary = document.createElement('div') + summary.className = 'font-mono text-xs text-neutral-400 text-center max-w-xl' + summary.textContent = + 'One O(n) preprocessing pass turns repeated range sums into O(1) lookups on a static array.' + wrap.append(summary) + } + + return wrap +}