@graphty/algorithms
Version:
Graph algorithms library for browser environments implemented in TypeScript
493 lines (424 loc) • 13.2 kB
text/typescript
/**
* Minimum Cut Algorithms
*
* Various algorithms for finding minimum cuts in graphs
*/
import type {Graph} from "../core/graph.js";
import {graphToMap} from "../utils/graph-converters.js";
import {fordFulkerson} from "./ford-fulkerson.js";
export interface MinCutResult {
cutValue: number;
partition1: Set<string>;
partition2: Set<string>;
cutEdges: {from: string, to: string, weight: number}[];
}
/**
* Find minimum s-t cut using max flow
* The minimum cut value equals the maximum flow value (max-flow min-cut theorem)
*
* @param graph - Weighted graph
* @param source - Source node
* @param sink - Sink node
* @returns Minimum cut information
*
* Time Complexity: Same as max flow algorithm used
*/
export function minSTCut(
graph: Graph,
source: string,
sink: string,
): MinCutResult {
const flowResult = fordFulkerson(graph, source, sink);
if (!flowResult.minCut) {
return {
cutValue: 0,
partition1: new Set(),
partition2: new Set(),
cutEdges: [],
};
}
const cutEdges: {from: string, to: string, weight: number}[] = [];
// Find actual cut edges and their weights
for (const [u, v] of flowResult.minCut.edges) {
const edge = graph.getEdge(u, v);
const weight = edge?.weight ?? 0;
if (weight > 0) {
cutEdges.push({from: u, to: v, weight});
}
}
return {
cutValue: flowResult.maxFlow,
partition1: flowResult.minCut.source,
partition2: flowResult.minCut.sink,
cutEdges,
};
}
/**
* Stoer-Wagner algorithm for finding global minimum cut
* Finds the minimum cut that separates the graph into two parts
*
* @param graph - Undirected weighted graph - accepts Graph class or Map representation
* @returns Global minimum cut
*
* Time Complexity: O(V³) or O(VE + V² log V) with heap
*/
export function stoerWagner(
graph: Graph | Map<string, Map<string, number>>,
): MinCutResult {
// Convert Graph to Map representation if needed
const graphMap = graph instanceof Map ? graph : graphToMap(graph);
// Convert to undirected if necessary
const undirectedGraph = makeUndirected(graphMap);
// Keep a copy for finding cut edges later
const originalGraph = makeUndirected(graphMap);
if (undirectedGraph.size < 2) {
return {
cutValue: 0,
partition1: new Set(undirectedGraph.keys()),
partition2: new Set(),
cutEdges: [],
};
}
// Initialize
const nodes = Array.from(undirectedGraph.keys());
const originalNodes = new Set(nodes); // Keep track of original nodes
let minCutValue = Infinity;
let bestPartition = new Set<string>();
const contractionMap = new Map<string, Set<string>>();
// Initialize contraction map
for (const node of nodes) {
contractionMap.set(node, new Set([node]));
}
// Contract graph V-1 times
while (nodes.length > 1) {
const cut = minimumCutPhase(undirectedGraph, nodes);
if (cut.value < minCutValue) {
minCutValue = cut.value;
// Store the actual nodes that would be in partition with t
const cutTNodes = contractionMap.get(cut.t);
if (cutTNodes) {
bestPartition = new Set(cutTNodes);
}
}
// Contract the last two nodes
const tNodes = contractionMap.get(cut.t);
const sNodes = contractionMap.get(cut.s);
if (!tNodes || !sNodes) {
continue;
}
for (const node of tNodes) {
sNodes.add(node);
}
contractionMap.delete(cut.t);
contractNodes(undirectedGraph, nodes, cut.s, cut.t);
}
// Build result
const partition1 = bestPartition;
const partition2 = new Set<string>();
for (const node of originalNodes) {
if (!partition1.has(node)) {
partition2.add(node);
}
}
// Find cut edges
const cutEdges: {from: string, to: string, weight: number}[] = [];
for (const u of partition1) {
const neighbors = originalGraph.get(u);
if (neighbors) {
for (const [v, weight] of neighbors) {
if (partition2.has(v)) {
cutEdges.push({from: u, to: v, weight});
}
}
}
}
return {
cutValue: minCutValue,
partition1,
partition2,
cutEdges,
};
}
/**
* Minimum cut phase of Stoer-Wagner algorithm
*/
function minimumCutPhase(
graph: Map<string, Map<string, number>>,
nodes: string[],
): {s: string, t: string, value: number, partition: string[]} {
const n = nodes.length;
const weight = new Map<string, number>();
const added = new Set<string>();
const order: string[] = [];
// Initialize weights to 0
for (const node of nodes) {
weight.set(node, 0);
}
// Start with arbitrary node
let lastAdded = nodes[0];
if (!lastAdded) {
return {s: "", t: "", value: 0, partition: []};
}
added.add(lastAdded);
order.push(lastAdded);
// Add remaining nodes
for (let i = 1; i < n; i++) {
// Update weights
if (!lastAdded) {
continue;
}
const neighbors = graph.get(lastAdded);
if (neighbors) {
for (const [neighbor, w] of neighbors) {
if (!added.has(neighbor) && nodes.includes(neighbor)) {
const currentWeight = weight.get(neighbor);
if (currentWeight !== undefined) {
weight.set(neighbor, currentWeight + w);
}
}
}
}
// Find maximum weight node not yet added
let maxWeight = -1;
let maxNode = "";
for (const node of nodes) {
const nodeWeight = weight.get(node);
if (!added.has(node) && nodeWeight !== undefined && nodeWeight > maxWeight) {
maxWeight = nodeWeight;
maxNode = node;
}
}
added.add(maxNode);
order.push(maxNode);
lastAdded = maxNode;
}
const s = order[order.length - 2];
const t = order[order.length - 1];
if (!s || !t) {
return {s: "", t: "", value: 0, partition: []};
}
const cutValue = weight.get(t) ?? 0;
// Partition is all nodes except t
const partition = order.slice(0, -1);
return {s, t, value: cutValue, partition};
}
/**
* Contract two nodes in the graph
*/
function contractNodes(
graph: Map<string, Map<string, number>>,
nodes: string[],
s: string,
t: string,
): void {
// Merge t into s
const sNeighbors = graph.get(s);
const tNeighbors = graph.get(t);
if (!sNeighbors || !tNeighbors) {
return;
}
// Add t's edges to s
for (const [neighbor, weight] of tNeighbors) {
if (neighbor !== s) {
sNeighbors.set(neighbor, (sNeighbors.get(neighbor) ?? 0) + weight);
// Update neighbor's edge to point to s instead of t
const neighborEdges = graph.get(neighbor);
if (neighborEdges?.has(t)) {
neighborEdges.delete(t);
neighborEdges.set(s, (neighborEdges.get(s) ?? 0) + weight);
}
}
}
// Remove t from graph
graph.delete(t);
sNeighbors.delete(t);
// Remove t from nodes array
const index = nodes.indexOf(t);
if (index > -1) {
nodes.splice(index, 1);
}
}
/**
* Convert directed graph to undirected
*/
function makeUndirected(
graph: Map<string, Map<string, number>>,
): Map<string, Map<string, number>> {
const undirected = new Map<string, Map<string, number>>();
// Initialize all nodes
for (const node of graph.keys()) {
undirected.set(node, new Map());
}
// Add edges in both directions
for (const [u, neighbors] of graph) {
for (const [v, weight] of neighbors) {
const uNeighbors = undirected.get(u);
if (uNeighbors) {
uNeighbors.set(v, weight);
}
if (!undirected.has(v)) {
undirected.set(v, new Map());
}
const vNeighbors = undirected.get(v);
if (vNeighbors) {
vNeighbors.set(u, weight);
}
}
}
return undirected;
}
/**
* Karger's randomized min-cut algorithm
* Probabilistic algorithm that finds min cut with high probability
*
* @param graph - Undirected graph - accepts Graph class or Map representation
* @param iterations - Number of iterations (higher = better accuracy)
* @returns Minimum cut found
*
* Time Complexity: O(V² * iterations)
*/
export function kargerMinCut(
graph: Graph | Map<string, Map<string, number>>,
iterations = 100,
): MinCutResult {
// Convert Graph to Map representation if needed
const graphMap = graph instanceof Map ? graph : graphToMap(graph);
let minCutValue = Infinity;
let bestPartition1 = new Set<string>();
let bestPartition2 = new Set<string>();
for (let i = 0; i < iterations; i++) {
const result = kargerSingleRun(graphMap);
if (result.cutValue < minCutValue) {
minCutValue = result.cutValue;
bestPartition1 = result.partition1;
bestPartition2 = result.partition2;
}
}
// Find cut edges
const cutEdges: {from: string, to: string, weight: number}[] = [];
for (const u of bestPartition1) {
const neighbors = graphMap.get(u);
if (neighbors) {
for (const [v, weight] of neighbors) {
if (bestPartition2.has(v)) {
cutEdges.push({from: u, to: v, weight});
}
}
}
}
return {
cutValue: minCutValue,
partition1: bestPartition1,
partition2: bestPartition2,
cutEdges,
};
}
/**
* Single run of Karger's algorithm
*/
function kargerSingleRun(
graph: Map<string, Map<string, number>>,
): {cutValue: number, partition1: Set<string>, partition2: Set<string>} {
// Create a copy of the graph
const workGraph = new Map<string, Map<string, number>>();
const superNodes = new Map<string, Set<string>>();
// Initialize
for (const [node, neighbors] of graph) {
workGraph.set(node, new Map(neighbors));
superNodes.set(node, new Set([node]));
}
// Contract until 2 nodes remain
while (workGraph.size > 2) {
// Pick random edge
const edges: [string, string, number][] = [];
for (const [u, neighbors] of workGraph) {
for (const [v, weight] of neighbors) {
if (u < v) { // Avoid duplicates
edges.push([u, v, weight]);
}
}
}
if (edges.length === 0) {
break;
}
const randomIndex = Math.floor(Math.random() * edges.length);
const edge = edges[randomIndex];
if (!edge) {
continue;
}
const [u, v] = edge;
// Contract edge
if (u && v) {
contractKarger(workGraph, superNodes, u, v);
}
}
// Calculate cut value
const nodes = Array.from(workGraph.keys());
if (nodes.length < 2) {
return {
cutValue: 0,
partition1: new Set(),
partition2: new Set(),
};
}
const node1 = nodes[0];
const node2 = nodes[1];
if (!node1 || !node2) {
return {
cutValue: 0,
partition1: new Set(),
partition2: new Set(),
};
}
const cutValue = workGraph.get(node1)?.get(node2) ?? 0;
return {
cutValue,
partition1: superNodes.get(node1) ?? new Set(),
partition2: superNodes.get(node2) ?? new Set(),
};
}
/**
* Contract edge in Karger's algorithm
*/
function contractKarger(
graph: Map<string, Map<string, number>>,
superNodes: Map<string, Set<string>>,
u: string,
v: string,
): void {
// Merge v into u
const uNeighbors = graph.get(u);
const vNeighbors = graph.get(v);
if (!uNeighbors || !vNeighbors) {
return;
}
// Merge supernodes
const uSuper = superNodes.get(u);
const vSuper = superNodes.get(v);
if (!uSuper || !vSuper) {
return;
}
for (const node of vSuper) {
uSuper.add(node);
}
// Merge edges
for (const [neighbor, weight] of vNeighbors) {
if (neighbor !== u) {
uNeighbors.set(neighbor, (uNeighbors.get(neighbor) ?? 0) + weight);
// Update neighbor's edges
const neighborEdges = graph.get(neighbor);
if (neighborEdges) {
neighborEdges.delete(v);
if (neighbor !== u) {
neighborEdges.set(u, (neighborEdges.get(u) ?? 0) + weight);
}
}
}
}
// Remove self-loops
uNeighbors.delete(u);
uNeighbors.delete(v);
// Remove v
graph.delete(v);
superNodes.delete(v);
}