UNPKG

sssp

Version:

TypeScript implementation of the algorithm that breaks the sorting barrier for directed single-source shortest paths

340 lines (280 loc) 9.31 kB
import { ClassicalDijkstra, createGraph, SSSPSolver } from "../src"; function arraysAlmostEqual( a: number[], b: number[], tolerance: number = 1e-10, ): boolean { return ( a.length === b.length && a.every((val, i) => Math.abs(val - b[i]) <= tolerance) ); } // Test cases describe("SSSP Algorithm Tests", () => { describe("Basic Functionality", () => { test("Single vertex graph", () => { const graph = createGraph(1, []); const solver = new SSSPSolver(graph); const result = solver.solve(0); expect(result.distances).toEqual([0]); expect(result.predecessors).toEqual([null]); }); test("Two vertex graph with single edge", () => { const graph = createGraph(2, [[0, 1, 5]]); const solver = new SSSPSolver(graph); const result = solver.solve(0); expect(result.distances).toEqual([0, 5]); expect(result.predecessors).toEqual([null, 0]); }); test("Disconnected graph", () => { const graph = createGraph(3, [[0, 1, 2]]); const solver = new SSSPSolver(graph); const result = solver.solve(0); expect(result.distances[0]).toBe(0); expect(result.distances[1]).toBe(2); expect(result.distances[2]).toBe(Infinity); expect(result.predecessors).toEqual([null, 0, null]); }); }); describe("Complex Graph Tests", () => { test("Triangle graph", () => { // Graph: 0 -> 1 (weight 4), 0 -> 2 (weight 2), 1 -> 2 (weight 1) const graph = createGraph(3, [ [0, 1, 4], [0, 2, 2], [1, 2, 1], ]); const solver = new SSSPSolver(graph); const result = solver.solve(0); expect(result.distances).toEqual([0, 4, 2]); expect(result.predecessors).toEqual([null, 0, 0]); }); test("Diamond graph with shorter indirect path", () => { // Graph forming a diamond: 0 -> 1,2 -> 3 // Direct path 0->3 vs indirect 0->1->3 or 0->2->3 const graph = createGraph(4, [ [0, 1, 1], [0, 2, 4], [1, 3, 2], [2, 3, 1], [0, 3, 10], // Direct but expensive path ]); const solver = new SSSPSolver(graph); const result = solver.solve(0); expect(result.distances).toEqual([0, 1, 4, 3]); expect(result.predecessors).toEqual([null, 0, 0, 1]); }); test("Larger graph with multiple paths", () => { const graph = createGraph(6, [ [0, 1, 2], [0, 2, 4], [1, 2, 1], [1, 3, 7], [2, 4, 3], [3, 4, 2], [3, 5, 1], [4, 5, 5], ]); const solver = new SSSPSolver(graph); const result = solver.solve(0); // Verify some key distances expect(result.distances[0]).toBe(0); expect(result.distances[1]).toBe(2); expect(result.distances[2]).toBe(3); // via 1, not direct expect(result.distances[4]).toBe(6); // via 1->2->4 }); }); describe("Comparison with Classical Dijkstra", () => { function testGraphAgainstDijkstra( vertices: number, edges: [number, number, number][], source: number, ) { const graph = createGraph(vertices, edges); const newSolver = new SSSPSolver(graph); const classicalSolver = new ClassicalDijkstra(graph); const newResult = newSolver.solve(source); const classicalResult = classicalSolver.solve(source); // Both should produce the same shortest distances expect( arraysAlmostEqual(newResult.distances, classicalResult.distances), ).toBe(true); return { newResult, classicalResult }; } test("Random graph comparison - small", () => { const edges: [number, number, number][] = [ [0, 1, 3], [0, 2, 8], [1, 2, 2], [1, 3, 1], [2, 3, 5], [2, 4, 2], [3, 4, 4], ]; testGraphAgainstDijkstra(5, edges, 0); }); test("Random graph comparison - medium", () => { const edges: [number, number, number][] = [ [0, 1, 4], [0, 2, 2], [1, 2, 3], [1, 3, 2], [1, 4, 3], [2, 3, 4], [2, 5, 5], [3, 4, 1], [3, 5, 6], [4, 5, 2], [4, 6, 4], [5, 6, 1], [5, 7, 3], [6, 7, 2], ]; testGraphAgainstDijkstra(8, edges, 0); }); test("Linear chain graph", () => { const edges: [number, number, number][] = []; for (let i = 0; i < 9; i++) { edges.push([i, i + 1, i + 1]); // Increasing weights } testGraphAgainstDijkstra(10, edges, 0); }); test("Complete graph (small)", () => { const edges: [number, number, number][] = []; const n = 5; for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { if (i !== j) { edges.push([i, j, Math.abs(i - j) + 1]); } } } testGraphAgainstDijkstra(n, edges, 0); }); }); describe("Path Reconstruction", () => { test("Path reconstruction in simple graph", () => { const graph = createGraph(4, [ [0, 1, 1], [1, 2, 2], [2, 3, 3], ]); const solver = new SSSPSolver(graph); solver.solve(0); const path = solver.getPath(3); expect(path).toEqual([0, 1, 2, 3]); }); test("Path reconstruction with multiple routes", () => { const graph = createGraph(4, [ [0, 1, 10], [0, 2, 1], [2, 1, 1], [1, 3, 1], ]); const solver = new SSSPSolver(graph); solver.solve(0); const pathTo3 = solver.getPath(3); expect(pathTo3).toEqual([0, 2, 1, 3]); // Should take shorter route }); test("Path to unreachable vertex", () => { const graph = createGraph(3, [[0, 1, 1]]); const solver = new SSSPSolver(graph); solver.solve(0); const pathTo2 = solver.getPath(2); expect(pathTo2).toEqual([2]); // Only the target vertex (unreachable) }); }); describe("Edge Cases", () => { test("Self-loops", () => { const graph = createGraph(2, [ [0, 0, 5], // Self-loop [0, 1, 3], ]); const solver = new SSSPSolver(graph); const result = solver.solve(0); expect(result.distances).toEqual([0, 3]); }); test("Negative weights (should still work for non-negative cycles)", () => { // Note: This algorithm assumes non-negative weights like Dijkstra // But let's test with some negative weights to see behavior const graph = createGraph(3, [ [0, 1, 5], [1, 2, -2], // Negative weight [0, 2, 4], ]); const solver = new SSSPSolver(graph); const result = solver.solve(0); // The algorithm might not handle negative weights correctly // This test documents the current behavior expect(result.distances[0]).toBe(0); expect(result.distances[1]).toBe(5); }); test("Zero weight edges", () => { const graph = createGraph(3, [ [0, 1, 0], [1, 2, 0], [0, 2, 5], ]); const solver = new SSSPSolver(graph); const result = solver.solve(0); expect(result.distances).toEqual([0, 0, 0]); expect(result.predecessors).toEqual([null, 0, 1]); }); test("Large weights", () => { const graph = createGraph(3, [ [0, 1, 1000000], [0, 2, 999999], [2, 1, 1], ]); const solver = new SSSPSolver(graph); const result = solver.solve(0); expect(result.distances).toEqual([0, 1000000, 999999]); expect(result.predecessors).toEqual([null, 0, 0]); }); }); describe("Performance Characteristics", () => { test("Algorithm completes in reasonable time", () => { // Create a moderately sized graph const n = 100; const edges: [number, number, number][] = []; // Create a connected graph with random edges for (let i = 0; i < n - 1; i++) { edges.push([i, i + 1, Math.floor(Math.random() * 10) + 1]); } // Add some random additional edges for (let i = 0; i < n; i++) { const target = Math.floor(Math.random() * n); if (target !== i) { edges.push([i, target, Math.floor(Math.random() * 20) + 1]); } } const graph = createGraph(n, edges); const solver = new SSSPSolver(graph); const startTime = Date.now(); const result = solver.solve(0); const endTime = Date.now(); expect(endTime - startTime).toBeLessThan(1000); // Should complete within 1 second expect(result.distances[0]).toBe(0); expect(result.distances.every((d) => d >= 0)).toBe(true); }); }); describe("Randomization Properties", () => { test("Algorithm produces consistent results with same seed", () => { const graph = createGraph(5, [ [0, 1, 3], [0, 2, 1], [1, 3, 2], [2, 3, 4], [3, 4, 1], ]); // Run multiple times - should get same results due to seeded randomization const solver1 = new SSSPSolver(graph); const result1 = solver1.solve(0); const solver2 = new SSSPSolver(graph); const result2 = solver2.solve(0); expect(arraysAlmostEqual(result1.distances, result2.distances)).toBe( true, ); }); }); });