@graphty/algorithms
Version:
Graph algorithms library for browser environments implemented in TypeScript
383 lines • 11.4 kB
JavaScript
/**
* Minimum Cut Algorithms
*
* Various algorithms for finding minimum cuts in graphs
*/
import { fordFulkerson } from "./ford-fulkerson.js";
/**
* 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, source, sink) {
const flowResult = fordFulkerson(graph, source, sink);
if (!flowResult.minCut) {
return {
cutValue: 0,
partition1: new Set(),
partition2: new Set(),
cutEdges: [],
};
}
const cutEdges = [];
// Find actual cut edges and their weights
for (const [u, v] of flowResult.minCut.edges) {
const weight = graph.get(u)?.get(v) ?? 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
* @returns Global minimum cut
*
* Time Complexity: O(V³) or O(VE + V² log V) with heap
*/
export function stoerWagner(graph) {
// Convert to undirected if necessary
const undirectedGraph = makeUndirected(graph);
if (undirectedGraph.size < 2) {
return {
cutValue: 0,
partition1: new Set(undirectedGraph.keys()),
partition2: new Set(),
cutEdges: [],
};
}
// Initialize
const nodes = Array.from(undirectedGraph.keys());
let minCutValue = Infinity;
let bestPartition = new Set();
const contractionMap = new Map();
// 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();
for (const node of graph.keys()) {
if (!partition1.has(node)) {
partition2.add(node);
}
}
// Find cut edges
const cutEdges = [];
for (const u of partition1) {
const neighbors = graph.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, nodes) {
const n = nodes.length;
const weight = new Map();
const added = new Set();
const order = [];
// 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, nodes, s, t) {
// 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) {
const undirected = new Map();
// 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
* @param iterations - Number of iterations (higher = better accuracy)
* @returns Minimum cut found
*
* Time Complexity: O(V² * iterations)
*/
export function kargerMinCut(graph, iterations = 100) {
let minCutValue = Infinity;
let bestPartition1 = new Set();
let bestPartition2 = new Set();
for (let i = 0; i < iterations; i++) {
const result = kargerSingleRun(graph);
if (result.cutValue < minCutValue) {
minCutValue = result.cutValue;
bestPartition1 = result.partition1;
bestPartition2 = result.partition2;
}
}
// Find cut edges
const cutEdges = [];
for (const u of bestPartition1) {
const neighbors = graph.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) {
// Create a copy of the graph
const workGraph = new Map();
const superNodes = new Map();
// 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 = [];
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, superNodes, u, v) {
// 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);
}
//# sourceMappingURL=min-cut.js.map