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
text/typescript
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,
);
});
});
});