From 58aabbfa0b24d067b3bb33baf3edbe162fdd95c7 Mon Sep 17 00:00:00 2001 From: dcq-31 <64748988+dcq-31@users.noreply.github.com> Date: Sat, 13 Jun 2026 21:55:27 -0400 Subject: [PATCH] feat(graphs): add adjacency matrix representation visualizer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add an Adjacency Matrix entry to the Graphs category that animates building the N×N adjacency matrix of a directed graph, edge by edge, with the graph drawn beside the matrix. - Add the `adjacencyMatrix` concept sub-type (`AdjacencyMatrixState`) and an inline `AdjacencyMatrixViz` component reusing the existing highlight styles. - Place it at the top of the Graphs category so the representation is taught before the traversal algorithms that rely on it. - Add bilingual (en/es) descriptions and update the README catalogs. --- README.md | 26 +- README_ES.md | 26 +- src/components/ConceptVisualizer.tsx | 551 +++++++++++++++++++++++---- src/i18n/translations.ts | 52 +++ src/lib/algorithms/graphs.ts | 298 +++++++++++++-- src/lib/algorithms/index.ts | 26 +- src/lib/types.ts | 13 + 7 files changed, 849 insertions(+), 143 deletions(-) diff --git a/README.md b/README.md index ce4d6a5..a9b8c7d 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ A free, interactive web tool to learn algorithms through animated step-by-step v - **Animated visualization** — watch how the data structure transforms at each step - **Active line highlighting** — code highlights in sync with the animation - **Variable tracking** — see the state of every variable in real time -- **Contextual explanation** — understand the *why* behind each operation +- **Contextual explanation** — understand the _why_ behind each operation ## 40+ algorithms across 8 categories @@ -34,24 +34,28 @@ A free, interactive web tool to learn algorithms through animated step-by-step v ### Sorting + Bubble Sort · Selection Sort · Insertion Sort · Quick Sort · Merge Sort · Heap Sort · Counting Sort · Radix Sort · Shell Sort ### Data Structures + Stack · Queue · Linked List · Hash Table · Binary Search Tree · Heap ### Graphs -BFS · DFS · Dijkstra · Prim · Topological Sort + +Adjacency Matrix · BFS · DFS · Dijkstra · Prim · Topological Sort ### Searching + Binary Search · Linear Search · Jump Search · Interpolation Search @@ -60,24 +64,28 @@ Binary Search · Linear Search · Jump Search · Interpolation Search ### Dynamic Programming + Fibonacci · 0/1 Knapsack · LCS (Longest Common Subsequence) ### Backtracking + N-Queens · Sudoku Solver · Maze Pathfinding ### Divide & Conquer + Tower of Hanoi ### Concepts + Big O · Recursion · Two Pointers · Sliding Window · Memoization · Greedy vs DP · Space Complexity @@ -86,13 +94,13 @@ Big O · Recursion · Two Pointers · Sliding Window · Memoization · Greedy vs ## Keyboard shortcuts -| Key | Action | -|:---:|:---| -| `Space` | Play / Pause | -| `→` | Next step | -| `←` | Previous step | -| `C` | Code tab | -| `E` | Explanation tab | +| Key | Action | +| :-----: | :-------------- | +| `Space` | Play / Pause | +| `→` | Next step | +| `←` | Previous step | +| `C` | Code tab | +| `E` | Explanation tab | Plus: speed control (5 levels), skip to start/end, and resizable panels. diff --git a/README_ES.md b/README_ES.md index c154734..5e3cbfc 100644 --- a/README_ES.md +++ b/README_ES.md @@ -25,7 +25,7 @@ Una herramienta web interactiva y gratuita para aprender algoritmos a través de - **Visualización animada** — observa cómo se transforma la estructura de datos en cada paso - **Código con línea activa** — el código se resalta sincronizado con la animación - **Seguimiento de variables** — ve el estado de cada variable en tiempo real -- **Explicación contextual** — entiende el *porqué* de cada operación +- **Explicación contextual** — entiende el _porqué_ de cada operación ## +40 algoritmos en 8 categorías @@ -34,24 +34,28 @@ Una herramienta web interactiva y gratuita para aprender algoritmos a través de ### Ordenamiento + Bubble Sort · Selection Sort · Insertion Sort · Quick Sort · Merge Sort · Heap Sort · Counting Sort · Radix Sort · Shell Sort ### Estructuras de datos + Stack · Queue · Linked List · Hash Table · Binary Search Tree · Heap ### Grafos -BFS · DFS · Dijkstra · Prim · Topological Sort + +Adjacency Matrix · BFS · DFS · Dijkstra · Prim · Topological Sort ### Búsqueda + Binary Search · Linear Search · Jump Search · Interpolation Search @@ -60,24 +64,28 @@ Binary Search · Linear Search · Jump Search · Interpolation Search ### Programación dinámica + Fibonacci · 0/1 Knapsack · LCS (Longest Common Subsequence) ### Backtracking + N-Queens · Sudoku Solver · Maze Pathfinding ### Divide y vencerás + Torre de Hanói ### Conceptos + Big O · Recursión · Two Pointers · Sliding Window · Memoización · Greedy vs DP · Space Complexity @@ -86,13 +94,13 @@ Big O · Recursión · Two Pointers · Sliding Window · Memoización · Greedy ## Atajos de teclado -| Tecla | Acción | -|:---:|:---| -| `Space` | Play / Pausa | -| `→` | Siguiente paso | -| `←` | Paso anterior | -| `C` | Pestaña de código | -| `E` | Pestaña de explicación | +| Tecla | Acción | +| :-----: | :--------------------- | +| `Space` | Play / Pausa | +| `→` | Siguiente paso | +| `←` | Paso anterior | +| `C` | Pestaña de código | +| `E` | Pestaña de explicación | Además: control de velocidad (5 niveles), saltar al inicio/final y paneles redimensionables. diff --git a/src/components/ConceptVisualizer.tsx b/src/components/ConceptVisualizer.tsx index 39a3c42..aa13252 100644 --- a/src/components/ConceptVisualizer.tsx +++ b/src/components/ConceptVisualizer.tsx @@ -11,7 +11,9 @@ import type { MemoTableState, CoinChangeState, BucketsState, + AdjacencyMatrixState, } from '@lib/types' +import { highlightStyles } from '@lib/highlight-colors' interface ConceptVisualizerProps { step: Step @@ -44,6 +46,8 @@ export default function ConceptVisualizer({ step }: ConceptVisualizerProps) { return case 'buckets': return + case 'adjacencyMatrix': + return default: return null } @@ -114,7 +118,14 @@ function BigOChart({ state }: { state: BigOState }) { aria-label="Big O complexity chart" > {/* Background */} - + {/* Horizontal grid lines */} {Array.from({ length: yTicks + 1 }, (_, i) => { @@ -122,8 +133,22 @@ function BigOChart({ state }: { state: BigOState }) { const val = maxY - (i / yTicks) * maxY return ( - - + + {val < 10 ? val.toFixed(1) : Math.round(val)} @@ -136,8 +161,22 @@ function BigOChart({ state }: { state: BigOState }) { const x = toX(n) return ( - - + + {Math.round(n)} @@ -145,11 +184,30 @@ function BigOChart({ state }: { state: BigOState }) { })} {/* Axes */} - - + + {/* Axis labels */} - + n (input size) {/* Stack label */} -
Call Stack
+
+ Call Stack +
{/* Frames — top of stack (last frame) is rendered first */}
@@ -288,9 +348,10 @@ function CallStackViz({ state }: { state: CallStackState }) { backgroundColor: colors.bg, borderColor: colors.border, color: colors.text, - boxShadow: frame.state === 'active' || frame.state === 'base' - ? `0 0 20px ${colors.border}` - : 'none', + boxShadow: + frame.state === 'active' || frame.state === 'base' + ? `0 0 20px ${colors.border}` + : 'none', }} > {/* Pulse animation for active/base frame */} @@ -303,15 +364,16 @@ function CallStackViz({ state }: { state: CallStackState }) {
{frame.label} - {frame.detail && ( - {frame.detail} - )} + {frame.detail && {frame.detail}}
{/* TOP indicator */} {isTop && ( -
+
← top
)} @@ -362,7 +424,9 @@ function StackViz({ return (
{/* Title */} -
Stack · LIFO
+
+ Stack · LIFO +
{/* Operation badge */} {operation && ( @@ -442,7 +506,9 @@ function QueueViz({ return (
{/* Title */} -
Queue · FIFO
+
+ Queue · FIFO +
{/* Operation badge */} {operation && ( @@ -464,8 +530,12 @@ function QueueViz({
{items.length > 0 && ( <> - front - back + + front + + + back + )}
@@ -511,10 +581,20 @@ function QueueViz({ - +
-
processing direction
+
+ processing direction +
) @@ -537,7 +617,9 @@ function LinkedListViz({ state }: { state: LinkedListState }) { return (
-
Linked List
+
+ Linked List +
{operation && (
@@ -551,9 +633,16 @@ function LinkedListViz({ state }: { state: LinkedListState }) {
{/* HEAD label */}
- head + + head + - +
@@ -575,7 +664,13 @@ function LinkedListViz({ state }: { state: LinkedListState }) { {node.value}
{/* Arrow to next */} - + @@ -616,7 +711,9 @@ function HashTableViz({ state }: { state: HashTableState }) { return (
-
Hash Table
+
+ Hash Table +
{operation && (
@@ -627,7 +724,12 @@ function HashTableViz({ state }: { state: HashTableState }) { {hashingKey != null && (
hash("{hashingKey}") - {hashResult != null && = {hashResult}} + {hashResult != null && ( + + {' '} + = {hashResult} + + )}
)} @@ -670,14 +772,27 @@ function HashTableViz({ state }: { state: HashTableState }) { backgroundColor: colors.bg, borderColor: colors.border, color: colors.text, - boxShadow: entry.state !== 'normal' ? `0 0 12px ${colors.border}` : 'none', + boxShadow: + entry.state !== 'normal' ? `0 0 12px ${colors.border}` : 'none', }} > {entry.key}:{entry.value}
{ei < entries.length - 1 && ( - - + + )} @@ -732,15 +847,16 @@ function BinaryTreeViz({ state }: { state: BinaryTreeState }) { return { x, y } } - const label = treeType === 'heap' - ? `${heapType === 'min' ? 'Min' : 'Max'} Heap` - : 'Binary Search Tree' + const label = + treeType === 'heap' ? `${heapType === 'min' ? 'Min' : 'Max'} Heap` : 'Binary Search Tree' const nonNullNodes = nodes.reduce((acc, n) => acc + (n ? 1 : 0), 0) return (
-
{label}
+
+ {label} +
{operation && (
@@ -820,7 +936,9 @@ function BinaryTreeViz({ state }: { state: BinaryTreeState }) { {/* Heap array view */} {treeType === 'heap' && nonNullNodes > 0 && (
-
array view
+
+ array view +
{nodes.map((node, idx) => { if (!node) return null @@ -865,7 +983,9 @@ function TwoPointersViz({ state }: { state: TwoPointersState }) { return (
-
Two Pointers
+
+ Two Pointers +
{operation && (
@@ -881,8 +1001,12 @@ function TwoPointersViz({ state }: { state: TwoPointersState }) { const isRight = i === right return (
- {isLeft && L ↓} - {isRight && !isLeft && R ↓} + {isLeft && ( + L ↓ + )} + {isRight && !isLeft && ( + R ↓ + )}
) })} @@ -901,7 +1025,8 @@ function TwoPointersViz({ state }: { state: TwoPointersState }) { backgroundColor: colors.bg, borderColor: colors.border, color: colors.text, - boxShadow: hl !== 'default' && hl !== 'checked' ? `0 0 12px ${colors.border}` : 'none', + boxShadow: + hl !== 'default' && hl !== 'checked' ? `0 0 12px ${colors.border}` : 'none', }} > {val} @@ -913,7 +1038,12 @@ function TwoPointersViz({ state }: { state: TwoPointersState }) { {/* Index row */}
{array.map((_, i) => ( -
{i}
+
+ {i} +
))}
@@ -921,8 +1051,16 @@ function TwoPointersViz({ state }: { state: TwoPointersState }) { {/* Sum display */} {sum != null && target != null && (
- arr[{left}] + arr[{right}] = {array[left]} + {array[right]} = {sum} - {sum === target ? ' ✓' : sum < target ? ` < ${target} → move L →` : ` > ${target} → ← move R`} + arr[{left}] + arr[{right}] = {array[left]} +{' '} + {array[right]} ={' '} + + {sum} + + {sum === target + ? ' ✓' + : sum < target + ? ` < ${target} → move L →` + : ` > ${target} → ← move R`}
)}
@@ -948,7 +1086,9 @@ function SlidingWindowViz({ state }: { state: SlidingWindowState }) { return (
-
Sliding Window
+
+ Sliding Window +
{operation && (
@@ -984,14 +1124,21 @@ function SlidingWindowViz({ state }: { state: SlidingWindowState }) { {/* Index row */}
{chars.map((_, i) => ( -
{i}
+
+ {i} +
))}
{/* Window bracket */} {windowEnd >= windowStart && windowEnd >= 0 && (
- window [{windowStart}..{windowEnd}] + + window [{windowStart}..{windowEnd}] + "{windowStr}" len={windowStr.length}
@@ -1001,7 +1148,8 @@ function SlidingWindowViz({ state }: { state: SlidingWindowState }) { {/* Best so far */} {bestStr && (
- best = "{bestStr}" (length {bestStr.length}) + best = "{bestStr}" (length{' '} + {bestStr.length})
)}
@@ -1024,7 +1172,9 @@ function MemoTableViz({ state }: { state: MemoTableState }) { return (
-
Memoization
+
+ Memoization +
{operation && (
@@ -1032,9 +1182,7 @@ function MemoTableViz({ state }: { state: MemoTableState }) {
)} - {currentCall && ( -
{currentCall}
- )} + {currentCall &&
{currentCall}
} {/* Memo table grid */}
@@ -1055,8 +1203,17 @@ function MemoTableViz({ state }: { state: MemoTableState }) { > {entry.value != null ? entry.value : '—'}
-
- {entry.state === 'hit' ? '↑ HIT' : entry.state === 'computing' ? '...' : entry.state === 'cached' ? '✓' : ''} +
+ {entry.state === 'hit' + ? '↑ HIT' + : entry.state === 'computing' + ? '...' + : entry.state === 'cached' + ? '✓' + : ''}
) @@ -1081,12 +1238,16 @@ function MemoTableViz({ state }: { state: MemoTableState }) { function CoinChangeViz({ state }: { state: CoinChangeState }) { const { coins, target, selected, remaining, approach, greedyResult, dpResult, operation } = state - const approachLabel = approach === 'greedy' ? 'Greedy' : approach === 'dp' ? 'Dynamic Programming' : 'Comparison' - const approachColor = approach === 'greedy' ? '#fb923c' : approach === 'dp' ? '#60a5fa' : '#c084fc' + const approachLabel = + approach === 'greedy' ? 'Greedy' : approach === 'dp' ? 'Dynamic Programming' : 'Comparison' + const approachColor = + approach === 'greedy' ? '#fb923c' : approach === 'dp' ? '#60a5fa' : '#c084fc' return (
-
Greedy vs DP
+
+ Greedy vs DP +
{operation && (
@@ -1095,14 +1256,25 @@ function CoinChangeViz({ state }: { state: CoinChangeState }) { )} {/* Approach label */} -
+
{approachLabel}
{/* Target */}
target = {target} - {remaining > 0 && remaining < target && remaining: {remaining}} + {remaining > 0 && remaining < target && ( + + remaining: {remaining} + + )}
{/* Available coins */} @@ -1110,7 +1282,10 @@ function CoinChangeViz({ state }: { state: CoinChangeState }) { coins:
{coins.map((c, i) => ( -
+
{c}
))} @@ -1137,7 +1312,9 @@ function CoinChangeViz({ state }: { state: CoinChangeState }) {
))}
- = {selected.reduce((a, b) => a + b, 0)} ({selected.length} coins) + + = {selected.reduce((a, b) => a + b, 0)} ({selected.length} coins) +
)} @@ -1145,19 +1322,33 @@ function CoinChangeViz({ state }: { state: CoinChangeState }) { {approach === 'compare' && greedyResult && dpResult && (
- Greedy + + Greedy +
{greedyResult.map((c, i) => ( -
{c}
+
+ {c} +
))}
{greedyResult.length} coins
- DP (optimal) + + DP (optimal) +
{dpResult.map((c, i) => ( -
{c}
+
+ {c} +
))}
{dpResult.length} coins ✓ @@ -1204,11 +1395,15 @@ function BucketsViz({ state }: { state: BucketsState }) {
- Current Min + + Current Min + {min ?? '—'}
- Current Max + + Current Max + {max ?? '—'}
@@ -1216,7 +1411,9 @@ function BucketsViz({ state }: { state: BucketsState }) { {/* Bucket Calculation Formula */} {buckets.length > 0 && (
-
Bucket Count Calculation
+
+ Bucket Count Calculation +
floor((max - min) / size) + 1
@@ -1224,7 +1421,8 @@ function BucketsViz({ state }: { state: BucketsState }) {
- {max} - {min} + {max} -{' '} + {min}
{bucketSize}
@@ -1265,10 +1463,7 @@ function BucketsViz({ state }: { state: BucketsState }) { const isProcessing = i === currentElementIndex const isCollected = phase === 'collecting' && i < currentElementIndex! return ( -
+
{ switch (highlight) { - case 'comparing': return { bg: 'rgba(59,130,246,0.3)', border: '#3b82f6', text: '#fff' } - case 'active': return { bg: 'rgba(234,179,8,0.3)', border: '#eab308', text: '#fff' } - case 'current': return { bg: 'rgba(168,85,247,0.3)', border: '#a855f7', text: '#fff' } - case 'found': return { bg: 'rgba(74,222,128,0.2)', border: '#4ade80', text: '#4ade80' } - default: return { bg: 'rgba(38,38,38,1)', border: 'rgba(255,255,255,0.1)', text: '#60a5fa' } + case 'comparing': + return { bg: 'rgba(59,130,246,0.3)', border: '#3b82f6', text: '#fff' } + case 'active': + return { bg: 'rgba(234,179,8,0.3)', border: '#eab308', text: '#fff' } + case 'current': + return { bg: 'rgba(168,85,247,0.3)', border: '#a855f7', text: '#fff' } + case 'found': + return { bg: 'rgba(74,222,128,0.2)', border: '#4ade80', text: '#4ade80' } + default: + return { + bg: 'rgba(38,38,38,1)', + border: 'rgba(255,255,255,0.1)', + text: '#60a5fa', + } } } const styles = getHighlightStyles() @@ -1355,7 +1559,10 @@ function BucketsViz({ state }: { state: BucketsState }) { borderColor: styles.border, borderWidth: '1px', color: styles.text, - animation: phase === 'distributing' && isActive && vIdx === bucket.length - 1 ? 'pop 0.3s ease-out' : 'none', + animation: + phase === 'distributing' && isActive && vIdx === bucket.length - 1 + ? 'pop 0.3s ease-out' + : 'none', transform: highlight ? 'scale(1.05)' : 'none', zIndex: highlight ? 10 : 1, }} @@ -1413,3 +1620,191 @@ function BucketsViz({ state }: { state: BucketsState }) {
) } + +// ════════════════════════════════════════════════════════════════ +// ADJACENCY MATRIX — directed graph + labeled N×N matrix +// ════════════════════════════════════════════════════════════════ + +const AM_COLORS = { + nodeFill: 'rgba(96,165,250,0.12)', + nodeStroke: 'rgba(96,165,250,0.35)', + nodeText: '#60a5fa', + nodeCurrentFill: 'rgba(251,146,60,0.2)', + nodeCurrentStroke: 'rgba(251,146,60,0.6)', + nodeCurrentText: '#fb923c', + edge: 'rgba(255,255,255,0.18)', + edgeCurrent: '#fff', +} + +function AdjacencyMatrixViz({ state }: { state: AdjacencyMatrixState }) { + const { nodes, edges, matrix, directed, currentEdge, highlightCells = [] } = state + + const R = 18 + const W = 360 + const H = 220 + + const byId = (id: number) => nodes.find((n) => n.id === id) ?? nodes[id] + + const isHighlightCell = (r: number, c: number) => + highlightCells.some(([hr, hc]) => hr === r && hc === c) + + // Screen-reader summary of the edges set so far + const setEdges: string[] = [] + for (let r = 0; r < matrix.length; r++) { + for (let c = 0; c < (matrix[r]?.length ?? 0); c++) { + if (matrix[r][c]) setEdges.push(`${byId(r).label}→${byId(c).label}`) + } + } + + return ( +
0 ? ` Edges set: ${setEdges.join(', ')}.` : ' No edges set yet.'}`} + > + + + {/* Graph */} + + + {/* Matrix */} + + ) +} diff --git a/src/i18n/translations.ts b/src/i18n/translations.ts index 5cbdb0b..f148394 100644 --- a/src/i18n/translations.ts +++ b/src/i18n/translations.ts @@ -899,6 +899,32 @@ Applications: Topological Sort is only possible for DAGs (Directed Acyclic Graphs). If the graph has a cycle, no valid ordering exists.`, + 'adjacency-matrix': `Adjacency Matrix + +An adjacency matrix represents a graph as a V×V grid, where the cell at row u, column v is 1 (or the edge weight) when there is an edge from u to v, and 0 otherwise. + +How it works: +1. Create a V×V matrix filled with 0 +2. For every edge u → v, set matrix[u][v] = 1 +3. For an undirected graph, also set matrix[v][u] = 1 (the matrix becomes symmetric) +4. For a directed graph the matrix is generally asymmetric: matrix[u][v] ≠ matrix[v][u] + +Time Complexity: + Edge lookup: O(1) + Iterate a node's neighbors: O(V) + Build the matrix: O(V²) + +Space Complexity: O(V²) — independent of the number of edges + +Adjacency matrix vs adjacency list: + - Matrix: O(1) edge lookup, O(V²) space — best for dense graphs + - List: O(V + E) space, fast neighbor iteration — best for sparse graphs + +Applications: + - Dense graphs where most pairs of nodes are connected + - Algorithms needing constant-time edge checks (e.g. Floyd–Warshall) + - Weighted graphs (store the weight instead of 1)`, + 'fibonacci-dp': `Fibonacci (Dynamic Programming) The Fibonacci sequence is a classic example of dynamic programming. Each number is the sum of the two preceding ones: F(n) = F(n-1) + F(n-2). @@ -1871,6 +1897,32 @@ Aplicaciones: El Ordenamiento Topológico solo es posible para DAGs (Grafos Acíclicos Dirigidos). Si el grafo tiene un ciclo, no existe un ordenamiento válido.`, + 'adjacency-matrix': `Matriz de Adyacencia + +Una matriz de adyacencia representa un grafo como una cuadrícula V×V, donde la celda en la fila u, columna v vale 1 (o el peso de la arista) cuando existe una arista de u a v, y 0 en caso contrario. + +Cómo funciona: +1. Crear una matriz V×V llena de 0 +2. Para cada arista u → v, asignar matrix[u][v] = 1 +3. En un grafo no dirigido, asignar también matrix[v][u] = 1 (la matriz se vuelve simétrica) +4. En un grafo dirigido la matriz suele ser asimétrica: matrix[u][v] ≠ matrix[v][u] + +Complejidad Temporal: + Consulta de arista: O(1) + Recorrer los vecinos de un nodo: O(V) + Construir la matriz: O(V²) + +Complejidad Espacial: O(V²) — independiente del número de aristas + +Matriz de adyacencia vs lista de adyacencia: + - Matriz: consulta de arista O(1), espacio O(V²) — ideal para grafos densos + - Lista: espacio O(V + E), recorrido de vecinos rápido — ideal para grafos dispersos + +Aplicaciones: + - Grafos densos donde la mayoría de los pares de nodos están conectados + - Algoritmos que necesitan comprobar aristas en tiempo constante (p. ej. Floyd–Warshall) + - Grafos ponderados (almacenar el peso en lugar de 1)`, + 'fibonacci-dp': `Fibonacci (Programación Dinámica) La secuencia de Fibonacci es un ejemplo clásico de programación dinámica. Cada número es la suma de los dos anteriores: F(n) = F(n-1) + F(n-2). diff --git a/src/lib/algorithms/graphs.ts b/src/lib/algorithms/graphs.ts index d5f3c9d..6b3ddbc 100644 --- a/src/lib/algorithms/graphs.ts +++ b/src/lib/algorithms/graphs.ts @@ -70,7 +70,11 @@ BFS guarantees finding the shortest path (fewest edges) between two nodes in an currentEdge: null, queue: [0], }, - description: d(locale, 'Starting BFS from node 0. Added to queue.', 'Iniciando BFS desde el nodo 0. Agregado a la cola.'), + description: d( + locale, + 'Starting BFS from node 0. Added to queue.', + 'Iniciando BFS desde el nodo 0. Agregado a la cola.', + ), codeLine: 3, variables: { start: 0, queue: '[0]', visited: '{0}', result: '[]' }, }) @@ -89,7 +93,11 @@ BFS guarantees finding the shortest path (fewest edges) between two nodes in an currentEdge: null, queue: [...queue], }, - description: d(locale, `Dequeued node ${node}. Processing neighbors...`, `Nodo ${node} desencolado. Procesando vecinos...`), + description: d( + locale, + `Dequeued node ${node}. Processing neighbors...`, + `Nodo ${node} desencolado. Procesando vecinos...`, + ), codeLine: 8, variables: { node, queue: `[${queue.join(', ')}]`, result: `[${visitedNodes.join(', ')}]` }, }) @@ -110,7 +118,11 @@ BFS guarantees finding the shortest path (fewest edges) between two nodes in an currentEdge: [node, neighbor], queue: [...queue], }, - description: d(locale, `Discovered node ${neighbor} via edge ${node}→${neighbor}. Added to queue.`, `Nodo ${neighbor} descubierto por arista ${node}→${neighbor}. Agregado a la cola.`), + description: d( + locale, + `Discovered node ${neighbor} via edge ${node}→${neighbor}. Added to queue.`, + `Nodo ${neighbor} descubierto por arista ${node}→${neighbor}. Agregado a la cola.`, + ), codeLine: 13, variables: { node, @@ -133,7 +145,11 @@ BFS guarantees finding the shortest path (fewest edges) between two nodes in an currentEdge: null, queue: [], }, - description: d(locale, `BFS complete! Visit order: ${visitedNodes.join(' → ')}`, `¡BFS completado! Orden de visita: ${visitedNodes.join(' → ')}`), + description: d( + locale, + `BFS complete! Visit order: ${visitedNodes.join(' → ')}`, + `¡BFS completado! Orden de visita: ${visitedNodes.join(' → ')}`, + ), codeLine: 19, variables: { result: `[${visitedNodes.join(', ')}]` }, }) @@ -230,7 +246,11 @@ DFS explores deep paths first, which makes it useful for topological sorting and currentEdge: null, stack: [...stack], }, - description: d(locale, `Visiting node ${node}. Exploring its neighbors...`, `Visitando nodo ${node}. Explorando sus vecinos...`), + description: d( + locale, + `Visiting node ${node}. Exploring its neighbors...`, + `Visitando nodo ${node}. Explorando sus vecinos...`, + ), codeLine: 6, variables: { node, @@ -254,7 +274,11 @@ DFS explores deep paths first, which makes it useful for topological sorting and currentEdge: [node, neighbor], stack: [...stack], }, - description: d(locale, `Exploring edge ${node} → ${neighbor}`, `Explorando arista ${node} → ${neighbor}`), + description: d( + locale, + `Exploring edge ${node} → ${neighbor}`, + `Explorando arista ${node} → ${neighbor}`, + ), codeLine: 9, variables: { node, @@ -281,7 +305,11 @@ DFS explores deep paths first, which makes it useful for topological sorting and currentEdge: null, stack: [...stack], }, - description: d(locale, `Backtracking from node ${node} to node ${stack[stack.length - 1]}`, `Retrocediendo del nodo ${node} al nodo ${stack[stack.length - 1]}`), + description: d( + locale, + `Backtracking from node ${node} to node ${stack[stack.length - 1]}`, + `Retrocediendo del nodo ${node} al nodo ${stack[stack.length - 1]}`, + ), codeLine: 12, variables: { node, @@ -304,7 +332,11 @@ DFS explores deep paths first, which makes it useful for topological sorting and currentEdge: null, stack: [], }, - description: d(locale, `DFS complete! Visit order: ${visitedNodes.join(' → ')}`, `¡DFS completado! Orden de visita: ${visitedNodes.join(' → ')}`), + description: d( + locale, + `DFS complete! Visit order: ${visitedNodes.join(' → ')}`, + `¡DFS completado! Orden de visita: ${visitedNodes.join(' → ')}`, + ), codeLine: 17, variables: { result: `[${visitedNodes.join(', ')}]` }, }) @@ -427,7 +459,11 @@ Dijkstra's is one of the most important graph algorithms and guarantees optimal currentEdge: null, distances: { ...dist }, }, - description: d(locale, 'Starting Dijkstra from node A. All distances set to ∞ except source (0).', 'Iniciando Dijkstra desde el nodo A. Todas las distancias en ∞ excepto el origen (0).'), + description: d( + locale, + 'Starting Dijkstra from node A. All distances set to ∞ except source (0).', + 'Iniciando Dijkstra desde el nodo A. Todas las distancias en ∞ excepto el origen (0).', + ), codeLine: 1, variables: { start: 'A', distances: distStr() }, }) @@ -457,7 +493,11 @@ Dijkstra's is one of the most important graph algorithms and guarantees optimal currentEdge: null, distances: { ...dist }, }, - description: d(locale, `Pick node ${djNodes[minNode].label} (distance ${dist[minNode]}). Relaxing its neighbors...`, `Seleccionado nodo ${djNodes[minNode].label} (distancia ${dist[minNode]}). Relajando sus vecinos...`), + description: d( + locale, + `Pick node ${djNodes[minNode].label} (distance ${dist[minNode]}). Relaxing its neighbors...`, + `Seleccionado nodo ${djNodes[minNode].label} (distancia ${dist[minNode]}). Relajando sus vecinos...`, + ), codeLine: 8, variables: { node: djNodes[minNode].label, @@ -487,7 +527,11 @@ Dijkstra's is one of the most important graph algorithms and guarantees optimal currentEdge: [minNode, neighbor], distances: { ...dist }, }, - description: d(locale, `Relaxed ${djNodes[minNode].label}→${djNodes[neighbor].label} (weight ${weight}). Distance to ${djNodes[neighbor].label}: ${oldDist} → ${newDist}`, `Relajado ${djNodes[minNode].label}→${djNodes[neighbor].label} (peso ${weight}). Distancia a ${djNodes[neighbor].label}: ${oldDist} → ${newDist}`), + description: d( + locale, + `Relaxed ${djNodes[minNode].label}→${djNodes[neighbor].label} (weight ${weight}). Distance to ${djNodes[neighbor].label}: ${oldDist} → ${newDist}`, + `Relajado ${djNodes[minNode].label}→${djNodes[neighbor].label} (peso ${weight}). Distancia a ${djNodes[neighbor].label}: ${oldDist} → ${newDist}`, + ), codeLine: 20, variables: { from: djNodes[minNode].label, @@ -508,7 +552,11 @@ Dijkstra's is one of the most important graph algorithms and guarantees optimal currentEdge: [minNode, neighbor], distances: { ...dist }, }, - description: d(locale, `Edge ${djNodes[minNode].label}→${djNodes[neighbor].label} (weight ${weight}): ${newDist} ≥ ${oldDist}. No improvement.`, `Arista ${djNodes[minNode].label}→${djNodes[neighbor].label} (peso ${weight}): ${newDist} ≥ ${oldDist}. Sin mejora.`), + description: d( + locale, + `Edge ${djNodes[minNode].label}→${djNodes[neighbor].label} (weight ${weight}): ${newDist} ≥ ${oldDist}. No improvement.`, + `Arista ${djNodes[minNode].label}→${djNodes[neighbor].label} (peso ${weight}): ${newDist} ≥ ${oldDist}. Sin mejora.`, + ), codeLine: 20, variables: { from: djNodes[minNode].label, @@ -532,7 +580,11 @@ Dijkstra's is one of the most important graph algorithms and guarantees optimal currentEdge: null, distances: { ...dist }, }, - description: d(locale, `Dijkstra complete! Shortest distances from A: ${distStr()}`, `¡Dijkstra completado! Distancias más cortas desde A: ${distStr()}`), + description: d( + locale, + `Dijkstra complete! Shortest distances from A: ${distStr()}`, + `¡Dijkstra completado! Distancias más cortas desde A: ${distStr()}`, + ), codeLine: 26, variables: { distances: distStr() }, }) @@ -659,7 +711,11 @@ Prim's Algorithm is a greedy algorithm that always picks the locally optimal edg currentEdge: null, distances: { ...key }, }, - description: d(locale, "Starting Prim's MST from node A. All key values set to ∞ except source (0).", 'Iniciando MST de Prim desde el nodo A. Todos los valores clave en ∞ excepto el origen (0).'), + description: d( + locale, + "Starting Prim's MST from node A. All key values set to ∞ except source (0).", + 'Iniciando MST de Prim desde el nodo A. Todos los valores clave en ∞ excepto el origen (0).', + ), codeLine: 1, variables: { start: 'A', keys: keyStr() }, }) @@ -696,8 +752,16 @@ Prim's Algorithm is a greedy algorithm that always picks the locally optimal edg }, description: parent[minNode] !== null - ? d(locale, `Added ${prNodes[minNode].label} to MST via edge ${prNodes[parent[minNode]!].label}→${prNodes[minNode].label} (weight ${key[minNode]})`, `${prNodes[minNode].label} agregado al MST por arista ${prNodes[parent[minNode]!].label}→${prNodes[minNode].label} (peso ${key[minNode]})`) - : d(locale, `Starting MST from node ${prNodes[minNode].label}`, `Iniciando MST desde el nodo ${prNodes[minNode].label}`), + ? d( + locale, + `Added ${prNodes[minNode].label} to MST via edge ${prNodes[parent[minNode]!].label}→${prNodes[minNode].label} (weight ${key[minNode]})`, + `${prNodes[minNode].label} agregado al MST por arista ${prNodes[parent[minNode]!].label}→${prNodes[minNode].label} (peso ${key[minNode]})`, + ) + : d( + locale, + `Starting MST from node ${prNodes[minNode].label}`, + `Iniciando MST desde el nodo ${prNodes[minNode].label}`, + ), codeLine: 8, variables: { node: prNodes[minNode].label, key: key[minNode] as number, keys: keyStr() }, }) @@ -721,7 +785,11 @@ Prim's Algorithm is a greedy algorithm that always picks the locally optimal edg currentEdge: [minNode, neighbor], distances: { ...key }, }, - description: d(locale, `Updated key of ${prNodes[neighbor].label}: ${oldKey} → ${weight} (via ${prNodes[minNode].label})`, `Clave de ${prNodes[neighbor].label} actualizada: ${oldKey} → ${weight} (vía ${prNodes[minNode].label})`), + description: d( + locale, + `Updated key of ${prNodes[neighbor].label}: ${oldKey} → ${weight} (via ${prNodes[minNode].label})`, + `Clave de ${prNodes[neighbor].label} actualizada: ${oldKey} → ${weight} (vía ${prNodes[minNode].label})`, + ), codeLine: 21, variables: { from: prNodes[minNode].label, @@ -752,7 +820,11 @@ Prim's Algorithm is a greedy algorithm that always picks the locally optimal edg currentEdge: null, distances: { ...key }, }, - description: d(locale, `Prim's complete! MST total weight: ${totalWeight}. Edges: ${visitedEdges.map(([f, t]) => `${prNodes[f].label}-${prNodes[t].label}`).join(', ')}`, `¡Prim completado! Peso total del MST: ${totalWeight}. Aristas: ${visitedEdges.map(([f, t]) => `${prNodes[f].label}-${prNodes[t].label}`).join(', ')}`), + description: d( + locale, + `Prim's complete! MST total weight: ${totalWeight}. Edges: ${visitedEdges.map(([f, t]) => `${prNodes[f].label}-${prNodes[t].label}`).join(', ')}`, + `¡Prim completado! Peso total del MST: ${totalWeight}. Aristas: ${visitedEdges.map(([f, t]) => `${prNodes[f].label}-${prNodes[t].label}`).join(', ')}`, + ), codeLine: 28, variables: { totalWeight, mstEdges: visitedEdges.length }, }) @@ -878,7 +950,11 @@ If the graph has a cycle, a topological ordering is not possible (not all nodes currentEdge: null, queue: [], }, - description: d(locale, `DAG with ${tsNodes.length} nodes. Computing in-degrees for Kahn's algorithm.`, `DAG con ${tsNodes.length} nodos. Calculando grados de entrada para el algoritmo de Kahn.`), + description: d( + locale, + `DAG with ${tsNodes.length} nodes. Computing in-degrees for Kahn's algorithm.`, + `DAG con ${tsNodes.length} nodos. Calculando grados de entrada para el algoritmo de Kahn.`, + ), codeLine: 1, variables: { inDegrees: inDegStr() }, }) @@ -898,7 +974,11 @@ If the graph has a cycle, a topological ordering is not possible (not all nodes currentEdge: null, queue: [...queue], }, - description: d(locale, `Nodes with in-degree 0: [${queue.map((id) => tsNodes[id].label).join(', ')}]. Added to queue.`, `Nodos con grado de entrada 0: [${queue.map((id) => tsNodes[id].label).join(', ')}]. Agregados a la cola.`), + description: d( + locale, + `Nodes with in-degree 0: [${queue.map((id) => tsNodes[id].label).join(', ')}]. Added to queue.`, + `Nodos con grado de entrada 0: [${queue.map((id) => tsNodes[id].label).join(', ')}]. Agregados a la cola.`, + ), codeLine: 12, variables: { queue: `[${queue.map((id) => tsNodes[id].label).join(', ')}]`, @@ -921,7 +1001,11 @@ If the graph has a cycle, a topological ordering is not possible (not all nodes currentEdge: null, queue: [...queue], }, - description: d(locale, `Dequeued ${tsNodes[node].label}. Order: [${order.map((id) => tsNodes[id].label).join(', ')}]`, `${tsNodes[node].label} desencolado. Orden: [${order.map((id) => tsNodes[id].label).join(', ')}]`), + description: d( + locale, + `Dequeued ${tsNodes[node].label}. Order: [${order.map((id) => tsNodes[id].label).join(', ')}]`, + `${tsNodes[node].label} desencolado. Orden: [${order.map((id) => tsNodes[id].label).join(', ')}]`, + ), codeLine: 18, variables: { node: tsNodes[node].label, @@ -948,7 +1032,11 @@ If the graph has a cycle, a topological ordering is not possible (not all nodes currentEdge: [node, neighbor], queue: [...queue], }, - description: d(locale, `Reduced in-degree of ${tsNodes[neighbor].label} to ${inDegree[neighbor]}${inDegree[neighbor] === 0 ? ' → added to queue' : ''}`, `Grado de entrada de ${tsNodes[neighbor].label} reducido a ${inDegree[neighbor]}${inDegree[neighbor] === 0 ? ' → agregado a la cola' : ''}`), + description: d( + locale, + `Reduced in-degree of ${tsNodes[neighbor].label} to ${inDegree[neighbor]}${inDegree[neighbor] === 0 ? ' → added to queue' : ''}`, + `Grado de entrada de ${tsNodes[neighbor].label} reducido a ${inDegree[neighbor]}${inDegree[neighbor] === 0 ? ' → agregado a la cola' : ''}`, + ), codeLine: 22, variables: { from: tsNodes[node].label, @@ -970,7 +1058,11 @@ If the graph has a cycle, a topological ordering is not possible (not all nodes currentEdge: null, queue: [], }, - description: d(locale, `Topological sort complete! Order: ${order.map((id) => tsNodes[id].label).join(' → ')}`, `¡Ordenamiento topológico completado! Orden: ${order.map((id) => tsNodes[id].label).join(' → ')}`), + description: d( + locale, + `Topological sort complete! Order: ${order.map((id) => tsNodes[id].label).join(' → ')}`, + `¡Ordenamiento topológico completado! Orden: ${order.map((id) => tsNodes[id].label).join(' → ')}`, + ), codeLine: 30, variables: { order: `[${order.map((id) => tsNodes[id].label).join(', ')}]` }, }) @@ -979,10 +1071,158 @@ If the graph has a cycle, a topological ordering is not possible (not all nodes }, } -export { - bfs, - dfs, - dijkstra, - prim, - topologicalSort, +// ============================================================ +// ADJACENCY MATRIX (graph representation) +// ============================================================ +const adjacencyMatrix: Algorithm = { + id: 'adjacency-matrix', + name: 'Adjacency Matrix', + category: 'Graphs', + difficulty: 'easy', + visualization: 'concept', + code: `function buildAdjacencyMatrix(numNodes, edges) { + // Initialize an N×N matrix filled with 0 + const matrix = Array.from({ length: numNodes }, () => + new Array(numNodes).fill(0) + ); + + // For each directed edge u → v, set matrix[u][v] = 1 + for (const [u, v] of edges) { + matrix[u][v] = 1; + } + + return matrix; +}`, + description: `Adjacency Matrix + +An adjacency matrix is a way to represent a graph as a 2D grid. For a graph with V vertices, it is a V×V matrix where the cell at row u, column v is 1 (or the edge weight) when there is an edge from u to v, and 0 otherwise. + +How it works: +1. Create a V×V matrix filled with 0 +2. For every edge u→v, set matrix[u][v] = 1 +3. For an undirected graph you also set matrix[v][u] = 1, which makes the matrix symmetric +4. For a directed graph the matrix is generally asymmetric: matrix[u][v] ≠ matrix[v][u] + +Time Complexity: + Edge lookup: O(1) — just read matrix[u][v] + Iterate a node's neighbors: O(V) + Build the matrix: O(V²) + +Space Complexity: O(V²) — independent of the number of edges + +Adjacency matrix vs adjacency list: + - Matrix: O(1) edge lookup, but O(V²) space — best for dense graphs + - List: O(V + E) space and fast neighbor iteration — best for sparse graphs + +Applications: + - Dense graphs where most pairs of nodes are connected + - Algorithms that need constant-time edge checks (e.g. Floyd–Warshall) + - Weighted graphs (store the weight instead of 1)`, + + generateSteps(locale = 'en') { + const amNodes: GraphNode[] = [ + { id: 0, label: 'A', x: 60, y: 50 }, + { id: 1, label: 'B', x: 300, y: 50 }, + { id: 2, label: 'C', x: 60, y: 170 }, + { id: 3, label: 'D', x: 300, y: 170 }, + { id: 4, label: 'E', x: 180, y: 110 }, + ] + const amEdges: GraphEdge[] = [ + { from: 0, to: 1 }, + { from: 0, to: 2 }, + { from: 1, to: 3 }, + { from: 2, to: 3 }, + { from: 2, to: 4 }, + { from: 3, to: 4 }, + { from: 4, to: 0 }, + ] + + const n = amNodes.length + const matrix: number[][] = amNodes.map(() => new Array(n).fill(0)) + const labelOf = (id: number) => amNodes[id].label + + const steps: Step[] = [] + const snapshot = () => matrix.map((row) => [...row]) + + steps.push({ + concept: { + type: 'adjacencyMatrix', + nodes: amNodes, + edges: amEdges, + matrix: snapshot(), + directed: true, + }, + description: d( + locale, + `A directed graph with ${n} nodes and ${amEdges.length} edges. Let's represent it as a ${n}×${n} adjacency matrix.`, + `Un grafo dirigido con ${n} nodos y ${amEdges.length} aristas. Vamos a representarlo como una matriz de adyacencia de ${n}×${n}.`, + ), + codeLine: 1, + variables: { nodes: n, edges: amEdges.length }, + }) + + steps.push({ + concept: { + type: 'adjacencyMatrix', + nodes: amNodes, + edges: amEdges, + matrix: snapshot(), + directed: true, + }, + description: d( + locale, + `Initialize a ${n}×${n} matrix filled with 0 — no edges recorded yet.`, + `Inicializa una matriz de ${n}×${n} llena de 0 — todavía sin aristas registradas.`, + ), + codeLine: 3, + variables: { matrix: `${n}×${n} of 0` }, + }) + + for (const e of amEdges) { + matrix[e.from][e.to] = 1 + + steps.push({ + concept: { + type: 'adjacencyMatrix', + nodes: amNodes, + edges: amEdges, + matrix: snapshot(), + directed: true, + currentEdge: [e.from, e.to], + highlightCells: [[e.from, e.to]], + }, + description: d( + locale, + `Edge ${labelOf(e.from)}→${labelOf(e.to)}: set matrix[${e.from}][${e.to}] = 1.`, + `Arista ${labelOf(e.from)}→${labelOf(e.to)}: asigna matrix[${e.from}][${e.to}] = 1.`, + ), + codeLine: 9, + variables: { + edge: `${labelOf(e.from)}→${labelOf(e.to)}`, + [`matrix[${e.from}][${e.to}]`]: 1, + }, + }) + } + + steps.push({ + concept: { + type: 'adjacencyMatrix', + nodes: amNodes, + edges: amEdges, + matrix: snapshot(), + directed: true, + }, + description: d( + locale, + 'Matrix complete. Because the graph is directed, it is asymmetric: matrix[u][v] does not always equal matrix[v][u]. Edge lookup is now O(1).', + 'Matriz completa. Como el grafo es dirigido, es asimétrica: matrix[u][v] no siempre es igual a matrix[v][u]. La consulta de aristas ahora es O(1).', + ), + codeLine: 12, + variables: { space: 'O(V²)', lookup: 'O(1)' }, + }) + + return steps + }, } + +export { bfs, dfs, dijkstra, prim, topologicalSort, adjacencyMatrix } diff --git a/src/lib/algorithms/index.ts b/src/lib/algorithms/index.ts index 0985581..916ae71 100644 --- a/src/lib/algorithms/index.ts +++ b/src/lib/algorithms/index.ts @@ -39,25 +39,11 @@ import { interpolationSearch, } from '@lib/algorithms/searching' -import { - bfs, - dfs, - dijkstra, - prim, - topologicalSort, -} from '@lib/algorithms/graphs' +import { bfs, dfs, dijkstra, prim, topologicalSort, adjacencyMatrix } from '@lib/algorithms/graphs' -import { - fibonacciDp, - knapsack, - lcs, -} from '@lib/algorithms/dynamic-programming' +import { fibonacciDp, knapsack, lcs } from '@lib/algorithms/dynamic-programming' -import { - nQueens, - sudokuSolver, - mazePathfinding, -} from '@lib/algorithms/backtracking' +import { nQueens, sudokuSolver, mazePathfinding } from '@lib/algorithms/backtracking' import { towerOfHanoi } from '@lib/algorithms/divide-and-conquer' @@ -96,6 +82,7 @@ export const algorithms: Algorithm[] = [ jumpSearch, interpolationSearch, // Graphs + adjacencyMatrix, bfs, dfs, dijkstra, @@ -117,7 +104,10 @@ export const algorithms: Algorithm[] = [ export const categories: Category[] = [ { name: 'Concepts', algorithms: algorithms.filter((a) => a.category === 'Concepts') }, - { name: 'Data Structures', algorithms: algorithms.filter((a) => a.category === 'Data Structures') }, + { + name: 'Data Structures', + algorithms: algorithms.filter((a) => a.category === 'Data Structures'), + }, { name: 'Sorting', algorithms: algorithms.filter((a) => a.category === 'Sorting') }, { name: 'Searching', algorithms: algorithms.filter((a) => a.category === 'Searching') }, { name: 'Graphs', algorithms: algorithms.filter((a) => a.category === 'Graphs') }, diff --git a/src/lib/types.ts b/src/lib/types.ts index f9054d1..cf62aeb 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -194,6 +194,18 @@ export interface BucketsState { operation?: string } +// ── Graph representation visualization types ── + +export interface AdjacencyMatrixState { + type: 'adjacencyMatrix' + nodes: GraphNode[] + edges: GraphEdge[] + matrix: number[][] // N×N, 0/1; rows = "from", cols = "to" + directed: boolean + currentEdge?: [number, number] | null // edge being processed (graph highlight) + highlightCells?: [number, number][] // matrix cells [row, col] to highlight +} + export type ConceptState = | BigOState | CallStackState @@ -206,6 +218,7 @@ export type ConceptState = | MemoTableState | CoinChangeState | BucketsState + | AdjacencyMatrixState export interface Step { array?: number[]