Skip to content

Instantly share code, notes, and snippets.

@Akjosch
Last active November 21, 2019 10:22
Show Gist options
  • Save Akjosch/f7913cc5c7a4164ab05dfbfecd762020 to your computer and use it in GitHub Desktop.
Save Akjosch/f7913cc5c7a4164ab05dfbfecd762020 to your computer and use it in GitHub Desktop.
Simple generic implementation of Dijkstra's algorithm for SugarCube - will work with almost no changes in any modern browser though
/* More complex, performance-improved version using a binary heap for the "open nodes" set */
/* Binary heap class source by Marijn Haverbeke, https://eloquentjavascript.net/1st_edition/appendix2.html, modified */
/**
* Use Dijkstra's algorithm to find the lowest-cost route between start and target nodes
* given a graph implementation which provides us with a suitable way to query
* the neighbors of a node as well as the cost of traversing from one (source) to
* a connected next (target) node.
*
* Note that this implementation relies on the individual nodes being equal under
* strict equality (===). Using primitive types like strings and numbers for node
* identification is safe, everything else might not be.
*
* The code relies on ES5 and ES6 features like the Set and Map datatypes to exist,
* as well as the "for ... of" iteration working on Maps. Use Babel and shims of your
* choice to make it work on IE11 or older. SugarCube provides all the shims you
* need, in particular.
*
* @param {{ neighbors: (note: T) => T[]; cost: (source: T, target: T) => number; }} graph
* @param {T} start
* @param {T} target
* @returns {T[]} - the least-cost route, or undefined if there isn't any
* @template T
*/
function dijkstra(graph, start, target) {
/** @type {Map<T, number>} */
const costs = new Map();
/** @type {Set<T>} */
const visited = new Set();
/** @type {Map<T, T>} */
const previous = new Map();
const open = new setup.BinaryHeap((node) => costs.get(node));
costs.set(start, 0);
var foundRoute = (start === target);
var current = start;
while(!foundRoute && current !== undefined) {
/* recalculate costs to neighbors, and make sure to record the links in previous */
for(const neighbor of graph.neighbors(current)) {
if(!visited.has(neighbor)) {
var tentativeDistance = costs.get(current) + graph.cost(current, neighbor);
if(!costs.has(neighbor)) {
costs.set(neighbor, tentativeDistance);
open.push(neighbor);
previous.set(neighbor, current);
} else if(costs.get(neighbor) > tentativeDistance) {
costs.set(neighbor, tentativeDistance);
open.rescore(neighbor);
previous.set(neighbor, current);
}
}
}
visited.add(current);
/* search for an unvisited node with the smallest cost */
current = open.pop();
foundRoute = (current === target);
}
/* If we found something, reconstruct the route from the "previous" map */
if(foundRoute) {
var route = [target];
while(previous.has(route[0])) {
route.unshift(previous.get(route[0]));
}
return route;
} else {
return undefined;
}
}
setup.dijkstra = dijkstra;
setup.BinaryHeap = (function() {
const _content = Symbol("content");
const _scoring = Symbol("scoring function");
/**
* @param {T[]} content
* @param {(value:T) => number} scoring}
* @param {number} pos
* @template T
*/
function bubbleUp(content, scoring, pos) {
var element = content[pos];
var score = scoring(element);
while(pos > 0) {
var parentIdx = Math.floor((pos + 1) / 2) - 1;
var parent = content[parentIdx];
if(score >= scoring(parent)) {
break;
}
content[parentIdx] = element;
content[pos] = parent;
pos = parentIdx;
}
}
/**
* @param {T[]} content
* @param {(value:T) => number} scoring}
* @param {number} pos
* @template T
*/
function sinkDown(content, scoring, pos) {
var length = content.length;
var element = content[pos];
var elementScore = scoring(element);
// eslint-disable-next-line no-constant-condition
while(true) {
var child2Idx = (pos + 1) * 2;
var child1Idx = child2Idx - 1;
/** @type {number} */
var swap = NaN;
if(child1Idx < length) {
var child1 = content[child1Idx];
var child1Score = scoring(child1);
if(child1Score < elementScore) {
swap = child1Idx;
}
}
if(child2Idx < length) {
var child2 = content[child2Idx];
var child2Score = scoring(child2);
if(child2Score < (Number.isNaN(swap) ? elementScore : child1Score)) {
swap = child2Idx;
}
}
if(Number.isNaN(swap)) {
break;
}
content[pos] = content[swap];
content[swap] = element;
pos = swap;
}
}
/**
* @template T
*/
class BinaryHeap {
/**
* @param {(value:T) => number} scoring
*/
constructor(scoring) {
this[_content] = [];
if(typeof scoring === "function") {
this[_scoring] = scoring;
}
}
/**
* @param {T} element
*/
push(element) {
this[_content].push(element);
bubbleUp(this[_content], this[_scoring], this[_content].length - 1);
}
/**
* @param {T} element
*/
rescore(element) {
var elementIdx = this[_content].indexOf(element);
if(elementIdx >= 0) {
sinkDown(this[_content], this[_scoring], elementIdx);
}
}
/**
* @returns {T}
*/
pop() {
/** @type {T} */
var result = this[_content][0];
/** @type {T} */
var end = this[_content].pop();
if(this[_content].length > 0) {
this[_content][0] = end;
sinkDown(this[_content], this[_scoring], 0);
}
return result;
}
/**
* @returns {T}
*/
peek() {
return this[_content][0];
}
get length() {
return this[_content].length;
}
}
return BinaryHeap;
})();
/* Example test code with a 1296-node graph */
function graphGen(seed) {
var seed = seed || Math.round(Date.now() * (1 + Math.random())).toString(36).padStart(9, "0").substring(1);
var rnd = new Math.seedrandom(seed);
function nodeName(nr) {
return nr.toString(36).padStart(2, "0");
}
var graph = {
_seed: seed,
_data: {},
neighbors(node) {
return Object.keys(this._data[node]);
},
cost(start, target) {
return (this._data[start] || {})[target] || Infinity;
}
};
for(var i = 0; i < 36 * 36; ++ i) {
var links = {};
for(var l = 0; l < 6; ++ l) {
links[nodeName(Math.floor(rnd() * 36 * 36))] = Math.floor(rnd() * 10 + 1);
}
graph._data[nodeName(i)] = links;
}
return graph;
}
var graph = graphGen("ldsdgl9n");
console.log(setup.dijkstra(graph, "00", "zz"));
/**
* Use Dijkstra's algorithm to find the lowest-cost route between start and target nodes
* given a graph implementation which provides us with a suitable way to query
* the neighbors of a node as well as the cost of traversing from one (source) to
* a connected next (target) node.
*
* Note that this implementation relies on the individual nodes being equal under
* strict equality (===). Using primitive types like strings and numbers for node
* identification is safe, everything else might not be.
*
* The code relies on ES5 and ES6 features like the Set and Map datatypes to exist,
* as well as the "for ... of" iteration working on Maps. Use Babel and shims of your
* choice to make it work on IE11 or older. SugarCube provides all the shims you
* need, in particular.
*
* @param {{ neighbors: (note: T) => T[]; cost: (source: T, target: T) => number; }} graph
* @param {T} start
* @param {T} target
* @returns {T[]} - the least-cost route, or undefined if there isn't any
* @template T
*/
function dijkstra(graph, start, target) {
/** @type {Map<T, number>} */
const costs = new Map();
/** @type {Set<T>} */
const visited = new Set();
/** @type {Map<T, T>} */
const previous = new Map();
costs.set(start, 0);
var foundRoute = (start === target);
var current = start;
while(!foundRoute && current !== undefined) {
/* recalculate costs to neighbors, and make sure to record the links in previous */
for(const neighbor of graph.neighbors(current)) {
if(!visited.has(neighbor)) {
var tentativeDistance = costs.get(current) + graph.cost(current, neighbor);
if(!costs.has(neighbor) || costs.get(neighbor) > tentativeDistance) {
costs.set(neighbor, tentativeDistance);
previous.set(neighbor, current);
}
}
}
visited.add(current);
/* search for an unvisited node with the smallest cost */
current = undefined;
var smallestCost = Infinity;
for(const [node, cost] of costs) {
if(!visited.has(node)) {
if(current === undefined || cost < smallestCost) {
current = node;
smallestCost = cost;
}
}
}
foundRoute = (current === target);
}
/* If we found something, reconstruct the route from the "previous" map */
if(foundRoute) {
var route = [target];
while(previous.has(route[0])) {
route.unshift(previous.get(route[0]));
}
return route;
} else {
return undefined;
}
}
setup.dijkstra = dijkstra;
/* Example test code - graph from https://hackernoon.com/how-to-implement-dijkstras-algorithm-in-javascript-abdfd1702d04 */
var graph = {
_data: {
start: { a: 5, b: 2 },
a: { c: 4, d: 2 },
b: { a: 8, d: 7 },
c: { finish: 3, d: 6 },
d: { finish: 1 },
finish: {}
},
/**
* @param {string} node
*/
neighbors(node) {
return Object.keys(this._data[node]);
},
/**
* @param {string} start
* @param {string} target
*/
cost(start, target) {
return (this._data[start] || {})[target] || Infinity;
}
}
console.log(setup.dijkstra(graph, "start", "finish"));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment