Skip to content

feat: added Astar Search Algorithm in Graphs #1739

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Fixed lint issues
  • Loading branch information
mathangpeddi committed Oct 20, 2024
commit 45b823136c18d2e9b14834e8e825226ff45d816c
324 changes: 189 additions & 135 deletions Graphs/Astar.js
Original file line number Diff line number Diff line change
@@ -1,154 +1,208 @@
/**
* @author Mathang Peddi
* @author : Mathang Peddi
* A* Algorithm calculates the minimum cost path between two nodes.
* It is used to find the shortest path using heuristics.
* It uses graph data structure.
*/

// Euclidean distance heuristic for 2D points
function euclideanHeuristic(pointA, pointB) {
const dx = pointA[0] - pointB[0];
const dy = pointA[1] - pointB[1];
return Math.sqrt(dx * dx + dy * dy);
const dx = pointA[0] - pointB[0]
const dy = pointA[1] - pointB[1]
return Math.sqrt(dx * dx + dy * dy)
}

// Priority Queue (Min-Heap) implementation
class PriorityQueue {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please import this (we have a priority queue implementation in this repo) rather than reimplementing it.

constructor() {
this.elements = []
}

// Priority Queue (Min-Heap) implementation
class PriorityQueue {
constructor() {
this.elements = [];
}

enqueue(node, priority) {
this.elements.push({ node, priority });
this.bubbleUp();

enqueue(node, priority) {
this.elements.push({ node, priority })
this.bubbleUp()
}

bubbleUp() {
let index = this.elements.length - 1
while (index > 0) {
let parentIndex = Math.floor((index - 1) / 2)
if (this.elements[index].priority >= this.elements[parentIndex].priority)
break
;[this.elements[index], this.elements[parentIndex]] = [
this.elements[parentIndex],
this.elements[index]
]
index = parentIndex
}

bubbleUp() {
let index = this.elements.length - 1;
while (index > 0) {
let parentIndex = Math.floor((index - 1) / 2);
if (this.elements[index].priority >= this.elements[parentIndex].priority) break;
[this.elements[index], this.elements[parentIndex]] = [this.elements[parentIndex], this.elements[index]];
index = parentIndex;
}
}

dequeue() {
if (this.elements.length === 1) {
return this.elements.pop().node
}

dequeue() {
if (this.elements.length === 1) {
return this.elements.pop().node;

const node = this.elements[0].node
this.elements[0] = this.elements.pop()
this.sinkDown(0)
return node
}

sinkDown(index) {
const length = this.elements.length
const element = this.elements[index]
while (true) {
let leftChildIndex = 2 * index + 1
let rightChildIndex = 2 * index + 2
let swapIndex = null

if (
leftChildIndex < length &&
this.elements[leftChildIndex].priority < element.priority
) {
swapIndex = leftChildIndex
}

const node = this.elements[0].node;
this.elements[0] = this.elements.pop();
this.sinkDown(0);
return node;
}

sinkDown(index) {
const length = this.elements.length;
const element = this.elements[index];
while (true) {
let leftChildIndex = 2 * index + 1;
let rightChildIndex = 2 * index + 2;
let swapIndex = null;

if (leftChildIndex < length && this.elements[leftChildIndex].priority < element.priority) {
swapIndex = leftChildIndex;
}

if (rightChildIndex < length && this.elements[rightChildIndex].priority < (swapIndex === null ? element.priority : this.elements[leftChildIndex].priority)) {
swapIndex = rightChildIndex;
}

if (swapIndex === null) break;

[this.elements[index], this.elements[swapIndex]] = [this.elements[swapIndex], this.elements[index]];
index = swapIndex;

if (
rightChildIndex < length &&
this.elements[rightChildIndex].priority <
(swapIndex === null
? element.priority
: this.elements[leftChildIndex].priority)
) {
swapIndex = rightChildIndex
}

if (swapIndex === null) break

;[this.elements[index], this.elements[swapIndex]] = [
this.elements[swapIndex],
this.elements[index]
]
index = swapIndex
}

isEmpty() {
return this.elements.length === 0;
}
}

function aStar(graph, src, target, points) {
const openSet = new PriorityQueue(); // Priority queue to explore nodes
openSet.enqueue(src, 0);

const cameFrom = Array(graph.length).fill(null); // Keep track of path
const gScore = Array(graph.length).fill(Infinity); // Actual cost from start to a node
gScore[src] = 0;

const fScore = Array(graph.length).fill(Infinity); // Estimated cost from start to goal (g + h)
fScore[src] = euclideanHeuristic(points[src], points[target]);

while (!openSet.isEmpty()) {
// Get the node in openSet with the lowest fScore
const current = openSet.dequeue();

// If the current node is the target, reconstruct the path and return
if (current === target) {
const path = [];
while (cameFrom[current] !== -1) {
path.push(current);
current = cameFrom[current];
}
path.push(src);
return path.reverse();

isEmpty() {
return this.elements.length === 0
}
}

function aStar(graph, src, target, points) {
Copy link
Collaborator

@appgurueu appgurueu Oct 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where are these parameters documented? Also why is the heuristic function not a parameter (which may default to a simple euclidean heuristic)?

const openSet = new PriorityQueue() // Priority queue to explore nodes
openSet.enqueue(src, 0)

const cameFrom = Array(graph.length).fill(null) // Keep track of path
const gScore = Array(graph.length).fill(Infinity) // Actual cost from start to a node
gScore[src] = 0

const fScore = Array(graph.length).fill(Infinity) // Estimated cost from start to goal (g + h)
fScore[src] = euclideanHeuristic(points[src], points[target])

while (!openSet.isEmpty()) {
// Get the node in openSet with the lowest fScore
const current = openSet.dequeue()

// If the current node is the target, reconstruct the path and return
if (current === target) {
const path = []
while (cameFrom[current] !== -1) {
path.push(current)
current = cameFrom[current]
}

// Explore neighbors using destructuring for cleaner code
for (const [neighbor, weight] of graph[current]) {
const tentative_gScore = gScore[current] + weight;

if (tentative_gScore < gScore[neighbor]) {
cameFrom[neighbor] = current;
gScore[neighbor] = tentative_gScore;
const priority = gScore[neighbor] + euclideanHeuristic(points[neighbor], points[target]);
fScore[neighbor] = priority;

openSet.enqueue(neighbor, priority);
}
path.push(src)
return path.reverse()
}

// Explore neighbors using destructuring for cleaner code
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the "using destructuring for cleaner code part" is obvious

for (const [neighbor, weight] of graph[current]) {
const tentative_gScore = gScore[current] + weight

if (tentative_gScore < gScore[neighbor]) {
cameFrom[neighbor] = current
gScore[neighbor] = tentative_gScore
const priority =
gScore[neighbor] +
euclideanHeuristic(points[neighbor], points[target])
fScore[neighbor] = priority

openSet.enqueue(neighbor, priority)
}
}

return null; // Return null if there's no path to the target
}

// Define the graph as an adjacency list
const graph = [
[[1, 4], [7, 8]], // Node 0 connects to node 1 (weight 4), node 7 (weight 8)
[[0, 4], [2, 8], [7, 11]], // Node 1 connects to node 0, node 2, node 7
[[1, 8], [3, 7], [5, 4], [8, 2]], // Node 2 connects to several nodes
[[2, 7], [4, 9], [5, 14]], // Node 3 connects to nodes 2, 4, 5
[[3, 9], [5, 10]], // Node 4 connects to nodes 3 and 5
[[2, 4], [3, 14], [4, 10], [6, 2]], // Node 5 connects to several nodes
[[5, 2], [7, 1], [8, 6]], // Node 6 connects to nodes 5, 7, 8
[[0, 8], [1, 11], [6, 1], [8, 7]], // Node 7 connects to several nodes
[[2, 2], [6, 6], [7, 7]] // Node 8 connects to nodes 2, 6, 7
];

// Define 2D coordinates for each node (these can be changed based on actual node positions)
const points = [
[0, 0], // Point for node 0
[1, 2], // Point for node 1
[2, 1], // Point for node 2
[3, 5], // Point for node 3
[4, 3], // Point for node 4
[5, 6], // Point for node 5
[6, 8], // Point for node 6
[7, 10], // Point for node 7
[8, 12] // Point for node 8
];

// Call the aStar function with the graph, source node (0), and target node (4)
const path = aStar(graph, 0, 4, points);

console.log('Shortest path from node 0 to node 4:', path);

/**
* The function returns the optimal path from the source to the target node.
* The heuristic used is Euclidean distance between nodes' 2D coordinates.
*/


return null // Return null if there's no path to the target
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a proper test case.

// Define the graph as an adjacency list
const graph = [
[
[1, 4],
[7, 8]
], // Node 0 connects to node 1 (weight 4), node 7 (weight 8)
[
[0, 4],
[2, 8],
[7, 11]
], // Node 1 connects to node 0, node 2, node 7
[
[1, 8],
[3, 7],
[5, 4],
[8, 2]
], // Node 2 connects to several nodes
[
[2, 7],
[4, 9],
[5, 14]
], // Node 3 connects to nodes 2, 4, 5
[
[3, 9],
[5, 10]
], // Node 4 connects to nodes 3 and 5
[
[2, 4],
[3, 14],
[4, 10],
[6, 2]
], // Node 5 connects to several nodes
[
[5, 2],
[7, 1],
[8, 6]
], // Node 6 connects to nodes 5, 7, 8
[
[0, 8],
[1, 11],
[6, 1],
[8, 7]
], // Node 7 connects to several nodes
[
[2, 2],
[6, 6],
[7, 7]
] // Node 8 connects to nodes 2, 6, 7
]

// Define 2D coordinates for each node (these can be changed based on actual node positions)
const points = [
[0, 0], // Point for node 0
[1, 2], // Point for node 1
[2, 1], // Point for node 2
[3, 5], // Point for node 3
[4, 3], // Point for node 4
[5, 6], // Point for node 5
[6, 8], // Point for node 6
[7, 10], // Point for node 7
[8, 12] // Point for node 8
]

// Call the aStar function with the graph, source node (0), and target node (4)
const path = aStar(graph, 0, 4, points)

console.log('Shortest path from node 0 to node 4:', path)

/**
* The function returns the optimal path from the source to the target node.
* The heuristic used is Euclidean distance between nodes' 2D coordinates.
*/
Loading
Oops, something went wrong.