git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
27 KiB
ADR-002: Dynamic Hierarchical j-Tree Decomposition for Approximate Cut Structure
Status: Proposed Date: 2026-01-25 Authors: ruv.io, RuVector Team Deciders: Architecture Review Board SDK: Claude-Flow
Version History
| Version | Date | Author | Changes |
|---|---|---|---|
| 0.1 | 2026-01-25 | ruv.io | Initial draft based on arXiv:2601.09139 research |
Plain Language Summary
What is it?
A new algorithmic framework for maintaining an approximate view of a graph's cut structure that updates in near-constant time even as edges are added and removed. It complements our existing exact min-cut implementation by providing a fast "global radar" that can answer approximate cut queries instantly.
Why does it matter?
Our current implementation (arXiv:2512.13105, El-Hayek/Henzinger/Li) excels at exact min-cut for superpolylogarithmic cuts but is optimized for a specific cut-size regime. The new j-tree decomposition (arXiv:2601.09139, Goranci/Henzinger/Kiss/Momeni/Zöcklein, January 2026) provides:
- Broader coverage: Poly-logarithmic approximation for ALL cut-based problems (sparsest cut, multi-way cut, multi-cut, all-pairs min-cuts)
- Faster updates: O(n^ε) amortized for any arbitrarily small ε > 0
- Low recourse: The underlying cut-sparsifier tolerates vertex splits with poly-logarithmic recourse
The Two-Tier Strategy:
| Tier | Algorithm | Purpose | When to Use |
|---|---|---|---|
| Tier 1 | j-Tree Decomposition | Fast approximate hierarchy for global structure | Continuous monitoring, routing decisions |
| Tier 2 | El-Hayek/Henzinger/Li | Exact deterministic min-cut | When Tier 1 detects critical cuts |
Think of it like sonar and radar: the j-tree is your wide-area radar that shows approximate threat positions instantly, while the exact algorithm is your precision sonar that confirms exact details when needed.
Context
Current State
RuVector MinCut implements the December 2025 breakthrough (arXiv:2512.13105) achieving:
| Property | Current Implementation |
|---|---|
| Update Time | O(n^{o(1)}) amortized |
| Approximation | Exact |
| Deterministic | Yes |
| Cut Regime | Superpolylogarithmic (λ > log^c n) |
| Verified Scaling | n^0.12 empirically |
This works excellently for the coherence gate (ADR-001) where we need exact cut values for safety decisions. However, several use cases require:
- Broader cut-based queries: Sparsest cut, multi-way cut, multi-cut, all-pairs min-cuts
- Even faster updates: When monitoring 10K+ updates/second
- Global structure awareness: Understanding the overall cut landscape, not just the minimum
The January 2026 Breakthrough
The paper "Dynamic Hierarchical j-Tree Decomposition and Its Applications" (arXiv:2601.09139, SODA 2026) by Goranci, Henzinger, Kiss, Momeni, and Zöcklein addresses the open question:
"Is there a fully dynamic algorithm for cut-based optimization problems that achieves poly-logarithmic approximation with very small polynomial update time?"
Key Results:
| Result | Complexity | Significance |
|---|---|---|
| Update Time | O(n^ε) amortized for any ε ∈ (0,1) | Arbitrarily close to polylog |
| Approximation | Poly-logarithmic | Sufficient for structure detection |
| Query Support | All cut-based problems | Not just min-cut |
| Recourse | Poly-logarithmic total | Sparsifier doesn't explode |
Technical Innovation: Vertex-Split-Tolerant Cut Sparsifier
The core innovation is a dynamic cut-sparsifier that handles vertex splits with low recourse:
Traditional approach: Vertex splits cause O(n) cascading updates
New approach: Forest packing with lazy repair → poly-log recourse
The sparsifier maintains (1±ε) approximation of all cuts while:
- Tolerating vertex splits (critical for dynamic hierarchies)
- Adjusting only poly-logarithmically many edges per update
- Serving as a backbone for the j-tree hierarchy
The (L,j) Hierarchy
The j-tree hierarchy reflects increasingly coarse views of the graph's cut landscape:
Level 0: Original graph G
Level 1: Contracted graph with j-tree quality α
Level 2: Further contracted with quality α²
...
Level L: Root (O(1) vertices)
L = O(log n / log α)
Each level preserves cut structure within an α^ℓ factor, enabling:
- Fast approximate queries: Traverse O(log n) levels
- Local updates: Changes propagate through O(log n) levels
- Multi-scale view: See both fine and coarse structure
Decision
Adopt Two-Tier Dynamic Cut Architecture
We will implement the j-tree decomposition as a complementary layer to our existing exact min-cut, creating a two-tier system:
┌─────────────────────────────────────────────────────────────────────────┐
│ TWO-TIER DYNAMIC CUT ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ TIER 1: J-TREE HIERARCHY (NEW) │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ Level L │ │ Level L-1 │ │ Level 0 │ │ │
│ │ │ (Root) │◄───│ (Coarse) │◄───│ (Original) │ │ │
│ │ │ O(1) vtx │ │ α^(L-1) cut │ │ Exact cuts │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │
│ │ │ │
│ │ Purpose: Fast approximate answers for global structure │ │
│ │ Update: O(n^ε) amortized for any ε > 0 │ │
│ │ Query: Poly-log approximation for all cut problems │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ Trigger: Approximate cut below threshold │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ TIER 2: EXACT MIN-CUT (EXISTING) │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ │ SubpolynomialMinCut (arXiv:2512.13105) │ │ │
│ │ │ • O(n^{o(1)}) amortized exact updates │ │ │
│ │ │ • Verified n^0.12 scaling │ │ │
│ │ │ • Deterministic, no randomization │ │ │
│ │ │ • For superpolylogarithmic cuts (λ > log^c n) │ │ │
│ │ └──────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Purpose: Exact verification when precision required │ │
│ │ Trigger: Tier 1 detects potential critical cut │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Module Structure
ruvector-mincut/
├── src/
│ ├── jtree/ # NEW: j-Tree Decomposition
│ │ ├── mod.rs # Module exports
│ │ ├── hierarchy.rs # (L,j) hierarchical decomposition
│ │ ├── sparsifier.rs # Vertex-split-tolerant cut sparsifier
│ │ ├── forest_packing.rs # Forest packing for sparsification
│ │ ├── vertex_split.rs # Vertex split handling with low recourse
│ │ ├── contraction.rs # Graph contraction for hierarchy levels
│ │ └── queries/ # Cut-based query implementations
│ │ ├── mod.rs
│ │ ├── all_pairs_mincut.rs
│ │ ├── sparsest_cut.rs
│ │ ├── multiway_cut.rs
│ │ └── multicut.rs
│ ├── tiered/ # NEW: Two-tier coordination
│ │ ├── mod.rs
│ │ ├── coordinator.rs # Tier 1/Tier 2 routing logic
│ │ ├── trigger.rs # Escalation trigger policies
│ │ └── cache.rs # Cross-tier result caching
│ └── ...existing modules...
Core Data Structures
j-Tree Hierarchy
/// Hierarchical j-tree decomposition for approximate cut structure
pub struct JTreeHierarchy {
/// Number of levels (L = O(log n / log α))
levels: usize,
/// Approximation quality per level
alpha: f64,
/// Contracted graphs at each level
contracted_graphs: Vec<ContractedGraph>,
/// Cut sparsifier backbone
sparsifier: DynamicCutSparsifier,
/// j-trees at each level
jtrees: Vec<JTree>,
}
/// Single level j-tree
pub struct JTree {
/// Tree structure
tree: DynamicTree,
/// Mapping from original vertices to tree nodes
vertex_map: HashMap<VertexId, TreeNodeId>,
/// Cached cut values between tree nodes
cut_cache: CutCache,
/// Level index
level: usize,
}
impl JTreeHierarchy {
/// Build hierarchy from graph
pub fn build(graph: &DynamicGraph, epsilon: f64) -> Self {
let alpha = compute_alpha(epsilon);
let levels = (graph.vertex_count() as f64).log(alpha as f64).ceil() as usize;
// Build sparsifier first
let sparsifier = DynamicCutSparsifier::build(graph, epsilon);
// Build contracted graphs level by level
let mut contracted_graphs = Vec::with_capacity(levels);
let mut current = sparsifier.sparse_graph();
for level in 0..levels {
contracted_graphs.push(current.clone());
current = contract_to_jtree(¤t, alpha);
}
Self {
levels,
alpha,
contracted_graphs,
sparsifier,
jtrees: build_jtrees(&contracted_graphs),
}
}
/// Insert edge with O(n^ε) amortized update
pub fn insert_edge(&mut self, u: VertexId, v: VertexId, weight: f64) -> Result<(), Error> {
// Update sparsifier (handles vertex splits internally)
self.sparsifier.insert_edge(u, v, weight)?;
// Propagate through hierarchy levels
for level in 0..self.levels {
self.update_level(level, EdgeUpdate::Insert(u, v, weight))?;
}
Ok(())
}
/// Delete edge with O(n^ε) amortized update
pub fn delete_edge(&mut self, u: VertexId, v: VertexId) -> Result<(), Error> {
self.sparsifier.delete_edge(u, v)?;
for level in 0..self.levels {
self.update_level(level, EdgeUpdate::Delete(u, v))?;
}
Ok(())
}
/// Query approximate min-cut (poly-log approximation)
pub fn approximate_min_cut(&self) -> ApproximateCut {
// Start from root level and refine
let mut cut = self.jtrees[self.levels - 1].min_cut();
for level in (0..self.levels - 1).rev() {
cut = self.jtrees[level].refine_cut(&cut);
}
ApproximateCut {
value: cut.value,
approximation_factor: self.alpha.powi(self.levels as i32),
partition: cut.partition,
}
}
}
Vertex-Split-Tolerant Cut Sparsifier
/// Dynamic cut sparsifier with low recourse under vertex splits
pub struct DynamicCutSparsifier {
/// Forest packing for edge sampling
forest_packing: ForestPacking,
/// Sparse graph maintaining (1±ε) cut approximation
sparse_graph: DynamicGraph,
/// Epsilon parameter
epsilon: f64,
/// Recourse counter for complexity verification
recourse: RecourseTracker,
}
impl DynamicCutSparsifier {
/// Handle vertex split with poly-log recourse
pub fn split_vertex(&mut self, v: VertexId, v1: VertexId, v2: VertexId,
partition: &[EdgeId]) -> Result<RecourseStats, Error> {
let before_edges = self.sparse_graph.edge_count();
// Forest packing handles the split
let affected_forests = self.forest_packing.split_vertex(v, v1, v2, partition)?;
// Lazy repair: only fix forests that actually need it
for forest_id in affected_forests {
self.repair_forest(forest_id)?;
}
let recourse = (self.sparse_graph.edge_count() as i64 - before_edges as i64).abs();
self.recourse.record(recourse as usize);
Ok(self.recourse.stats())
}
/// The key insight: forest packing limits cascading updates
fn repair_forest(&mut self, forest_id: ForestId) -> Result<(), Error> {
// Only O(log n) edges need adjustment per forest
// Total forests = O(log n / ε²)
// Total recourse = O(log² n / ε²) per vertex split
self.forest_packing.repair(forest_id, &mut self.sparse_graph)
}
}
Two-Tier Coordinator
/// Coordinates between j-tree approximation (Tier 1) and exact min-cut (Tier 2)
pub struct TwoTierCoordinator {
/// Tier 1: Fast approximate hierarchy
jtree: JTreeHierarchy,
/// Tier 2: Exact min-cut for verification
exact: SubpolynomialMinCut,
/// Trigger policy for escalation
trigger: EscalationTrigger,
/// Result cache to avoid redundant computation
cache: TierCache,
}
/// When to escalate from Tier 1 to Tier 2
pub struct EscalationTrigger {
/// Approximate cut threshold below which we verify exactly
critical_threshold: f64,
/// Maximum approximation factor before requiring exact
max_approx_factor: f64,
/// Whether the query requires exact answer
exact_required: bool,
}
impl TwoTierCoordinator {
/// Query min-cut with tiered strategy
pub fn min_cut(&mut self, exact_required: bool) -> CutResult {
// Check cache first
if let Some(cached) = self.cache.get() {
if !exact_required || cached.is_exact {
return cached.clone();
}
}
// Tier 1: Fast approximate query
let approx = self.jtree.approximate_min_cut();
// Decide whether to escalate
let should_escalate = exact_required
|| approx.value < self.trigger.critical_threshold
|| approx.approximation_factor > self.trigger.max_approx_factor;
if should_escalate {
// Tier 2: Exact verification
let exact_value = self.exact.min_cut_value();
let exact_partition = self.exact.partition();
let result = CutResult {
value: exact_value,
partition: exact_partition,
is_exact: true,
approximation_factor: 1.0,
tier_used: Tier::Exact,
};
self.cache.store(result.clone());
result
} else {
let result = CutResult {
value: approx.value,
partition: approx.partition,
is_exact: false,
approximation_factor: approx.approximation_factor,
tier_used: Tier::Approximate,
};
self.cache.store(result.clone());
result
}
}
/// Insert edge, updating both tiers
pub fn insert_edge(&mut self, u: VertexId, v: VertexId, weight: f64) -> Result<(), Error> {
self.cache.invalidate();
// Update Tier 1 (fast)
self.jtree.insert_edge(u, v, weight)?;
// Update Tier 2 (also fast, but only if we're tracking that edge regime)
self.exact.insert_edge(u, v, weight)?;
Ok(())
}
}
Extended Query Support
The j-tree hierarchy enables queries beyond min-cut:
impl JTreeHierarchy {
/// All-pairs minimum cuts (approximate)
pub fn all_pairs_min_cuts(&self) -> AllPairsResult {
// Use hierarchy to avoid O(n²) explicit computation
// Query time: O(n log n) for all pairs
let mut results = HashMap::new();
for (u, v) in self.vertex_pairs() {
let cut = self.min_cut_between(u, v);
results.insert((u, v), cut);
}
AllPairsResult { cuts: results }
}
/// Sparsest cut (approximate)
pub fn sparsest_cut(&self) -> SparsestCutResult {
// Leverage hierarchy for O(n^ε) approximate sparsest cut
let mut best_sparsity = f64::INFINITY;
let mut best_cut = None;
for level in 0..self.levels {
let candidate = self.jtrees[level].sparsest_cut_candidate();
let sparsity = candidate.value / candidate.size.min() as f64;
if sparsity < best_sparsity {
best_sparsity = sparsity;
best_cut = Some(candidate);
}
}
SparsestCutResult {
cut: best_cut.unwrap(),
sparsity: best_sparsity,
approximation: self.alpha.powi(self.levels as i32),
}
}
/// Multi-way cut (approximate)
pub fn multiway_cut(&self, terminals: &[VertexId]) -> MultiwayCutResult {
// Use j-tree hierarchy to find approximate multiway cut
// Approximation: O(log k) where k = number of terminals
self.compute_multiway_cut(terminals)
}
/// Multi-cut (approximate)
pub fn multicut(&self, pairs: &[(VertexId, VertexId)]) -> MulticutResult {
// Approximate multicut using hierarchy
self.compute_multicut(pairs)
}
}
Integration with Coherence Gate (ADR-001)
The j-tree hierarchy integrates with the Anytime-Valid Coherence Gate:
/// Enhanced coherence gate using two-tier cut architecture
pub struct TieredCoherenceGate {
/// Two-tier cut coordinator
cut_coordinator: TwoTierCoordinator,
/// Conformal prediction component
conformal: ShiftAdaptiveConformal,
/// E-process evidence accumulator
evidence: EProcessAccumulator,
/// Gate thresholds
thresholds: GateThresholds,
}
impl TieredCoherenceGate {
/// Fast structural check using Tier 1
pub fn fast_structural_check(&self, action: &Action) -> QuickDecision {
// Use j-tree for O(n^ε) approximate check
let approx_cut = self.cut_coordinator.jtree.approximate_min_cut();
if approx_cut.value > self.thresholds.definitely_safe {
QuickDecision::Permit
} else if approx_cut.value < self.thresholds.definitely_unsafe {
QuickDecision::Deny
} else {
QuickDecision::NeedsExactCheck
}
}
/// Full evaluation with exact verification if needed
pub fn evaluate(&mut self, action: &Action, context: &Context) -> GateDecision {
// Quick check first
let quick = self.fast_structural_check(action);
match quick {
QuickDecision::Permit => {
// Fast path: structure is definitely safe
self.issue_permit_fast(action)
}
QuickDecision::Deny => {
// Fast path: structure is definitely unsafe
self.issue_denial_fast(action)
}
QuickDecision::NeedsExactCheck => {
// Invoke Tier 2 for exact verification
let exact_cut = self.cut_coordinator.min_cut(true);
self.evaluate_with_exact_cut(action, context, exact_cut)
}
}
}
}
Performance Characteristics
| Operation | Tier 1 (j-Tree) | Tier 2 (Exact) | Combined |
|---|---|---|---|
| Insert Edge | O(n^ε) | O(n^{o(1)}) | O(n^ε) |
| Delete Edge | O(n^ε) | O(n^{o(1)}) | O(n^ε) |
| Min-Cut Query | O(log n) approx | O(1) exact | O(1) - O(log n) |
| All-Pairs Min-Cut | O(n log n) | N/A | O(n log n) |
| Sparsest Cut | O(n^ε) | N/A | O(n^ε) |
| Multi-Way Cut | O(k log k · n^ε) | N/A | O(k log k · n^ε) |
Recourse Guarantees
The vertex-split-tolerant sparsifier provides:
| Metric | Guarantee |
|---|---|
| Edges adjusted per update | O(log² n / ε²) |
| Total recourse over m updates | O(m · log² n / ε²) |
| Forest repairs per vertex split | O(log n) |
This is critical for maintaining hierarchy stability under dynamic changes.
Implementation Phases
Phase 1: Core Sparsifier (Weeks 1-3)
- Implement
ForestPackingwith edge sampling - Implement
DynamicCutSparsifierwith vertex split handling - Add recourse tracking and verification
- Unit tests for sparsifier correctness
Phase 2: j-Tree Hierarchy (Weeks 4-6)
- Implement
JTreesingle-level structure - Implement
JTreeHierarchymulti-level decomposition - Add contraction algorithms for level construction
- Integration tests for hierarchy maintenance
Phase 3: Query Support (Weeks 7-9)
- Implement approximate min-cut queries
- Implement all-pairs min-cut
- Implement sparsest cut
- Implement multi-way cut and multi-cut
- Benchmark query performance
Phase 4: Two-Tier Integration (Weeks 10-12)
- Implement
TwoTierCoordinator - Define escalation trigger policies
- Integrate with coherence gate
- End-to-end testing with coherence scenarios
Feature Flags
[features]
# Existing features
default = ["exact", "approximate"]
exact = []
approximate = []
# New features
jtree = [] # j-Tree hierarchical decomposition
tiered = ["jtree", "exact"] # Two-tier coordinator
all-cut-queries = ["jtree"] # Sparsest cut, multiway, multicut
Consequences
Benefits
- Broader Query Support: Sparsest cut, multi-way cut, multi-cut, all-pairs - not just minimum cut
- Faster Continuous Monitoring: O(n^ε) updates enable 10K+ updates/second even on large graphs
- Global Structure Awareness: Hierarchical view shows cut landscape at multiple scales
- Graceful Degradation: Approximate answers when exact isn't needed, exact when it is
- Low Recourse: Sparsifier stability prevents update cascades
- Coherence Gate Enhancement: Fast structural checks with exact fallback
Risks & Mitigations
| Risk | Probability | Impact | Mitigation |
|---|---|---|---|
| Implementation complexity | High | Medium | Phase incrementally, extensive testing |
| Approximation too loose | Medium | Medium | Tunable α parameter, exact fallback |
| Memory overhead from hierarchy | Medium | Low | Lazy level construction |
| Integration complexity with existing code | Medium | Medium | Clean interface boundaries |
Complexity Analysis
| Component | Space | Time (Update) | Time (Query) |
|---|---|---|---|
| Forest Packing | O(m log n / ε²) | O(log² n / ε²) | O(1) |
| j-Tree Level | O(n_ℓ) | O(n_ℓ^ε) | O(log n_ℓ) |
| Full Hierarchy | O(n log n) | O(n^ε) | O(log n) |
| Two-Tier Cache | O(n) | O(1) | O(1) |
References
Primary
- Goranci, G., Henzinger, M., Kiss, P., Momeni, A., & Zöcklein, G. (January 2026). "Dynamic Hierarchical j-Tree Decomposition and Its Applications." arXiv:2601.09139. SODA 2026. [Core paper for this ADR]
Complementary
-
El-Hayek, A., Henzinger, M., & Li, J. (December 2025). "Deterministic and Exact Fully-dynamic Minimum Cut of Superpolylogarithmic Size in Subpolynomial Time." arXiv:2512.13105. [Existing Tier 2 implementation]
-
Mądry, A. (2010). "Fast Approximation Algorithms for Cut-Based Problems in Undirected Graphs." FOCS 2010. [Original j-tree decomposition]
Background
-
Benczúr, A. A., & Karger, D. R. (1996). "Approximating s-t Minimum Cuts in Õ(n²) Time." STOC. [Cut sparsification foundations]
-
Thorup, M. (2007). "Fully-Dynamic Min-Cut." Combinatorica. [Dynamic min-cut foundations]
Related Decisions
- ADR-001: Anytime-Valid Coherence Gate (uses Tier 2 exact min-cut)
- ADR-014: Coherence Engine Architecture (coherence computation)
- ADR-CE-001: Sheaf Laplacian Coherence (structural coherence foundation)
Appendix: Paper Comparison
El-Hayek/Henzinger/Li (Dec 2025) vs Goranci et al. (Jan 2026)
| Aspect | arXiv:2512.13105 | arXiv:2601.09139 |
|---|---|---|
| Focus | Exact min-cut | Approximate cut hierarchy |
| Update Time | O(n^{o(1)}) | O(n^ε) for any ε > 0 |
| Approximation | Exact | Poly-logarithmic |
| Cut Regime | Superpolylogarithmic | All sizes |
| Query Types | Min-cut only | All cut problems |
| Deterministic | Yes | Yes |
| Key Technique | Cluster hierarchy + LocalKCut | j-Tree + vertex-split sparsifier |
Synergy: The two approaches complement each other perfectly:
- Use Goranci et al. for fast global monitoring and diverse cut queries
- Use El-Hayek et al. for exact verification when critical cuts are detected
This two-tier strategy provides both breadth (approximate queries on all cut problems) and depth (exact min-cut when needed).