@graphty/algorithms
Version:
Graph algorithms library for browser environments implemented in TypeScript
368 lines • 12.9 kB
JavaScript
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