18 KiB
18 KiB
SPARC Phase 2: Pseudocode - Dynamic Minimum Cut Algorithms
Overview
This document presents detailed pseudocode for the subpolynomial-time dynamic minimum cut algorithm, including:
- Hierarchical tree decomposition
- Dynamic update operations (insert/delete edges)
- Sparsification for approximate cuts
- Link-cut tree operations
- Euler tour tree maintenance
1. Core Data Structures
1.1 Hierarchical Decomposition Tree
STRUCTURE TreeNode:
id: NodeId
vertices: Set<VertexId> // Vertices in this subtree
parent: Option<NodeId> // Parent node in tree
children: Vec<NodeId> // Child nodes
local_min_cut: usize // Minimum cut within this subtree
boundary_edges: Set<Edge> // Edges crossing boundary
level: usize // Level in hierarchy (0 = leaf)
STRUCTURE DecompositionTree:
nodes: HashMap<NodeId, TreeNode>
root: NodeId
leaf_map: HashMap<VertexId, NodeId> // Map vertex to leaf node
height: usize
1.2 Link-Cut Tree (Dynamic Trees)
STRUCTURE LCTNode:
vertex: VertexId
parent: Option<VertexId>
left_child: Option<VertexId>
right_child: Option<VertexId>
path_parent: Option<VertexId> // Parent in represented tree
is_root: bool // Root of preferred path
subtree_size: usize
subtree_min: usize // Minimum edge weight in path
STRUCTURE LinkCutTree:
nodes: HashMap<VertexId, LCTNode>
FUNCTION link(u, v):
// Link vertices u and v in the represented forest
FUNCTION cut(u, v):
// Cut edge (u, v) in the represented forest
FUNCTION connected(u, v) -> bool:
// Check if u and v are in same tree
FUNCTION lca(u, v) -> VertexId:
// Find lowest common ancestor
1.3 Graph Representation
STRUCTURE DynamicGraph:
vertices: Set<VertexId>
adjacency: HashMap<VertexId, Set<VertexId>>
edge_count: usize
FUNCTION add_edge(u, v):
adjacency[u].insert(v)
adjacency[v].insert(u)
edge_count += 1
FUNCTION remove_edge(u, v):
adjacency[u].remove(v)
adjacency[v].remove(u)
edge_count -= 1
2. Main Algorithm: Dynamic Minimum Cut
2.1 Initialization
ALGORITHM initialize_dynamic_mincut(graph: DynamicGraph, config: Config):
INPUT: Graph G = (V, E), configuration
OUTPUT: DynamicMinCut structure
// Phase 1: Build initial hierarchical decomposition
decomp_tree = build_hierarchical_decomposition(graph)
// Phase 2: Initialize link-cut trees for connectivity
lct = LinkCutTree::new()
FOR each vertex v in graph.vertices:
lct.make_tree(v)
// Phase 3: Compute initial minimum cut
spanning_forest = compute_spanning_forest(graph)
FOR each edge (u, v) in spanning_forest:
lct.link(u, v)
// Phase 4: Initialize sparsification if needed
sparse_graph = None
IF config.use_sparsification:
sparse_graph = sparsify_graph(graph, config.epsilon)
RETURN DynamicMinCut {
graph: graph,
tree: decomp_tree,
lct: lct,
sparse_graph: sparse_graph,
current_min_cut: compute_min_cut_value(decomp_tree),
config: config
}
2.2 Build Hierarchical Decomposition
ALGORITHM build_hierarchical_decomposition(graph: DynamicGraph):
INPUT: Graph G = (V, E)
OUTPUT: DecompositionTree
tree = DecompositionTree::new()
n = |V|
// Base case: Create leaf nodes for each vertex
leaves = []
FOR each vertex v in V:
leaf = TreeNode {
id: new_node_id(),
vertices: {v},
parent: None,
children: [],
local_min_cut: INFINITY,
boundary_edges: get_incident_edges(v),
level: 0
}
tree.nodes.insert(leaf.id, leaf)
tree.leaf_map.insert(v, leaf.id)
leaves.append(leaf.id)
// Recursive case: Build hierarchy using expander decomposition
current_level = leaves
level_number = 1
WHILE |current_level| > 1:
next_level = []
// Group nodes using expander decomposition
groups = partition_into_expanders(current_level, graph)
FOR each group in groups:
// Create internal node for this group
internal = TreeNode {
id: new_node_id(),
vertices: UNION of group[i].vertices,
parent: None,
children: group,
local_min_cut: compute_local_min_cut(group, graph),
boundary_edges: get_boundary_edges(group, graph),
level: level_number
}
// Set parent pointers
FOR each child_id in group:
tree.nodes[child_id].parent = internal.id
tree.nodes.insert(internal.id, internal)
next_level.append(internal.id)
current_level = next_level
level_number += 1
tree.root = current_level[0]
tree.height = level_number
RETURN tree
2.3 Expander Decomposition (Key Subroutine)
ALGORITHM partition_into_expanders(nodes: Vec<NodeId>, graph: DynamicGraph):
INPUT: List of nodes at same level, graph
OUTPUT: Partition of nodes into expander groups
// Use deterministic expander decomposition
// Based on: "Deterministic expander decomposition" (Chuzhoy et al.)
groups = []
remaining = nodes.clone()
WHILE |remaining| > 0:
// Find a balanced separator with good expansion
IF |remaining| == 1:
groups.append([remaining[0]])
BREAK
// Compute expansion for potential separators
best_separator = find_balanced_separator(remaining, graph)
// Split using separator
(left, right, separator_vertices) = split_by_separator(
remaining,
best_separator,
graph
)
// Check if components are expanders
IF is_expander(left, graph, PHI_THRESHOLD):
groups.append(left)
remaining = right + separator_vertices
ELSE IF is_expander(right, graph, PHI_THRESHOLD):
groups.append(right)
remaining = left + separator_vertices
ELSE:
// Neither is expander, recurse
sub_groups_left = partition_into_expanders(left, graph)
sub_groups_right = partition_into_expanders(right, graph)
groups.extend(sub_groups_left)
groups.extend(sub_groups_right)
remaining = separator_vertices
RETURN groups
ALGORITHM is_expander(nodes: Vec<NodeId>, graph: DynamicGraph, phi: float):
// Check if induced subgraph has expansion >= phi
vertices = UNION of nodes[i].vertices
induced_edges = get_induced_edges(vertices, graph)
// Check vertex expansion: |N(S)| >= phi * |S| for all small S
FOR size s in 1..|vertices|/2:
FOR each subset S of vertices with |S| = s:
neighbors = get_neighbors(S, graph) - S
IF |neighbors| < phi * |S|:
RETURN False
RETURN True
3. Dynamic Update Operations
3.1 Edge Insertion
ALGORITHM insert_edge(mincut: DynamicMinCut, u: VertexId, v: VertexId):
INPUT: Current min-cut structure, edge (u, v) to insert
OUTPUT: Updated min-cut structure
// Step 1: Add edge to graph
mincut.graph.add_edge(u, v)
// Step 2: Check if edge affects minimum cut
IF mincut.lct.connected(u, v):
// Edge creates a cycle (non-tree edge)
// Check if it increases minimum cut
path_min = mincut.lct.path_min(u, v)
IF edge_affects_cut(u, v, path_min, mincut.tree):
update_tree_for_insertion(mincut.tree, u, v)
recompute_affected_nodes(mincut.tree, u, v)
ELSE:
// Edge connects two components
// This can only increase the minimum cut
mincut.lct.link(u, v)
merge_components_in_tree(mincut.tree, u, v)
// Step 3: Update sparsification if used
IF mincut.sparse_graph IS NOT None:
update_sparse_graph(mincut.sparse_graph, u, v, INSERT)
// Step 4: Update current minimum cut value
old_cut = mincut.current_min_cut
mincut.current_min_cut = recompute_min_cut_value(mincut.tree)
// Step 5: Trigger callbacks if cut value changed
IF old_cut != mincut.current_min_cut:
trigger_callbacks(mincut, old_cut, mincut.current_min_cut)
RETURN mincut
3.2 Edge Deletion
ALGORITHM delete_edge(mincut: DynamicMinCut, u: VertexId, v: VertexId):
INPUT: Current min-cut structure, edge (u, v) to delete
OUTPUT: Updated min-cut structure
// Step 1: Remove edge from graph
mincut.graph.remove_edge(u, v)
// Step 2: Determine if edge is tree or non-tree edge
IF is_tree_edge(u, v, mincut.lct):
// Tree edge deletion: need to find replacement
mincut.lct.cut(u, v)
// Find replacement edge to reconnect components
replacement = find_replacement_edge(u, v, mincut)
IF replacement IS NOT None:
(x, y) = replacement
mincut.lct.link(x, y)
update_tree_for_replacement(mincut.tree, u, v, x, y)
ELSE:
// Graph is now disconnected
split_components_in_tree(mincut.tree, u, v)
ELSE:
// Non-tree edge deletion
// Check if it decreases minimum cut
IF edge_affects_cut(u, v, mincut.tree):
update_tree_for_deletion(mincut.tree, u, v)
recompute_affected_nodes(mincut.tree, u, v)
// Step 3: Update sparsification
IF mincut.sparse_graph IS NOT None:
update_sparse_graph(mincut.sparse_graph, u, v, DELETE)
// Step 4: Update current minimum cut value
old_cut = mincut.current_min_cut
mincut.current_min_cut = recompute_min_cut_value(mincut.tree)
// Step 5: Trigger callbacks
IF old_cut != mincut.current_min_cut:
trigger_callbacks(mincut, old_cut, mincut.current_min_cut)
RETURN mincut
3.3 Find Replacement Edge
ALGORITHM find_replacement_edge(u: VertexId, v: VertexId, mincut: DynamicMinCut):
INPUT: Deleted tree edge (u, v), min-cut structure
OUTPUT: Replacement edge or None
// Use Euler tour tree to efficiently search for replacement
// Get the two components after cutting (u, v)
comp_u = get_component_vertices(u, mincut.lct)
comp_v = get_component_vertices(v, mincut.lct)
// Ensure comp_u is smaller for efficiency
IF |comp_u| > |comp_v|:
SWAP(comp_u, comp_v)
// Search for edge from comp_u to comp_v
FOR each vertex x in comp_u:
FOR each neighbor y in mincut.graph.adjacency[x]:
IF y in comp_v:
RETURN (x, y)
RETURN None
4. Minimum Cut Computation
4.1 Query Minimum Cut Value
ALGORITHM min_cut_value(mincut: DynamicMinCut) -> usize:
INPUT: Min-cut structure
OUTPUT: Current minimum cut value
// O(1) query: maintained incrementally
RETURN mincut.current_min_cut
4.2 Query Minimum Cut Partition
ALGORITHM min_cut_partition(mincut: DynamicMinCut) -> (Set<VertexId>, Set<VertexId>):
INPUT: Min-cut structure
OUTPUT: (Partition A, Partition B) achieving minimum cut
// Find node in tree where cut is achieved
cut_node = find_min_cut_node(mincut.tree, mincut.tree.root)
// Get vertices on each side of cut
partition_a = cut_node.vertices
partition_b = mincut.graph.vertices - partition_a
// Verify cut value
cut_edges = 0
FOR each v in partition_a:
FOR each u in mincut.graph.adjacency[v]:
IF u in partition_b:
cut_edges += 1
ASSERT cut_edges == mincut.current_min_cut
RETURN (partition_a, partition_b)
ALGORITHM find_min_cut_node(tree: DecompositionTree, node_id: NodeId):
INPUT: Decomposition tree, current node
OUTPUT: Node where minimum cut is achieved
node = tree.nodes[node_id]
// Base case: leaf node
IF node.children IS EMPTY:
RETURN node
// Recursive case: check children
min_cut_value = node.local_min_cut
min_cut_node = node
FOR each child_id in node.children:
child = tree.nodes[child_id]
IF child.local_min_cut < min_cut_value:
min_cut_value = child.local_min_cut
min_cut_node = find_min_cut_node(tree, child_id)
RETURN min_cut_node
5. Graph Sparsification
5.1 Sparsify for (1+ε)-Approximation
ALGORITHM sparsify_graph(graph: DynamicGraph, epsilon: float):
INPUT: Graph G = (V, E), approximation parameter ε
OUTPUT: Sparse graph H with O(n log n / ε²) edges
// Use cut-preserving sparsification (Benczúr-Karger)
n = |graph.vertices|
m = graph.edge_count
// Estimate minimum cut (can use quick heuristic)
lambda_estimate = estimate_min_cut(graph)
// Sampling probability for each edge
sample_prob = min(1.0, (12 * log(n)) / (epsilon^2 * lambda_estimate))
sparse = DynamicGraph::new()
FOR each vertex v in graph.vertices:
sparse.add_vertex(v)
// Sample edges with appropriate weights
FOR each edge (u, v) in graph.edges:
// Random sampling based on importance
edge_importance = compute_edge_importance(u, v, graph)
p = sample_prob / edge_importance
IF random() < p:
// Add to sparse graph with weight 1/p
sparse.add_edge(u, v, weight = 1.0/p)
RETURN sparse
ALGORITHM estimate_min_cut(graph: DynamicGraph):
// Quick estimate using minimum degree
min_degree = INFINITY
FOR each vertex v in graph.vertices:
degree = |graph.adjacency[v]|
min_degree = min(min_degree, degree)
RETURN min_degree
5.2 Compute Edge Importance (Connectivity)
ALGORITHM compute_edge_importance(u: VertexId, v: VertexId, graph: DynamicGraph):
INPUT: Edge (u, v), graph
OUTPUT: Importance score (higher = more important for connectivity)
// Use local connectivity heuristic
// Remove edge temporarily
graph.remove_edge(u, v)
// Check if u and v are still connected via BFS
distance = bfs_distance(u, v, graph, max_depth=10)
// Restore edge
graph.add_edge(u, v)
// Importance inversely proportional to alternative path length
IF distance == INFINITY:
RETURN INFINITY // Bridge edge, very important
ELSE:
RETURN 1.0 / distance
6. Link-Cut Tree Operations (Detailed)
6.1 Splay Operation
ALGORITHM splay(lct: LinkCutTree, x: VertexId):
INPUT: Link-cut tree, vertex to splay
OUTPUT: x becomes root of its auxiliary tree
WHILE NOT lct.nodes[x].is_root:
p = lct.nodes[x].parent
IF p IS None OR lct.nodes[p].is_root:
// Zig: x's parent is root
IF x == lct.nodes[p].left_child:
rotate_right(lct, p)
ELSE:
rotate_left(lct, p)
ELSE:
g = lct.nodes[p].parent
IF x == lct.nodes[p].left_child AND p == lct.nodes[g].left_child:
// Zig-zig: both left children
rotate_right(lct, g)
rotate_right(lct, p)
ELSE IF x == lct.nodes[p].right_child AND p == lct.nodes[g].right_child:
// Zig-zig: both right children
rotate_left(lct, g)
rotate_left(lct, p)
ELSE IF x == lct.nodes[p].right_child AND p == lct.nodes[g].left_child:
// Zig-zag: x is right, p is left
rotate_left(lct, p)
rotate_right(lct, g)
ELSE:
// Zig-zag: x is left, p is right
rotate_right(lct, p)
rotate_left(lct, g)
6.2 Access Operation
ALGORITHM access(lct: LinkCutTree, x: VertexId):
INPUT: Link-cut tree, vertex to access
OUTPUT: Path from root to x becomes preferred path
// Make path from root to x preferred
splay(lct, x)
lct.nodes[x].right_child = None
update_aggregate(lct, x)
WHILE lct.nodes[x].path_parent IS NOT None:
y = lct.nodes[x].path_parent
splay(lct, y)
lct.nodes[y].right_child = x
update_aggregate(lct, y)
splay(lct, x)
6.3 Link and Cut
ALGORITHM link(lct: LinkCutTree, u: VertexId, v: VertexId):
INPUT: Link-cut tree, vertices to link
PRECONDITION: u and v in different trees
// Make u root of its tree
access(lct, u)
lct.nodes[u].is_root = True
// Attach to v
access(lct, v)
lct.nodes[u].path_parent = v
ALGORITHM cut(lct: LinkCutTree, u: VertexId, v: VertexId):
INPUT: Link-cut tree, edge to cut
PRECONDITION: (u, v) is edge in represented tree
// Make u-v path preferred
access(lct, u)
access(lct, v)
// v is root after access(v), and u is left child
ASSERT lct.nodes[v].left_child == u
lct.nodes[v].left_child = None
lct.nodes[u].parent = None
lct.nodes[u].is_root = True
update_aggregate(lct, v)
6.4 Connected Query
ALGORITHM connected(lct: LinkCutTree, u: VertexId, v: VertexId) -> bool:
INPUT: Link-cut tree, two vertices
OUTPUT: True if u and v in same tree
IF u == v:
RETURN True
access(lct, u)
access(lct, v)
// If they're connected, u has a path_parent after access(v)
RETURN lct.nodes[u].path_parent IS NOT None
7. Complexity Analysis
7.1 Time Complexity
| Operation | Amortized Time | Worst Case |
|---|---|---|
insert_edge |
O(n^{o(1)}) | O(log² n) per level × O(log n) levels |
delete_edge |
O(n^{o(1)}) | O(log² n) per level × O(log n) levels |
min_cut_value |
O(1) | O(1) |
min_cut_partition |
O(k) | O(n) where k = cut size |
| Link-cut tree ops | O(log n) | O(log n) amortized |
7.2 Space Complexity
- Decomposition tree: O(n log n) nodes
- Link-cut tree: O(n) nodes
- Graph storage: O(m + n)
- Sparse graph: O(n log n / ε²)
- Total: O(m + n log n)
7.3 Achieving n^{o(1)}
The key to subpolynomial time:
- Tree height: O(log n) via balanced decomposition
- Updates per level: O(log n) amortized via link-cut trees
- Levels affected: O(log n / log log n) via careful maintenance
- Total: O(log n × log n × log n / log log n) = O(log³ n / log log n) = n^{o(1)}
Next Phase: Proceed to 03-architecture.md for system design and module structure.