Skip to content
Merged
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
212 changes: 212 additions & 0 deletions algorithms/graph/a-star/aStar.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import {aStar, createManhattanHeuristic, createEuclideanHeuristic} from './aStar';

describe('A* Search Algorithm', () => {
test('should find the shortest path in a simple graph', () => {
// Graph: 0 -> 1 -> 2 -> 3
const graph = [[{to: 1, weight: 1}], [{to: 2, weight: 1}], [{to: 3, weight: 1}], []];
const heuristic = (v: number): number => 3 - v; // Simple heuristic

const result = aStar(graph, 0, 3, heuristic);
expect(result.path).toEqual([0, 1, 2, 3]);
expect(result.distance).toBe(3);
});

test('should find optimal path when multiple paths exist', () => {
// Graph with two paths: 0->1->3 (cost 5) and 0->2->3 (cost 3)
const graph = [
[
{to: 1, weight: 2},
{to: 2, weight: 1},
],
[{to: 3, weight: 3}],
[{to: 3, weight: 2}],
[],
];
const heuristic = (): number => 0; // Zero heuristic (degrades to Dijkstra)

const result = aStar(graph, 0, 3, heuristic);
expect(result.path).toEqual([0, 2, 3]);
expect(result.distance).toBe(3);
});

test('should return null path when no path exists', () => {
const graph = [[{to: 1, weight: 1}], [], [{to: 3, weight: 1}], []];
const heuristic = (): number => 0;

const result = aStar(graph, 0, 3, heuristic);
expect(result.path).toBeNull();
expect(result.distance).toBe(Infinity);
});

test('should handle source equals goal', () => {
const graph = [[{to: 1, weight: 1}], []];
const heuristic = (): number => 0;

const result = aStar(graph, 0, 0, heuristic);
expect(result.path).toEqual([0]);
expect(result.distance).toBe(0);
});

test('should handle invalid source vertex', () => {
const graph = [[{to: 1, weight: 1}], []];
const heuristic = (): number => 0;

const result = aStar(graph, -1, 1, heuristic);
expect(result.path).toBeNull();
expect(result.distance).toBe(Infinity);
});

test('should handle invalid goal vertex', () => {
const graph = [[{to: 1, weight: 1}], []];
const heuristic = (): number => 0;

const result = aStar(graph, 0, 5, heuristic);
expect(result.path).toBeNull();
expect(result.distance).toBe(Infinity);
});

test('should work with a grid-based graph using Manhattan heuristic', () => {
// 3x3 grid:
// 0 - 1 - 2
// | | |
// 3 - 4 - 5
// | | |
// 6 - 7 - 8
const graph = [
[
{to: 1, weight: 1},
{to: 3, weight: 1},
],
[
{to: 0, weight: 1},
{to: 2, weight: 1},
{to: 4, weight: 1},
],
[
{to: 1, weight: 1},
{to: 5, weight: 1},
],
[
{to: 0, weight: 1},
{to: 4, weight: 1},
{to: 6, weight: 1},
],
[
{to: 1, weight: 1},
{to: 3, weight: 1},
{to: 5, weight: 1},
{to: 7, weight: 1},
],
[
{to: 2, weight: 1},
{to: 4, weight: 1},
{to: 8, weight: 1},
],
[
{to: 3, weight: 1},
{to: 7, weight: 1},
],
[
{to: 4, weight: 1},
{to: 6, weight: 1},
{to: 8, weight: 1},
],
[
{to: 5, weight: 1},
{to: 7, weight: 1},
],
];

const goalX = 2;
const goalY = 2;
const gridWidth = 3;
const heuristic = createManhattanHeuristic(goalX, goalY, gridWidth);

const result = aStar(graph, 0, 8, heuristic);
expect(result.distance).toBe(4);
expect(result.path?.length).toBe(5);
expect(result.path?.[0]).toBe(0);
expect(result.path?.[result.path.length - 1]).toBe(8);
});

test('should work with Euclidean heuristic', () => {
// Simple 2x2 grid
const graph = [
[
{to: 1, weight: 1},
{to: 2, weight: 1},
],
[
{to: 0, weight: 1},
{to: 3, weight: 1},
],
[
{to: 0, weight: 1},
{to: 3, weight: 1},
],
[
{to: 1, weight: 1},
{to: 2, weight: 1},
],
];

const heuristic = createEuclideanHeuristic(1, 1, 2);
const result = aStar(graph, 0, 3, heuristic);

expect(result.distance).toBe(2);
expect(result.path?.length).toBe(3);
});

test('should handle disconnected components', () => {
const graph = [
[{to: 1, weight: 1}],
[{to: 0, weight: 1}],
[{to: 3, weight: 1}],
[{to: 2, weight: 1}],
];
const heuristic = (): number => 0;

const result = aStar(graph, 0, 3, heuristic);
expect(result.path).toBeNull();
expect(result.distance).toBe(Infinity);
});

test('should handle graph with cycles', () => {
// Triangle graph: 0 - 1 - 2 - 0
const graph = [
[
{to: 1, weight: 1},
{to: 2, weight: 3},
],
[
{to: 0, weight: 1},
{to: 2, weight: 1},
],
[
{to: 0, weight: 3},
{to: 1, weight: 1},
],
];
const heuristic = (): number => 0;

const result = aStar(graph, 0, 2, heuristic);
expect(result.path).toEqual([0, 1, 2]);
expect(result.distance).toBe(2);
});
});

describe('Heuristic functions', () => {
test('Manhattan heuristic should calculate correct distance', () => {
const heuristic = createManhattanHeuristic(2, 2, 3);
expect(heuristic(0)).toBe(4); // (0,0) to (2,2)
expect(heuristic(4)).toBe(2); // (1,1) to (2,2)
expect(heuristic(8)).toBe(0); // (2,2) to (2,2)
});

test('Euclidean heuristic should calculate correct distance', () => {
const heuristic = createEuclideanHeuristic(3, 0, 4);
expect(heuristic(0)).toBe(3); // (0,0) to (3,0)
expect(heuristic(3)).toBe(0); // (3,0) to (3,0)
expect(heuristic(4)).toBeCloseTo(Math.sqrt(10)); // (0,1) to (3,0)
});
});
171 changes: 171 additions & 0 deletions algorithms/graph/a-star/aStar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/**
* A* Search algorithm implementation for finding the shortest path in a weighted graph.
* A* uses a heuristic function to guide the search towards the goal more efficiently than Dijkstra.
*/

interface Edge {
to: number;
weight: number;
}

interface AStarResult {
path: number[] | null;
distance: number;
}

/**
* Implements the A* search algorithm to find the shortest path from source to goal.
*
* @param graph - Adjacency list representation of the graph where graph[i] is an array of edges from vertex i.
* @param source - The source vertex to start the search from.
* @param goal - The goal vertex to reach.
* @param heuristic - A function that estimates the distance from a vertex to the goal. Must be admissible (never overestimate).
* @returns An object containing the path array and the total distance, or null path if no path exists.
*/
export function aStar(
graph: Edge[][],
source: number,
goal: number,
heuristic: (vertex: number) => number,
): AStarResult {
const n = graph.length;

if (source < 0 || source >= n || goal < 0 || goal >= n) {
return {path: null, distance: Infinity};
}

// g[n] = actual cost from source to n
const gScore: number[] = Array(n).fill(Infinity);
gScore[source] = 0;

// f[n] = g[n] + h[n] (estimated total cost)
const fScore: number[] = Array(n).fill(Infinity);
fScore[source] = heuristic(source);

const predecessors: number[] = Array(n).fill(-1);
const closedSet: boolean[] = Array(n).fill(false);
const openSet: boolean[] = Array(n).fill(false);
openSet[source] = true;

while (hasOpenNodes(openSet)) {
// Find node in openSet with lowest fScore
const current = getLowestFScore(openSet, fScore);

if (current === goal) {
return {
path: reconstructPath(source, goal, predecessors),
distance: gScore[goal],
};
}

openSet[current] = false;
closedSet[current] = true;

for (const edge of graph[current]) {
const neighbor = edge.to;

if (closedSet[neighbor]) {
continue;
}

const tentativeGScore = gScore[current] + edge.weight;

if (!openSet[neighbor]) {
openSet[neighbor] = true;
} else if (tentativeGScore >= gScore[neighbor]) {
continue;
}

// This path is the best so far
predecessors[neighbor] = current;
gScore[neighbor] = tentativeGScore;
fScore[neighbor] = gScore[neighbor] + heuristic(neighbor);
}
}

// No path found
return {path: null, distance: Infinity};
}

/**
* Checks if there are any nodes in the open set.
*/
function hasOpenNodes(openSet: boolean[]): boolean {
return openSet.some((isOpen) => isOpen);
}

/**
* Finds the node in the open set with the lowest f score.
*/
function getLowestFScore(openSet: boolean[], fScore: number[]): number {
let minScore = Infinity;
let minIndex = -1;

for (let i = 0; i < openSet.length; i++) {
if (openSet[i] && fScore[i] < minScore) {
minScore = fScore[i];
minIndex = i;
}
}

return minIndex;
}

/**
* Reconstructs the path from source to goal using the predecessors array.
*/
function reconstructPath(source: number, goal: number, predecessors: number[]): number[] {
const path: number[] = [];
let current = goal;

while (current !== -1) {
path.unshift(current);
if (current === source) {
break;
}
current = predecessors[current];
}

return path[0] === source ? path : [];
}

/**
* Creates a Manhattan distance heuristic for grid-based pathfinding.
* Assumes vertices are numbered row by row in a grid.
*
* @param goalX - The x coordinate of the goal.
* @param goalY - The y coordinate of the goal.
* @param gridWidth - The width of the grid.
* @returns A heuristic function that calculates Manhattan distance to the goal.
*/
export function createManhattanHeuristic(
goalX: number,
goalY: number,
gridWidth: number,
): (vertex: number) => number {
return (vertex: number): number => {
const x = vertex % gridWidth;
const y = Math.floor(vertex / gridWidth);
return Math.abs(x - goalX) + Math.abs(y - goalY);
};
}

/**
* Creates a Euclidean distance heuristic for grid-based pathfinding.
*
* @param goalX - The x coordinate of the goal.
* @param goalY - The y coordinate of the goal.
* @param gridWidth - The width of the grid.
* @returns A heuristic function that calculates Euclidean distance to the goal.
*/
export function createEuclideanHeuristic(
goalX: number,
goalY: number,
gridWidth: number,
): (vertex: number) => number {
return (vertex: number): number => {
const x = vertex % gridWidth;
const y = Math.floor(vertex / gridWidth);
return Math.sqrt((x - goalX) ** 2 + (y - goalY) ** 2);
};
}
File renamed without changes.
File renamed without changes.
Loading