Last active
November 21, 2019 10:22
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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")); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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