UNPKG

@graphty/algorithms

Version:

Graph algorithms library for browser environments implemented in TypeScript

368 lines 12.9 kB
import { PriorityQueue } from "../../data-structures/priority-queue.js"; import { DirectionOptimizedBFS } from "../../optimized/direction-optimized-bfs.js"; import { toCSRGraph } from "../../optimized/graph-adapter.js"; /** * Threshold for switching to CSR-optimized implementations. * Based on benchmarks showing CSR benefits at this scale due to * improved cache locality and reduced memory allocation overhead. */ const LARGE_GRAPH_THRESHOLD = 10000; /** * BFS that tracks shortest path counts (for betweenness centrality) * * This variant of BFS computes: * - Shortest distances from source to all reachable nodes * - All predecessors on shortest paths * - Number of shortest paths (sigma) * - Stack of nodes in reverse BFS order */ export function bfsWithPathCounting(graph, source, options = {}) { const useOptimized = options.optimized ?? false; // Use CSR format for large graphs if (useOptimized && graph.nodeCount > LARGE_GRAPH_THRESHOLD) { const csrGraph = toCSRGraph(graph); return bfsWithPathCountingCSR(csrGraph, source); } const distances = new Map(); const predecessors = new Map(); const sigma = new Map(); const stack = []; const queue = []; // Initialize distances.set(source, 0); sigma.set(source, 1); queue.push(source); while (queue.length > 0) { const current = queue.shift(); if (current === undefined) { continue; } stack.push(current); const currentDistance = distances.get(current) ?? 0; const currentSigma = sigma.get(current) ?? 0; // Process neighbors const neighbors = graph.neighbors(current); for (const neighbor of neighbors) { // First time visiting this neighbor if (!distances.has(neighbor)) { distances.set(neighbor, currentDistance + 1); queue.push(neighbor); } // Found a shortest path to neighbor if (distances.get(neighbor) === currentDistance + 1) { const neighborSigma = sigma.get(neighbor) ?? 0; sigma.set(neighbor, neighborSigma + currentSigma); const preds = predecessors.get(neighbor) ?? []; preds.push(current); predecessors.set(neighbor, preds); } } } return { distances, predecessors, sigma, stack, }; } /** * BFS that only returns distances (for closeness centrality) * * Optimized variant that skips predecessor tracking */ export function bfsDistancesOnly(graph, source, cutoff, options = {}) { const useOptimized = options.optimized ?? false; // Use CSR format for large graphs if (useOptimized && graph.nodeCount > LARGE_GRAPH_THRESHOLD) { const csrGraph = toCSRGraph(graph); return bfsDistancesOnlyCSR(csrGraph, source, cutoff); } const distances = new Map(); const queue = []; distances.set(source, 0); queue.push(source); while (queue.length > 0) { const current = queue.shift(); if (current === undefined) { continue; } const currentDistance = distances.get(current) ?? 0; // Stop if we've reached the cutoff distance if (cutoff !== undefined && currentDistance >= cutoff) { continue; } const neighbors = graph.neighbors(current); for (const neighbor of neighbors) { if (!distances.has(neighbor)) { distances.set(neighbor, currentDistance + 1); queue.push(neighbor); } } } return distances; } /** * BFS for bipartite checking with partition sets * * Returns whether the graph is bipartite and the two partitions if it is */ export function bfsColoringWithPartitions(graph) { const colors = new Map(); const partitionA = new Set(); const partitionB = new Set(); // Handle disconnected components for (const node of graph.nodes()) { if (!colors.has(node.id)) { const queue = [node.id]; colors.set(node.id, 0); partitionA.add(node.id); while (queue.length > 0) { const current = queue.shift(); if (current === undefined) { continue; } const currentColor = colors.get(current) ?? 0; const nextColor = 1 - currentColor; for (const neighbor of graph.neighbors(current)) { if (!colors.has(neighbor)) { colors.set(neighbor, nextColor); queue.push(neighbor); if (nextColor === 0) { partitionA.add(neighbor); } else { partitionB.add(neighbor); } } else if (colors.get(neighbor) === currentColor) { // Same color as current node - not bipartite return { isBipartite: false }; } } } } } return { isBipartite: true, partitions: [partitionA, partitionB], }; } /** * BFS for finding augmenting paths (for flow algorithms) * * Finds a path from source to sink in a residual graph with positive capacity */ export function bfsAugmentingPath(residualGraph, source, sink) { const parent = new Map(); const queue = []; parent.set(source, null); queue.push(source); while (queue.length > 0) { const current = queue.shift(); if (current === undefined) { continue; } if (current === sink) { // Reconstruct path const path = []; let node = sink; let pathCapacity = Infinity; while (node !== null) { path.unshift(node); const parentNode = parent.get(node); if (parentNode !== null && parentNode !== undefined) { const capacity = residualGraph.get(parentNode)?.get(node) ?? 0; pathCapacity = Math.min(pathCapacity, capacity); } node = parentNode ?? null; } return { path, pathCapacity }; } const neighbors = residualGraph.get(current); if (neighbors) { for (const [neighbor, capacity] of neighbors) { if (!parent.has(neighbor) && capacity > 0) { parent.set(neighbor, current); queue.push(neighbor); } } } } return null; } /** * BFS for weighted graphs using priority queue (simplified Dijkstra) * * Returns distances from source using edge weights */ export function bfsWeightedDistances(graph, source, cutoff, options = {}) { const useOptimized = options.optimized ?? false; // Use CSR format for large graphs if (useOptimized && graph.nodeCount > LARGE_GRAPH_THRESHOLD) { const csrGraph = toCSRGraph(graph); return bfsWeightedDistancesCSR(csrGraph, source, cutoff); } const distances = new Map(); const visited = new Set(); // Use efficient priority queue instead of array-based queue const queue = new PriorityQueue(); distances.set(source, 0); queue.enqueue(source, 0); while (!queue.isEmpty()) { const current = queue.dequeue(); if (current === undefined) { continue; } const currentDist = distances.get(current) ?? 0; if (visited.has(current)) { continue; } visited.add(current); // Stop if we've reached the cutoff distance if (cutoff !== undefined && currentDist >= cutoff) { continue; } for (const neighbor of graph.neighbors(current)) { if (!visited.has(neighbor)) { const edge = graph.getEdge(current, neighbor); const weight = edge?.weight ?? 1; const newDistance = currentDist + weight; const oldDistance = distances.get(neighbor); if (oldDistance === undefined || newDistance < oldDistance) { distances.set(neighbor, newDistance); queue.enqueue(neighbor, newDistance); } } } } return distances; } /** * CSR-optimized version of bfsWithPathCounting */ function bfsWithPathCountingCSR(graph, source) { const distances = new Map(); const predecessors = new Map(); const sigma = new Map(); const stack = []; const queue = []; // Initialize distances.set(source, 0); sigma.set(source, 1); queue.push(source); while (queue.length > 0) { const current = queue.shift(); if (current === undefined) { continue; } stack.push(current); const currentDistance = distances.get(current) ?? 0; const currentSigma = sigma.get(current) ?? 0; // Process neighbors using CSR iteration for (const neighbor of graph.neighbors(current)) { // First time visiting this neighbor if (!distances.has(neighbor)) { distances.set(neighbor, currentDistance + 1); queue.push(neighbor); } // Found a shortest path to neighbor if (distances.get(neighbor) === currentDistance + 1) { const neighborSigma = sigma.get(neighbor) ?? 0; sigma.set(neighbor, neighborSigma + currentSigma); const preds = predecessors.get(neighbor) ?? []; preds.push(current); predecessors.set(neighbor, preds); } } } return { distances, predecessors, sigma, stack, }; } /** * CSR-optimized version of bfsDistancesOnly */ function bfsDistancesOnlyCSR(graph, source, cutoff) { // Use Direction-Optimized BFS for best performance if (graph.nodeCount() > LARGE_GRAPH_THRESHOLD) { const dobfs = new DirectionOptimizedBFS(graph); const result = dobfs.search(source); // Filter by cutoff if specified if (cutoff !== undefined) { const filtered = new Map(); for (const [nodeId, distance] of result.distances) { if (distance <= cutoff) { filtered.set(nodeId, distance); } } return filtered; } return result.distances; } // Fallback to standard BFS on CSR const distances = new Map(); const queue = []; distances.set(source, 0); queue.push(source); while (queue.length > 0) { const current = queue.shift(); if (current === undefined) { continue; } const currentDistance = distances.get(current) ?? 0; // Stop if we've reached the cutoff distance if (cutoff !== undefined && currentDistance >= cutoff) { continue; } for (const neighbor of graph.neighbors(current)) { if (!distances.has(neighbor)) { distances.set(neighbor, currentDistance + 1); queue.push(neighbor); } } } return distances; } /** * CSR-optimized version of bfsWeightedDistances */ function bfsWeightedDistancesCSR(graph, source, cutoff) { const distances = new Map(); const visited = new Set(); // Use efficient priority queue const queue = new PriorityQueue(); distances.set(source, 0); queue.enqueue(source, 0); while (!queue.isEmpty()) { const current = queue.dequeue(); if (current === undefined) { continue; } const currentDist = distances.get(current) ?? 0; if (visited.has(current)) { continue; } visited.add(current); // Stop if we've reached the cutoff distance if (cutoff !== undefined && currentDist >= cutoff) { continue; } for (const neighbor of graph.neighbors(current)) { if (!visited.has(neighbor)) { // Use actual edge weight from CSR graph, default to 1 const weight = graph.getEdgeWeight(current, neighbor) ?? 1; const newDistance = currentDist + weight; const oldDistance = distances.get(neighbor); if (oldDistance === undefined || newDistance < oldDistance) { distances.set(neighbor, newDistance); queue.enqueue(neighbor, newDistance); } } } } return distances; } //# sourceMappingURL=bfs-variants.js.map