From c98aab7480a95c9a1a4107d797082adc8731ac7b Mon Sep 17 00:00:00 2001 From: Huai-Ming Yu Date: Tue, 10 Mar 2026 16:40:31 +0800 Subject: [PATCH 01/13] Add plan for #184: MinimumMultiwayCut model Co-authored-by: Claude --- docs/plans/2026-03-10-minimum-multiway-cut.md | 611 ++++++++++++++++++ 1 file changed, 611 insertions(+) create mode 100644 docs/plans/2026-03-10-minimum-multiway-cut.md diff --git a/docs/plans/2026-03-10-minimum-multiway-cut.md b/docs/plans/2026-03-10-minimum-multiway-cut.md new file mode 100644 index 000000000..58a8937b5 --- /dev/null +++ b/docs/plans/2026-03-10-minimum-multiway-cut.md @@ -0,0 +1,611 @@ +# MinimumMultiwayCut Model Implementation Plan + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add the MinimumMultiwayCut problem model — a graph partitioning problem that finds a minimum-weight edge set whose removal disconnects all terminal pairs. + +**Architecture:** New optimization model in `src/models/graph/` with edge-based binary variables (`dims = vec![2; num_edges]`), a feasibility check via BFS connectivity, and `Direction::Minimize`. Solved via existing BruteForce solver. + +**Tech Stack:** Rust, serde, inventory crate for schema registration + +**Issue:** #184 + +--- + +## Task 1: Implement the model struct and Problem trait + +**Files:** +- Create: `src/models/graph/minimum_multiway_cut.rs` + +### Design Notes + +**Configuration space:** Unlike most graph problems (vertex-based), this problem uses **edge-based binary variables**: `dims() = vec![2; num_edges]`. Each variable `x_e ∈ {0, 1}` indicates whether edge `e` is removed (1) or kept (0). + +**Feasibility check:** A configuration is feasible iff removing the cut edges disconnects every pair of terminals. Implementation: build adjacency list from non-cut edges, run BFS/DFS from first terminal, check that no other terminal is reachable. Repeat for all terminal components. + +**Complexity getters:** The best-known algorithm is `O(1.84^k * n^3)` (Cao, Chen & Fan 2013), so we need `num_terminals()`, `num_vertices()`, and `num_edges()` getters. + +- [ ] **Step 1: Create the model file with struct, inventory, and inherent methods** + +```rust +// src/models/graph/minimum_multiway_cut.rs + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::topology::{Graph, SimpleGraph}; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::{Direction, SolutionSize, WeightElement}; +use num_traits::Zero; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "MinimumMultiwayCut", + module_path: module_path!(), + description: "Find minimum weight set of edges whose removal disconnects all terminal pairs", + fields: &[ + FieldInfo { name: "graph", type_name: "G", description: "The undirected graph G=(V,E)" }, + FieldInfo { name: "terminals", type_name: "Vec", description: "Terminal vertices that must be separated" }, + FieldInfo { name: "edge_weights", type_name: "Vec", description: "Edge weights w: E -> R (same order as graph.edges())" }, + ], + } +} + +/// The Minimum Multiway Cut problem. +/// +/// Given an undirected weighted graph G = (V, E, w) and a set of k terminal +/// vertices T = {t_1, ..., t_k}, find a minimum-weight set of edges C ⊆ E +/// such that no two terminals remain in the same connected component of +/// G' = (V, E \ C). +/// +/// # Representation +/// +/// Each edge is assigned a binary variable: +/// - 0: edge is kept +/// - 1: edge is removed (in the cut) +/// +/// A configuration is feasible if removing the cut edges disconnects all +/// terminal pairs. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MinimumMultiwayCut { + graph: G, + terminals: Vec, + edge_weights: Vec, +} + +impl MinimumMultiwayCut { + /// Create a MinimumMultiwayCut problem. + /// + /// # Panics + /// - If `edge_weights.len() != graph.num_edges()` + /// - If `terminals.len() < 2` + /// - If any terminal index is out of bounds + pub fn new(graph: G, terminals: Vec, edge_weights: Vec) -> Self { + assert_eq!( + edge_weights.len(), + graph.num_edges(), + "edge_weights length must match num_edges" + ); + assert!(terminals.len() >= 2, "need at least 2 terminals"); + // Check for duplicate terminals + let mut sorted = terminals.clone(); + sorted.sort(); + sorted.dedup(); + assert_eq!(sorted.len(), terminals.len(), "duplicate terminal indices"); + for &t in &terminals { + assert!(t < graph.num_vertices(), "terminal index out of bounds"); + } + Self { + graph, + terminals, + edge_weights, + } + } + + /// Get a reference to the underlying graph. + pub fn graph(&self) -> &G { + &self.graph + } + + /// Get the terminal vertices. + pub fn terminals(&self) -> &[usize] { + &self.terminals + } + + /// Get the edge weights. + pub fn edge_weights(&self) -> &[W] { + &self.edge_weights + } +} + +impl MinimumMultiwayCut { + /// Number of vertices in the graph. + pub fn num_vertices(&self) -> usize { + self.graph.num_vertices() + } + + /// Number of edges in the graph. + pub fn num_edges(&self) -> usize { + self.graph.num_edges() + } + + /// Number of terminal vertices. + pub fn num_terminals(&self) -> usize { + self.terminals.len() + } +} +``` + +- [ ] **Step 2: Add the feasibility check helper** + +```rust +/// Check if all terminals are in distinct connected components +/// when edges marked as cut (config[e] == 1) are removed. +fn terminals_separated( + graph: &G, + terminals: &[usize], + config: &[usize], +) -> bool { + let n = graph.num_vertices(); + let edges = graph.edges(); + + // Build adjacency list from non-cut edges + let mut adj: Vec> = vec![vec![]; n]; + for (idx, &(u, v)) in edges.iter().enumerate() { + if config[idx] == 0 { + adj[u].push(v); + adj[v].push(u); + } + } + + // Find connected component of each terminal via BFS + let mut component = vec![usize::MAX; n]; + let mut comp_id = 0; + for &t in terminals { + if component[t] != usize::MAX { + // Terminal already reached from a previous terminal's BFS + // => two terminals share a component => infeasible + return false; + } + // BFS from terminal t + let mut queue = std::collections::VecDeque::new(); + queue.push_back(t); + component[t] = comp_id; + while let Some(u) = queue.pop_front() { + for &v in &adj[u] { + if component[v] == usize::MAX { + component[v] = comp_id; + queue.push_back(v); + } + } + } + comp_id += 1; + } + true +} +``` + +- [ ] **Step 3: Implement Problem and OptimizationProblem traits** + +```rust +impl Problem for MinimumMultiwayCut +where + G: Graph + crate::variant::VariantParam, + W: WeightElement + crate::variant::VariantParam, +{ + const NAME: &'static str = "MinimumMultiwayCut"; + type Metric = SolutionSize; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![G, W] + } + + fn dims(&self) -> Vec { + vec![2; self.graph.num_edges()] + } + + fn evaluate(&self, config: &[usize]) -> SolutionSize { + // Check feasibility: all terminals must be in distinct components + if !terminals_separated(&self.graph, &self.terminals, config) { + return SolutionSize::Invalid; + } + // Sum weights of cut edges + let mut total = W::Sum::zero(); + for (idx, &selected) in config.iter().enumerate() { + if selected == 1 { + total += self.edge_weights[idx].to_sum(); + } + } + SolutionSize::Valid(total) + } +} + +impl OptimizationProblem for MinimumMultiwayCut +where + G: Graph + crate::variant::VariantParam, + W: WeightElement + crate::variant::VariantParam, +{ + type Value = W::Sum; + + fn direction(&self) -> Direction { + Direction::Minimize + } +} +``` + +- [ ] **Step 4: Add `declare_variants!` and test link** + +```rust +crate::declare_variants! { + MinimumMultiwayCut => "1.84^num_terminals * num_vertices^3", +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/minimum_multiway_cut.rs"] +mod tests; +``` + +- [ ] **Step 5: Verify the file compiles (no tests yet)** + +Run: `cargo check 2>&1 | head -30` +Expected: Compilation errors about missing module registration (fixed in Task 2) + +--- + +## Task 2: Register the model in module system and CLI + +**Files:** +- Modify: `src/models/graph/mod.rs` +- Modify: `src/models/mod.rs` +- Modify: `src/lib.rs` +- Modify: `problemreductions-cli/src/dispatch.rs` +- Modify: `problemreductions-cli/src/problem_name.rs` + +- [ ] **Step 1: Register in `src/models/graph/mod.rs`** + +Add to the module declarations (alphabetically) and update the module doc comment: +```rust +//! - [`MinimumMultiwayCut`]: Minimum weight multiway cut +``` + +```rust +pub(crate) mod minimum_multiway_cut; +``` + +Add to the re-exports: +```rust +pub use minimum_multiway_cut::MinimumMultiwayCut; +``` + +- [ ] **Step 2: Register in `src/models/mod.rs`** + +Add `MinimumMultiwayCut` to the `graph` re-export line: +```rust +pub use graph::{ + BicliqueCover, KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, + MaximumMatching, MinimumDominatingSet, MinimumMultiwayCut, MinimumVertexCover, SpinGlass, + TravelingSalesman, +}; +``` + +- [ ] **Step 3: Register in prelude (`src/lib.rs`)** + +Add `MinimumMultiwayCut` to the prelude graph imports (line ~42-45): +```rust +pub use crate::models::graph::{ + KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, + MinimumDominatingSet, MinimumMultiwayCut, MinimumVertexCover, TravelingSalesman, +}; +``` + +- [ ] **Step 4: Add CLI dispatch in `problemreductions-cli/src/dispatch.rs`** + +Add import at the top (if needed) and match arms in both `load_problem()` and `serialize_any_problem()`: + +```rust +// In load_problem(): +"MinimumMultiwayCut" => deser_opt::>(data), + +// In serialize_any_problem(): +"MinimumMultiwayCut" => try_ser::>(any), +``` + +- [ ] **Step 4: Add CLI alias in `problemreductions-cli/src/problem_name.rs`** + +```rust +// In resolve_alias(): +"minimummultiwaycut" | "mmc" => "MinimumMultiwayCut".to_string(), +``` + +- [ ] **Step 6: Verify compilation** + +Run: `cargo check` +Expected: PASS (no errors) + +--- + +## Task 3: Write unit tests + +**Files:** +- Create: `src/unit_tests/models/graph/minimum_multiway_cut.rs` + +- [ ] **Step 1: Write the test file** + +```rust +use super::*; +use crate::solvers::BruteForce; +use crate::topology::SimpleGraph; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::{Direction, SolutionSize}; + +#[test] +fn test_minimummultiwaycut_creation() { + // 5 vertices, 6 edges, 3 terminals + let graph = SimpleGraph::new( + 5, + vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)], + ); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); + assert_eq!(problem.dims().len(), 6); // 6 edges + assert_eq!(problem.num_vertices(), 5); + assert_eq!(problem.num_edges(), 6); + assert_eq!(problem.num_terminals(), 3); +} + +#[test] +fn test_minimummultiwaycut_evaluate_valid() { + // Issue example: 5 vertices, terminals {0,2,4} + // Edges: (0,1)w=2, (1,2)w=3, (2,3)w=1, (3,4)w=2, (0,4)w=4, (1,3)w=5 + let graph = SimpleGraph::new( + 5, + vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)], + ); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); + + // Optimal cut: remove edges (0,1), (0,4), (3,4) => indices 0, 4, 3 + // config: [1, 0, 0, 1, 1, 0] => weight 2 + 2 + 4 = 8 + let config = vec![1, 0, 0, 1, 1, 0]; + let result = problem.evaluate(&config); + assert_eq!(result, SolutionSize::Valid(8)); +} + +#[test] +fn test_minimummultiwaycut_evaluate_invalid() { + let graph = SimpleGraph::new( + 5, + vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)], + ); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); + + // No edges cut: all terminals connected => invalid + let config = vec![0, 0, 0, 0, 0, 0]; + let result = problem.evaluate(&config); + assert_eq!(result, SolutionSize::Invalid); +} + +#[test] +fn test_minimummultiwaycut_direction() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![1i32, 1]); + assert_eq!(problem.direction(), Direction::Minimize); +} + +#[test] +fn test_minimummultiwaycut_brute_force() { + // Issue example: optimal cut has weight 8 + let graph = SimpleGraph::new( + 5, + vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)], + ); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); + + let solver = BruteForce::new(); + let solutions = solver.find_all_best(&problem); + assert!(!solutions.is_empty()); + for sol in &solutions { + let val = problem.evaluate(sol); + assert_eq!(val, SolutionSize::Valid(8)); + } +} + +#[test] +fn test_minimummultiwaycut_two_terminals() { + // k=2: classical min s-t cut. Path graph: 0-1-2, terminals {0,2} + // Edges: (0,1)w=3, (1,2)w=5 + // Min cut: remove (0,1) with weight 3 + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![3i32, 5]); + + let solver = BruteForce::new(); + let solutions = solver.find_all_best(&problem); + for sol in &solutions { + assert_eq!(problem.evaluate(sol), SolutionSize::Valid(3)); + } +} + +#[test] +fn test_minimummultiwaycut_all_edges_cut() { + // Cutting all edges should always be valid (trivially separates everything) + let graph = SimpleGraph::new( + 5, + vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)], + ); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); + let config = vec![1, 1, 1, 1, 1, 1]; // cut all edges + let result = problem.evaluate(&config); + assert_eq!(result, SolutionSize::Valid(2 + 3 + 1 + 2 + 4 + 5)); // sum = 17 +} + +#[test] +fn test_minimummultiwaycut_already_disconnected() { + // Terminals already in different components => empty cut is valid + // Graph: 0-1 2-3, terminals {0, 2} + let graph = SimpleGraph::new(4, vec![(0, 1), (2, 3)]); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![1i32, 1]); + let config = vec![0, 0]; // no edges cut + let result = problem.evaluate(&config); + assert_eq!(result, SolutionSize::Valid(0)); + + // BruteForce should find the empty cut as optimal + let solver = BruteForce::new(); + let solutions = solver.find_all_best(&problem); + for sol in &solutions { + assert_eq!(problem.evaluate(sol), SolutionSize::Valid(0)); + } +} + +#[test] +fn test_minimummultiwaycut_serialization() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![1i32, 2]); + let json = serde_json::to_string(&problem).unwrap(); + let restored: MinimumMultiwayCut = serde_json::from_str(&json).unwrap(); + assert_eq!(restored.num_vertices(), 3); + assert_eq!(restored.num_edges(), 2); + assert_eq!(restored.terminals(), &[0, 2]); +} +``` + +- [ ] **Step 2: Run tests** + +Run: `cargo test minimum_multiway_cut --lib -- --nocapture` +Expected: All tests PASS + +- [ ] **Step 3: Commit model + tests + registration** + +```bash +git add src/models/graph/minimum_multiway_cut.rs \ + src/unit_tests/models/graph/minimum_multiway_cut.rs \ + src/models/graph/mod.rs \ + src/models/mod.rs \ + problemreductions-cli/src/dispatch.rs \ + problemreductions-cli/src/problem_name.rs +git commit -m "feat: add MinimumMultiwayCut model (#184)" +``` + +--- + +## Task 4: Write example program + +**Files:** +- Create: `examples/minimummultiwaycut.rs` +- Modify: `tests/suites/examples.rs` + +- [ ] **Step 1: Write the example program** + +Use the issue's worked example (5 vertices, 3 terminals, optimal cut weight 8). + +```rust +// examples/minimummultiwaycut.rs +// MinimumMultiwayCut example: find minimum weight edge cut separating terminals. + +use problemreductions::models::graph::MinimumMultiwayCut; +use problemreductions::topology::SimpleGraph; +use problemreductions::{BruteForce, Problem, Solver}; + +pub fn run() { + // 5 vertices, terminals {0, 2, 4} + // Edges with weights: (0,1)=2, (1,2)=3, (2,3)=1, (3,4)=2, (0,4)=4, (1,3)=5 + let graph = SimpleGraph::new( + 5, + vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)], + ); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); + + let solver = BruteForce::new(); + let best = solver.find_best(&problem).expect("should find a solution"); + let value = problem.evaluate(&best); + + println!("Optimal multiway cut: {:?}", best); + println!("Cut weight: {:?}", value); + + // Export as JSON + let json = serde_json::json!({ + "problem": problem, + "solution": best, + "objective": 8, + }); + println!("{}", serde_json::to_string_pretty(&json).unwrap()); +} + +fn main() { + run(); +} +``` + +- [ ] **Step 2: Register in `tests/suites/examples.rs`** + +Add `example_test!` and `example_fn!` entries: +```rust +example_test!(minimummultiwaycut); +// ...in the test list: +example_fn!(test_minimummultiwaycut, minimummultiwaycut); +``` + +- [ ] **Step 3: Run the example** + +Run: `cargo run --example minimummultiwaycut` +Expected: Prints optimal cut with weight 8 + +- [ ] **Step 4: Run example test** + +Run: `cargo test test_minimummultiwaycut --test main` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add examples/minimummultiwaycut.rs tests/suites/examples.rs +git commit -m "feat: add MinimumMultiwayCut example (#184)" +``` + +--- + +## Task 5: Regenerate exports and run full checks + +- [ ] **Step 1: Regenerate reduction graph and schemas** + +```bash +cargo run --example export_graph +cargo run --example export_schemas +``` + +- [ ] **Step 2: Run full check suite** + +```bash +make check # fmt + clippy + test +``` +Expected: All pass + +- [ ] **Step 3: Commit any generated file changes** + +```bash +git add docs/data/reduction_graph.json docs/data/problem_schemas.json +git commit -m "chore: regenerate exports for MinimumMultiwayCut (#184)" +``` + +--- + +## Task 6: Document in paper + +Invoke `/write-model-in-paper` to add the problem definition entry in `docs/paper/reductions.typ`. + +Key content to include: +- **Formal definition:** Given G=(V,E,w) and terminals T={t_1,...,t_k}, find minimum-weight C⊆E separating all terminals +- **Background:** Generalizes min s-t cut (k=2, polynomial) to k≥3 (NP-hard). Important in VLSI, image segmentation. (2−2/k)-approximation exists. +- **Example:** The issue's 5-vertex instance with CeTZ visualization showing the graph, terminals highlighted, and cut edges +- **Algorithm:** Cao, Chen & Fan (2013), O*(1.84^k) + +- [ ] **Step 1: Run `/write-model-in-paper`** +- [ ] **Step 2: Commit paper changes** + +--- + +## Task 7: Final verification + +- [ ] **Step 1: Run full test suite** + +```bash +make test clippy +``` + +- [ ] **Step 2: Run `/review-implementation` to verify all structural and semantic checks pass** From b4c758939dbb00e58cc79f3941b8936d38c52e23 Mon Sep 17 00:00:00 2001 From: Huai-Ming Yu Date: Tue, 10 Mar 2026 16:58:25 +0800 Subject: [PATCH 02/13] feat: add MinimumMultiwayCut model (#184) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the Minimum Multiway Cut problem — find minimum-weight edge set whose removal disconnects all terminal pairs. Edge-based binary variables with BFS feasibility check. Solved via BruteForce. - Model struct with Problem/OptimizationProblem traits - CLI dispatch and alias (mmc) - 10 unit tests + example program - Regenerated schemas and reduction graph Co-authored-by: Claude --- docs/src/reductions/problem_schemas.json | 21 ++ docs/src/reductions/reduction_graph.json | 76 ++++--- examples/minimummultiwaycut.rs | 31 +++ problemreductions-cli/src/dispatch.rs | 2 + problemreductions-cli/src/problem_name.rs | 2 + src/lib.rs | 2 +- src/models/graph/minimum_multiway_cut.rs | 196 ++++++++++++++++++ src/models/graph/mod.rs | 3 + src/models/mod.rs | 3 +- .../models/graph/minimum_multiway_cut.rs | 122 +++++++++++ tests/suites/examples.rs | 2 + 11 files changed, 425 insertions(+), 35 deletions(-) create mode 100644 examples/minimummultiwaycut.rs create mode 100644 src/models/graph/minimum_multiway_cut.rs create mode 100644 src/unit_tests/models/graph/minimum_multiway_cut.rs diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 15eafb74c..8929021d6 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -301,6 +301,27 @@ } ] }, + { + "name": "MinimumMultiwayCut", + "description": "Find minimum weight set of edges whose removal disconnects all terminal pairs", + "fields": [ + { + "name": "graph", + "type_name": "G", + "description": "The undirected graph G=(V,E)" + }, + { + "name": "terminals", + "type_name": "Vec", + "description": "Terminal vertices that must be separated" + }, + { + "name": "edge_weights", + "type_name": "Vec", + "description": "Edge weights w: E -> R (same order as graph.edges())" + } + ] + }, { "name": "MinimumSetCovering", "description": "Find minimum weight collection covering the universe", diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index 3e73b9c15..17fe772e7 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -302,6 +302,16 @@ "doc_path": "models/graph/struct.MinimumDominatingSet.html", "complexity": "1.4969^num_vertices" }, + { + "name": "MinimumMultiwayCut", + "variant": { + "graph": "SimpleGraph", + "weight": "i32" + }, + "category": "graph", + "doc_path": "models/graph/struct.MinimumMultiwayCut.html", + "complexity": "1.84^num_terminals * num_vertices^3" + }, { "name": "MinimumSetCovering", "variant": { @@ -393,7 +403,7 @@ }, { "source": 4, - "target": 39, + "target": 40, "overhead": [ { "field": "num_spins", @@ -438,7 +448,7 @@ }, { "source": 8, - "target": 36, + "target": 37, "overhead": [ { "field": "num_vars", @@ -479,7 +489,7 @@ }, { "source": 13, - "target": 36, + "target": 37, "overhead": [ { "field": "num_vars", @@ -505,7 +515,7 @@ }, { "source": 14, - "target": 36, + "target": 37, "overhead": [ { "field": "num_vars", @@ -516,7 +526,7 @@ }, { "source": 14, - "target": 37, + "target": 38, "overhead": [ { "field": "num_clauses", @@ -550,7 +560,7 @@ }, { "source": 15, - "target": 36, + "target": 37, "overhead": [ { "field": "num_vars", @@ -561,7 +571,7 @@ }, { "source": 15, - "target": 37, + "target": 38, "overhead": [ { "field": "num_clauses", @@ -580,7 +590,7 @@ }, { "source": 16, - "target": 37, + "target": 38, "overhead": [ { "field": "num_clauses", @@ -599,7 +609,7 @@ }, { "source": 18, - "target": 39, + "target": 40, "overhead": [ { "field": "num_spins", @@ -779,7 +789,7 @@ }, { "source": 24, - "target": 34, + "target": 35, "overhead": [ { "field": "num_vertices", @@ -794,7 +804,7 @@ }, { "source": 24, - "target": 36, + "target": 37, "overhead": [ { "field": "num_vars", @@ -925,7 +935,7 @@ }, { "source": 30, - "target": 36, + "target": 37, "overhead": [ { "field": "num_vars", @@ -995,7 +1005,7 @@ "doc_path": "rules/minimumdominatingset_ilp/index.html" }, { - "source": 33, + "source": 34, "target": 8, "overhead": [ { @@ -1010,7 +1020,7 @@ "doc_path": "rules/minimumsetcovering_ilp/index.html" }, { - "source": 34, + "source": 35, "target": 8, "overhead": [ { @@ -1025,7 +1035,7 @@ "doc_path": "rules/minimumvertexcover_ilp/index.html" }, { - "source": 34, + "source": 35, "target": 24, "overhead": [ { @@ -1040,8 +1050,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 34, - "target": 33, + "source": 35, + "target": 34, "overhead": [ { "field": "num_sets", @@ -1055,8 +1065,8 @@ "doc_path": "rules/minimumvertexcover_minimumsetcovering/index.html" }, { - "source": 34, - "target": 36, + "source": 35, + "target": 37, "overhead": [ { "field": "num_vars", @@ -1066,7 +1076,7 @@ "doc_path": "rules/minimumvertexcover_qubo/index.html" }, { - "source": 36, + "source": 37, "target": 8, "overhead": [ { @@ -1081,8 +1091,8 @@ "doc_path": "rules/qubo_ilp/index.html" }, { - "source": 36, - "target": 38, + "source": 37, + "target": 39, "overhead": [ { "field": "num_spins", @@ -1092,7 +1102,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 37, + "source": 38, "target": 4, "overhead": [ { @@ -1107,7 +1117,7 @@ "doc_path": "rules/sat_circuitsat/index.html" }, { - "source": 37, + "source": 38, "target": 10, "overhead": [ { @@ -1122,7 +1132,7 @@ "doc_path": "rules/sat_coloring/index.html" }, { - "source": 37, + "source": 38, "target": 15, "overhead": [ { @@ -1137,7 +1147,7 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 37, + "source": 38, "target": 23, "overhead": [ { @@ -1152,7 +1162,7 @@ "doc_path": "rules/sat_maximumindependentset/index.html" }, { - "source": 37, + "source": 38, "target": 32, "overhead": [ { @@ -1167,8 +1177,8 @@ "doc_path": "rules/sat_minimumdominatingset/index.html" }, { - "source": 38, - "target": 36, + "source": 39, + "target": 37, "overhead": [ { "field": "num_vars", @@ -1178,7 +1188,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 39, + "source": 40, "target": 18, "overhead": [ { @@ -1193,8 +1203,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 39, - "target": 38, + "source": 40, + "target": 39, "overhead": [ { "field": "num_spins", @@ -1208,7 +1218,7 @@ "doc_path": "rules/spinglass_casts/index.html" }, { - "source": 40, + "source": 41, "target": 8, "overhead": [ { diff --git a/examples/minimummultiwaycut.rs b/examples/minimummultiwaycut.rs new file mode 100644 index 000000000..59f4022de --- /dev/null +++ b/examples/minimummultiwaycut.rs @@ -0,0 +1,31 @@ +// MinimumMultiwayCut example: find minimum weight edge cut separating terminals. + +use problemreductions::models::graph::MinimumMultiwayCut; +use problemreductions::topology::SimpleGraph; +use problemreductions::{BruteForce, Problem, Solver}; + +pub fn run() { + // 5 vertices, terminals {0, 2, 4} + // Edges with weights: (0,1)=2, (1,2)=3, (2,3)=1, (3,4)=2, (0,4)=4, (1,3)=5 + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); + + let solver = BruteForce::new(); + let best = solver.find_best(&problem).expect("should find a solution"); + let value = problem.evaluate(&best); + + println!("Optimal multiway cut: {:?}", best); + println!("Cut weight: {:?}", value); + + // Export as JSON + let json = serde_json::json!({ + "problem": problem, + "solution": best, + "objective": 8, + }); + println!("{}", serde_json::to_string_pretty(&json).unwrap()); +} + +fn main() { + run(); +} diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 7a8498421..5840f6a6e 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -210,6 +210,7 @@ pub fn load_problem( "MaximumClique" => deser_opt::>(data), "MaximumMatching" => deser_opt::>(data), "MinimumDominatingSet" => deser_opt::>(data), + "MinimumMultiwayCut" => deser_opt::>(data), "MaxCut" => deser_opt::>(data), "MaximalIS" => deser_opt::>(data), "TravelingSalesman" => deser_opt::>(data), @@ -267,6 +268,7 @@ pub fn serialize_any_problem( "MaximumClique" => try_ser::>(any), "MaximumMatching" => try_ser::>(any), "MinimumDominatingSet" => try_ser::>(any), + "MinimumMultiwayCut" => try_ser::>(any), "MaxCut" => try_ser::>(any), "MaximalIS" => try_ser::>(any), "TravelingSalesman" => try_ser::>(any), diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index acd9b4b59..a3e55f4d4 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -21,6 +21,7 @@ pub const ALIASES: &[(&str, &str)] = &[ ("TSP", "TravelingSalesman"), ("CVP", "ClosestVectorProblem"), ("MaxMatching", "MaximumMatching"), + ("MMC", "MinimumMultiwayCut"), ]; /// Resolve a short alias to the canonical problem name. @@ -41,6 +42,7 @@ pub fn resolve_alias(input: &str) -> String { "maximumclique" => "MaximumClique".to_string(), "maxmatching" | "maximummatching" => "MaximumMatching".to_string(), "minimumdominatingset" => "MinimumDominatingSet".to_string(), + "minimummultiwaycut" | "mmc" => "MinimumMultiwayCut".to_string(), "minimumsetcovering" => "MinimumSetCovering".to_string(), "maximumsetpacking" => "MaximumSetPacking".to_string(), "kcoloring" => "KColoring".to_string(), diff --git a/src/lib.rs b/src/lib.rs index c9ada7ef1..b6a0e7ae5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,7 +41,7 @@ pub mod prelude { pub use crate::models::graph::{BicliqueCover, SpinGlass}; pub use crate::models::graph::{ KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, - MinimumDominatingSet, MinimumVertexCover, TravelingSalesman, + MinimumDominatingSet, MinimumMultiwayCut, MinimumVertexCover, TravelingSalesman, }; pub use crate::models::misc::{BinPacking, Factoring, Knapsack, PaintShop}; pub use crate::models::set::{MaximumSetPacking, MinimumSetCovering}; diff --git a/src/models/graph/minimum_multiway_cut.rs b/src/models/graph/minimum_multiway_cut.rs new file mode 100644 index 000000000..2e60f058a --- /dev/null +++ b/src/models/graph/minimum_multiway_cut.rs @@ -0,0 +1,196 @@ +//! Minimum Multiway Cut problem implementation. +//! +//! The Minimum Multiway Cut problem asks for a minimum weight set of edges +//! whose removal disconnects all terminal pairs. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::topology::{Graph, SimpleGraph}; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::{Direction, SolutionSize, WeightElement}; +use num_traits::Zero; +use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; + +inventory::submit! { + ProblemSchemaEntry { + name: "MinimumMultiwayCut", + module_path: module_path!(), + description: "Find minimum weight set of edges whose removal disconnects all terminal pairs", + fields: &[ + FieldInfo { name: "graph", type_name: "G", description: "The undirected graph G=(V,E)" }, + FieldInfo { name: "terminals", type_name: "Vec", description: "Terminal vertices that must be separated" }, + FieldInfo { name: "edge_weights", type_name: "Vec", description: "Edge weights w: E -> R (same order as graph.edges())" }, + ], + } +} + +/// The Minimum Multiway Cut problem. +/// +/// Given an undirected weighted graph G = (V, E, w) and a set of k terminal +/// vertices T = {t_1, ..., t_k}, find a minimum-weight set of edges C ⊆ E +/// such that no two terminals remain in the same connected component of +/// G' = (V, E \ C). +/// +/// # Representation +/// +/// Each edge is assigned a binary variable: +/// - 0: edge is kept +/// - 1: edge is removed (in the cut) +/// +/// A configuration is feasible if removing the cut edges disconnects all +/// terminal pairs. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MinimumMultiwayCut { + graph: G, + terminals: Vec, + edge_weights: Vec, +} + +impl MinimumMultiwayCut { + /// Create a MinimumMultiwayCut problem. + /// + /// # Panics + /// - If `edge_weights.len() != graph.num_edges()` + /// - If `terminals.len() < 2` + /// - If any terminal index is out of bounds + /// - If there are duplicate terminal indices + pub fn new(graph: G, terminals: Vec, edge_weights: Vec) -> Self { + assert_eq!( + edge_weights.len(), + graph.num_edges(), + "edge_weights length must match num_edges" + ); + assert!(terminals.len() >= 2, "need at least 2 terminals"); + let mut sorted = terminals.clone(); + sorted.sort(); + sorted.dedup(); + assert_eq!(sorted.len(), terminals.len(), "duplicate terminal indices"); + for &t in &terminals { + assert!(t < graph.num_vertices(), "terminal index out of bounds"); + } + Self { + graph, + terminals, + edge_weights, + } + } + + /// Get a reference to the underlying graph. + pub fn graph(&self) -> &G { + &self.graph + } + + /// Get the terminal vertices. + pub fn terminals(&self) -> &[usize] { + &self.terminals + } + + /// Get the edge weights. + pub fn edge_weights(&self) -> &[W] { + &self.edge_weights + } +} + +impl MinimumMultiwayCut { + /// Number of vertices in the graph. + pub fn num_vertices(&self) -> usize { + self.graph.num_vertices() + } + + /// Number of edges in the graph. + pub fn num_edges(&self) -> usize { + self.graph.num_edges() + } + + /// Number of terminal vertices. + pub fn num_terminals(&self) -> usize { + self.terminals.len() + } +} + +/// Check if all terminals are in distinct connected components +/// when edges marked as cut (config[e] == 1) are removed. +fn terminals_separated(graph: &G, terminals: &[usize], config: &[usize]) -> bool { + let n = graph.num_vertices(); + let edges = graph.edges(); + + // Build adjacency list from non-cut edges + let mut adj: Vec> = vec![vec![]; n]; + for (idx, (u, v)) in edges.iter().enumerate() { + if config[idx] == 0 { + adj[*u].push(*v); + adj[*v].push(*u); + } + } + + // BFS from each terminal; if a terminal is already visited by a previous + // terminal's BFS, they share a component => infeasible. + let mut component = vec![usize::MAX; n]; + for (comp_id, &t) in terminals.iter().enumerate() { + if component[t] != usize::MAX { + return false; + } + let mut queue = VecDeque::new(); + queue.push_back(t); + component[t] = comp_id; + while let Some(u) = queue.pop_front() { + for &v in &adj[u] { + if component[v] == usize::MAX { + component[v] = comp_id; + queue.push_back(v); + } + } + } + } + true +} + +impl Problem for MinimumMultiwayCut +where + G: Graph + crate::variant::VariantParam, + W: WeightElement + crate::variant::VariantParam, +{ + const NAME: &'static str = "MinimumMultiwayCut"; + type Metric = SolutionSize; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![G, W] + } + + fn dims(&self) -> Vec { + vec![2; self.graph.num_edges()] + } + + fn evaluate(&self, config: &[usize]) -> SolutionSize { + if !terminals_separated(&self.graph, &self.terminals, config) { + return SolutionSize::Invalid; + } + let mut total = W::Sum::zero(); + for (idx, &selected) in config.iter().enumerate() { + if selected == 1 { + total += self.edge_weights[idx].to_sum(); + } + } + SolutionSize::Valid(total) + } +} + +impl OptimizationProblem for MinimumMultiwayCut +where + G: Graph + crate::variant::VariantParam, + W: WeightElement + crate::variant::VariantParam, +{ + type Value = W::Sum; + + fn direction(&self) -> Direction { + Direction::Minimize + } +} + +crate::declare_variants! { + MinimumMultiwayCut => "1.84^num_terminals * num_vertices^3", +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/minimum_multiway_cut.rs"] +mod tests; diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 1198f7fbc..f7c56aeb1 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -11,6 +11,7 @@ //! - [`MaximumMatching`]: Maximum weight matching //! - [`TravelingSalesman`]: Traveling Salesman (minimum weight Hamiltonian cycle) //! - [`SpinGlass`]: Ising model Hamiltonian +//! - [`MinimumMultiwayCut`]: Minimum weight multiway cut //! - [`BicliqueCover`]: Biclique cover on bipartite graphs pub(crate) mod biclique_cover; @@ -21,6 +22,7 @@ pub(crate) mod maximum_clique; pub(crate) mod maximum_independent_set; pub(crate) mod maximum_matching; pub(crate) mod minimum_dominating_set; +pub(crate) mod minimum_multiway_cut; pub(crate) mod minimum_vertex_cover; pub(crate) mod spin_glass; pub(crate) mod traveling_salesman; @@ -33,6 +35,7 @@ pub use maximum_clique::MaximumClique; pub use maximum_independent_set::MaximumIndependentSet; pub use maximum_matching::MaximumMatching; pub use minimum_dominating_set::MinimumDominatingSet; +pub use minimum_multiway_cut::MinimumMultiwayCut; pub use minimum_vertex_cover::MinimumVertexCover; pub use spin_glass::SpinGlass; pub use traveling_salesman::TravelingSalesman; diff --git a/src/models/mod.rs b/src/models/mod.rs index 96b4b79d1..fcf31153a 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -13,7 +13,8 @@ pub use algebraic::{ClosestVectorProblem, BMF, ILP, QUBO}; pub use formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability}; pub use graph::{ BicliqueCover, KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, - MaximumMatching, MinimumDominatingSet, MinimumVertexCover, SpinGlass, TravelingSalesman, + MaximumMatching, MinimumDominatingSet, MinimumMultiwayCut, MinimumVertexCover, SpinGlass, + TravelingSalesman, }; pub use misc::{BinPacking, Factoring, Knapsack, PaintShop}; pub use set::{MaximumSetPacking, MinimumSetCovering}; diff --git a/src/unit_tests/models/graph/minimum_multiway_cut.rs b/src/unit_tests/models/graph/minimum_multiway_cut.rs new file mode 100644 index 000000000..4fe8748b4 --- /dev/null +++ b/src/unit_tests/models/graph/minimum_multiway_cut.rs @@ -0,0 +1,122 @@ +use super::*; +use crate::solvers::BruteForce; +use crate::topology::SimpleGraph; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::{Direction, SolutionSize}; + +#[test] +fn test_minimummultiwaycut_creation() { + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); + assert_eq!(problem.dims().len(), 6); + assert_eq!(problem.num_vertices(), 5); + assert_eq!(problem.num_edges(), 6); + assert_eq!(problem.num_terminals(), 3); +} + +#[test] +fn test_minimummultiwaycut_evaluate_valid() { + // Issue example: 5 vertices, terminals {0,2,4} + // Edges: (0,1)w=2, (1,2)w=3, (2,3)w=1, (3,4)w=2, (0,4)w=4, (1,3)w=5 + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); + + // Optimal cut: remove edges (0,1), (3,4), (0,4) => indices 0, 3, 4 + // config: [1, 0, 0, 1, 1, 0] => weight 2 + 2 + 4 = 8 + let config = vec![1, 0, 0, 1, 1, 0]; + let result = problem.evaluate(&config); + assert_eq!(result, SolutionSize::Valid(8)); +} + +#[test] +fn test_minimummultiwaycut_evaluate_invalid() { + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); + + // No edges cut: all terminals connected => invalid + let config = vec![0, 0, 0, 0, 0, 0]; + let result = problem.evaluate(&config); + assert_eq!(result, SolutionSize::Invalid); +} + +#[test] +fn test_minimummultiwaycut_direction() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![1i32, 1]); + assert_eq!(problem.direction(), Direction::Minimize); +} + +#[test] +fn test_minimummultiwaycut_brute_force() { + // Issue example: optimal cut has weight 8 + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); + + let solver = BruteForce::new(); + let solutions = solver.find_all_best(&problem); + assert!(!solutions.is_empty()); + for sol in &solutions { + let val = problem.evaluate(sol); + assert_eq!(val, SolutionSize::Valid(8)); + } +} + +#[test] +fn test_minimummultiwaycut_two_terminals() { + // k=2: classical min s-t cut. Path graph: 0-1-2, terminals {0,2} + // Edges: (0,1)w=3, (1,2)w=5 + // Min cut: remove (0,1) with weight 3 + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![3i32, 5]); + + let solver = BruteForce::new(); + let solutions = solver.find_all_best(&problem); + for sol in &solutions { + assert_eq!(problem.evaluate(sol), SolutionSize::Valid(3)); + } +} + +#[test] +fn test_minimummultiwaycut_all_edges_cut() { + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); + let config = vec![1, 1, 1, 1, 1, 1]; + let result = problem.evaluate(&config); + assert_eq!(result, SolutionSize::Valid(2 + 3 + 1 + 2 + 4 + 5)); +} + +#[test] +fn test_minimummultiwaycut_already_disconnected() { + // Terminals already in different components => empty cut is valid + // Graph: 0-1 2-3, terminals {0, 2} + let graph = SimpleGraph::new(4, vec![(0, 1), (2, 3)]); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![1i32, 1]); + let config = vec![0, 0]; + let result = problem.evaluate(&config); + assert_eq!(result, SolutionSize::Valid(0)); + + let solver = BruteForce::new(); + let solutions = solver.find_all_best(&problem); + for sol in &solutions { + assert_eq!(problem.evaluate(sol), SolutionSize::Valid(0)); + } +} + +#[test] +fn test_minimummultiwaycut_serialization() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![1i32, 2]); + let json = serde_json::to_string(&problem).unwrap(); + let restored: MinimumMultiwayCut = serde_json::from_str(&json).unwrap(); + assert_eq!(restored.num_vertices(), 3); + assert_eq!(restored.num_edges(), 2); + assert_eq!(restored.terminals(), &[0, 2]); +} + +#[test] +fn test_minimummultiwaycut_name() { + assert_eq!( + as Problem>::NAME, + "MinimumMultiwayCut" + ); +} diff --git a/tests/suites/examples.rs b/tests/suites/examples.rs index 3c9ad8033..da9b1d30b 100644 --- a/tests/suites/examples.rs +++ b/tests/suites/examples.rs @@ -12,6 +12,7 @@ macro_rules! example_test { example_test!(chained_reduction_factoring_to_spinglass); example_test!(chained_reduction_ksat_to_mis); +example_test!(minimummultiwaycut); example_test!(reduction_circuitsat_to_ilp); example_test!(reduction_circuitsat_to_spinglass); example_test!(reduction_factoring_to_circuitsat); @@ -66,6 +67,7 @@ example_fn!( test_chained_reduction_ksat_to_mis, chained_reduction_ksat_to_mis ); +example_fn!(test_minimummultiwaycut, minimummultiwaycut); example_fn!(test_circuitsat_to_ilp, reduction_circuitsat_to_ilp); example_fn!( test_circuitsat_to_spinglass, From 4e3043721dc224201a52c0262fce26a7c453552d Mon Sep 17 00:00:00 2001 From: Huai-Ming Yu Date: Tue, 10 Mar 2026 18:09:40 +0800 Subject: [PATCH 03/13] fix: CLI support, paper entry, and QA for MinimumMultiwayCut (#184) - Add `pred create MinimumMultiwayCut` with --terminals, --edge-weights, --graph - Add problem-def entry in Typst paper with example figure - Improve `pred solve` error: suggest --solver brute-force when no ILP path - Remove spurious MMC alias (not a well-known abbreviation) - Regenerate reduction graph and schema exports - Delete implementation plan Co-authored-by: Claude --- docs/paper/reductions.typ | 35 + docs/paper/references.bib | 20 + docs/plans/2026-03-10-minimum-multiway-cut.md | 611 ------------------ problemreductions-cli/src/cli.rs | 4 + problemreductions-cli/src/commands/create.rs | 36 ++ problemreductions-cli/src/dispatch.rs | 10 +- problemreductions-cli/src/problem_name.rs | 3 +- 7 files changed, 104 insertions(+), 615 deletions(-) delete mode 100644 docs/plans/2026-03-10-minimum-multiway-cut.md diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 975d74cb0..3ede25b6d 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -52,6 +52,7 @@ "BicliqueCover": [Biclique Cover], "BinPacking": [Bin Packing], "ClosestVectorProblem": [Closest Vector Problem], + "MinimumMultiwayCut": [Minimum Multiway Cut], ) // Definition label: "def:" — each definition block must have a matching label @@ -456,6 +457,40 @@ One of the most intensely studied NP-hard problems, with applications in logisti caption: [Complete graph $K_4$ with weighted edges. The optimal tour $v_0 -> v_1 -> v_2 -> v_3 -> v_0$ (blue edges) has cost 6.], ) ] +#problem-def("MinimumMultiwayCut")[ + Given an undirected graph $G=(V,E)$ with edge weights $w: E -> RR_(>0)$ and a set of $k$ terminal vertices $T = {t_1, ..., t_k} subset.eq V$, find a minimum-weight set of edges $C subset.eq E$ such that no two terminals remain in the same connected component of $G' = (V, E backslash C)$. +][ +The Minimum Multiway Cut problem generalizes the classical minimum $s$-$t$ cut: for $k=2$ it reduces to max-flow and is solvable in polynomial time, but for $k >= 3$ on general graphs it becomes NP-hard @dahlhaus1994. The problem arises in VLSI design, image segmentation, and network design. A $(2 - 2 slash k)$-approximation is achievable in polynomial time by taking the union of the $k - 1$ cheapest isolating cuts @dahlhaus1994. The best known exact algorithm runs in $O^*(1.84^k)$ time (suppressing polynomial factors) via submodular functions on isolating cuts @cao2013. + +*Example.* Consider a graph with $n = 5$ vertices $V = {0, 1, 2, 3, 4}$, terminals $T = {0, 2, 4}$, and 6 edges with weights: $w(0,1) = 2$, $w(1,2) = 3$, $w(2,3) = 1$, $w(3,4) = 2$, $w(0,4) = 4$, $w(1,3) = 5$. The optimal multiway cut removes edges ${(0,1), (3,4), (0,4)}$ with total weight $2 + 2 + 4 = 8$, yielding connected components ${0}$, ${1, 2, 3}$, ${4}$ — each terminal in a distinct component. + +#figure({ + let verts = ((0, 0.8), (1.2, 1.5), (2.4, 0.8), (1.8, -0.2), (0.6, -0.2)) + let edges = ((0,1),(1,2),(2,3),(3,4),(0,4),(1,3)) + let weights = ("2", "3", "1", "2", "4", "5") + let cut-edges = (0, 3, 4) // indices of cut edges + let terminals = (0, 2, 4) + canvas(length: 1cm, { + for (idx, (u, v)) in edges.enumerate() { + let is-cut = cut-edges.any(c => c == idx) + g-edge(verts.at(u), verts.at(v), + stroke: if is-cut { (paint: red, thickness: 2pt, dash: "dashed") } else { 1pt + luma(120) }) + let mx = (verts.at(u).at(0) + verts.at(v).at(0)) / 2 + let my = (verts.at(u).at(1) + verts.at(v).at(1)) / 2 + let dy = if idx == 5 { 0.15 } else { 0 } + draw.content((mx, my + dy), text(7pt, fill: luma(80))[#weights.at(idx)]) + } + for (k, pos) in verts.enumerate() { + let is-terminal = terminals.any(t => t == k) + g-node(pos, name: "v" + str(k), + fill: if is-terminal { graph-colors.at(0) } else { luma(180) }, + label: text(fill: white)[$#k$]) + } + }) +}, +caption: [Minimum Multiway Cut with terminals ${0, 2, 4}$ (blue). Dashed red edges form the optimal cut (weight 8).], +) +] #problem-def("MaximumClique")[ Given $G = (V, E)$, find $K subset.eq V$ maximizing $|K|$ such that all pairs in $K$ are adjacent: $forall u, v in K: (u, v) in E$. Equivalent to MIS on the complement graph $overline(G)$. ][ diff --git a/docs/paper/references.bib b/docs/paper/references.bib index c3129a691..a02959cfa 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -387,3 +387,23 @@ @article{ibarra1975 year = {1975}, doi = {10.1145/321906.321909} } + +@article{dahlhaus1994, + author = {Elias Dahlhaus and David S. Johnson and Christos H. Papadimitriou and Paul D. Seymour and Mihalis Yannakakis}, + title = {The Complexity of Multiterminal Cuts}, + journal = {SIAM Journal on Computing}, + volume = {23}, + number = {4}, + pages = {864--894}, + year = {1994}, + doi = {10.1137/S0097539292225297} +} + +@inproceedings{cao2013, + author = {Yixin Cao and Jianer Chen and Jianxin Wang}, + title = {An Improved Fixed-Parameter Algorithm for the Minimum Weight Multiway Cut Problem}, + booktitle = {Fundamentals of Computation Theory (FCT 2013)}, + pages = {96--107}, + year = {2013}, + doi = {10.1007/978-3-642-40164-0_11} +} diff --git a/docs/plans/2026-03-10-minimum-multiway-cut.md b/docs/plans/2026-03-10-minimum-multiway-cut.md deleted file mode 100644 index 58a8937b5..000000000 --- a/docs/plans/2026-03-10-minimum-multiway-cut.md +++ /dev/null @@ -1,611 +0,0 @@ -# MinimumMultiwayCut Model Implementation Plan - -> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add the MinimumMultiwayCut problem model — a graph partitioning problem that finds a minimum-weight edge set whose removal disconnects all terminal pairs. - -**Architecture:** New optimization model in `src/models/graph/` with edge-based binary variables (`dims = vec![2; num_edges]`), a feasibility check via BFS connectivity, and `Direction::Minimize`. Solved via existing BruteForce solver. - -**Tech Stack:** Rust, serde, inventory crate for schema registration - -**Issue:** #184 - ---- - -## Task 1: Implement the model struct and Problem trait - -**Files:** -- Create: `src/models/graph/minimum_multiway_cut.rs` - -### Design Notes - -**Configuration space:** Unlike most graph problems (vertex-based), this problem uses **edge-based binary variables**: `dims() = vec![2; num_edges]`. Each variable `x_e ∈ {0, 1}` indicates whether edge `e` is removed (1) or kept (0). - -**Feasibility check:** A configuration is feasible iff removing the cut edges disconnects every pair of terminals. Implementation: build adjacency list from non-cut edges, run BFS/DFS from first terminal, check that no other terminal is reachable. Repeat for all terminal components. - -**Complexity getters:** The best-known algorithm is `O(1.84^k * n^3)` (Cao, Chen & Fan 2013), so we need `num_terminals()`, `num_vertices()`, and `num_edges()` getters. - -- [ ] **Step 1: Create the model file with struct, inventory, and inherent methods** - -```rust -// src/models/graph/minimum_multiway_cut.rs - -use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize, WeightElement}; -use num_traits::Zero; -use serde::{Deserialize, Serialize}; - -inventory::submit! { - ProblemSchemaEntry { - name: "MinimumMultiwayCut", - module_path: module_path!(), - description: "Find minimum weight set of edges whose removal disconnects all terminal pairs", - fields: &[ - FieldInfo { name: "graph", type_name: "G", description: "The undirected graph G=(V,E)" }, - FieldInfo { name: "terminals", type_name: "Vec", description: "Terminal vertices that must be separated" }, - FieldInfo { name: "edge_weights", type_name: "Vec", description: "Edge weights w: E -> R (same order as graph.edges())" }, - ], - } -} - -/// The Minimum Multiway Cut problem. -/// -/// Given an undirected weighted graph G = (V, E, w) and a set of k terminal -/// vertices T = {t_1, ..., t_k}, find a minimum-weight set of edges C ⊆ E -/// such that no two terminals remain in the same connected component of -/// G' = (V, E \ C). -/// -/// # Representation -/// -/// Each edge is assigned a binary variable: -/// - 0: edge is kept -/// - 1: edge is removed (in the cut) -/// -/// A configuration is feasible if removing the cut edges disconnects all -/// terminal pairs. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MinimumMultiwayCut { - graph: G, - terminals: Vec, - edge_weights: Vec, -} - -impl MinimumMultiwayCut { - /// Create a MinimumMultiwayCut problem. - /// - /// # Panics - /// - If `edge_weights.len() != graph.num_edges()` - /// - If `terminals.len() < 2` - /// - If any terminal index is out of bounds - pub fn new(graph: G, terminals: Vec, edge_weights: Vec) -> Self { - assert_eq!( - edge_weights.len(), - graph.num_edges(), - "edge_weights length must match num_edges" - ); - assert!(terminals.len() >= 2, "need at least 2 terminals"); - // Check for duplicate terminals - let mut sorted = terminals.clone(); - sorted.sort(); - sorted.dedup(); - assert_eq!(sorted.len(), terminals.len(), "duplicate terminal indices"); - for &t in &terminals { - assert!(t < graph.num_vertices(), "terminal index out of bounds"); - } - Self { - graph, - terminals, - edge_weights, - } - } - - /// Get a reference to the underlying graph. - pub fn graph(&self) -> &G { - &self.graph - } - - /// Get the terminal vertices. - pub fn terminals(&self) -> &[usize] { - &self.terminals - } - - /// Get the edge weights. - pub fn edge_weights(&self) -> &[W] { - &self.edge_weights - } -} - -impl MinimumMultiwayCut { - /// Number of vertices in the graph. - pub fn num_vertices(&self) -> usize { - self.graph.num_vertices() - } - - /// Number of edges in the graph. - pub fn num_edges(&self) -> usize { - self.graph.num_edges() - } - - /// Number of terminal vertices. - pub fn num_terminals(&self) -> usize { - self.terminals.len() - } -} -``` - -- [ ] **Step 2: Add the feasibility check helper** - -```rust -/// Check if all terminals are in distinct connected components -/// when edges marked as cut (config[e] == 1) are removed. -fn terminals_separated( - graph: &G, - terminals: &[usize], - config: &[usize], -) -> bool { - let n = graph.num_vertices(); - let edges = graph.edges(); - - // Build adjacency list from non-cut edges - let mut adj: Vec> = vec![vec![]; n]; - for (idx, &(u, v)) in edges.iter().enumerate() { - if config[idx] == 0 { - adj[u].push(v); - adj[v].push(u); - } - } - - // Find connected component of each terminal via BFS - let mut component = vec![usize::MAX; n]; - let mut comp_id = 0; - for &t in terminals { - if component[t] != usize::MAX { - // Terminal already reached from a previous terminal's BFS - // => two terminals share a component => infeasible - return false; - } - // BFS from terminal t - let mut queue = std::collections::VecDeque::new(); - queue.push_back(t); - component[t] = comp_id; - while let Some(u) = queue.pop_front() { - for &v in &adj[u] { - if component[v] == usize::MAX { - component[v] = comp_id; - queue.push_back(v); - } - } - } - comp_id += 1; - } - true -} -``` - -- [ ] **Step 3: Implement Problem and OptimizationProblem traits** - -```rust -impl Problem for MinimumMultiwayCut -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - const NAME: &'static str = "MinimumMultiwayCut"; - type Metric = SolutionSize; - - fn variant() -> Vec<(&'static str, &'static str)> { - crate::variant_params![G, W] - } - - fn dims(&self) -> Vec { - vec![2; self.graph.num_edges()] - } - - fn evaluate(&self, config: &[usize]) -> SolutionSize { - // Check feasibility: all terminals must be in distinct components - if !terminals_separated(&self.graph, &self.terminals, config) { - return SolutionSize::Invalid; - } - // Sum weights of cut edges - let mut total = W::Sum::zero(); - for (idx, &selected) in config.iter().enumerate() { - if selected == 1 { - total += self.edge_weights[idx].to_sum(); - } - } - SolutionSize::Valid(total) - } -} - -impl OptimizationProblem for MinimumMultiwayCut -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - type Value = W::Sum; - - fn direction(&self) -> Direction { - Direction::Minimize - } -} -``` - -- [ ] **Step 4: Add `declare_variants!` and test link** - -```rust -crate::declare_variants! { - MinimumMultiwayCut => "1.84^num_terminals * num_vertices^3", -} - -#[cfg(test)] -#[path = "../../unit_tests/models/graph/minimum_multiway_cut.rs"] -mod tests; -``` - -- [ ] **Step 5: Verify the file compiles (no tests yet)** - -Run: `cargo check 2>&1 | head -30` -Expected: Compilation errors about missing module registration (fixed in Task 2) - ---- - -## Task 2: Register the model in module system and CLI - -**Files:** -- Modify: `src/models/graph/mod.rs` -- Modify: `src/models/mod.rs` -- Modify: `src/lib.rs` -- Modify: `problemreductions-cli/src/dispatch.rs` -- Modify: `problemreductions-cli/src/problem_name.rs` - -- [ ] **Step 1: Register in `src/models/graph/mod.rs`** - -Add to the module declarations (alphabetically) and update the module doc comment: -```rust -//! - [`MinimumMultiwayCut`]: Minimum weight multiway cut -``` - -```rust -pub(crate) mod minimum_multiway_cut; -``` - -Add to the re-exports: -```rust -pub use minimum_multiway_cut::MinimumMultiwayCut; -``` - -- [ ] **Step 2: Register in `src/models/mod.rs`** - -Add `MinimumMultiwayCut` to the `graph` re-export line: -```rust -pub use graph::{ - BicliqueCover, KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, - MaximumMatching, MinimumDominatingSet, MinimumMultiwayCut, MinimumVertexCover, SpinGlass, - TravelingSalesman, -}; -``` - -- [ ] **Step 3: Register in prelude (`src/lib.rs`)** - -Add `MinimumMultiwayCut` to the prelude graph imports (line ~42-45): -```rust -pub use crate::models::graph::{ - KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, - MinimumDominatingSet, MinimumMultiwayCut, MinimumVertexCover, TravelingSalesman, -}; -``` - -- [ ] **Step 4: Add CLI dispatch in `problemreductions-cli/src/dispatch.rs`** - -Add import at the top (if needed) and match arms in both `load_problem()` and `serialize_any_problem()`: - -```rust -// In load_problem(): -"MinimumMultiwayCut" => deser_opt::>(data), - -// In serialize_any_problem(): -"MinimumMultiwayCut" => try_ser::>(any), -``` - -- [ ] **Step 4: Add CLI alias in `problemreductions-cli/src/problem_name.rs`** - -```rust -// In resolve_alias(): -"minimummultiwaycut" | "mmc" => "MinimumMultiwayCut".to_string(), -``` - -- [ ] **Step 6: Verify compilation** - -Run: `cargo check` -Expected: PASS (no errors) - ---- - -## Task 3: Write unit tests - -**Files:** -- Create: `src/unit_tests/models/graph/minimum_multiway_cut.rs` - -- [ ] **Step 1: Write the test file** - -```rust -use super::*; -use crate::solvers::BruteForce; -use crate::topology::SimpleGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; - -#[test] -fn test_minimummultiwaycut_creation() { - // 5 vertices, 6 edges, 3 terminals - let graph = SimpleGraph::new( - 5, - vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)], - ); - let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); - assert_eq!(problem.dims().len(), 6); // 6 edges - assert_eq!(problem.num_vertices(), 5); - assert_eq!(problem.num_edges(), 6); - assert_eq!(problem.num_terminals(), 3); -} - -#[test] -fn test_minimummultiwaycut_evaluate_valid() { - // Issue example: 5 vertices, terminals {0,2,4} - // Edges: (0,1)w=2, (1,2)w=3, (2,3)w=1, (3,4)w=2, (0,4)w=4, (1,3)w=5 - let graph = SimpleGraph::new( - 5, - vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)], - ); - let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); - - // Optimal cut: remove edges (0,1), (0,4), (3,4) => indices 0, 4, 3 - // config: [1, 0, 0, 1, 1, 0] => weight 2 + 2 + 4 = 8 - let config = vec![1, 0, 0, 1, 1, 0]; - let result = problem.evaluate(&config); - assert_eq!(result, SolutionSize::Valid(8)); -} - -#[test] -fn test_minimummultiwaycut_evaluate_invalid() { - let graph = SimpleGraph::new( - 5, - vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)], - ); - let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); - - // No edges cut: all terminals connected => invalid - let config = vec![0, 0, 0, 0, 0, 0]; - let result = problem.evaluate(&config); - assert_eq!(result, SolutionSize::Invalid); -} - -#[test] -fn test_minimummultiwaycut_direction() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![1i32, 1]); - assert_eq!(problem.direction(), Direction::Minimize); -} - -#[test] -fn test_minimummultiwaycut_brute_force() { - // Issue example: optimal cut has weight 8 - let graph = SimpleGraph::new( - 5, - vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)], - ); - let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); - - let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); - assert!(!solutions.is_empty()); - for sol in &solutions { - let val = problem.evaluate(sol); - assert_eq!(val, SolutionSize::Valid(8)); - } -} - -#[test] -fn test_minimummultiwaycut_two_terminals() { - // k=2: classical min s-t cut. Path graph: 0-1-2, terminals {0,2} - // Edges: (0,1)w=3, (1,2)w=5 - // Min cut: remove (0,1) with weight 3 - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![3i32, 5]); - - let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); - for sol in &solutions { - assert_eq!(problem.evaluate(sol), SolutionSize::Valid(3)); - } -} - -#[test] -fn test_minimummultiwaycut_all_edges_cut() { - // Cutting all edges should always be valid (trivially separates everything) - let graph = SimpleGraph::new( - 5, - vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)], - ); - let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); - let config = vec![1, 1, 1, 1, 1, 1]; // cut all edges - let result = problem.evaluate(&config); - assert_eq!(result, SolutionSize::Valid(2 + 3 + 1 + 2 + 4 + 5)); // sum = 17 -} - -#[test] -fn test_minimummultiwaycut_already_disconnected() { - // Terminals already in different components => empty cut is valid - // Graph: 0-1 2-3, terminals {0, 2} - let graph = SimpleGraph::new(4, vec![(0, 1), (2, 3)]); - let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![1i32, 1]); - let config = vec![0, 0]; // no edges cut - let result = problem.evaluate(&config); - assert_eq!(result, SolutionSize::Valid(0)); - - // BruteForce should find the empty cut as optimal - let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); - for sol in &solutions { - assert_eq!(problem.evaluate(sol), SolutionSize::Valid(0)); - } -} - -#[test] -fn test_minimummultiwaycut_serialization() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![1i32, 2]); - let json = serde_json::to_string(&problem).unwrap(); - let restored: MinimumMultiwayCut = serde_json::from_str(&json).unwrap(); - assert_eq!(restored.num_vertices(), 3); - assert_eq!(restored.num_edges(), 2); - assert_eq!(restored.terminals(), &[0, 2]); -} -``` - -- [ ] **Step 2: Run tests** - -Run: `cargo test minimum_multiway_cut --lib -- --nocapture` -Expected: All tests PASS - -- [ ] **Step 3: Commit model + tests + registration** - -```bash -git add src/models/graph/minimum_multiway_cut.rs \ - src/unit_tests/models/graph/minimum_multiway_cut.rs \ - src/models/graph/mod.rs \ - src/models/mod.rs \ - problemreductions-cli/src/dispatch.rs \ - problemreductions-cli/src/problem_name.rs -git commit -m "feat: add MinimumMultiwayCut model (#184)" -``` - ---- - -## Task 4: Write example program - -**Files:** -- Create: `examples/minimummultiwaycut.rs` -- Modify: `tests/suites/examples.rs` - -- [ ] **Step 1: Write the example program** - -Use the issue's worked example (5 vertices, 3 terminals, optimal cut weight 8). - -```rust -// examples/minimummultiwaycut.rs -// MinimumMultiwayCut example: find minimum weight edge cut separating terminals. - -use problemreductions::models::graph::MinimumMultiwayCut; -use problemreductions::topology::SimpleGraph; -use problemreductions::{BruteForce, Problem, Solver}; - -pub fn run() { - // 5 vertices, terminals {0, 2, 4} - // Edges with weights: (0,1)=2, (1,2)=3, (2,3)=1, (3,4)=2, (0,4)=4, (1,3)=5 - let graph = SimpleGraph::new( - 5, - vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)], - ); - let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); - - let solver = BruteForce::new(); - let best = solver.find_best(&problem).expect("should find a solution"); - let value = problem.evaluate(&best); - - println!("Optimal multiway cut: {:?}", best); - println!("Cut weight: {:?}", value); - - // Export as JSON - let json = serde_json::json!({ - "problem": problem, - "solution": best, - "objective": 8, - }); - println!("{}", serde_json::to_string_pretty(&json).unwrap()); -} - -fn main() { - run(); -} -``` - -- [ ] **Step 2: Register in `tests/suites/examples.rs`** - -Add `example_test!` and `example_fn!` entries: -```rust -example_test!(minimummultiwaycut); -// ...in the test list: -example_fn!(test_minimummultiwaycut, minimummultiwaycut); -``` - -- [ ] **Step 3: Run the example** - -Run: `cargo run --example minimummultiwaycut` -Expected: Prints optimal cut with weight 8 - -- [ ] **Step 4: Run example test** - -Run: `cargo test test_minimummultiwaycut --test main` -Expected: PASS - -- [ ] **Step 5: Commit** - -```bash -git add examples/minimummultiwaycut.rs tests/suites/examples.rs -git commit -m "feat: add MinimumMultiwayCut example (#184)" -``` - ---- - -## Task 5: Regenerate exports and run full checks - -- [ ] **Step 1: Regenerate reduction graph and schemas** - -```bash -cargo run --example export_graph -cargo run --example export_schemas -``` - -- [ ] **Step 2: Run full check suite** - -```bash -make check # fmt + clippy + test -``` -Expected: All pass - -- [ ] **Step 3: Commit any generated file changes** - -```bash -git add docs/data/reduction_graph.json docs/data/problem_schemas.json -git commit -m "chore: regenerate exports for MinimumMultiwayCut (#184)" -``` - ---- - -## Task 6: Document in paper - -Invoke `/write-model-in-paper` to add the problem definition entry in `docs/paper/reductions.typ`. - -Key content to include: -- **Formal definition:** Given G=(V,E,w) and terminals T={t_1,...,t_k}, find minimum-weight C⊆E separating all terminals -- **Background:** Generalizes min s-t cut (k=2, polynomial) to k≥3 (NP-hard). Important in VLSI, image segmentation. (2−2/k)-approximation exists. -- **Example:** The issue's 5-vertex instance with CeTZ visualization showing the graph, terminals highlighted, and cut edges -- **Algorithm:** Cao, Chen & Fan (2013), O*(1.84^k) - -- [ ] **Step 1: Run `/write-model-in-paper`** -- [ ] **Step 2: Commit paper changes** - ---- - -## Task 7: Final verification - -- [ ] **Step 1: Run full test suite** - -```bash -make test clippy -``` - -- [ ] **Step 2: Run `/review-implementation` to verify all structural and semantic checks pass** diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 91e9bd252..004330e7c 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -208,6 +208,7 @@ Flags by problem type: QUBO --matrix SpinGlass --graph, --couplings, --fields KColoring --graph, --k + MinimumMultiwayCut --graph, --terminals, --edge-weights Factoring --target, --m, --n BinPacking --sizes, --capacity PaintShop --sequence @@ -284,6 +285,9 @@ pub struct CreateArgs { /// Bits for second factor (for Factoring) #[arg(long)] pub n: Option, + /// Terminal vertices for MinimumMultiwayCut (comma-separated, e.g., 0,2,4) + #[arg(long)] + pub terminals: Option, /// Vertex positions for geometry-based graphs (semicolon-separated x,y pairs, e.g., "0,0;1,0;1,1") #[arg(long)] pub positions: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 418bc520f..aa6a098c8 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -5,6 +5,7 @@ use crate::problem_name::{parse_problem_spec, resolve_variant}; use crate::util; use anyhow::{bail, Context, Result}; use problemreductions::models::algebraic::{ClosestVectorProblem, BMF}; +use problemreductions::models::graph::MinimumMultiwayCut; use problemreductions::models::misc::{BinPacking, PaintShop}; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; @@ -45,6 +46,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.basis.is_none() && args.target_vec.is_none() && args.bounds.is_none() + && args.terminals.is_none() } fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { @@ -55,6 +57,7 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { _ => "edge list: 0-1,1-2,2-3", }, "Vec" => "comma-separated: 1,2,3", + "Vec" => "comma-separated indices: 0,2,4", "Vec" => "semicolon-separated clauses: \"1,2;-1,3\"", "Vec>" => "semicolon-separated rows: \"1,0.5;0.5,2\"", "usize" => "integer", @@ -83,6 +86,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "SpinGlass" => "--graph 0-1,1-2 --couplings 1,1", "KColoring" => "--graph 0-1,1-2,2-0 --k 3", "Factoring" => "--target 15 --m 4 --n 4", + "MinimumMultiwayCut" => "--graph 0-1,1-2,2-3 --terminals 0,2 --edge-weights 1,1,1", _ => "", } } @@ -443,6 +447,21 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // MinimumMultiwayCut + "MinimumMultiwayCut" => { + let (graph, _) = parse_graph(args).map_err(|e| { + anyhow::anyhow!( + "{e}\n\nUsage: pred create MinimumMultiwayCut --graph 0-1,1-2,2-3 --terminals 0,2 [--edge-weights 1,1,1]" + ) + })?; + let terminals = parse_terminals(args)?; + let edge_weights = parse_edge_weights(args, graph.num_edges())?; + ( + ser(MinimumMultiwayCut::new(graph, terminals, edge_weights))?, + resolved_variant.clone(), + ) + } + _ => bail!("{}", crate::problem_name::unknown_problem_error(canonical)), }; @@ -632,6 +651,23 @@ fn parse_edge_weights(args: &CreateArgs, num_edges: usize) -> Result> { } } +/// Parse `--terminals` as a comma-separated list of vertex indices. +fn parse_terminals(args: &CreateArgs) -> Result> { + let terminals_str = args + .terminals + .as_deref() + .ok_or_else(|| anyhow::anyhow!("MinimumMultiwayCut requires --terminals (e.g., 0,2,4)"))?; + + terminals_str + .split(',') + .map(|s| { + s.trim() + .parse::() + .map_err(|e| anyhow::anyhow!("Invalid terminal index '{}': {}", s.trim(), e)) + }) + .collect() +} + /// Parse `--couplings` as SpinGlass pairwise couplings (i32), defaulting to all 1s. fn parse_couplings(args: &CreateArgs, num_edges: usize) -> Result> { match &args.couplings { diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 5840f6a6e..c6f3afd46 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -169,8 +169,14 @@ impl LoadedProblem { } } - let reduction_path = - best_path.ok_or_else(|| anyhow::anyhow!("No reduction path from {} to ILP", name))?; + let reduction_path = best_path.ok_or_else(|| { + anyhow::anyhow!( + "No reduction path from {} to ILP\n\n\ + Hint: try --solver brute-force for exhaustive search on small instances:\n \ + pred solve --solver brute-force", + name + ) + })?; let chain = graph .reduce_along_path(&reduction_path, self.as_any()) diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index a3e55f4d4..82aedc847 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -21,7 +21,6 @@ pub const ALIASES: &[(&str, &str)] = &[ ("TSP", "TravelingSalesman"), ("CVP", "ClosestVectorProblem"), ("MaxMatching", "MaximumMatching"), - ("MMC", "MinimumMultiwayCut"), ]; /// Resolve a short alias to the canonical problem name. @@ -42,7 +41,7 @@ pub fn resolve_alias(input: &str) -> String { "maximumclique" => "MaximumClique".to_string(), "maxmatching" | "maximummatching" => "MaximumMatching".to_string(), "minimumdominatingset" => "MinimumDominatingSet".to_string(), - "minimummultiwaycut" | "mmc" => "MinimumMultiwayCut".to_string(), + "minimummultiwaycut" => "MinimumMultiwayCut".to_string(), "minimumsetcovering" => "MinimumSetCovering".to_string(), "maximumsetpacking" => "MaximumSetPacking".to_string(), "kcoloring" => "KColoring".to_string(), From b98c68e62d20d2d72f7f8045f850dd3501d9702f Mon Sep 17 00:00:00 2001 From: Huai-Ming Yu Date: Tue, 10 Mar 2026 18:36:18 +0800 Subject: [PATCH 04/13] docs: improve MinimumMultiwayCut discoverability and update CLI docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Document edge weight ordering and config encoding on `new()` docstring - Add MinimumMultiwayCut create example to CLI docs - Update `pred list` output in CLI docs (17 → 25 problem types) - Add CVP and MaxMatching aliases to CLI docs alias table Co-authored-by: Claude --- docs/src/cli.md | 53 ++++++++++++++---------- src/models/graph/minimum_multiway_cut.rs | 4 ++ 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/docs/src/cli.md b/docs/src/cli.md index af1c62372..2e9d6f1f6 100644 --- a/docs/src/cli.md +++ b/docs/src/cli.md @@ -76,27 +76,35 @@ Lists all registered problem types with their short aliases. ```bash $ pred list -Registered problems: 17 types, 48 reductions, 25 variant nodes - - Problem Aliases Variants Reduces to - ───────────────────── ────────── ──────── ────────── - CircuitSAT 1 1 - Factoring 1 2 - ILP 1 1 - KColoring 2 3 - KSatisfiability 3SAT, KSAT 3 7 - MaxCut 1 1 - MaximumClique 1 1 - MaximumIndependentSet MIS 4 10 - MaximumMatching 1 2 - MaximumSetPacking 2 4 - MinimumDominatingSet 1 1 - MinimumSetCovering 1 1 - MinimumVertexCover MVC 1 4 - QUBO 1 1 - Satisfiability SAT 1 5 - SpinGlass 2 3 - TravelingSalesman TSP 1 1 +Registered problems: 25 types, 58 reductions, 42 variant nodes + + Problem Aliases Variants Reduces to + ───────────────────── ─────────── ──────── ────────── + BMF 1 0 + BicliqueCover 1 0 + BinPacking 2 0 + CircuitSAT 1 2 + ClosestVectorProblem CVP 2 0 + Factoring 1 2 + ILP 1 1 + KColoring 5 3 + KSatisfiability 3SAT, KSAT 3 7 + Knapsack 1 0 + MaxCut 1 1 + MaximalIS 1 0 + MaximumClique 1 1 + MaximumIndependentSet MIS 7 16 + MaximumMatching MaxMatching 1 2 + MaximumSetPacking 3 6 + MinimumDominatingSet 1 1 + MinimumMultiwayCut 1 0 + MinimumSetCovering 1 1 + MinimumVertexCover MVC 1 4 + PaintShop 1 0 + QUBO 1 2 + Satisfiability SAT 1 5 + SpinGlass 2 3 + TravelingSalesman TSP 1 1 Use `pred show ` to see variants, reductions, and fields. ``` @@ -239,6 +247,7 @@ pred create QUBO --matrix "1,0.5;0.5,2" -o qubo.json pred create KColoring --k 3 --graph 0-1,1-2,2-0 -o kcol.json pred create SpinGlass --graph 0-1,1-2 -o sg.json pred create MaxCut --graph 0-1,1-2,2-0 -o maxcut.json +pred create MinimumMultiwayCut --graph 0-1,1-2,2-3,3-0 --terminals 0,2 --edge-weights 3,1,2,4 -o mmc.json pred create Factoring --target 15 --bits-m 4 --bits-n 4 -o factoring.json pred create Factoring --target 21 --bits-m 3 --bits-n 3 -o factoring2.json ``` @@ -423,6 +432,8 @@ You can use short aliases instead of full problem names (shown in `pred list`): | `SAT` | `Satisfiability` | | `3SAT` / `KSAT` | `KSatisfiability` | | `TSP` | `TravelingSalesman` | +| `CVP` | `ClosestVectorProblem` | +| `MaxMatching` | `MaximumMatching` | You can also specify variants with a slash: `MIS/UnitDiskGraph`, `SpinGlass/SimpleGraph`. diff --git a/src/models/graph/minimum_multiway_cut.rs b/src/models/graph/minimum_multiway_cut.rs index 2e60f058a..cc9ab65cf 100644 --- a/src/models/graph/minimum_multiway_cut.rs +++ b/src/models/graph/minimum_multiway_cut.rs @@ -49,6 +49,10 @@ pub struct MinimumMultiwayCut { impl MinimumMultiwayCut { /// Create a MinimumMultiwayCut problem. /// + /// `edge_weights` must have one entry per edge, in the same order as + /// [`Graph::edges()`](crate::topology::Graph::edges). Each binary + /// variable corresponds to an edge: 0 = keep, 1 = cut. + /// /// # Panics /// - If `edge_weights.len() != graph.num_edges()` /// - If `terminals.len() < 2` From c81bcaceb518525707197b63057079692ca5e07a Mon Sep 17 00:00:00 2001 From: Huai-Ming Yu Date: Tue, 10 Mar 2026 18:46:35 +0800 Subject: [PATCH 05/13] fix: use defensive bounds checking in MinimumMultiwayCut evaluate Use config.get(idx) and edge_weights.get(idx) instead of direct indexing to avoid panics on malformed input, matching the codebase convention used by MaximumIndependentSet, TravelingSalesman, etc. Made-with: Cursor --- src/models/graph/minimum_multiway_cut.rs | 6 ++++-- .../models/graph/minimum_multiway_cut.rs | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/models/graph/minimum_multiway_cut.rs b/src/models/graph/minimum_multiway_cut.rs index cc9ab65cf..d4f45d36c 100644 --- a/src/models/graph/minimum_multiway_cut.rs +++ b/src/models/graph/minimum_multiway_cut.rs @@ -121,7 +121,7 @@ fn terminals_separated(graph: &G, terminals: &[usize], config: &[usize // Build adjacency list from non-cut edges let mut adj: Vec> = vec![vec![]; n]; for (idx, (u, v)) in edges.iter().enumerate() { - if config[idx] == 0 { + if config.get(idx).copied().unwrap_or(0) == 0 { adj[*u].push(*v); adj[*v].push(*u); } @@ -172,7 +172,9 @@ where let mut total = W::Sum::zero(); for (idx, &selected) in config.iter().enumerate() { if selected == 1 { - total += self.edge_weights[idx].to_sum(); + if let Some(w) = self.edge_weights.get(idx) { + total += w.to_sum(); + } } } SolutionSize::Valid(total) diff --git a/src/unit_tests/models/graph/minimum_multiway_cut.rs b/src/unit_tests/models/graph/minimum_multiway_cut.rs index 4fe8748b4..793449593 100644 --- a/src/unit_tests/models/graph/minimum_multiway_cut.rs +++ b/src/unit_tests/models/graph/minimum_multiway_cut.rs @@ -120,3 +120,17 @@ fn test_minimummultiwaycut_name() { "MinimumMultiwayCut" ); } + +#[test] +fn test_minimummultiwaycut_short_config_no_panic() { + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); + + let short_config = vec![1, 0]; + let result = problem.evaluate(&short_config); + assert_ne!(result, SolutionSize::Valid(0)); + + let empty_config: Vec = vec![]; + let result = problem.evaluate(&empty_config); + assert_ne!(result, SolutionSize::Valid(0)); +} From 0310fd8377415d0b6d7887ada5f8bfd4988a4127 Mon Sep 17 00:00:00 2001 From: zazabap Date: Sun, 15 Mar 2026 10:36:38 +0000 Subject: [PATCH 06/13] fix: resolve merge conflicts with main and update ProblemSchemaEntry Add missing fields (display_name, aliases, dimensions) required by updated ProblemSchemaEntry struct, and add opt/default to declare_variants. Co-Authored-By: Claude Opus 4.6 --- docs/src/reductions/problem_schemas.json | 4 +- docs/src/reductions/reduction_graph.json | 64 ++++++++++++------------ src/models/graph/minimum_multiway_cut.rs | 10 +++- 3 files changed, 42 insertions(+), 36 deletions(-) diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index bcd8d2bf7..c4f25d257 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -553,7 +553,7 @@ { "name": "required_edges", "type_name": "Vec", - "description": "Edge indices of the required subset E' \u2286 E" + "description": "Edge indices of the required subset E' ⊆ E" }, { "name": "bound", @@ -668,4 +668,4 @@ } ] } -] +] \ No newline at end of file diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index 78c736d6a..d86b765b0 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -545,7 +545,7 @@ }, { "source": 4, - "target": 52, + "target": 53, "overhead": [ { "field": "num_spins", @@ -605,7 +605,7 @@ }, { "source": 11, - "target": 47, + "target": 48, "overhead": [ { "field": "num_vars", @@ -646,7 +646,7 @@ }, { "source": 18, - "target": 47, + "target": 48, "overhead": [ { "field": "num_vars", @@ -672,7 +672,7 @@ }, { "source": 19, - "target": 47, + "target": 48, "overhead": [ { "field": "num_vars", @@ -698,7 +698,7 @@ }, { "source": 20, - "target": 47, + "target": 48, "overhead": [ { "field": "num_vars", @@ -709,7 +709,7 @@ }, { "source": 20, - "target": 54, + "target": 55, "overhead": [ { "field": "num_elements", @@ -720,7 +720,7 @@ }, { "source": 21, - "target": 49, + "target": 50, "overhead": [ { "field": "num_clauses", @@ -739,7 +739,7 @@ }, { "source": 22, - "target": 47, + "target": 48, "overhead": [ { "field": "num_vars", @@ -765,7 +765,7 @@ }, { "source": 24, - "target": 52, + "target": 53, "overhead": [ { "field": "num_spins", @@ -945,7 +945,7 @@ }, { "source": 30, - "target": 43, + "target": 44, "overhead": [ { "field": "num_vertices", @@ -1080,7 +1080,7 @@ }, { "source": 36, - "target": 47, + "target": 48, "overhead": [ { "field": "num_vars", @@ -1150,7 +1150,7 @@ "doc_path": "rules/minimumdominatingset_ilp/index.html" }, { - "source": 41, + "source": 42, "target": 11, "overhead": [ { @@ -1165,7 +1165,7 @@ "doc_path": "rules/minimumsetcovering_ilp/index.html" }, { - "source": 43, + "source": 44, "target": 30, "overhead": [ { @@ -1180,8 +1180,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 43, - "target": 41, + "source": 44, + "target": 42, "overhead": [ { "field": "num_sets", @@ -1195,7 +1195,7 @@ "doc_path": "rules/minimumvertexcover_minimumsetcovering/index.html" }, { - "source": 47, + "source": 48, "target": 11, "overhead": [ { @@ -1210,8 +1210,8 @@ "doc_path": "rules/qubo_ilp/index.html" }, { - "source": 47, - "target": 51, + "source": 48, + "target": 52, "overhead": [ { "field": "num_spins", @@ -1221,7 +1221,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 49, + "source": 50, "target": 4, "overhead": [ { @@ -1236,7 +1236,7 @@ "doc_path": "rules/sat_circuitsat/index.html" }, { - "source": 49, + "source": 50, "target": 15, "overhead": [ { @@ -1251,7 +1251,7 @@ "doc_path": "rules/sat_coloring/index.html" }, { - "source": 49, + "source": 50, "target": 20, "overhead": [ { @@ -1266,7 +1266,7 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 49, + "source": 50, "target": 29, "overhead": [ { @@ -1281,7 +1281,7 @@ "doc_path": "rules/sat_maximumindependentset/index.html" }, { - "source": 49, + "source": 50, "target": 38, "overhead": [ { @@ -1296,8 +1296,8 @@ "doc_path": "rules/sat_minimumdominatingset/index.html" }, { - "source": 51, - "target": 47, + "source": 52, + "target": 48, "overhead": [ { "field": "num_vars", @@ -1307,7 +1307,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 52, + "source": 53, "target": 24, "overhead": [ { @@ -1322,8 +1322,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 52, - "target": 51, + "source": 53, + "target": 52, "overhead": [ { "field": "num_spins", @@ -1337,7 +1337,7 @@ "doc_path": "rules/spinglass_casts/index.html" }, { - "source": 55, + "source": 56, "target": 11, "overhead": [ { @@ -1352,8 +1352,8 @@ "doc_path": "rules/travelingsalesman_ilp/index.html" }, { - "source": 55, - "target": 47, + "source": 56, + "target": 48, "overhead": [ { "field": "num_vars", @@ -1363,4 +1363,4 @@ "doc_path": "rules/travelingsalesman_qubo/index.html" } ] -} +} \ No newline at end of file diff --git a/src/models/graph/minimum_multiway_cut.rs b/src/models/graph/minimum_multiway_cut.rs index d4f45d36c..f813868ab 100644 --- a/src/models/graph/minimum_multiway_cut.rs +++ b/src/models/graph/minimum_multiway_cut.rs @@ -3,7 +3,7 @@ //! The Minimum Multiway Cut problem asks for a minimum weight set of edges //! whose removal disconnects all terminal pairs. -use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; use crate::traits::{OptimizationProblem, Problem}; use crate::types::{Direction, SolutionSize, WeightElement}; @@ -14,6 +14,12 @@ use std::collections::VecDeque; inventory::submit! { ProblemSchemaEntry { name: "MinimumMultiwayCut", + display_name: "Minimum Multiway Cut", + aliases: &[], + dimensions: &[ + VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]), + VariantDimension::new("weight", "i32", &["i32"]), + ], module_path: module_path!(), description: "Find minimum weight set of edges whose removal disconnects all terminal pairs", fields: &[ @@ -194,7 +200,7 @@ where } crate::declare_variants! { - MinimumMultiwayCut => "1.84^num_terminals * num_vertices^3", + default opt MinimumMultiwayCut => "1.84^num_terminals * num_vertices^3", } #[cfg(test)] From c1e95f955b443a8d5b397e2a0c26b8197c8f3420 Mon Sep 17 00:00:00 2001 From: zazabap Date: Sun, 15 Mar 2026 10:38:48 +0000 Subject: [PATCH 07/13] chore: remove per-model example file (not needed) Per reviewer comment: example binaries should be utility/export tools or pedagogical demos, not per-model files. Co-Authored-By: Claude Opus 4.6 --- examples/minimummultiwaycut.rs | 31 ------------------------------- 1 file changed, 31 deletions(-) delete mode 100644 examples/minimummultiwaycut.rs diff --git a/examples/minimummultiwaycut.rs b/examples/minimummultiwaycut.rs deleted file mode 100644 index 59f4022de..000000000 --- a/examples/minimummultiwaycut.rs +++ /dev/null @@ -1,31 +0,0 @@ -// MinimumMultiwayCut example: find minimum weight edge cut separating terminals. - -use problemreductions::models::graph::MinimumMultiwayCut; -use problemreductions::topology::SimpleGraph; -use problemreductions::{BruteForce, Problem, Solver}; - -pub fn run() { - // 5 vertices, terminals {0, 2, 4} - // Edges with weights: (0,1)=2, (1,2)=3, (2,3)=1, (3,4)=2, (0,4)=4, (1,3)=5 - let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]); - let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); - - let solver = BruteForce::new(); - let best = solver.find_best(&problem).expect("should find a solution"); - let value = problem.evaluate(&best); - - println!("Optimal multiway cut: {:?}", best); - println!("Cut weight: {:?}", value); - - // Export as JSON - let json = serde_json::json!({ - "problem": problem, - "solution": best, - "objective": 8, - }); - println!("{}", serde_json::to_string_pretty(&json).unwrap()); -} - -fn main() { - run(); -} From 5c9bcd7b0c7d4f7b305d541662025e783a36d116 Mon Sep 17 00:00:00 2001 From: zazabap Date: Sun, 15 Mar 2026 10:47:20 +0000 Subject: [PATCH 08/13] fix: address structural review findings - Add MinimumMultiwayCut to trait_consistency tests - Add canonical model example in example_db - Strengthen brute-force test to verify specific optimal config - Strengthen short_config test assertions - Regenerate problem_schemas.json Co-Authored-By: Claude Opus 4.6 --- src/models/graph/minimum_multiway_cut.rs | 19 +++++++++++++++++++ src/models/graph/mod.rs | 1 + .../models/graph/minimum_multiway_cut.rs | 13 +++++++++++-- src/unit_tests/trait_consistency.rs | 17 +++++++++++++++++ 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/models/graph/minimum_multiway_cut.rs b/src/models/graph/minimum_multiway_cut.rs index f813868ab..71d66cb2f 100644 --- a/src/models/graph/minimum_multiway_cut.rs +++ b/src/models/graph/minimum_multiway_cut.rs @@ -203,6 +203,25 @@ crate::declare_variants! { default opt MinimumMultiwayCut => "1.84^num_terminals * num_vertices^3", } +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "minimum_multiway_cut_simplegraph_i32", + build: || { + // 5 vertices, terminals {0, 2, 4}, 6 edges + let graph = + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]); + let problem = + MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); + // Optimal cut: edges {(0,1), (3,4), (0,4)} = config [1,0,0,1,1,0], weight=8 + crate::example_db::specs::optimization_example( + problem, + vec![vec![1, 0, 0, 1, 1, 0]], + ) + }, + }] +} + #[cfg(test)] #[path = "../../unit_tests/models/graph/minimum_multiway_cut.rs"] mod tests; diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 094104570..ffdbf516e 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -85,6 +85,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec = vec![]; let result = problem.evaluate(&empty_config); - assert_ne!(result, SolutionSize::Valid(0)); + assert_eq!(result, SolutionSize::Invalid); } diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index ebbc68a0e..b82294841 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -90,6 +90,14 @@ fn test_all_problems_implement_trait_correctly() { ), "MinimumFeedbackArcSet", ); + check_problem_trait( + &MinimumMultiwayCut::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + vec![0, 2], + vec![1i32; 2], + ), + "MinimumMultiwayCut", + ); check_problem_trait( &MinimumSumMulticenter::new( SimpleGraph::new(3, vec![(0, 1), (1, 2)]), @@ -171,6 +179,15 @@ fn test_direction() { .direction(), Direction::Minimize ); + assert_eq!( + MinimumMultiwayCut::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + vec![0, 2], + vec![1i32; 2] + ) + .direction(), + Direction::Minimize + ); assert_eq!( MinimumSumMulticenter::new( SimpleGraph::new(3, vec![(0, 1), (1, 2)]), From ea8df53746c59be202fc9786c47715bd184a1ac3 Mon Sep 17 00:00:00 2001 From: zazabap Date: Mon, 16 Mar 2026 11:58:44 +0000 Subject: [PATCH 09/13] fix: add coverage tests and CLI terminal validation for MinimumMultiwayCut Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/src/commands/create.rs | 34 +++++++++++++++--- src/lib.rs | 4 +-- src/models/graph/maximum_independent_set.rs | 3 +- src/models/graph/minimum_multiway_cut.rs | 11 ++---- .../models/graph/minimum_multiway_cut.rs | 36 +++++++++++++++++++ 5 files changed, 72 insertions(+), 16 deletions(-) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 7d0462705..d9f548307 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -754,7 +754,7 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { "{e}\n\nUsage: pred create MinimumMultiwayCut --graph 0-1,1-2,2-3 --terminals 0,2 [--edge-weights 1,1,1]" ) })?; - let terminals = parse_terminals(args)?; + let terminals = parse_terminals(args, graph.num_vertices())?; let edge_weights = parse_edge_weights(args, graph.num_edges())?; ( ser(MinimumMultiwayCut::new(graph, terminals, edge_weights))?, @@ -1234,20 +1234,46 @@ fn parse_edge_weights(args: &CreateArgs, num_edges: usize) -> Result> { } /// Parse `--terminals` as a comma-separated list of vertex indices. -fn parse_terminals(args: &CreateArgs) -> Result> { +fn parse_terminals(args: &CreateArgs, num_vertices: usize) -> Result> { let terminals_str = args .terminals .as_deref() .ok_or_else(|| anyhow::anyhow!("MinimumMultiwayCut requires --terminals (e.g., 0,2,4)"))?; - terminals_str + let terminals: Vec = terminals_str .split(',') .map(|s| { s.trim() .parse::() .map_err(|e| anyhow::anyhow!("Invalid terminal index '{}': {}", s.trim(), e)) }) - .collect() + .collect::>>()?; + + anyhow::ensure!( + terminals.len() >= 2, + "at least 2 terminals required, got {}", + terminals.len() + ); + + for &t in &terminals { + anyhow::ensure!( + t < num_vertices, + "terminal index {} out of bounds (graph has {} vertices)", + t, + num_vertices + ); + } + + let mut sorted = terminals.clone(); + sorted.sort(); + sorted.dedup(); + anyhow::ensure!( + sorted.len() == terminals.len(), + "duplicate terminal indices in {:?}", + terminals + ); + + Ok(terminals) } /// Parse `--couplings` as SpinGlass pairwise couplings (i32), defaulting to all 1s. diff --git a/src/lib.rs b/src/lib.rs index 8b3169eed..58dfe16c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,8 +50,8 @@ pub mod prelude { }; pub use crate::models::graph::{ KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, - MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet, - MinimumMultiwayCut, MinimumSumMulticenter, MinimumVertexCover, OptimalLinearArrangement, + MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumMultiwayCut, + MinimumSumMulticenter, MinimumVertexCover, OptimalLinearArrangement, PartitionIntoTriangles, RuralPostman, TravelingSalesman, }; pub use crate::models::misc::{ diff --git a/src/models/graph/maximum_independent_set.rs b/src/models/graph/maximum_independent_set.rs index 0b8a3ddfc..1177398b9 100644 --- a/src/models/graph/maximum_independent_set.rs +++ b/src/models/graph/maximum_independent_set.rs @@ -233,8 +233,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec Date: Mon, 16 Mar 2026 12:23:20 +0000 Subject: [PATCH 10/13] fix: data-driven paper example, CLI tests, dispatch cleanup for MinimumMultiwayCut - Convert paper problem-def to use load-model-example(), deriving all concrete values (vertices, edges, terminals, cut edges, cost) from the canonical example fixture instead of hardcoded literals - Add CLI tests: create with flags, create with --example, reject single terminal - dispatch.rs already uses registry-based dispatch (no manual arms) Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/paper/reductions.typ | 74 ++++++++++++++---------- problemreductions-cli/tests/cli_tests.rs | 74 ++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 32 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 2113ff3a4..9be609221 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -854,40 +854,50 @@ Graph Partitioning is a core NP-hard problem arising in VLSI design, parallel co ] ] } -#problem-def("MinimumMultiwayCut")[ - Given an undirected graph $G=(V,E)$ with edge weights $w: E -> RR_(>0)$ and a set of $k$ terminal vertices $T = {t_1, ..., t_k} subset.eq V$, find a minimum-weight set of edges $C subset.eq E$ such that no two terminals remain in the same connected component of $G' = (V, E backslash C)$. -][ -The Minimum Multiway Cut problem generalizes the classical minimum $s$-$t$ cut: for $k=2$ it reduces to max-flow and is solvable in polynomial time, but for $k >= 3$ on general graphs it becomes NP-hard @dahlhaus1994. The problem arises in VLSI design, image segmentation, and network design. A $(2 - 2 slash k)$-approximation is achievable in polynomial time by taking the union of the $k - 1$ cheapest isolating cuts @dahlhaus1994. The best known exact algorithm runs in $O^*(1.84^k)$ time (suppressing polynomial factors) via submodular functions on isolating cuts @cao2013. +#{ + let x = load-model-example("MinimumMultiwayCut") + let nv = graph-num-vertices(x.instance) + let ne = graph-num-edges(x.instance) + let edges = x.instance.graph.inner.edges.map(e => (e.at(0), e.at(1))) + let weights = x.instance.edge_weights + let terminals = x.instance.terminals + let sol = x.optimal.at(0) + let cut-edge-indices = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let cut-edges = cut-edge-indices.map(i => edges.at(i)) + let cost = sol.metric.Valid + [ + #problem-def("MinimumMultiwayCut")[ + Given an undirected graph $G=(V,E)$ with edge weights $w: E -> RR_(>0)$ and a set of $k$ terminal vertices $T = {t_1, ..., t_k} subset.eq V$, find a minimum-weight set of edges $C subset.eq E$ such that no two terminals remain in the same connected component of $G' = (V, E backslash C)$. + ][ + The Minimum Multiway Cut problem generalizes the classical minimum $s$-$t$ cut: for $k=2$ it reduces to max-flow and is solvable in polynomial time, but for $k >= 3$ on general graphs it becomes NP-hard @dahlhaus1994. The problem arises in VLSI design, image segmentation, and network design. A $(2 - 2 slash k)$-approximation is achievable in polynomial time by taking the union of the $k - 1$ cheapest isolating cuts @dahlhaus1994. The best known exact algorithm runs in $O^*(1.84^k)$ time (suppressing polynomial factors) via submodular functions on isolating cuts @cao2013. -*Example.* Consider a graph with $n = 5$ vertices $V = {0, 1, 2, 3, 4}$, terminals $T = {0, 2, 4}$, and 6 edges with weights: $w(0,1) = 2$, $w(1,2) = 3$, $w(2,3) = 1$, $w(3,4) = 2$, $w(0,4) = 4$, $w(1,3) = 5$. The optimal multiway cut removes edges ${(0,1), (3,4), (0,4)}$ with total weight $2 + 2 + 4 = 8$, yielding connected components ${0}$, ${1, 2, 3}$, ${4}$ — each terminal in a distinct component. + *Example.* Consider a graph with $n = #nv$ vertices, $m = #ne$ edges, and $k = #terminals.len()$ terminals $T = {#terminals.map(t => $#t$).join(", ")}$, with edge weights #edges.zip(weights).map(((e, w)) => $w(#(e.at(0)), #(e.at(1))) = #w$).join(", "). The optimal multiway cut removes edges ${#cut-edges.map(e => $(#(e.at(0)), #(e.at(1)))$).join(", ")}$ with total weight #cut-edge-indices.map(i => $#(weights.at(i))$).join($+$) $= #cost$, placing each terminal in a distinct component. -#figure({ - let verts = ((0, 0.8), (1.2, 1.5), (2.4, 0.8), (1.8, -0.2), (0.6, -0.2)) - let edges = ((0,1),(1,2),(2,3),(3,4),(0,4),(1,3)) - let weights = ("2", "3", "1", "2", "4", "5") - let cut-edges = (0, 3, 4) // indices of cut edges - let terminals = (0, 2, 4) - canvas(length: 1cm, { - for (idx, (u, v)) in edges.enumerate() { - let is-cut = cut-edges.any(c => c == idx) - g-edge(verts.at(u), verts.at(v), - stroke: if is-cut { (paint: red, thickness: 2pt, dash: "dashed") } else { 1pt + luma(120) }) - let mx = (verts.at(u).at(0) + verts.at(v).at(0)) / 2 - let my = (verts.at(u).at(1) + verts.at(v).at(1)) / 2 - let dy = if idx == 5 { 0.15 } else { 0 } - draw.content((mx, my + dy), text(7pt, fill: luma(80))[#weights.at(idx)]) - } - for (k, pos) in verts.enumerate() { - let is-terminal = terminals.any(t => t == k) - g-node(pos, name: "v" + str(k), - fill: if is-terminal { graph-colors.at(0) } else { luma(180) }, - label: text(fill: white)[$#k$]) - } - }) -}, -caption: [Minimum Multiway Cut with terminals ${0, 2, 4}$ (blue). Dashed red edges form the optimal cut (weight 8).], -) -] + #figure({ + let verts = ((0, 0.8), (1.2, 1.5), (2.4, 0.8), (1.8, -0.2), (0.6, -0.2)) + canvas(length: 1cm, { + for (idx, (u, v)) in edges.enumerate() { + let is-cut = cut-edge-indices.contains(idx) + g-edge(verts.at(u), verts.at(v), + stroke: if is-cut { (paint: red, thickness: 2pt, dash: "dashed") } else { 1pt + luma(120) }) + let mx = (verts.at(u).at(0) + verts.at(v).at(0)) / 2 + let my = (verts.at(u).at(1) + verts.at(v).at(1)) / 2 + let dy = if idx == 5 { 0.15 } else { 0 } + draw.content((mx, my + dy), text(7pt, fill: luma(80))[#weights.at(idx)]) + } + for (k, pos) in verts.enumerate() { + let is-terminal = terminals.contains(k) + g-node(pos, name: "v" + str(k), + fill: if is-terminal { graph-colors.at(0) } else { luma(180) }, + label: text(fill: white)[$#k$]) + } + }) + }, + caption: [Minimum Multiway Cut with terminals ${#terminals.map(t => $#t$).join(", ")}$ (blue). Dashed red edges form the optimal cut (weight #cost).], + ) + ] + ] +} #problem-def("OptimalLinearArrangement")[ Given an undirected graph $G=(V,E)$ and a non-negative integer $K$, is there a bijection $f: V -> {0, 1, dots, |V|-1}$ such that $sum_({u,v} in E) |f(u) - f(v)| <= K$? ][ diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index d29ba2478..27465ac80 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -3681,3 +3681,77 @@ fn test_create_weighted_mis_round_trips_into_solve() { let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); assert_eq!(json["evaluation"], "Valid(5)"); } + +#[test] +fn test_create_minimum_multiway_cut() { + let output_file = std::env::temp_dir().join("pred_test_create_minimum_multiway_cut.json"); + let output = pred() + .args([ + "-o", + output_file.to_str().unwrap(), + "create", + "MinimumMultiwayCut", + "--graph", + "0-1,1-2,2-3", + "--terminals", + "0,2", + "--edge-weights", + "1,1,1", + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let content = std::fs::read_to_string(&output_file).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + assert_eq!(json["type"], "MinimumMultiwayCut"); + assert_eq!(json["variant"]["graph"], "SimpleGraph"); + assert_eq!(json["variant"]["weight"], "i32"); + assert_eq!(json["data"]["terminals"], serde_json::json!([0, 2])); + assert_eq!(json["data"]["edge_weights"], serde_json::json!([1, 1, 1])); + std::fs::remove_file(&output_file).ok(); +} + +#[test] +fn test_create_model_example_minimum_multiway_cut() { + let output = pred() + .args(["create", "--example", "MinimumMultiwayCut"]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); + assert_eq!(json["type"], "MinimumMultiwayCut"); + assert_eq!(json["variant"]["graph"], "SimpleGraph"); + assert_eq!(json["variant"]["weight"], "i32"); +} + +#[test] +fn test_create_minimum_multiway_cut_rejects_single_terminal() { + let output = pred() + .args([ + "create", + "MinimumMultiwayCut", + "--graph", + "0-1,1-2", + "--edge-weights", + "1,1", + "--terminals", + "0", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("terminal") || stderr.contains("Terminal"), + "expected terminal-related error, got: {stderr}" + ); +} From 41509df4819ae4aac492f5b9a96f2f44dbaa1824 Mon Sep 17 00:00:00 2001 From: zazabap Date: Mon, 16 Mar 2026 12:38:50 +0000 Subject: [PATCH 11/13] fix: remove duplicate terminals field and parse_terminals from merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The merge with main introduced duplicate `terminals` CLI field (from SteinerTree) and duplicate `parse_terminals` function. Removed the duplicates — both problems now share a single `--terminals` flag and validation function. Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/src/cli.rs | 3 -- problemreductions-cli/src/commands/create.rs | 45 +------------------- 2 files changed, 1 insertion(+), 47 deletions(-) diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index bc98e087d..e3299f0c5 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -372,9 +372,6 @@ pub struct CreateArgs { /// Variable bounds for CVP as "lower,upper" (e.g., "-10,10") [default: -10,10] #[arg(long, allow_hyphen_values = true)] pub bounds: Option, - /// Terminal vertices for SteinerTree (comma-separated indices, e.g., "0,2,4") - #[arg(long)] - pub terminals: Option, /// Tree edge list for IsomorphicSpanningTree (e.g., 0-1,1-2,2-3) #[arg(long)] pub tree: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index c82a1eab7..614ffe4c0 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -1335,7 +1335,7 @@ fn parse_terminals(args: &CreateArgs, num_vertices: usize) -> Result> let s = args .terminals .as_deref() - .ok_or_else(|| anyhow::anyhow!("SteinerTree requires --terminals (e.g., \"0,2,4\")"))?; + .ok_or_else(|| anyhow::anyhow!("--terminals required (e.g., \"0,2,4\")"))?; let terminals: Vec = s .split(',') .map(|t| t.trim().parse::()) @@ -1377,49 +1377,6 @@ fn parse_edge_weights(args: &CreateArgs, num_edges: usize) -> Result> { } } -/// Parse `--terminals` as a comma-separated list of vertex indices. -fn parse_terminals(args: &CreateArgs, num_vertices: usize) -> Result> { - let terminals_str = args - .terminals - .as_deref() - .ok_or_else(|| anyhow::anyhow!("MinimumMultiwayCut requires --terminals (e.g., 0,2,4)"))?; - - let terminals: Vec = terminals_str - .split(',') - .map(|s| { - s.trim() - .parse::() - .map_err(|e| anyhow::anyhow!("Invalid terminal index '{}': {}", s.trim(), e)) - }) - .collect::>>()?; - - anyhow::ensure!( - terminals.len() >= 2, - "at least 2 terminals required, got {}", - terminals.len() - ); - - for &t in &terminals { - anyhow::ensure!( - t < num_vertices, - "terminal index {} out of bounds (graph has {} vertices)", - t, - num_vertices - ); - } - - let mut sorted = terminals.clone(); - sorted.sort(); - sorted.dedup(); - anyhow::ensure!( - sorted.len() == terminals.len(), - "duplicate terminal indices in {:?}", - terminals - ); - - Ok(terminals) -} - /// Parse `--couplings` as SpinGlass pairwise couplings (i32), defaulting to all 1s. fn parse_couplings(args: &CreateArgs, num_edges: usize) -> Result> { match &args.couplings { From cf08d9e5bc7d84eca1468970a720c50086806275 Mon Sep 17 00:00:00 2001 From: zazabap Date: Tue, 17 Mar 2026 07:49:43 +0000 Subject: [PATCH 12/13] fix: remove duplicate terminals field and Vec match arm The branch had a duplicate `terminals` field in CreateArgs (lines 354 and 426) and a duplicate `Vec` match arm in type_format_hint, both caused by merge with main. Removes the earlier duplicates to fix the Code Coverage CI build failure. Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/src/cli.rs | 3 --- problemreductions-cli/src/commands/create.rs | 1 - 2 files changed, 4 deletions(-) diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index ba56a3c08..1bafd29a5 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -349,9 +349,6 @@ pub struct CreateArgs { /// Bits for second factor (for Factoring) #[arg(long)] pub n: Option, - /// Terminal vertices for MinimumMultiwayCut (comma-separated, e.g., 0,2,4) - #[arg(long)] - pub terminals: Option, /// Vertex positions for geometry-based graphs (semicolon-separated x,y pairs, e.g., "0,0;1,0;1,1") #[arg(long)] pub positions: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index dd57fb0ae..18aa50f15 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -234,7 +234,6 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { "Vec>" => "semicolon-separated groups: \"0,1;2,3\"", "usize" => "integer", "u64" => "integer", - "Vec" => "comma-separated integers: 0,0,5", "i64" => "integer", "BigUint" => "nonnegative decimal integer", "Vec" => "comma-separated nonnegative decimal integers: 3,7,1,8", From d4f4a7782c1ad97242eefa3846b99fec44f275a8 Mon Sep 17 00:00:00 2001 From: zazabap Date: Tue, 17 Mar 2026 08:06:16 +0000 Subject: [PATCH 13/13] fix: update pred list output in cli.md to match current state The pred list table was stale (25 types/58 reductions). Updated to current output (50 types/59 reductions/69 variant nodes) with full problem catalog including aliases, rules, and complexity columns. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/src/cli.md | 107 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 76 insertions(+), 31 deletions(-) diff --git a/docs/src/cli.md b/docs/src/cli.md index 24106115e..78f34e46c 100644 --- a/docs/src/cli.md +++ b/docs/src/cli.md @@ -111,37 +111,82 @@ Lists all registered problem types with their short aliases. ```bash $ pred list -Registered problems: 25 types, 58 reductions, 42 variant nodes - - Problem Aliases Variants Reduces to - ───────────────────── ─────────── ──────── ────────── - BMF 1 0 - BicliqueCover 1 0 - BinPacking 2 0 - CircuitSAT 1 2 - ClosestVectorProblem CVP 2 0 - Factoring 1 2 - ILP 1 1 - KColoring 5 3 - KSatisfiability 3SAT, KSAT 3 7 - Knapsack 1 0 - MaxCut 1 1 - MaximalIS 1 0 - MaximumClique 1 1 - MaximumIndependentSet MIS 7 16 - MaximumMatching MaxMatching 1 2 - MaximumSetPacking 3 6 - MinimumDominatingSet 1 1 - MinimumMultiwayCut 1 0 - MinimumSetCovering 1 1 - MinimumVertexCover MVC 1 4 - PaintShop 1 0 - QUBO 1 2 - Satisfiability SAT 1 5 - SpinGlass 2 3 - TravelingSalesman TSP 1 1 - -Use `pred show ` to see variants, reductions, and fields. +Registered problems: 50 types, 59 reductions, 69 variant nodes + + Problem Aliases Rules Complexity + ──────────────────────────────────────────────── ─────────── ───── ────────────────────────────────────────────────────────────────── + BMF * O(2^(cols * rank + rank * rows)) + BicliqueCover * O(2^num_vertices) + BiconnectivityAugmentation/SimpleGraph/i32 * O(2^num_potential_edges) + BinPacking/f64 1 O(2^num_items) + BinPacking/i32 * O(2^num_items) + BoundedComponentSpanningForest/SimpleGraph/i32 * O(3^num_vertices) + CircuitSAT * 2 O(2^num_variables) + ClosestVectorProblem/f64 CVP O(2^num_basis_vectors) + ClosestVectorProblem/i32 * O(2^num_basis_vectors) + DirectedTwoCommodityIntegralFlow * D2CIF O((max_capacity + 1)^(2 * num_arcs)) + ExactCoverBy3Sets * X3C O(2^universe_size) + Factoring * 2 O(exp((m + n)^0.3333333333333333 * log(m + n)^0.6666666666666666)) + FlowShopScheduling * O(factorial(num_jobs)) + GraphPartitioning/SimpleGraph * O(2^num_vertices) + HamiltonianPath/SimpleGraph * O(1.657^num_vertices) + ILP/bool * 2 O(2^num_vars) + ILP/i32 O(num_vars^num_vars) + IsomorphicSpanningTree * O(factorial(num_vertices)) + KColoring/SimpleGraph/KN * 3 O(2^num_vertices) + KColoring/SimpleGraph/K2 O(num_edges + num_vertices) + KColoring/SimpleGraph/K3 O(1.3289^num_vertices) + KColoring/SimpleGraph/K4 O(1.7159^num_vertices) + KColoring/SimpleGraph/K5 O(2^num_vertices) + KSatisfiability/KN * KSAT 6 O(2^num_variables) + KSatisfiability/K2 O(num_clauses + num_variables) + KSatisfiability/K3 O(1.307^num_variables) + Knapsack * 1 O(2^(0.5 * num_items)) + LengthBoundedDisjointPaths/SimpleGraph * O(2^(num_paths_required * num_vertices)) + LongestCommonSubsequence * LCS 1 O(2^min_string_length) + MaxCut/SimpleGraph/i32 * 1 O(2^(0.7906666666666666 * num_vertices)) + MaximalIS/SimpleGraph/i32 * O(3^(0.3333333333333333 * num_vertices)) + MaximumClique/SimpleGraph/i32 * 2 O(1.1996^num_vertices) + MaximumIndependentSet/SimpleGraph/One * MIS 14 O(1.1996^num_vertices) + MaximumIndependentSet/KingsSubgraph/One O(2^sqrt(num_vertices)) + MaximumIndependentSet/SimpleGraph/i32 O(1.1996^num_vertices) + MaximumIndependentSet/UnitDiskGraph/One O(2^sqrt(num_vertices)) + MaximumIndependentSet/KingsSubgraph/i32 O(2^sqrt(num_vertices)) + MaximumIndependentSet/TriangularSubgraph/i32 O(2^sqrt(num_vertices)) + MaximumIndependentSet/UnitDiskGraph/i32 O(2^sqrt(num_vertices)) + MaximumMatching/SimpleGraph/i32 * MaxMatching 2 O(num_vertices^3) + MaximumSetPacking/One * 6 O(2^num_sets) + MaximumSetPacking/f64 O(2^num_sets) + MaximumSetPacking/i32 O(2^num_sets) + MinimumDominatingSet/SimpleGraph/i32 * 1 O(1.4969^num_vertices) + MinimumFeedbackArcSet/i32 * FAS O(2^num_vertices) + MinimumFeedbackVertexSet/i32 * FVS O(1.9977^num_vertices) + MinimumMultiwayCut/SimpleGraph/i32 * O(num_vertices^3 * 1.84^num_terminals) + MinimumSetCovering/i32 * 1 O(2^num_sets) + MinimumSumMulticenter/SimpleGraph/i32 * pmedian O(2^num_vertices) + MinimumTardinessSequencing * O(2^num_tasks) + MinimumVertexCover/SimpleGraph/i32 * MVC 2 O(1.1996^num_vertices) + MultipleChoiceBranching/i32 * O(2^num_arcs) + OptimalLinearArrangement/SimpleGraph * OLA O(2^num_vertices) + PaintShop * O(2^num_cars) + PartitionIntoTriangles/SimpleGraph * O(2^num_vertices) + QUBO/f64 * 2 O(2^num_vars) + RuralPostman/SimpleGraph/i32 * RPP O(num_vertices^2 * 2^num_vertices) + Satisfiability * SAT 5 O(2^num_variables) + SequencingWithinIntervals * O(2^num_tasks) + SetBasis * O(2^(basis_size * universe_size)) + ShortestCommonSupersequence * SCS O(alphabet_size^bound) + SpinGlass/SimpleGraph/f64 3 O(2^num_spins) + SpinGlass/SimpleGraph/i32 * O(2^num_spins) + SteinerTree/SimpleGraph/One O(num_vertices * 3^num_terminals) + SteinerTree/SimpleGraph/i32 * O(num_vertices * 3^num_terminals) + SubgraphIsomorphism * O(num_host_vertices^num_pattern_vertices) + SubsetSum * O(2^(0.5 * num_elements)) + TravelingSalesman/SimpleGraph/i32 * TSP 2 O(2^num_vertices) + UndirectedTwoCommodityIntegralFlow * O(5^num_edges) + +* = default variant +Use `pred show ` to see reductions and fields. ``` ### `pred show` — Inspect a problem