Skip to content

Commit b4ce091

Browse files
committed
initial commit
1 parent 502f98c commit b4ce091

File tree

6 files changed

+277
-2
lines changed

6 files changed

+277
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
**/.vscode/

README.md

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,49 @@
1-
# deno-dijkstras-algorithm
2-
A pretty good implementation of Dijkstra's shortest path algorithm that can scale to millions of edges.
1+
# Dijkstra's Shortest Path Algorithm
2+
3+
A pretty good implementation of Dijkstra's shortest-path algorithm.
4+
5+
This code was adapted to Typescript/Deno from
6+
[A Walkthrough of Dijkstra's Algorithm (In JavaScript!)](https://medium.com/@adriennetjohnson/a-walkthrough-of-dijkstras-algorithm-in-javascript-e94b74192026)
7+
on Medium. This code was originally part of
8+
[BlackholeSuns](https://github.com/j50n/blackholesuns), an open source project
9+
that allowed thousands of No Man's Sky players to navigate the galaxy using
10+
mapped black holes. See also
11+
[Dijkstra's algorithm](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm) on
12+
Wikipedia.
13+
14+
# Example
15+
16+
This example recreates the example from the article referenced earlier. The
17+
nodes are mapped to integers from `0` to `n-1`. The names and weights are taken
18+
from the article.
19+
20+
```ts
21+
const FULLSTACK = 0;
22+
const DIGINN = 1;
23+
const DUBLINER = 2;
24+
const STARBUCKS = 3;
25+
const CAFEGRUMPY = 4;
26+
const INSOMNIACOOKIES = 5;
27+
28+
const cafes = DijkstraShortestPathSolver.init(6);
29+
30+
cafes.addBidirEdge(DIGINN, FULLSTACK, 7);
31+
cafes.addBidirEdge(FULLSTACK, STARBUCKS, 6);
32+
cafes.addBidirEdge(DIGINN, DUBLINER, 4);
33+
cafes.addBidirEdge(FULLSTACK, DUBLINER, 2);
34+
cafes.addBidirEdge(DUBLINER, STARBUCKS, 3);
35+
cafes.addBidirEdge(DIGINN, CAFEGRUMPY, 9);
36+
cafes.addBidirEdge(CAFEGRUMPY, INSOMNIACOOKIES, 5);
37+
cafes.addBidirEdge(DUBLINER, INSOMNIACOOKIES, 7);
38+
cafes.addBidirEdge(STARBUCKS, INSOMNIACOOKIES, 6);
39+
40+
assertEquals(
41+
cafes.calculateFor(FULLSTACK).shortestPathTo(CAFEGRUMPY),
42+
[FULLSTACK, DUBLINER, INSOMNIACOOKIES, CAFEGRUMPY],
43+
);
44+
45+
assertEquals(
46+
cafes.calculateFor(FULLSTACK).weights[CAFEGRUMPY],
47+
14,
48+
);
49+
```

deps.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "https://deno.land/[email protected]/testing/asserts.ts";

dijkstra.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { DijkstraShortestPathSolver } from "./dijkstra.ts";
2+
import { assertEquals } from "./deps.ts";
3+
4+
const FULLSTACK = 0;
5+
const DIGINN = 1;
6+
const DUBLINER = 2;
7+
const STARBUCKS = 3;
8+
const CAFEGRUMPY = 4;
9+
const INSOMNIACOOKIES = 5;
10+
11+
const cafes = DijkstraShortestPathSolver.init(6);
12+
13+
cafes.addBidirEdge(DIGINN, FULLSTACK, 7);
14+
cafes.addBidirEdge(FULLSTACK, STARBUCKS, 6);
15+
cafes.addBidirEdge(DIGINN, DUBLINER, 4);
16+
cafes.addBidirEdge(FULLSTACK, DUBLINER, 2);
17+
cafes.addBidirEdge(DUBLINER, STARBUCKS, 3);
18+
cafes.addBidirEdge(DIGINN, CAFEGRUMPY, 9);
19+
cafes.addBidirEdge(CAFEGRUMPY, INSOMNIACOOKIES, 5);
20+
cafes.addBidirEdge(DUBLINER, INSOMNIACOOKIES, 7);
21+
cafes.addBidirEdge(STARBUCKS, INSOMNIACOOKIES, 6);
22+
23+
Deno.test("demonstrate finding shortest path", () => {
24+
const path = cafes.calculateFor(FULLSTACK).shortestPathTo(CAFEGRUMPY);
25+
assertEquals(
26+
path,
27+
[FULLSTACK, DUBLINER, INSOMNIACOOKIES, CAFEGRUMPY],
28+
"shortest path from FULLSTACK to CAFEGRUMPY",
29+
);
30+
});
31+
32+
Deno.test("demonstrate finding the weight of the shortest path", () => {
33+
assertEquals(
34+
cafes.calculateFor(FULLSTACK).weights[CAFEGRUMPY],
35+
14,
36+
"weight of the shortest path",
37+
);
38+
});

dijkstra.ts

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
// @deno-types="https://raw.githubusercontent.com/mourner/tinyqueue/v2.0.3/index.d.ts"
2+
import TinyQueue from "https://raw.githubusercontent.com/mourner/tinyqueue/v2.0.3/index.js";
3+
4+
/**
5+
* An edge.
6+
*/
7+
export interface IEdge {
8+
/** Destination node. */
9+
readonly toNode: number;
10+
/** Weight of the path to the destination node. */
11+
readonly weight: number;
12+
}
13+
14+
interface IPath {
15+
readonly toNode: number;
16+
readonly weight: number;
17+
}
18+
19+
/**
20+
* Implementation of Dijkstra's Shortest Path algorithm. This quickly finds the shortest path between a single point
21+
* and every other point it it connected to.
22+
*
23+
* Nodes are numbered from 0 to n-1.
24+
*
25+
* Adapted from https://medium.com/@adriennetjohnson/a-walkthrough-of-dijkstras-algorithm-in-javascript-e94b74192026
26+
* This has been made much faster by treating nodes as an index rather than a string (name). We use `tinyqueue`
27+
* as our priority queue. All map-likes have been eliminated, but there are still object references. So this is
28+
* not as fast as possible, but it should be plenty fast and not too heavy on memory.
29+
*/
30+
export class DijkstraShortestPathSolver {
31+
private constructor(
32+
public readonly nodes: number,
33+
public readonly adjacencyList: IEdge[][],
34+
) {
35+
}
36+
37+
/**
38+
* Initialize a new empty solver with the number of nodes needed.
39+
* @param nodes The number of nodes in the graph.
40+
* @returns A new solver.
41+
*/
42+
static init(nodes: number): DijkstraShortestPathSolver {
43+
return new DijkstraShortestPathSolver(
44+
nodes,
45+
new Array(nodes).fill(null).map((_v) => new Array(0)),
46+
);
47+
}
48+
49+
/**
50+
* A clone of this solver.
51+
* @returns A cloned solver.
52+
*/
53+
clone(): DijkstraShortestPathSolver {
54+
return new DijkstraShortestPathSolver(
55+
this.nodes,
56+
this.adjacencyList.map((a) => a.slice(0)),
57+
);
58+
}
59+
60+
/**
61+
* Add an edge (in one direction).
62+
* @param fromNode Starting node.
63+
* @param toNode Ending node.
64+
* @param weight Weight of the edge. Must be greater than 0.
65+
*/
66+
addEdge(fromNode: number, toNode: number, weight: number): void {
67+
if (weight < 0) {
68+
throw new RangeError("weight must be >= 0");
69+
}
70+
71+
if (fromNode < 0 || fromNode >= this.nodes) {
72+
throw new RangeError(
73+
`fromNode must be in range 0..${this.nodes - 1}: ${fromNode}`,
74+
);
75+
}
76+
77+
if (toNode < 0 || toNode >= this.nodes) {
78+
throw new RangeError(
79+
`toNode must be in range 0..${this.nodes - 1}: ${toNode}`,
80+
);
81+
}
82+
83+
this.adjacencyList[fromNode].push({ toNode, weight });
84+
}
85+
86+
/**
87+
* Add an edge in both directions.
88+
* @param fromNode Starting node.
89+
* @param toNode Ending node.
90+
* @param weight Weight of the edge. Must be greater than 0.
91+
*/
92+
addBidirEdge(fromNode: number, toNode: number, weight: number): void {
93+
if (weight < 0) {
94+
throw new RangeError("weight must be >= 0");
95+
}
96+
97+
if (fromNode < 0 || fromNode >= this.nodes) {
98+
throw new RangeError(
99+
`fromNode must be in range 0..${this.nodes - 1}: ${fromNode}`,
100+
);
101+
}
102+
103+
if (toNode < 0 || toNode >= this.nodes) {
104+
throw new RangeError(
105+
`toNode must be in range 0..${this.nodes - 1}: ${toNode}`,
106+
);
107+
}
108+
109+
this.adjacencyList[fromNode].push({ toNode, weight });
110+
this.adjacencyList[toNode].push({ toNode: fromNode, weight });
111+
}
112+
113+
setEdges(node: number, edges: IEdge[]): void {
114+
this.adjacencyList[node] = edges;
115+
}
116+
117+
/**
118+
* Calculate shortest paths for all nodes for the given start node.
119+
* @param startNode The start node.
120+
*/
121+
calculateFor(startNode: number): ShortestPaths {
122+
const weights: number[] = new Array(this.nodes).fill(Infinity);
123+
weights[startNode] = 0;
124+
125+
const pq = new TinyQueue<IPath>(
126+
[{ toNode: startNode, weight: 0 }],
127+
(a, b) => a.weight - b.weight,
128+
);
129+
130+
const backtrace: number[] = new Array(this.nodes).fill(-1);
131+
132+
while (pq.length !== 0) {
133+
const shortestStep = pq.pop();
134+
if (shortestStep === undefined) {
135+
throw new Error("shortest-step undefined");
136+
}
137+
const currentNode = shortestStep.toNode;
138+
139+
this.adjacencyList[currentNode].forEach((neighbor) => {
140+
const weight = weights[currentNode] + neighbor.weight;
141+
142+
if (weight < weights[neighbor.toNode]) {
143+
weights[neighbor.toNode] = weight;
144+
backtrace[neighbor.toNode] = currentNode;
145+
pq.push({ toNode: neighbor.toNode, weight: weight });
146+
}
147+
});
148+
}
149+
150+
return new ShortestPaths(startNode, backtrace, weights);
151+
}
152+
}
153+
154+
/**
155+
* Shortest paths result.
156+
*/
157+
export class ShortestPaths {
158+
constructor(
159+
public readonly startNode: number,
160+
public readonly backtrace: number[],
161+
public readonly weights: number[],
162+
) {}
163+
164+
/**
165+
* Find the shortest path to the given end node.
166+
* @param endNode The end node.
167+
*/
168+
shortestPathTo(endNode: number): number[] {
169+
const path = [endNode];
170+
let lastStep = endNode;
171+
172+
while (lastStep != this.startNode) {
173+
path.unshift(this.backtrace[lastStep]);
174+
lastStep = this.backtrace[lastStep];
175+
}
176+
177+
return path;
178+
}
179+
180+
/**
181+
* Total weight of the path from the start node to the given end node.
182+
* @param endNode The end node.
183+
*/
184+
totalWeight(endNode: number): number {
185+
return this.weights[endNode];
186+
}
187+
}

mod.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./dijkstra.ts";

0 commit comments

Comments
 (0)