diff --git a/ROADMAP.md b/ROADMAP.md index 209b3ea..56ae73b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -33,7 +33,7 @@ - [x] Create benchmarking scripts to compare algorithm variants - [x] Expand CI to include coverage gating and bundle size checks -- ## Milestone 0.4.0 – Procedural Worlds & Game Systems (Planned) +## Milestone 0.4.0 – Procedural Worlds & Game Systems (Planned) - Procedural generators: - [x] Wave Function Collapse tile solver with options + example - [x] Cellular automata cave/organic generator utilities @@ -82,36 +82,36 @@ - [x] Boyer–Moore fast substring search - [x] Suffix array construction utilities - [x] Longest common subsequence (LCS) enhancements and diff helpers -- **Data pipelines & utilities** +**Data pipelines & utilities** - [x] Flatten/unflatten helpers for nested structures - [x] Pagination utilities for client-side paging - [x] Advanced diff tooling (tree diff, selective patches) -- **Visual & simulation tools** +**Visual & simulation tools** - [x] Color manipulation helpers (RGB/HSL conversion, blending) - [x] Force-directed graph layout - [x] Marching squares contour extraction - [x] Marching cubes isosurface generation -- **Graph algorithms** +**Graph algorithms** - [x] Minimum spanning tree (Kruskal) - [ ] Strongly connected components (Tarjan/Kosaraju) - [ ] Maximum flow (Dinic preferred; Edmonds–Karp fallback) -- **Spatial & collision expansion** +**Spatial & collision expansion** - [ ] Octree partitioning for 3D space - [ ] Circle collision helpers - [ ] Raycasting utilities - [ ] Bounding volume hierarchy (BVH) builder -- **Data structures** +**Data structures** - [ ] Binary heap priority queue - [ ] Disjoint set union (union-find) - [ ] Bloom filter probabilistic membership - [ ] Skip list sorted structure - [ ] Segment tree range query helper -- **Compression & encoding** +**Compression & encoding** - [ ] Run-length encoding (RLE) - [ ] Huffman coding utilities - [ ] LZ77 dictionary compression helper - [ ] Base64 encode/decode utilities -- **Geometric & numeric utilities** +**Geometric & numeric utilities** - [ ] Closest pair of points solver for geometry toolkit ### LLM‑Optimised Additions (Priority Rationale) diff --git a/docs/index.d.ts b/docs/index.d.ts index 39835fd..ff0e160 100644 --- a/docs/index.d.ts +++ b/docs/index.d.ts @@ -142,6 +142,7 @@ export const examples: { readonly graphDFS: 'examples/graph.ts'; readonly topologicalSort: 'examples/graph.ts'; readonly computeMinimumSpanningTree: 'examples/kruskal.ts'; + readonly computeStronglyConnectedComponents: 'examples/scc.ts'; }; readonly geometry: { readonly convexHull: 'examples/geometry.ts'; @@ -2898,6 +2899,26 @@ export interface MinimumSpanningTree { */ export function computeMinimumSpanningTree(options: KruskalOptions): MinimumSpanningTree; +/** + * Strongly connected components result payload. + * Import: graph/scc.ts + */ +export interface SCCResult { + components: string[][]; +} + +/** + * Computes strongly connected components via Tarjan's algorithm. + * Import: graph/scc.ts + */ +export function computeStronglyConnectedComponents(graph: Graph): SCCResult; + +/** + * Builds a condensation DAG from SCCs where nodes are component indices. + * Import: graph/scc.ts + */ +export function buildCondensationGraph(graph: Graph, components: string[][]): Graph; + // ============================================================================ // 📐 GEOMETRY & VISUALS // ============================================================================ diff --git a/examples/scc.ts b/examples/scc.ts new file mode 100644 index 0000000..b91bb91 --- /dev/null +++ b/examples/scc.ts @@ -0,0 +1,17 @@ +import { buildCondensationGraph, computeStronglyConnectedComponents } from '../src/index.js'; + +const graph = { + A: [{ node: 'B' }], + B: [{ node: 'C' }, { node: 'E' }], + C: [{ node: 'A' }, { node: 'D' }], + D: [{ node: 'E' }], + E: [{ node: 'F' }], + F: [{ node: 'D' }], +}; + +const { components } = computeStronglyConnectedComponents(graph); +console.log('SCCs:', components); + +const dag = buildCondensationGraph(graph, components); +console.log('Condensation DAG:', dag); + diff --git a/src/graph/scc.ts b/src/graph/scc.ts new file mode 100644 index 0000000..beb1287 --- /dev/null +++ b/src/graph/scc.ts @@ -0,0 +1,90 @@ +import type { Graph } from '../types.js'; + +export interface SCCResult { + components: string[][]; +} + +/** + * Computes strongly connected components using Tarjan's algorithm. + * Use for: condensation graphs, cycle detection, program analysis. + */ +export function computeStronglyConnectedComponents(graph: Graph): SCCResult { + const indexByNode = new Map(); + const lowlinkByNode = new Map(); + const onStack = new Set(); + const stack: string[] = []; + const components: string[][] = []; + let index = 0; + + function strongConnect(v: string): void { + indexByNode.set(v, index); + lowlinkByNode.set(v, index); + index += 1; + stack.push(v); + onStack.add(v); + + const edges = graph[v] ?? []; + for (const { node: w } of edges) { + if (!indexByNode.has(w)) { + strongConnect(w); + lowlinkByNode.set(v, Math.min(lowlinkByNode.get(v)!, lowlinkByNode.get(w)!)); + } else if (onStack.has(w)) { + lowlinkByNode.set(v, Math.min(lowlinkByNode.get(v)!, indexByNode.get(w)!)); + } + } + + if (lowlinkByNode.get(v) === indexByNode.get(v)) { + const component: string[] = []; + // Pop nodes until we close the current component + // Using an explicit loop with break to avoid recursion overhead. + for (;;) { + const w = stack.pop(); + if (w === undefined) { + break; + } + onStack.delete(w); + component.push(w); + if (w === v) { + break; + } + } + components.push(component); + } + } + + for (const v of Object.keys(graph)) { + if (!indexByNode.has(v)) { + strongConnect(v); + } + } + + return { components }; +} + +/** + * Builds a condensation DAG from SCC components. + * Nodes become component indices, edges preserved between components. + */ +export function buildCondensationGraph(graph: Graph, components: string[][]): Graph { + const compIndex = new Map(); + components.forEach((comp, idx) => { + for (const node of comp) compIndex.set(node, idx); + }); + const dag: Graph = {}; + for (let i = 0; i < components.length; i += 1) { + dag[String(i)] = []; + } + for (const [v, edges] of Object.entries(graph)) { + const from = compIndex.get(v)!; + for (const { node: w } of edges ?? []) { + const to = compIndex.get(w)!; + if (from !== to) { + const list = dag[String(from)]; + if (!list.some((e) => e.node === String(to))) { + list.push({ node: String(to) }); + } + } + } + } + return dag; +} diff --git a/src/index.ts b/src/index.ts index 5dc480e..3f8d06e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -141,6 +141,7 @@ export const examples = { graphDFS: 'examples/graph.ts', topologicalSort: 'examples/graph.ts', computeMinimumSpanningTree: 'examples/kruskal.ts', + computeStronglyConnectedComponents: 'examples/scc.ts', }, geometry: { convexHull: 'examples/geometry.ts', @@ -1003,6 +1004,13 @@ export { computeMinimumSpanningTree } from './graph/kruskal.js'; export type { KruskalOptions, MinimumSpanningTree, WeightedEdge } from './graph/kruskal.js'; +/** + * Strongly connected components and condensation graph helpers. + */ +export { computeStronglyConnectedComponents, buildCondensationGraph } from './graph/scc.js'; + +export type { SCCResult } from './graph/scc.js'; + // ============================================================================ // 📐 GEOMETRY UTILITIES // ============================================================================ diff --git a/tests/scc.test.ts b/tests/scc.test.ts new file mode 100644 index 0000000..2baafd8 --- /dev/null +++ b/tests/scc.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; + +import { buildCondensationGraph, computeStronglyConnectedComponents } from '../src/index.js'; + +describe('strongly connected components', () => { + it('finds SCCs in a directed graph and builds condensation DAG', () => { + const graph = { + A: [{ node: 'B' }], + B: [{ node: 'C' }, { node: 'E' }], + C: [{ node: 'A' }, { node: 'D' }], + D: [{ node: 'E' }], + E: [{ node: 'F' }], + F: [{ node: 'D' }], + }; + + const { components } = computeStronglyConnectedComponents(graph); + // Expect two cyclic components: {A,B,C} and {D,E,F} + const sorted = components.map((c) => c.sort()).sort((a, b) => a[0].localeCompare(b[0])); + expect(sorted).toEqual([ + ['A', 'B', 'C'], + ['D', 'E', 'F'], + ]); + + const dag = buildCondensationGraph(graph, components); + // There should be one edge from comp(ABC) -> comp(DEF) + const edgesOutOf0 = dag['0']?.map((e) => e.node) ?? []; + const edgesOutOf1 = dag['1']?.map((e) => e.node) ?? []; + const totalEdges = edgesOutOf0.length + edgesOutOf1.length; + expect(totalEdges).toBe(1); + }); +}); +