@graphty/algorithms
Version:
Graph algorithms library for browser environments implemented in TypeScript
497 lines (415 loc) • 14.3 kB
text/typescript
import type {Graph} from "../../core/graph.js";
import {PriorityQueue} from "../../data-structures/priority-queue.js";
import {CSRGraph} from "../../optimized/csr-graph.js";
import {DirectionOptimizedBFS} from "../../optimized/direction-optimized-bfs.js";
import {toCSRGraph} from "../../optimized/graph-adapter.js";
import type {NodeId} from "../../types/index.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: Graph,
source: NodeId,
options: {optimized?: boolean} = {},
): {
distances: Map<NodeId, number>;
predecessors: Map<NodeId, NodeId[]>;
sigma: Map<NodeId, number>;
stack: NodeId[];
} {
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<NodeId, number>();
const predecessors = new Map<NodeId, NodeId[]>();
const sigma = new Map<NodeId, number>();
const stack: NodeId[] = [];
const queue: NodeId[] = [];
// 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: Graph,
source: NodeId,
cutoff?: number,
options: {optimized?: boolean} = {},
): Map<NodeId, number> {
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<NodeId, number>();
const queue: NodeId[] = [];
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: Graph,
): {
isBipartite: boolean;
partitions?: [Set<NodeId>, Set<NodeId>];
} {
const colors = new Map<NodeId, number>();
const partitionA = new Set<NodeId>();
const partitionB = new Set<NodeId>();
// Handle disconnected components
for (const node of graph.nodes()) {
if (!colors.has(node.id)) {
const queue: NodeId[] = [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: Map<string, Map<string, number>>,
source: string,
sink: string,
): {
path: string[];
pathCapacity: number;
} | null {
const parent = new Map<string, string | null>();
const queue: string[] = [];
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: string[] = [];
let node: string | null = 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: Graph,
source: NodeId,
cutoff?: number,
options: {optimized?: boolean} = {},
): Map<NodeId, number> {
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<NodeId, number>();
const visited = new Set<NodeId>();
// Use efficient priority queue instead of array-based queue
const queue = new PriorityQueue<NodeId>();
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: CSRGraph,
source: NodeId,
): {
distances: Map<NodeId, number>;
predecessors: Map<NodeId, NodeId[]>;
sigma: Map<NodeId, number>;
stack: NodeId[];
} {
const distances = new Map<NodeId, number>();
const predecessors = new Map<NodeId, NodeId[]>();
const sigma = new Map<NodeId, number>();
const stack: NodeId[] = [];
const queue: NodeId[] = [];
// 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: CSRGraph,
source: NodeId,
cutoff?: number,
): Map<NodeId, number> {
// 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<NodeId, number>();
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<NodeId, number>();
const queue: NodeId[] = [];
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: CSRGraph,
source: NodeId,
cutoff?: number,
): Map<NodeId, number> {
const distances = new Map<NodeId, number>();
const visited = new Set<NodeId>();
// Use efficient priority queue
const queue = new PriorityQueue<NodeId>();
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;
}