UNPKG

@graphty/algorithms

Version:

Graph algorithms library for browser environments implemented in TypeScript

249 lines (207 loc) 7.95 kB
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; } }