Skip to content

Commit 946a5b0

Browse files
committed
✨ add dijkstra implementation
1 parent ce6ef1d commit 946a5b0

5 files changed

Lines changed: 413 additions & 0 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"husky": "^8.0.3",
2626
"jest": "^29.6.2",
2727
"lint-staged": "^14.0.0",
28+
"mnemonist": "^0.39.6",
2829
"npm-run-all": "^4.1.5",
2930
"prettier": "^3.0.2",
3031
"rimraf": "^5.0.1",

src/graph/dijkstra.bench.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import * as mnemonist from "mnemonist";
2+
import { HeapNode, DijkstraShortestPath } from "./dijkstra";
3+
4+
export const createSPA = (edges: any[]) => {
5+
return new DijkstraShortestPath(
6+
edges,
7+
() => new mnemonist.Heap<HeapNode>((a, b) => a.distance - b.distance),
8+
1e9,
9+
);
10+
};
11+
12+
function generateSampleData(
13+
N: number,
14+
edgeProb: number,
15+
): [number, number, number][] {
16+
const edges: [number, number, number][] = [];
17+
18+
// For simplicity, we will just generate a random set of edges.
19+
// There is no guarantee every node will be connected.
20+
for (let i = 0; i < N; i++) {
21+
for (let j = 0; j < N; j++) {
22+
if (Math.random() < edgeProb && i !== j) {
23+
// 5% probability of an edge between any two nodes.
24+
const distance = Math.floor(Math.random() * 10) + 1;
25+
if (distance !== 0) {
26+
edges.push([i, j, distance]);
27+
}
28+
}
29+
}
30+
}
31+
32+
return edges;
33+
}
34+
35+
const N = 300;
36+
const EDGE_PROB = 0.1;
37+
const ITERS = 10000;
38+
const edges = generateSampleData(N, EDGE_PROB);
39+
40+
let result: any;
41+
// warmup
42+
const spa = createSPA(edges);
43+
for (let i = 0; i < ITERS; i++) {
44+
const a = Math.floor(Math.random() * N);
45+
const b = Math.floor(Math.random() * N);
46+
result = spa.calculate(a, b);
47+
}
48+
const start = Date.now();
49+
for (let i = 0; i < ITERS; i++) {
50+
const a = Math.floor(Math.random() * N);
51+
const b = Math.floor(Math.random() * N);
52+
result = spa.calculate(a, b);
53+
}
54+
const end = Date.now();
55+
console.log("Benchmarking dijsktra", (end - start) / ITERS, "ms");
56+
console.log(result.distance);

src/graph/dijkstra.spec.ts

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import * as mnemonist from "mnemonist";
2+
import { DijkstraShortestPath, HeapNode } from "./dijkstra";
3+
4+
const INF = 1e9;
5+
6+
const createSPAlgoUndirected = (edges: any[]) => {
7+
return new DijkstraShortestPath(
8+
edges,
9+
() => new mnemonist.Heap<HeapNode>((a, b) => a.distance - b.distance),
10+
INF,
11+
);
12+
};
13+
14+
const createSPAlgoDirected = (edges: any[]) => {
15+
return new DijkstraShortestPath(
16+
edges,
17+
() => new mnemonist.Heap<HeapNode>((a, b) => a.distance - b.distance),
18+
INF,
19+
);
20+
};
21+
22+
test("simple graph 1", () => {
23+
const edges = [
24+
["A", "B", 4],
25+
["A", "C", 2],
26+
["B", "E", 3],
27+
["C", "D", 2],
28+
["C", "F", 4],
29+
["D", "E", 3],
30+
["D", "F", 1],
31+
["F", "E", 1],
32+
];
33+
createSPAlgoUndirected(edges);
34+
expect(createSPAlgoUndirected(edges).calculate("A", "E")).toStrictEqual({
35+
path: ["A", "C", "D", "F", "E"],
36+
distance: 6,
37+
});
38+
});
39+
40+
test("graph with loops and multiple equal paths", () => {
41+
const edges = [
42+
["A", "B", 7],
43+
["A", "D", 5],
44+
["B", "C", 8],
45+
["B", "D", 9],
46+
["B", "E", 7],
47+
["C", "E", 5],
48+
["D", "E", 15],
49+
["D", "F", 6],
50+
["E", "F", 8],
51+
["E", "G", 9],
52+
["F", "G", 11],
53+
];
54+
expect(createSPAlgoUndirected(edges).calculate("A", "G")).toStrictEqual({
55+
path: ["A", "D", "F", "G"],
56+
distance: 22,
57+
});
58+
});
59+
60+
test("graph with a disconnected node", () => {
61+
const edges = [
62+
["A", "B", 6],
63+
["A", "C", 3],
64+
["B", "C", 2],
65+
["B", "D", 5],
66+
["C", "D", 3],
67+
// "E" is disconnected
68+
["F", "G", 1],
69+
["E", "N", 1],
70+
];
71+
expect(createSPAlgoUndirected(edges).calculate("A", "D")).toStrictEqual({
72+
path: ["A", "C", "D"],
73+
distance: 6,
74+
});
75+
// Should return null or equivalent for disconnected nodes
76+
expect(createSPAlgoUndirected(edges).calculate("A", "E")).toStrictEqual({
77+
path: [],
78+
distance: INF,
79+
});
80+
});
81+
82+
test("graph with zero-distance edges", () => {
83+
const edges = [
84+
["A", "B", 0],
85+
["A", "C", 0],
86+
["B", "C", 0],
87+
["B", "D", 1],
88+
["C", "D", 1],
89+
["D", "E", 1],
90+
];
91+
expect(
92+
createSPAlgoUndirected(edges).calculate("A", "E").distance,
93+
).toStrictEqual(2);
94+
});
95+
96+
test("larger graph with multiple routes and varying distances", () => {
97+
const edges = [
98+
["A", "B", 10],
99+
["A", "C", 20],
100+
["B", "D", 10],
101+
["B", "C", 5],
102+
["C", "D", 30],
103+
["C", "E", 75],
104+
["D", "E", 50],
105+
["D", "F", 100],
106+
["E", "G", 60],
107+
["F", "G", 10],
108+
];
109+
expect(createSPAlgoUndirected(edges).calculate("A", "G")).toStrictEqual({
110+
path: ["A", "B", "D", "E", "G"],
111+
distance: 130,
112+
});
113+
});
114+
115+
test("graph with a negative distance edge", () => {
116+
const edges = [
117+
["A", "B", 2],
118+
["A", "C", 5],
119+
["B", "C", -3],
120+
["C", "D", 4],
121+
];
122+
// Dijkstra's algorithm should not allow negative distance edges
123+
// Depending on how your algorithm is designed to handle this,
124+
// it should throw an error, or you can test for a specific error message.
125+
expect(() => createSPAlgoUndirected(edges).calculate("A", "D")).toThrow();
126+
});
127+
128+
test("directed graph with multiple paths and dead ends", () => {
129+
const edges = [
130+
["A", "B", 5],
131+
["A", "D", 1],
132+
["B", "C", 1],
133+
["D", "B", 1],
134+
["C", "E", 2],
135+
["D", "E", 12], // Longer path, but direct to E
136+
["C", "F", 3], // Dead end
137+
];
138+
expect(createSPAlgoDirected(edges).calculate("A", "E")).toStrictEqual({
139+
path: ["A", "D", "B", "C", "E"],
140+
distance: 5,
141+
});
142+
});
143+
144+
test("simple directed graph", () => {
145+
const edges = [
146+
["A", "B", 3],
147+
["A", "C", 6],
148+
["B", "C", 2],
149+
["B", "D", 1],
150+
["C", "D", 1],
151+
["D", "E", 5],
152+
];
153+
expect(createSPAlgoDirected(edges).calculate("A", "E")).toStrictEqual({
154+
path: ["A", "B", "D", "E"],
155+
distance: 9,
156+
});
157+
});
158+
159+
test("directed graph with no path", () => {
160+
const edges = [
161+
["A", "B", 1],
162+
["B", "C", 2],
163+
["C", "D", 3],
164+
// No edge leading to E directly or indirectly from A
165+
["E", "Z", 20],
166+
];
167+
expect(createSPAlgoDirected(edges).calculate("A", "E")).toStrictEqual({
168+
distance: INF,
169+
path: [],
170+
});
171+
});
172+
173+
test("directed acyclic graph (DAG)", () => {
174+
const edges = [
175+
["A", "B", 2],
176+
["A", "C", 6],
177+
["B", "D", 1],
178+
["B", "E", 2],
179+
["C", "E", 2],
180+
["D", "E", 1],
181+
["D", "F", 4],
182+
["E", "F", 2],
183+
];
184+
expect(createSPAlgoDirected(edges).calculate("A", "F")).toStrictEqual({
185+
path: ["A", "B", "E", "F"],
186+
distance: 6,
187+
});
188+
});
189+
190+
test("complex directed graph", () => {
191+
const edges = [
192+
["A", "B", 3],
193+
["A", "C", 8],
194+
["B", "D", 7],
195+
["B", "C", 1],
196+
["C", "E", 3],
197+
["D", "E", 2],
198+
["D", "F", 5],
199+
["E", "G", 2],
200+
["F", "G", 1],
201+
];
202+
expect(createSPAlgoDirected(edges).calculate("A", "G")).toStrictEqual({
203+
path: ["A", "B", "C", "E", "G"],
204+
distance: 9,
205+
});
206+
});

0 commit comments

Comments
 (0)