Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions Graphs/AStar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* A* Search Algorithm
*
* Finds the shortest path between a start node and a target node in a
* weighted graph, using a heuristic function to guide the search toward
* the target, generally exploring fewer nodes than an uninformed search
* such as Dijkstra's algorithm.
*
* The algorithm is only guaranteed to find the true shortest path if the
* heuristic is admissible, i.e. it never overestimates the real cost from
* a node to the target. Passing a heuristic that always returns 0 makes
* A* behave exactly like Dijkstra's algorithm.
*
* For more info: https://en.wikipedia.org/wiki/A*_search_algorithm
*
* @param {Object<string, Array<[string, number]>>} graph - Adjacency list mapping each node to an array of [neighbor, weight] pairs.
* @param {string} start - The node to start the search from.
* @param {string} target - The node to search for.
* @param {(node: string) => number} heuristic - Estimated cost from a node to the target.
* @returns {{ path: string[], cost: number } | null} The shortest path from start to target and its total cost, or null if no path exists.
*
* @example
* const graph = {
* A: [['B', 1], ['C', 4]],
* B: [['C', 1], ['D', 5]],
* C: [['D', 1]],
* D: []
* }
* const heuristic = (node) => ({ A: 2, B: 2, C: 1, D: 0 })[node]
*
* aStarSearch(graph, 'A', 'D', heuristic)
* // => { path: ['A', 'B', 'C', 'D'], cost: 3 }
*/

import { KeyPriorityQueue } from '../Data-Structures/Heap/KeyPriorityQueue.js'

const reconstructPath = (cameFrom, current) => {
const path = [current]
while (cameFrom.has(current)) {
current = cameFrom.get(current)
path.unshift(current)
}
return path
}

function aStarSearch(graph, start, target, heuristic) {
const gScore = new Map([[start, 0]])
const cameFrom = new Map()

const openSet = new KeyPriorityQueue()
openSet.push(start, heuristic(start))

while (!openSet.isEmpty()) {
const current = openSet.pop()

if (current === target) {
return {
path: reconstructPath(cameFrom, current),
cost: gScore.get(current)
}
}

const neighbors = graph[current] || []
for (const [neighbor, weight] of neighbors) {
const tentativeGScore = gScore.get(current) + weight

if (tentativeGScore < (gScore.get(neighbor) ?? Infinity)) {
cameFrom.set(neighbor, current)
gScore.set(neighbor, tentativeGScore)
openSet.update(neighbor, tentativeGScore + heuristic(neighbor))
}
}
}

return null
}

export { aStarSearch }
67 changes: 67 additions & 0 deletions Graphs/test/AStar.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { aStarSearch } from '../AStar.js'

const graph = {
A: [
['B', 1],
['C', 4]
],
B: [
['C', 1],
['D', 5]
],
C: [['D', 1]],
D: []
}

const heuristics = { A: 2, B: 2, C: 1, D: 0 }
const heuristic = (node) => heuristics[node]
const zeroHeuristic = () => 0

test('Finds the shortest path using an admissible heuristic', () => {
expect(aStarSearch(graph, 'A', 'D', heuristic)).toEqual({
path: ['A', 'B', 'C', 'D'],
cost: 3
})
})

test('Matches Dijkstra (zero heuristic) on the same graph', () => {
expect(aStarSearch(graph, 'A', 'D', zeroHeuristic)).toEqual({
path: ['A', 'B', 'C', 'D'],
cost: 3
})
})

test('Returns a path with cost 0 when start equals target', () => {
expect(aStarSearch(graph, 'A', 'A', heuristic)).toEqual({
path: ['A'],
cost: 0
})
})

test('Returns null when no path exists', () => {
const disconnectedGraph = { A: [['B', 1]], B: [], C: [] }
expect(aStarSearch(disconnectedGraph, 'A', 'C', zeroHeuristic)).toBeNull()
})

test('Finds the shortest path in a larger graph with multiple routes', () => {
const largerGraph = {
A: [
['B', 2],
['C', 5]
],
B: [
['D', 4],
['E', 2]
],
C: [['E', 1]],
D: [['F', 1]],
E: [['F', 4]],
F: []
}
const largerHeuristic = () => 0

expect(aStarSearch(largerGraph, 'A', 'F', largerHeuristic)).toEqual({
path: ['A', 'B', 'D', 'F'],
cost: 7
})
})
Loading