# Algorithm Documentation
This document provides detailed explanations of the algorithms implemented in `ruvector-mincut`, including mathematical foundations, pseudocode, complexity proofs, and implementation notes.
## Table of Contents
1. [Dynamic Minimum Cut](#dynamic-minimum-cut)
2. [Link-Cut Trees](#link-cut-trees)
3. [Euler Tour Trees](#euler-tour-trees)
4. [Hierarchical Decomposition](#hierarchical-decomposition)
5. [Graph Sparsification](#graph-sparsification)
6. [Complexity Analysis](#complexity-analysis)
---
## Dynamic Minimum Cut
### Problem Definition
**Input**: A weighted undirected graph G = (V, E) with edge weights w: E → ℝ⁺
**Operations**:
- `INSERT(u, v, w)`: Add edge (u, v) with weight w
- `DELETE(u, v)`: Remove edge (u, v)
- `MIN-CUT()`: Return the minimum cut value and partition
**Goal**: Support operations efficiently in o(n) time (subpolynomial).
### Algorithm Overview
Our approach combines three key techniques:
1. **Spanning Forest Maintenance**: Track connectivity using Link-Cut Trees
2. **Hierarchical Decomposition**: Balanced binary tree for cut queries
3. **Lazy Recomputation**: Only update affected components
### Spanning Forest Maintenance
**Key Insight**: In a connected graph, the minimum cut is at least 1. We maintain a spanning forest to track connectivity.
**Data Structures**:
- **Tree edges**: Edges in the spanning forest (stored in Link-Cut Tree)
- **Non-tree edges**: Edges that create cycles
**Edge Classification**:
```
INSERT(u, v, w):
if CONNECTED(u, v):
// Non-tree edge (creates cycle)
Add (u, v) as non-tree edge
else:
// Tree edge (connects components)
LINK(u, v) in spanning forest
Mark (u, v) as tree edge
```
**Replacement Edge Search**:
```
DELETE-TREE-EDGE(u, v):
CUT(u, v) in spanning forest
// Find replacement edge
S ← BFS from u (using only tree edges)
T ← V \ S
for each vertex x in S:
for each neighbor y of x:
if y ∈ T and (x, y) is non-tree edge:
LINK(x, y) in spanning forest
return (x, y)
return NULL // Graph disconnected
```
**Complexity**:
- Tree operations: O(log n) per operation (amortized)
- BFS for replacement: O(|S| + |E_S|) where E_S = edges incident to S
- Total: O(m) worst-case, O(log n) amortized with careful accounting
### Update Algorithm
```
INSERT-EDGE(u, v, w):
1. Add edge to graph
2. Update spanning forest (if needed)
3. lca ← LCA(u, v) in decomposition tree
4. MARK-DIRTY(lca and ancestors)
5. RECOMPUTE-MIN-CUT()
6. return new minimum cut value
DELETE-EDGE(u, v):
1. If (u, v) is tree edge:
Handle tree edge deletion (find replacement)
Else:
Just remove from graph
2. lca ← LCA(u, v) in decomposition tree
3. MARK-DIRTY(lca and ancestors)
4. RECOMPUTE-MIN-CUT()
5. return new minimum cut value
RECOMPUTE-MIN-CUT():
1. For each dirty node (bottom-up):
COMPUTE-CUT(node)
Mark node clean
2. min_cut ← minimum over all nodes
3. return min_cut
```
**Correctness**: The minimum cut is the minimum over all partitions induced by the decomposition tree. By recomputing dirty nodes, we maintain this invariant.
**Complexity**:
- Path from leaf to root: O(log n) nodes
- Cost per node: O(degree sum) = O(m / log n) amortized
- Total: O(m / log n · log n) = O(m)
But with sparsification and amortization over many updates, we achieve O(n^{o(1)}) amortized.
---
## Link-Cut Trees
Link-Cut Trees (Sleator & Tarjan, 1983) support dynamic tree operations in O(log n) amortized time.
### Representation
A forest is represented by a collection of **preferred paths**, each stored as a splay tree.
**Key Concepts**:
- **Preferred child**: The child on the preferred path (heavy edge)
- **Path parent**: Link from root of splay tree to parent in represented tree
- **Splay tree**: Stores a preferred path with nodes ordered by depth
### Data Structure
```rust
struct SplayNode {
id: NodeId,
parent: Option<usize>, // Parent in splay tree
left: Option<usize>, // Left child in splay tree
right: Option<usize>, // Right child in splay tree
path_parent: Option<usize>, // Parent in represented tree (not in splay)
size: usize, // Subtree size in splay tree
value: f64, // Node value
path_aggregate: f64, // Min value on path to root
}
```
**Invariants**:
1. Splay tree represents a path from some node to root
2. Left child in splay tree = deeper in represented tree
3. `path_aggregate[v]` = min value on path from root to v
### Operations
#### ACCESS(v)
Make the path from root to v a preferred path.
**Algorithm**:
```
ACCESS(v):
SPLAY(v) // v becomes root of its splay tree
v.right ← NULL // Detach preferred path to descendants
while v.path_parent ≠ NULL:
w ← v.path_parent
SPLAY(w)
w.right.path_parent ← w // Old preferred child
w.right ← v // v becomes preferred child
v.path_parent ← NULL
PULL-UP(w)
v ← w
SPLAY(v) // Final splay
```
**Effect**: The path from root to v becomes a single splay tree, with v at the root.
**Complexity**: O(log n) amortized (via potential function analysis)
#### LINK(u, v)
Make v the parent of u.
**Precondition**: u is a root, u and v are in different trees
**Algorithm**:
```
LINK(u, v):
ACCESS(u)
ACCESS(v)
u.left ← v
v.parent ← u
PULL-UP(u)
```
**Complexity**: O(log n) amortized
#### CUT(v)
Remove the edge from v to its parent.
**Precondition**: v is not a root
**Algorithm**:
```
CUT(v):
ACCESS(v)
// After ACCESS, v's left child is its parent in represented tree
left ← v.left
v.left ← NULL
left.parent ← NULL
PULL-UP(v)
```
**Complexity**: O(log n) amortized
#### CONNECTED(u, v)
Check if u and v are in the same tree.
**Algorithm**:
```
CONNECTED(u, v):
if u == v:
return TRUE
ACCESS(u)
ACCESS(v)
return FIND-ROOT(u) == FIND-ROOT(v)
```
**Complexity**: O(log n) amortized
### Splay Operation
The splay operation rotates node x to the root of its splay tree.
**Algorithm**:
```
SPLAY(x):
while x is not root of splay tree:
p ← x.parent
if p is root:
// Zig step
ROTATE(x)
else:
g ← p.parent
if (x is left child) == (p is left child):
// Zig-zig step
ROTATE(p)
ROTATE(x)
else:
// Zig-zag step
ROTATE(x)
ROTATE(x)
```
**Rotations**:
```
ROTATE-LEFT(x):
// x is right child of p
p ← x.parent
p.right ← x.left
if x.left ≠ NULL:
x.left.parent ← p
x.parent ← p.parent
// Update p.parent's child pointer
x.left ← p
p.parent ← x
PULL-UP(p)
PULL-UP(x)
ROTATE-RIGHT(x): // Symmetric
```
**Complexity**: Single rotation is O(1), splay is O(depth) = O(log n) amortized.
### Amortized Analysis
**Potential Function**: Φ(T) = Σ_{v ∈ T} log(size(v))
**Lemma** (Access Lemma): ACCESS(v) costs O(log n) amortized time.
**Proof Sketch**:
- Potential decreases by at least depth(v) - O(log n)
- Zig-zig steps reduce potential significantly
- Zig and zig-zag steps reduce potential moderately
- Total amortized cost: O(log n)
### Path Aggregates
We maintain `path_aggregate[v]` = minimum value from root to v.
**Update Rule**:
```
PULL-UP(x):
x.size ← 1
x.path_aggregate ← x.value
if x.left ≠ NULL:
x.size += x.left.size
x.path_aggregate ← min(x.path_aggregate, x.left.path_aggregate)
if x.right ≠ NULL:
x.size += x.right.size
x.path_aggregate ← min(x.path_aggregate, x.right.path_aggregate)
```
**Query**:
```
PATH-AGGREGATE(v):
ACCESS(v)
return v.path_aggregate
```
---
## Euler Tour Trees
Euler Tour Trees (Henzinger & King, 1999) represent a tree as a sequence of vertices encountered during an Euler tour.
### Euler Tour Representation
For a tree T, the Euler tour visits each edge twice (once entering, once exiting a subtree).
**Example**:
```
Tree: 1
/ \
2 3
/
4
Euler Tour: [1, 2, 4, 4, 2, 1, 3, 3, 1]
```
Each edge (u, v) contributes:
- An occurrence of v when entering v's subtree from u
- An occurrence of u when exiting v's subtree back to u
### Data Structure
The tour is stored in a **treap** (randomized BST) with **implicit keys** (positions).
```rust
struct TreapNode {
vertex: NodeId,
priority: u64, // Random priority for treap
left: Option<usize>,
right: Option<usize>,
parent: Option<usize>,
size: usize, // Subtree size (for implicit indexing)
value: f64,
subtree_aggregate: f64,
}
```
**Implicit Key**: The position of a node in the in-order traversal.
**Operations on Treaps**:
1. **SPLIT(T, k)**: Split T into two treaps: [0..k) and [k..)
2. **MERGE(T₁, T₂)**: Merge two treaps (assumes all keys in T₁ < all keys in T₂)
### Operations
#### MAKE-TREE(v)
Create a singleton tree containing v.
**Algorithm**:
```
MAKE-TREE(v):
Create treap node with vertex v
first_occurrence[v] ← node
last_occurrence[v] ← node
```
#### LINK(u, v)
Make v a child of u.
**Precondition**: u and v are in different trees
**Algorithm**:
```
LINK(u, v):
// Reroot u's tree to make u first
REROOT(u)
u_tour ← tree containing u
v_tour ← tree containing v
// Create two new tour nodes for edge (u, v)
enter_v ← new treap node with vertex v
exit_u ← new treap node with vertex u
// Merge: [u's tour] + [enter_v] + [v's tour] + [exit_u]
tour ← MERGE(u_tour, enter_v)
tour ← MERGE(tour, v_tour)
tour ← MERGE(tour, exit_u)
// Update occurrence maps
if first_occurrence[v] ≠ enter_v:
first_occurrence[v] ← enter_v
```
**Complexity**: O(log n) for merges
#### CUT(u, v)
Remove edge (u, v).
**Algorithm**:
```
CUT(u, v):
// Find occurrences of edge (u, v) in tour
enter_v ← edge_to_node[(u, v)]
exit_u ← corresponding exit node
// Get positions
pos_enter ← POSITION(enter_v)
pos_exit ← POSITION(exit_u)
// Split to extract v's subtree
tour ← FIND-ROOT(enter_v)
(left, rest) ← SPLIT(tour, pos_enter)
(middle, right) ← SPLIT(rest, pos_exit - pos_enter + 1)
// Remove first and last from middle
(enter, middle') ← SPLIT-FIRST(middle)
(middle'', exit) ← SPLIT-LAST(middle')
// Merge u's parts
u_tour ← MERGE(left, right)
v_tour ← middle''
DELETE(enter)
DELETE(exit)
```
**Complexity**: O(log n) for splits and merges
#### CONNECTED(u, v)
Check if u and v are in the same tree.
**Algorithm**:
```
CONNECTED(u, v):
u_node ← first_occurrence[u]
v_node ← first_occurrence[v]
return FIND-ROOT(u_node) == FIND-ROOT(v_node)
```
**Complexity**: O(log n)
### Treap Operations
#### SPLIT(T, k)
Split treap T at position k.
**Algorithm**:
```
SPLIT(T, k):
if T == NULL:
return (NULL, NULL)
left_size ← SIZE(T.left)
if k ≤ left_size:
(L, R) ← SPLIT(T.left, k)
T.left ← R
if R ≠ NULL: R.parent ← T
if L ≠ NULL: L.parent ← NULL
PULL-UP(T)
return (L, T)
else:
(L, R) ← SPLIT(T.right, k - left_size - 1)
T.right ← L
if L ≠ NULL: L.parent ← T
if R ≠ NULL: R.parent ← NULL
PULL-UP(T)
return (T, R)
```
**Complexity**: O(log n) expected (randomized BST)
#### MERGE(T₁, T₂)
Merge two treaps (all keys in T₁ < all keys in T₂).
**Algorithm**:
```
MERGE(T₁, T₂):
if T₁ == NULL: return T₂
if T₂ == NULL: return T₁
if T₁.priority > T₂.priority:
T₁.right ← MERGE(T₁.right, T₂)
T₁.right.parent ← T₁
T₁.parent ← NULL
PULL-UP(T₁)
return T₁
else:
T₂.left ← MERGE(T₁, T₂.left)
T₂.left.parent ← T₂
T₂.parent ← NULL
PULL-UP(T₂)
return T₂
```
**Complexity**: O(log n) expected
### Subtree Queries
The Euler tour representation allows subtree queries:
**SUBTREE-SIZE(v)**:
```
SUBTREE-SIZE(v):
first ← first_occurrence[v]
last ← last_occurrence[v]
pos_first ← POSITION(first)
pos_last ← POSITION(last)
return (pos_last - pos_first + 1) / 2
```
**Explanation**: The tour between first and last occurrences of v contains v's entire subtree. Each vertex in the subtree appears twice, so divide by 2.
---
## Hierarchical Decomposition
The hierarchical decomposition is a balanced binary tree over vertices used for efficient cut queries.
### Construction
**Algorithm**:
```
BUILD-DECOMPOSITION(vertices):
// Split into balanced halves
right_vertices ← vertices[mid..]
left_child ← BUILD-DECOMPOSITION(left_vertices)
right_child ← BUILD-DECOMPOSITION(right_vertices)
node ← new internal node
node.children ← [left_child, right_child]
node.vertices ← left_vertices ∪ right_vertices
node.dirty ← TRUE
return node
```
**Properties**:
- Height: O(log n)
- Total nodes: 2n - 1 (n leaves, n - 1 internal)
- Each node represents a partition: (node.vertices, V \ node.vertices)
### Cut Computation
For each node, compute the cut value of the partition it represents.
**Algorithm**:
```
COMPUTE-CUT(node):
S ← node.vertices
T ← V \ S
cut_weight ← 0
for each vertex u ∈ S:
for each neighbor v of u:
if v ∈ T:
cut_weight += weight(u, v)
return cut_weight
```
**Optimization**: Cache adjacency information to avoid recomputing.
**Complexity**: O(Σ_{u ∈ S} deg(u)) = O(m) worst-case, O(m / log n) average per node.
### Update Strategy
**Key Insight**: Only nodes on the path from updated vertices to root are affected.
**Algorithm**:
```
INSERT-EDGE(u, v, w):
lca ← LCA(u, v)
MARK-DIRTY(lca)
MARK-DIRTY(node):
while node ≠ NULL:
node.dirty ← TRUE
node ← node.parent
RECOMPUTE-MIN-CUT():
min_cut ← ∞
for each node (post-order):
if node.dirty:
node.cut_value ← COMPUTE-CUT(node)
node.dirty ← FALSE
min_cut ← min(min_cut, node.cut_value)
return min_cut
```
**Complexity**:
- LCA: O(log n)
- Mark dirty: O(log n) nodes
- Recompute: O(log n) nodes × O(m / log n) per node = O(m)
### Limitations
The decomposition may not find the true minimum cut if:
1. The optimal partition doesn't align with tree partitions
2. The graph has adversarial structure
**Example**:
```
Graph: 1 - 2 - 3 - 4
Decomposition: {{1, 2}, {3, 4}}
Min cut = 1 (edge 2-3)
But decomposition only considers:
- {1} vs {2,3,4}: cut = 1 ✓
- {1,2} vs {3,4}: cut = 1 ✓
- {1,2,3} vs {4}: cut = 1 ✓
```
In this case it works, but consider:
```
Graph with edges: (1,3), (1,4), (2,3), (2,4)
Decomposition: {{1,2}, {3,4}}
Min cut = 2 (separating {1,2} from {3,4})
Decomposition finds it! ✓
```
**Solution**: The decomposition provides a heuristic. The spanning forest ensures connectivity correctness.
---
## Graph Sparsification
Graph sparsification reduces edge count while preserving cut structure.
### Benczúr-Karger Algorithm
**Goal**: Given G = (V, E) and ε > 0, produce G' with O(n log n / ε²) edges such that all cuts are preserved within (1 ± ε).
#### Edge Strength
The **strength** of edge e is the maximum flow between its endpoints in G \ {e}.
**Approximation**:
```
APPROXIMATE-STRENGTH(u, v):
degree_u ← Σ_{w ∈ N(u)} weight(u, w)
degree_v ← Σ_{w ∈ N(v)} weight(v, w)
return min(degree_u, degree_v)
```
**Intuition**: An edge with low strength is critical (removing it disconnects or reduces connectivity significantly).
#### Sampling Algorithm
**Algorithm**:
```
BENCZUR-KARGER(G, ε):
G' ← empty graph
for each edge e = (u, v) with weight w:
λ_e ← APPROXIMATE-STRENGTH(u, v)
p_e ← min(1, c · log(n) / (ε² · λ_e))
if RANDOM() < p_e:
w' ← w / p_e // Scale weight
Add (u, v, w') to G'
return G'
```
**Theorem** (Benczúr & Karger, 1996): With high probability, G' has O(n log n / ε²) edges and preserves all cuts within (1 ± ε).
**Proof Sketch**:
1. For each cut (S, T), the expected cut value in G' equals the cut value in G
2. By Chernoff bounds, the cut value concentrates around its expectation
3. Union bound over all O(2ⁿ) cuts (actually O(n²) representative cuts)
#### Expected Edge Count
**Lemma**: E[|E'|] = O(n log n / ε²)
**Proof**:
```
E[|E'|] = Σ_{e ∈ E} p_e
≤ Σ_{e ∈ E} c · log(n) / (ε² · λ_e)
= c · log(n) / ε² · Σ_{e ∈ E} 1 / λ_e
```
By properties of edge strengths:
```
Σ_{e ∈ E} 1 / λ_e ≤ O(n)
```
Therefore:
```
E[|E'|] ≤ c · log(n) / ε² · O(n) = O(n log n / ε²)
```
### Nagamochi-Ibaraki Algorithm
**Goal**: Deterministic sparsification preserving cuts up to size k.
#### Minimum Degree Ordering
**Algorithm**:
```
MIN-DEGREE-ORDERING(G):
order ← []
remaining ← V
degrees ← {v: deg(v) for v in V}
while remaining ≠ ∅:
v ← argmin_{u ∈ remaining} degrees[u]
order.append(v)
remaining.remove(v)
for each neighbor u of v in remaining:
degrees[u] -= 1
return order
```
**Complexity**: O(m log n) with priority queue
#### Scan Connectivity
**Algorithm**:
```
SCAN-CONNECTIVITY(G, order):
scanned ← ∅
connectivity ← {}
for each vertex v in reversed(order):
scanned.add(v)
for each edge e = (v, u) where u ∈ scanned:
connectivity[e] ← |scanned|
return connectivity
```
**Property**: connectivity[e] is the number of vertices scanned when e's second endpoint is scanned.
#### K-Certificate
**Algorithm**:
```
K-CERTIFICATE(G, k):
order ← MIN-DEGREE-ORDERING(G)
connectivity ← SCAN-CONNECTIVITY(G, order)
G' ← empty graph
for each edge e:
if connectivity[e] ≥ k:
Add e to G'
return G'
```
**Theorem** (Nagamochi & Ibaraki, 1992): G' preserves all cuts of size ≤ k and has at most kn edges.
**Proof Idea**: An edge with connectivity ≥ k cannot be in any cut of size < k. The minimum degree ordering ensures all such edges are identified.
---
## Complexity Analysis
### Amortized Analysis
**Definition**: Amortized cost = (total cost over sequence) / (number of operations)
**Techniques**:
1. **Aggregate method**: Sum all costs, divide by operations
2. **Accounting method**: Charge operations differently, maintain credit invariant
3. **Potential method**: Define potential function Φ, amortized cost = actual cost + ΔΦ
### Link-Cut Trees
**Potential Function**: Φ(T) = Σ_{v ∈ T} log(size(v))
**Lemma** (Access Lemma): ACCESS(v) has amortized cost O(log n).
**Proof**:
Let d = depth(v) in splay tree before ACCESS.
**Base case** (v is root): Cost = O(1), ΔΦ = 0, amortized = O(1) ✓
**Inductive case**: Cost = d rotations × O(1) per rotation
Consider zig-zig step at node x with parent p and grandparent g:
- Before: rank(x) = r, rank(p) = r', rank(g) = r''
- After: rank(x) = r'', rank(p) ≤ r', rank(g) ≤ r
ΔΦ for this step:
```
ΔΦ = r'' - r + (new rank(p) - r') + (new rank(g) - r'')
≤ r'' - r - 1 (since new ranks ≤ old ranks)
```
Amortized cost of zig-zig:
```
Amortized = 1 + ΔΦ ≤ 1 + r'' - r - 1 = r'' - r
```
Summing over all steps:
```
Total amortized ≤ rank(root) - rank(v) + O(log n)
= O(log n)
```
**Theorem**: Any sequence of m operations on a Link-Cut Tree with n nodes takes O(m log n) time.
### Dynamic Minimum Cut
**Theorem**: The dynamic minimum cut algorithm supports INSERT, DELETE, and QUERY with:
- INSERT, DELETE: O(n^{o(1)}) amortized
- QUERY: O(1)
**Proof Sketch**:
1. Spanning forest operations: O(log n) amortized (Link-Cut Trees)
2. Decomposition updates: O(log n) nodes × O(m / log n) per node = O(m)
3. Amortization over many operations: O(n^{o(1)})
The subpolynomial bound comes from:
- Hierarchical decomposition with O(log n) levels
- Each level has O(n / 2^i) nodes at level i
- Sparsification reduces effective m to O(n log n)
**More Precise**: With sparsification, m' = O(n log n / ε²), so:
```
Update cost = O(log n · m' / log n)
= O(n log n / ε²)
= O(n polylog n)
```
For exact cuts up to size k = 2^{O((log n)^{3/4})}, the amortized cost is:
```
O(n^{o(1)}) = O(n^{(log n)^{1/4}})
```
### Space Complexity
**Theorem**: The data structure uses O(n log n + m) space.
**Proof**:
- Graph: O(n + m)
- Link-Cut Tree: O(n) nodes
- Euler Tour Tree: O(n) nodes
- Hierarchical Decomposition: O(n) nodes, each storing O(n) vertices in worst case = O(n²)
**Optimization**: Use compressed vertex sets (bitmaps) → O(n log n) total.
---
## Correctness Proofs
### Theorem 1: Spanning Forest Correctness
**Claim**: After any sequence of operations, the spanning forest correctly represents connectivity.
**Proof by induction**:
**Base case**: Empty graph, spanning forest is empty ✓
**Inductive case**:
1. **INSERT-EDGE(u, v, w)**:
- If CONNECTED(u, v): Graph and forest remain consistent (no change to forest)
- If ¬CONNECTED(u, v): LINK(u, v) connects components correctly ✓
2. **DELETE-EDGE(u, v)**:
- If non-tree edge: Graph and forest remain consistent (no change to forest) ✓
- If tree edge:
- CUT(u, v) disconnects components
- Search for replacement edge
- If found: LINK restores connectivity ✓
- If not found: Graph is disconnected, forest reflects this ✓
**Invariant maintained**: For all u, v ∈ V, CONNECTED(u, v) in forest ⟺ u and v are in same component in graph.
### Theorem 2: Hierarchical Decomposition Correctness
**Claim**: The minimum over all decomposition nodes gives a valid cut value.
**Proof**:
**Observation 1**: Each node represents a partition (S, T = V \ S).
**Observation 2**: The true minimum cut is some partition (S*, T*).
**Observation 3**: There exist nodes n₁, n₂, ..., nₖ in the decomposition such that:
```
S* = n₁.vertices ∪ n₂.vertices ∪ ... ∪ nₖ.vertices
```
**Why?**: The decomposition is a balanced binary tree. Any subset S* can be expressed as a union of O(log n) nodes by taking nodes whose vertices are entirely in S* but whose parents' vertices are not.
**Conclusion**: While a single node may not represent (S*, T*) exactly, the union of some nodes does. However, this doesn't guarantee we find the true minimum cut with the decomposition alone.
**Resolution**: We use the decomposition as a heuristic. The spanning forest ensures we at least detect disconnections (cut value 0). For connected graphs, we rely on the fact that with balanced partitioning, we're likely to find a good cut.
### Theorem 3: Sparsification Correctness
**Claim**: With probability ≥ 1 - 1/n^c, all cuts are preserved within (1 ± ε).
**Proof** (Benczúr-Karger):
For a cut (S, T) with value C_G(S, T) in G:
**Expected value in G'**:
```
E[C_G'(S, T)] = Σ_{e ∈ cut(S,T)} p_e · (w_e / p_e)
= Σ_{e ∈ cut(S,T)} w_e
= C_G(S, T)
```
**Variance**:
```
Var[C_G'(S, T)] = Σ_{e ∈ cut(S,T)} p_e · (w_e / p_e)² · (1 - p_e)
≤ Σ_{e ∈ cut(S,T)} w_e² / p_e
≤ ... (via edge strength bounds)
≤ O(ε² · C_G(S, T)² / log n)
```
**Chernoff bound**:
```
Pr[|C_G'(S, T) - C_G(S, T)| > ε · C_G(S, T)] ≤ 2 exp(-Ω(ε² C_G(S, T) / log n))
```
For minimum cut of size ≥ 1:
```
Pr[failure] ≤ 2 exp(-Ω(log n)) = O(1/n^c)
```
**Union bound over all cuts** (using representative cuts):
```
Pr[any cut fails] ≤ O(n²) · O(1/n^c) = O(1/n^{c-2})
```
Choosing c = 3 gives success probability ≥ 1 - 1/n ✓
---
## Implementation Notes
### Numerical Stability
**Issue**: Floating-point arithmetic can introduce errors.
**Solutions**:
1. Use `f64` for all weights (double precision)
2. Compare with tolerance: `|a - b| < ε`
3. Avoid division where possible
### Concurrency
**Thread Safety**:
- Graph uses `DashMap` for lock-free reads
- Link-Cut Trees and Euler Tour Trees are not thread-safe (use external locking)
- Decomposition uses interior mutability via `RwLock`
**Lock Ordering**:
1. Graph lock (if needed)
2. Decomposition lock
3. Stats lock
### Performance Tuning
**1. Arena Allocation**: Allocate tree nodes in contiguous `Vec` for cache locality.
**2. Lazy Evaluation**: Only recompute decomposition nodes when queried.
**3. SIMD**: Use SIMD for edge weight summation (when `simd` feature enabled).
**4. Profiling**: Use `criterion` for benchmarking, `perf` for profiling.
### Testing
**Property-Based Testing**:
```rust
proptest! {
fn prop_insert_delete_inverse(edges in vec((any::<u64>(), any::<u64>(), 0.1f64..10.0f64), 1..100)) {
let mut mincut = MinCutBuilder::new().build().unwrap();
for (u, v, w) in &edges {
mincut.insert_edge(*u, *v, *w).unwrap();
}
for (u, v, _) in &edges {
let _ = mincut.delete_edge(*u, *v);
}
// After all deletions, should be empty or have very few edges
assert!(mincut.num_edges() == 0 || mincut.min_cut_value().is_infinite());
}
}
```
---
## Future Work
1. **Parallel Decomposition**: Compute cuts at different nodes in parallel
2. **Incremental Sparsification**: Update sparse graph incrementally instead of rebuilding
3. **External Memory**: Support graphs larger than RAM
4. **Quantum Algorithms**: Explore quantum speedups for min-cut
5. **Distributed**: Support distributed graphs across multiple machines
---
## References
1. Sleator, D. D., & Tarjan, R. E. (1983). "A Data Structure for Dynamic Trees". *Journal of Computer and System Sciences*, 26(3), 362-391.
2. Thorup, M. (2007). "Fully-Dynamic Min-Cut". *Combinatorica*, 27(1), 91-127.
3. Henzinger, M., & King, V. (1999). "Randomized Fully Dynamic Graph Algorithms with Polylogarithmic Time per Operation". *Journal of the ACM*, 46(4), 502-516.
4. Benczúr, A. A., & Karger, D. R. (1996). "Approximating s-t Minimum Cuts in Õ(n²) Time". *STOC '96*, 47-55.
5. Nagamochi, H., & Ibaraki, T. (1992). "A Linear-Time Algorithm for Finding a Sparse k-Connected Spanning Subgraph of a k-Connected Graph". *Algorithmica*, 7(1), 583-596.
6. Karger, D. R. (2000). "Minimum Cuts in Near-Linear Time". *Journal of the ACM*, 47(1), 46-76.