UNPKG

@graphty/algorithms

Version:

Graph algorithms library for browser environments implemented in TypeScript

292 lines (250 loc) 9.34 kB
import type {NodeId} from "../types/index.js"; import {CompactDistanceArray, GraphBitSet} from "./bit-packed.js"; import {CSRGraph} from "./csr-graph.js"; /** * Options for Direction-Optimized BFS */ export interface DirectionOptimizedBFSOptions { alpha?: number; // Default: 15 - Controls top-down to bottom-up switch beta?: number; // Default: 18 - Controls bottom-up to top-down switch } /** * Result of BFS traversal */ export interface BFSResult<TNodeId> { distances: Map<TNodeId, number>; parents: Map<TNodeId, TNodeId | null>; visitedCount: number; } /** * Direction-Optimized Breadth-First Search * * Dynamically switches between top-down and bottom-up search strategies * based on the size of the frontier. Particularly effective for low-diameter * graphs like social networks. * * Based on: Beamer, S., Asanović, K., & Patterson, D. (2012). * "Direction-optimizing breadth-first search." SC'12. */ export class DirectionOptimizedBFS<TNodeId = NodeId> { private graph: CSRGraph<TNodeId>; private alpha: number; private beta: number; // Data structures private parent: Int32Array; // -1 = unvisited, -2 = source, else parent ID private frontier: GraphBitSet; // Current frontier bitmap private nextFrontier: GraphBitSet; // Next level frontier private distances: CompactDistanceArray; constructor(graph: CSRGraph<TNodeId>, options?: DirectionOptimizedBFSOptions) { this.graph = graph; this.alpha = options?.alpha ?? 15; this.beta = options?.beta ?? 18; const nodeCount = graph.nodeCount(); this.parent = new Int32Array(nodeCount).fill(-1); this.frontier = new GraphBitSet(nodeCount); this.nextFrontier = new GraphBitSet(nodeCount); this.distances = new CompactDistanceArray(nodeCount); } /** * Perform BFS from a single source */ search(source: TNodeId): BFSResult<TNodeId> { const sourceIndex = this.graph.nodeToIndex(source); this.parent[sourceIndex] = -2; // Mark as source this.frontier.add(sourceIndex); this.distances.set(sourceIndex, 0); // let currentDistance = 0; // Variable not used // Initialize edgesToCheck to total edges to prevent immediate switch to bottom-up let edgesToCheck = this.graph.edgeCount(); let scoutCount = 0; // Will be set after first top-down step let awakeCount = 1; // Number of nodes in current frontier let oldAwakeCount = 0; // Direction tracking let useBottomUp = false; while (!this.frontier.isEmpty()) { oldAwakeCount = awakeCount; if (useBottomUp) { // Check if we should switch back to top-down if (awakeCount >= oldAwakeCount || awakeCount > this.graph.nodeCount() / this.beta) { useBottomUp = false; } } else { // Check if we should switch to bottom-up if (scoutCount > edgesToCheck / this.alpha) { useBottomUp = true; } } if (useBottomUp) { awakeCount = this.bottomUpStep(); } else { scoutCount = this.topDownStep(); awakeCount = this.nextFrontier.size(); } // Swap frontiers this.frontier.swap(this.nextFrontier); this.nextFrontier.clear(); // currentDistance++; // Variable not used edgesToCheck = this.calculateEdgesToCheck(); } return this.buildResult(); } /** * Top-down BFS step - explore from frontier */ private topDownStep(): number { let scoutCount = 0; // Iterate through frontier nodes for (const node of this.frontier) { const currentDistance = this.distances.get(node); for (const neighbor of this.graph.iterateNeighborIndices(node)) { if (this.parent[neighbor] === -1) { this.parent[neighbor] = node; this.distances.set(neighbor, currentDistance + 1); this.nextFrontier.add(neighbor); scoutCount += this.graph.outDegreeByIndex(neighbor); } } } return scoutCount; } /** * Bottom-up BFS step - check unvisited nodes */ private bottomUpStep(): number { const nodeCount = this.graph.nodeCount(); let awakeCount = 0; // Check all unvisited nodes for (let node = 0; node < nodeCount; node++) { if (this.parent[node] === -1) { // Use incoming edges for bottom-up traversal for (const neighbor of this.graph.iterateIncomingNeighborIndices(node)) { if (this.frontier.has(neighbor)) { this.parent[node] = neighbor; const neighborDist = this.distances.get(neighbor); this.distances.set(node, neighborDist + 1); this.nextFrontier.add(node); awakeCount++; break; // Found a parent, no need to check more } } } } return awakeCount; } /** * Calculate edges to check for switching heuristic */ private calculateEdgesToCheck(): number { let count = 0; for (const node of this.frontier) { count += this.graph.outDegreeByIndex(node); } return count; } /** * Build result map from internal data structures */ private buildResult(): BFSResult<TNodeId> { const distances = new Map<TNodeId, number>(); const parents = new Map<TNodeId, TNodeId | null>(); let visitedCount = 0; for (let i = 0; i < this.graph.nodeCount(); i++) { if (this.parent[i] !== -1) { const nodeId = this.graph.indexToNodeId(i); distances.set(nodeId, this.distances.get(i)); if (this.parent[i] === -2) { // Source node parents.set(nodeId, null); } else { // Regular node with parent const parentIndex = this.parent[i]; if (parentIndex !== undefined && parentIndex >= 0) { parents.set(nodeId, this.graph.indexToNodeId(parentIndex)); } } visitedCount++; } } return {distances, parents, visitedCount}; } /** * Perform multi-source BFS */ searchMultiple(sources: TNodeId[]): BFSResult<TNodeId> { // Initialize multiple sources for (const source of sources) { const sourceIndex = this.graph.nodeToIndex(source); this.parent[sourceIndex] = -2; // Mark as source this.frontier.add(sourceIndex); this.distances.set(sourceIndex, 0); } // let currentDistance = 0; // Variable not used let edgesToCheck = 0; let scoutCount = 0; // Calculate initial edges to check for (const node of this.frontier) { const degree = this.graph.outDegreeByIndex(node); edgesToCheck += degree; scoutCount += degree; } let awakeCount = sources.length; let oldAwakeCount = 0; let useBottomUp = false; while (!this.frontier.isEmpty()) { oldAwakeCount = awakeCount; if (useBottomUp) { if (awakeCount >= oldAwakeCount || awakeCount > this.graph.nodeCount() / this.beta) { useBottomUp = false; } } else { if (scoutCount > edgesToCheck / this.alpha) { useBottomUp = true; } } if (useBottomUp) { awakeCount = this.bottomUpStep(); } else { scoutCount = this.topDownStep(); awakeCount = this.nextFrontier.size(); } this.frontier.swap(this.nextFrontier); this.nextFrontier.clear(); // currentDistance++; // Variable not used edgesToCheck = this.calculateEdgesToCheck(); } return this.buildResult(); } /** * Reset internal state for reuse */ reset(): void { this.parent.fill(-1); this.frontier.clear(); this.nextFrontier.clear(); this.distances.clear(); } } /** * Convenience function for single-source BFS */ export function directionOptimizedBFS<TNodeId = NodeId>( graph: CSRGraph<TNodeId>, source: TNodeId, options?: DirectionOptimizedBFSOptions, ): BFSResult<TNodeId> { const bfs = new DirectionOptimizedBFS(graph, options); return bfs.search(source); } /** * Convenience function for multi-source BFS */ export function directionOptimizedBFSMultiple<TNodeId = NodeId>( graph: CSRGraph<TNodeId>, sources: TNodeId[], options?: DirectionOptimizedBFSOptions, ): BFSResult<TNodeId> { const bfs = new DirectionOptimizedBFS(graph, options); return bfs.searchMultiple(sources); }