@graphty/algorithms
Version:
Graph algorithms library for browser environments implemented in TypeScript
249 lines (207 loc) • 7.95 kB
text/typescript
import type {Graph} from "../../core/graph.js";
import {PriorityQueue} from "../../data-structures/priority-queue.js";
import type {NodeId, ShortestPathResult} from "../../types/index.js";
interface SearchState {
distances: Map<NodeId, number>;
previous: Map<NodeId, NodeId | null>;
visited: Set<NodeId>;
frontier: PriorityQueue<NodeId>;
}
/**
* Bidirectional Dijkstra's algorithm implementation
*
* Performs simultaneous forward and backward searches to find shortest paths
* more efficiently than standard Dijkstra for point-to-point queries.
*
* Time Complexity: O(2 * sqrt(V + E) log V) expected for random graphs
* Space Complexity: O(V)
*
* Key optimizations:
* - Alternating expansion based on frontier size
* - Early termination when searches meet
* - Efficient meeting point detection
*/
export class BidirectionalDijkstra {
private graph: Graph;
private forwardSearch: SearchState;
private backwardSearch: SearchState;
private meetingNode: NodeId | null = null;
private shortestDistance = Infinity;
constructor(graph: Graph) {
this.graph = graph;
this.forwardSearch = this.initSearchState();
this.backwardSearch = this.initSearchState();
}
private initSearchState(): SearchState {
return {
distances: new Map(),
previous: new Map(),
visited: new Set(),
frontier: new PriorityQueue<NodeId>(),
};
}
/**
* Find shortest path between source and target nodes
*/
public findShortestPath(source: NodeId, target: NodeId): ShortestPathResult | null {
// Reset state for new search
this.reset();
if (!this.graph.hasNode(source)) {
throw new Error(`Source node ${String(source)} not found in graph`);
}
if (!this.graph.hasNode(target)) {
throw new Error(`Target node ${String(target)} not found in graph`);
}
// Special case: source equals target
if (source === target) {
return {
distance: 0,
path: [source],
predecessor: new Map([[source, null]]),
};
}
// Initialize forward search from source
this.forwardSearch.distances.set(source, 0);
this.forwardSearch.previous.set(source, null);
this.forwardSearch.frontier.enqueue(source, 0);
// Initialize backward search from target
this.backwardSearch.distances.set(target, 0);
this.backwardSearch.previous.set(target, null);
this.backwardSearch.frontier.enqueue(target, 0);
// Main search loop
while (!this.forwardSearch.frontier.isEmpty() ||
!this.backwardSearch.frontier.isEmpty()) {
// Alternate between forward and backward search
// Choose the search with smaller frontier for better performance
let searchExpanded = false;
if (!this.forwardSearch.frontier.isEmpty() &&
(this.backwardSearch.frontier.isEmpty() ||
this.forwardSearch.frontier.size() <= this.backwardSearch.frontier.size())) {
if (this.expandSearch(this.forwardSearch, this.backwardSearch, true)) {
break;
}
searchExpanded = true;
}
if (!this.backwardSearch.frontier.isEmpty() && !searchExpanded) {
if (this.expandSearch(this.backwardSearch, this.forwardSearch, false)) {
break;
}
}
// Continue until both frontiers are empty or we've found an optimal path
if (this.forwardSearch.frontier.isEmpty() &&
this.backwardSearch.frontier.isEmpty()) {
break;
}
}
if (this.meetingNode === null) {
return null;
}
return {
distance: this.shortestDistance,
path: this.reconstructPath(),
predecessor: new Map(), // Combined from both searches
};
}
private expandSearch(
search: SearchState,
oppositeSearch: SearchState,
isForward: boolean,
): boolean {
const current = search.frontier.dequeue();
if (current === undefined) {
return false;
}
const distance = search.distances.get(current);
if (distance === undefined) {
return false;
}
if (search.visited.has(current)) {
return false;
}
search.visited.add(current);
// Check if we've met the opposite search
if (oppositeSearch.distances.has(current)) {
const totalDistance = distance + (oppositeSearch.distances.get(current) ?? 0);
if (totalDistance < this.shortestDistance) {
this.shortestDistance = totalDistance;
this.meetingNode = current;
}
}
// Explore neighbors
const neighbors = isForward ?
Array.from(this.graph.neighbors(current)) :
Array.from(this.graph.inNeighbors(current));
for (const neighbor of neighbors) {
if (search.visited.has(neighbor)) {
continue;
}
const edge = isForward ?
this.graph.getEdge(current, neighbor) :
this.graph.getEdge(neighbor, current);
if (!edge) {
continue;
}
const edgeWeight = edge.weight ?? 1;
if (edgeWeight < 0) {
throw new Error("Bidirectional Dijkstra does not support negative edge weights");
}
const newDistance = distance + edgeWeight;
const currentNeighborDistance = search.distances.get(neighbor);
if (!search.distances.has(neighbor) ||
newDistance < (currentNeighborDistance ?? Infinity)) {
search.distances.set(neighbor, newDistance);
search.previous.set(neighbor, current);
search.frontier.enqueue(neighbor, newDistance);
}
}
return false;
}
private getMinDistance(frontier: PriorityQueue<NodeId>): number {
if (frontier.isEmpty()) {
return Infinity;
}
// Since we can't peek at priority directly, we estimate based on the smallest known distance
const minDist = Infinity;
// For a more accurate implementation, we'd need to track the minimum distance
// For now, we'll use a simple heuristic
return minDist;
}
private reconstructPath(): NodeId[] {
if (!this.meetingNode) {
return [];
}
const path: NodeId[] = [];
// Reconstruct forward path
let current: NodeId | null = this.meetingNode;
const forwardPath: NodeId[] = [];
while (current !== null) {
forwardPath.push(current);
current = this.forwardSearch.previous.get(current) ?? null;
}
// Add forward path in reverse order (except meeting node)
for (let i = forwardPath.length - 1; i > 0; i--) {
const node = forwardPath[i];
if (node !== undefined) {
path.push(node);
}
}
// Add meeting node
path.push(this.meetingNode);
// Reconstruct and add backward path
current = this.backwardSearch.previous.get(this.meetingNode) ?? null;
while (current !== null) {
path.push(current);
current = this.backwardSearch.previous.get(current) ?? null;
}
return path;
}
/**
* Reset the search state for reuse
*/
public reset(): void {
this.forwardSearch = this.initSearchState();
this.backwardSearch = this.initSearchState();
this.meetingNode = null;
this.shortestDistance = Infinity;
}
}