Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
- [x] Marching cubes isosurface generation
**Graph algorithms**
- [x] Minimum spanning tree (Kruskal)
- [ ] Strongly connected components (Tarjan/Kosaraju)
- [x] Strongly connected components (Tarjan/Kosaraju)
- [ ] Maximum flow (Dinic preferred; Edmonds–Karp fallback)
**Spatial & collision expansion**
- [ ] Octree partitioning for 3D space
Expand Down
37 changes: 37 additions & 0 deletions docs/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export const examples: {
readonly topologicalSort: 'examples/graph.ts';
readonly computeMinimumSpanningTree: 'examples/kruskal.ts';
readonly computeStronglyConnectedComponents: 'examples/scc.ts';
readonly computeMaximumFlowDinic: 'examples/maxflow.ts';
};
readonly geometry: {
readonly convexHull: 'examples/geometry.ts';
Expand Down Expand Up @@ -2919,6 +2920,42 @@ export function computeStronglyConnectedComponents(graph: Graph): SCCResult;
*/
export function buildCondensationGraph(graph: Graph, components: string[][]): Graph;

/**
* Edge description for capacity graphs.
* Import: graph/maxflow.ts
*/
export interface FlowEdge {
source: string;
target: string;
capacity: number;
}

/**
* Dinic maximum flow configuration.
* Import: graph/maxflow.ts
*/
export interface MaxFlowOptions {
nodes: ReadonlyArray<string>;
edges: ReadonlyArray<FlowEdge>;
source: string;
sink: string;
}

/**
* Maximum flow result with per-edge flows.
* Import: graph/maxflow.ts
*/
export interface MaxFlowResult {
maxFlow: number;
flows: Array<{ source: string; target: string; flow: number }>;
}

/**
* Computes maximum flow using Dinic's algorithm with residual network.
* Import: graph/maxflow.ts
*/
export function computeMaximumFlowDinic(options: MaxFlowOptions): MaxFlowResult;

// ============================================================================
// 📐 GEOMETRY & VISUALS
// ============================================================================
Expand Down
15 changes: 15 additions & 0 deletions examples/maxflow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { computeMaximumFlowDinic } from '../src/index.js';

const nodes = ['S', 'A', 'B', 'T'];
const edges = [
{ source: 'S', target: 'A', capacity: 10 },
{ source: 'S', target: 'B', capacity: 5 },
{ source: 'A', target: 'B', capacity: 15 },
{ source: 'A', target: 'T', capacity: 10 },
{ source: 'B', target: 'T', capacity: 10 },
];

const result = computeMaximumFlowDinic({ nodes, edges, source: 'S', sink: 'T' });
console.log('MaxFlow:', result.maxFlow);
console.log('Flows:', result.flows);

129 changes: 129 additions & 0 deletions src/graph/maxflow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
export interface FlowEdge {
source: string;
target: string;
capacity: number;
}

export interface MaxFlowOptions {
nodes: ReadonlyArray<string>;
edges: ReadonlyArray<FlowEdge>;
source: string;
sink: string;
}

export interface MaxFlowResult {
maxFlow: number;
flows: Array<{ source: string; target: string; flow: number }>;
}

interface EdgeInternal {
to: number;
rev: number;
capacity: number;
}

export function computeMaximumFlowDinic(options: MaxFlowOptions): MaxFlowResult {
validateOptions(options);
const indexByNode = new Map<string, number>();
options.nodes.forEach((id, idx) => indexByNode.set(id, idx));
const n = options.nodes.length;
const graph: EdgeInternal[][] = Array.from({ length: n }, () => []);

function addEdge(uIdx: number, vIdx: number, cap: number): void {
const a: EdgeInternal = { to: vIdx, rev: graph[vIdx].length, capacity: cap };
const b: EdgeInternal = { to: uIdx, rev: graph[uIdx].length, capacity: 0 };
graph[uIdx].push(a);
graph[vIdx].push(b);
}

for (const e of options.edges) {
const u = indexByNode.get(e.source)!;
const v = indexByNode.get(e.target)!;
addEdge(u, v, e.capacity);
}

const s = indexByNode.get(options.source)!;
const t = indexByNode.get(options.sink)!;

let maxFlow = 0;
const level = new Array<number>(n).fill(-1);
const it = new Array<number>(n).fill(0);

function bfs(): boolean {
level.fill(-1);
const queue: number[] = [s];
level[s] = 0;
for (let qi = 0; qi < queue.length; qi += 1) {
const v = queue[qi];
for (const e of graph[v]) {
if (e.capacity > 0 && level[e.to] < 0) {
level[e.to] = level[v] + 1;
queue.push(e.to);
}
}
}
return level[t] >= 0;
}

function dfs(v: number, f: number): number {
if (v === t) return f;
for (let i = it[v]; i < graph[v].length; i += 1) {
it[v] = i;
const e = graph[v][i];
if (e.capacity <= 0 || level[v] + 1 !== level[e.to]) continue;
const d = dfs(e.to, Math.min(f, e.capacity));
if (d > 0) {
e.capacity -= d;
graph[e.to][e.rev].capacity += d;
return d;
}
}
return 0;
}

while (bfs()) {
it.fill(0);
let flow;
// eslint-disable-next-line no-cond-assign
while ((flow = dfs(s, Number.POSITIVE_INFINITY)) > 0) {
maxFlow += flow;
}
}

// Extract flows on original directed edges
const flows: Array<{ source: string; target: string; flow: number }> = [];
for (const e of options.edges) {
const u = indexByNode.get(e.source)!;
const v = indexByNode.get(e.target)!;
// Residual capacity back edge holds the flow pushed
let pushed = 0;
for (const edge of graph[v]) {
if (edge.to === u) {
pushed += edge.capacity; // this is the accumulated back capacity
}
}
const flowValue = Math.max(0, pushed);
flows.push({ source: e.source, target: e.target, flow: flowValue });
}

return { maxFlow, flows };
}

function validateOptions(options: MaxFlowOptions): void {
if (!Array.isArray(options.nodes) || options.nodes.length === 0) {
throw new Error('nodes must contain at least one node.');
}
const seen = new Set(options.nodes);
if (!seen.has(options.source) || !seen.has(options.sink)) {
throw new Error('source and sink must be present in nodes.');
}
for (const { source, target, capacity } of options.edges) {
if (!seen.has(source) || !seen.has(target)) {
throw new Error(`Edge references unknown node: ${source}-${target}`);
}
if (capacity < 0 || !Number.isFinite(capacity)) {
throw new Error('capacity must be a finite non-negative number.');
}
}
}

8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export const examples = {
topologicalSort: 'examples/graph.ts',
computeMinimumSpanningTree: 'examples/kruskal.ts',
computeStronglyConnectedComponents: 'examples/scc.ts',
computeMaximumFlowDinic: 'examples/maxflow.ts',
},
geometry: {
convexHull: 'examples/geometry.ts',
Expand Down Expand Up @@ -1011,6 +1012,13 @@ export { computeStronglyConnectedComponents, buildCondensationGraph } from './gr

export type { SCCResult } from './graph/scc.js';

/**
* Maximum flow via Dinic with residual network and edge flows.
*/
export { computeMaximumFlowDinic } from './graph/maxflow.js';

export type { MaxFlowOptions, MaxFlowResult, FlowEdge } from './graph/maxflow.js';

// ============================================================================
// 📐 GEOMETRY UTILITIES
// ============================================================================
Expand Down
33 changes: 33 additions & 0 deletions tests/maxflow.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, expect, it } from 'vitest';

import { computeMaximumFlowDinic } from '../src/index.js';

describe('computeMaximumFlowDinic', () => {
it('computes max flow on a small graph', () => {
const nodes = ['S', 'A', 'B', 'T'];
const edges = [
{ source: 'S', target: 'A', capacity: 10 },
{ source: 'S', target: 'B', capacity: 5 },
{ source: 'A', target: 'B', capacity: 15 },
{ source: 'A', target: 'T', capacity: 10 },
{ source: 'B', target: 'T', capacity: 10 },
];

const result = computeMaximumFlowDinic({ nodes, edges, source: 'S', sink: 'T' });
expect(result.maxFlow).toBe(15);
const outOfS = result.flows
.filter((e) => e.source === 'S')
.reduce((sum, e) => sum + e.flow, 0);
expect(outOfS).toBe(result.maxFlow);
});

it('validates inputs', () => {
expect(() =>
computeMaximumFlowDinic({ nodes: [], edges: [], source: 'S', sink: 'T' })
).toThrow('nodes must contain at least one node.');
expect(() =>
computeMaximumFlowDinic({ nodes: ['S'], edges: [], source: 'S', sink: 'T' })
).toThrow('source and sink must be present in nodes.');
});
});

Loading