From 38690a9e3576cc602dd31f6cdde6fe63fc66ed7f Mon Sep 17 00:00:00 2001
From: dcq-31 <64748988+dcq-31@users.noreply.github.com>
Date: Sat, 8 Aug 2026 23:26:43 +0100
Subject: [PATCH] feat(concepts): add Prefix Sum Array
---
README.md | 4 +-
README_ES.md | 4 +-
src/content/algorithms/prefix-sum-array.ts | 46 ++++++
src/lib/algorithms/catalog.ts | 7 +
src/lib/algorithms/concepts.ts | 172 +++++++++++++++++++++
src/lib/algorithms/cpp/concepts.ts | 17 ++
src/lib/algorithms/index.ts | 2 +
src/lib/algorithms/java/concepts.ts | 17 ++
src/lib/algorithms/loaders.ts | 2 +
src/lib/algorithms/python/concepts.ts | 13 ++
src/lib/algorithms/rust/concepts.ts | 17 ++
src/lib/types.ts | 18 +++
src/lib/visualizers/concept/index.ts | 3 +
src/lib/visualizers/concept/prefix-sum.ts | 155 +++++++++++++++++++
14 files changed, 473 insertions(+), 4 deletions(-)
create mode 100644 src/content/algorithms/prefix-sum-array.ts
create mode 100644 src/lib/visualizers/concept/prefix-sum.ts
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
+}