@graphty/algorithms
Version:
Graph algorithms library for browser environments implemented in TypeScript
234 lines • 8.03 kB
JavaScript
import { UnionFind } from "../../data-structures/union-find.js";
/**
* Connected components algorithms
*
* Finds connected components in undirected graphs and strongly connected
* components in directed graphs using various efficient algorithms.
*/
/**
* Find connected components in an undirected graph using Union-Find
*/
export function connectedComponents(graph) {
if (graph.isDirected) {
throw new Error("Connected components algorithm requires an undirected graph. Use stronglyConnectedComponents for directed graphs.");
}
const nodes = Array.from(graph.nodes()).map((node) => node.id);
if (nodes.length === 0) {
return [];
}
const unionFind = new UnionFind(nodes);
// Union connected nodes
for (const edge of Array.from(graph.edges())) {
unionFind.union(edge.source, edge.target);
}
return unionFind.getAllComponents();
}
/**
* Find connected components using DFS (alternative implementation)
*/
export function connectedComponentsDFS(graph) {
if (graph.isDirected) {
throw new Error("Connected components algorithm requires an undirected graph");
}
const visited = new Set();
const components = [];
for (const node of Array.from(graph.nodes())) {
if (!visited.has(node.id)) {
const component = [];
dfsComponent(graph, node.id, visited, component);
components.push(component);
}
}
return components;
}
/**
* DFS helper for connected components
*/
function dfsComponent(graph, nodeId, visited, component) {
visited.add(nodeId);
component.push(nodeId);
for (const neighbor of Array.from(graph.neighbors(nodeId))) {
if (!visited.has(neighbor)) {
dfsComponent(graph, neighbor, visited, component);
}
}
}
/**
* Find the number of connected components
*/
export function numberOfConnectedComponents(graph) {
return connectedComponents(graph).length;
}
/**
* Check if the graph is connected (has exactly one connected component)
*/
export function isConnected(graph) {
return numberOfConnectedComponents(graph) <= 1;
}
/**
* Find the largest connected component
*/
export function largestConnectedComponent(graph) {
const components = connectedComponents(graph);
if (components.length === 0) {
return [];
}
return components.reduce((largest, current) => current.length > largest.length ? current : largest);
}
/**
* Get the connected component containing a specific node
*/
export function getConnectedComponent(graph, nodeId) {
if (!graph.hasNode(nodeId)) {
throw new Error(`Node ${String(nodeId)} not found in graph`);
}
if (graph.isDirected) {
throw new Error("Connected components algorithm requires an undirected graph");
}
const visited = new Set();
const component = [];
dfsComponent(graph, nodeId, visited, component);
return component;
}
/**
* Find strongly connected components using Tarjan's algorithm
*/
export function stronglyConnectedComponents(graph) {
if (!graph.isDirected) {
throw new Error("Strongly connected components require a directed graph");
}
const nodes = Array.from(graph.nodes()).map((node) => node.id);
const components = [];
const indices = new Map();
const lowLinks = new Map();
const onStack = new Set();
const stack = [];
let index = 0;
function tarjanSCC(nodeId) {
// Set the depth index for this node
indices.set(nodeId, index);
lowLinks.set(nodeId, index);
index++;
stack.push(nodeId);
onStack.add(nodeId);
// Consider successors
for (const neighbor of Array.from(graph.neighbors(nodeId))) {
if (!indices.has(neighbor)) {
// Successor has not yet been visited; recurse on it
tarjanSCC(neighbor);
const nodeLL = lowLinks.get(nodeId) ?? 0;
const neighborLL = lowLinks.get(neighbor) ?? 0;
lowLinks.set(nodeId, Math.min(nodeLL, neighborLL));
}
else if (onStack.has(neighbor)) {
// Successor is in stack and hence in the current SCC
const nodeLL = lowLinks.get(nodeId) ?? 0;
const neighborIndex = indices.get(neighbor) ?? 0;
lowLinks.set(nodeId, Math.min(nodeLL, neighborIndex));
}
}
// If nodeId is a root node, pop the stack and create an SCC
const nodeIndex = indices.get(nodeId) ?? 0;
const nodeLowLink = lowLinks.get(nodeId) ?? 0;
if (nodeLowLink === nodeIndex) {
const component = [];
let w;
do {
const popped = stack.pop();
if (popped === undefined) {
break;
}
w = popped;
onStack.delete(w);
component.push(w);
} while (w !== nodeId);
components.push(component);
}
}
for (const nodeId of nodes) {
if (!indices.has(nodeId)) {
tarjanSCC(nodeId);
}
}
return components;
}
/**
* Check if a directed graph is strongly connected
*/
export function isStronglyConnected(graph) {
if (!graph.isDirected) {
throw new Error("Strong connectivity check requires a directed graph");
}
const components = stronglyConnectedComponents(graph);
return components.length <= 1;
}
/**
* Find weakly connected components in a directed graph
* (treat the graph as undirected for connectivity)
*/
export function weaklyConnectedComponents(graph) {
if (!graph.isDirected) {
throw new Error("Weakly connected components are for directed graphs. Use connectedComponents for undirected graphs.");
}
const nodes = Array.from(graph.nodes()).map((node) => node.id);
if (nodes.length === 0) {
return [];
}
const unionFind = new UnionFind(nodes);
// Union nodes connected by edges (ignore direction)
for (const edge of Array.from(graph.edges())) {
unionFind.union(edge.source, edge.target);
}
return unionFind.getAllComponents();
}
/**
* Check if a directed graph is weakly connected
*/
export function isWeaklyConnected(graph) {
if (!graph.isDirected) {
throw new Error("Weak connectivity check requires a directed graph");
}
return weaklyConnectedComponents(graph).length <= 1;
}
/**
* Find condensation graph (quotient graph of strongly connected components)
*/
export function condensationGraph(graph) {
if (!graph.isDirected) {
throw new Error("Condensation graph requires a directed graph");
}
const components = stronglyConnectedComponents(graph);
const componentMap = new Map();
const condensedGraph = new graph.constructor({ directed: true });
// Map each node to its component index
for (let i = 0; i < components.length; i++) {
const component = components[i];
if (component) {
for (const nodeId of component) {
componentMap.set(nodeId, i);
}
// Add component as a node in condensed graph
condensedGraph.addNode(i);
}
}
// Add edges between components
const addedEdges = new Set();
for (const edge of Array.from(graph.edges())) {
const sourceComponent = componentMap.get(edge.source);
const targetComponent = componentMap.get(edge.target);
if (sourceComponent !== undefined && targetComponent !== undefined &&
sourceComponent !== targetComponent) {
const edgeKey = `${String(sourceComponent)}-${String(targetComponent)}`;
if (!addedEdges.has(edgeKey)) {
condensedGraph.addEdge(sourceComponent, targetComponent);
addedEdges.add(edgeKey);
}
}
}
return {
condensedGraph,
componentMap,
components,
};
}
//# sourceMappingURL=connected.js.map